Correctly rebuild cc::PaintCanvas matrix stack when layers are used.

Whenever a frame is rendered while layers are opened (e.g. by calling
getImageData between calls to beginLayer and endLayer), the
cc::PaintCanvas stack have to be flushed and all layer closed. This stack
then has to be rebuilt, re-opening any layers that were previously
opened.

Bug: 1237275, 1396372, 1403977
Fixed: 1278112
Change-Id: I8a6d0aded67d35ff625f72c8138b1f2fe682561a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4538303
Reviewed-by: Justin Novosad <junov@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1146245}
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap-expected.html
new file mode 100644
index 0000000..8ce0c51
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.createImageBitmap</title>
+<h1>2d.layer.render-opportunities.createImageBitmap</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap.html b/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap.html
new file mode 100644
index 0000000..4ffcaa3
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.createImageBitmap.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.createImageBitmap-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.createImageBitmap</title>
+<h1>2d.layer.render-opportunities.createImageBitmap</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  createImageBitmap(canvas);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.drawImage-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.drawImage-expected.html
new file mode 100644
index 0000000..1ddc6d1
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.drawImage-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.drawImage</title>
+<h1>2d.layer.render-opportunities.drawImage</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.drawImage.html b/html/canvas/element/layers/2d.layer.render-opportunities.drawImage.html
new file mode 100644
index 0000000..4eb0b6f
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.drawImage.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.drawImage-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.drawImage</title>
+<h1>2d.layer.render-opportunities.drawImage</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  const canvas2 = new OffscreenCanvas(200, 200);
+  const ctx2 = canvas2.getContext('2d');
+  ctx2.drawImage(canvas, 0, 0);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.getImageData-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.getImageData-expected.html
new file mode 100644
index 0000000..ea0e78e
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.getImageData-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.getImageData</title>
+<h1>2d.layer.render-opportunities.getImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.getImageData.html b/html/canvas/element/layers/2d.layer.render-opportunities.getImageData.html
new file mode 100644
index 0000000..3cf514a
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.getImageData.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.getImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.getImageData</title>
+<h1>2d.layer.render-opportunities.getImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  ctx.getImageData(0, 0, 200, 200);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.putImageData-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.putImageData-expected.html
new file mode 100644
index 0000000..3d63bbe
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.putImageData-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.putImageData</title>
+<h1>2d.layer.render-opportunities.putImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.putImageData.html b/html/canvas/element/layers/2d.layer.render-opportunities.putImageData.html
new file mode 100644
index 0000000..8da3daf
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.putImageData.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.putImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.putImageData</title>
+<h1>2d.layer.render-opportunities.putImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  const canvas2 = new OffscreenCanvas(200, 200);
+  const ctx2 = canvas2.getContext('2d');
+  ctx.putImageData(ctx2.getImageData(0, 0, 1, 1), 0, 0);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame-expected.html
new file mode 100644
index 0000000..138f3a7
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame-expected.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<title>Canvas test: 2d.layer.render-opportunities.requestAnimationFrame</title>
+<h1>2d.layer.render-opportunities.requestAnimationFrame</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame.html b/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame.html
new file mode 100644
index 0000000..889ff5b
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.requestAnimationFrame.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.requestAnimationFrame-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.requestAnimationFrame</title>
+<h1>2d.layer.render-opportunities.requestAnimationFrame</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  await new Promise(resolve => requestAnimationFrame(resolve));
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.toBlob-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.toBlob-expected.html
new file mode 100644
index 0000000..fda8e8d
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.toBlob-expected.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<title>Canvas test: 2d.layer.render-opportunities.toBlob</title>
+<h1>2d.layer.render-opportunities.toBlob</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.toBlob.html b/html/canvas/element/layers/2d.layer.render-opportunities.toBlob.html
new file mode 100644
index 0000000..9087304
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.toBlob.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.toBlob-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.toBlob</title>
+<h1>2d.layer.render-opportunities.toBlob</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  await new Promise(resolve => canvas.toBlob(resolve));
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL-expected.html b/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL-expected.html
new file mode 100644
index 0000000..22a9770
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.toDataURL</title>
+<h1>2d.layer.render-opportunities.toDataURL</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL.html b/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL.html
new file mode 100644
index 0000000..152f66a
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.render-opportunities.toDataURL.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.toDataURL-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.toDataURL</title>
+<h1>2d.layer.render-opportunities.toDataURL</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  canvas.toDataURL();
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+</script>
diff --git a/html/canvas/element/layers/2d.layer.unclosed-expected.html b/html/canvas/element/layers/2d.layer.unclosed-expected.html
new file mode 100644
index 0000000..c41b253
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.unclosed-expected.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.unclosed</title>
+<h1>2d.layer.unclosed</h1>
+<p class="desc">Check that layers are rendered even if not closed.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+</script>
diff --git a/html/canvas/element/layers/2d.layer.unclosed.html b/html/canvas/element/layers/2d.layer.unclosed.html
new file mode 100644
index 0000000..788889e
--- /dev/null
+++ b/html/canvas/element/layers/2d.layer.unclosed.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.unclosed-expected.html">
+<title>Canvas test: 2d.layer.unclosed</title>
+<h1>2d.layer.unclosed</h1>
+<p class="desc">Check that layers are rendered even if not closed.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob-expected.html
new file mode 100644
index 0000000..1e61775
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob-expected.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<title>Canvas test: 2d.layer.render-opportunities.convertToBlob</title>
+<h1>2d.layer.render-opportunities.convertToBlob</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.html
new file mode 100644
index 0000000..c1a139a
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.convertToBlob-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.convertToBlob</title>
+<h1>2d.layer.render-opportunities.convertToBlob</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script type="module">
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  await canvas.convertToBlob();
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+  document.documentElement.classList.remove("reftest-wait");
+</script>
+</html>
\ No newline at end of file
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.w.html
new file mode 100644
index 0000000..404fef3
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.convertToBlob.w.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.convertToBlob-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.convertToBlob</title>
+<h1>2d.layer.render-opportunities.convertToBlob</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = async function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    await canvas.convertToBlob();
+
+    ctx.fillRect(70, 70, 75, 50);
+    ctx.fillStyle = 'orange';
+    ctx.fillRect(80, 80, 75, 50);
+    ctx.endLayer();
+
+    ctx.fillRect(80, 40, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap-expected.html
new file mode 100644
index 0000000..8ce0c51
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.createImageBitmap</title>
+<h1>2d.layer.render-opportunities.createImageBitmap</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.html
new file mode 100644
index 0000000..876b027
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.createImageBitmap-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.createImageBitmap</title>
+<h1>2d.layer.render-opportunities.createImageBitmap</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  createImageBitmap(canvas);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.w.html
new file mode 100644
index 0000000..9a89492
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.createImageBitmap.w.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.createImageBitmap-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.createImageBitmap</title>
+<h1>2d.layer.render-opportunities.createImageBitmap</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    createImageBitmap(canvas);
+
+    ctx.fillRect(70, 70, 75, 50);
+    ctx.fillStyle = 'orange';
+    ctx.fillRect(80, 80, 75, 50);
+    ctx.endLayer();
+
+    ctx.fillRect(80, 40, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage-expected.html
new file mode 100644
index 0000000..1ddc6d1
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.drawImage</title>
+<h1>2d.layer.render-opportunities.drawImage</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.html
new file mode 100644
index 0000000..dd17820
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.drawImage-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.drawImage</title>
+<h1>2d.layer.render-opportunities.drawImage</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  const canvas2 = new OffscreenCanvas(200, 200);
+  const ctx2 = canvas2.getContext('2d');
+  ctx2.drawImage(canvas, 0, 0);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.w.html
new file mode 100644
index 0000000..80fbfbd
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.drawImage.w.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.drawImage-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.drawImage</title>
+<h1>2d.layer.render-opportunities.drawImage</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    const canvas2 = new OffscreenCanvas(200, 200);
+    const ctx2 = canvas2.getContext('2d');
+    ctx2.drawImage(canvas, 0, 0);
+
+    ctx.fillRect(70, 70, 75, 50);
+    ctx.fillStyle = 'orange';
+    ctx.fillRect(80, 80, 75, 50);
+    ctx.endLayer();
+
+    ctx.fillRect(80, 40, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData-expected.html
new file mode 100644
index 0000000..ea0e78e
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.getImageData</title>
+<h1>2d.layer.render-opportunities.getImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.html
new file mode 100644
index 0000000..b6f3c1b
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.getImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.getImageData</title>
+<h1>2d.layer.render-opportunities.getImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  ctx.getImageData(0, 0, 200, 200);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.w.html
new file mode 100644
index 0000000..187eb0f
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.getImageData.w.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.getImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.getImageData</title>
+<h1>2d.layer.render-opportunities.getImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    ctx.getImageData(0, 0, 200, 200);
+
+    ctx.fillRect(70, 70, 75, 50);
+    ctx.fillStyle = 'orange';
+    ctx.fillRect(80, 80, 75, 50);
+    ctx.endLayer();
+
+    ctx.fillRect(80, 40, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData-expected.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData-expected.html
new file mode 100644
index 0000000..3d63bbe
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData-expected.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.putImageData</title>
+<h1>2d.layer.render-opportunities.putImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.html
new file mode 100644
index 0000000..b460015
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.putImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.putImageData</title>
+<h1>2d.layer.render-opportunities.putImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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:
+  const canvas2 = new OffscreenCanvas(200, 200);
+  const ctx2 = canvas2.getContext('2d');
+  ctx.putImageData(ctx2.getImageData(0, 0, 1, 1), 0, 0);
+
+  ctx.fillRect(70, 70, 75, 50);
+  ctx.fillStyle = 'orange';
+  ctx.fillRect(80, 80, 75, 50);
+  ctx.endLayer();
+
+  ctx.fillRect(80, 40, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.w.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.w.html
new file mode 100644
index 0000000..9ffac07
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.putImageData.w.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.putImageData-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.putImageData</title>
+<h1>2d.layer.render-opportunities.putImageData</h1>
+<p class="desc">Check that layers state stack is flushed and rebuilt on frame renders.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    const canvas2 = new OffscreenCanvas(200, 200);
+    const ctx2 = canvas2.getContext('2d');
+    ctx.putImageData(ctx2.getImageData(0, 0, 1, 1), 0, 0);
+
+    ctx.fillRect(70, 70, 75, 50);
+    ctx.fillStyle = 'orange';
+    ctx.fillRect(80, 80, 75, 50);
+    ctx.endLayer();
+
+    ctx.fillRect(80, 40, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
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
new file mode 100644
index 0000000..2833849
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap-expected.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.render-opportunities.transferToImageBitmap</title>
+<h1>2d.layer.render-opportunities.transferToImageBitmap</h1>
+<p class="desc">Checks that transferToImageBitmap flushes and rebuilds the state stack.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html
new file mode 100644
index 0000000..2783064
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.render-opportunities.transferToImageBitmap-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.transferToImageBitmap</title>
+<h1>2d.layer.render-opportunities.transferToImageBitmap</h1>
+<p class="desc">Checks that transferToImageBitmap flushes and rebuilds the state stack.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: '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.
+  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);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
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
new file mode 100644
index 0000000..06e0160
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.render-opportunities.transferToImageBitmap.w.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.render-opportunities.transferToImageBitmap-expected.html">
+<title>Canvas test: 2d.layer.render-opportunities.transferToImageBitmap</title>
+<h1>2d.layer.render-opportunities.transferToImageBitmap</h1>
+<p class="desc">Checks that transferToImageBitmap flushes and rebuilds the state stack.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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.
+    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);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/offscreen/layers/2d.layer.unclosed-expected.html b/html/canvas/offscreen/layers/2d.layer.unclosed-expected.html
new file mode 100644
index 0000000..c41b253
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.unclosed-expected.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<title>Canvas test: 2d.layer.unclosed</title>
+<h1>2d.layer.unclosed</h1>
+<p class="desc">Check that layers are rendered even if not closed.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = document.getElementById("canvas");
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+  ctx.endLayer();
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.unclosed.html b/html/canvas/offscreen/layers/2d.layer.unclosed.html
new file mode 100644
index 0000000..689ee80
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.unclosed.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<link rel="match" href="2d.layer.unclosed-expected.html">
+<title>Canvas test: 2d.layer.unclosed</title>
+<h1>2d.layer.unclosed</h1>
+<p class="desc">Check that layers are rendered even if not closed.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script>
+  const canvas = new OffscreenCanvas(200, 200);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'purple';
+  ctx.fillRect(60, 60, 75, 50);
+  ctx.globalAlpha = 0.5;
+
+  ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+  ctx.fillRect(40, 40, 75, 50);
+  ctx.fillStyle = 'grey';
+  ctx.fillRect(50, 50, 75, 50);
+
+  const outputCanvas = document.getElementById("canvas");
+  outputCanvas.getContext('2d').drawImage(canvas, 0, 0);
+</script>
diff --git a/html/canvas/offscreen/layers/2d.layer.unclosed.w.html b/html/canvas/offscreen/layers/2d.layer.unclosed.w.html
new file mode 100644
index 0000000..0c7812e
--- /dev/null
+++ b/html/canvas/offscreen/layers/2d.layer.unclosed.w.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+<html class="reftest-wait">
+<link rel="match" href="2d.layer.unclosed-expected.html">
+<title>Canvas test: 2d.layer.unclosed</title>
+<h1>2d.layer.unclosed</h1>
+<p class="desc">Check that layers are rendered even if not closed.</p>
+<canvas id="canvas" width="200" height="200">
+  <p class="fallback">FAIL (fallback content)</p>
+</canvas>
+<script id='myWorker' type='text/worker'>
+  self.onmessage = function(e) {
+    const canvas = new OffscreenCanvas(200, 200);
+    const ctx = canvas.getContext('2d');
+
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+    ctx.fillRect(40, 40, 75, 50);
+    ctx.fillStyle = 'grey';
+    ctx.fillRect(50, 50, 75, 50);
+
+    const bitmap = canvas.transferToImageBitmap();
+    self.postMessage(bitmap, bitmap);
+  };
+</script>
+<script>
+  const blob = new Blob([document.getElementById('myWorker').textContent]);
+  const worker = new Worker(URL.createObjectURL(blob));
+  worker.addEventListener('message', msg => {
+    const outputCtx = document.getElementById("canvas").getContext('2d');
+    outputCtx.drawImage(msg.data, 0, 0);
+    document.documentElement.classList.remove("reftest-wait");
+  });
+  worker.postMessage(null);
+</script>
+</html>
diff --git a/html/canvas/tools/templates-new.yaml b/html/canvas/tools/templates-new.yaml
index 581c1d7..7d8ebfc 100644
--- a/html/canvas/tools/templates-new.yaml
+++ b/html/canvas/tools/templates-new.yaml
@@ -131,6 +131,9 @@
 offscreen_ref_test: |-
     <!DOCTYPE html>
     <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+    {% if promise_test %}\
+    <html class="reftest-wait">
+    {% endif %}\
     %(links)s\
     %(fuzzy)s\
     %(timeout)s\
@@ -140,7 +143,7 @@
     %(fonts)s%(fonthack)s%(notes)s<canvas id="canvas" width="%(width)s" height="%(height)s"%(canvas)s>
       %(fallback)s
     </canvas>
-    <script>
+    <script{% if promise_test %} type="module"{% endif %}>
       const canvas = new OffscreenCanvas(%(width)s, %(height)s);
       const ctx = canvas.getContext(%(context_args)s);
 
@@ -148,9 +151,12 @@
 
       const outputCanvas = document.getElementById("canvas");
       outputCanvas.getContext(%(context_args)s).drawImage(canvas, 0, 0);
+    {% if promise_test %}\
+      document.documentElement.classList.remove("reftest-wait");
+    {% endif %}\
     </script>
-    %(images)s
-
+    %(images)s\
+    {% if promise_test %}</html>{% endif %}
 
 worker_ref_test: |
     <!DOCTYPE html>
@@ -166,7 +172,7 @@
       %(fallback)s
     </canvas>
     <script id='myWorker' type='text/worker'>
-      self.onmessage = function(e) {
+      self.onmessage = {% if promise_test %}async {% endif %}function(e) {
         const canvas = new OffscreenCanvas(%(width)s, %(height)s);
         const ctx = canvas.getContext('2d');
 
@@ -192,6 +198,9 @@
 element_ref_test: |-
     <!DOCTYPE html>
     <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
+    {% if promise_test %}\
+    <html class="reftest-wait">
+    {% endif %}\
     %(links)s\
     %(fuzzy)s\
     %(timeout)s\
@@ -201,13 +210,17 @@
     %(fonts)s%(fonthack)s%(notes)s<canvas id="canvas" width="%(width)s" height="%(height)s"%(canvas)s>
       %(fallback)s
     </canvas>
-    <script>
+    <script{% if promise_test %} type="module"{% endif %}>
       const canvas = document.getElementById("canvas");
       const ctx = canvas.getContext(%(context_args)s);
 
     %(code)s
+    {% if promise_test %}\
+      document.documentElement.classList.remove("reftest-wait");
+    {% endif %}\
     </script>
-    %(images)s
+    %(images)s\
+    {% if promise_test %}</html>{% endif %}
 
 
 html_ref_test: |-
diff --git a/html/canvas/tools/yaml-new/layers.yaml b/html/canvas/tools/yaml-new/layers.yaml
index d398ef2..fe1902c 100644
--- a/html/canvas/tools/yaml-new/layers.yaml
+++ b/html/canvas/tools/yaml-new/layers.yaml
@@ -247,6 +247,146 @@
     ctx.fillRect(70, 70, 75, 50);
 
 
+- name: 2d.layer.unclosed
+  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;
+
+    ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+    ctx.fillRect(40, 40, 75, 50);
+    ctx.fillStyle = 'grey';
+    ctx.fillRect(50, 50, 75, 50);
+  reference: |
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(40, 40, 75, 50);
+    ctx.fillStyle = 'grey';
+    ctx.fillRect(50, 50, 75, 50);
+    ctx.endLayer();
+
+
+- name: 2d.layer.render-opportunities
+  desc: Check that layers state stack is flushed and rebuilt on frame renders.
+  size: 200, 200
+  code: |
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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:
+    %(flush_canvas)s
+
+    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.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: 'dropShadow', dx: -2, dy: 2});
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(40, 40, 75, 50);
+    ctx.fillStyle = 'grey';
+    ctx.fillRect(50, 50, 75, 50);
+    ctx.endLayer();
+
+    ctx.beginLayer({filter: '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);
+  variants:
+    convertToBlob:
+      test_type: "promise"
+      canvasType: ['OffscreenCanvas']
+      flush_canvas: |-
+        await canvas.convertToBlob();
+    createImageBitmap:
+      flush_canvas: createImageBitmap(canvas);
+    drawImage:
+      flush_canvas: |-
+        const canvas2 = new OffscreenCanvas(200, 200);
+        const ctx2 = canvas2.getContext('2d');
+        ctx2.drawImage(canvas, 0, 0);
+    getImageData:
+      flush_canvas: ctx.getImageData(0, 0, 200, 200);
+    requestAnimationFrame:
+      canvasType: ['HTMLCanvas']
+      test_type: "promise"
+      flush_canvas: |-
+        await new Promise(resolve => requestAnimationFrame(resolve));
+    putImageData:
+      flush_canvas: |-
+        const canvas2 = new OffscreenCanvas(200, 200);
+        const ctx2 = canvas2.getContext('2d');
+        ctx.putImageData(ctx2.getImageData(0, 0, 1, 1), 0, 0);
+    toBlob:
+      test_type: "promise"
+      canvasType: ['HTMLCanvas']
+      flush_canvas: |-
+        await new Promise(resolve => canvas.toBlob(resolve));
+    toDataURL:
+      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
+  canvasType: ['OffscreenCanvas']
+  code: |
+    ctx.fillStyle = 'purple';
+    ctx.fillRect(60, 60, 75, 50);
+    ctx.globalAlpha = 0.5;
+
+    ctx.beginLayer({filter: '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.
+    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: '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);
+
+
 - name: 2d.layer.several-complex
   desc: >-
     Test to ensure beginlayer works for filter, alpha and shadow, even with