From 3375400712353d2c9b011ed3dbb24c8d756b784f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Gravel Date: Fri, 20 Oct 2023 06:21:12 -0700 Subject: [PATCH] Throw an exception in transferToImageBitmap if canvas layers are opened This API is incompatible with how the 2D canvas is rasterized when it contains unclosed layers. Because layers can have filters that get applied on their final content, they can't be presented until they are closed. Instead, we normally keep the layer content alive after a flush, so that it can be presented in a later frame when the layer is finally closed. OffscreenCanvas.transferToImageBitmap however is supposed to release the canvas content, leaving the offscreen canvas empty. We cannot release the recording if layers are incomplete, and if we kept the layer content alive for later, we would not be leaving the canvas empty as the spec requires. This behavior is part of the current 2D Canvas Layer spec draft: Explainer: https://github.com/fserb/canvas2D/blob/master/spec/layers.md Spec draft: https://github.com/whatwg/html/pull/9537 Bug: 1484741 Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633 Commit-Queue: Jean-Philippe Gravel Reviewed-by: Fernando Serboncini Cr-Commit-Position: refs/heads/main@{#1212692} --- ...nities.transferToImageBitmap-expected.html | 24 ---- ...r-opportunities.transferToImageBitmap.html | 36 ------ ...opportunities.transferToImageBitmap.w.html | 50 -------- .../2d.layer.transferToImageBitmap.html | 34 ++++++ .../2d.layer.transferToImageBitmap.worker.js | 29 +++++ .../layers/2d.layer.unclosed-nested.w.html | 31 +++-- .../offscreen/layers/2d.layer.unclosed.w.html | 23 ++-- html/canvas/tools/yaml-new/layers.yaml | 110 +++++++++--------- 8 files changed, 156 insertions(+), 181 deletions(-) delete mode 100644 html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap-expected.html delete mode 100644 html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html delete mode 100644 html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.w.html create mode 100644 html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.html create mode 100644 html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.worker.js diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap-expected.html deleted file mode 100644 index 36deca50f4a30f..00000000000000 --- a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap-expected.html +++ /dev/null @@ -1,24 +0,0 @@ - - -Canvas test: 2d.layer.render-opportunities.transferToImageBitmap -

2d.layer.render-opportunities.transferToImageBitmap

-

Checks that transferToImageBitmap flushes and rebuilds the state stack.

- -

FAIL (fallback content)

-
- diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html deleted file mode 100644 index 634d9e3a78c1a5..00000000000000 --- a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -Canvas test: 2d.layer.render-opportunities.transferToImageBitmap -

2d.layer.render-opportunities.transferToImageBitmap

-

Checks that transferToImageBitmap flushes and rebuilds the state stack.

- -

FAIL (fallback content)

-
- diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.w.html deleted file mode 100644 index 823bfac58b09ee..00000000000000 --- a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.w.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - -Canvas test: 2d.layer.render-opportunities.transferToImageBitmap -

2d.layer.render-opportunities.transferToImageBitmap

-

Checks that transferToImageBitmap flushes and rebuilds the state stack.

- -

FAIL (fallback content)

-
- - - diff --git a/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.html b/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.html new file mode 100644 index 00000000000000..5e5d6d16c6b11d --- /dev/null +++ b/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.html @@ -0,0 +1,34 @@ + + +OffscreenCanvas test: 2d.layer.transferToImageBitmap + + + + +

2d.layer.transferToImageBitmap

+

Check that calling transferToImageBitmap in a layer throws an exception.

+ + + diff --git a/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.worker.js b/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.worker.js new file mode 100644 index 00000000000000..073a3e84a486ac --- /dev/null +++ b/html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.worker.js @@ -0,0 +1,29 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.layer.transferToImageBitmap +// Description:Check that calling transferToImageBitmap in a layer throws an exception. +// Note: + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +var t = async_test("Check that calling transferToImageBitmap in a layer throws an exception."); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + // `transferToImageBitmap` shouldn't throw on it's own. + canvas.transferToImageBitmap(); + // Make sure the exception isn't caused by calling the function twice. + canvas.transferToImageBitmap(); + // Calling again inside a layer should throw. + ctx.beginLayer(); + assert_throws_dom("InvalidStateError", + () => canvas.transferToImageBitmap()); + t.done(); +}); +done(); diff --git a/html/canvas/offscreen/layers/2d.layer.unclosed-nested.w.html b/html/canvas/offscreen/layers/2d.layer.unclosed-nested.w.html index dd1c36ea471975..e330e1710cf488 100644 --- a/html/canvas/offscreen/layers/2d.layer.unclosed-nested.w.html +++ b/html/canvas/offscreen/layers/2d.layer.unclosed-nested.w.html @@ -13,20 +13,29 @@

2d.layer.unclosed-nested

const canvas = new OffscreenCanvas(200, 200); const ctx = canvas.getContext('2d'); - ctx.fillStyle = 'rgba(0, 0, 255, 1)'; - ctx.fillRect(60, 60, 75, 50); - ctx.globalAlpha = 0.5; + // `transferToImageBitmap` is used to transfer the test result to the + // worker's parent, but `transferToImageBitmap` can't be called on canvas + // with unclosed layers. We can however draw to a separate offscreen canvas + // and write it to the main canvas using `drawImage`. + const canvas2 = new OffscreenCanvas(200, 200); + const ctx2 = canvas2.getContext('2d'); - ctx.beginLayer(); - ctx.fillStyle = 'rgba(225, 0, 0, 1)'; - ctx.fillRect(50, 50, 75, 50); + ctx2.fillStyle = 'rgba(0, 0, 255, 1)'; + ctx2.fillRect(60, 60, 75, 50); + ctx2.globalAlpha = 0.5; - ctx.beginLayer(); - ctx.fillStyle = 'rgba(0, 255, 0, 1)'; - ctx.fillRect(70, 70, 75, 50); + ctx2.beginLayer(); + ctx2.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx2.fillRect(50, 50, 75, 50); - ctx.endLayer(); - // Missing ctx.endLayer() here. + ctx2.beginLayer(); + ctx2.fillStyle = 'rgba(0, 255, 0, 1)'; + ctx2.fillRect(70, 70, 75, 50); + + ctx2.endLayer(); + // Missing ctx2.endLayer() here. + + ctx.drawImage(canvas2, 0, 0); const bitmap = canvas.transferToImageBitmap(); self.postMessage(bitmap, bitmap); diff --git a/html/canvas/offscreen/layers/2d.layer.unclosed.w.html b/html/canvas/offscreen/layers/2d.layer.unclosed.w.html index 830dea7e3d1165..1da44c26c8661b 100644 --- a/html/canvas/offscreen/layers/2d.layer.unclosed.w.html +++ b/html/canvas/offscreen/layers/2d.layer.unclosed.w.html @@ -13,14 +13,23 @@

2d.layer.unclosed

const canvas = new OffscreenCanvas(200, 200); const ctx = canvas.getContext('2d'); - ctx.fillStyle = 'purple'; - ctx.fillRect(60, 60, 75, 50); - ctx.globalAlpha = 0.5; + // `transferToImageBitmap` is used to transfer the test result to the + // worker's parent, but `transferToImageBitmap` can't be called on canvas + // with unclosed layers. We can however draw to a separate offscreen canvas + // and write it to the main canvas using `drawImage`. + const canvas2 = new OffscreenCanvas(200, 200); + const ctx2 = canvas2.getContext('2d'); - ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); - ctx.fillRect(40, 40, 75, 50); - ctx.fillStyle = 'grey'; - ctx.fillRect(50, 50, 75, 50); + ctx2.fillStyle = 'purple'; + ctx2.fillRect(60, 60, 75, 50); + ctx2.globalAlpha = 0.5; + + ctx2.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); + ctx2.fillRect(40, 40, 75, 50); + ctx2.fillStyle = 'grey'; + ctx2.fillRect(50, 50, 75, 50); + + ctx.drawImage(canvas2, 0, 0); const bitmap = canvas.transferToImageBitmap(); self.postMessage(bitmap, bitmap); diff --git a/html/canvas/tools/yaml-new/layers.yaml b/html/canvas/tools/yaml-new/layers.yaml index 9145108bd3c27f..41cd2ddfb3d88a 100644 --- a/html/canvas/tools/yaml-new/layers.yaml +++ b/html/canvas/tools/yaml-new/layers.yaml @@ -312,14 +312,29 @@ desc: Check that layers are rendered even if not closed. size: [200, 200] code: | - ctx.fillStyle = 'purple'; - ctx.fillRect(60, 60, 75, 50); - ctx.globalAlpha = 0.5; + {% set ns = namespace(ctx='ctx') %} + {% if canvas_type == 'worker' %} + // `transferToImageBitmap` is used to transfer the test result to the + // worker's parent, but `transferToImageBitmap` can't be called on canvas + // with unclosed layers. We can however draw to a separate offscreen canvas + // and write it to the main canvas using `drawImage`. + const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); + const ctx2 = canvas2.getContext('2d'); + {% set ns.ctx = 'ctx2' %} + {% endif %} - ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); - ctx.fillRect(40, 40, 75, 50); - ctx.fillStyle = 'grey'; - ctx.fillRect(50, 50, 75, 50); + {{ ns.ctx }}.fillStyle = 'purple'; + {{ ns.ctx }}.fillRect(60, 60, 75, 50); + {{ ns.ctx }}.globalAlpha = 0.5; + + {{ ns.ctx }}.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); + {{ ns.ctx }}.fillRect(40, 40, 75, 50); + {{ ns.ctx }}.fillStyle = 'grey'; + {{ ns.ctx }}.fillRect(50, 50, 75, 50); + + {% if canvas_type == 'worker' %} + ctx.drawImage(canvas2, 0, 0); + {% endif %} reference: | ctx.fillStyle = 'purple'; ctx.fillRect(60, 60, 75, 50); @@ -336,20 +351,35 @@ desc: Check that layers are rendered even if not closed. size: [200, 200] code: | - ctx.fillStyle = 'rgba(0, 0, 255, 1)'; - ctx.fillRect(60, 60, 75, 50); - ctx.globalAlpha = 0.5; + {% set ns = namespace(ctx='ctx') %} + {% if canvas_type == 'worker' %} + // `transferToImageBitmap` is used to transfer the test result to the + // worker's parent, but `transferToImageBitmap` can't be called on canvas + // with unclosed layers. We can however draw to a separate offscreen canvas + // and write it to the main canvas using `drawImage`. + const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }}); + const ctx2 = canvas2.getContext('2d'); + {% set ns.ctx = 'ctx2' %} + {% endif %} - ctx.beginLayer(); - ctx.fillStyle = 'rgba(225, 0, 0, 1)'; - ctx.fillRect(50, 50, 75, 50); + {{ ns.ctx }}.fillStyle = 'rgba(0, 0, 255, 1)'; + {{ ns.ctx }}.fillRect(60, 60, 75, 50); + {{ ns.ctx }}.globalAlpha = 0.5; - ctx.beginLayer(); - ctx.fillStyle = 'rgba(0, 255, 0, 1)'; - ctx.fillRect(70, 70, 75, 50); + {{ ns.ctx }}.beginLayer(); + {{ ns.ctx }}.fillStyle = 'rgba(225, 0, 0, 1)'; + {{ ns.ctx }}.fillRect(50, 50, 75, 50); - ctx.endLayer(); - // Missing ctx.endLayer() here. + {{ ns.ctx }}.beginLayer(); + {{ ns.ctx }}.fillStyle = 'rgba(0, 255, 0, 1)'; + {{ ns.ctx }}.fillRect(70, 70, 75, 50); + + {{ ns.ctx }}.endLayer(); + // Missing {{ ns.ctx }}.endLayer() here. + + {% if canvas_type == 'worker' %} + ctx.drawImage(canvas2, 0, 0); + {% endif %} reference: | const canvas1 = document.createElement('canvas'); const ctx1 = canvas1.getContext('2d'); @@ -443,44 +473,18 @@ canvasType: ['HTMLCanvas'] flush_canvas: canvas.toDataURL(); - -- name: 2d.layer.render-opportunities.transferToImageBitmap - desc: Checks that transferToImageBitmap flushes and rebuilds the state stack. - size: [200, 200] +- name: 2d.layer.transferToImageBitmap + desc: Check that calling transferToImageBitmap in a layer throws an exception. canvasType: ['OffscreenCanvas', 'Worker'] code: | - ctx.fillStyle = 'purple'; - ctx.fillRect(60, 60, 75, 50); - ctx.globalAlpha = 0.5; - - ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); - ctx.fillRect(40, 40, 75, 50); - ctx.fillStyle = 'grey'; - ctx.fillRect(50, 50, 75, 50); - - // Force a flush and restoration of the state stack. - // `transferToImageBitmap` clears the frame but preserves render states. + // `transferToImageBitmap` shouldn't throw on it's own. canvas.transferToImageBitmap(); - - ctx.fillRect(70, 70, 75, 50); - ctx.fillStyle = 'orange'; - ctx.fillRect(80, 80, 75, 50); - ctx.endLayer(); - - ctx.fillRect(80, 40, 75, 50); - reference: | - ctx.fillStyle = 'purple'; - ctx.globalAlpha = 0.5; - - ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}}); - ctx.fillStyle = 'grey'; - ctx.fillRect(70, 70, 75, 50); - ctx.fillStyle = 'orange'; - ctx.fillRect(80, 80, 75, 50); - ctx.endLayer(); - - ctx.fillRect(80, 40, 75, 50); - + // Make sure the exception isn't caused by calling the function twice. + canvas.transferToImageBitmap(); + // Calling again inside a layer should throw. + ctx.beginLayer(); + assert_throws_dom("InvalidStateError", + () => canvas.transferToImageBitmap()); - name: 2d.layer.several-complex desc: >-