ladybird/Meta/Generators/generate_libweb_webgl_proxy.py

228 lines
9 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
# Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import sys
from pathlib import Path
from typing import TextIO
sys.path.append(str(Path(__file__).resolve().parent))
from libweb_webgl import command_name
from libweb_webgl import is_const_pointer
from libweb_webgl import is_pointer
from libweb_webgl import method_signature
from libweb_webgl import run_generator
from libweb_webgl import snake_case
from libweb_webgl import sync_reply_fields
# Generates WebGLContextProxy, WebContent's drop-in replacement for the GL seam: the
# same method signatures as the generated GLFunctions class, but recording commands into
# a WebGLCommandList (flushed to the Compositor) instead of calling GL. Value-returning
# entry points become synchronous round trips. Method parameter names match the GL
# argument names from GLFunctions.json, so the JSON's payload-size expressions are used
# verbatim.
def object_id_expression(arg: dict) -> str:
if arg["type"] == "GLsync":
return f"static_cast<WebGLObjectId>(reinterpret_cast<uintptr_t>({arg['name']}))"
return arg["name"]
# Emits statements declaring `<field>_bytes` spans and a fully-initialized `command`
# whose WebGLDataSpan fields point at them in append order.
def emit_command_construction(out: TextIO, function: dict, command_type: str) -> list:
blobs = [] # (field, bytes_variable)
initializers = []
for arg in function["args"]:
field = snake_case(arg["name"])
name = arg["name"]
if arg.get("string"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, __builtin_strlen({name}) + 1 }};\n")
blobs.append(field)
initializers.append((field, None))
elif arg.get("offset"):
initializers.append((field, f"static_cast<GLintptr>(reinterpret_cast<uintptr_t>({name}))"))
elif arg.get("object") and not is_pointer(arg):
initializers.append((field, object_id_expression(arg)))
elif (arg.get("object") and is_const_pointer(arg)) or "payload" in arg:
size = f"static_cast<size_t>({arg['payload']})"
if arg.get("nullable"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, {name} ? {size} : 0 }};\n")
initializers.append((f"has_{field}", f"{name} != nullptr"))
else:
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, {size} }};\n")
blobs.append(field)
initializers.append((field, None))
else:
initializers.append((field, name))
fields = ", ".join(f".{field} = {value}" for field, value in initializers if value is not None)
out.write(f" {command_type} command {{ {fields} }};\n")
for index, field in enumerate(blobs):
if index == 0:
offset = "WebGLCommandList::first_inline_data_offset(sizeof(command))"
else:
offset = f"WebGLCommandList::next_inline_data_offset(command.{blobs[index - 1]})"
out.write(f" command.{field} = {{ {offset}, static_cast<u32>({field}_bytes.size()) }};\n")
return blobs
def emit_record(out: TextIO, blobs: list) -> None:
blob_args = "".join(f", {field}_bytes" for field in blobs)
out.write(f" record(command{blob_args});\n")
def emit_command_method(out: TextIO, function: dict) -> None:
command_type = f"Commands::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
blobs = emit_command_construction(out, function, command_type)
assert len(blobs) <= 1, f"{function['name']} needs more inline blobs than record() supports"
emit_record(out, blobs)
out.write("}\n\n")
def emit_gen_method(out: TextIO, function: dict) -> None:
command_type = f"Commands::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
if function["return"] != "void":
scalars = "".join(f", .{snake_case(a['name'])} = {a['name']}" for a in function["args"])
out.write(" auto id = allocate_object_id();\n")
out.write(f" record({command_type} {{ .id = id{scalars} }});\n")
if function["return"] == "GLsync":
out.write(" return reinterpret_cast<GLsync>(static_cast<uintptr_t>(id));\n")
else:
out.write(" return id;\n")
out.write("}\n\n")
return
count_name = function["args"][0]["name"]
span_field = snake_case(function["args"][1]["name"])
out_name = function["args"][1]["name"]
out.write(f""" if ({count_name} <= 0)
return;
static_assert(IsSame<WebGLObjectId, GLuint>);
for (GLsizei i = 0; i < {count_name}; ++i)
{out_name}[i] = allocate_object_id();
ReadonlyBytes {span_field}_bytes {{ {out_name}, static_cast<size_t>({count_name}) * sizeof(WebGLObjectId) }};
Commands::{command_name(function)} command {{ .{snake_case(count_name)} = {count_name} }};
command.{span_field} = {{ WebGLCommandList::first_inline_data_offset(sizeof(command)), static_cast<u32>({span_field}_bytes.size()) }};
record(command, {span_field}_bytes);
}}
""")
def emit_sync_method(out: TextIO, function: dict) -> None:
call = f"SyncCalls::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
# Build the request (in-args only) plus its inline blobs.
blobs = []
initializers = []
for arg in function["args"]:
if arg.get("out"):
continue
field = snake_case(arg["name"])
name = arg["name"]
if arg.get("string"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, __builtin_strlen({name}) + 1 }};\n")
blobs.append(field)
elif "payload" in arg:
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, static_cast<size_t>({arg['payload']}) }};\n")
blobs.append(field)
elif arg.get("object") and not is_pointer(arg):
initializers.append((field, object_id_expression(arg)))
else:
initializers.append((field, name))
assert len(blobs) <= 1, f"{function['name']} needs more request blobs than encode_request supports"
fields = ", ".join(f".{field} = {value}" for field, value in initializers)
out.write(f" {call}::Request request {{ {fields} }};\n")
for field in blobs:
out.write(
f" request.{field} = {{ WebGLCommandList::first_inline_data_offset(sizeof(request)), static_cast<u32>({field}_bytes.size()) }};\n"
)
blob_argument = f", {blobs[0]}_bytes" if blobs else ""
out.write(
f" auto reply_bytes = send_sync_call(WebGLSyncCall::encode_request<{call}>(request{blob_argument}));\n"
)
has_return = function["return"] != "void"
failure = " return {};\n" if has_return else " return;\n"
out.write(" if (is_lost())\n")
out.write(failure)
if sync_reply_fields(function):
out.write(f" auto reply = WebGLSyncCall::decode_reply<{call}::Reply>(reply_bytes);\n")
for _, field_name, arg in sync_reply_fields(function):
if arg is None:
continue
name = arg["name"]
if "payload" in arg:
out.write(f""" if ({name})
WebGLCommandList::copy_data_span(reply_bytes, reply.{field_name}, {{ {name}, static_cast<size_t>({arg["payload"]}) }});
""")
else:
out.write(f" if ({name})\n *{name} = reply.{field_name};\n")
if has_return:
out.write(" return reply.return_value;\n")
out.write("}\n\n")
def write_header_file(out: TextIO, functions: list) -> None:
out.write("""#pragma once
#include <LibWeb/WebGL/WebGLCommands.h>
#include <LibWeb/WebGL/WebGLContextProxyBase.h>
namespace Web::WebGL {
// The remote-recording implementation of the GL seam: identical signatures to
// GLFunctions, so the WebGL implementation files cannot tell the difference.
class WEB_API WebGLContextProxy final : public WebGLContextProxyBase {
public:
using WebGLContextProxyBase::WebGLContextProxyBase;
""")
for function in functions:
if function["category"] not in ("command", "gen", "sync"):
continue
out.write(f" {method_signature(function)};\n")
out.write("""
// Custom-handled entry points; defined manually in WebGLContextProxyBase.cpp.
""")
for function in functions:
if function["category"] != "custom":
continue
out.write(f" {method_signature(function)};\n")
out.write("""};
}
""")
def write_implementation_file(out: TextIO, functions: list) -> None:
out.write("""#include <LibWeb/WebGL/WebGLContextProxy.h>
namespace Web::WebGL {
""")
for function in functions:
if function["category"] == "command":
emit_command_method(out, function)
elif function["category"] == "gen":
emit_gen_method(out, function)
elif function["category"] == "sync":
emit_sync_method(out, function)
out.write("}\n")
if __name__ == "__main__":
run_generator("Generate the WebGL context proxy recorder", write_header_file, write_implementation_file)