From 5f0e95de1378897fc0b85cbf201990c5ea233894 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Mon, 15 Jun 2026 19:29:43 +0200 Subject: [PATCH] Meta+LibWeb+Compositor: Add remote canvas transports The display list can now refer to canvas ids, but WebContent still had no channel for creating or updating those canvas resources in the Compositor. Both 2D and WebGL canvases would have had to grow the IPC plumbing in the same commit that changes the rendering contexts. This adds the Compositor-side CanvasHost, WebContent transport objects, and the IPC/CMake pieces needed to allocate, update, read back, and destroy remote canvas contexts. The rendering contexts are not switched over yet, keeping this as plumbing for later commits. --- Libraries/LibWeb/Compositor/CompositorHost.h | 3 + Libraries/LibWeb/Forward.h | 10 + .../HTML/Canvas/RemoteCanvas2DTransport.h | 30 ++ Libraries/LibWeb/WebGL/OpenGLContext.h | 1 + .../generate_compositor_webgl_replayer.py | 339 +++++++++++++++++ Services/Compositor/CMakeLists.txt | 16 +- Services/Compositor/CanvasHost.cpp | 183 +++++++++ Services/Compositor/CanvasHost.h | 77 ++++ .../Compositor/CompositorWebContentServer.ipc | 17 + .../Compositor/ConnectionFromWebContent.cpp | 66 ++++ .../Compositor/ConnectionFromWebContent.h | 17 + Services/Compositor/HostWebGLContext.cpp | 351 ++++++++++++++++++ Services/Compositor/HostWebGLContext.h | 57 +++ Services/Compositor/WebGLObjectMap.cpp | 64 ++++ Services/Compositor/WebGLObjectMap.h | 31 ++ Services/WebContent/CompositorConnection.cpp | 97 +++++ Services/WebContent/CompositorConnection.h | 13 + .../WebContent/WebContentCompositorHost.cpp | 109 ++++++ 18 files changed, 1480 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h create mode 100644 Meta/Generators/generate_compositor_webgl_replayer.py create mode 100644 Services/Compositor/CanvasHost.cpp create mode 100644 Services/Compositor/CanvasHost.h create mode 100644 Services/Compositor/HostWebGLContext.cpp create mode 100644 Services/Compositor/HostWebGLContext.h create mode 100644 Services/Compositor/WebGLObjectMap.cpp create mode 100644 Services/Compositor/WebGLObjectMap.h diff --git a/Libraries/LibWeb/Compositor/CompositorHost.h b/Libraries/LibWeb/Compositor/CompositorHost.h index 91d5635c13..44221e67e9 100644 --- a/Libraries/LibWeb/Compositor/CompositorHost.h +++ b/Libraries/LibWeb/Compositor/CompositorHost.h @@ -69,6 +69,9 @@ public: OwnPtr create_context(CompositorContextId); + virtual RefPtr create_webgl_transport() = 0; + virtual RefPtr create_canvas_2d_transport() = 0; + virtual void destroy_context(CompositorContextId) = 0; virtual void set_presentation_mode(CompositorContextId, PresentationMode) = 0; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 9cf2487ed8..5c27a7c761 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -41,6 +41,13 @@ AK_TYPEDEF_DISTINCT_NUMERIC_GENERAL(i64, UniqueNodeID, Comparison, Increment, Ca } +namespace Web::Compositor { + +class CompositorContextHandle; +class CompositorHost; + +} + namespace Web::Painting { class AccumulatedVisualContextTree; @@ -692,6 +699,7 @@ class BroadcastChannel; class BrowsingContext; class BrowsingContextGroup; class CanvasRenderingContext2D; +class RemoteCanvas2DTransport; class ClassicScript; class CloseEvent; class CloseWatcher; @@ -1304,6 +1312,8 @@ enum class AudioContextState; namespace Web::WebGL { class OpenGLContext; +class RemoteWebGLTransport; +class WebGLContextProxy; class WebGL2RenderingContext; class WebGLActiveInfo; class WebGLBuffer; diff --git a/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h b/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h new file mode 100644 index 0000000000..e5bc3a2156 --- /dev/null +++ b/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +class WEB_API RemoteCanvas2DTransport : public RefCounted { +public: + virtual ~RemoteCanvas2DTransport() = default; + + virtual Optional create_context(Gfx::IntSize, bool alpha) = 0; + virtual void destroy_context(Painting::CanvasId) = 0; + virtual void update_commands(Painting::CanvasId, Gfx::CanvasCommandList const&) = 0; + + virtual RefPtr read_back_pixels(Painting::CanvasId, Gfx::IntRect const&) = 0; +}; + +} diff --git a/Libraries/LibWeb/WebGL/OpenGLContext.h b/Libraries/LibWeb/WebGL/OpenGLContext.h index 151e1690f9..3a5a71e329 100644 --- a/Libraries/LibWeb/WebGL/OpenGLContext.h +++ b/Libraries/LibWeb/WebGL/OpenGLContext.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include diff --git a/Meta/Generators/generate_compositor_webgl_replayer.py b/Meta/Generators/generate_compositor_webgl_replayer.py new file mode 100644 index 0000000000..5c1dda2150 --- /dev/null +++ b/Meta/Generators/generate_compositor_webgl_replayer.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026, Aliaksandr Kalenik +# +# 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({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({field}_bytes.size()) != static_cast({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({field}_bytes.data())") + elif arg.get("offset"): + call_args.append(f"reinterpret_cast(static_cast(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 {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(payload, command.{span_field}); + VERIFY(static_cast(ids.size()) == static_cast(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 replay_webgl_command(Web::WebGL::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({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({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({field}.size() * sizeof({element})) == static_cast({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({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(Web::WebGL::OpenGLContext& gl, {objects}, " + f"SyncCalls::{name}::Request {request}, {payload})" + ) + + +def write_header_file(out: TextIO, functions: list) -> None: + out.write("""#pragma once + +#include +#include +#include +#include + +namespace Compositor { +""") + for function in functions: + if function["category"] not in ("command", "gen"): + continue + out.write( + f"ErrorOr replay_webgl_command(Web::WebGL::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 replay_webgl_command(Web::WebGL::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 handle_one(Web::WebGL::OpenGLContext&, WebGLObjectMap&, " + f"Web::WebGL::SyncCalls::{command_name(function)}::Request const&, ReadonlyBytes);\n" + ) + out.write(""" +ErrorOr handle_webgl_sync_call(Web::WebGL::OpenGLContext&, WebGLObjectMap&, ReadonlyBytes request); + +} +""") + + +def write_implementation_file(out: TextIO, functions: list) -> None: + out.write("""#include +#include +#include + +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 handle_webgl_sync_call(Web::WebGL::OpenGLContext& gl, WebGLObjectMap& objects, ReadonlyBytes request) +{ + return WebGLSyncCall::dispatch_request(request, [&](typename Call::Request const& call_request, ReadonlyBytes payload) -> ErrorOr { + 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) diff --git a/Services/Compositor/CMakeLists.txt b/Services/Compositor/CMakeLists.txt index 837635ceef..73313c2fdc 100644 --- a/Services/Compositor/CMakeLists.txt +++ b/Services/Compositor/CMakeLists.txt @@ -1,11 +1,24 @@ set(SOURCES BackingStoreManager.cpp + CanvasHost.cpp CompositorState.cpp ContextState.cpp ConnectionFromClient.cpp ConnectionFromWebContent.cpp + HostWebGLContext.cpp VSyncScheduler.cpp ViewportScrollbarController.cpp + WebGLObjectMap.cpp +) + +invoke_py_generator( + "WebGLCommandReplayer.cpp" + "generate_compositor_webgl_replayer.py" + "${LADYBIRD_SOURCE_DIR}/Libraries/LibWeb/WebGL/GLFunctions.json" + "WebGLCommandReplayer.h" + "WebGLCommandReplayer.cpp" + arguments -j "${LADYBIRD_SOURCE_DIR}/Libraries/LibWeb/WebGL/GLFunctions.json" + dependencies "${LADYBIRD_SOURCE_DIR}/Meta/Generators/libweb_webgl.py" ) set(GENERATED_SOURCES @@ -13,6 +26,7 @@ set(GENERATED_SOURCES CompositorControlServerEndpoint.h CompositorWebContentClientEndpoint.h CompositorWebContentServerEndpoint.h + WebGLCommandReplayer.cpp ) add_library(compositorservice STATIC ${SOURCES} ${GENERATED_SOURCES}) @@ -32,7 +46,7 @@ target_include_directories(compositorservice PRIVATE ${CMAKE_CURRENT_BINARY_DIR} target_include_directories(compositorservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/) target_link_libraries(Compositor PRIVATE compositorservice LibCore LibMain LibSandbox LibWebView) -target_link_libraries(compositorservice PRIVATE LibCore LibGfx LibIPC LibMedia LibSync LibWeb) +target_link_libraries(compositorservice PRIVATE LibCore LibGfx LibIPC LibMedia LibSync LibWeb ${ANGLE_TARGETS}) if (APPLE) target_link_libraries(Compositor PRIVATE "-framework CoreGraphics" "-framework CoreVideo") diff --git a/Services/Compositor/CanvasHost.cpp b/Services/Compositor/CanvasHost.cpp new file mode 100644 index 0000000000..b1822733a0 --- /dev/null +++ b/Services/Compositor/CanvasHost.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace Compositor { + +CanvasHost::CanvasHost(RefPtr skia_backend_context, Web::Painting::CanvasSurfaceRegistry& canvas_surface_registry) + : m_skia_backend_context(move(skia_backend_context)) + , m_canvas_surface_registry(canvas_surface_registry) +{ +} + +CanvasHost::~CanvasHost() +{ + for (auto canvas_id : m_contexts.keys()) + m_canvas_surface_registry.remove_canvas_surface(canvas_id); +} + +OwnPtr CanvasHost::create_2d_command_player(Gfx::IntSize size, bool alpha) +{ + if (size.is_empty() || static_cast(size.width()) * static_cast(size.height()) > Gfx::max_canvas_area) + return nullptr; + + auto format = alpha ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888; + auto player = make(m_skia_backend_context, size, format, Gfx::AlphaType::Premultiplied); + + // https://html.spec.whatwg.org/multipage/canvas.html#the-canvas-settings:concept-canvas-alpha + // "Thus, the bitmap of such a context starts off as opaque black instead of transparent black" + // AD-HOC: Skia hands out a fully transparent surface by default; only clear when alpha is disabled. + if (!alpha) + player->clear(Gfx::Color::Black); + + return player; +} + +Gfx::CanvasCommandPlayer& CanvasHost::as_2d(Context& context) +{ + auto* player = context.get_pointer(); + VERIFY(player); + return **player; +} + +HostWebGLContext& CanvasHost::as_webgl(Context& context) +{ + auto* webgl_context = context.get_pointer(); + VERIFY(webgl_context); + return **webgl_context; +} + +Optional CanvasHost::create_2d_context(Gfx::IntSize size, bool alpha) +{ + auto context = create_2d_command_player(size, alpha); + if (!context) + return {}; + + auto canvas_id = m_canvas_surface_registry.create_canvas_surface(context->surface()); + m_contexts.set(canvas_id, context.release_nonnull()); + return canvas_id; +} + +CanvasHost::CreateWebGLContextResult CanvasHost::create_webgl_context(Web::WebGL::WebGLVersion version, Gfx::IntSize size, bool depth, bool stencil, bool antialias) +{ + if (!m_skia_backend_context) + return {}; + + auto context = HostWebGLContext::create(*m_skia_backend_context, version, { .depth = depth, .stencil = stencil, .antialias = antialias }, size); + if (!context) + return {}; + + auto canvas_id = m_canvas_surface_registry.allocate_canvas_id(); + auto supported_extensions = context->gl_context().get_supported_opengl_extensions(); + m_contexts.set(canvas_id, context.release_nonnull()); + return { .success = true, .canvas_id = canvas_id, .supported_extensions = move(supported_extensions) }; +} + +void CanvasHost::destroy_context(Web::Painting::CanvasId canvas_id) +{ + m_contexts.remove(canvas_id); + m_canvas_surface_registry.remove_canvas_surface(canvas_id); +} + +bool CanvasHost::has_context(Web::Painting::CanvasId canvas_id) const +{ + return m_contexts.contains(canvas_id); +} + +CanvasHost::Context* CanvasHost::context(Web::Painting::CanvasId canvas_id) +{ + auto it = m_contexts.find(canvas_id); + if (it == m_contexts.end()) + return nullptr; + return &it->value; +} + +void CanvasHost::execute_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + as_2d(*context).play(commands); +} + +void CanvasHost::execute_webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer const& commands, Vector const& bitmaps) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + auto& webgl_context = as_webgl(*context); + MUST(webgl_context.execute_commands(commands, bitmaps)); + if (auto surface = webgl_context.surface()) + m_canvas_surface_registry.set_canvas_surface(canvas_id, surface.release_nonnull()); +} + +ErrorOr CanvasHost::execute_webgl_sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + return as_webgl(*context).execute_sync_call(request); +} + +Web::WebGL::ReadPixelsResult CanvasHost::webgl_read_pixels_robust_angle(Web::Painting::CanvasId canvas_id, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer pixels) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + return as_webgl(*context).read_pixels_robust_angle(x, y, width, height, format, type, buf_size, move(pixels)); +} + +void CanvasHost::webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer data) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + as_webgl(*context).read_buffer_sub_data(target, offset, size, move(data)); +} + +void CanvasHost::present_webgl_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) +{ + auto* context = this->context(canvas_id); + VERIFY(context); + + auto surface = MUST(as_webgl(*context).prepare_for_compositing(preserve_drawing_buffer)); + m_canvas_surface_registry.set_canvas_surface(canvas_id, move(surface)); +} + +static Gfx::ShareableBitmap read_back_surface(Gfx::PaintingSurface& surface, Gfx::IntRect rect) +{ + auto clipped_rect = rect.intersected(surface.rect()); + if (clipped_rect.is_empty()) + return {}; + + auto bitmap_or_error = Gfx::Bitmap::create_shareable(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, clipped_rect.size()); + if (bitmap_or_error.is_error()) + return {}; + + auto bitmap = bitmap_or_error.release_value(); + surface.flush(); + surface.read_into_bitmap(*bitmap, clipped_rect.location()); + return Gfx::ShareableBitmap { move(bitmap), Gfx::ShareableBitmap::ConstructWithKnownGoodBitmap }; +} + +Gfx::ShareableBitmap CanvasHost::read_back_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect rect) +{ + auto* context = this->context(canvas_id); + if (!context) + return {}; + + return context->visit( + [rect](Canvas2DContext& player) { + return read_back_surface(player->surface(), rect); + }, + [rect](WebGLContext& webgl_context) { + return webgl_context->read_back_drawing_buffer(rect); + }); +} + +} diff --git a/Services/Compositor/CanvasHost.h b/Services/Compositor/CanvasHost.h new file mode 100644 index 0000000000..f9a277c49c --- /dev/null +++ b/Services/Compositor/CanvasHost.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Painting { + +class CanvasSurfaceRegistry; + +} + +namespace Compositor { + +class HostWebGLContext; + +class CanvasHost { +public: + struct CreateWebGLContextResult { + bool success { false }; + Web::Painting::CanvasId canvas_id { 0 }; + Vector supported_extensions; + }; + + CanvasHost(RefPtr, Web::Painting::CanvasSurfaceRegistry&); + ~CanvasHost(); + + Optional create_2d_context(Gfx::IntSize, bool alpha); + CreateWebGLContextResult create_webgl_context(Web::WebGL::WebGLVersion, Gfx::IntSize, bool depth, bool stencil, bool antialias); + void destroy_context(Web::Painting::CanvasId); + bool has_context(Web::Painting::CanvasId) const; + + void execute_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&); + void execute_webgl_commands(Web::Painting::CanvasId, ByteBuffer const&, Vector const&); + ErrorOr execute_webgl_sync_call(Web::Painting::CanvasId, ByteBuffer request); + Web::WebGL::ReadPixelsResult webgl_read_pixels_robust_angle(Web::Painting::CanvasId, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer pixels); + void webgl_read_buffer_sub_data(Web::Painting::CanvasId, Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer data); + + void present_webgl_canvas(Web::Painting::CanvasId, bool preserve_drawing_buffer); + Gfx::ShareableBitmap read_back_pixels(Web::Painting::CanvasId, Gfx::IntRect); + +private: + using Canvas2DContext = NonnullOwnPtr; + using WebGLContext = NonnullOwnPtr; + using Context = Variant; + + Context* context(Web::Painting::CanvasId); + OwnPtr create_2d_command_player(Gfx::IntSize, bool alpha); + static Gfx::CanvasCommandPlayer& as_2d(Context&); + static HostWebGLContext& as_webgl(Context&); + + RefPtr m_skia_backend_context; + Web::Painting::CanvasSurfaceRegistry& m_canvas_surface_registry; + HashMap m_contexts; +}; + +} diff --git a/Services/Compositor/CompositorWebContentServer.ipc b/Services/Compositor/CompositorWebContentServer.ipc index 0795f319d9..def6972368 100644 --- a/Services/Compositor/CompositorWebContentServer.ipc +++ b/Services/Compositor/CompositorWebContentServer.ipc @@ -1,6 +1,10 @@ #include +#include +#include +#include #include #include +#include #include #include #include @@ -10,6 +14,7 @@ #include #include #include +#include endpoint CompositorWebContentServer { @@ -28,6 +33,18 @@ endpoint CompositorWebContentServer update_compositor_surface(Web::Compositor::CompositorContextId context_id, Web::Painting::CompositorSurfaceId surface_id, Gfx::SharedImage shared_image) =| clear_compositor_surface(Web::Compositor::CompositorContextId context_id, Web::Painting::CompositorSurfaceId surface_id) =| + create_canvas_2d_context(Gfx::IntSize size, bool alpha) => (bool success, Web::Painting::CanvasId canvas_id) + update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands) =| + destroy_canvas_context(Web::Painting::CanvasId canvas_id) =| + get_canvas_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect rect) => (Gfx::ShareableBitmap pixels) + + create_webgl_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize size, bool depth, bool stencil, bool antialias) => (bool success, Web::Painting::CanvasId canvas_id, Vector supported_extensions) + webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer commands, Vector bitmaps) =| + webgl_present_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) =| + webgl_sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) => (ByteBuffer reply) + webgl_read_pixels(Web::Painting::CanvasId canvas_id, i32 x, i32 y, i32 width, i32 height, u32 format, u32 type, i32 buf_size, Core::AnonymousBuffer pixels) => (i32 length, i32 columns, i32 rows) + webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, u32 target, i64 offset, i64 size, Core::AnonymousBuffer data) => () + invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId context_id, u64 generation) =| async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) => (Web::Compositor::AsyncScrollEnqueueResult result) should_defer_main_thread_present_for_async_scroll(Web::Compositor::CompositorContextId context_id) => (bool should_defer) diff --git a/Services/Compositor/ConnectionFromWebContent.cpp b/Services/Compositor/ConnectionFromWebContent.cpp index 2deadda1a0..82482a370a 100644 --- a/Services/Compositor/ConnectionFromWebContent.cpp +++ b/Services/Compositor/ConnectionFromWebContent.cpp @@ -13,6 +13,7 @@ namespace Compositor { ConnectionFromWebContent::ConnectionFromWebContent(NonnullOwnPtr transport, NonnullRefPtr compositor_state, int client_id) : IPC::ConnectionFromClient(*this, move(transport), client_id) , m_compositor_state(move(compositor_state)) + , m_canvas_host(m_compositor_state->skia_backend_context(), m_compositor_state->canvas_surface_registry()) { } @@ -117,6 +118,71 @@ void ConnectionFromWebContent::clear_compositor_surface(Web::Compositor::Composi m_compositor_state->clear_compositor_surface(context_id, surface_id); } +Messages::CompositorWebContentServer::CreateCanvas2dContextResponse ConnectionFromWebContent::create_canvas_2d_context(Gfx::IntSize size, bool alpha) +{ + auto canvas_id = m_canvas_host.create_2d_context(size, alpha); + if (!canvas_id.has_value()) + return { false, Web::Painting::CanvasId { 0 } }; + return { true, *canvas_id }; +} + +void ConnectionFromWebContent::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands) +{ + m_canvas_host.execute_canvas_2d_commands(canvas_id, commands); +} + +void ConnectionFromWebContent::destroy_canvas_context(Web::Painting::CanvasId canvas_id) +{ + m_canvas_host.destroy_context(canvas_id); +} + +Messages::CompositorWebContentServer::GetCanvasPixelsResponse ConnectionFromWebContent::get_canvas_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect rect) +{ + return m_canvas_host.read_back_pixels(canvas_id, rect); +} + +Messages::CompositorWebContentServer::CreateWebglContextResponse ConnectionFromWebContent::create_webgl_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize size, bool depth, bool stencil, bool antialias) +{ + auto result = m_canvas_host.create_webgl_context(webgl_version, size, depth, stencil, antialias); + return { result.success, result.canvas_id, move(result.supported_extensions) }; +} + +void ConnectionFromWebContent::webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer commands, Vector bitmaps) +{ + m_canvas_host.execute_webgl_commands(canvas_id, commands, bitmaps); +} + +void ConnectionFromWebContent::webgl_present_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) +{ + m_canvas_host.present_webgl_canvas(canvas_id, preserve_drawing_buffer); +} + +Messages::CompositorWebContentServer::WebglSyncCallResponse ConnectionFromWebContent::webgl_sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) +{ + return MUST(m_canvas_host.execute_webgl_sync_call(canvas_id, move(request))); +} + +Messages::CompositorWebContentServer::WebglReadPixelsResponse ConnectionFromWebContent::webgl_read_pixels(Web::Painting::CanvasId canvas_id, i32 x, i32 y, i32 width, i32 height, u32 format, u32 type, i32 buf_size, Core::AnonymousBuffer pixels) +{ + if (buf_size < 0 || (buf_size > 0 && (!pixels.is_valid() || pixels.size() < static_cast(buf_size)))) { + did_misbehave("WebContent sent an invalid WebGL readPixels buffer"); + return { 0, 0, 0 }; + } + + auto result = m_canvas_host.webgl_read_pixels_robust_angle(canvas_id, x, y, width, height, format, type, buf_size, move(pixels)); + return { result.length, result.columns, result.rows }; +} + +void ConnectionFromWebContent::webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, u32 target, i64 offset, i64 size, Core::AnonymousBuffer data) +{ + if (size < 0 || (size > 0 && (!data.is_valid() || data.size() < static_cast(size)))) { + did_misbehave("WebContent sent an invalid WebGL buffer readback target"); + return; + } + + m_canvas_host.webgl_read_buffer_sub_data(canvas_id, target, offset, size, move(data)); +} + void ConnectionFromWebContent::invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId context_id, u64 generation) { verify_context_is_owned_by_this_connection(context_id); diff --git a/Services/Compositor/ConnectionFromWebContent.h b/Services/Compositor/ConnectionFromWebContent.h index 9b4776a3bc..942ddfab68 100644 --- a/Services/Compositor/ConnectionFromWebContent.h +++ b/Services/Compositor/ConnectionFromWebContent.h @@ -7,12 +7,17 @@ #pragma once #include +#include +#include #include #include #include +#include +#include #include #include #include +#include namespace Compositor { @@ -41,6 +46,17 @@ private: virtual void clear_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId) override; virtual void update_compositor_surface(Web::Compositor::CompositorContextId, Web::Painting::CompositorSurfaceId, Gfx::SharedImage) override; virtual void clear_compositor_surface(Web::Compositor::CompositorContextId, Web::Painting::CompositorSurfaceId) override; + virtual Messages::CompositorWebContentServer::CreateCanvas2dContextResponse create_canvas_2d_context(Gfx::IntSize, bool) override; + virtual void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList) override; + virtual void destroy_canvas_context(Web::Painting::CanvasId) override; + virtual Messages::CompositorWebContentServer::GetCanvasPixelsResponse get_canvas_pixels(Web::Painting::CanvasId, Gfx::IntRect) override; + + virtual Messages::CompositorWebContentServer::CreateWebglContextResponse create_webgl_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize size, bool depth, bool stencil, bool antialias) override; + virtual void webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer commands, Vector bitmaps) override; + virtual void webgl_present_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) override; + virtual Messages::CompositorWebContentServer::WebglSyncCallResponse webgl_sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) override; + virtual Messages::CompositorWebContentServer::WebglReadPixelsResponse webgl_read_pixels(Web::Painting::CanvasId canvas_id, i32 x, i32 y, i32 width, i32 height, u32 format, u32 type, i32 buf_size, Core::AnonymousBuffer pixels) override; + virtual void webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, u32 target, i64 offset, i64 size, Core::AnonymousBuffer data) override; virtual void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation) override; virtual Messages::CompositorWebContentServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking) override; virtual Messages::CompositorWebContentServer::ShouldDeferMainThreadPresentForAsyncScrollResponse should_defer_main_thread_present_for_async_scroll(Web::Compositor::CompositorContextId) override; @@ -54,6 +70,7 @@ private: void verify_context_is_owned_by_this_connection(Web::Compositor::CompositorContextId); NonnullRefPtr m_compositor_state; + CanvasHost m_canvas_host; Function m_on_death; }; diff --git a/Services/Compositor/HostWebGLContext.cpp b/Services/Compositor/HostWebGLContext.cpp new file mode 100644 index 0000000000..c22167a434 --- /dev/null +++ b/Services/Compositor/HostWebGLContext.cpp @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Compositor { + +using namespace Web::WebGL; + +static constexpr GLsizei max_webgl_string_list_entries = 16384; + +static Web::WebGL::OpenGLContext::WebGLVersion to_opengl_webgl_version(WebGLVersion version) +{ + switch (version) { + case WebGLVersion::WebGL1: + return Web::WebGL::OpenGLContext::WebGLVersion::WebGL1; + case WebGLVersion::WebGL2: + return Web::WebGL::OpenGLContext::WebGLVersion::WebGL2; + } + VERIFY_NOT_REACHED(); +} + +static Optional texture_export_format(GLenum format, GLenum type) +{ + switch (format) { + case GL_RGB: + switch (type) { + case GL_UNSIGNED_BYTE: + return Gfx::ExportFormat::RGB888; + case GL_UNSIGNED_SHORT_5_6_5: + return Gfx::ExportFormat::RGB565; + default: + break; + } + break; + case GL_RGBA: + switch (type) { + case GL_UNSIGNED_BYTE: + return Gfx::ExportFormat::RGBA8888; + case GL_UNSIGNED_SHORT_4_4_4_4: + // FIXME: This is not exactly the same as RGBA. + return Gfx::ExportFormat::RGBA4444; + case GL_UNSIGNED_SHORT_5_5_5_1: + return Gfx::ExportFormat::RGBA5551; + default: + break; + } + break; + case GL_ALPHA: + switch (type) { + case GL_UNSIGNED_BYTE: + return Gfx::ExportFormat::Alpha8; + default: + break; + } + break; + case GL_LUMINANCE: + switch (type) { + case GL_UNSIGNED_BYTE: + return Gfx::ExportFormat::Gray8; + default: + break; + } + break; + default: + break; + } + + dbgln("WebGL: Unsupported format and type combination. format: 0x{:04x}, type: 0x{:04x}", format, type); + return {}; +} + +HostWebGLContext::HostWebGLContext(NonnullOwnPtr gl_context) + : m_gl_context(move(gl_context)) +{ +} + +OwnPtr HostWebGLContext::create(NonnullRefPtr skia_backend_context, WebGLVersion version, Web::WebGL::OpenGLContext::DrawingBufferOptions options, Gfx::IntSize initial_size) +{ + if (initial_size.width() < 1 || initial_size.width() > max_webgl_drawing_buffer_dimension + || initial_size.height() < 1 || initial_size.height() > max_webgl_drawing_buffer_dimension) + return {}; + auto gl_context = Web::WebGL::OpenGLContext::create(skia_backend_context, to_opengl_webgl_version(version), options); + if (!gl_context) + return {}; + gl_context->set_size(initial_size); + return adopt_own(*new HostWebGLContext(gl_context.release_nonnull())); +} + +ErrorOr HostWebGLContext::execute_commands(ReadonlyBytes bytes, Vector const& bitmaps) +{ + m_gl_context->make_current(); + + // A non-preserving context's drawing buffer is cleared after being prepared for + // compositing, but the clear is deferred to here (the start of the next frame's + // commands) so a readback taken before then still sees the rendered frame. + if (m_needs_clear_before_next_frame) { + m_gl_context->clear_buffer_to_default_values(); + m_needs_clear_before_next_frame = false; + } + + return WebGLCommandList::for_each_command(bytes, [&](Command const& command, [[maybe_unused]] ReadonlyBytes payload) -> ErrorOr { + if constexpr (IsSame) { + return set_drawing_buffer_size(command.width, command.height); + } else if constexpr (IsSame) { + m_gl_context->read_pixels_robust_angle(command.x, command.y, command.width, command.height, command.format, command.type, 0, nullptr, nullptr, nullptr, reinterpret_cast(static_cast(command.offset))); + return {}; + } else if constexpr (IsSame) { + return tex_image2d_from_bitmap(command, bitmaps); + } else if constexpr (IsSame) { + return tex_sub_image2d_from_bitmap(command, bitmaps); + } else { + return replay_webgl_command(*m_gl_context, m_objects, command, payload); + } + }); +} + +static ErrorOr convert_bitmap_for_upload(Vector const& bitmaps, u32 bitmap_index, GLenum format, GLenum type, bool has_explicit_destination_size, GLsizei destination_width, GLsizei destination_height, bool flip_y, bool premultiply_alpha) +{ + if (bitmap_index >= bitmaps.size()) + return Error::from_string_literal("WebGL image upload references an out-of-range bitmap"); + + auto export_format = texture_export_format(format, type); + if (!export_format.has_value()) + return Error::from_string_literal("WebGL image upload has an unsupported format+type combination"); + + int export_flags = 0; + if (flip_y) + export_flags |= Gfx::ExportFlags::FlipY; + if (premultiply_alpha) + export_flags |= Gfx::ExportFlags::PremultiplyAlpha; + + Optional target_width; + Optional target_height; + if (has_explicit_destination_size) { + if (destination_width < 0 || destination_height < 0) { + return Gfx::BitmapExportResult { + .buffer = {}, + .width = destination_width, + .height = destination_height, + }; + } + target_width = destination_width; + target_height = destination_height; + } + + auto const& frame = bitmaps[bitmap_index]; + return Gfx::export_bitmap_to_byte_buffer(frame.bitmap(), frame.color_space(), export_format.value(), export_flags, target_width, target_height); +} + +ErrorOr HostWebGLContext::tex_image2d_from_bitmap(Commands::TexImage2DFromBitmap const& command, Vector const& bitmaps) +{ + auto converted = TRY(convert_bitmap_for_upload(bitmaps, command.bitmap_index, command.format, command.type, command.has_explicit_destination_size, command.destination_width, command.destination_height, command.flip_y, command.premultiply_alpha)); + m_gl_context->tex_image2d_robust_angle(command.target, command.level, command.internalformat, converted.width, converted.height, 0, command.format, command.type, converted.buffer.size(), converted.buffer.data()); + return {}; +} + +ErrorOr HostWebGLContext::tex_sub_image2d_from_bitmap(Commands::TexSubImage2DFromBitmap const& command, Vector const& bitmaps) +{ + auto converted = TRY(convert_bitmap_for_upload(bitmaps, command.bitmap_index, command.format, command.type, command.has_explicit_destination_size, command.destination_width, command.destination_height, command.flip_y, command.premultiply_alpha)); + m_gl_context->tex_sub_image2d_robust_angle(command.target, command.level, command.xoffset, command.yoffset, converted.width, converted.height, command.format, command.type, converted.buffer.size(), converted.buffer.data()); + return {}; +} + +ErrorOr HostWebGLContext::execute_sync_call(ReadonlyBytes request) +{ + m_gl_context->make_current(); + return handle_webgl_sync_call(*m_gl_context, m_objects, request); +} + +ErrorOr> HostWebGLContext::prepare_for_compositing(bool preserve_drawing_buffer) +{ + // Flush all pending GL work so Skia samples the finished drawing buffer. The + // default framebuffer was written behind Skia's back, so discard cached snapshots + // before the display-list player asks Skia for an image. + m_gl_context->present(/* preserve_drawing_buffer= */ true); + + auto drawing_surface = m_gl_context->surface(); + if (!drawing_surface) + return Error::from_string_literal("WebGL context has no drawing buffer"); + m_gl_context->notify_content_will_change(); + + // Defer the clear (see execute_commands) so a readback before the next frame still sees + // this frame. + if (!preserve_drawing_buffer) + m_needs_clear_before_next_frame = true; + + return drawing_surface.release_nonnull(); +} + +RefPtr HostWebGLContext::surface() +{ + return m_gl_context->surface(); +} + +Gfx::ShareableBitmap HostWebGLContext::read_back_drawing_buffer(Gfx::IntRect rect) +{ + m_gl_context->make_current(); + m_gl_context->present(/* preserve_drawing_buffer= */ true); + auto surface = m_gl_context->surface(); + if (!surface) + return {}; + + auto clipped_rect = rect.intersected(surface->rect()); + if (clipped_rect.is_empty()) + return {}; + + auto bitmap_or_error = Gfx::Bitmap::create_shareable(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, clipped_rect.size()); + if (bitmap_or_error.is_error()) + return {}; + auto bitmap = bitmap_or_error.release_value(); + surface->flush(); + surface->read_into_bitmap(*bitmap, clipped_rect.location()); + return Gfx::ShareableBitmap { move(bitmap), Gfx::ShareableBitmap::ConstructWithKnownGoodBitmap }; +} + +ReadPixelsResult HostWebGLContext::read_pixels_robust_angle(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei buf_size, Core::AnonymousBuffer pixels) +{ + VERIFY(buf_size >= 0); + VERIFY(static_cast(buf_size) <= pixels.size()); + + m_gl_context->make_current(); + + GLsizei length = 0; + GLsizei columns = 0; + GLsizei rows = 0; + m_gl_context->read_pixels_robust_angle(x, y, width, height, format, type, buf_size, &length, &columns, &rows, pixels.data()); + return { + .length = length, + .columns = columns, + .rows = rows, + }; +} + +void HostWebGLContext::read_buffer_sub_data(GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer data) +{ + VERIFY(size >= 0); + VERIFY(static_cast(size) <= data.size()); + + m_gl_context->make_current(); + + if (auto* mapped = m_gl_context->map_buffer_range(target, offset, size, GL_MAP_READ_BIT)) { + __builtin_memcpy(data.data(), mapped, static_cast(size)); + m_gl_context->unmap_buffer(target); + } +} + +ErrorOr HostWebGLContext::set_drawing_buffer_size(int width, int height) +{ + VERIFY(width >= 1); + VERIFY(width <= max_webgl_drawing_buffer_dimension); + VERIFY(height >= 1); + VERIFY(height <= max_webgl_drawing_buffer_dimension); + m_gl_context->set_size({ width, height }); + m_gl_context->make_current(); + return {}; +} + +ErrorOr replay_webgl_command(Web::WebGL::OpenGLContext& gl, WebGLObjectMap& objects, Commands::ShaderSource const& command, ReadonlyBytes payload) +{ + auto source_bytes = WebGLCommandList::resolve_string_span(payload, command.source); + auto shader = objects.lookup(command.shader); + GLchar const* source = reinterpret_cast(source_bytes.data()); + GLint length = static_cast(source_bytes.size() - 1); + gl.shader_source(shader, 1, &source, &length); + return {}; +} + +// Splits a payload of `count` packed NUL-terminated strings into pointers. +static ErrorOr> split_packed_strings(ReadonlyBytes bytes, GLsizei count) +{ + if (count < 0 || count > max_webgl_string_list_entries) + return Error::from_string_literal("WebGL string list is too long"); + Vector strings; + strings.ensure_capacity(count); + size_t cursor = 0; + for (GLsizei i = 0; i < count; ++i) { + auto start = cursor; + while (cursor < bytes.size() && bytes[cursor] != 0) + ++cursor; + if (cursor >= bytes.size()) + return Error::from_string_literal("WebGL string is not NUL-terminated"); + strings.unchecked_append(reinterpret_cast(bytes.data() + start)); + ++cursor; + } + return strings; +} + +ErrorOr replay_webgl_command(Web::WebGL::OpenGLContext& gl, WebGLObjectMap& objects, Commands::TransformFeedbackVaryings const& command, ReadonlyBytes payload) +{ + auto varyings_bytes = WebGLCommandList::resolve_data_span(payload, command.varyings); + auto varyings = TRY(split_packed_strings(varyings_bytes, command.count)); + auto program = objects.lookup(command.program); + gl.transform_feedback_varyings(program, command.count, varyings.data(), command.buffer_mode); + return {}; +} + +// --- Wire-specified synchronous calls ------------------------------------------------ + +ErrorOr handle_one(Web::WebGL::OpenGLContext& gl, WebGLObjectMap&, SyncCalls::GetString::Request const& request, ReadonlyBytes) +{ + auto const* value = gl.get_string(request.name); + static constexpr u8 empty_string[] { 0 }; + auto value_bytes = value + ? ReadonlyBytes { value, __builtin_strlen(reinterpret_cast(value)) + 1 } + : ReadonlyBytes { empty_string, sizeof(empty_string) }; + SyncCalls::GetString::Reply reply { + .value = { WebGLCommandList::first_inline_data_offset(sizeof(SyncCalls::GetString::Reply)), static_cast(value_bytes.size()) }, + }; + return WebGLSyncCall::encode_reply(reply, value_bytes); +} + +ErrorOr handle_one(Web::WebGL::OpenGLContext& gl, WebGLObjectMap&, SyncCalls::GetVertexAttribPointervRobustANGLE::Request const& request, ReadonlyBytes) +{ + void* pointer = nullptr; + GLsizei length = 0; + gl.get_vertex_attrib_pointerv_robust_angle(request.index, request.pname, 1, &length, &pointer); + SyncCalls::GetVertexAttribPointervRobustANGLE::Reply reply { + .pointer = static_cast(reinterpret_cast(pointer)), + }; + return WebGLSyncCall::encode_reply(reply); +} + +ErrorOr handle_one(Web::WebGL::OpenGLContext& gl, WebGLObjectMap& objects, SyncCalls::GetUniformIndices::Request const& request, ReadonlyBytes payload) +{ + auto names_bytes = WebGLCommandList::resolve_data_span(payload, request.uniform_names); + auto names = TRY(split_packed_strings(names_bytes, request.uniform_count)); + auto program = objects.lookup(request.program); + Vector indices; + indices.resize(request.uniform_count); + gl.get_uniform_indices(program, request.uniform_count, names.data(), indices.data()); + ReadonlyBytes indices_bytes { indices.data(), indices.size() * sizeof(GLuint) }; + SyncCalls::GetUniformIndices::Reply reply { + .uniform_indices = { WebGLCommandList::first_inline_data_offset(sizeof(SyncCalls::GetUniformIndices::Reply)), static_cast(indices_bytes.size()) }, + }; + return WebGLSyncCall::encode_reply(reply, indices_bytes); +} + +} diff --git a/Services/Compositor/HostWebGLContext.h b/Services/Compositor/HostWebGLContext.h new file mode 100644 index 0000000000..31580a0185 --- /dev/null +++ b/Services/Compositor/HostWebGLContext.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::WebGL::Commands { + +struct TexImage2DFromBitmap; +struct TexSubImage2DFromBitmap; + +} + +namespace Compositor { + +class HostWebGLContext { +public: + static OwnPtr create(NonnullRefPtr, Web::WebGL::WebGLVersion, Web::WebGL::OpenGLContext::DrawingBufferOptions, Gfx::IntSize initial_size); + + ErrorOr execute_commands(ReadonlyBytes, Vector const& bitmaps); + ErrorOr execute_sync_call(ReadonlyBytes request); + Gfx::ShareableBitmap read_back_drawing_buffer(Gfx::IntRect); + Web::WebGL::ReadPixelsResult read_pixels_robust_angle(Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer pixels); + void read_buffer_sub_data(Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer data); + ErrorOr> prepare_for_compositing(bool preserve_drawing_buffer); + RefPtr surface(); + + Web::WebGL::OpenGLContext& gl_context() { return *m_gl_context; } + +private: + explicit HostWebGLContext(NonnullOwnPtr); + + ErrorOr set_drawing_buffer_size(int width, int height); + ErrorOr tex_image2d_from_bitmap(Web::WebGL::Commands::TexImage2DFromBitmap const&, Vector const& bitmaps); + ErrorOr tex_sub_image2d_from_bitmap(Web::WebGL::Commands::TexSubImage2DFromBitmap const&, Vector const& bitmaps); + + NonnullOwnPtr m_gl_context; + WebGLObjectMap m_objects; + bool m_needs_clear_before_next_frame { false }; +}; + +} diff --git a/Services/Compositor/WebGLObjectMap.cpp b/Services/Compositor/WebGLObjectMap.cpp new file mode 100644 index 0000000000..26c76ed007 --- /dev/null +++ b/Services/Compositor/WebGLObjectMap.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Compositor { + +template +static Value lookup_or_default(HashMap const& map, Web::WebGL::WebGLObjectId id) +{ + return map.get(id).value_or(Value {}); +} + +template +static Value take_or_default(HashMap& map, Web::WebGL::WebGLObjectId id) +{ + return map.take(id).value_or(Value {}); +} + +template +static ErrorOr add_unique(HashMap& map, Web::WebGL::WebGLObjectId id, Value value) +{ + if (id == 0) + return Error::from_string_literal("WebGL object id 0 is reserved"); + if (map.contains(id)) + return Error::from_string_literal("WebGL object id is already in use"); + map.set(id, value); + return {}; +} + +GLuint WebGLObjectMap::lookup(Web::WebGL::WebGLObjectId id) const +{ + return lookup_or_default(m_objects, id); +} + +GLuint WebGLObjectMap::take(Web::WebGL::WebGLObjectId id) +{ + return take_or_default(m_objects, id); +} + +ErrorOr WebGLObjectMap::add(Web::WebGL::WebGLObjectId id, GLuint name) +{ + return add_unique(m_objects, id, name); +} + +GLsync WebGLObjectMap::lookup_sync(Web::WebGL::WebGLObjectId id) const +{ + return lookup_or_default(m_syncs, id); +} + +GLsync WebGLObjectMap::take_sync(Web::WebGL::WebGLObjectId id) +{ + return take_or_default(m_syncs, id); +} + +ErrorOr WebGLObjectMap::add_sync(Web::WebGL::WebGLObjectId id, GLsync sync) +{ + return add_unique(m_syncs, id, sync); +} + +} diff --git a/Services/Compositor/WebGLObjectMap.h b/Services/Compositor/WebGLObjectMap.h new file mode 100644 index 0000000000..ac14951345 --- /dev/null +++ b/Services/Compositor/WebGLObjectMap.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Compositor { + +class WebGLObjectMap { +public: + GLuint lookup(Web::WebGL::WebGLObjectId) const; + GLuint take(Web::WebGL::WebGLObjectId); + ErrorOr add(Web::WebGL::WebGLObjectId, GLuint); + + GLsync lookup_sync(Web::WebGL::WebGLObjectId) const; + GLsync take_sync(Web::WebGL::WebGLObjectId); + ErrorOr add_sync(Web::WebGL::WebGLObjectId, GLsync); + +private: + HashMap m_objects; + HashMap m_syncs; +}; + +} diff --git a/Services/WebContent/CompositorConnection.cpp b/Services/WebContent/CompositorConnection.cpp index de55c1fa12..0074275230 100644 --- a/Services/WebContent/CompositorConnection.cpp +++ b/Services/WebContent/CompositorConnection.cpp @@ -93,6 +93,43 @@ void CompositorConnection::clear_compositor_surface(Web::Compositor::CompositorC async_clear_compositor_surface(context_id, surface_id); } +Optional CompositorConnection::create_canvas_2d_context(Gfx::IntSize size, bool alpha) +{ + if (!can_send_message_to_compositor()) + return {}; + + auto response = send_sync(size, alpha); + if (!response->success()) + return {}; + return response->canvas_id(); +} + +void CompositorConnection::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands) +{ + if (!can_send_message_to_compositor()) + return; + + auto encoded_message = MUST(Messages::CompositorWebContentServer::UpdateCanvas2dCommands::static_encode(canvas_id, commands)); + if (post_message(encoded_message).is_error()) + did_lose_compositor(); +} + +void CompositorConnection::destroy_canvas_context(Web::Painting::CanvasId canvas_id) +{ + if (!can_send_message_to_compositor()) + return; + async_destroy_canvas_context(canvas_id); +} + +Gfx::ShareableBitmap CompositorConnection::get_canvas_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect rect) +{ + if (!can_send_message_to_compositor()) + return {}; + + auto response = send_sync(canvas_id, rect); + return response->take_pixels(); +} + void CompositorConnection::invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId context_id, u64 generation) { if (!can_send_message_to_compositor()) @@ -153,6 +190,66 @@ void CompositorConnection::present_frame(Web::Compositor::CompositorContextId co async_present_frame(context_id, viewport_rect); } +Optional CompositorConnection::create_webgl_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize size, bool depth, bool stencil, bool antialias, Vector& out_supported_extensions) +{ + if (!can_send_message_to_compositor()) + return {}; + + auto response = send_sync(webgl_version, size, depth, stencil, antialias); + out_supported_extensions = response->take_supported_extensions(); + if (!response->success()) + return {}; + return response->canvas_id(); +} + +void CompositorConnection::send_webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer const& commands, Vector const& bitmaps) +{ + if (!can_send_message_to_compositor()) + return; + + auto encoded_message = MUST(Messages::CompositorWebContentServer::WebglCommands::static_encode(canvas_id, commands, bitmaps)); + if (post_message(encoded_message).is_error()) + did_lose_compositor(); +} + +void CompositorConnection::present_webgl_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) +{ + if (!can_send_message_to_compositor()) + return; + + async_webgl_present_canvas(canvas_id, preserve_drawing_buffer); +} + +ByteBuffer CompositorConnection::webgl_sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) +{ + if (!can_send_message_to_compositor()) + return {}; + + auto response = send_sync(canvas_id, move(request)); + return response->take_reply(); +} + +Web::WebGL::ReadPixelsResult CompositorConnection::read_webgl_pixels(Web::Painting::CanvasId canvas_id, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer const& pixels) +{ + if (!can_send_message_to_compositor()) + return {}; + + auto response = send_sync(canvas_id, x, y, width, height, format, type, buf_size, pixels); + return { + .length = response->length(), + .columns = response->columns(), + .rows = response->rows(), + }; +} + +void CompositorConnection::read_webgl_buffer_sub_data(Web::Painting::CanvasId canvas_id, Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer const& data) +{ + if (!can_send_message_to_compositor()) + return; + + (void)send_sync(canvas_id, target, offset, size, data); +} + void CompositorConnection::request_screenshot(Web::Compositor::CompositorContextId context_id, NonnullRefPtr target_surface, Function&& callback) { if (!can_send_message_to_compositor()) { diff --git a/Services/WebContent/CompositorConnection.h b/Services/WebContent/CompositorConnection.h index 3654f03735..22819eb4a9 100644 --- a/Services/WebContent/CompositorConnection.h +++ b/Services/WebContent/CompositorConnection.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace WebContent { @@ -46,6 +47,10 @@ public: void clear_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId); void update_compositor_surface(Web::Compositor::CompositorContextId, Web::Painting::CompositorSurfaceId, Gfx::SharedImage const&); void clear_compositor_surface(Web::Compositor::CompositorContextId, Web::Painting::CompositorSurfaceId); + Optional create_canvas_2d_context(Gfx::IntSize, bool alpha); + void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&); + void destroy_canvas_context(Web::Painting::CanvasId); + Gfx::ShareableBitmap get_canvas_pixels(Web::Painting::CanvasId, Gfx::IntRect); void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation); Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking); bool should_defer_main_thread_present_for_async_scroll(Web::Compositor::CompositorContextId); @@ -53,6 +58,14 @@ public: void viewport_size_updated(Web::Compositor::CompositorContextId, Gfx::IntSize, Web::Compositor::WindowResizingInProgress); void present_frame(Web::Compositor::CompositorContextId, Gfx::IntRect); void request_screenshot(Web::Compositor::CompositorContextId, NonnullRefPtr, Function&&); + + Optional create_webgl_context(Web::WebGL::WebGLVersion, Gfx::IntSize, bool depth, bool stencil, bool antialias, Vector& out_supported_extensions); + void send_webgl_commands(Web::Painting::CanvasId, ByteBuffer const&, Vector const& bitmaps); + void present_webgl_canvas(Web::Painting::CanvasId, bool preserve_drawing_buffer); + ByteBuffer webgl_sync_call(Web::Painting::CanvasId, ByteBuffer request); + Web::WebGL::ReadPixelsResult read_webgl_pixels(Web::Painting::CanvasId, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer const& pixels); + void read_webgl_buffer_sub_data(Web::Painting::CanvasId, Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer const& data); + Function on_mouse_event; private: diff --git a/Services/WebContent/WebContentCompositorHost.cpp b/Services/WebContent/WebContentCompositorHost.cpp index 6fdb8aaa50..45b0cb927c 100644 --- a/Services/WebContent/WebContentCompositorHost.cpp +++ b/Services/WebContent/WebContentCompositorHost.cpp @@ -5,15 +5,110 @@ */ #include +#include +#include #include #include #include +#include +#include #include #include #include namespace WebContent { +class WebContentRemoteWebGLTransport final : public Web::WebGL::RemoteWebGLTransport { +public: + explicit WebContentRemoteWebGLTransport(NonnullRefPtr connection) + : m_connection(move(connection)) + { + } + +private: + virtual CreateResult create_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize initial_size, bool depth, bool stencil, bool antialias) override + { + CreateResult result; + auto canvas_id = m_connection->create_webgl_context(webgl_version, initial_size, depth, stencil, antialias, result.supported_extensions); + if (canvas_id.has_value()) { + result.success = true; + result.canvas_id = *canvas_id; + } + return result; + } + + virtual void destroy_context(Web::Painting::CanvasId canvas_id) override + { + m_connection->destroy_canvas_context(canvas_id); + } + + virtual void send_commands(Web::Painting::CanvasId canvas_id, ByteBuffer const& commands, Vector const& bitmaps) override + { + m_connection->send_webgl_commands(canvas_id, commands, bitmaps); + } + + virtual void present_canvas(Web::Painting::CanvasId canvas_id, bool preserve_drawing_buffer) override + { + m_connection->present_webgl_canvas(canvas_id, preserve_drawing_buffer); + } + + virtual ByteBuffer sync_call(Web::Painting::CanvasId canvas_id, ByteBuffer request) override + { + return m_connection->webgl_sync_call(canvas_id, move(request)); + } + + virtual Web::WebGL::ReadPixelsResult read_pixels_robust_angle(Web::Painting::CanvasId canvas_id, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer pixels) override + { + return m_connection->read_webgl_pixels(canvas_id, x, y, width, height, format, type, buf_size, pixels); + } + + virtual void read_buffer_sub_data(Web::Painting::CanvasId canvas_id, Web::WebGL::GLenum target, Web::WebGL::GLintptr offset, Web::WebGL::GLintptr size, Core::AnonymousBuffer data) override + { + m_connection->read_webgl_buffer_sub_data(canvas_id, target, offset, size, data); + } + + virtual Gfx::ShareableBitmap read_back_drawing_buffer(Web::Painting::CanvasId canvas_id, Gfx::IntRect const& rect) override + { + return m_connection->get_canvas_pixels(canvas_id, rect); + } + + NonnullRefPtr m_connection; +}; + +class WebContentRemoteCanvas2DTransport final : public Web::HTML::RemoteCanvas2DTransport { +public: + explicit WebContentRemoteCanvas2DTransport(NonnullRefPtr connection) + : m_connection(move(connection)) + { + } + +private: + virtual Optional create_context(Gfx::IntSize size, bool alpha) override + { + return m_connection->create_canvas_2d_context(size, alpha); + } + + virtual void destroy_context(Web::Painting::CanvasId canvas_id) override + { + m_connection->destroy_canvas_context(canvas_id); + } + + virtual void update_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands) override + { + m_connection->update_canvas_2d_commands(canvas_id, commands); + } + + virtual RefPtr read_back_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect const& rect) override + { + auto shareable_bitmap = m_connection->get_canvas_pixels(canvas_id, rect); + if (!shareable_bitmap.is_valid()) + return nullptr; + return shareable_bitmap.bitmap(); + } + + NonnullRefPtr m_connection; +}; + class WebContentCompositorHost final : public Web::Compositor::CompositorHost { public: explicit WebContentCompositorHost(ConnectionFromClient& client) @@ -22,6 +117,20 @@ public: } private: + virtual RefPtr create_webgl_transport() override + { + if (auto* connection = compositor_connection()) + return adopt_ref(*new WebContentRemoteWebGLTransport(*connection)); + return nullptr; + } + + virtual RefPtr create_canvas_2d_transport() override + { + if (auto* connection = compositor_connection()) + return adopt_ref(*new WebContentRemoteCanvas2DTransport(*connection)); + return nullptr; + } + virtual void destroy_context(Web::Compositor::CompositorContextId context_id) override { if (auto* connection = compositor_connection())