From 94c4dbea9f58c9191e8a587c128c522c87b6db49 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 10 Sep 2026 17:23:55 -0700 Subject: [PATCH 1/4] Fix nested canvas clips and clipped clears Intersect rectangular clips with the existing NanoVG scissor so nested clips preserve parent bounds and save/restore state. Clear clipped regions with destination-out coverage while ignoring global alpha and filters. Initialize empty filter stacks and add GPU-readback regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- .../JavaScript/dist/tests.javaScript.all.js | 241 ++++++++++++++---- .../JavaScript/src/tests.javaScript.all.ts | 134 ++++++++++ Apps/UnitTests/Source/Tests.JavaScript.cpp | 5 + Polyfills/Canvas/Source/Context.cpp | 11 +- .../Canvas/Source/nanovg/nanovg_filterstack.h | 2 +- 5 files changed, 335 insertions(+), 58 deletions(-) diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index 3974dd1466..9836a9b1ac 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js +++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js @@ -28382,6 +28382,7 @@ mocha__WEBPACK_IMPORTED_MODULE_2__.reporter("spec"); + (0,_tests_nativeEngine_png__WEBPACK_IMPORTED_MODULE_6__.registerPngTests)(describe, it, hasGpuRendering && hasNativeImageLoading); describe("RequestFile", function () { @@ -28544,6 +28545,138 @@ describe("Canvas2D", function () { } + (skipCanvasGpuTests ? it.skip : it)( + "intersects nested clips and restores parent clips on the GPU", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( + function _callee() {var _loop, _i, _arr;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: + this.timeout(10000);_loop = /*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _loop() {var translated, engine, scene, texture, ctx, pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + translated = _arr[_i]; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Scene(engine);_context.prev = 1; + + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.DynamicTexture("nested clips", 64, scene, false); + ctx = texture.getContext(); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(8, 0, 24, 64); + ctx.clip(); + + ctx.save(); + if (translated) { + ctx.translate(16, 0); + } + ctx.beginPath(); + ctx.rect(0, 0, 64, 64); + ctx.clip(); + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + + ctx.fillStyle = "#00ff00"; + ctx.fillRect(24, 0, 16, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(40, 0, 16, 64); + ctx.clip(); + ctx.fillStyle = "magenta"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + ctx.restore(); + + ctx.fillStyle = "blue"; + ctx.fillRect(40, 0, 8, 64); + texture.update(false);_context.next = 2;return ( + texture.readPixels());case 2:pixels = _context.sent;if ( + pixels instanceof Uint8Array) {_context.next = 3;break;}throw ( + new Error("Expected RGBA8 GPU readback for the canvas texture"));case 3: + + pixel = function pixel(x) {return ( + Array.from( + pixels.subarray( + (32 * 64 + x) * 4, + (32 * 64 + x + 1) * 4 + ) + ));}; + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(4), "outside parent").to.deep.equal([ + 255, 255, 255, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(12), "translated child boundary").to.deep.equal( + translated ? [255, 255, 255, 255] : [255, 0, 0, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(20), "inside intersection").to.deep.equal([ + 255, 0, 0, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(28), "restored parent").to.deep.equal([ + 0, 255, 0, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(36), "outside restored parent").to.deep.equal([ + 255, 255, 255, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(44), "restored unclipped state").to.deep.equal([ + 0, 0, 255, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(52), "disjoint clip").to.deep.equal([ + 255, 255, 255, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(60), "outside every fill").to.deep.equal([ + 255, 255, 255, 255] + );case 4:_context.prev = 4; + + scene.dispose(); + engine.dispose();return _context.finish(4);case 5:case "end":return _context.stop();}}, _loop, null, [[1,, 4, 5]]);});_i = 0, _arr = [false, true];case 1:if (!(_i < _arr.length)) {_context2.next = 3;break;}return _context2.delegateYield(_loop(), "t0", 2);case 2:_i++;_context2.next = 1;break;case 3:case "end":return _context2.stop();}}, _callee, this);})) + + + + ); + + (skipCanvasGpuTests ? it.skip : it)( + "clears only the clipped GPU region and ignores globalAlpha and filters", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( + function _callee2() {var engine, scene, texture, ctx, _pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: + this.timeout(10000); + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Scene(engine);_context3.prev = 1; + + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.DynamicTexture("clipped clear", 64, scene, false); + ctx = texture.getContext(); + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 64, 64); + texture.update(false); + + ctx.filter = "blur(2px)"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(8, 0, 24, 64); + ctx.clip(); + ctx.globalAlpha = 0.25; + ctx.clearRect(0, 0, 64, 64); + ctx.restore(); + texture.update(false);_context3.next = 2;return ( + texture.readPixels());case 2:_pixels = _context3.sent;if ( + _pixels instanceof Uint8Array) {_context3.next = 3;break;}throw ( + new Error("Expected RGBA8 GPU readback for the canvas texture"));case 3: + + pixel = function pixel(x) {return ( + Array.from( + _pixels.subarray((32 * 64 + x) * 4, (32 * 64 + x + 1) * 4) + ));}; + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(36), "preserved near clip").to.deep.equal([ + 255, 0, 0, 255] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(16), "fully cleared inside clip").to.deep.equal([ + 0, 0, 0, 0] + ); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(44), "preserved after clip").to.deep.equal([ + 255, 0, 0, 255] + );case 4:_context3.prev = 4; + + scene.dispose(); + engine.dispose();return _context3.finish(4);case 5:case "end":return _context3.stop();}}, _callee2, this, [[1,, 4, 5]]);})) + + + ); + it("round-trips a string fillStyle and strokeStyle", function () { var ctx = createContext(); ctx.fillStyle = "#ff0000"; @@ -28976,14 +29109,14 @@ describe("Canvas2D", function () { 8.5 ); - var pixels = captureGpuPixels(destination.canvas); + var _pixels2 = captureGpuPixels(destination.canvas); // Pixel (12,3), sampled at its center (12.5,3.5), maps to source // x = 5.99 + (12.5 - 2.75) * 1.01 / 24.5 = 6.39194. // Between red texel center 5.5 and green center 6.5, ideal bilinear // weights are 10.8% red / 89.2% green (R~28, G~227). // Integer truncation maps x to 5.4375; ignoring the crop maps x to // 5.25. Both negative controls are solid red at this sample. - var green = pixelAt(pixels, destination.canvas.width, 12, 3); + var green = pixelAt(_pixels2, destination.canvas.width, 12, 3); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(green[1] - green[0]).to.be.greaterThan(100); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(green[3]).to.be.greaterThan(200); @@ -28992,12 +29125,12 @@ describe("Canvas2D", function () { // That is 18.3% top red / 81.7% bottom blue (R~47, B~208). // Integer truncation maps y to 3.4375 and ignoring the crop maps y // to 3.5, both solid red rather than blue. - var blue = pixelAt(pixels, destination.canvas.width, 3, 14); + var blue = pixelAt(_pixels2, destination.canvas.width, 3, 14); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blue[2] - blue[0]).to.be.greaterThan(80); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blue[3]).to.be.greaterThan(200); var fractionalEdge = pixelAt( - pixels, + _pixels2, destination.canvas.width, 26, 3 @@ -29033,14 +29166,14 @@ describe("Canvas2D", function () { -6 ); - var pixels = captureGpuPixels(destination.canvas); - var inside = pixelAt(pixels, destination.canvas.width, 7, 7); + var _pixels3 = captureGpuPixels(destination.canvas); + var inside = pixelAt(_pixels3, destination.canvas.width, 7, 7); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[1]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[2]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[3]).to.be.greaterThan(220); - var outside = pixelAt(pixels, destination.canvas.width, 2, 7); + var outside = pixelAt(_pixels3, destination.canvas.width, 2, 7); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[3]).to.equal(0); } finally { disposeCanvas(destination); @@ -29066,10 +29199,10 @@ describe("Canvas2D", function () { 12 ); - var pixels = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, destination.canvas.width, 5, 5)[3]).to.equal(0); + var _pixels4 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels4, destination.canvas.width, 5, 5)[3]).to.equal(0); - var inside = pixelAt(pixels, destination.canvas.width, 10, 10); + var inside = pixelAt(_pixels4, destination.canvas.width, 10, 10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[1]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[2]).to.be.greaterThan(220); @@ -29092,14 +29225,14 @@ describe("Canvas2D", function () { source.context.fillRect(0, 0, 8, 8); destination.context.drawImage(source.canvas, 8, 0, 8, 8); - var pixels = captureGpuPixels(destination.canvas); - var first = pixelAt(pixels, destination.canvas.width, 4, 4); + var _pixels5 = captureGpuPixels(destination.canvas); + var first = pixelAt(_pixels5, destination.canvas.width, 4, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[2]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[3]).to.be.greaterThan(220); - var second = pixelAt(pixels, destination.canvas.width, 12, 4); + var second = pixelAt(_pixels5, destination.canvas.width, 12, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[2]).to.be.greaterThan(220); @@ -29140,14 +29273,14 @@ describe("Canvas2D", function () { 1 ); - var pixels = captureGpuPixels(destination.canvas); - var putSample = pixelAt(pixels, destination.canvas.width, 2, 2); + var _pixels6 = captureGpuPixels(destination.canvas); + var putSample = pixelAt(_pixels6, destination.canvas.width, 2, 2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[1]).to.be.within(75, 115); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[2]).to.be.within(15, 50); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[3]).to.be.greaterThan(240); - var drawSample = pixelAt(pixels, destination.canvas.width, 8, 2); + var drawSample = pixelAt(_pixels6, destination.canvas.width, 8, 2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[0]).to.be.within(75, 115); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[1]).to.be.within(30, 70); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[2]).to.be.greaterThan(220); @@ -29188,15 +29321,15 @@ describe("Canvas2D", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(transform.e).to.equal(2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(transform.f).to.equal(1); - var pixels = captureGpuPixels(destination.canvas); - var cropped = pixelAt(pixels, destination.canvas.width, 5, 4); + var _pixels7 = captureGpuPixels(destination.canvas); + var cropped = pixelAt(_pixels7, destination.canvas.width, 5, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[1]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[2]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[3]).to.be.greaterThan(240); var translatedEdge = pixelAt( - pixels, + _pixels7, destination.canvas.width, 8, 4 @@ -29206,7 +29339,7 @@ describe("Canvas2D", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(translatedEdge[2]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(translatedEdge[3]).to.be.greaterThan(240); - var clippedOut = pixelAt(pixels, destination.canvas.width, 5, 8); + var clippedOut = pixelAt(_pixels7, destination.canvas.width, 5, 8); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[0]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[1]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[2]).to.be.lessThan(10); @@ -29235,8 +29368,8 @@ describe("Canvas2D", function () { destination.context.drawImage(source.canvas, 2, 2, 4, 4); destination.context.drawImage(source.canvas, 10, 8, 4, 4); - var pixels = captureGpuPixels(destination.canvas); - var firstDraw = pixelAt(pixels, destination.canvas.width, 3, 3); + var _pixels8 = captureGpuPixels(destination.canvas); + var firstDraw = pixelAt(_pixels8, destination.canvas.width, 3, 3); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[2]).to.be.lessThan(30); @@ -29244,9 +29377,9 @@ describe("Canvas2D", function () { [ // Inside the triangle but outside both destination rectangles. - pixelAt(pixels, destination.canvas.width, 2, 8), + pixelAt(_pixels8, destination.canvas.width, 2, 8), // Inside the second destination rectangle but outside the triangle. - pixelAt(pixels, destination.canvas.width, 12, 9)]. + pixelAt(_pixels8, destination.canvas.width, 12, 9)]. forEach(function (outside) { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[0]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[1]).to.be.lessThan(10); @@ -29267,13 +29400,13 @@ describe("Canvas2D", function () { source.context.fillRect(0, 0, 2, 2); destination.context.drawImage(source.canvas, 1.75, 1.75, 2.5, 2.5); - var pixels = destination.context.getImageData(0, 0, 8, 8).data; - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, destination.canvas.width, 2, 2)[3]).to.equal(255); + var _pixels9 = destination.context.getImageData(0, 0, 8, 8).data; + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels9, destination.canvas.width, 2, 2)[3]).to.equal(255); [ - pixelAt(pixels, destination.canvas.width, 1, 2), - pixelAt(pixels, destination.canvas.width, 4, 2), - pixelAt(pixels, destination.canvas.width, 2, 1), - pixelAt(pixels, destination.canvas.width, 2, 4)]. + pixelAt(_pixels9, destination.canvas.width, 1, 2), + pixelAt(_pixels9, destination.canvas.width, 4, 2), + pixelAt(_pixels9, destination.canvas.width, 2, 1), + pixelAt(_pixels9, destination.canvas.width, 2, 4)]. forEach(function (edge) { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(edge[3]).to.be.within(40, 90); }); @@ -29293,9 +29426,9 @@ describe("Canvas2D", function () { resource.context.fillStyle = "#0000ff"; resource.context.fillRect(0, 0, 4, 8); - var pixels = captureGpuPixels(resource.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 2, 2)).to.deep.equal([0, 0, 255, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 6, 6)).to.deep.equal([255, 0, 0, 255]); + var _pixels0 = captureGpuPixels(resource.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels0, 8, 2, 2)).to.deep.equal([0, 0, 255, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels0, 8, 6, 6)).to.deep.equal([255, 0, 0, 255]); } finally { disposeCanvas(resource); } @@ -29309,9 +29442,9 @@ describe("Canvas2D", function () { source.context.fillRect(4, 4, 8, 8); destination.context.filter = "blur(2px)"; destination.context.drawImage(source.canvas, 0, 0); - var pixels = captureGpuPixels(destination.canvas); - var fringe = pixelAt(pixels, 16, 3, 8); - var center = pixelAt(pixels, 16, 8, 8); + var _pixels1 = captureGpuPixels(destination.canvas); + var fringe = pixelAt(_pixels1, 16, 3, 8); + var center = pixelAt(_pixels1, 16, 8, 8); // The unfiltered CPU copy has zero alpha outside the source ink. (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(fringe[0]).to.equal(fringe[1]); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(fringe[1]).to.equal(fringe[2]); @@ -29338,9 +29471,9 @@ describe("Canvas2D", function () { } }, 0); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(conversions).to.equal(1); - var pixels = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); + var _pixels10 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels10, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels10, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); } finally { disposeCanvas(destination); disposeCanvas(source); @@ -29365,9 +29498,9 @@ describe("Canvas2D", function () { } }, 0); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(conversions).to.equal(1); - var pixels = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(pixels, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); + var _pixels11 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); } finally { Object.setPrototypeOf(source.canvas, prototype); disposeCanvas(destination); @@ -29661,9 +29794,9 @@ describe("PostProcesses", function () { describe("NativeEncoding", function () { this.timeout(0);function - expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee3(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blob).to.be.instanceOf(Blob);_context3.next = 1;return ( - blob.arrayBuffer());case 1:arrayBuffer = _context3.sent; + expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee5(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0: + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blob).to.be.instanceOf(Blob);_context6.next = 1;return ( + blob.arrayBuffer());case 1:arrayBuffer = _context6.sent; (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(arrayBuffer.byteLength).to.be.greaterThan(0); pngSignature = new Uint8Array(arrayBuffer.slice(0, 4)); @@ -29671,23 +29804,23 @@ describe("NativeEncoding", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[1]).to.equal(80); // 'P' (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[2]).to.equal(78); // 'N' (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[3]).to.equal(71); // 'G' - case 2:case "end":return _context3.stop();}}, _callee3);}));return _expectValidPNG.apply(this, arguments);} + case 2:case "end":return _context6.stop();}}, _callee5);}));return _expectValidPNG.apply(this, arguments);} - it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: - pixelData = new Uint8Array(4).fill(255);_context.next = 1;return ( - _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));case 1:result = _context.sent;_context.next = 2;return ( - expectValidPNG(result));case 2:case "end":return _context.stop();}}, _callee);})) + it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee3() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: + pixelData = new Uint8Array(4).fill(255);_context4.next = 1;return ( + _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));case 1:result = _context4.sent;_context4.next = 2;return ( + expectValidPNG(result));case 2:case "end":return _context4.stop();}}, _callee3);})) ); - it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee2() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: + it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee4() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: pixelDatas = []; for (i = 0; i < 10; i++) { pixelDatas.push(new Uint8Array(4).fill(255)); - }_context2.next = 1;return ( + }_context5.next = 1;return ( Promise.all(pixelDatas.map(function (pixelData) {return ( _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));} - )));case 1:results = _context2.sent;_context2.next = 2;return ( - Promise.all(results.map(function (b) {return expectValidPNG(b);})));case 2:case "end":return _context2.stop();}}, _callee2);})) + )));case 1:results = _context5.sent;_context5.next = 2;return ( + Promise.all(results.map(function (b) {return expectValidPNG(b);})));case 2:case "end":return _context5.stop();}}, _callee4);})) ); }); diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts index 6134bb2edc..c236afb707 100644 --- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts +++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts @@ -4,6 +4,7 @@ import { Buffer } from "buffer"; import { RequestFile, NativeEngine, + DynamicTexture, MeshBuilder, DefaultRenderingPipeline, RefractionPostProcess, @@ -34,6 +35,7 @@ declare const hostPlatform: string; declare const hasGpuRendering: boolean; declare const hasNativeImageLoading: boolean; declare const setExitCode: (code: number) => void; +declare const skipCanvasGpuTests: boolean; declare const _native: any; registerPngTests(describe, it, hasGpuRendering && hasNativeImageLoading); @@ -198,6 +200,138 @@ describe("Canvas2D", function () { ]; } + (skipCanvasGpuTests ? it.skip : it)( + "intersects nested clips and restores parent clips on the GPU", + async function () { + this.timeout(10000); + for (const translated of [false, true]) { + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const texture = new DynamicTexture("nested clips", 64, scene, false); + const ctx = texture.getContext(); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(8, 0, 24, 64); + ctx.clip(); + + ctx.save(); + if (translated) { + ctx.translate(16, 0); + } + ctx.beginPath(); + ctx.rect(0, 0, 64, 64); + ctx.clip(); + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + + ctx.fillStyle = "#00ff00"; + ctx.fillRect(24, 0, 16, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(40, 0, 16, 64); + ctx.clip(); + ctx.fillStyle = "magenta"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + ctx.restore(); + + ctx.fillStyle = "blue"; + ctx.fillRect(40, 0, 8, 64); + texture.update(false); + const pixels = await texture.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 GPU readback for the canvas texture"); + } + const pixel = (x: number) => + Array.from( + pixels.subarray( + (32 * 64 + x) * 4, + (32 * 64 + x + 1) * 4 + ) + ); + expect(pixel(4), "outside parent").to.deep.equal([ + 255, 255, 255, 255 + ]); + expect(pixel(12), "translated child boundary").to.deep.equal( + translated ? [255, 255, 255, 255] : [255, 0, 0, 255] + ); + expect(pixel(20), "inside intersection").to.deep.equal([ + 255, 0, 0, 255 + ]); + expect(pixel(28), "restored parent").to.deep.equal([ + 0, 255, 0, 255 + ]); + expect(pixel(36), "outside restored parent").to.deep.equal([ + 255, 255, 255, 255 + ]); + expect(pixel(44), "restored unclipped state").to.deep.equal([ + 0, 0, 255, 255 + ]); + expect(pixel(52), "disjoint clip").to.deep.equal([ + 255, 255, 255, 255 + ]); + expect(pixel(60), "outside every fill").to.deep.equal([ + 255, 255, 255, 255 + ]); + } finally { + scene.dispose(); + engine.dispose(); + } + } + } + ); + + (skipCanvasGpuTests ? it.skip : it)( + "clears only the clipped GPU region and ignores globalAlpha and filters", + async function () { + this.timeout(10000); + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const texture = new DynamicTexture("clipped clear", 64, scene, false); + const ctx = texture.getContext(); + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 64, 64); + texture.update(false); + + ctx.filter = "blur(2px)"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(8, 0, 24, 64); + ctx.clip(); + ctx.globalAlpha = 0.25; + ctx.clearRect(0, 0, 64, 64); + ctx.restore(); + texture.update(false); + const pixels = await texture.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 GPU readback for the canvas texture"); + } + const pixel = (x: number) => + Array.from( + pixels.subarray((32 * 64 + x) * 4, (32 * 64 + x + 1) * 4) + ); + expect(pixel(36), "preserved near clip").to.deep.equal([ + 255, 0, 0, 255 + ]); + expect(pixel(16), "fully cleared inside clip").to.deep.equal([ + 0, 0, 0, 0 + ]); + expect(pixel(44), "preserved after clip").to.deep.equal([ + 255, 0, 0, 255 + ]); + } finally { + scene.dispose(); + engine.dispose(); + } + } + ); + it("round-trips a string fillStyle and strokeStyle", function () { const ctx = createContext(); ctx.fillStyle = "#ff0000"; diff --git a/Apps/UnitTests/Source/Tests.JavaScript.cpp b/Apps/UnitTests/Source/Tests.JavaScript.cpp index d0dbbcbd42..d61a02b374 100644 --- a/Apps/UnitTests/Source/Tests.JavaScript.cpp +++ b/Apps/UnitTests/Source/Tests.JavaScript.cpp @@ -84,6 +84,11 @@ TEST(JavaScript, All) #else env.Global().Set("hasGpuRendering", true); #endif +#if defined(SKIP_RENDER_TESTS) || defined(SKIP_EXTERNAL_TEXTURE_TESTS) + env.Global().Set("skipCanvasGpuTests", true); +#else + env.Global().Set("skipCanvasGpuTests", false); +#endif Babylon::Polyfills::XMLHttpRequest::Initialize(env); Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) { diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index f3e2d74db4..306a3b3482 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -393,7 +393,11 @@ namespace Babylon::Polyfills::Internal const float height = info[3].As().FloatValue(); nvgSave(*m_nvg); - nvgGlobalCompositeOperation(*m_nvg, NVG_COPY); + // NanoVG's clip is shader coverage, so COPY would erase even outside the clip. + nvgGlobalCompositeOperation(*m_nvg, NVG_DESTINATION_OUT); + nvgGlobalAlpha(*m_nvg, 1.f); + nanovg_filterstack clearFilters; + nvgFilterStack(*m_nvg, clearFilters); // See FillRect: clipping is a scissor, so the path must always be reset. Resetting it // invalidates the emulated clip, which points at a path that no longer exists, and the @@ -404,7 +408,7 @@ namespace Babylon::Polyfills::Internal nvgClosePath(*m_nvg); - nvgFillColor(*m_nvg, TRANSPARENT_BLACK); + nvgFillColor(*m_nvg, nvgRGBA(0, 0, 0, 255)); nvgFill(*m_nvg); nvgRestore(*m_nvg); } @@ -551,7 +555,8 @@ namespace Babylon::Polyfills::Internal auto h = m_rectangleClipping.height != 0 ? m_rectangleClipping.height : m_canvas->GetHeight(); // expand clipping 1pix in each direction because nanovg AA gets cut a bit short. - nvgScissor(*m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1); + // A nested clip must not expand its parent's clipping region. + nvgIntersectScissor(*m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1); } void Context::StrokeRect(const Napi::CallbackInfo& info) diff --git a/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h b/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h index 8a330d3993..e3cbcf0b20 100644 --- a/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h +++ b/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h @@ -85,7 +85,7 @@ class nanovg_filterstack Blur blurElement; }; }; - int stackElementCount; + int stackElementCount{0}; static const int MAX_STACK_SIZE = 32; StackElement stackElements[MAX_STACK_SIZE]; From edd2f837bcfb51bbe1da151ffd572da7c209a694 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 10 Sep 2026 17:44:38 -0700 Subject: [PATCH 2/4] Initialize copied Canvas filter storage Zero-initialize the filter element array because NanoVG copies the full filter stack into state and draw calls. Clarify the existing asymmetric anti-alias clip extension without changing its bounds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- Polyfills/Canvas/Source/Context.cpp | 2 +- Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index 306a3b3482..f7e660f5eb 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -554,7 +554,7 @@ namespace Babylon::Polyfills::Internal auto w = m_rectangleClipping.width != 0 ? m_rectangleClipping.width : m_canvas->GetWidth(); auto h = m_rectangleClipping.height != 0 ? m_rectangleClipping.height : m_canvas->GetHeight(); - // expand clipping 1pix in each direction because nanovg AA gets cut a bit short. + // Extend the clip one pixel toward the left/top because NanoVG AA gets cut a bit short. // A nested clip must not expand its parent's clipping region. nvgIntersectScissor(*m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1); } diff --git a/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h b/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h index e3cbcf0b20..7e987005df 100644 --- a/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h +++ b/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h @@ -87,7 +87,7 @@ class nanovg_filterstack }; int stackElementCount{0}; static const int MAX_STACK_SIZE = 32; - StackElement stackElements[MAX_STACK_SIZE]; + StackElement stackElements[MAX_STACK_SIZE]{}; private: std::vector CalculateGaussianKernel(float sigma, int kernelSize); From aa757fb90505b82f47de69603db19a8431d0cbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Wed, 16 Sep 2026 11:07:11 -0700 Subject: [PATCH 3/4] Wait for the OpenPBR validation scene's mesh import The snippet returns its scene before ImportMeshAsync and onMeshLoad finish, so readiness can capture an empty scene. Preserve the import promise and return the scene only after its completion, using the existing per-test source replacement support. A delayed import reproduced every pixel of the blank CI image on both the PR and its upstream base. The scene now passes with the same delay, without changing the reference image, render count, or tolerance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48fa7ec8-0cf6-48a0-b836-10b7ace38c1a --- Apps/Playground/Scripts/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 632241e78c..43239db430 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -3766,6 +3766,7 @@ { "title": "OpenPBR Transmission Roughness vs IOR - Analytic Lights", "playgroundId": "#GRQHVV#134", + "replace": "BABYLON.ImportMeshAsync, var meshLoad = BABYLON.ImportMeshAsync, return scene;, return Promise.resolve(meshLoad).then(function () { return scene; });", "referenceImage": "OpenPBR-Transmission-Roughness-vs-IOR---Analytic-Lights.png" }, { From 763b288e616239b2b288f2025dc79ca3dda7e5d7 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 17 Sep 2026 12:56:29 -0700 Subject: [PATCH 4/4] Normalize signed Canvas clip rectangles Normalize negative rectangle extents before NanoVG scissor intersection, preserving parent bounds, transforms, and the existing AA adjustment. Add GPU readback coverage for both signed axes and rotated nested clips, comparing all pixels with equivalent positive rectangles. The regression fails against the previous PR head and passes with the normalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- .../JavaScript/dist/tests.javaScript.all.js | 195 ++++++++++++------ .../JavaScript/src/tests.javaScript.all.ts | 71 +++++++ Polyfills/Canvas/Source/Context.cpp | 5 +- 3 files changed, 208 insertions(+), 63 deletions(-) diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index 9836a9b1ac..a874301b13 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js +++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js @@ -28631,12 +28631,83 @@ describe("Canvas2D", function () { ); (skipCanvasGpuTests ? it.skip : it)( - "clears only the clipped GPU region and ignores globalAlpha and filters", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( - function _callee2() {var engine, scene, texture, ctx, _pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: + "normalizes negative rectangle dimensions before intersecting GPU clips", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( + function _callee2() {var engine, scene, texture, ctx, _i2, _arr2, rotated, expected, _i3, _arr3, flips, flipX, flipY, _pixels, description, changed, index;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: this.timeout(10000); engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.NativeEngine(); scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Scene(engine);_context3.prev = 1; + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.DynamicTexture("signed clips", 64, scene, false); + ctx = texture.getContext();_i2 = 0, _arr2 = + [false, true];case 2:if (!(_i2 < _arr2.length)) {_context3.next = 8;break;}rotated = _arr2[_i2]; + expected = void 0;_i3 = 0, _arr3 = + [ + [false, false], [true, false], [false, true], [true, true]];case 3:if (!(_i3 < _arr3.length)) {_context3.next = 7;break;}flips = _arr3[_i3]; + + flipX = flips[0]; + flipY = flips[1]; + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(18, 12, 36, 44); + ctx.clip(); + ctx.save(); + if (rotated) { + ctx.translate(32, 32); + ctx.rotate(0.3); + ctx.translate(-32, -32); + } + ctx.beginPath(); + ctx.rect( + flipX ? 40 : 16, flipY ? 40 : 20, + flipX ? -24 : 24, flipY ? -20 : 20 + ); + ctx.clip(); + ctx.fillStyle = "blue"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + ctx.restore(); + texture.update(false);_context3.next = 4;return ( + + texture.readPixels());case 4:_pixels = _context3.sent;if ( + _pixels instanceof Uint8Array) {_context3.next = 5;break;}throw ( + new Error("Expected RGBA8 GPU readback for signed clips"));case 5: + + description = "rotated=".concat( + rotated, ", flipX=").concat(flipX, ", flipY=").concat(flipY); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels, 64, 28, 30), "inside clip, ".concat(description)). + to.deep.equal([0, 0, 255, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels, 64, 0, 0), "outside parent clip"). + to.deep.equal([255, 255, 255, 255]); + if (expected) { + changed = 0; + for (index = 0; index < _pixels.length; ++index) { + if (_pixels[index] !== expected[index]) { + ++changed; + } + } + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(changed, "signed clip equivalence, ".concat(description)). + to.equal(0); + } else { + expected = _pixels.slice(); + }case 6:_i3++;_context3.next = 3;break;case 7:_i2++;_context3.next = 2;break;case 8:_context3.prev = 8; + + + + scene.dispose(); + engine.dispose();return _context3.finish(8);case 9:case "end":return _context3.stop();}}, _callee2, this, [[1,, 8, 9]]);})) + + + ); + + (skipCanvasGpuTests ? it.skip : it)( + "clears only the clipped GPU region and ignores globalAlpha and filters", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( + function _callee3() {var engine, scene, texture, ctx, _pixels2, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: + this.timeout(10000); + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Scene(engine);_context4.prev = 1; + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.DynamicTexture("clipped clear", 64, scene, false); ctx = texture.getContext(); ctx.fillStyle = "red"; @@ -28652,14 +28723,14 @@ describe("Canvas2D", function () { ctx.globalAlpha = 0.25; ctx.clearRect(0, 0, 64, 64); ctx.restore(); - texture.update(false);_context3.next = 2;return ( - texture.readPixels());case 2:_pixels = _context3.sent;if ( - _pixels instanceof Uint8Array) {_context3.next = 3;break;}throw ( + texture.update(false);_context4.next = 2;return ( + texture.readPixels());case 2:_pixels2 = _context4.sent;if ( + _pixels2 instanceof Uint8Array) {_context4.next = 3;break;}throw ( new Error("Expected RGBA8 GPU readback for the canvas texture"));case 3: pixel = function pixel(x) {return ( Array.from( - _pixels.subarray((32 * 64 + x) * 4, (32 * 64 + x + 1) * 4) + _pixels2.subarray((32 * 64 + x) * 4, (32 * 64 + x + 1) * 4) ));}; (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(36), "preserved near clip").to.deep.equal([ 255, 0, 0, 255] @@ -28669,10 +28740,10 @@ describe("Canvas2D", function () { ); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixel(44), "preserved after clip").to.deep.equal([ 255, 0, 0, 255] - );case 4:_context3.prev = 4; + );case 4:_context4.prev = 4; scene.dispose(); - engine.dispose();return _context3.finish(4);case 5:case "end":return _context3.stop();}}, _callee2, this, [[1,, 4, 5]]);})) + engine.dispose();return _context4.finish(4);case 5:case "end":return _context4.stop();}}, _callee3, this, [[1,, 4, 5]]);})) ); @@ -29109,14 +29180,14 @@ describe("Canvas2D", function () { 8.5 ); - var _pixels2 = captureGpuPixels(destination.canvas); + var _pixels3 = captureGpuPixels(destination.canvas); // Pixel (12,3), sampled at its center (12.5,3.5), maps to source // x = 5.99 + (12.5 - 2.75) * 1.01 / 24.5 = 6.39194. // Between red texel center 5.5 and green center 6.5, ideal bilinear // weights are 10.8% red / 89.2% green (R~28, G~227). // Integer truncation maps x to 5.4375; ignoring the crop maps x to // 5.25. Both negative controls are solid red at this sample. - var green = pixelAt(_pixels2, destination.canvas.width, 12, 3); + var green = pixelAt(_pixels3, destination.canvas.width, 12, 3); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(green[1] - green[0]).to.be.greaterThan(100); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(green[3]).to.be.greaterThan(200); @@ -29125,12 +29196,12 @@ describe("Canvas2D", function () { // That is 18.3% top red / 81.7% bottom blue (R~47, B~208). // Integer truncation maps y to 3.4375 and ignoring the crop maps y // to 3.5, both solid red rather than blue. - var blue = pixelAt(_pixels2, destination.canvas.width, 3, 14); + var blue = pixelAt(_pixels3, destination.canvas.width, 3, 14); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blue[2] - blue[0]).to.be.greaterThan(80); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blue[3]).to.be.greaterThan(200); var fractionalEdge = pixelAt( - _pixels2, + _pixels3, destination.canvas.width, 26, 3 @@ -29166,14 +29237,14 @@ describe("Canvas2D", function () { -6 ); - var _pixels3 = captureGpuPixels(destination.canvas); - var inside = pixelAt(_pixels3, destination.canvas.width, 7, 7); + var _pixels4 = captureGpuPixels(destination.canvas); + var inside = pixelAt(_pixels4, destination.canvas.width, 7, 7); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[1]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[2]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[3]).to.be.greaterThan(220); - var outside = pixelAt(_pixels3, destination.canvas.width, 2, 7); + var outside = pixelAt(_pixels4, destination.canvas.width, 2, 7); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[3]).to.equal(0); } finally { disposeCanvas(destination); @@ -29199,10 +29270,10 @@ describe("Canvas2D", function () { 12 ); - var _pixels4 = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels4, destination.canvas.width, 5, 5)[3]).to.equal(0); + var _pixels5 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels5, destination.canvas.width, 5, 5)[3]).to.equal(0); - var inside = pixelAt(_pixels4, destination.canvas.width, 10, 10); + var inside = pixelAt(_pixels5, destination.canvas.width, 10, 10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[1]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(inside[2]).to.be.greaterThan(220); @@ -29225,14 +29296,14 @@ describe("Canvas2D", function () { source.context.fillRect(0, 0, 8, 8); destination.context.drawImage(source.canvas, 8, 0, 8, 8); - var _pixels5 = captureGpuPixels(destination.canvas); - var first = pixelAt(_pixels5, destination.canvas.width, 4, 4); + var _pixels6 = captureGpuPixels(destination.canvas); + var first = pixelAt(_pixels6, destination.canvas.width, 4, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[2]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(first[3]).to.be.greaterThan(220); - var second = pixelAt(_pixels5, destination.canvas.width, 12, 4); + var second = pixelAt(_pixels6, destination.canvas.width, 12, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(second[2]).to.be.greaterThan(220); @@ -29273,14 +29344,14 @@ describe("Canvas2D", function () { 1 ); - var _pixels6 = captureGpuPixels(destination.canvas); - var putSample = pixelAt(_pixels6, destination.canvas.width, 2, 2); + var _pixels7 = captureGpuPixels(destination.canvas); + var putSample = pixelAt(_pixels7, destination.canvas.width, 2, 2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[1]).to.be.within(75, 115); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[2]).to.be.within(15, 50); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(putSample[3]).to.be.greaterThan(240); - var drawSample = pixelAt(_pixels6, destination.canvas.width, 8, 2); + var drawSample = pixelAt(_pixels7, destination.canvas.width, 8, 2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[0]).to.be.within(75, 115); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[1]).to.be.within(30, 70); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(drawSample[2]).to.be.greaterThan(220); @@ -29321,15 +29392,15 @@ describe("Canvas2D", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(transform.e).to.equal(2); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(transform.f).to.equal(1); - var _pixels7 = captureGpuPixels(destination.canvas); - var cropped = pixelAt(_pixels7, destination.canvas.width, 5, 4); + var _pixels8 = captureGpuPixels(destination.canvas); + var cropped = pixelAt(_pixels8, destination.canvas.width, 5, 4); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[0]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[1]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[2]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(cropped[3]).to.be.greaterThan(240); var translatedEdge = pixelAt( - _pixels7, + _pixels8, destination.canvas.width, 8, 4 @@ -29339,7 +29410,7 @@ describe("Canvas2D", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(translatedEdge[2]).to.be.within(100, 155); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(translatedEdge[3]).to.be.greaterThan(240); - var clippedOut = pixelAt(_pixels7, destination.canvas.width, 5, 8); + var clippedOut = pixelAt(_pixels8, destination.canvas.width, 5, 8); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[0]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[1]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(clippedOut[2]).to.be.lessThan(10); @@ -29368,8 +29439,8 @@ describe("Canvas2D", function () { destination.context.drawImage(source.canvas, 2, 2, 4, 4); destination.context.drawImage(source.canvas, 10, 8, 4, 4); - var _pixels8 = captureGpuPixels(destination.canvas); - var firstDraw = pixelAt(_pixels8, destination.canvas.width, 3, 3); + var _pixels9 = captureGpuPixels(destination.canvas); + var firstDraw = pixelAt(_pixels9, destination.canvas.width, 3, 3); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[0]).to.be.greaterThan(220); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[1]).to.be.lessThan(30); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(firstDraw[2]).to.be.lessThan(30); @@ -29377,9 +29448,9 @@ describe("Canvas2D", function () { [ // Inside the triangle but outside both destination rectangles. - pixelAt(_pixels8, destination.canvas.width, 2, 8), + pixelAt(_pixels9, destination.canvas.width, 2, 8), // Inside the second destination rectangle but outside the triangle. - pixelAt(_pixels8, destination.canvas.width, 12, 9)]. + pixelAt(_pixels9, destination.canvas.width, 12, 9)]. forEach(function (outside) { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[0]).to.be.lessThan(10); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(outside[1]).to.be.lessThan(10); @@ -29400,13 +29471,13 @@ describe("Canvas2D", function () { source.context.fillRect(0, 0, 2, 2); destination.context.drawImage(source.canvas, 1.75, 1.75, 2.5, 2.5); - var _pixels9 = destination.context.getImageData(0, 0, 8, 8).data; - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels9, destination.canvas.width, 2, 2)[3]).to.equal(255); + var _pixels0 = destination.context.getImageData(0, 0, 8, 8).data; + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels0, destination.canvas.width, 2, 2)[3]).to.equal(255); [ - pixelAt(_pixels9, destination.canvas.width, 1, 2), - pixelAt(_pixels9, destination.canvas.width, 4, 2), - pixelAt(_pixels9, destination.canvas.width, 2, 1), - pixelAt(_pixels9, destination.canvas.width, 2, 4)]. + pixelAt(_pixels0, destination.canvas.width, 1, 2), + pixelAt(_pixels0, destination.canvas.width, 4, 2), + pixelAt(_pixels0, destination.canvas.width, 2, 1), + pixelAt(_pixels0, destination.canvas.width, 2, 4)]. forEach(function (edge) { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(edge[3]).to.be.within(40, 90); }); @@ -29426,9 +29497,9 @@ describe("Canvas2D", function () { resource.context.fillStyle = "#0000ff"; resource.context.fillRect(0, 0, 4, 8); - var _pixels0 = captureGpuPixels(resource.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels0, 8, 2, 2)).to.deep.equal([0, 0, 255, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels0, 8, 6, 6)).to.deep.equal([255, 0, 0, 255]); + var _pixels1 = captureGpuPixels(resource.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels1, 8, 2, 2)).to.deep.equal([0, 0, 255, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels1, 8, 6, 6)).to.deep.equal([255, 0, 0, 255]); } finally { disposeCanvas(resource); } @@ -29442,9 +29513,9 @@ describe("Canvas2D", function () { source.context.fillRect(4, 4, 8, 8); destination.context.filter = "blur(2px)"; destination.context.drawImage(source.canvas, 0, 0); - var _pixels1 = captureGpuPixels(destination.canvas); - var fringe = pixelAt(_pixels1, 16, 3, 8); - var center = pixelAt(_pixels1, 16, 8, 8); + var _pixels10 = captureGpuPixels(destination.canvas); + var fringe = pixelAt(_pixels10, 16, 3, 8); + var center = pixelAt(_pixels10, 16, 8, 8); // The unfiltered CPU copy has zero alpha outside the source ink. (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(fringe[0]).to.equal(fringe[1]); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(fringe[1]).to.equal(fringe[2]); @@ -29471,9 +29542,9 @@ describe("Canvas2D", function () { } }, 0); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(conversions).to.equal(1); - var _pixels10 = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels10, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels10, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); + var _pixels11 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); } finally { disposeCanvas(destination); disposeCanvas(source); @@ -29498,9 +29569,9 @@ describe("Canvas2D", function () { } }, 0); (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(conversions).to.equal(1); - var _pixels11 = captureGpuPixels(destination.canvas); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels11, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); + var _pixels12 = captureGpuPixels(destination.canvas); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels12, 8, 2, 2)).to.deep.equal([255, 0, 0, 255]); + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pixelAt(_pixels12, 8, 6, 2)).to.deep.equal([0, 0, 0, 0]); } finally { Object.setPrototypeOf(source.canvas, prototype); disposeCanvas(destination); @@ -29794,9 +29865,9 @@ describe("PostProcesses", function () { describe("NativeEncoding", function () { this.timeout(0);function - expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee5(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0: - (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blob).to.be.instanceOf(Blob);_context6.next = 1;return ( - blob.arrayBuffer());case 1:arrayBuffer = _context6.sent; + expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee6(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context7) {while (1) switch (_context7.prev = _context7.next) {case 0: + (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blob).to.be.instanceOf(Blob);_context7.next = 1;return ( + blob.arrayBuffer());case 1:arrayBuffer = _context7.sent; (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(arrayBuffer.byteLength).to.be.greaterThan(0); pngSignature = new Uint8Array(arrayBuffer.slice(0, 4)); @@ -29804,23 +29875,23 @@ describe("NativeEncoding", function () { (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[1]).to.equal(80); // 'P' (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[2]).to.equal(78); // 'N' (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[3]).to.equal(71); // 'G' - case 2:case "end":return _context6.stop();}}, _callee5);}));return _expectValidPNG.apply(this, arguments);} + case 2:case "end":return _context7.stop();}}, _callee6);}));return _expectValidPNG.apply(this, arguments);} - it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee3() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: - pixelData = new Uint8Array(4).fill(255);_context4.next = 1;return ( - _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));case 1:result = _context4.sent;_context4.next = 2;return ( - expectValidPNG(result));case 2:case "end":return _context4.stop();}}, _callee3);})) + it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee4() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: + pixelData = new Uint8Array(4).fill(255);_context5.next = 1;return ( + _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));case 1:result = _context5.sent;_context5.next = 2;return ( + expectValidPNG(result));case 2:case "end":return _context5.stop();}}, _callee4);})) ); - it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee4() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: + it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee5() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0: pixelDatas = []; for (i = 0; i < 10; i++) { pixelDatas.push(new Uint8Array(4).fill(255)); - }_context5.next = 1;return ( + }_context6.next = 1;return ( Promise.all(pixelDatas.map(function (pixelData) {return ( _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));} - )));case 1:results = _context5.sent;_context5.next = 2;return ( - Promise.all(results.map(function (b) {return expectValidPNG(b);})));case 2:case "end":return _context5.stop();}}, _callee4);})) + )));case 1:results = _context6.sent;_context6.next = 2;return ( + Promise.all(results.map(function (b) {return expectValidPNG(b);})));case 2:case "end":return _context6.stop();}}, _callee5);})) ); }); diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts index c236afb707..beb107ec8c 100644 --- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts +++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts @@ -285,6 +285,77 @@ describe("Canvas2D", function () { } ); + (skipCanvasGpuTests ? it.skip : it)( + "normalizes negative rectangle dimensions before intersecting GPU clips", + async function () { + this.timeout(10000); + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const texture = new DynamicTexture("signed clips", 64, scene, false); + const ctx = texture.getContext(); + for (const rotated of [false, true]) { + let expected: Uint8Array | undefined; + for (const flips of [ + [false, false], [true, false], [false, true], [true, true] + ]) { + const flipX = flips[0]; + const flipY = flips[1]; + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 64, 64); + ctx.save(); + ctx.beginPath(); + ctx.rect(18, 12, 36, 44); + ctx.clip(); + ctx.save(); + if (rotated) { + ctx.translate(32, 32); + ctx.rotate(0.3); + ctx.translate(-32, -32); + } + ctx.beginPath(); + ctx.rect( + flipX ? 40 : 16, flipY ? 40 : 20, + flipX ? -24 : 24, flipY ? -20 : 20 + ); + ctx.clip(); + ctx.fillStyle = "blue"; + ctx.fillRect(0, 0, 64, 64); + ctx.restore(); + ctx.restore(); + texture.update(false); + + const pixels = await texture.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 GPU readback for signed clips"); + } + const description = + `rotated=${rotated}, flipX=${flipX}, flipY=${flipY}`; + expect(pixelAt(pixels, 64, 28, 30), `inside clip, ${description}`) + .to.deep.equal([0, 0, 255, 255]); + expect(pixelAt(pixels, 64, 0, 0), "outside parent clip") + .to.deep.equal([255, 255, 255, 255]); + if (expected) { + let changed = 0; + for (let index = 0; index < pixels.length; ++index) { + if (pixels[index] !== expected[index]) { + ++changed; + } + } + expect(changed, `signed clip equivalence, ${description}`) + .to.equal(0); + } else { + expected = pixels.slice(); + } + } + } + } finally { + scene.dispose(); + engine.dispose(); + } + } + ); + (skipCanvasGpuTests ? it.skip : it)( "clears only the clipped GPU region and ignores globalAlpha and filters", async function () { diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index f7e660f5eb..898b4f01fc 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -553,10 +553,13 @@ namespace Babylon::Polyfills::Internal //By default m_rectangleClipping is not set, in this case we use the canvas width and height. auto w = m_rectangleClipping.width != 0 ? m_rectangleClipping.width : m_canvas->GetWidth(); auto h = m_rectangleClipping.height != 0 ? m_rectangleClipping.height : m_canvas->GetHeight(); + // Canvas rectangles can extend left/up; NanoVG scissors require positive extents. + const auto left = m_rectangleClipping.left + std::min(w, 0.f); + const auto top = m_rectangleClipping.top + std::min(h, 0.f); // Extend the clip one pixel toward the left/top because NanoVG AA gets cut a bit short. // A nested clip must not expand its parent's clipping region. - nvgIntersectScissor(*m_nvg, m_rectangleClipping.left - 1, m_rectangleClipping.top - 1, w + 1, h + 1); + nvgIntersectScissor(*m_nvg, left - 1, top - 1, std::abs(w) + 1, std::abs(h) + 1); } void Context::StrokeRect(const Napi::CallbackInfo& info)