Moving WebGL execution into the Compositor needs a serializable command stream and a client-side proxy that can queue commands before sending them over IPC. The existing generator metadata only described direct GL wrappers, so generated code could not distinguish async commands from sync calls or object factory methods. This teaches the WebGL metadata and generators about command streams and adds the unused LibWeb proxy/list types. No rendering behavior changes yet; the later host wiring can build on these generated interfaces without mixing the metadata churn into that commit.
227 lines
9 KiB
Python
227 lines
9 KiB
Python
#!/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)
|