Meta+LibWeb: Generate WebGL command proxy scaffolding

Moving WebGL execution into the Compositor needs a serializable command
stream and a client-side proxy that can queue commands before sending
them over IPC. The existing generator metadata only described direct GL
wrappers, so generated code could not distinguish async commands from
sync calls or object factory methods.

This teaches the WebGL metadata and generators about command streams and
adds the unused LibWeb proxy/list types. No rendering behavior changes
yet; the later host wiring can build on these generated interfaces
without mixing the metadata churn into that commit.
This commit is contained in:
Aliaksandr Kalenik 2026-06-15 19:27:13 +02:00 committed by Alexander Kalenik
parent af8b41e1cb
commit 9a8a7798ec
13 changed files with 2309 additions and 395 deletions

View file

@ -1155,7 +1155,9 @@ set(SOURCES
WebGL/WebGL2RenderingContextOverloads.cpp
WebGL/WebGLActiveInfo.cpp
WebGL/WebGLBuffer.cpp
WebGL/WebGLCommandList.cpp
WebGL/WebGLContextAttributes.cpp
WebGL/WebGLContextProxyBase.cpp
WebGL/WebGLContextEvent.cpp
WebGL/WebGLFramebuffer.cpp
WebGL/WebGLObject.cpp
@ -1256,6 +1258,8 @@ set(GENERATED_SOURCES
HTML/MediaControlsDOM.cpp
HTML/Parser/NamedCharacterReferences.cpp
WebGL/GLFunctions.cpp
WebGL/WebGLCommands.cpp
WebGL/WebGLContextProxy.cpp
)
ladybird_lib(LibWeb web EXPLICIT_SYMBOL_EXPORT)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/RefCounted.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/Forward.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibWeb/Compositor/Types.h>
#include <LibWeb/Export.h>
#include <LibWeb/Painting/DisplayListResourceIds.h>
#include <LibWeb/WebGL/Types.h>
namespace Web::WebGL {
class WEB_API RemoteWebGLTransport : public RefCounted<RemoteWebGLTransport> {
public:
virtual ~RemoteWebGLTransport() = default;
struct CreateResult {
bool success { false };
Painting::CanvasId canvas_id { 0 };
Vector<String> supported_extensions;
};
virtual CreateResult create_context(WebGLVersion, Gfx::IntSize initial_size, bool depth, bool stencil, bool antialias) = 0;
virtual void destroy_context(Painting::CanvasId) = 0;
virtual void send_commands(Painting::CanvasId, ByteBuffer const&, Vector<Gfx::DecodedImageFrame> const& bitmaps) = 0;
virtual void present_canvas(Painting::CanvasId, bool preserve_drawing_buffer) = 0;
virtual ByteBuffer sync_call(Painting::CanvasId, ByteBuffer request) = 0;
virtual ReadPixelsResult read_pixels_robust_angle(Painting::CanvasId, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei buf_size, Core::AnonymousBuffer pixels) = 0;
virtual void read_buffer_sub_data(Painting::CanvasId, GLenum target, GLintptr offset, GLintptr size, Core::AnonymousBuffer data) = 0;
virtual Gfx::ShareableBitmap read_back_drawing_buffer(Painting::CanvasId, Gfx::IntRect const&) = 0;
};
}

View file

@ -22,4 +22,24 @@ using GLchar = char;
// Since this conflicts with the original definition of GLsync, the suffix "Internal" has been added.
using GLsyncInternal = void*;
enum class WebGLVersion {
WebGL1,
WebGL2,
};
static constexpr int max_webgl_drawing_buffer_dimension = 16384;
using WebGLObjectId = u32;
struct WebGLDataSpan {
u32 offset { 0 };
u32 size { 0 };
};
struct ReadPixelsResult {
GLsizei length { 0 };
GLsizei columns { 0 };
GLsizei rows { 0 };
};
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/NumericLimits.h>
#include <LibWeb/WebGL/WebGLCommandList.h>
namespace Web::WebGL {
static size_t payload_layout_size(ReadonlyBytes payload, ReadonlyBytes inline_data, ReadonlyBytes more_inline_data = {})
{
auto size = payload.size();
if (!inline_data.is_empty())
size = align_up_to(size, WebGLCommandList::command_alignment) + inline_data.size();
if (!more_inline_data.is_empty())
size = align_up_to(size, WebGLCommandList::command_alignment) + more_inline_data.size();
return size;
}
static void write_payload(Bytes destination, ReadonlyBytes payload, ReadonlyBytes inline_data, ReadonlyBytes more_inline_data = {})
{
__builtin_memcpy(destination.data(), payload.data(), payload.size());
auto cursor = payload.size();
for (auto blob : { inline_data, more_inline_data }) {
if (blob.is_empty())
continue;
auto offset = align_up_to(cursor, WebGLCommandList::command_alignment);
__builtin_memset(destination.offset_pointer(cursor), 0, offset - cursor);
__builtin_memcpy(destination.offset_pointer(offset), blob.data(), blob.size());
cursor = offset + blob.size();
}
__builtin_memset(destination.offset_pointer(cursor), 0, destination.size() - cursor);
}
void WebGLCommandList::append_bytes(WebGLCommandType type, ReadonlyBytes payload, ReadonlyBytes inline_data)
{
VERIFY(m_bytes.size() % command_alignment == 0);
auto record_size = sizeof(WebGLCommandHeader) + payload_layout_size(payload, inline_data);
auto padded_record_size = align_up_to(record_size, command_alignment);
auto padded_payload_size = padded_record_size - sizeof(WebGLCommandHeader);
VERIFY(padded_payload_size <= NumericLimits<u32>::max());
WebGLCommandHeader header {
.type = type,
.payload_size = static_cast<u32>(padded_payload_size),
};
auto record_offset = m_bytes.size();
m_bytes.resize(record_offset + padded_record_size);
auto record = m_bytes.bytes().slice(record_offset);
__builtin_memcpy(record.data(), &header, sizeof(header));
write_payload(record.slice(sizeof(header)), payload, inline_data);
}
ByteBuffer WebGLSyncCall::encode_request_bytes(WebGLSyncCallType type, ReadonlyBytes request, ReadonlyBytes inline_data)
{
auto padded_payload_size = align_up_to(payload_layout_size(request, inline_data), WebGLCommandList::command_alignment);
VERIFY(padded_payload_size <= NumericLimits<u32>::max());
WebGLSyncCallHeader header {
.type = type,
.payload_size = static_cast<u32>(padded_payload_size),
};
auto bytes = MUST(ByteBuffer::create_uninitialized(sizeof(header) + padded_payload_size));
__builtin_memcpy(bytes.data(), &header, sizeof(header));
write_payload(bytes.bytes().slice(sizeof(header)), request, inline_data);
return bytes;
}
ByteBuffer WebGLSyncCall::encode_reply_bytes(ReadonlyBytes reply, ReadonlyBytes inline_data, ReadonlyBytes more_inline_data)
{
VERIFY(more_inline_data.is_empty() || !inline_data.is_empty());
auto reply_size = align_up_to(payload_layout_size(reply, inline_data, more_inline_data), WebGLCommandList::command_alignment);
auto bytes = MUST(ByteBuffer::create_uninitialized(reply_size));
write_payload(bytes, reply, inline_data, more_inline_data);
return bytes;
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Assertions.h>
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Span.h>
#include <AK/StdLibExtras.h>
#include <LibWeb/Export.h>
#include <LibWeb/WebGL/WebGLCommands.h>
namespace Web::WebGL {
struct WebGLCommandHeader {
WebGLCommandType type;
u32 payload_size { 0 }; // command struct + inline data + trailing padding
};
static_assert(IsTriviallyCopyable<WebGLCommandHeader>);
class WEB_API WebGLCommandList {
public:
static constexpr size_t command_alignment = 16;
static constexpr u32 first_inline_data_offset(size_t command_size)
{
return static_cast<u32>(align_up_to(command_size, command_alignment));
}
static constexpr u32 next_inline_data_offset(WebGLDataSpan previous)
{
return static_cast<u32>(align_up_to(previous.offset + previous.size, command_alignment));
}
template<typename Command>
void append(Command const& command, ReadonlyBytes inline_data = {})
{
append_bytes(Command::command_type, { &command, sizeof(command) }, inline_data);
}
void append_bytes(WebGLCommandType, ReadonlyBytes payload, ReadonlyBytes inline_data);
template<typename Callback>
static ErrorOr<void> for_each_command(ReadonlyBytes bytes, Callback&& callback)
{
size_t offset = 0;
while (offset < bytes.size()) {
if (bytes.size() - offset < sizeof(WebGLCommandHeader))
return Error::from_string_literal("Truncated WebGL command header");
WebGLCommandHeader header;
__builtin_memcpy(&header, bytes.offset_pointer(offset), sizeof(header));
if (to_underlying(header.type) >= webgl_command_type_count)
return Error::from_string_literal("Invalid WebGL command type");
if (header.payload_size > bytes.size() - offset - sizeof(header))
return Error::from_string_literal("Truncated WebGL command payload");
auto payload = bytes.slice(offset + sizeof(header), header.payload_size);
TRY(visit_webgl_command_type(header.type, [&]<typename Command>() -> ErrorOr<void> {
if (payload.size() < sizeof(Command))
return Error::from_string_literal("WebGL command payload too small");
Command command;
__builtin_memcpy(&command, payload.data(), sizeof(Command));
return callback(command, payload);
}));
offset += sizeof(WebGLCommandHeader) + header.payload_size;
}
return {};
}
static ReadonlyBytes resolve_data_span(ReadonlyBytes payload, WebGLDataSpan span)
{
VERIFY(span.offset <= payload.size());
VERIFY(span.size <= payload.size() - span.offset);
return payload.slice(span.offset, span.size);
}
template<typename T>
static Span<T const> resolve_typed_span(ReadonlyBytes payload, WebGLDataSpan span)
{
auto bytes = resolve_data_span(payload, span);
VERIFY(reinterpret_cast<uintptr_t>(bytes.data()) % alignof(T) == 0);
VERIFY(bytes.size() % sizeof(T) == 0);
return Span<T const> { reinterpret_cast<T const*>(bytes.data()), bytes.size() / sizeof(T) };
}
static ReadonlyBytes resolve_string_span(ReadonlyBytes payload, WebGLDataSpan span)
{
auto bytes = resolve_data_span(payload, span);
VERIFY(!bytes.is_empty());
VERIFY(bytes[bytes.size() - 1] == 0);
return bytes;
}
static void copy_data_span(ReadonlyBytes payload, WebGLDataSpan span, Bytes destination)
{
auto resolved = resolve_data_span(payload, span);
VERIFY(resolved.size() <= destination.size());
__builtin_memcpy(destination.data(), resolved.data(), resolved.size());
}
ReadonlyBytes bytes() const { return m_bytes; }
ByteBuffer const& buffer() const { return m_bytes; }
void clear_with_capacity() { m_bytes.set_size(0); }
bool is_empty() const { return m_bytes.is_empty(); }
size_t size_in_bytes() const { return m_bytes.size(); }
private:
ByteBuffer m_bytes;
};
struct WebGLSyncCallHeader {
WebGLSyncCallType type;
u32 payload_size { 0 };
};
static_assert(IsTriviallyCopyable<WebGLSyncCallHeader>);
class WEB_API WebGLSyncCall {
public:
template<typename Call>
static ByteBuffer encode_request(typename Call::Request const& request, ReadonlyBytes inline_data = {})
{
return encode_request_bytes(Call::call_type, { &request, sizeof(request) }, inline_data);
}
template<typename Callback>
static ErrorOr<ByteBuffer> dispatch_request(ReadonlyBytes bytes, Callback&& callback)
{
if (bytes.size() < sizeof(WebGLSyncCallHeader))
return Error::from_string_literal("Truncated WebGL sync call header");
WebGLSyncCallHeader header;
__builtin_memcpy(&header, bytes.data(), sizeof(header));
if (to_underlying(header.type) >= webgl_sync_call_type_count)
return Error::from_string_literal("Invalid WebGL sync call type");
if (header.payload_size != bytes.size() - sizeof(header))
return Error::from_string_literal("Truncated WebGL sync call payload");
auto payload = bytes.slice(sizeof(header), header.payload_size);
return visit_webgl_sync_call_type(header.type, [&]<typename Call>() -> ErrorOr<ByteBuffer> {
if (payload.size() < sizeof(typename Call::Request))
return Error::from_string_literal("WebGL sync call payload too small");
typename Call::Request request;
__builtin_memcpy(&request, payload.data(), sizeof(request));
return callback.template operator()<Call>(request, payload);
});
}
template<typename Reply>
static ByteBuffer encode_reply(Reply const& reply, ReadonlyBytes inline_data = {}, ReadonlyBytes more_inline_data = {})
{
return encode_reply_bytes({ &reply, sizeof(reply) }, inline_data, more_inline_data);
}
template<typename Reply>
static Reply decode_reply(ReadonlyBytes bytes)
{
VERIFY(bytes.size() >= sizeof(Reply));
Reply reply;
__builtin_memcpy(&reply, bytes.data(), sizeof(reply));
return reply;
}
private:
static ByteBuffer encode_request_bytes(WebGLSyncCallType, ReadonlyBytes request, ReadonlyBytes inline_data);
static ByteBuffer encode_reply_bytes(ReadonlyBytes reply, ReadonlyBytes inline_data, ReadonlyBytes more_inline_data);
};
}

View file

@ -0,0 +1,258 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/PaintingSurface.h>
#include <LibWeb/WebGL/WebGLContextProxy.h>
#include <LibWeb/WebGL/WebGLContextProxyBase.h>
namespace Web::WebGL {
WebGLContextProxyBase::WebGLContextProxyBase(NonnullRefPtr<RemoteWebGLTransport> transport, Painting::CanvasId canvas_id, WebGLVersion webgl_version, Vector<String> supported_extensions)
: m_transport(move(transport))
, m_canvas_id(canvas_id)
, m_webgl_version(webgl_version)
, m_supported_extensions(move(supported_extensions))
{
}
WebGLContextProxyBase::~WebGLContextProxyBase()
{
m_transport->destroy_context(m_canvas_id);
}
void WebGLContextProxyBase::flush_commands()
{
if (m_commands.is_empty())
return;
m_transport->send_commands(m_canvas_id, m_commands.buffer(), m_pending_bitmaps);
m_commands.clear_with_capacity();
m_pending_bitmaps.clear_with_capacity();
}
ByteBuffer WebGLContextProxyBase::send_sync_call(ByteBuffer request)
{
if (m_lost)
return {};
flush_commands();
return m_transport->sync_call(m_canvas_id, move(request));
}
ReadPixelsResult WebGLContextProxyBase::read_pixels_robust_angle_into_shared_buffer(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei buf_size, Core::AnonymousBuffer const& pixels)
{
flush_commands();
return m_transport->read_pixels_robust_angle(m_canvas_id, x, y, width, height, format, type, buf_size, pixels);
}
void WebGLContextProxyBase::set_size(Gfx::IntSize const& size)
{
record(Commands::SetDrawingBufferSize { .width = size.width(), .height = size.height() });
}
void WebGLContextProxyBase::present_canvas_for_compositing(bool preserve_drawing_buffer)
{
flush_commands();
m_transport->present_canvas(m_canvas_id, preserve_drawing_buffer);
}
RefPtr<Gfx::Bitmap> WebGLContextProxyBase::read_back_drawing_buffer(Gfx::IntRect const& rect)
{
if (m_lost)
return nullptr;
flush_commands();
auto bitmap = m_transport->read_back_drawing_buffer(m_canvas_id, rect);
if (!bitmap.is_valid())
return nullptr;
return bitmap.bitmap();
}
void WebGLContextProxyBase::read_pixels_into_pixel_pack_buffer(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, long long offset)
{
record(Commands::ReadPixelsIntoPixelPackBuffer {
.x = x,
.y = y,
.width = width,
.height = height,
.format = format,
.type = type,
.offset = static_cast<GLintptr>(offset),
});
}
void WebGLContextProxyBase::tex_image2d_from_bitmap(GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, Gfx::DecodedImageFrame frame, Optional<Gfx::IntSize> destination_size, bool flip_y, bool premultiply_alpha)
{
if (m_lost)
return;
auto bitmap_index = static_cast<u32>(m_pending_bitmaps.size());
m_pending_bitmaps.append(move(frame));
auto has_explicit_destination_size = destination_size.has_value();
record(Commands::TexImage2DFromBitmap {
.target = target,
.level = level,
.internalformat = internalformat,
.format = format,
.type = type,
.bitmap_index = bitmap_index,
.has_explicit_destination_size = has_explicit_destination_size,
.destination_width = has_explicit_destination_size ? destination_size->width() : 0,
.destination_height = has_explicit_destination_size ? destination_size->height() : 0,
.flip_y = flip_y,
.premultiply_alpha = premultiply_alpha,
});
}
void WebGLContextProxyBase::tex_sub_image2d_from_bitmap(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, Gfx::DecodedImageFrame frame, Optional<Gfx::IntSize> destination_size, bool flip_y, bool premultiply_alpha)
{
if (m_lost)
return;
auto bitmap_index = static_cast<u32>(m_pending_bitmaps.size());
m_pending_bitmaps.append(move(frame));
auto has_explicit_destination_size = destination_size.has_value();
record(Commands::TexSubImage2DFromBitmap {
.target = target,
.level = level,
.xoffset = xoffset,
.yoffset = yoffset,
.format = format,
.type = type,
.bitmap_index = bitmap_index,
.has_explicit_destination_size = has_explicit_destination_size,
.destination_width = has_explicit_destination_size ? destination_size->width() : 0,
.destination_height = has_explicit_destination_size ? destination_size->height() : 0,
.flip_y = flip_y,
.premultiply_alpha = premultiply_alpha,
});
}
void WebGLContextProxyBase::read_buffer_sub_data(GLenum target, long long offset, Bytes destination)
{
if (m_lost || destination.is_empty())
return;
auto shared_data_or_error = Core::AnonymousBuffer::create_with_size(destination.size());
auto shared_data = shared_data_or_error.release_value_but_fixme_should_propagate_errors();
flush_commands();
m_transport->read_buffer_sub_data(m_canvas_id, target, static_cast<GLintptr>(offset), static_cast<GLintptr>(destination.size()), shared_data);
if (m_lost)
return;
__builtin_memcpy(destination.data(), shared_data.data<void>(), destination.size());
}
void WebGLContextProxy::shader_source(GLuint shader, GLsizei count, GLchar const* const* string, GLint const* length)
{
VERIFY(count == 1);
auto source_length = length ? static_cast<size_t>(length[0]) : __builtin_strlen(string[0]);
ByteBuffer source_bytes = MUST(ByteBuffer::create_uninitialized(source_length + 1));
__builtin_memcpy(source_bytes.data(), string[0], source_length);
source_bytes[source_length] = 0;
Commands::ShaderSource command { .shader = shader, .source = {} };
command.source = { WebGLCommandList::first_inline_data_offset(sizeof(command)), static_cast<u32>(source_bytes.size()) };
record(command, source_bytes);
}
static ByteBuffer pack_strings(GLsizei count, GLchar const* const* strings)
{
StringBuilder builder;
for (GLsizei i = 0; i < count; ++i) {
builder.append({ strings[i], __builtin_strlen(strings[i]) });
builder.append('\0');
}
return MUST(builder.to_byte_buffer());
}
void WebGLContextProxy::transform_feedback_varyings(GLuint program, GLsizei count, GLchar const* const* varyings, GLenum bufferMode)
{
auto varyings_bytes = pack_strings(count, varyings);
Commands::TransformFeedbackVaryings command { .program = program, .count = count, .varyings = {}, .buffer_mode = bufferMode };
command.varyings = { WebGLCommandList::first_inline_data_offset(sizeof(command)), static_cast<u32>(varyings_bytes.size()) };
record(command, varyings_bytes);
}
void WebGLContextProxy::read_pixels_robust_angle(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, GLsizei* length, GLsizei* columns, GLsizei* rows, void* pixels)
{
if (is_lost())
return;
Core::AnonymousBuffer shared_pixels;
if (bufSize > 0) {
shared_pixels = Core::AnonymousBuffer::create_with_size(static_cast<size_t>(bufSize)).release_value_but_fixme_should_propagate_errors();
}
auto result = read_pixels_robust_angle_into_shared_buffer(x, y, width, height, format, type, bufSize, shared_pixels);
if (is_lost())
return;
if (length)
*length = result.length;
if (columns)
*columns = result.columns;
if (rows)
*rows = result.rows;
if (pixels && result.length > 0) {
VERIFY(result.length <= bufSize);
__builtin_memcpy(pixels, shared_pixels.data<void>(), static_cast<size_t>(result.length));
}
}
GLubyte const* WebGLContextProxy::get_string(GLenum name)
{
if (auto cached = m_string_cache.get(name); cached.has_value())
return cached.value()->data();
SyncCalls::GetString::Request request { .name = name };
auto reply_bytes = send_sync_call(WebGLSyncCall::encode_request<SyncCalls::GetString>(request));
if (is_lost())
return reinterpret_cast<GLubyte const*>("");
auto reply = WebGLSyncCall::decode_reply<SyncCalls::GetString::Reply>(reply_bytes);
auto resolved = WebGLCommandList::resolve_string_span(reply_bytes, reply.value);
auto value = make<ByteBuffer>(MUST(ByteBuffer::copy(resolved)));
auto const* data = value->data();
m_string_cache.set(name, move(value));
return data;
}
void WebGLContextProxy::get_vertex_attrib_pointerv_robust_angle(GLuint index, GLenum pname, GLsizei bufSize, GLsizei* length, void** pointer)
{
(void)bufSize;
SyncCalls::GetVertexAttribPointervRobustANGLE::Request request { .index = index, .pname = pname };
auto reply_bytes = send_sync_call(WebGLSyncCall::encode_request<SyncCalls::GetVertexAttribPointervRobustANGLE>(request));
if (is_lost())
return;
auto reply = WebGLSyncCall::decode_reply<SyncCalls::GetVertexAttribPointervRobustANGLE::Reply>(reply_bytes);
if (length)
*length = 1;
if (pointer)
*pointer = reinterpret_cast<void*>(static_cast<uintptr_t>(reply.pointer));
}
void WebGLContextProxy::get_uniform_indices(GLuint program, GLsizei uniformCount, GLchar const* const* uniformNames, GLuint* uniformIndices)
{
auto names_bytes = pack_strings(uniformCount, uniformNames);
SyncCalls::GetUniformIndices::Request request { .program = program, .uniform_count = uniformCount, .uniform_names = {} };
request.uniform_names = { WebGLCommandList::first_inline_data_offset(sizeof(request)), static_cast<u32>(names_bytes.size()) };
auto reply_bytes = send_sync_call(WebGLSyncCall::encode_request<SyncCalls::GetUniformIndices>(request, names_bytes));
if (is_lost())
return;
auto reply = WebGLSyncCall::decode_reply<SyncCalls::GetUniformIndices::Reply>(reply_bytes);
if (uniformIndices)
WebGLCommandList::copy_data_span(reply_bytes, reply.uniform_indices, { uniformIndices, static_cast<size_t>(uniformCount) * sizeof(GLuint) });
}
void* WebGLContextProxy::map_buffer_range(GLenum, GLintptr, GLsizeiptr, GLbitfield)
{
// getBufferSubData() goes through read_buffer_sub_data() instead; nothing else maps.
VERIFY_NOT_REACHED();
}
GLboolean WebGLContextProxy::unmap_buffer(GLenum)
{
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/HashMap.h>
#include <AK/Noncopyable.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Size.h>
#include <LibWeb/Compositor/Types.h>
#include <LibWeb/Export.h>
#include <LibWeb/Painting/DisplayListResourceIds.h>
#include <LibWeb/WebGL/RemoteWebGLTransport.h>
#include <LibWeb/WebGL/WebGLCommandList.h>
namespace Web::WebGL {
class WEB_API WebGLContextProxyBase {
AK_MAKE_NONCOPYABLE(WebGLContextProxyBase);
AK_MAKE_NONMOVABLE(WebGLContextProxyBase);
public:
WebGLContextProxyBase(NonnullRefPtr<RemoteWebGLTransport>, Painting::CanvasId, WebGLVersion, Vector<String> supported_extensions);
~WebGLContextProxyBase();
void flush_commands();
Painting::CanvasId canvas_id() const { return m_canvas_id; }
void make_current() { }
void notify_content_will_change() { }
u32 default_framebuffer() const { return 0; }
u32 default_renderbuffer() const { return 0; }
WebGLVersion webgl_version() const { return m_webgl_version; }
Vector<String> const& get_supported_opengl_extensions() const { return m_supported_extensions; }
void set_size(Gfx::IntSize const&);
void present_canvas_for_compositing(bool preserve_drawing_buffer);
RefPtr<Gfx::Bitmap> read_back_drawing_buffer(Gfx::IntRect const&);
void read_pixels_into_pixel_pack_buffer(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, long long offset);
void read_buffer_sub_data(GLenum target, long long offset, Bytes destination);
void tex_image2d_from_bitmap(GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, Gfx::DecodedImageFrame, Optional<Gfx::IntSize> destination_size, bool flip_y, bool premultiply_alpha);
void tex_sub_image2d_from_bitmap(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, Gfx::DecodedImageFrame, Optional<Gfx::IntSize> destination_size, bool flip_y, bool premultiply_alpha);
GLenum take_pending_local_error()
{
auto error = m_pending_local_error;
m_pending_local_error = 0;
return error;
}
protected:
static constexpr size_t max_pending_command_bytes = 4 * MiB;
WebGLObjectId allocate_object_id() { return m_next_object_id++; }
void set_pending_local_error(GLenum error)
{
if (m_pending_local_error == 0)
m_pending_local_error = error;
}
ReadPixelsResult read_pixels_robust_angle_into_shared_buffer(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei buf_size, Core::AnonymousBuffer const& pixels);
template<typename Command>
void record(Command const& command, ReadonlyBytes inline_data = {})
{
if (m_lost)
return;
m_commands.append(command, inline_data);
if (m_commands.size_in_bytes() >= max_pending_command_bytes)
flush_commands();
}
ByteBuffer send_sync_call(ByteBuffer request);
bool is_lost() const { return m_lost; }
HashMap<GLenum, NonnullOwnPtr<ByteBuffer>> m_string_cache;
private:
NonnullRefPtr<RemoteWebGLTransport> m_transport;
Painting::CanvasId m_canvas_id { 0 };
WebGLVersion m_webgl_version { WebGLVersion::WebGL1 };
Vector<String> m_supported_extensions;
WebGLCommandList m_commands;
Vector<Gfx::DecodedImageFrame> m_pending_bitmaps;
u32 m_next_object_id { 1 };
bool m_lost { false };
GLenum m_pending_local_error { 0 };
};
}

View file

@ -254,8 +254,30 @@ function (generate_webgl_implementation)
dependencies "${LADYBIRD_SOURCE_DIR}/Meta/Generators/libweb_webgl.py"
)
invoke_py_generator(
"WebGLCommands.cpp"
"generate_libweb_webgl_commands.py"
"${LIBWEB_INPUT_FOLDER}/WebGL/GLFunctions.json"
"WebGL/WebGLCommands.h"
"WebGL/WebGLCommands.cpp"
arguments -j "${LIBWEB_INPUT_FOLDER}/WebGL/GLFunctions.json"
dependencies "${LADYBIRD_SOURCE_DIR}/Meta/Generators/libweb_webgl.py"
)
invoke_py_generator(
"WebGLContextProxy.cpp"
"generate_libweb_webgl_proxy.py"
"${LIBWEB_INPUT_FOLDER}/WebGL/GLFunctions.json"
"WebGL/WebGLContextProxy.h"
"WebGL/WebGLContextProxy.cpp"
arguments -j "${LIBWEB_INPUT_FOLDER}/WebGL/GLFunctions.json"
dependencies "${LADYBIRD_SOURCE_DIR}/Meta/Generators/libweb_webgl.py"
)
set(WEBGL_GENERATED_HEADERS
"WebGL/GLFunctions.h"
"WebGL/WebGLCommands.h"
"WebGL/WebGLContextProxy.h"
)
list(TRANSFORM WEBGL_GENERATED_HEADERS PREPEND "${CMAKE_CURRENT_BINARY_DIR}/")
if (ENABLE_INSTALL_HEADERS)

View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import sys
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 command_stream_entries
from libweb_webgl import command_struct_fields
from libweb_webgl import is_wire_command
from libweb_webgl import is_wire_sync
from libweb_webgl import run_generator
from libweb_webgl import sync_call_entries
from libweb_webgl import sync_reply_fields
from libweb_webgl import sync_request_fields
# Generates the WebGL command stream vocabulary from GLFunctions.json: one opcode and one
# trivially-copyable struct per "command"/"gen" entry. The recorder (WebContent) and the
# replayer (Compositor) are generated from the same source of truth, so the wire format
# cannot drift between the two sides.
# The command and sync-call vocabularies share their X-macro shape; everything but the
# structs themselves is emitted by the helpers below.
def write_type_enum(out: TextIO, macro: str, enum_name: str, count_name: str, names: list) -> None:
out.write(f"#define {macro}(V) \\\n")
for name in names:
out.write(f" V({name}) \\\n")
out.write(f"""
enum class {enum_name} : u16 {{
#define __ENUMERATE(name) name,
{macro}(__ENUMERATE)
#undef __ENUMERATE
}};
inline constexpr u16 {count_name} = {len(names)};
WEB_API StringView to_string({enum_name});
""")
def write_visit_function(out: TextIO, macro: str, enum_name: str, function_name: str, namespace: str) -> None:
out.write(f"""
template<typename Callback>
decltype(auto) {function_name}({enum_name} type, Callback&& callback)
{{
switch (type) {{
#define __ENUMERATE(name) \\
case {enum_name}::name: \\
return callback.template operator()<{namespace}::name>();
{macro}(__ENUMERATE)
#undef __ENUMERATE
}}
VERIFY_NOT_REACHED();
}}
""")
def write_static_asserts(out: TextIO, macro: str, assert_body: str) -> None:
out.write(f"""
#define __ENUMERATE(name) {assert_body}
{macro}(__ENUMERATE)
#undef __ENUMERATE
""")
def write_to_string_implementation(out: TextIO, macro: str, enum_name: str) -> None:
out.write(f"""
StringView to_string({enum_name} type)
{{
switch (type) {{
#define __ENUMERATE(name) \\
case {enum_name}::name: \\
return #name##sv;
{macro}(__ENUMERATE)
#undef __ENUMERATE
}}
VERIFY_NOT_REACHED();
}}
""")
def write_header_file(out: TextIO, functions: list) -> None:
commands = command_stream_entries(functions)
sync_calls = sync_call_entries(functions)
out.write("""#pragma once
#include <AK/Assertions.h>
#include <AK/StdLibExtras.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <LibWeb/Export.h>
#include <LibWeb/WebGL/GLFunctions.h>
namespace Web::WebGL {
""")
write_type_enum(
out,
"ENUMERATE_WEBGL_COMMANDS",
"WebGLCommandType",
"webgl_command_type_count",
[command_name(f) for f in commands],
)
out.write("\nnamespace Commands {\n")
for f in commands:
name = command_name(f)
out.write(f"\nstruct {name} {{\n")
out.write(f" static constexpr auto command_type = WebGLCommandType::{name};\n")
if is_wire_command(f):
for field in f["wire_command"]:
out.write(f" {field['type']} {field['name']} {{}};\n")
else:
for cpp_type, field_name, _ in command_struct_fields(f):
out.write(f" {cpp_type} {field_name} {{}};\n")
out.write("};\n")
out.write("\n}\n")
write_visit_function(out, "ENUMERATE_WEBGL_COMMANDS", "WebGLCommandType", "visit_webgl_command_type", "Commands")
write_static_asserts(out, "ENUMERATE_WEBGL_COMMANDS", "static_assert(IsTriviallyCopyable<Commands::name>);")
out.write("\n")
write_type_enum(
out,
"ENUMERATE_WEBGL_SYNC_CALLS",
"WebGLSyncCallType",
"webgl_sync_call_type_count",
[command_name(f) for f in sync_calls],
)
out.write("\nnamespace SyncCalls {\n")
for f in sync_calls:
name = command_name(f)
if is_wire_sync(f):
request_fields = [(field["type"], field["name"]) for field in f["wire_request"]]
reply_fields = [(field["type"], field["name"]) for field in f["wire_reply"]]
else:
request_fields = [(cpp_type, field_name) for cpp_type, field_name, _ in sync_request_fields(f)]
reply_fields = [(cpp_type, field_name) for cpp_type, field_name, _ in sync_reply_fields(f)]
out.write(f"\nstruct {name} {{\n")
out.write(f" static constexpr auto call_type = WebGLSyncCallType::{name};\n")
out.write(" struct Request {\n")
for cpp_type, field_name in request_fields:
out.write(f" {cpp_type} {field_name} {{}};\n")
out.write(" };\n")
out.write(" struct Reply {\n")
for cpp_type, field_name in reply_fields:
out.write(f" {cpp_type} {field_name} {{}};\n")
out.write(" };\n")
out.write("};\n")
out.write("\n}\n")
write_visit_function(
out, "ENUMERATE_WEBGL_SYNC_CALLS", "WebGLSyncCallType", "visit_webgl_sync_call_type", "SyncCalls"
)
write_static_asserts(
out,
"ENUMERATE_WEBGL_SYNC_CALLS",
"static_assert(IsTriviallyCopyable<SyncCalls::name::Request>); static_assert(IsTriviallyCopyable<SyncCalls::name::Reply>);",
)
out.write("\n}\n")
def write_implementation_file(out: TextIO, functions: list) -> None:
out.write("""#include <LibWeb/WebGL/WebGLCommands.h>
namespace Web::WebGL {
""")
write_to_string_implementation(out, "ENUMERATE_WEBGL_COMMANDS", "WebGLCommandType")
write_to_string_implementation(out, "ENUMERATE_WEBGL_SYNC_CALLS", "WebGLSyncCallType")
out.write("\n}\n")
if __name__ == "__main__":
run_generator("Generate WebGL command stream vocabulary", write_header_file, write_implementation_file)

View file

@ -42,6 +42,8 @@ public:
""")
for function in functions:
if function["category"].startswith("builtin"):
continue
out.write(f" {method_signature(function)};\n")
out.write("""};
@ -65,6 +67,8 @@ namespace Web::WebGL {
""")
for function in functions:
if function["category"].startswith("builtin"):
continue
forwarded_args = ", ".join(arg["name"] for arg in function["args"])
call = f"::{function['name']}({forwarded_args})"
if function["return"] != "void":

View file

@ -0,0 +1,227 @@
#!/usr/bin/env python3
# Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import sys
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 is_const_pointer
from libweb_webgl import is_pointer
from libweb_webgl import method_signature
from libweb_webgl import run_generator
from libweb_webgl import snake_case
from libweb_webgl import sync_reply_fields
# Generates WebGLContextProxy, WebContent's drop-in replacement for the GL seam: the
# same method signatures as the generated GLFunctions class, but recording commands into
# a WebGLCommandList (flushed to the Compositor) instead of calling GL. Value-returning
# entry points become synchronous round trips. Method parameter names match the GL
# argument names from GLFunctions.json, so the JSON's payload-size expressions are used
# verbatim.
def object_id_expression(arg: dict) -> str:
if arg["type"] == "GLsync":
return f"static_cast<WebGLObjectId>(reinterpret_cast<uintptr_t>({arg['name']}))"
return arg["name"]
# Emits statements declaring `<field>_bytes` spans and a fully-initialized `command`
# whose WebGLDataSpan fields point at them in append order.
def emit_command_construction(out: TextIO, function: dict, command_type: str) -> list:
blobs = [] # (field, bytes_variable)
initializers = []
for arg in function["args"]:
field = snake_case(arg["name"])
name = arg["name"]
if arg.get("string"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, __builtin_strlen({name}) + 1 }};\n")
blobs.append(field)
initializers.append((field, None))
elif arg.get("offset"):
initializers.append((field, f"static_cast<GLintptr>(reinterpret_cast<uintptr_t>({name}))"))
elif arg.get("object") and not is_pointer(arg):
initializers.append((field, object_id_expression(arg)))
elif (arg.get("object") and is_const_pointer(arg)) or "payload" in arg:
size = f"static_cast<size_t>({arg['payload']})"
if arg.get("nullable"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, {name} ? {size} : 0 }};\n")
initializers.append((f"has_{field}", f"{name} != nullptr"))
else:
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, {size} }};\n")
blobs.append(field)
initializers.append((field, None))
else:
initializers.append((field, name))
fields = ", ".join(f".{field} = {value}" for field, value in initializers if value is not None)
out.write(f" {command_type} command {{ {fields} }};\n")
for index, field in enumerate(blobs):
if index == 0:
offset = "WebGLCommandList::first_inline_data_offset(sizeof(command))"
else:
offset = f"WebGLCommandList::next_inline_data_offset(command.{blobs[index - 1]})"
out.write(f" command.{field} = {{ {offset}, static_cast<u32>({field}_bytes.size()) }};\n")
return blobs
def emit_record(out: TextIO, blobs: list) -> None:
blob_args = "".join(f", {field}_bytes" for field in blobs)
out.write(f" record(command{blob_args});\n")
def emit_command_method(out: TextIO, function: dict) -> None:
command_type = f"Commands::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
blobs = emit_command_construction(out, function, command_type)
assert len(blobs) <= 1, f"{function['name']} needs more inline blobs than record() supports"
emit_record(out, blobs)
out.write("}\n\n")
def emit_gen_method(out: TextIO, function: dict) -> None:
command_type = f"Commands::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
if function["return"] != "void":
scalars = "".join(f", .{snake_case(a['name'])} = {a['name']}" for a in function["args"])
out.write(" auto id = allocate_object_id();\n")
out.write(f" record({command_type} {{ .id = id{scalars} }});\n")
if function["return"] == "GLsync":
out.write(" return reinterpret_cast<GLsync>(static_cast<uintptr_t>(id));\n")
else:
out.write(" return id;\n")
out.write("}\n\n")
return
count_name = function["args"][0]["name"]
span_field = snake_case(function["args"][1]["name"])
out_name = function["args"][1]["name"]
out.write(f""" if ({count_name} <= 0)
return;
static_assert(IsSame<WebGLObjectId, GLuint>);
for (GLsizei i = 0; i < {count_name}; ++i)
{out_name}[i] = allocate_object_id();
ReadonlyBytes {span_field}_bytes {{ {out_name}, static_cast<size_t>({count_name}) * sizeof(WebGLObjectId) }};
Commands::{command_name(function)} command {{ .{snake_case(count_name)} = {count_name} }};
command.{span_field} = {{ WebGLCommandList::first_inline_data_offset(sizeof(command)), static_cast<u32>({span_field}_bytes.size()) }};
record(command, {span_field}_bytes);
}}
""")
def emit_sync_method(out: TextIO, function: dict) -> None:
call = f"SyncCalls::{command_name(function)}"
out.write(f"{method_signature(function, 'WebGLContextProxy::')}\n{{\n")
# Build the request (in-args only) plus its inline blobs.
blobs = []
initializers = []
for arg in function["args"]:
if arg.get("out"):
continue
field = snake_case(arg["name"])
name = arg["name"]
if arg.get("string"):
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, __builtin_strlen({name}) + 1 }};\n")
blobs.append(field)
elif "payload" in arg:
out.write(f" ReadonlyBytes {field}_bytes {{ {name}, static_cast<size_t>({arg['payload']}) }};\n")
blobs.append(field)
elif arg.get("object") and not is_pointer(arg):
initializers.append((field, object_id_expression(arg)))
else:
initializers.append((field, name))
assert len(blobs) <= 1, f"{function['name']} needs more request blobs than encode_request supports"
fields = ", ".join(f".{field} = {value}" for field, value in initializers)
out.write(f" {call}::Request request {{ {fields} }};\n")
for field in blobs:
out.write(
f" request.{field} = {{ WebGLCommandList::first_inline_data_offset(sizeof(request)), static_cast<u32>({field}_bytes.size()) }};\n"
)
blob_argument = f", {blobs[0]}_bytes" if blobs else ""
out.write(
f" auto reply_bytes = send_sync_call(WebGLSyncCall::encode_request<{call}>(request{blob_argument}));\n"
)
has_return = function["return"] != "void"
failure = " return {};\n" if has_return else " return;\n"
out.write(" if (is_lost())\n")
out.write(failure)
if sync_reply_fields(function):
out.write(f" auto reply = WebGLSyncCall::decode_reply<{call}::Reply>(reply_bytes);\n")
for _, field_name, arg in sync_reply_fields(function):
if arg is None:
continue
name = arg["name"]
if "payload" in arg:
out.write(f""" if ({name})
WebGLCommandList::copy_data_span(reply_bytes, reply.{field_name}, {{ {name}, static_cast<size_t>({arg["payload"]}) }});
""")
else:
out.write(f" if ({name})\n *{name} = reply.{field_name};\n")
if has_return:
out.write(" return reply.return_value;\n")
out.write("}\n\n")
def write_header_file(out: TextIO, functions: list) -> None:
out.write("""#pragma once
#include <LibWeb/WebGL/WebGLCommands.h>
#include <LibWeb/WebGL/WebGLContextProxyBase.h>
namespace Web::WebGL {
// The remote-recording implementation of the GL seam: identical signatures to
// GLFunctions, so the WebGL implementation files cannot tell the difference.
class WEB_API WebGLContextProxy final : public WebGLContextProxyBase {
public:
using WebGLContextProxyBase::WebGLContextProxyBase;
""")
for function in functions:
if function["category"] not in ("command", "gen", "sync"):
continue
out.write(f" {method_signature(function)};\n")
out.write("""
// Custom-handled entry points; defined manually in WebGLContextProxyBase.cpp.
""")
for function in functions:
if function["category"] != "custom":
continue
out.write(f" {method_signature(function)};\n")
out.write("""};
}
""")
def write_implementation_file(out: TextIO, functions: list) -> None:
out.write("""#include <LibWeb/WebGL/WebGLContextProxy.h>
namespace Web::WebGL {
""")
for function in functions:
if function["category"] == "command":
emit_command_method(out, function)
elif function["category"] == "gen":
emit_gen_method(out, function)
elif function["category"] == "sync":
emit_sync_method(out, function)
out.write("}\n")
if __name__ == "__main__":
run_generator("Generate the WebGL context proxy recorder", write_header_file, write_implementation_file)

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: BSD-2-Clause
# Shared model and CLI driver for the WebGL generators: loads GLFunctions.json and
# derives the generated method name and signature of each GL entry point.
# derives the command-struct shape each annotated GL function serializes to.
import argparse
import json
@ -41,6 +41,8 @@ def run_generator(description: str, write_header_file, write_implementation_file
def command_name(function: dict) -> str:
if function["category"].startswith("builtin"):
return function["name"]
assert function["name"].startswith("gl")
return function["name"][2:]
@ -49,6 +51,104 @@ def method_name(function: dict) -> str:
return snake_case(command_name(function))
# The GLFunctions and WebGLContextProxy classes must stay signature-identical (the WebGL
# implementation cannot tell them apart), so both generators emit from this one helper.
def method_signature(function: dict, qualifier: str = "") -> str:
args = ", ".join(f"{arg['type']} {arg['name']}" for arg in function["args"])
return f"{function['return']} {qualifier}{method_name(function)}({args})"
# Entries carried by the command stream: regular commands and object creation (struct
# shapes derived from the GL signature) plus wire-specified ops (custom-handled GL
# functions and builtins, whose struct shapes are spelled out in the JSON).
def is_wire_command(function: dict) -> bool:
return "wire_command" in function
def is_wire_sync(function: dict) -> bool:
return "wire_request" in function
def command_stream_entries(functions: list) -> list:
return [f for f in functions if f["category"] in ("command", "gen") or is_wire_command(f)]
def sync_call_entries(functions: list) -> list:
return [f for f in functions if f["category"] == "sync" or is_wire_sync(f)]
def is_pointer(arg: dict) -> bool:
return arg["type"].endswith("*")
def is_const_pointer(arg: dict) -> bool:
return is_pointer(arg) and "const" in arg["type"]
def deref_type(pointer_type: str) -> str:
return pointer_type.replace("*", "").strip()
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of a synchronous
# call's request struct: every non-out argument, with object ids, strings, and input
# payloads in their wire representations.
def sync_request_fields(function: dict) -> list:
fields = []
for arg in function["args"]:
if arg.get("out"):
continue
field_name = snake_case(arg["name"])
if arg.get("object") and not is_pointer(arg):
fields.append(("WebGLObjectId", field_name, arg))
elif arg.get("string") or "payload" in arg:
fields.append(("WebGLDataSpan", field_name, arg))
else:
assert not is_pointer(arg), f"unhandled pointer arg {function['name']}.{arg['name']}"
fields.append((arg["type"], field_name, arg))
return fields
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of a synchronous
# call's reply struct: the return value, scalar outs by value, buffer outs as spans
# into the reply's inline data.
def sync_reply_fields(function: dict) -> list:
fields = []
if function["return"] != "void":
fields.append((function["return"], "return_value", None))
for arg in function["args"]:
if not arg.get("out"):
continue
field_name = snake_case(arg["name"])
if "payload" in arg:
fields.append(("WebGLDataSpan", field_name, arg))
else:
fields.append((deref_type(arg["type"]), field_name, arg))
return fields
# Returns the ordered (cpp_type, field_name, arg_or_none) triples of the
# trivially-copyable struct a command/gen function serializes to.
def command_struct_fields(function: dict) -> list:
fields = []
if function["category"] == "gen" and function["return"] != "void":
fields.append(("WebGLObjectId", "id", None))
for arg in function["args"]:
field_name = snake_case(arg["name"])
if function["category"] == "gen" and is_pointer(arg) and not is_const_pointer(arg):
fields.append(("WebGLDataSpan", field_name, arg)) # client-allocated ids
elif arg.get("object") and not is_pointer(arg):
fields.append(("WebGLObjectId", field_name, arg))
elif arg.get("object") and is_const_pointer(arg):
fields.append(("WebGLDataSpan", field_name, arg)) # array of client ids
elif arg.get("string"):
fields.append(("WebGLDataSpan", field_name, arg)) # NUL-terminated bytes
elif arg.get("offset"):
fields.append(("GLintptr", field_name, arg))
elif "payload" in arg:
if arg.get("nullable"):
fields.append(("bool", "has_" + field_name, arg))
fields.append(("WebGLDataSpan", field_name, arg))
else:
assert not is_pointer(arg), f"unhandled pointer arg {function['name']}.{arg['name']}"
fields.append((arg["type"], field_name, arg))
return fields