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.
351 lines
15 KiB
C++
351 lines
15 KiB
C++
/*
|
|
* 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);
|
|
}
|
|
|
|
}
|