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.
This commit is contained in:
Aliaksandr Kalenik 2026-06-15 19:29:43 +02:00 committed by Alexander Kalenik
parent 9a8a7798ec
commit 5f0e95de13
18 changed files with 1480 additions and 1 deletions

View file

@ -69,6 +69,9 @@ public:
OwnPtr<CompositorContextHandle> create_context(CompositorContextId);
virtual RefPtr<WebGL::RemoteWebGLTransport> create_webgl_transport() = 0;
virtual RefPtr<HTML::RemoteCanvas2DTransport> create_canvas_2d_transport() = 0;
virtual void destroy_context(CompositorContextId) = 0;
virtual void set_presentation_mode(CompositorContextId, PresentationMode) = 0;

View file

@ -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;

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <LibGfx/Forward.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Painting/DisplayListResourceIds.h>
namespace Web::HTML {
class WEB_API RemoteCanvas2DTransport : public RefCounted<RemoteCanvas2DTransport> {
public:
virtual ~RemoteCanvas2DTransport() = default;
virtual Optional<Painting::CanvasId> 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<Gfx::Bitmap> read_back_pixels(Painting::CanvasId, Gfx::IntRect const&) = 0;
};
}

View file

@ -8,6 +8,7 @@
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <AK/OwnPtr.h>
#include <AK/RefPtr.h>
#include <AK/Vector.h>
#include <LibGfx/Forward.h>

View file

@ -0,0 +1,339 @@
#!/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(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<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(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 <AK/Error.h>
#include <Compositor/WebGLObjectMap.h>
#include <LibWeb/WebGL/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(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<void> 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<ByteBuffer> handle_one(Web::WebGL::OpenGLContext&, WebGLObjectMap&, "
f"Web::WebGL::SyncCalls::{command_name(function)}::Request const&, ReadonlyBytes);\n"
)
out.write("""
ErrorOr<ByteBuffer> handle_webgl_sync_call(Web::WebGL::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(Web::WebGL::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)

View file

@ -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")

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Compositor/CanvasHost.h>
#include <Compositor/HostWebGLContext.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/CanvasCommandPlayer.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SkiaBackendContext.h>
#include <LibWeb/Painting/CanvasSurfaceRegistry.h>
namespace Compositor {
CanvasHost::CanvasHost(RefPtr<Gfx::SkiaBackendContext> 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<Gfx::CanvasCommandPlayer> CanvasHost::create_2d_command_player(Gfx::IntSize size, bool alpha)
{
if (size.is_empty() || static_cast<i64>(size.width()) * static_cast<i64>(size.height()) > Gfx::max_canvas_area)
return nullptr;
auto format = alpha ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888;
auto player = make<Gfx::CanvasCommandPlayer>(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<Canvas2DContext>();
VERIFY(player);
return **player;
}
HostWebGLContext& CanvasHost::as_webgl(Context& context)
{
auto* webgl_context = context.get_pointer<WebGLContext>();
VERIFY(webgl_context);
return **webgl_context;
}
Optional<Web::Painting::CanvasId> 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<Gfx::DecodedImageFrame> 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<ByteBuffer> 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);
});
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Forward.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibWeb/Compositor/Types.h>
#include <LibWeb/Painting/DisplayListResourceIds.h>
#include <LibWeb/WebGL/Types.h>
namespace Web::Painting {
class CanvasSurfaceRegistry;
}
namespace Compositor {
class HostWebGLContext;
class CanvasHost {
public:
struct CreateWebGLContextResult {
bool success { false };
Web::Painting::CanvasId canvas_id { 0 };
Vector<String> supported_extensions;
};
CanvasHost(RefPtr<Gfx::SkiaBackendContext>, Web::Painting::CanvasSurfaceRegistry&);
~CanvasHost();
Optional<Web::Painting::CanvasId> 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<Gfx::DecodedImageFrame> const&);
ErrorOr<ByteBuffer> 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<Gfx::CanvasCommandPlayer>;
using WebGLContext = NonnullOwnPtr<HostWebGLContext>;
using Context = Variant<Canvas2DContext, WebGLContext>;
Context* context(Web::Painting::CanvasId);
OwnPtr<Gfx::CanvasCommandPlayer> create_2d_command_player(Gfx::IntSize, bool alpha);
static Gfx::CanvasCommandPlayer& as_2d(Context&);
static HostWebGLContext& as_webgl(Context&);
RefPtr<Gfx::SkiaBackendContext> m_skia_backend_context;
Web::Painting::CanvasSurfaceRegistry& m_canvas_surface_registry;
HashMap<Web::Painting::CanvasId, Context> m_contexts;
};
}

View file

@ -1,6 +1,10 @@
#include <AK/NonnullRefPtr.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/CanvasCommandList.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibGfx/SharedImage.h>
#include <LibGfx/Size.h>
#include <LibMedia/VideoFrame.h>
@ -10,6 +14,7 @@
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
#include <LibWeb/Painting/ScrollState.h>
#include <LibWeb/WebGL/Types.h>
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<String> supported_extensions)
webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer commands, Vector<Gfx::DecodedImageFrame> 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)

View file

@ -13,6 +13,7 @@ namespace Compositor {
ConnectionFromWebContent::ConnectionFromWebContent(NonnullOwnPtr<IPC::Transport> transport, NonnullRefPtr<CompositorState> compositor_state, int client_id)
: IPC::ConnectionFromClient<CompositorWebContentClientEndpoint, CompositorWebContentServerEndpoint>(*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<Gfx::DecodedImageFrame> 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<size_t>(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_t>(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);

View file

@ -7,12 +7,17 @@
#pragma once
#include <AK/Function.h>
#include <AK/Optional.h>
#include <Compositor/CanvasHost.h>
#include <Compositor/CompositorState.h>
#include <Compositor/CompositorWebContentClientEndpoint.h>
#include <Compositor/CompositorWebContentServerEndpoint.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Size.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
#include <LibWeb/WebGL/Types.h>
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<Gfx::DecodedImageFrame> 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<CompositorState> m_compositor_state;
CanvasHost m_canvas_host;
Function<void(ConnectionFromWebContent&)> m_on_death;
};

View file

@ -0,0 +1,351 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Vector.h>
#include <Compositor/HostWebGLContext.h>
#include <Compositor/WebGLCommandReplayer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/BitmapExport.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibGfx/SkiaBackendContext.h>
#include <LibWeb/WebGL/WebGLCommandList.h>
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<Gfx::ExportFormat> 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<Web::WebGL::OpenGLContext> gl_context)
: m_gl_context(move(gl_context))
{
}
OwnPtr<HostWebGLContext> HostWebGLContext::create(NonnullRefPtr<Gfx::SkiaBackendContext> 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<void> HostWebGLContext::execute_commands(ReadonlyBytes bytes, Vector<Gfx::DecodedImageFrame> 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, [&]<typename Command>(Command const& command, [[maybe_unused]] ReadonlyBytes payload) -> ErrorOr<void> {
if constexpr (IsSame<Command, Commands::SetDrawingBufferSize>) {
return set_drawing_buffer_size(command.width, command.height);
} else if constexpr (IsSame<Command, Commands::ReadPixelsIntoPixelPackBuffer>) {
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<void*>(static_cast<uintptr_t>(command.offset)));
return {};
} else if constexpr (IsSame<Command, Commands::TexImage2DFromBitmap>) {
return tex_image2d_from_bitmap(command, bitmaps);
} else if constexpr (IsSame<Command, Commands::TexSubImage2DFromBitmap>) {
return tex_sub_image2d_from_bitmap(command, bitmaps);
} else {
return replay_webgl_command(*m_gl_context, m_objects, command, payload);
}
});
}
static ErrorOr<Gfx::BitmapExportResult> convert_bitmap_for_upload(Vector<Gfx::DecodedImageFrame> 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<int> target_width;
Optional<int> 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<void> HostWebGLContext::tex_image2d_from_bitmap(Commands::TexImage2DFromBitmap const& command, Vector<Gfx::DecodedImageFrame> 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<void> HostWebGLContext::tex_sub_image2d_from_bitmap(Commands::TexSubImage2DFromBitmap const& command, Vector<Gfx::DecodedImageFrame> 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<ByteBuffer> HostWebGLContext::execute_sync_call(ReadonlyBytes request)
{
m_gl_context->make_current();
return handle_webgl_sync_call(*m_gl_context, m_objects, request);
}
ErrorOr<NonnullRefPtr<Gfx::PaintingSurface>> 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<Gfx::PaintingSurface> 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<size_t>(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<void>());
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_t>(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<void>(), mapped, static_cast<size_t>(size));
m_gl_context->unmap_buffer(target);
}
}
ErrorOr<void> 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<void> 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<GLchar const*>(source_bytes.data());
GLint length = static_cast<GLint>(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<Vector<GLchar const*>> 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<GLchar const*> 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<GLchar const*>(bytes.data() + start));
++cursor;
}
return strings;
}
ErrorOr<void> 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<ByteBuffer> 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<char const*>(value)) + 1 }
: ReadonlyBytes { empty_string, sizeof(empty_string) };
SyncCalls::GetString::Reply reply {
.value = { WebGLCommandList::first_inline_data_offset(sizeof(SyncCalls::GetString::Reply)), static_cast<u32>(value_bytes.size()) },
};
return WebGLSyncCall::encode_reply(reply, value_bytes);
}
ErrorOr<ByteBuffer> 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<Web::WebGL::GLintptr>(reinterpret_cast<uintptr_t>(pointer)),
};
return WebGLSyncCall::encode_reply(reply);
}
ErrorOr<ByteBuffer> 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<GLuint> 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<u32>(indices_bytes.size()) },
};
return WebGLSyncCall::encode_reply(reply, indices_bytes);
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/OwnPtr.h>
#include <AK/RefPtr.h>
#include <Compositor/WebGLObjectMap.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Size.h>
#include <LibWeb/Painting/DisplayListResourceIds.h>
#include <LibWeb/WebGL/OpenGLContext.h>
#include <LibWeb/WebGL/Types.h>
namespace Web::WebGL::Commands {
struct TexImage2DFromBitmap;
struct TexSubImage2DFromBitmap;
}
namespace Compositor {
class HostWebGLContext {
public:
static OwnPtr<HostWebGLContext> create(NonnullRefPtr<Gfx::SkiaBackendContext>, Web::WebGL::WebGLVersion, Web::WebGL::OpenGLContext::DrawingBufferOptions, Gfx::IntSize initial_size);
ErrorOr<void> execute_commands(ReadonlyBytes, Vector<Gfx::DecodedImageFrame> const& bitmaps);
ErrorOr<ByteBuffer> 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<NonnullRefPtr<Gfx::PaintingSurface>> prepare_for_compositing(bool preserve_drawing_buffer);
RefPtr<Gfx::PaintingSurface> surface();
Web::WebGL::OpenGLContext& gl_context() { return *m_gl_context; }
private:
explicit HostWebGLContext(NonnullOwnPtr<Web::WebGL::OpenGLContext>);
ErrorOr<void> set_drawing_buffer_size(int width, int height);
ErrorOr<void> tex_image2d_from_bitmap(Web::WebGL::Commands::TexImage2DFromBitmap const&, Vector<Gfx::DecodedImageFrame> const& bitmaps);
ErrorOr<void> tex_sub_image2d_from_bitmap(Web::WebGL::Commands::TexSubImage2DFromBitmap const&, Vector<Gfx::DecodedImageFrame> const& bitmaps);
NonnullOwnPtr<Web::WebGL::OpenGLContext> m_gl_context;
WebGLObjectMap m_objects;
bool m_needs_clear_before_next_frame { false };
};
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Compositor/WebGLObjectMap.h>
namespace Compositor {
template<typename Value>
static Value lookup_or_default(HashMap<Web::WebGL::WebGLObjectId, Value> const& map, Web::WebGL::WebGLObjectId id)
{
return map.get(id).value_or(Value {});
}
template<typename Value>
static Value take_or_default(HashMap<Web::WebGL::WebGLObjectId, Value>& map, Web::WebGL::WebGLObjectId id)
{
return map.take(id).value_or(Value {});
}
template<typename Value>
static ErrorOr<void> add_unique(HashMap<Web::WebGL::WebGLObjectId, Value>& 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<void> 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<void> WebGLObjectMap::add_sync(Web::WebGL::WebGLObjectId id, GLsync sync)
{
return add_unique(m_syncs, id, sync);
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/HashMap.h>
#include <LibWeb/WebGL/GLFunctions.h>
#include <LibWeb/WebGL/Types.h>
namespace Compositor {
class WebGLObjectMap {
public:
GLuint lookup(Web::WebGL::WebGLObjectId) const;
GLuint take(Web::WebGL::WebGLObjectId);
ErrorOr<void> add(Web::WebGL::WebGLObjectId, GLuint);
GLsync lookup_sync(Web::WebGL::WebGLObjectId) const;
GLsync take_sync(Web::WebGL::WebGLObjectId);
ErrorOr<void> add_sync(Web::WebGL::WebGLObjectId, GLsync);
private:
HashMap<Web::WebGL::WebGLObjectId, GLuint> m_objects;
HashMap<Web::WebGL::WebGLObjectId, GLsync> m_syncs;
};
}

View file

@ -93,6 +93,43 @@ void CompositorConnection::clear_compositor_surface(Web::Compositor::CompositorC
async_clear_compositor_surface(context_id, surface_id);
}
Optional<Web::Painting::CanvasId> CompositorConnection::create_canvas_2d_context(Gfx::IntSize size, bool alpha)
{
if (!can_send_message_to_compositor())
return {};
auto response = send_sync<Messages::CompositorWebContentServer::CreateCanvas2dContext>(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<Messages::CompositorWebContentServer::GetCanvasPixels>(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<Web::Painting::CanvasId> CompositorConnection::create_webgl_context(Web::WebGL::WebGLVersion webgl_version, Gfx::IntSize size, bool depth, bool stencil, bool antialias, Vector<String>& out_supported_extensions)
{
if (!can_send_message_to_compositor())
return {};
auto response = send_sync<Messages::CompositorWebContentServer::CreateWebglContext>(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<Gfx::DecodedImageFrame> 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<Messages::CompositorWebContentServer::WebglSyncCall>(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<Messages::CompositorWebContentServer::WebglReadPixels>(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<Messages::CompositorWebContentServer::WebglReadBufferSubData>(canvas_id, target, offset, size, data);
}
void CompositorConnection::request_screenshot(Web::Compositor::CompositorContextId context_id, NonnullRefPtr<Gfx::PaintingSurface> target_surface, Function<void()>&& callback)
{
if (!can_send_message_to_compositor()) {

View file

@ -26,6 +26,7 @@
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
#include <LibWeb/Painting/ScrollState.h>
#include <LibWeb/WebGL/Types.h>
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<Web::Painting::CanvasId> 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<Gfx::PaintingSurface>, Function<void()>&&);
Optional<Web::Painting::CanvasId> create_webgl_context(Web::WebGL::WebGLVersion, Gfx::IntSize, bool depth, bool stencil, bool antialias, Vector<String>& out_supported_extensions);
void send_webgl_commands(Web::Painting::CanvasId, ByteBuffer const&, Vector<Gfx::DecodedImageFrame> 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<void(u64 page_id, Web::MouseEvent)> on_mouse_event;
private:

View file

@ -5,15 +5,110 @@
*/
#include <AK/NonnullOwnPtr.h>
#include <AK/Optional.h>
#include <LibGfx/CanvasCommandList.h>
#include <LibGfx/PaintingSurface.h>
#include <LibMedia/VideoFrame.h>
#include <LibWeb/Compositor/CompositorHost.h>
#include <LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h>
#include <LibWeb/WebGL/RemoteWebGLTransport.h>
#include <WebContent/CompositorConnection.h>
#include <WebContent/ConnectionFromClient.h>
#include <WebContent/WebContentCompositorHost.h>
namespace WebContent {
class WebContentRemoteWebGLTransport final : public Web::WebGL::RemoteWebGLTransport {
public:
explicit WebContentRemoteWebGLTransport(NonnullRefPtr<CompositorConnection> 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<Gfx::DecodedImageFrame> 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<CompositorConnection> m_connection;
};
class WebContentRemoteCanvas2DTransport final : public Web::HTML::RemoteCanvas2DTransport {
public:
explicit WebContentRemoteCanvas2DTransport(NonnullRefPtr<CompositorConnection> connection)
: m_connection(move(connection))
{
}
private:
virtual Optional<Web::Painting::CanvasId> 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<Gfx::Bitmap> 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<CompositorConnection> m_connection;
};
class WebContentCompositorHost final : public Web::Compositor::CompositorHost {
public:
explicit WebContentCompositorHost(ConnectionFromClient& client)
@ -22,6 +117,20 @@ public:
}
private:
virtual RefPtr<Web::WebGL::RemoteWebGLTransport> create_webgl_transport() override
{
if (auto* connection = compositor_connection())
return adopt_ref(*new WebContentRemoteWebGLTransport(*connection));
return nullptr;
}
virtual RefPtr<Web::HTML::RemoteCanvas2DTransport> 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())