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" }, { diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index 3974dd1466..a874301b13 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,209 @@ 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)( + "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"; + 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);_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( + _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] + ); + (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:_context4.prev = 4; + + scene.dispose(); + engine.dispose();return _context4.finish(4);case 5:case "end":return _context4.stop();}}, _callee3, this, [[1,, 4, 5]]);})) + + + ); + it("round-trips a string fillStyle and strokeStyle", function () { var ctx = createContext(); ctx.fillStyle = "#ff0000"; @@ -28976,14 +29180,14 @@ describe("Canvas2D", function () { 8.5 ); - var pixels = 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(pixels, 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); @@ -28992,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(pixels, 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( - pixels, + _pixels3, destination.canvas.width, 26, 3 @@ -29033,14 +29237,14 @@ describe("Canvas2D", function () { -6 ); - var pixels = captureGpuPixels(destination.canvas); - var inside = pixelAt(pixels, 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(pixels, 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); @@ -29066,10 +29270,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 _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(pixels, 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); @@ -29092,14 +29296,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 _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(pixels, 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); @@ -29140,14 +29344,14 @@ describe("Canvas2D", function () { 1 ); - var pixels = captureGpuPixels(destination.canvas); - var putSample = pixelAt(pixels, 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(pixels, 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); @@ -29188,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 pixels = captureGpuPixels(destination.canvas); - var cropped = pixelAt(pixels, 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( - pixels, + _pixels8, destination.canvas.width, 8, 4 @@ -29206,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(pixels, 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); @@ -29235,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 pixels = captureGpuPixels(destination.canvas); - var firstDraw = pixelAt(pixels, 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); @@ -29244,9 +29448,9 @@ describe("Canvas2D", function () { [ // Inside the triangle but outside both destination rectangles. - pixelAt(pixels, destination.canvas.width, 2, 8), + pixelAt(_pixels9, destination.canvas.width, 2, 8), // Inside the second destination rectangle but outside the triangle. - pixelAt(pixels, 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); @@ -29267,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 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 _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(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(_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); }); @@ -29293,9 +29497,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 _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); } @@ -29309,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 pixels = captureGpuPixels(destination.canvas); - var fringe = pixelAt(pixels, 16, 3, 8); - var center = pixelAt(pixels, 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]); @@ -29338,9 +29542,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 { disposeCanvas(destination); disposeCanvas(source); @@ -29365,9 +29569,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 _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); @@ -29661,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 _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 _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)); @@ -29671,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 _context3.stop();}}, _callee3);}));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 _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 _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 _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 _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)); - }_context2.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 = _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 = _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 6134bb2edc..beb107ec8c 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,209 @@ 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)( + "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 () { + 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..898b4f01fc 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); } @@ -549,9 +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); - // 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); + // 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, left - 1, top - 1, std::abs(w) + 1, std::abs(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..7e987005df 100644 --- a/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h +++ b/Polyfills/Canvas/Source/nanovg/nanovg_filterstack.h @@ -85,9 +85,9 @@ class nanovg_filterstack Blur blurElement; }; }; - int stackElementCount; + 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);