2026-06-10 17:43:02 -03:00
|
|
|
# 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
|
2026-06-15 14:27:13 -03:00
|
|
|
# derives the command-struct shape each annotated GL function serializes to.
|
2026-06-10 17:43:02 -03:00
|
|
|
|
|
|
|
|
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:
|
2026-06-15 14:27:13 -03:00
|
|
|
if function["category"].startswith("builtin"):
|
|
|
|
|
return function["name"]
|
2026-06-10 17:43:02 -03:00
|
|
|
assert function["name"].startswith("gl")
|
|
|
|
|
return function["name"][2:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def method_name(function: dict) -> str:
|
|
|
|
|
return snake_case(command_name(function))
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 14:27:13 -03:00
|
|
|
# The GLFunctions and WebGLContextProxy classes must stay signature-identical (the WebGL
|
|
|
|
|
# implementation cannot tell them apart), so both generators emit from this one helper.
|
2026-06-10 17:43:02 -03:00
|
|
|
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})"
|
2026-06-15 14:27:13 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|