ladybird/Meta/Generators/generate_compositor_webgl_replayer.py

338 lines
15 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import re
import sys
from io import StringIO
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 deref_type
from libweb_webgl import is_const_pointer
from libweb_webgl import is_pointer
from libweb_webgl import is_wire_command
from libweb_webgl import is_wire_sync
from libweb_webgl import method_name
from libweb_webgl import run_generator
from libweb_webgl import snake_case
from libweb_webgl import sync_reply_fields
# Generates the Compositor-side replayer for the WebGL command stream: one
# replay_webgl_command() overload per command. The stream crosses a process boundary, so
# every payload span is asserted against the size the command's own fields imply before
# anything reaches GL; object ids are translated through WebGLObjectMap. GL-level
# validation stays in ANGLE (the host context runs with EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE),
# exactly as it did when WebGL lived in WebContent.
def element_type(pointer_type: str):
base = pointer_type.replace("const", "").replace("*", "").strip()
return None if base in ("void", "GLchar") else base
def rewrite_size_expression(expression: str, function: dict, holder: str = "command") -> str:
# Wire fields are GLsizei/GLint (32-bit) and arrive unvalidated from WebContent. Widen
# each to i64 before it participates in the size product so that an attacker-chosen
# count cannot overflow the multiplication (which is signed-overflow UB, and on a
# wraparound to a small value would let a mismatched payload pass the size check).
for arg_name in sorted((a["name"] for a in function["args"]), key=len, reverse=True):
expression = re.sub(
rf"\b{re.escape(arg_name)}\b", f"static_cast<i64>({holder}.{snake_case(arg_name)})", expression
)
return expression
def emit_payload_resolution(lines: list, function: dict, arg: dict) -> str:
field = snake_case(arg["name"])
lines.append(f" auto {field}_bytes = WebGLCommandList::resolve_data_span(payload, command.{field});")
expression = rewrite_size_expression(arg["payload"], function)
size_check = f"static_cast<i64>({field}_bytes.size()) != static_cast<i64>({expression})"
if arg.get("nullable"):
lines.append(f" VERIFY(!command.has_{field} || !({size_check}));")
lines.append(f" VERIFY(command.has_{field} || {field}_bytes.is_empty());")
else:
lines.append(f" VERIFY(!({size_check}));")
typed = element_type(arg["type"])
if typed:
lines.append(f" auto {field} = WebGLCommandList::resolve_typed_span<{typed}>(payload, command.{field});")
data_expression = f"{field}.data()"
else:
data_expression = f"{field}_bytes.data()"
if arg.get("nullable"):
return f"command.has_{field} ? {data_expression} : nullptr"
return data_expression
def emit_command_body(out: TextIO, function: dict) -> bool:
lines: list = []
call_args: list = []
payload_used = False
deletes = function.get("deletes_objects", False)
for arg in function["args"]:
field = snake_case(arg["name"])
if arg.get("string"):
payload_used = True
lines.append(f" auto {field}_bytes = WebGLCommandList::resolve_string_span(payload, command.{field});")
call_args.append(f"reinterpret_cast<GLchar const*>({field}_bytes.data())")
elif arg.get("offset"):
call_args.append(f"reinterpret_cast<void const*>(static_cast<uintptr_t>(command.{field}))")
elif arg.get("object") and not is_pointer(arg):
if arg["type"] == "GLsync":
lookup = "take_sync" if deletes else "lookup_sync"
lines.append(f" auto {field} = objects.{lookup}(command.{field});")
elif arg.get("zero_means_default"):
# The JSON names the OpenGLContext getter that supplies the host-side
# object for client id 0.
default_getter = arg["zero_means_default"]
assert default_getter in (
"default_framebuffer",
"default_renderbuffer",
), f"unknown zero_means_default getter {default_getter!r} on {function['name']}.{arg['name']}"
lines.append(
f" GLuint {field} = command.{field} ? objects.lookup(command.{field}) : gl.{default_getter}();"
)
else:
lookup = "take" if deletes else "lookup"
lines.append(f" auto {field} = objects.{lookup}(command.{field});")
call_args.append(field)
elif arg.get("object") and is_const_pointer(arg):
payload_used = True
data_expression = emit_payload_resolution(lines, function, arg)
assert data_expression == f"{field}.data()", "object arrays are typed WebGLObjectId spans"
lines.append(f" Vector<GLuint> {field}_names;")
lines.append(f" {field}_names.ensure_capacity({field}.size());")
lines.append(f" for (auto id : {field})")
lines.append(f" {field}_names.unchecked_append(objects.{'take' if deletes else 'lookup'}(id));")
call_args.append(f"{field}_names.data()")
elif "payload" in arg:
payload_used = True
call_args.append(emit_payload_resolution(lines, function, arg))
else:
call_args.append(f"command.{field}")
lines.append(f" gl.{method_name(function)}({', '.join(call_args)});")
out.write("\n".join(lines) + "\n")
return payload_used
def emit_gen_body(out: TextIO, function: dict) -> bool:
if function["return"] != "void":
scalar_args = ", ".join(f"command.{snake_case(a['name'])}" for a in function["args"])
add = "add_sync" if function["return"] == "GLsync" else "add"
out.write(f" TRY(objects.{add}(command.id, gl.{method_name(function)}({scalar_args})));\n")
return False
# glGen*(GLsizei n, GLuint* out) shape: the span carries the client-allocated ids.
count_field = snake_case(function["args"][0]["name"])
span_field = snake_case(function["args"][1]["name"])
out.write(f""" auto ids = WebGLCommandList::resolve_typed_span<WebGLObjectId>(payload, command.{span_field});
VERIFY(static_cast<i64>(ids.size()) == static_cast<i64>(command.{count_field}));
for (auto id : ids) {{
GLuint name = 0;
gl.{method_name(function)}(1, &name);
TRY(objects.add(id, name));
}}
""")
return True
def signature(function: dict, payload_used: bool) -> str:
uses_command = function["category"] == "gen" or function["args"]
uses_objects = function["category"] == "gen" or any(a.get("object") for a in function["args"])
command = "const& command" if uses_command else "const&"
objects = "WebGLObjectMap& objects" if uses_objects else "WebGLObjectMap&"
payload = "ReadonlyBytes payload" if payload_used else "ReadonlyBytes"
return (
f"ErrorOr<void> replay_webgl_command(OpenGLContext& gl, {objects}, "
f"Web::WebGL::Commands::{command_name(function)} {command}, {payload})"
)
def emit_sync_body(out: TextIO, function: dict) -> tuple:
lines: list = []
call_args: list = []
out_blobs: list = [] # field names of Vector-backed reply blobs, in arg order
payload_used = False
objects_used = False
for arg in function["args"]:
field = snake_case(arg["name"])
# An arg-level "host_override" in GLFunctions.json replaces the wire value with a
# host-chosen constant (e.g. a page must never be able to block the compositor).
override = arg.get("host_override")
if override is not None:
call_args.append(override)
elif arg.get("out"):
if "payload" in arg:
element = deref_type(arg["type"])
expression = rewrite_size_expression(arg["payload"], function, "request")
lines.append(f" auto {field}_byte_size = static_cast<size_t>({expression});")
lines.append(f" Vector<{element}> {field};")
lines.append(f" {field}.resize({field}_byte_size / sizeof({element}));")
call_args.append(f"{field}.data()")
out_blobs.append((field, element))
else:
lines.append(f" {deref_type(arg['type'])} {field} {{}};")
call_args.append(f"&{field}")
elif arg.get("object") and not is_pointer(arg):
objects_used = True
lookup = "lookup_sync" if arg["type"] == "GLsync" else "lookup"
lines.append(f" auto {field} = objects.{lookup}(request.{field});")
call_args.append(field)
elif arg.get("string"):
payload_used = True
lines.append(f" auto {field}_bytes = WebGLCommandList::resolve_string_span(payload, request.{field});")
call_args.append(f"reinterpret_cast<GLchar const*>({field}_bytes.data())")
elif "payload" in arg:
payload_used = True
element = deref_type(arg["type"].replace("const", "").strip())
expression = rewrite_size_expression(arg["payload"], function, "request")
lines.append(
f" auto {field} = WebGLCommandList::resolve_typed_span<{element}>(payload, request.{field});"
)
lines.append(
f" VERIFY(static_cast<i64>({field}.size() * sizeof({element})) == static_cast<i64>({expression}));"
)
call_args.append(f"{field}.data()")
else:
call_args.append(f"request.{field}")
invocation = f"gl.{method_name(function)}({', '.join(call_args)});"
if function["return"] != "void":
invocation = "auto return_value = " + invocation
lines.append(f" {invocation}")
# Assemble the reply: span fields point at the Vector blobs, laid out in order.
reply_type = f"SyncCalls::{command_name(function)}::Reply"
blob_spans = []
for index, (field, element) in enumerate(out_blobs):
if index == 0:
offset = f"WebGLCommandList::first_inline_data_offset(sizeof({reply_type}))"
else:
offset = f"WebGLCommandList::next_inline_data_offset({out_blobs[index - 1][0]}_span)"
lines.append(
f" WebGLDataSpan {field}_span {{ {offset}, static_cast<u32>({field}.size() * sizeof({element})) }};"
)
blob_spans.append(field)
initializers = []
for _, field_name, arg in sync_reply_fields(function):
if arg is not None and "payload" in arg:
initializers.append(f".{field_name} = {field_name}_span")
else:
initializers.append(f".{field_name} = {field_name}")
lines.append(f" {reply_type} reply {{ {', '.join(initializers)} }};")
blob_arguments = "".join(
f", ReadonlyBytes {{ {field}.data(), {field}.size() * sizeof({element}) }}" for field, element in out_blobs
)
lines.append(f" return WebGLSyncCall::encode_reply(reply{blob_arguments});")
out.write("\n".join(lines) + "\n")
return payload_used, objects_used
def sync_signature(function: dict, payload_used: bool, objects_used: bool) -> str:
name = command_name(function)
request = "const& request" if function["args"] else "const&"
objects = "WebGLObjectMap& objects" if objects_used else "WebGLObjectMap&"
payload = "ReadonlyBytes payload" if payload_used else "ReadonlyBytes"
return (
f"static ByteBuffer handle_one(OpenGLContext& gl, {objects}, SyncCalls::{name}::Request {request}, {payload})"
)
def write_header_file(out: TextIO, functions: list) -> None:
out.write("""#pragma once
#include <AK/Error.h>
#include <Compositor/WebGLObjectMap.h>
#include <Compositor/OpenGLContext.h>
#include <LibWeb/WebGL/WebGLCommandList.h>
namespace Compositor {
""")
for function in functions:
if function["category"] not in ("command", "gen"):
continue
out.write(
f"ErrorOr<void> replay_webgl_command(OpenGLContext&, WebGLObjectMap&, "
f"Web::WebGL::Commands::{command_name(function)} const&, ReadonlyBytes);\n"
)
out.write("""
// Wire-specified ops; defined manually in HostWebGLContext.cpp. Builtin commands carry
// host-level semantics (presenting, resizing) and are dispatched by the host itself
// rather than through replay_webgl_command.
""")
for function in functions:
if function["category"] == "custom" and is_wire_command(function):
out.write(
f"ErrorOr<void> replay_webgl_command(OpenGLContext&, WebGLObjectMap&, "
f"Web::WebGL::Commands::{command_name(function)} const&, ReadonlyBytes);\n"
)
for function in functions:
if is_wire_sync(function):
out.write(
f"ErrorOr<ByteBuffer> handle_one(OpenGLContext&, WebGLObjectMap&, "
f"Web::WebGL::SyncCalls::{command_name(function)}::Request const&, ReadonlyBytes);\n"
)
out.write("""
ErrorOr<ByteBuffer> handle_webgl_sync_call(OpenGLContext&, WebGLObjectMap&, ReadonlyBytes request);
}
""")
def write_implementation_file(out: TextIO, functions: list) -> None:
out.write("""#include <AK/Assertions.h>
#include <AK/Vector.h>
#include <Compositor/WebGLCommandReplayer.h>
namespace Compositor {
using namespace Web::WebGL;
""")
for function in functions:
if function["category"] not in ("command", "gen"):
continue
body = StringIO()
if function["category"] == "gen":
payload_used = emit_gen_body(body, function)
else:
payload_used = emit_command_body(body, function)
out.write(f"{signature(function, payload_used)}\n{{\n")
out.write(body.getvalue())
out.write(" return {};\n}\n\n")
for function in functions:
if function["category"] != "sync":
continue
body = StringIO()
payload_used, objects_used = emit_sync_body(body, function)
out.write(f"{sync_signature(function, payload_used, objects_used)}\n{{\n")
out.write(body.getvalue())
out.write("}\n\n")
out.write("""ErrorOr<ByteBuffer> handle_webgl_sync_call(OpenGLContext& gl, WebGLObjectMap& objects, ReadonlyBytes request)
{
return WebGLSyncCall::dispatch_request(request, [&]<typename Call>(typename Call::Request const& call_request, ReadonlyBytes payload) -> ErrorOr<ByteBuffer> {
return handle_one(gl, objects, call_request, payload);
});
}
}
""")
if __name__ == "__main__":
run_generator("Generate the Compositor WebGL command replayer", write_header_file, write_implementation_file)