From 1f22462f13f7f37af888c057634c96bf6688edb5 Mon Sep 17 00:00:00 2001 From: evan108108 Date: Fri, 27 Mar 2026 21:06:09 -0400 Subject: [PATCH 1/2] fix: cache canvas 2D context and lock context type per spec Per the HTML spec, HTMLCanvasElement.getContext() should: 1. Return the same object on repeated calls with the same type 2. Return null if a different context type was already requested Previously, every getContext("2d") call created a new CanvasRenderingContext2D object. This caused issues with code that relies on identity checks (ctx === canvas.getContext("2d")) and wasted memory by allocating duplicate contexts. The fix caches the 2D context and tracks which context type was first requested, returning null for incompatible subsequent calls. --- src/browser/webapi/element/html/Canvas.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/browser/webapi/element/html/Canvas.zig b/src/browser/webapi/element/html/Canvas.zig index 13002f66..c01fb651 100644 --- a/src/browser/webapi/element/html/Canvas.zig +++ b/src/browser/webapi/element/html/Canvas.zig @@ -30,6 +30,13 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig"); const Canvas = @This(); _proto: *HtmlElement, +/// Cached context type. Once set, requesting a different type returns null (per spec). +_context_type: ContextType = .none, +/// Cached 2D rendering context (same object returned on repeated getContext("2d") calls). +_ctx_2d: ?*CanvasRenderingContext2D = null, + +const ContextType = enum { none, @"2d", webgl }; + pub fn asElement(self: *Canvas) *Element { return self._proto._proto; } @@ -69,12 +76,23 @@ const DrawingContext = union(enum) { pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { if (std.mem.eql(u8, context_type, "2d")) { + // Return cached context if available. + if (self._ctx_2d) |cached| return .{ .@"2d" = cached }; + // Per spec: return null if a different context type was already requested. + if (self._context_type != .none) return null; + const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); + self._ctx_2d = ctx; + self._context_type = .@"2d"; return .{ .@"2d" = ctx }; } if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { + // Per spec: return null if a different context type was already requested. + if (self._context_type != .none and self._context_type != .webgl) return null; + const ctx = try page._factory.create(WebGLRenderingContext{}); + self._context_type = .webgl; return .{ .webgl = ctx }; } From 25889ff9182e408bac124ba099473b90a5d0403b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 30 Mar 2026 12:14:32 +0800 Subject: [PATCH 2/2] Improve canvas context caching Improve https://github.com/lightpanda-io/browser/pull/2022 to also cache webgl context and add tests. --- .../canvas/canvas_rendering_context_2d.html | 10 +++++ .../tests/canvas/webgl_rendering_context.html | 10 +++++ src/browser/webapi/element/html/Canvas.zig | 44 +++++++++---------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/browser/tests/canvas/canvas_rendering_context_2d.html b/src/browser/tests/canvas/canvas_rendering_context_2d.html index 9b1b1806..66a673ad 100644 --- a/src/browser/tests/canvas/canvas_rendering_context_2d.html +++ b/src/browser/tests/canvas/canvas_rendering_context_2d.html @@ -148,3 +148,13 @@ } + + diff --git a/src/browser/tests/canvas/webgl_rendering_context.html b/src/browser/tests/canvas/webgl_rendering_context.html index 24bad4fd..7c0e8637 100644 --- a/src/browser/tests/canvas/webgl_rendering_context.html +++ b/src/browser/tests/canvas/webgl_rendering_context.html @@ -85,3 +85,13 @@ loseContext.restoreContext(); } + + diff --git a/src/browser/webapi/element/html/Canvas.zig b/src/browser/webapi/element/html/Canvas.zig index c01fb651..70da796b 100644 --- a/src/browser/webapi/element/html/Canvas.zig +++ b/src/browser/webapi/element/html/Canvas.zig @@ -29,11 +29,7 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig"); const Canvas = @This(); _proto: *HtmlElement, - -/// Cached context type. Once set, requesting a different type returns null (per spec). -_context_type: ContextType = .none, -/// Cached 2D rendering context (same object returned on repeated getContext("2d") calls). -_ctx_2d: ?*CanvasRenderingContext2D = null, +_cached: ?DrawingContext = null, const ContextType = enum { none, @"2d", webgl }; @@ -75,28 +71,28 @@ const DrawingContext = union(enum) { }; pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { - if (std.mem.eql(u8, context_type, "2d")) { - // Return cached context if available. - if (self._ctx_2d) |cached| return .{ .@"2d" = cached }; - // Per spec: return null if a different context type was already requested. - if (self._context_type != .none) return null; - - const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); - self._ctx_2d = ctx; - self._context_type = .@"2d"; - return .{ .@"2d" = ctx }; + if (self._cached) |cached| { + const matches = switch (cached) { + .@"2d" => std.mem.eql(u8, context_type, "2d"), + .webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"), + }; + return if (matches) cached else null; } - if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { - // Per spec: return null if a different context type was already requested. - if (self._context_type != .none and self._context_type != .webgl) return null; + const drawing_context: DrawingContext = blk: { + if (std.mem.eql(u8, context_type, "2d")) { + const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); + break :blk .{ .@"2d" = ctx }; + } - const ctx = try page._factory.create(WebGLRenderingContext{}); - self._context_type = .webgl; - return .{ .webgl = ctx }; - } - - return null; + if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { + const ctx = try page._factory.create(WebGLRenderingContext{}); + break :blk .{ .webgl = ctx }; + } + return null; + }; + self._cached = drawing_context; + return drawing_context; } /// Transfers control of the canvas to an OffscreenCanvas.