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.
154 lines
6.1 KiB
Python
154 lines
6.1 KiB
Python
# Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
#
|
|
# SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
# Shared model and CLI driver for the WebGL generators: loads GLFunctions.json and
|
|
# derives the command-struct shape each annotated GL function serializes to.
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
from pathlib import Path
|
|
|
|
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
|
|
|
from Utils.utils import title_case_to_snake_case as snake_case
|
|
|
|
|
|
def load_functions(path: str) -> list:
|
|
with open(path, "r", encoding="utf-8") as input_file:
|
|
return json.load(input_file)
|
|
|
|
|
|
# All WebGL generators take the same -h/-c/-j arguments and emit one header and one
|
|
# implementation file from the loaded function list.
|
|
def run_generator(description: str, write_header_file, write_implementation_file) -> None:
|
|
parser = argparse.ArgumentParser(description=description, add_help=False)
|
|
parser.add_argument("--help", action="help", help="Show this help message and exit")
|
|
parser.add_argument("-h", "--header", required=True, help="Path to the header file to generate")
|
|
parser.add_argument("-c", "--implementation", required=True, help="Path to the implementation file to generate")
|
|
parser.add_argument("-j", "--json", required=True, help="Path to the JSON file to read from")
|
|
args = parser.parse_args()
|
|
|
|
functions = load_functions(args.json)
|
|
|
|
with open(args.header, "w", encoding="utf-8") as output_file:
|
|
write_header_file(output_file, functions)
|
|
|
|
with open(args.implementation, "w", encoding="utf-8") as output_file:
|
|
write_implementation_file(output_file, functions)
|
|
|
|
|
|
def command_name(function: dict) -> str:
|
|
if function["category"].startswith("builtin"):
|
|
return function["name"]
|
|
assert function["name"].startswith("gl")
|
|
return function["name"][2:]
|
|
|
|
|
|
def method_name(function: dict) -> str:
|
|
return snake_case(command_name(function))
|
|
|
|
|
|
# The GLFunctions and WebGLContextProxy classes must stay signature-identical (the WebGL
|
|
# implementation cannot tell them apart), so both generators emit from this one helper.
|
|
def method_signature(function: dict, qualifier: str = "") -> str:
|
|
args = ", ".join(f"{arg['type']} {arg['name']}" for arg in function["args"])
|
|
return f"{function['return']} {qualifier}{method_name(function)}({args})"
|
|
|
|
|
|
# Entries carried by the command stream: regular commands and object creation (struct
|
|
# shapes derived from the GL signature) plus wire-specified ops (custom-handled GL
|
|
# functions and builtins, whose struct shapes are spelled out in the JSON).
|
|
def is_wire_command(function: dict) -> bool:
|
|
return "wire_command" in function
|
|
|
|
|
|
def is_wire_sync(function: dict) -> bool:
|
|
return "wire_request" in function
|
|
|
|
|
|
def command_stream_entries(functions: list) -> list:
|
|
return [f for f in functions if f["category"] in ("command", "gen") or is_wire_command(f)]
|
|
|
|
|
|
def sync_call_entries(functions: list) -> list:
|
|
return [f for f in functions if f["category"] == "sync" or is_wire_sync(f)]
|
|
|
|
|
|
def is_pointer(arg: dict) -> bool:
|
|
return arg["type"].endswith("*")
|
|
|
|
|
|
def is_const_pointer(arg: dict) -> bool:
|
|
return is_pointer(arg) and "const" in arg["type"]
|
|
|
|
|
|
def deref_type(pointer_type: str) -> str:
|
|
return pointer_type.replace("*", "").strip()
|
|
|
|
|
|
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of a synchronous
|
|
# call's request struct: every non-out argument, with object ids, strings, and input
|
|
# payloads in their wire representations.
|
|
def sync_request_fields(function: dict) -> list:
|
|
fields = []
|
|
for arg in function["args"]:
|
|
if arg.get("out"):
|
|
continue
|
|
field_name = snake_case(arg["name"])
|
|
if arg.get("object") and not is_pointer(arg):
|
|
fields.append(("WebGLObjectId", field_name, arg))
|
|
elif arg.get("string") or "payload" in arg:
|
|
fields.append(("WebGLDataSpan", field_name, arg))
|
|
else:
|
|
assert not is_pointer(arg), f"unhandled pointer arg {function['name']}.{arg['name']}"
|
|
fields.append((arg["type"], field_name, arg))
|
|
return fields
|
|
|
|
|
|
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of a synchronous
|
|
# call's reply struct: the return value, scalar outs by value, buffer outs as spans
|
|
# into the reply's inline data.
|
|
def sync_reply_fields(function: dict) -> list:
|
|
fields = []
|
|
if function["return"] != "void":
|
|
fields.append((function["return"], "return_value", None))
|
|
for arg in function["args"]:
|
|
if not arg.get("out"):
|
|
continue
|
|
field_name = snake_case(arg["name"])
|
|
if "payload" in arg:
|
|
fields.append(("WebGLDataSpan", field_name, arg))
|
|
else:
|
|
fields.append((deref_type(arg["type"]), field_name, arg))
|
|
return fields
|
|
|
|
|
|
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of the
|
|
# trivially-copyable struct a command/gen function serializes to.
|
|
def command_struct_fields(function: dict) -> list:
|
|
fields = []
|
|
if function["category"] == "gen" and function["return"] != "void":
|
|
fields.append(("WebGLObjectId", "id", None))
|
|
for arg in function["args"]:
|
|
field_name = snake_case(arg["name"])
|
|
if function["category"] == "gen" and is_pointer(arg) and not is_const_pointer(arg):
|
|
fields.append(("WebGLDataSpan", field_name, arg)) # client-allocated ids
|
|
elif arg.get("object") and not is_pointer(arg):
|
|
fields.append(("WebGLObjectId", field_name, arg))
|
|
elif arg.get("object") and is_const_pointer(arg):
|
|
fields.append(("WebGLDataSpan", field_name, arg)) # array of client ids
|
|
elif arg.get("string"):
|
|
fields.append(("WebGLDataSpan", field_name, arg)) # NUL-terminated bytes
|
|
elif arg.get("offset"):
|
|
fields.append(("GLintptr", field_name, arg))
|
|
elif "payload" in arg:
|
|
if arg.get("nullable"):
|
|
fields.append(("bool", "has_" + field_name, arg))
|
|
fields.append(("WebGLDataSpan", field_name, arg))
|
|
else:
|
|
assert not is_pointer(arg), f"unhandled pointer arg {function['name']}.{arg['name']}"
|
|
fields.append((arg["type"], field_name, arg))
|
|
return fields
|