diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index b41af4515e..42a8ab2fb2 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -48,15 +48,20 @@ set(SOURCES "Source/Tests.JavaScript.cpp" "Source/Tests.NativeEngine.InstanceData.cpp" "Source/Tests.NativeEngine.ImageFormats.cpp" + "Source/Tests.NativeEngine.StorageBuffer.cpp" + "Source/Tests.NativeEngine.Compute.cpp" "Source/Tests.NativeEngine.Mipmaps.cpp" + "Source/Tests.NativeEngine.PrimitiveModeExpansion.cpp" "Source/Tests.NativeEngine.DepthTextures.cpp" "Source/Tests.NativeEngine.CubeRenderTargets.cpp" "Source/Tests.NativeEngine.Msaa.cpp" "Source/Tests.NativeEngine.Teardown.cpp" + "Source/Tests.NativeEngine.TransformFeedback.cpp" "Source/Tests.NativeEngine.TextureFormats.cpp" "Source/Tests.NativeOptimizations.cpp" "Source/Tests.NativeEngine.VertexArray.cpp" "Source/Tests.ShaderCache.cpp" + "Source/Tests.ShaderCache.TransformFeedback.cpp" "Source/Tests.ShaderCompilation.cpp" "Source/Tests.ShaderCompilation.FragCoord.cpp" "Source/Tests.UniformPadding.cpp" @@ -112,6 +117,7 @@ target_link_libraries(UnitTests PRIVATE NativeEncoding PRIVATE ScriptLoader PRIVATE ShaderCache + PRIVATE ShaderCacheInternal PRIVATE Window PRIVATE XMLHttpRequest PRIVATE gtest_main @@ -186,9 +192,13 @@ if(GRAPHICS_API STREQUAL "OpenGL") # OpenGL the "device" is a context bgfx created, and a bare context carries no display or # surface, so Helpers::CreateDevice cannot make one current to allocate against. # See https://github.com/BabylonJS/BabylonNative/issues/1810. + # + # Flipped sampling: the sampler and texture row flips these tests cover only run on + # top-left-origin backends; GLSL samples in GL's native bottom-left orientation. target_compile_definitions(UnitTests PRIVATE SKIP_MULTISAMPLE_TESTS - SKIP_DEVICE_LOSS_TESTS) + SKIP_DEVICE_LOSS_TESTS + SKIP_FLIPPED_SAMPLING_TESTS) endif() add_test(NAME UnitTests COMMAND UnitTests) diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index 24de5e6cf7..61cc0ef10c 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js +++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js @@ -135,10 +135,10 @@ function fillImageData(context, width, height, pixel) {var dx = arguments.length context.putImageData(imageData, dx, dy); }function -readTexture(_x) {return _readTexture.apply(this, arguments);}function _readTexture() {_readTexture = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee5(texture) {var pixels, _texture$getSize, width, height, stride, topDown, y;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: - texture.update(false);_context5.next = 1;return ( - texture.readPixels());case 1:pixels = _context5.sent;if ( - pixels instanceof Uint8Array) {_context5.next = 2;break;}throw ( +readTexture(_x) {return _readTexture.apply(this, arguments);}function _readTexture() {_readTexture = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee6(texture) {var premulAlpha,pixels,_texture$getSize,width,height,stride,topDown,y,_args7 = arguments;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context7) {while (1) switch (_context7.prev = _context7.next) {case 0:premulAlpha = _args7.length > 1 && _args7[1] !== undefined ? _args7[1] : false; + texture.update(false, premulAlpha);_context7.next = 1;return ( + texture.readPixels());case 1:pixels = _context7.sent;if ( + pixels instanceof Uint8Array) {_context7.next = 2;break;}throw ( new Error("Expected RGBA8 GPU readback for the canvas texture"));case 2: // Native readback uses WebGL's bottom-up row order; assertions use Canvas coordinates. @@ -147,8 +147,8 @@ readTexture(_x) {return _readTexture.apply(this, arguments);}function _readTextu topDown = new Uint8Array(pixels.length); for (y = 0; y < height; ++y) { topDown.set(pixels.subarray(y * stride, (y + 1) * stride), (height - 1 - y) * stride); - }return _context5.abrupt("return", - topDown);case 3:case "end":return _context5.stop();}}, _callee5);}));return _readTexture.apply(this, arguments);} + }return _context7.abrupt("return", + topDown);case 3:case "end":return _context7.stop();}}, _callee6);}));return _readTexture.apply(this, arguments);} function registerCanvasImageTests( @@ -160,9 +160,47 @@ skipCanvasGpuTests) this.timeout(10000); var test = skipCanvasGpuTests ? it.skip : it; - test("keeps putImageData pixels alive through the GPU flush", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {var engine, scene, texture, pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + test("copies regenerated Canvas mipmaps in the requested alpha representation", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {var engine, scene, _texture$getInternalT, texture, context, _i, _arr, premulAlpha, _loop, _i2, _arr2;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context.prev = 1; + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context2.prev = 1; + + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.DynamicTexture("Canvas mip chain", { width: 8, height: 8 }, scene, true, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_TRILINEAR_SAMPLINGMODE); + if (!((_texture$getInternalT = texture.getInternalTexture()) !== null && _texture$getInternalT !== void 0 && _texture$getInternalT.generateMipMaps)) { + this.skip(); + } + context = texture.getContext();_i = 0, _arr = + [false, true];case 2:if (!(_i < _arr.length)) {_context2.next = 6;break;}premulAlpha = _arr[_i];_loop = /*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _loop() {var blue, _i3, _arr3, mip, pixels, rgba, size, half, expected, index;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + blue = _arr2[_i2]; + context.clearRect(0, 0, 8, 8); + fillImageData(context, 8, 8, function (x, y) {return ( + (x + y) % 2 === 0 ? [255, 0, 0, 102] : blue ? [0, 0, 255, 102] : [0, 255, 0, 102]);} + ); + texture.update(false, premulAlpha);_i3 = 0, _arr3 = + [1, 2, 3];case 1:if (!(_i3 < _arr3.length)) {_context.next = 5;break;}mip = _arr3[_i3];_context.next = 2;return ( + texture.readPixels(0, mip));case 2:pixels = _context.sent;if ( + pixels instanceof Uint8Array) {_context.next = 3;break;}throw ( + new Error("Expected RGBA8 Canvas mip readback"));case 3: + + rgba = pixels; + size = 8 >> mip; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(rgba.length).to.equal(size * size * 4); + half = premulAlpha ? 51 : 128; + expected = blue ? [half, 0, half, 102] : [half, half, 0, 102]; + for (index = 0; index < rgba.length; ++index) { + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(rgba[index], "mip ".concat(mip, ", byte ").concat(index)).to.be.closeTo(expected[index % 4], 1); + }case 4:_i3++;_context.next = 1;break;case 5:case "end":return _context.stop();}}, _loop);});_i2 = 0, _arr2 = [true, false];case 3:if (!(_i2 < _arr2.length)) {_context2.next = 5;break;}return _context2.delegateYield(_loop(), "t0", 4);case 4:_i2++;_context2.next = 3;break;case 5:_i++;_context2.next = 2;break;case 6:_context2.prev = 6; + + + + + scene.dispose(); + engine.dispose();return _context2.finish(6);case 7:case "end":return _context2.stop();}}, _callee, this, [[1,, 6, 7]]);})) + + ); + + test("keeps putImageData pixels alive through the GPU flush", /*#__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, pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context3.prev = 1; texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.DynamicTexture( "putImageData lifetime", @@ -171,22 +209,22 @@ skipCanvasGpuTests) false, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_NEAREST_SAMPLINGMODE ); - fillImageData(texture.getContext(), 4, 3, function () {return [0, 255, 255, 255];}, 3, 2);_context.next = 2;return ( + fillImageData(texture.getContext(), 4, 3, function () {return [0, 255, 255, 255];}, 3, 2);_context3.next = 2;return ( - readTexture(texture));case 2:pixels = _context.sent; + readTexture(texture));case 2:pixels = _context3.sent; pixel = function pixel(x, y) {return Array.from(pixels.subarray((y * 10 + x) * 4, (y * 10 + x + 1) * 4));}; (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 3), "inside putImageData rectangle").to.deep.equal([0, 255, 255, 255]); (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 3), "left of putImageData rectangle").to.deep.equal([0, 0, 0, 0]); - (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 5), "below putImageData rectangle").to.deep.equal([0, 0, 0, 0]);case 3:_context.prev = 3; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 5), "below putImageData rectangle").to.deep.equal([0, 0, 0, 0]);case 3:_context3.prev = 3; scene.dispose(); - engine.dispose();return _context.finish(3);case 4:case "end":return _context.stop();}}, _callee, null, [[1,, 3, 4]]);})) + engine.dispose();return _context3.finish(3);case 4:case "end":return _context3.stop();}}, _callee2, null, [[1,, 3, 4]]);})) ); - test("draws an offscreen canvas at a nonzero destination", /*#__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, source, texture, _pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: + test("draws an offscreen canvas at a nonzero destination", /*#__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, source, texture, _pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context2.prev = 1; + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context4.prev = 1; source = engine.createCanvas(4, 4); fillImageData(source.getContext("2d"), 4, 4, function () {return [255, 0, 255, 255];}); @@ -198,24 +236,24 @@ skipCanvasGpuTests) false, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_NEAREST_SAMPLINGMODE ); - texture.getContext().drawImage(source, 3, 2);_context2.next = 2;return ( + texture.getContext().drawImage(source, 3, 2);_context4.next = 2;return ( - readTexture(texture));case 2:_pixels = _context2.sent; + readTexture(texture));case 2:_pixels = _context4.sent; pixel = function pixel(x, y) {return Array.from(_pixels.subarray((y * 12 + x) * 4, (y * 12 + x + 1) * 4));}; (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 3), "inside copied canvas").to.deep.equal([255, 0, 255, 255]); (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 3), "left of copied canvas").to.deep.equal([0, 0, 0, 0]); - (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 6), "below copied canvas").to.deep.equal([0, 0, 0, 0]);case 3:_context2.prev = 3; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 6), "below copied canvas").to.deep.equal([0, 0, 0, 0]);case 3:_context4.prev = 3; scene.dispose(); - engine.dispose();return _context2.finish(3);case 4:case "end":return _context2.stop();}}, _callee2, null, [[1,, 3, 4]]);})) + engine.dispose();return _context4.finish(3);case 4:case "end":return _context4.stop();}}, _callee3, null, [[1,, 3, 4]]);})) ); - test("draws an ImageBitmap source through the GPU path", /*#__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, bitmap, texture, _pixels2, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: + test("draws an ImageBitmap source through the GPU path", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee4() {var engine, scene, bitmap, texture, _pixels2, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context3.prev = 1;_context3.next = 2;return ( + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context5.prev = 1;_context5.next = 2;return ( - engine._createImageBitmapFromSource(opaqueOrangePng));case 2:bitmap = _context3.sent; + engine._createImageBitmapFromSource(opaqueOrangePng));case 2:bitmap = _context5.sent; (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(bitmap.width).to.equal(4); (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(bitmap.height).to.equal(4); @@ -226,21 +264,21 @@ skipCanvasGpuTests) false, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_NEAREST_SAMPLINGMODE ); - texture.getContext().drawImage(bitmap, 3, 2);_context3.next = 3;return ( + texture.getContext().drawImage(bitmap, 3, 2);_context5.next = 3;return ( - readTexture(texture));case 3:_pixels2 = _context3.sent; + readTexture(texture));case 3:_pixels2 = _context5.sent; pixel = function pixel(x, y) {return Array.from(_pixels2.subarray((y * 10 + x) * 4, (y * 10 + x + 1) * 4));}; (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 3), "inside copied ImageBitmap").to.deep.equal([255, 128, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 3), "left of copied ImageBitmap").to.deep.equal([0, 0, 0, 0]);case 4:_context3.prev = 4; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 3), "left of copied ImageBitmap").to.deep.equal([0, 0, 0, 0]);case 4:_context5.prev = 4; scene.dispose(); - engine.dispose();return _context3.finish(4);case 5:case "end":return _context3.stop();}}, _callee3, null, [[1,, 4, 5]]);})) + engine.dispose();return _context5.finish(4);case 5:case "end":return _context5.stop();}}, _callee4, null, [[1,, 4, 5]]);})) ); - test("crops and scales a nonzero offscreen-canvas source rectangle", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee4() {var engine, scene, source, texture, _pixels3, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: + test("crops and scales a nonzero offscreen-canvas source rectangle", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee5() {var engine, scene, source, texture, _pixels3, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0: engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context4.prev = 1; + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context6.prev = 1; source = engine.createCanvas(18, 8); fillImageData(source.getContext("2d"), 18, 8, function (x) {return ( @@ -256,16 +294,108 @@ skipCanvasGpuTests) false, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_NEAREST_SAMPLINGMODE ); - texture.getContext().drawImage(source, 7, 2, 4, 4, 3, 2, 8, 8);_context4.next = 2;return ( + texture.getContext().drawImage(source, 7, 2, 4, 4, 3, 2, 8, 8);_context6.next = 2;return ( - readTexture(texture));case 2:_pixels3 = _context4.sent; + readTexture(texture));case 2:_pixels3 = _context6.sent; pixel = function pixel(x, y) {return Array.from(_pixels3.subarray((y * 14 + x) * 4, (y * 14 + x + 1) * 4));}; (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(4, 5), "left interior of scaled crop").to.deep.equal([0, 255, 0, 255]); (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(9, 5), "right interior of scaled crop").to.deep.equal([0, 255, 0, 255]); - (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 5), "outside destination rectangle").to.deep.equal([0, 0, 0, 0]);case 3:_context4.prev = 3; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel(2, 5), "outside destination rectangle").to.deep.equal([0, 0, 0, 0]);case 3:_context6.prev = 3; + + scene.dispose(); + engine.dispose();return _context6.finish(3);case 4:case "end":return _context6.stop();}}, _callee5, null, [[1,, 3, 4]]);})) + + ); + }); +} + +/***/ }, + +/***/ "./src/tests.nativeEngine.iblCdf.ts" +/*!******************************************!*\ + !*** ./src/tests.nativeEngine.iblCdf.ts ***! + \******************************************/ +(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ registerIblCdfTests: () => (/* binding */ registerIblCdfTests) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/asyncToGenerator */ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime/regenerator */ "../../node_modules/@babel/runtime/regenerator/index.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var chai__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! chai */ "../../node_modules/chai/index.js"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babylonjs/core */ "@babylonjs/core"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__); + + + + +function registerIblCdfTests( +describe, +it, +skipCanvasGpuTests) +{ + describe("NativeEngine IBL CDF texel bins", function () { + this.timeout(10000); + (skipCanvasGpuTests ? it.skip : it)("preserves boundary ties, adjacent bins, and clamped endpoints", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {var engine, scene, camera, data, y, x, texture, material, plane, target, pixels, expectedBins, i, value;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context.prev = 1; + + camera = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.FreeCamera("camera", new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3(0, 0, -3), scene); + camera.setTarget(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3.Zero()); + data = new Uint8Array(4 * 4 * 4); + for (y = 0; y < 4; y++) { + for (x = 0; x < 4; x++) { + data.set([x * 64, y * 64, 0, 255], (y * 4 + x) * 4); + } + } + texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.RawTexture.CreateRGBATexture(data, 4, 4, scene, false, false, _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_NEAREST_SAMPLINGMODE); + texture.wrapU = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_CLAMP_ADDRESSMODE; + texture.wrapV = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Constants.TEXTURE_CLAMP_ADDRESSMODE; + material = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.ShaderMaterial("cdfBins", scene, { + vertexSource: "\n precision highp float;\n attribute vec3 position;\n void main() { gl_Position = vec4(position.xy, 0.0, 1.0); }", + + + + fragmentSource: "\n precision highp float;\n uniform sampler2D icdf;\n void main() {\n float index = floor(gl_FragCoord.x);\n float coordinate = index / 4.0;\n if (index == 5.0) coordinate = -1.0;\n if (index == 6.0) coordinate = 1.5;\n if (index == 7.0) coordinate = 0.5 - 1.0 / 1024.0;\n if (index == 8.0) coordinate = 0.5 + 1.0 / 1024.0;\n gl_FragColor = texture2D(icdf, vec2(coordinate));\n }" + + + + + + + + + + + + }, { attributes: ["position"], uniforms: [], samplers: ["icdf"] }); + material.backFaceCulling = false; + material.setTexture("icdf", texture); + plane = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.MeshBuilder.CreatePlane("plane", { size: 2 }, scene); + plane.material = material; + target = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.RenderTargetTexture("cdfBins", { width: 9, height: 1 }, scene, { + generateDepthBuffer: false, + samples: 1 + }); + target.renderList = [plane];_context.next = 2;return ( + material.forceCompilationAsync(plane));case 2:_context.next = 3;return ( + scene.whenReadyAsync());case 3: + target.render();_context.next = 4;return ( + target.readPixels());case 4:pixels = _context.sent;if ( + pixels instanceof Uint8Array) {_context.next = 5;break;}throw ( + new Error("Expected RGBA8 CDF bin readback"));case 5: + + expectedBins = [0, 1, 2, 3, 3, 0, 3, 1, 2]; + for (i = 0; i < expectedBins.length; i++) { + value = expectedBins[i] * 64; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(Array.from(pixels.subarray(i * 4, i * 4 + 4)), "CDF query ".concat(i)).to.deep.equal([value, value, 0, 255]); + }case 6:_context.prev = 6; scene.dispose(); - engine.dispose();return _context4.finish(3);case 4:case "end":return _context4.stop();}}, _callee4, null, [[1,, 3, 4]]);})) + engine.dispose();return _context.finish(6);case 7:case "end":return _context.stop();}}, _callee, null, [[1,, 6, 7]]);})) ); }); @@ -434,6 +564,141 @@ enabled) ); };for (_iterator.s(); !(_step = _iterator.n()).done;) {_loop();}} catch (err) {_iterator.e(err);} finally {_iterator.f();} + + }); +} + +/***/ }, + +/***/ "./src/tests.nativeEngine.triangleStripDepth.ts" +/*!******************************************************!*\ + !*** ./src/tests.nativeEngine.triangleStripDepth.ts ***! + \******************************************************/ +(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ registerTriangleStripDepthTests: () => (/* binding */ registerTriangleStripDepthTests) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/asyncToGenerator */ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime/regenerator */ "../../node_modules/@babel/runtime/regenerator/index.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var chai__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! chai */ "../../node_modules/chai/index.js"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babylonjs/core */ "@babylonjs/core"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__); + + +function + +renderTriangleStripDepthOverlap(_x, _x2) {return _renderTriangleStripDepthOverlap.apply(this, arguments);}function _renderTriangleStripDepthOverlap() {_renderTriangleStripDepthOverlap = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee2(instanced, depthWrite) {var engine, scene, camera, material, createStrip, renderTarget, strip, matrices, nearStrip, farStrip, pixels, offset;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context2.prev = 1; + + camera = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.FreeCamera("camera", new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3(0, 0, -3), scene); + camera.setTarget(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3.Zero()); + camera.mode = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Camera.ORTHOGRAPHIC_CAMERA; + camera.orthoTop = 1; + camera.orthoBottom = -1; + camera.orthoLeft = -1; + camera.orthoRight = 1; + scene.activeCamera = camera; + + material = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.ShaderMaterial( + "triangleStripDepth", + scene, + { + vertexSource: "\n precision highp float;\n attribute vec3 position;\n uniform mat4 viewProjection;\n #include\n varying vec3 vColor;\n void main(void) {\n #include\n gl_Position = viewProjection * finalWorld * vec4(position, 1.0);\n vColor = finalWorld[3].z < 0.25 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0);\n }", + + + + + + + + + + + fragmentSource: "\n precision highp float;\n varying vec3 vColor;\n void main(void) {\n gl_FragColor = vec4(vColor, 1.0);\n }" + + + + + + }, + { + attributes: ["position"], + uniforms: ["world", "viewProjection"] + } + ); + material.fillMode = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Material.TriangleStripDrawMode; + material.disableDepthWrite = !depthWrite; + + createStrip = function createStrip(name) { + var mesh = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Mesh(name, scene); + mesh.setVerticesData(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.VertexBuffer.PositionKind, [-0.8, -0.8, 0, 0.8, -0.8, 0, -0.8, 0.8, 0, 0.8, 0.8, 0]); + mesh.isUnIndexed = true; + mesh.material = material; + return mesh; + }; + + renderTarget = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.RenderTargetTexture("triangleStripDepthTarget", { width: 16, height: 16 }, scene, { + generateDepthBuffer: true, + generateStencilBuffer: false, + samples: 1 + }); + renderTarget.clearColor = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Color4(0, 0, 0, 1); + renderTarget.activeCamera = camera; + + if (instanced) { + strip = createStrip("instancedStrip"); + matrices = new Float32Array(32); + _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Matrix.Identity().copyToArray(matrices, 0); + _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Matrix.Translation(0, 0, 0.5).copyToArray(matrices, 16); + strip.thinInstanceSetBuffer("matrix", matrices, 16, true); + renderTarget.renderList.push(strip); + } else { + nearStrip = createStrip("nearStrip"); + farStrip = createStrip("farStrip"); + farStrip.position.z = 0.5; + nearStrip.alphaIndex = 0; + farStrip.alphaIndex = 1; + renderTarget.setRenderingOrder(0, function (a, b) {return a.getMesh().alphaIndex - b.getMesh().alphaIndex;}); + renderTarget.renderList.push(nearStrip, farStrip); + }_context2.next = 2;return ( + + scene.whenReadyAsync());case 2: + renderTarget.render();_context2.next = 3;return ( + renderTarget.readPixels());case 3:pixels = _context2.sent;if ( + pixels instanceof Uint8Array) {_context2.next = 4;break;}throw ( + new Error("Expected RGBA8 triangle-strip render target readback"));case 4: + + offset = (8 * 16 + 8) * 4;return _context2.abrupt("return", + Array.from(pixels.subarray(offset, offset + 4)));case 5:_context2.prev = 5; + + scene.dispose(); + engine.dispose();return _context2.finish(5);case 6:case "end":return _context2.stop();}}, _callee2, null, [[1,, 5, 6]]);}));return _renderTriangleStripDepthOverlap.apply(this, arguments);} + + + +function registerTriangleStripDepthTests( +describe, +it, +skipCanvasGpuTests) +{ + describe("NativeEngine triangle-strip depth writes", function () { + this.timeout(10000);var _loop = function _loop() + {var instanced = _arr[_i];var _loop2 = function _loop2() + {var depthWrite = _arr2[_i2]; + (skipCanvasGpuTests ? it.skip : it)("".concat( + instanced ? "instanced" : "non-instanced", " strips preserve depthWrite=").concat(depthWrite), /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark( + function _callee() {var pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0:_context.next = 1;return ( + renderTriangleStripDepthOverlap(instanced, depthWrite));case 1:pixel = _context.sent; + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(pixel).to.deep.equal(depthWrite ? [255, 0, 0, 255] : [0, 0, 255, 255]);case 2:case "end":return _context.stop();}}, _callee);})) + + ); + };for (var _i2 = 0, _arr2 = [false, true]; _i2 < _arr2.length; _i2++) {_loop2();} + };for (var _i = 0, _arr = [false, true]; _i < _arr.length; _i++) {_loop();} }); } @@ -28832,12 +29097,14 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! mocha */ "../../node_modules/mocha/browser-entry.js"); /* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(mocha__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var chai__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! chai */ "../../node_modules/chai/index.js"); -/* harmony import */ var _tests_nativeEngine_png__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./tests.nativeEngine.png */ "./src/tests.nativeEngine.png.ts"); -/* harmony import */ var _tests_nativeEngine_canvasImage__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./tests.nativeEngine.canvasImage */ "./src/tests.nativeEngine.canvasImage.ts"); -/* harmony import */ var buffer__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! buffer */ "../../node_modules/buffer/index.js"); -/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @babylonjs/materials */ "@babylonjs/core"); -/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_8__); -/* harmony import */ var _tests_nativeEngine_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./tests.nativeEngine.attributeLessInstancing */ "./src/tests.nativeEngine.attributeLessInstancing.ts"); +/* harmony import */ var _tests_nativeEngine_triangleStripDepth__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./tests.nativeEngine.triangleStripDepth */ "./src/tests.nativeEngine.triangleStripDepth.ts"); +/* harmony import */ var _tests_nativeEngine_iblCdf__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./tests.nativeEngine.iblCdf */ "./src/tests.nativeEngine.iblCdf.ts"); +/* harmony import */ var _tests_nativeEngine_png__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./tests.nativeEngine.png */ "./src/tests.nativeEngine.png.ts"); +/* harmony import */ var _tests_nativeEngine_canvasImage__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./tests.nativeEngine.canvasImage */ "./src/tests.nativeEngine.canvasImage.ts"); +/* harmony import */ var buffer__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! buffer */ "../../node_modules/buffer/index.js"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! @babylonjs/materials */ "@babylonjs/core"); +/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_10__); +/* harmony import */ var _tests_nativeEngine_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./tests.nativeEngine.attributeLessInstancing */ "./src/tests.nativeEngine.attributeLessInstancing.ts"); function _createForOfIteratorHelper(r, e) {var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];if (!t) {if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {t && (r = t);var _n = 0,F = function F() {};return { s: F, n: function n() {return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] };}, e: function e(r) {throw r;}, f: F };}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}var o,a = !0,u = !1;return { s: function s() {t = t.call(r);}, n: function n() {var r = t.next();return a = r.done, r;}, e: function e(r) {u = !0, o = r;}, f: function f() {try {a || null == t.return || t.return();} finally {if (u) throw o;}} };}function _unsupportedIterableToArray(r, a) {if (r) {if ("string" == typeof r) return _arrayLikeToArray(r, a);var t = {}.toString.call(r).slice(8, -1);return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;}}function _arrayLikeToArray(r, a) {(null == a || a > r.length) && (a = r.length);for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];return n;} @@ -28850,6 +29117,8 @@ function _createForOfIteratorHelper(r, e) {var t = "undefined" != typeof Symbol + + mocha__WEBPACK_IMPORTED_MODULE_3__.setup("bdd"); // @ts-ignore mocha__WEBPACK_IMPORTED_MODULE_3__.reporter("spec"); @@ -28862,8 +29131,11 @@ mocha__WEBPACK_IMPORTED_MODULE_3__.reporter("spec"); -(0,_tests_nativeEngine_png__WEBPACK_IMPORTED_MODULE_5__.registerPngTests)(describe, it, hasGpuRendering && hasNativeImageLoading); -(0,_tests_nativeEngine_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_9__.registerAttributeLessInstancingTests)(describe, it, hasAttributeLessInstancing); + +(0,_tests_nativeEngine_triangleStripDepth__WEBPACK_IMPORTED_MODULE_5__.registerTriangleStripDepthTests)(describe, it, skipCanvasGpuTests); +(0,_tests_nativeEngine_iblCdf__WEBPACK_IMPORTED_MODULE_6__.registerIblCdfTests)(describe, it, skipCanvasGpuTests); +(0,_tests_nativeEngine_png__WEBPACK_IMPORTED_MODULE_7__.registerPngTests)(describe, it, hasGpuRendering && hasNativeImageLoading); +(0,_tests_nativeEngine_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_11__.registerAttributeLessInstancingTests)(describe, it, hasAttributeLessInstancing); describe("Native texture readback", function () { this.timeout(10000); @@ -28900,8 +29172,8 @@ describe("Native texture readback", function () { ); itWithRawTexture("returns bottom-origin RGBA8 crops and preserves destination offsets", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee2() {var engine, scene, _i3, _arr3, invertY, width, height, data, y, x, texture, _i4, _arr4, _arr4$_i, _x, _y, readWidth, readHeight, storage, destination, result, row, sourceY, column, offset;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context2.prev = 1;_i3 = 0, _arr3 = + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context2.prev = 1;_i3 = 0, _arr3 = [false, true];case 2:if (!(_i3 < _arr3.length)) {_context2.next = 9;break;}invertY = _arr3[_i3]; width = 5; @@ -28912,7 +29184,7 @@ describe("Native texture readback", function () { data.set([x * 40, y * 30, 128, 255], (y * width + x) * 4); } } - texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RawTexture.CreateRGBATexture(data, width, height, scene, false, invertY);_context2.prev = 3;_i4 = 0, _arr4 = + texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RawTexture.CreateRGBATexture(data, width, height, scene, false, invertY);_context2.prev = 3;_i4 = 0, _arr4 = [ [0, 0, width, height], [0, 0, 1, 1], [4, 6, 1, 1], @@ -28945,8 +29217,8 @@ describe("Native texture readback", function () { ); itWithRawTexture("returns wide odd-height readbacks without changing the middle row", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee3() {var engine, scene, width, height, rowPitch, data, y, x, _i5, _arr5, invertY, texture, result, row, sourceY;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0: - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context3.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context3.prev = 1; width = 2049; height = 3; @@ -28958,7 +29230,7 @@ describe("Native texture readback", function () { } }_i5 = 0, _arr5 = [false, true];case 2:if (!(_i5 < _arr5.length)) {_context3.next = 8;break;}invertY = _arr5[_i5]; - texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RawTexture.CreateRGBATexture(data, width, height, scene, false, invertY);_context3.prev = 3;_context3.next = 4;return ( + texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RawTexture.CreateRGBATexture(data, width, height, scene, false, invertY);_context3.prev = 3;_context3.next = 4;return ( texture.readPixels());case 4:result = _context3.sent;if ( result instanceof Uint8Array) {_context3.next = 5;break;}throw ( @@ -28981,8 +29253,8 @@ describe("Native texture readback", function () { ); itWithRawTexture("uses mip extents for cropped readback", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee4() {var engine, scene, data, y, x, texture, _i6, _arr6, _arr6$_i, _y2, green, result, internalTexture, _i7, _arr7, _arr7$_i, _x2, _y3, width, height, error, _i8, _arr8, invalid, _i9, _arr9, component, request, mip, _x3, _y4, _width, _height, destination, _error, maximum, parameter, _t2, _t3;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0: - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context4.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context4.prev = 1; data = new Uint8Array(4 * 8 * 4); for (y = 0; y < 8; ++y) { @@ -28990,7 +29262,7 @@ describe("Native texture readback", function () { data.set([x < 2 ? 255 : 0, y < 4 ? 255 : 0, 0, 255], (y * 4 + x) * 4); } } - texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RawTexture.CreateRGBATexture(data, 4, 8, scene, true, false);_i6 = 0, _arr6 = + texture = _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RawTexture.CreateRGBATexture(data, 4, 8, scene, true, false);_i6 = 0, _arr6 = [[0, 255], [3, 0]];case 2:if (!(_i6 < _arr6.length)) {_context4.next = 6;break;}_arr6$_i = (0,_babel_runtime_helpers_slicedToArray__WEBPACK_IMPORTED_MODULE_0__["default"])(_arr6[_i6], 2), _y2 = _arr6$_i[0], green = _arr6$_i[1];_context4.next = 3;return ( texture.readPixels(0, 1, null, true, false, 1, _y2, 1, 1));case 3:result = _context4.sent;if ( result instanceof Uint8Array) {_context4.next = 4;break;}throw ( @@ -29053,13 +29325,13 @@ describe("Native texture readback", function () { ); }); -(0,_tests_nativeEngine_canvasImage__WEBPACK_IMPORTED_MODULE_6__.registerCanvasImageTests)(describe, it, skipCanvasGpuTests); +(0,_tests_nativeEngine_canvasImage__WEBPACK_IMPORTED_MODULE_8__.registerCanvasImageTests)(describe, it, skipCanvasGpuTests); describe("RequestFile", function () { this.timeout(0); it("should throw when requesting a URL with no protocol", function () { function requestFile() { - (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RequestFile)("noprotocol.gltf", function () {}); + (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RequestFile)("noprotocol.gltf", function () {}); } (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(requestFile).to.throw(); }); @@ -29213,10 +29485,10 @@ 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_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee5() {var _loop2, _i10, _arr10;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0: this.timeout(10000);_loop2 = /*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _loop2() {var translated, engine, scene, texture, ctx, pixels, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0: translated = _arr10[_i10]; - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context5.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context5.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("nested clips", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("nested clips", 64, scene, false); ctx = texture.getContext(); ctx.fillStyle = "white"; ctx.fillRect(0, 0, 64, 64); @@ -29272,10 +29544,10 @@ describe("Canvas2D", function () { (skipCanvasGpuTests ? it.skip : it)("normalizes negative rectangle dimensions before intersecting GPU clips", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee6() {var engine, scene, texture, ctx, _i11, _arr11, rotated, expected, _i12, _arr12, _arr12$_i, flipX, flipY, _pixels, inside, changed, index;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context7) {while (1) switch (_context7.prev = _context7.next) {case 0: this.timeout(10000); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context7.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context7.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("signed clips", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("signed clips", 64, scene, false); ctx = texture.getContext();_i11 = 0, _arr11 = [false, true];case 2:if (!(_i11 < _arr11.length)) {_context7.next = 8;break;}rotated = _arr11[_i11]; expected = void 0;_i12 = 0, _arr12 = @@ -29330,7 +29602,7 @@ describe("Canvas2D", function () { (skipCanvasGpuTests ? it.skip : it)("clips text, fills, strokes and images to saved curved paths on the GPU", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee8() {var fontData, engine, scene, texture, source, ctx, read, _i13, _arr13, mode, _pixels2, changedInside, y, x, offset, changed, distance, restored, nested, pixel, _i14, _arr14, _mode, path, result, _center, filled, preserved, outside, center;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context9) {while (1) switch (_context9.prev = _context9.next) {case 0: this.timeout(20000);_context9.next = 1;return ( new Promise(function (resolve, reject) { - (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RequestFile)("app:///Assets/Arimo-Regular.ttf", function (data) { + (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RequestFile)("app:///Assets/Arimo-Regular.ttf", function (data) { if (typeof data === "string") { reject(new Error("Expected binary clip-test font")); } else { @@ -29339,11 +29611,11 @@ describe("Canvas2D", function () { }, undefined, undefined, true, reject); }));case 1:fontData = _context9.sent; _native.Canvas.loadTTF("clip-test-font", fontData); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context9.prev = 2; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context9.prev = 2; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("curved clips", 64, scene, false); - source = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("clip image", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("curved clips", 64, scene, false); + source = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("clip image", 64, scene, false); source.getContext().fillStyle = "blue"; source.getContext().fillRect(0, 0, 64, 64); ctx = texture.getContext(); @@ -29497,10 +29769,10 @@ 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_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee9() {var engine, scene, texture, ctx, _pixels3, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context0) {while (1) switch (_context0.prev = _context0.next) {case 0: this.timeout(10000); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context0.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context0.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("clipped clear", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("clipped clear", 64, scene, false); ctx = texture.getContext(); ctx.fillStyle = "red"; ctx.fillRect(0, 0, 64, 64); @@ -29532,10 +29804,10 @@ describe("Canvas2D", function () { (skipCanvasGpuTests ? it.skip : it)("uses the strokeRect geometry after a preceding fillRect", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee0() {var engine, scene, texture, ctx, _pixels4, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context1) {while (1) switch (_context1.prev = _context1.next) {case 0: this.timeout(10000); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context1.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context1.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("fill then inset stroke", 16, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("fill then inset stroke", 16, scene, false); ctx = texture.getContext(); ctx.fillStyle = "blue"; ctx.fillRect(2, 2, 12, 12); @@ -29561,7 +29833,7 @@ describe("Canvas2D", function () { it("matches browser text layout metrics", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee1() {var fontData, ctx;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context10) {while (1) switch (_context10.prev = _context10.next) {case 0: this.timeout(10000);_context10.next = 1;return ( new Promise(function (resolve, reject) { - (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RequestFile)( + (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RequestFile)( "app:///Assets/Arimo-Regular.ttf", function (data) { if (typeof data === "string") { @@ -29700,10 +29972,10 @@ describe("Canvas2D", function () { function _callee10() {var _loop3, _i15, _arr15;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context12) {while (1) switch (_context12.prev = _context12.next) {case 0: this.timeout(10000);_loop3 = /*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _loop3() {var translated, engine, scene, texture, ctx, _pixels8, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context11) {while (1) switch (_context11.prev = _context11.next) {case 0: translated = _arr15[_i15]; - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context11.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context11.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("nested clips", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("nested clips", 64, scene, false); ctx = texture.getContext(); ctx.fillStyle = "white"; ctx.fillRect(0, 0, 64, 64); @@ -29784,10 +30056,10 @@ describe("Canvas2D", function () { "normalizes negative rectangle dimensions before intersecting GPU clips", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark( function _callee11() {var engine, scene, texture, ctx, _i16, _arr16, rotated, expected, _i17, _arr17, flips, flipX, flipY, _pixels9, description, changed, index;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context13) {while (1) switch (_context13.prev = _context13.next) {case 0: this.timeout(10000); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context13.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context13.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("signed clips", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("signed clips", 64, scene, false); ctx = texture.getContext();_i16 = 0, _arr16 = [false, true];case 2:if (!(_i16 < _arr16.length)) {_context13.next = 8;break;}rotated = _arr16[_i16]; expected = void 0;_i17 = 0, _arr17 = @@ -29855,10 +30127,10 @@ describe("Canvas2D", function () { "clears only the clipped GPU region and ignores globalAlpha and filters", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark( function _callee12() {var engine, scene, texture, ctx, _pixels0, pixel;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context14) {while (1) switch (_context14.prev = _context14.next) {case 0: this.timeout(10000); - engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine);_context14.prev = 1; + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine);_context14.prev = 1; - texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DynamicTexture("clipped clear", 64, scene, false); + texture = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DynamicTexture("clipped clear", 64, scene, false); ctx = texture.getContext(); ctx.fillStyle = "red"; ctx.fillRect(0, 0, 64, 64); @@ -30917,7 +31189,7 @@ describe("Canvas2D", function () { canvas.height = size; try { var url = canvas.toDataURL(); - var png = buffer__WEBPACK_IMPORTED_MODULE_7__.Buffer.from(url.substring("data:image/png;base64,".length), "base64"); + var png = buffer__WEBPACK_IMPORTED_MODULE_9__.Buffer.from(url.substring("data:image/png;base64,".length), "base64"); (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(Array.from(png.subarray(0, 8))).to.deep.equal([137, 80, 78, 71, 13, 10, 26, 10]); var offset = 8; var foundEnd = false; @@ -30943,8 +31215,8 @@ describe("Canvas2D", function () { }); function createSceneAndWait(callback, done) { - var engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.NativeEngine(); - var scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Scene(engine); + var engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.NativeEngine(); + var scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Scene(engine); scene.createDefaultCamera(); callback(engine, scene); scene.executeWhenReady(function () { @@ -30983,7 +31255,7 @@ describe("Materials", function () { it("Empty ShaderMaterial should compile", function (done) { function createEmptyShaderMat() { createSceneAndWait(function (engine, scene) { - var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.MeshBuilder.CreateSphere( + var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.MeshBuilder.CreateSphere( "sphere", { diameter: 2, segments: 32 }, scene @@ -30992,7 +31264,7 @@ describe("Materials", function () { vertexSource: "void main() {}", fragmentSource: "void main() {}" }; - var mat = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.ShaderMaterial("shader", scene, shaders, {}); + var mat = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.ShaderMaterial("shader", scene, shaders, {}); sphere.material = mat; }, done); } @@ -31000,12 +31272,12 @@ describe("Materials", function () { }); it("GradientMaterial should compile", function (done) { createSceneAndWait(function (engine, scene) { - var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.MeshBuilder.CreateSphere( + var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.MeshBuilder.CreateSphere( "sphere", { diameter: 2, segments: 32 }, scene ); - var gradientMaterial = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.GradientMaterial("grad", scene); + var gradientMaterial = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.GradientMaterial("grad", scene); sphere.material = gradientMaterial; }, done); }); @@ -31016,21 +31288,21 @@ describe("PostProcesses", function () { it("PassPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.PassPostProcess("Scene copy", 1.0, camera); + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.PassPostProcess("Scene copy", 1.0, camera); }, done); }); it("BlackAndWhitePostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.BlackAndWhitePostProcess("bandw", 1.0, camera); + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.BlackAndWhitePostProcess("bandw", 1.0, camera); }, done); }); it("BlurPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.BlurPostProcess( + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.BlurPostProcess( "Horizontal blur", - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Vector2(1.0, 0), + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Vector2(1.0, 0), 32, 0.25, camera @@ -31040,9 +31312,9 @@ describe("PostProcesses", function () { it("ConvolutionPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.ConvolutionPostProcess( + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.ConvolutionPostProcess( "Sepia", - _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.ConvolutionPostProcess.EmbossKernel, + _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.ConvolutionPostProcess.EmbossKernel, 1.0, camera ); @@ -31051,28 +31323,28 @@ describe("PostProcesses", function () { it("HighlightsPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.HighlightsPostProcess("highlights", 1.0, camera); + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.HighlightsPostProcess("highlights", 1.0, camera); }, done); }); it("TonemapPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.TonemapPostProcess("tonemap", _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.TonemappingOperator.Hable, 1.0, camera); + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.TonemapPostProcess("tonemap", _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.TonemappingOperator.Hable, 1.0, camera); }, done); }); it("ImageProcessingPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.ImageProcessingPostProcess("processing", 1.0, camera); + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.ImageProcessingPostProcess("processing", 1.0, camera); }, done); }); it("RefractionPostProcess", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.RefractionPostProcess( + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.RefractionPostProcess( "Refraction", "https://playground.babylonjs.com/textures/grass.jpg", - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.Color3(1.0, 1.0, 1.0), + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.Color3(1.0, 1.0, 1.0), 0.5, 0.5, 1.0, @@ -31083,7 +31355,7 @@ describe("PostProcesses", function () { it("DefaultPipeline", function (done) { createSceneAndWait(function (engine, scene) { var camera = scene._activeCamera; - new _babylonjs_core__WEBPACK_IMPORTED_MODULE_8__.DefaultRenderingPipeline( + new _babylonjs_core__WEBPACK_IMPORTED_MODULE_10__.DefaultRenderingPipeline( "defaultPipeline", // The name of the pipeline true, // Do you want the pipeline to use HDR texture? scene, // The scene instance @@ -31617,7 +31889,7 @@ describe("Canvas image reloads", function () { {var fromUrl = _arr20[_i20]; test("only delivers the latest assignment after a pending ".concat(fromUrl ? "URL" : "data", " load"), /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee18() {var image, barrier, loaded, errors;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context21) {while (1) switch (_context21.prev = _context21.next) {case 0: - setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_7__.Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADklEQVR4nGP4z8AAQv8BD/kD/YURmXYAAAAASUVORK5CYII=", "base64")); + setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_9__.Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADklEQVR4nGP4z8AAQv8BD/kD/YURmXYAAAAASUVORK5CYII=", "base64")); image = new _native.Image(); barrier = new _native.Image(); loaded = []; @@ -31665,7 +31937,7 @@ describe("Canvas image reloads", function () { (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(data[3]).to.be.greaterThan(240); }; - setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_7__.Buffer.from(redPng, "base64"));_context22.next = 2;return ( + setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_9__.Buffer.from(redPng, "base64"));_context22.next = 2;return ( load(function () { image.src = "image-reload-test:///red.png"; }));case 2: @@ -31677,7 +31949,7 @@ describe("Canvas image reloads", function () { image.src = "data:image/png;base64," + greenPng; }));case 3:_context22.next = 5;break;case 4: - setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_7__.Buffer.from(greenPng, "base64"));_context22.next = 5;return ( + setImageReloadTestResponse(buffer__WEBPACK_IMPORTED_MODULE_9__.Buffer.from(greenPng, "base64"));_context22.next = 5;return ( load(function () { image.src = "image-reload-test:///green.png"; }));case 5: diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts index cefcb3751f..16edab700b 100644 --- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts +++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts @@ -1,5 +1,7 @@ import * as Mocha from "mocha"; import { expect } from "chai"; +import { registerTriangleStripDepthTests } from "./tests.nativeEngine.triangleStripDepth"; +import { registerIblCdfTests } from "./tests.nativeEngine.iblCdf"; import { registerPngTests } from "./tests.nativeEngine.png"; import { registerCanvasImageTests } from "./tests.nativeEngine.canvasImage"; import { Buffer } from "buffer"; @@ -42,6 +44,9 @@ declare const setExitCode: (code: number) => void; declare const setImageReloadTestResponse: (bytes: Uint8Array) => void; declare const skipCanvasGpuTests: boolean; declare const _native: any; + +registerTriangleStripDepthTests(describe, it, skipCanvasGpuTests); +registerIblCdfTests(describe, it, skipCanvasGpuTests); registerPngTests(describe, it, hasGpuRendering && hasNativeImageLoading); registerAttributeLessInstancingTests(describe, it, hasAttributeLessInstancing); diff --git a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.canvasImage.ts b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.canvasImage.ts index a0473d86c5..3240a7fdde 100644 --- a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.canvasImage.ts +++ b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.canvasImage.ts @@ -1,6 +1,6 @@ import * as Mocha from "mocha"; import { expect } from "chai"; -import { Constants, DynamicTexture, NativeEngine, Scene } from "@babylonjs/core"; +import { Constants, DynamicTexture, FreeCamera, NativeEngine, Scene, Vector3 } from "@babylonjs/core"; type CanvasContext = ReturnType; @@ -16,8 +16,8 @@ function fillImageData(context: CanvasContext, width: number, height: number, pi context.putImageData(imageData, dx, dy); } -async function readTexture(texture: DynamicTexture): Promise { - texture.update(false); +async function readTexture(texture: DynamicTexture, premulAlpha = false): Promise { + texture.update(false, premulAlpha); const pixels = await texture.readPixels(); if (!(pixels instanceof Uint8Array)) { throw new Error("Expected RGBA8 GPU readback for the canvas texture"); @@ -41,6 +41,44 @@ export function registerCanvasImageTests( this.timeout(10000); const test = skipCanvasGpuTests ? it.skip : it; + test("copies regenerated Canvas mipmaps in the requested alpha representation", async function () { + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const texture = new DynamicTexture("Canvas mip chain", { width: 8, height: 8 }, scene, true, Constants.TEXTURE_TRILINEAR_SAMPLINGMODE); + if (!texture.getInternalTexture()?.generateMipMaps) { + this.skip(); + } + const context = texture.getContext(); + for (const premulAlpha of [false, true]) { + for (const blue of [true, false]) { + context.clearRect(0, 0, 8, 8); + fillImageData(context, 8, 8, (x, y) => ( + (x + y) % 2 === 0 ? [255, 0, 0, 102] : blue ? [0, 0, 255, 102] : [0, 255, 0, 102] + )); + texture.update(false, premulAlpha); + for (const mip of [1, 2, 3]) { + const pixels = await texture.readPixels(0, mip); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 Canvas mip readback"); + } + const rgba = pixels; + const size = 8 >> mip; + expect(rgba.length).to.equal(size * size * 4); + const half = premulAlpha ? 51 : 128; + const expected = blue ? [half, 0, half, 102] : [half, half, 0, 102]; + for (let index = 0; index < rgba.length; ++index) { + expect(rgba[index], `mip ${mip}, byte ${index}`).to.be.closeTo(expected[index % 4], 1); + } + } + } + } + } finally { + scene.dispose(); + engine.dispose(); + } + }); + test("keeps putImageData pixels alive through the GPU flush", async function () { const engine = new NativeEngine(); const scene = new Scene(engine); @@ -126,8 +164,8 @@ export function registerCanvasImageTests( const source = engine.createCanvas(18, 8); fillImageData(source.getContext("2d"), 18, 8, (x) => ( x < 6 ? [255, 0, 0, 255] : - x < 12 ? [0, 255, 0, 255] : - [0, 0, 255, 255] + x < 12 ? [0, 255, 0, 255] : + [0, 0, 255, 255] )); const texture = new DynamicTexture( diff --git a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.iblCdf.ts b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.iblCdf.ts new file mode 100644 index 0000000000..4920254bea --- /dev/null +++ b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.iblCdf.ts @@ -0,0 +1,82 @@ +import * as Mocha from "mocha"; +import { expect } from "chai"; +import { + Constants, + FreeCamera, + MeshBuilder, + NativeEngine, + RawTexture, + RenderTargetTexture, + Scene, + ShaderMaterial, + Vector3, +} from "@babylonjs/core"; + +export function registerIblCdfTests( + describe: typeof Mocha.describe, + it: typeof Mocha.it, + skipCanvasGpuTests: boolean +): void { + describe("NativeEngine IBL CDF texel bins", function () { + this.timeout(10000); + (skipCanvasGpuTests ? it.skip : it)("preserves boundary ties, adjacent bins, and clamped endpoints", async function () { + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const camera = new FreeCamera("camera", new Vector3(0, 0, -3), scene); + camera.setTarget(Vector3.Zero()); + const data = new Uint8Array(4 * 4 * 4); + for (let y = 0; y < 4; y++) { + for (let x = 0; x < 4; x++) { + data.set([x * 64, y * 64, 0, 255], (y * 4 + x) * 4); + } + } + const texture = RawTexture.CreateRGBATexture(data, 4, 4, scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE); + texture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; + texture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; + const material = new ShaderMaterial("cdfBins", scene, { + vertexSource: ` + precision highp float; + attribute vec3 position; + void main() { gl_Position = vec4(position.xy, 0.0, 1.0); }`, + fragmentSource: ` + precision highp float; + uniform sampler2D icdf; + void main() { + float index = floor(gl_FragCoord.x); + float coordinate = index / 4.0; + if (index == 5.0) coordinate = -1.0; + if (index == 6.0) coordinate = 1.5; + if (index == 7.0) coordinate = 0.5 - 1.0 / 1024.0; + if (index == 8.0) coordinate = 0.5 + 1.0 / 1024.0; + gl_FragColor = texture2D(icdf, vec2(coordinate)); + }`, + }, { attributes: ["position"], uniforms: [], samplers: ["icdf"] }); + material.backFaceCulling = false; + material.setTexture("icdf", texture); + const plane = MeshBuilder.CreatePlane("plane", { size: 2 }, scene); + plane.material = material; + const target = new RenderTargetTexture("cdfBins", { width: 9, height: 1 }, scene, { + generateDepthBuffer: false, + samples: 1, + }); + target.renderList = [plane]; + await material.forceCompilationAsync(plane); + await scene.whenReadyAsync(); + target.render(); + const pixels = await target.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 CDF bin readback"); + } + const expectedBins = [0, 1, 2, 3, 3, 0, 3, 1, 2]; + for (let i = 0; i < expectedBins.length; i++) { + const value = expectedBins[i] * 64; + expect(Array.from(pixels.subarray(i * 4, i * 4 + 4)), `CDF query ${i}`).to.deep.equal([value, value, 0, 255]); + } + } finally { + scene.dispose(); + engine.dispose(); + } + }); + }); +} diff --git a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.png.ts b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.png.ts index bf6fa0776a..531536a918 100644 --- a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.png.ts +++ b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.png.ts @@ -1,6 +1,6 @@ import * as Mocha from "mocha"; import { expect } from "chai"; -import { Constants, NativeEngine, Scene, Texture } from "@babylonjs/core"; +import { Constants, NativeEngine, RawTexture, Scene, Texture } from "@babylonjs/core"; const fixtures: { name: string; png: string; pixels: number[]; width?: number; height?: number; tolerance?: number }[] = [ { @@ -142,5 +142,6 @@ export function registerPngTests( } }); } + }); } diff --git a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.triangleStripDepth.ts b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.triangleStripDepth.ts new file mode 100644 index 0000000000..088df82516 --- /dev/null +++ b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.triangleStripDepth.ts @@ -0,0 +1,127 @@ +import * as Mocha from "mocha"; +import { expect } from "chai"; +import { + Camera, + Color4, + FreeCamera, + Material, + Matrix, + Mesh, + NativeEngine, + RenderTargetTexture, + Scene, + ShaderMaterial, + Vector3, + VertexBuffer, +} from "@babylonjs/core"; + +async function renderTriangleStripDepthOverlap(instanced: boolean, depthWrite: boolean): Promise { + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + const camera = new FreeCamera("camera", new Vector3(0, 0, -3), scene); + camera.setTarget(Vector3.Zero()); + camera.mode = Camera.ORTHOGRAPHIC_CAMERA; + camera.orthoTop = 1; + camera.orthoBottom = -1; + camera.orthoLeft = -1; + camera.orthoRight = 1; + scene.activeCamera = camera; + + const material = new ShaderMaterial( + "triangleStripDepth", + scene, + { + vertexSource: ` + precision highp float; + attribute vec3 position; + uniform mat4 viewProjection; + #include + varying vec3 vColor; + void main(void) { + #include + gl_Position = viewProjection * finalWorld * vec4(position, 1.0); + vColor = finalWorld[3].z < 0.25 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0); + }`, + fragmentSource: ` + precision highp float; + varying vec3 vColor; + void main(void) { + gl_FragColor = vec4(vColor, 1.0); + }`, + }, + { + attributes: ["position"], + uniforms: ["world", "viewProjection"], + } + ); + material.fillMode = Material.TriangleStripDrawMode; + material.disableDepthWrite = !depthWrite; + + const createStrip = (name: string): Mesh => { + const mesh = new Mesh(name, scene); + mesh.setVerticesData(VertexBuffer.PositionKind, [-0.8, -0.8, 0, 0.8, -0.8, 0, -0.8, 0.8, 0, 0.8, 0.8, 0]); + mesh.isUnIndexed = true; + mesh.material = material; + return mesh; + }; + + const renderTarget = new RenderTargetTexture("triangleStripDepthTarget", { width: 16, height: 16 }, scene, { + generateDepthBuffer: true, + generateStencilBuffer: false, + samples: 1, + }); + renderTarget.clearColor = new Color4(0, 0, 0, 1); + renderTarget.activeCamera = camera; + + if (instanced) { + const strip = createStrip("instancedStrip"); + const matrices = new Float32Array(32); + Matrix.Identity().copyToArray(matrices, 0); + Matrix.Translation(0, 0, 0.5).copyToArray(matrices, 16); + strip.thinInstanceSetBuffer("matrix", matrices, 16, true); + renderTarget.renderList!.push(strip); + } else { + const nearStrip = createStrip("nearStrip"); + const farStrip = createStrip("farStrip"); + farStrip.position.z = 0.5; + nearStrip.alphaIndex = 0; + farStrip.alphaIndex = 1; + renderTarget.setRenderingOrder(0, (a, b) => a.getMesh().alphaIndex - b.getMesh().alphaIndex); + renderTarget.renderList!.push(nearStrip, farStrip); + } + + await scene.whenReadyAsync(); + renderTarget.render(); + const pixels = await renderTarget.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 triangle-strip render target readback"); + } + const offset = (8 * 16 + 8) * 4; + return Array.from(pixels.subarray(offset, offset + 4)); + } finally { + scene.dispose(); + engine.dispose(); + } +} + +export function registerTriangleStripDepthTests( + describe: typeof Mocha.describe, + it: typeof Mocha.it, + skipCanvasGpuTests: boolean +): void { + describe("NativeEngine triangle-strip depth writes", function () { + this.timeout(10000); + for (const instanced of [false, true]) { + for (const depthWrite of [false, true]) { + (skipCanvasGpuTests ? it.skip : it)( + `${instanced ? "instanced" : "non-instanced"} strips preserve depthWrite=${depthWrite}`, + async function () { + const pixel = await renderTriangleStripDepthOverlap(instanced, depthWrite); + expect(pixel).to.deep.equal(depthWrite ? [255, 0, 0, 255] : [0, 0, 255, 255]); + } + ); + } + } + }); +} diff --git a/Apps/UnitTests/Source/Tests.Canvas.Readback.cpp b/Apps/UnitTests/Source/Tests.Canvas.Readback.cpp index d5d68d31c1..99c7a15799 100644 --- a/Apps/UnitTests/Source/Tests.Canvas.Readback.cpp +++ b/Apps/UnitTests/Source/Tests.Canvas.Readback.cpp @@ -194,6 +194,44 @@ TEST(CanvasReadback, GaussianBlurPadsUniformUploads) }); } +TEST(CanvasReadback, FilterBordersDoNotReuseClearPaletteColors) +{ +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + RunCanvasTest([](Napi::Env env) { + auto& graphics = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto scope = graphics.AcquireFrameCompletionScope(); + graphics.AcquireClearPaletteIndex({0.2f, 0.4f, 0.8f, 1.f}); + Napi::Eval(env, R"JS( + const source = new _native.Canvas(); + const destination = new _native.Canvas(); + source.width = destination.width = 16; + source.height = destination.height = 16; + const sourceContext = source.getContext("2d"); + const destinationContext = destination.getContext("2d"); + try { + sourceContext.fillStyle = "#ffffff"; + sourceContext.fillRect(4, 4, 8, 8); + destinationContext.filter = "blur(2px)"; + destinationContext.drawImage(source, 0, 0); + const pixels = destinationContext.getImageData(0, 0, 16, 16).data; + const offset = (8 * 16 + 3) * 4; + if (pixels[offset] !== pixels[offset + 1] || pixels[offset + 1] !== pixels[offset + 2] || + pixels[offset + 3] === 0 || pixels[offset + 3] === 255) { + throw new Error("Canvas filter border inherited a framebuffer clear color"); + } + } finally { + sourceContext.dispose(); + destinationContext.dispose(); + source.dispose(); + destination.dispose(); + } + )JS", "canvas-filter-border-palette.js"); + }); +#endif +} + #ifdef HAS_NATIVE_IMAGE_LOADING TEST(CanvasImages, SvgUsesBimgParserAndRgbaPixels) { diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.Compute.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.Compute.cpp new file mode 100644 index 0000000000..bf41e78ddf --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeEngine.Compute.cpp @@ -0,0 +1,291 @@ +#ifdef HAS_SHADER_COMPILER +#include + +#include "StorageBuffer.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +namespace +{ + [[maybe_unused]] constexpr std::string_view WriteSource = R"(#version 310 es +precision highp float; +precision highp int; +layout(local_size_x = 1) in; +layout(std430, binding = 3) readonly buffer Source { uint sourceData[]; }; +layout(std430, binding = 5) buffer Destination { uint destinationData[]; }; +void main() { + uint i = gl_GlobalInvocationID.x; + destinationData[i] = sourceData[i] * 2u + 5u; +} +)"; + + [[maybe_unused]] constexpr std::string_view ReadSource = R"(#version 310 es +precision highp float; +precision highp int; +layout(local_size_x = 1, local_size_y = 1) in; +layout(std430, binding = 6) readonly buffer Source { uint data[]; }; +layout(rgba8, binding = 0) writeonly uniform highp image2D dest; +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + uint i = uint(p.y) * 4u + uint(p.x); + imageStore(dest, p, vec4(float(data[i]) / 255.0, float(i) / 255.0, 0.0, 1.0)); +} +)"; + + [[maybe_unused]] std::array ReadRawMasks(const std::vector& bytes) + { + if (bytes.size() < 20 || bytes[3] != 13) + { + throw std::runtime_error{"Expected a bgfx v13 shader header"}; + } + std::array masks{}; + std::memcpy(masks.data(), bytes.data() + 12, sizeof(masks)); + return masks; + } + + [[maybe_unused]] bgfx::ProgramHandle CreateComputeProgram(const Babylon::Graphics::BgfxShaderInfo& info) + { + const auto shader = bgfx::createShader(bgfx::copy(info.ComputeBytes.data(), static_cast(info.ComputeBytes.size()))); + if (!bgfx::isValid(shader)) + { + throw std::runtime_error{"Failed to create compute shader"}; + } + const auto program = bgfx::createProgram(shader, true); + if (!bgfx::isValid(program)) + { + throw std::runtime_error{"Failed to create compute program"}; + } + return program; + } + + [[maybe_unused]] bool RendererSupportsCompute() + { + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + const bool supported = (bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) != 0; + device.FinishRenderingCurrentFrame(); + return supported; + } + + struct ComputeResources + { + std::unique_ptr Source; + std::unique_ptr Destination; + bgfx::ProgramHandle WriteProgram{bgfx::kInvalidHandle}; + bgfx::ProgramHandle ReadProgram{bgfx::kInvalidHandle}; + bgfx::TextureHandle Texture{bgfx::kInvalidHandle}; + bgfx::TextureHandle Readback{bgfx::kInvalidHandle}; + + ~ComputeResources() + { + if (bgfx::isValid(WriteProgram)) + { + bgfx::destroy(WriteProgram); + } + if (bgfx::isValid(ReadProgram)) + { + bgfx::destroy(ReadProgram); + } + if (bgfx::isValid(Texture)) + { + bgfx::destroy(Texture); + } + if (bgfx::isValid(Readback)) + { + bgfx::destroy(Readback); + } + } + }; +} + +#if defined(BABYLON_NATIVE_GRAPHICS_API_D3D11) || defined(BABYLON_NATIVE_GRAPHICS_API_D3D12) +TEST(NativeEngineCompute, PackagesRawBindingMasks) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto write = compiler.CompileCompute(WriteSource); + EXPECT_EQ(ReadRawMasks(write.ComputeBytes), (std::array{1u << 3, 1u << 5})); + const auto read = compiler.CompileCompute(ReadSource); + EXPECT_EQ(ReadRawMasks(read.ComputeBytes), (std::array{1u << 6, 0u})); +} + +TEST(NativeEngineCompute, OmitsOptimizedOutRawBindings) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto shader = compiler.CompileCompute(R"(#version 310 es +precision highp float; +precision highp int; +layout(local_size_x = 1) in; +layout(std430, binding = 3) readonly buffer Unused { uint unusedData[]; }; +layout(std430, binding = 5) buffer Destination { uint data[]; }; +void main() { data[gl_GlobalInvocationID.x] = 17u; } +)"); + EXPECT_EQ(ReadRawMasks(shader.ComputeBytes), (std::array{0u, 1u << 5})); +} + +TEST(NativeEngineCompute, ImageBindingsAreNotRawBuffers) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto shader = compiler.CompileCompute(R"(#version 310 es +precision highp float; +layout(local_size_x = 1) in; +layout(rgba8, binding = 0) writeonly uniform highp image2D dest; +void main() { imageStore(dest, ivec2(gl_GlobalInvocationID.xy), vec4(1.0)); } +)"); + EXPECT_EQ(ReadRawMasks(shader.ComputeBytes), (std::array{0u, 0u})); +} + +TEST(NativeEngineCompute, RejectsRawBindingsOutsideHeaderMask) +{ + Babylon::Plugins::ShaderCompiler compiler; + try + { + compiler.CompileCompute(R"(#version 310 es +precision highp float; +precision highp int; +layout(local_size_x = 1) in; +layout(std430, binding = 32) readonly buffer Source { uint sourceData[]; }; +layout(std430, binding = 0) buffer Destination { uint data[]; }; +void main() { data[0] = sourceData[0]; } +)"); + FAIL() << "Accepted a raw binding outside the shader header mask"; + } + catch (const std::runtime_error& error) + { + EXPECT_STREQ(error.what(), "Raw buffer binding exceeds bgfx's 32-bit shader mask"); + } +} + +TEST(NativeEngineCompute, GraphicsStagesKeepEmptyRawMasks) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto shader = compiler.Compile( + "in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }", + "precision highp float; layout(location = 0) out vec4 color; void main() { color = vec4(1.0); }"); + EXPECT_EQ(ReadRawMasks(shader.VertexBytes), (std::array{0u, 0u})); + EXPECT_EQ(ReadRawMasks(shader.FragmentBytes), (std::array{0u, 0u})); +} +#endif + +TEST(NativeEngineCompute, RawStorageBufferRoundTrip) +{ +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + if (!RendererSupportsCompute()) + { + GTEST_SKIP() << "Renderer does not support compute"; + } + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + std::array pixels{}; + std::promise completed; + auto completion = completed.get_future(); + { + Babylon::AppRuntime runtime; + ComputeResources resources; + runtime.Dispatch([&](Napi::Env env) { + try + { + device.AddToJavaScript(env); + auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto scope = context.AcquireFrameCompletionScope(); + Babylon::Plugins::ShaderCompiler compiler; + resources.WriteProgram = CreateComputeProgram(compiler.CompileCompute(WriteSource)); + resources.ReadProgram = CreateComputeProgram(compiler.CompileCompute(ReadSource)); + + resources.Source = std::make_unique(context, 64, 16, false); + resources.Destination = std::make_unique(context, 64); + std::array source{}; + resources.Source->Update(gsl::make_span(reinterpret_cast(source.data()), sizeof(source)), 0); + for (uint32_t i = 0; i < source.size(); ++i) + { + source[i] = 3u * i + 1u; + } + resources.Source->Update(gsl::make_span(reinterpret_cast(source.data()), sizeof(source)), 0); + + resources.Texture = bgfx::createTexture2D(4, 4, false, 1, bgfx::TextureFormat::RGBA8, + BGFX_TEXTURE_COMPUTE_WRITE); + resources.Readback = bgfx::createTexture2D(4, 4, false, 1, bgfx::TextureFormat::RGBA8, + BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK); + if (!bgfx::isValid(resources.Texture) || !bgfx::isValid(resources.Readback)) + { + throw std::runtime_error{"Failed to create compute readback texture"}; + } + + auto* encoder = context.GetActiveEncoder(); + if (encoder == nullptr) + { + throw std::runtime_error{"Expected an active compute encoder"}; + } + resources.Source->SetCompute(encoder, 3, bgfx::Access::Read); + resources.Destination->SetCompute(encoder, 5, bgfx::Access::ReadWrite); + encoder->dispatch(context.AcquireNewViewId(), resources.WriteProgram, 16); + resources.Destination->SetCompute(encoder, 6, bgfx::Access::Read); + encoder->setImage(0, resources.Texture, 0, bgfx::Access::Write, bgfx::TextureFormat::RGBA8); + encoder->dispatch(context.AcquireNewViewId(), resources.ReadProgram, 4, 4); + bgfx::TextureRegion destinationRegion{}; + destinationRegion.init(resources.Readback); + bgfx::TextureRegion sourceRegion{}; + sourceRegion.init(resources.Texture); + encoder->blit(context.AcquireNewViewId(), destinationRegion, sourceRegion); + + context.ReadTextureAsync(resources.Readback, pixels) + .then(arcana::inline_scheduler, arcana::cancellation::none(), + [&completed](const arcana::expected& result) { + if (result.has_error()) + { + completed.set_exception(result.error()); + } + else + { + completed.set_value(); + } + }); + } + catch (...) + { + completed.set_exception(std::current_exception()); + } + }); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30}; + while (completion.wait_for(std::chrono::milliseconds{1}) != std::future_status::ready) + { + if (std::chrono::steady_clock::now() >= deadline) + { + std::cerr << "Timed out waiting for raw storage-buffer GPU readback" << std::endl; + std::quick_exit(1); + } + device.FinishRenderingCurrentFrame(); + device.StartRenderingCurrentFrame(); + } + } + device.FinishRenderingCurrentFrame(); + ASSERT_NO_THROW(completion.get()); + for (uint32_t i = 0; i < 16; ++i) + { + EXPECT_EQ(pixels[i * 4], 6u * i + 7u) << "pixel " << i; + EXPECT_EQ(pixels[i * 4 + 1], i) << "pixel " << i; + EXPECT_EQ(pixels[i * 4 + 2], 0u) << "pixel " << i; + EXPECT_EQ(pixels[i * 4 + 3], 255u) << "pixel " << i; + } +#endif +} + +#endif diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.InstanceData.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.InstanceData.cpp index 75b50d446b..d2a7e27f0f 100644 --- a/Apps/UnitTests/Source/Tests.NativeEngine.InstanceData.cpp +++ b/Apps/UnitTests/Source/Tests.NativeEngine.InstanceData.cpp @@ -1,7 +1,26 @@ +#include + +#include "InstanceRepacker.h" +#include "VertexArray.h" #include "VertexBuffer.h" +#include #include -#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; namespace { @@ -69,3 +88,132 @@ TEST(NativeEngineInstanceData, BuiltInsKeepAssignedSlotsAtRealLocations) EXPECT_EQ(layout.Slots.at(static_cast(bgfx::Attrib::TexCoord3)), 2u); EXPECT_EQ(layout.Slots.at(static_cast(bgfx::Attrib::Position)), 3u); } + +// Instances sourced from a buffer promoted to GPU storage (transform feedback output) are repacked by compute +// into the shader's layout slots. Each repack in a frame keeps its own parameters. +TEST(NativeEngineInstanceData, EmbeddedShaderRepacksStridedInstances) +{ + Babylon::Graphics::Device device{g_deviceConfig}; +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP() << "GPU rendering/readback is unavailable in this test configuration"; +#endif + device.StartRenderingCurrentFrame(); + if ((bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) == 0 || + !bgfx::isTextureValid(0, false, 1, bgfx::TextureFormat::RGBA32F, BGFX_TEXTURE_READ_BACK | BGFX_TEXTURE_BLIT_DST)) + { + device.FinishRenderingCurrentFrame(); + GTEST_SKIP() << "Compute and RGBA32F readback are required"; + } + + constexpr uint32_t sourceStride = 20; + constexpr std::array locations{3, 7, 13, 29}; + constexpr std::array offsets{1, 5, 9, 13}; + constexpr std::array sizes{1, 2, 3, 4}; + { + Babylon::AppRuntime runtime; + std::unique_ptr source; + std::unique_ptr vertexArray; + std::unique_ptr repacker; + Babylon::VertexBuffer::InstanceDataLayout layout; + // The second count is repacked into the same destination later in the same frame. + constexpr std::array, 3> passes{{{65u, 1u}, {129u, 0u}, {1u, 0u}}}; + for (const auto& [count, laterCount] : passes) + { + std::vector input(count * sourceStride); + for (size_t word = 0; word < input.size(); ++word) + { + input[word] = static_cast(word + count * 100); + } + std::vector output(count * 16, -1.f); + std::promise completed; + auto completion = completed.get_future(); + runtime.Dispatch([&](Napi::Env env) { + try + { + device.AddToJavaScript(env); + auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto scope = context.AcquireFrameCompletionScope(); + if (!repacker) + { + repacker = std::make_unique(context); + const std::vector initial(129 * sourceStride * sizeof(float)); + source = std::make_unique(context, initial, false); + source->PromoteToGpuStorage(); + vertexArray = std::make_unique(context); + for (size_t attr = 0; attr < locations.size(); ++attr) + { + vertexArray->RecordVertexBuffer(source.get(), locations[attr], offsets[attr] * sizeof(float), + sourceStride * sizeof(float), sizes[attr], bgfx::AttribType::Float, false, 1); + } + // A built-in (like world0) keeps slot 0, ahead of the reverse-ordered generic attributes. + layout = Babylon::VertexBuffer::CreateInstanceDataLayout( + vertexArray->GetInstances(), {{locations[0], 0}}, Babylon::Graphics::MAX_INSTANCE_DATA_SLOT_COUNT); + } + source->Update(gsl::make_span(reinterpret_cast(input.data()), input.size() * sizeof(float)), 0); + Babylon::Graphics::FrameBuffer frameBuffer{context, BGFX_INVALID_HANDLE, 0, 0, true, false, false}; + const auto buffer = repacker->Repack(frameBuffer, vertexArray.get(), vertexArray->GetInstances(), layout, count); + if (laterCount != 0 && + repacker->Repack(frameBuffer, vertexArray.get(), vertexArray->GetInstances(), layout, laterCount).idx != buffer.idx) + { + throw std::runtime_error{"Instance repacker replaced the destination buffer"}; + } + if (!bgfx::isValid(buffer)) + { + throw std::runtime_error{"Instance repacker returned an invalid buffer"}; + } + context.ForceMidFrameFlush(); + auto readback = std::make_shared(context); + readback->Create2D(static_cast(count * 4), 1, false, 1, bgfx::TextureFormat::RGBA32F, + BGFX_TEXTURE_READ_BACK | BGFX_TEXTURE_BLIT_DST); + bgfx::TextureRegion destination{}; + destination.init(readback->Handle()); + bgfx::BufferRegion bufferRegion{}; + bufferRegion.init(buffer, 0, static_cast(output.size() * sizeof(float))); + context.GetActiveEncoder()->blit(context.AcquireNewViewId(), destination, bufferRegion); + context.ReadTextureAsync(readback->Handle(), + gsl::make_span(reinterpret_cast(output.data()), output.size() * sizeof(float))) + .then(arcana::inline_scheduler, arcana::cancellation::none(), + [readback, &completed](const arcana::expected& result) { + if (result.has_error()) + { + completed.set_exception(result.error()); + } + else + { + completed.set_value(); + } + }); + } + catch (...) + { + completed.set_exception(std::current_exception()); + } + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30}; + while (completion.wait_for(std::chrono::milliseconds{1}) != std::future_status::ready) + { + if (std::chrono::steady_clock::now() >= deadline) + { + std::cerr << "Timed out waiting for embedded instance repack readback" << std::endl; + std::quick_exit(1); + } + device.FinishRenderingCurrentFrame(); + device.StartRenderingCurrentFrame(); + } + ASSERT_NO_THROW(completion.get()); + for (uint32_t instance = 0; instance < count; ++instance) + { + for (size_t attr = 0; attr < locations.size(); ++attr) + { + const size_t slot = layout.Slots.at(locations[attr]); + for (uint32_t word = 0; word < sizes[attr]; ++word) + { + EXPECT_EQ(output[instance * 16 + slot * 4 + word], input[instance * sourceStride + offsets[attr] + word]) + << "count " << count << ", instance " << instance << ", attribute " << attr << ", word " << word; + } + } + } + } + } + device.FinishRenderingCurrentFrame(); +} diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.PrimitiveModeExpansion.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.PrimitiveModeExpansion.cpp new file mode 100644 index 0000000000..82e32532a3 --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeEngine.PrimitiveModeExpansion.cpp @@ -0,0 +1,181 @@ +#include "PrimitiveModeExpansion.h" + +#include + +#include +#include +#include + +namespace +{ + template + std::vector Bytes(std::initializer_list values) + { + std::vector bytes(values.size() * sizeof(T)); + if (!values.size()) + { + return bytes; + } + std::memcpy(bytes.data(), values.begin(), bytes.size()); + return bytes; + } + + template + std::vector Indices(const Babylon::PrimitiveModeExpansion::Result& result) + { + EXPECT_EQ(result.Bytes.size() % sizeof(T), 0u); + std::vector indices(result.Bytes.size() / sizeof(T)); + if (!indices.empty()) + { + std::memcpy(indices.data(), result.Bytes.data(), result.Bytes.size()); + } + return indices; + } +} + +TEST(NativeEnginePrimitiveModeExpansion, UnindexedLineLoopClosesAndOffsetsFromNonzeroFirstVertex) +{ + constexpr uint32_t FirstVertex = 7; + const auto result = Babylon::PrimitiveModeExpansion::ExpandUnindexed( + 4, Babylon::PrimitiveModeExpansion::Mode::LineLoop); + + EXPECT_FALSE(result.Index32); + EXPECT_EQ(result.IndexCount, 8u); + const auto relativeIndices = Indices(result); + EXPECT_EQ(relativeIndices, (std::vector{0, 1, 1, 2, 2, 3, 3, 0})); + + std::vector resolvedIndices{}; + for (const auto index : relativeIndices) + { + resolvedIndices.emplace_back(FirstVertex + index); + } + EXPECT_EQ(resolvedIndices, (std::vector{7, 8, 8, 9, 9, 10, 10, 7})); +} + +TEST(NativeEnginePrimitiveModeExpansion, UnindexedTriangleFanPreservesWinding) +{ + const auto result = Babylon::PrimitiveModeExpansion::ExpandUnindexed( + 5, Babylon::PrimitiveModeExpansion::Mode::TriangleFan); + + EXPECT_FALSE(result.Index32); + EXPECT_EQ(Indices(result), (std::vector{0, 1, 2, 0, 2, 3, 0, 3, 4})); +} + +TEST(NativeEnginePrimitiveModeExpansion, UnindexedExpansionUses32BitsBeforeRestartValue) +{ + const auto result = Babylon::PrimitiveModeExpansion::ExpandUnindexed( + static_cast(std::numeric_limits::max()) + 1, + Babylon::PrimitiveModeExpansion::Mode::LineLoop); + + ASSERT_TRUE(result.Index32); + const auto indices = Indices(result); + ASSERT_EQ(indices.size(), 131072u); + EXPECT_EQ(indices[indices.size() - 2], 65535u); + EXPECT_EQ(indices.back(), 0u); +} + +TEST(NativeEnginePrimitiveModeExpansion, Indexed16BitSubrangeBecomesItsOwnLoop) +{ + const auto bytes = Bytes({20, 21, 22, 23, 24, 25}); + const Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, false}; + const auto result = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 2, 3, Babylon::PrimitiveModeExpansion::Mode::LineLoop); + + EXPECT_FALSE(result.Index32); + EXPECT_EQ(Indices(result), (std::vector{22, 23, 23, 24, 24, 22})); +} + +TEST(NativeEnginePrimitiveModeExpansion, Indexed32BitFanUsesSubrangeRootAndPreservesValues) +{ + const auto bytes = Bytes({100000, 200000, 300000, 400000, 500000}); + const Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, true}; + const auto result = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 1, 4, Babylon::PrimitiveModeExpansion::Mode::TriangleFan); + + EXPECT_TRUE(result.Index32); + EXPECT_EQ(Indices(result), (std::vector{200000, 300000, 400000, 200000, 400000, 500000})); +} + +TEST(NativeEnginePrimitiveModeExpansion, RestartSeparatesLineLoops) +{ + constexpr uint16_t Restart = std::numeric_limits::max(); + const auto bytes = Bytes({1, 2, 3, Restart, 10, 11, Restart, 20}); + const Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, false}; + const auto result = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 0, 8, Babylon::PrimitiveModeExpansion::Mode::LineLoop); + + EXPECT_EQ(Indices(result), (std::vector{1, 2, 2, 3, 3, 1, 10, 11, 11, 10})); +} + +TEST(NativeEnginePrimitiveModeExpansion, RestartSeparatesTriangleFans) +{ + constexpr uint32_t Restart = std::numeric_limits::max(); + const auto bytes = Bytes({5, 6, 7, 8, Restart, 20, 21, Restart, 30, 31, 32}); + const Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, true}; + const auto result = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 0, 11, Babylon::PrimitiveModeExpansion::Mode::TriangleFan); + + EXPECT_EQ(Indices(result), (std::vector{5, 6, 7, 5, 7, 8, 30, 31, 32})); +} + +TEST(NativeEnginePrimitiveModeExpansion, DynamicUpdateUsesIndexOffsetAndChangesExpansion) +{ + auto bytes = Bytes({0, 1, 2, 3}); + Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, false}; + const auto before = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 0, 4, Babylon::PrimitiveModeExpansion::Mode::TriangleFan); + + const auto update = Bytes({9, 10}); + data.Update(gsl::span{update.data(), update.size()}, 1); + const auto after = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 0, 4, Babylon::PrimitiveModeExpansion::Mode::TriangleFan); + + EXPECT_EQ(data.Revision(), 1u); + EXPECT_EQ(Indices(before), (std::vector{0, 1, 2, 0, 2, 3})); + EXPECT_EQ(Indices(after), (std::vector{0, 9, 10, 0, 10, 3})); +} + +TEST(NativeEnginePrimitiveModeExpansion, Dynamic32BitUpdateUsesIndexOffset) +{ + auto bytes = Bytes({100000, 200000, 300000, 400000}); + Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, true}; + const auto update = Bytes({900000}); + + data.Update(gsl::span{update.data(), update.size()}, 2); + const auto result = Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 1, 3, Babylon::PrimitiveModeExpansion::Mode::LineLoop); + + EXPECT_EQ(Indices(result), (std::vector{200000, 900000, 900000, 400000, 400000, 200000})); +} + +TEST(NativeEnginePrimitiveModeExpansion, TooShortInputsProduceNoPrimitives) +{ + EXPECT_EQ( + Babylon::PrimitiveModeExpansion::ExpandUnindexed(1, Babylon::PrimitiveModeExpansion::Mode::LineLoop).IndexCount, + 0u); + EXPECT_EQ( + Babylon::PrimitiveModeExpansion::ExpandUnindexed(2, Babylon::PrimitiveModeExpansion::Mode::TriangleFan).IndexCount, + 0u); +} + +TEST(NativeEnginePrimitiveModeExpansion, OversizedExpansionThrowsBeforeAllocation) +{ + EXPECT_THROW( + Babylon::PrimitiveModeExpansion::ExpandUnindexed( + std::numeric_limits::max(), + Babylon::PrimitiveModeExpansion::Mode::TriangleFan), + std::length_error); +} + +TEST(NativeEnginePrimitiveModeExpansion, InvalidRangesAndUpdatesThrow) +{ + auto bytes = Bytes({0, 1, 2}); + Babylon::PrimitiveModeExpansion::IndexData data{gsl::span{bytes.data(), bytes.size()}, false}; + const auto update = Bytes({7, 8}); + + EXPECT_THROW( + Babylon::PrimitiveModeExpansion::ExpandIndexed( + data, 2, 2, Babylon::PrimitiveModeExpansion::Mode::LineLoop), + std::out_of_range); + EXPECT_THROW(data.Update(gsl::span{update.data(), update.size()}, 2), std::out_of_range); +} diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.StorageBuffer.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.StorageBuffer.cpp new file mode 100644 index 0000000000..4eb41e17dc --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeEngine.StorageBuffer.cpp @@ -0,0 +1,83 @@ +#include + +#include "StorageBuffer.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +namespace +{ + void RunStorageTest(const std::function& test) + { + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + std::promise completed; + auto completion = completed.get_future(); + { + Babylon::AppRuntime runtime; + runtime.Dispatch([&](Napi::Env env) { + try + { + device.AddToJavaScript(env); + test(Babylon::Graphics::DeviceContext::GetFromJavaScript(env)); + completed.set_value(); + } + catch (...) + { + completed.set_exception(std::current_exception()); + } + }); + if (completion.wait_for(std::chrono::seconds{30}) != std::future_status::ready) + { + std::cerr << "Timed out waiting for storage buffer validation" << std::endl; + std::quick_exit(1); + } + } + device.FinishRenderingCurrentFrame(); + ASSERT_NO_THROW(completion.get()); + } +} + +TEST(NativeEngineStorageBuffer, RejectsInvalidSizesBeforeAllocating) +{ + RunStorageTest([](Babylon::Graphics::DeviceContext& context) { + EXPECT_THROW((Babylon::StorageBuffer{context, 0}), std::invalid_argument); + EXPECT_THROW((Babylon::StorageBuffer{context, UINT32_MAX}), std::overflow_error); + EXPECT_THROW((Babylon::StorageBuffer{context, UINT32_MAX - 14}), std::overflow_error); + EXPECT_THROW((Babylon::StorageBuffer{context, 16, 65536}), std::invalid_argument); + Babylon::StorageBuffer buffer{context, 17}; + EXPECT_EQ(buffer.ByteLength(), 17u); + EXPECT_EQ(buffer.ShadowBytes().size(), 32u); + }); +} + +TEST(NativeEngineStorageBuffer, RejectedUpdatesPreserveShadow) +{ + RunStorageTest([](Babylon::Graphics::DeviceContext& context) { + Babylon::StorageBuffer buffer{context, 32}; + std::array bytes{}; + bytes.fill(91); + for (const uint32_t offset : {1u, 17u, 32u, UINT32_MAX}) + { + SCOPED_TRACE(offset); + EXPECT_THROW(buffer.Update(bytes, offset), std::runtime_error); + EXPECT_TRUE(std::all_of(buffer.ShadowBytes().begin(), buffer.ShadowBytes().end(), + [](uint8_t value) { return value == 0; })); + } + EXPECT_NO_THROW(buffer.Update({}, 32)); + buffer.Dispose(); + EXPECT_THROW(buffer.Update(bytes, 0), std::runtime_error); + }); +} diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.TextureFormats.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.TextureFormats.cpp index d82a91c757..c551223ebf 100644 --- a/Apps/UnitTests/Source/Tests.NativeEngine.TextureFormats.cpp +++ b/Apps/UnitTests/Source/Tests.NativeEngine.TextureFormats.cpp @@ -70,8 +70,8 @@ namespace std::cerr << "Timed out waiting for NativeEngine texture format test" << std::endl; std::quick_exit(1); } + device.FinishRenderingCurrentFrame(); } - device.FinishRenderingCurrentFrame(); ASSERT_NO_THROW(completion.get()); } @@ -472,5 +472,40 @@ TEST(NativeEngineTextureFormats, SamplingModesSeparateLodClampFromFilterFlags) EXPECT_EQ(texture->SamplerFlags(), preservedFlags | test.Flags); EXPECT_EQ(texture->SamplerMaxLod(), test.MaxLod); } + + }); +} + +TEST(NativeEngineCommands, LegacyAndExtendedPayloadsKeepFollowingCommandsAligned) +{ + RunTextureTest([](Napi::Object engine, Napi::Value value) { + auto* texture = value.As>().Get(); + auto run = Napi::Eval(engine.Env(), R"( + (function(engine, texture) { + if (_native.Engine.PROTOCOL_VERSION !== 9) throw new Error("Expected protocol 9"); + const commands = _native.Engine; + const stream = new _native.NativeDataStream(function() {}); + engine.setCommandDataStream({_nativeDataStream: stream}); + const clear = [0, 0, 0, 0, 0x3f800000, 0, 0x3f800000, 0, 0]; + const stencil = [255, commands.STENCIL_OP_FAIL_S_KEEP, commands.STENCIL_OP_FAIL_Z_KEEP, + commands.STENCIL_OP_PASS_Z_KEEP, commands.STENCIL_TEST_ALWAYS, 0]; + const words = []; + function append(command, values) { + words.push(...command, ...values); + words.push(...commands.COMMAND_SETTEXTURESAMPLING, ...texture, commands.TEXTURE_NEAREST_NEAREST); + } + append(commands.COMMAND_CLEAR, clear); + append(commands.COMMAND_SETSTENCIL2, stencil.concat(3)); + append(commands.COMMAND_CLEAR2, clear.concat(0)); + append(commands.COMMAND_SETSTENCIL, stencil); + const data = new Uint32Array(words); + stream.writeBuffer(data.buffer, data.length); + engine.submitCommands(); + }) + )", "native-command-versions.js").As(); + EXPECT_NO_THROW(run.Call({engine, value})); + EXPECT_EQ(texture->SamplerFlags() & (BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT), + BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT); + EXPECT_EQ(texture->SamplerMaxLod(), 0); }); } diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.TransformFeedback.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.TransformFeedback.cpp new file mode 100644 index 0000000000..b7fb4f5eed --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeEngine.TransformFeedback.cpp @@ -0,0 +1,191 @@ +#ifdef HAS_SHADER_COMPILER +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +namespace +{ + bool RendererSupportsCompute() + { + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + const bool supported = (bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) != 0; + device.FinishRenderingCurrentFrame(); + return supported; + } +} + +// WebGL reads transform feedback results back with a synchronous getBufferSubData. The native engine +// mirrors that with a compute copy plus mid-frame flushes, so the call must return the GPU contents. +TEST(NativeEngineTransformFeedback, ReadTransformFeedbackBufferIsSynchronous) +{ +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + if (!RendererSupportsCompute()) + { + GTEST_SKIP() << "Renderer does not support compute"; + } + constexpr uint32_t kCount = 300; + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + std::promise completed; + auto future = completed.get_future(); + // Frames keep cycling only after the JavaScript holds a frame completion scope, so its calls run in an active frame. + std::promise scoped; + auto scopedFuture = scoped.get_future(); + { + Babylon::AppRuntime runtime{}; + runtime.Dispatch([&](Napi::Env env) { + try + { + device.AddToJavaScript(env); + Babylon::Plugins::NativeEngine::Initialize(env); + auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto scope = context.AcquireFrameCompletionScope(); + scoped.set_value(); + auto engine = env.Global().Get("_native").As().Get("Engine").As().New({}); + auto result = Napi::Eval(env, R"( + (function(engine, count) { + const data = new Float32Array(count); + for (let i = 0; i < count; ++i) { data[i] = i * 0.5 - 7; } + const buffer = engine.createVertexBuffer(data.buffer, 0, data.byteLength, false); + const target = new Float32Array(count + 2).fill(-1); + engine.readTransformFeedbackBuffer(buffer, target.buffer, 4, count * 4); + if (target[0] !== -1 || target[count + 1] !== -1) { return "wrote outside the target range"; } + // Byte-component data (e.g. three normalized unsigned bytes) is not float-sized. + const bytes = new Uint8Array([1, 2, 3]); + engine.readTransformFeedbackBuffer(engine.createVertexBuffer(bytes.buffer, 0, 3, false), target.buffer, 0, 0); + for (let i = 0; i < count; ++i) { + if (target[i + 1] !== data[i]) { return "value " + i + " is " + target[i + 1]; } + } + return ""; + }) + )", "read-transform-feedback.js").As().Call({engine, Napi::Number::New(env, kCount)}); + completed.set_value(result.As().Utf8Value()); + } + catch (const std::exception& ex) + { + completed.set_value(ex.what()); + } + }); + while (scopedFuture.wait_for(std::chrono::milliseconds{1}) != std::future_status::ready && future.wait_for(std::chrono::seconds{0}) != std::future_status::ready) + { + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30}; + while (future.wait_for(std::chrono::milliseconds{1}) != std::future_status::ready) + { + if (std::chrono::steady_clock::now() >= deadline) + { + std::cerr << "Timed out waiting for readTransformFeedbackBuffer" << std::endl; + std::quick_exit(1); + } + device.FinishRenderingCurrentFrame(); + device.StartRenderingCurrentFrame(); + } + EXPECT_EQ(future.get(), ""); + } + device.FinishRenderingCurrentFrame(); +#endif +} +// A transform feedback program captures its varyings for every vertex through the emulating compute shader. +TEST(NativeEngineTransformFeedback, CapturesVaryingsFromVertexInputs) +{ +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + if (!RendererSupportsCompute()) + { + GTEST_SKIP() << "Renderer does not support compute"; + } + constexpr uint32_t kCount = 100; + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + std::promise completed; + auto future = completed.get_future(); + { + Babylon::AppRuntime runtime{}; + runtime.Dispatch([&](Napi::Env env) { + try + { + device.AddToJavaScript(env); + Babylon::Plugins::NativeEngine::Initialize(env); + auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto scope = context.AcquireFrameCompletionScope(); + auto engine = env.Global().Get("_native").As().Get("Engine").As().New({}); + auto result = Napi::Eval(env, R"( + (function(engine, count, floatType) { + const program = engine.createProgram( + "#version 300 es\nprecision highp float;\nin vec2 position;\nin float weight;\n" + + "out vec2 doubled;\nout float sum;\n" + + "void main() { doubled = position * 2.0; sum = position.x + position.y + weight; gl_Position = vec4(0.0); }\n", + "", ["sum", "doubled"]); + const [position, weight] = engine.getAttributes(program, ["position", "weight"]); + if (position < 0 || weight < 0) { return "missing input locations " + position + ", " + weight; } + + const input = new Float32Array(count * 3); + for (let i = 0; i < count; ++i) { input.set([i, i * 0.25 - 3, i % 7], i * 3); } + const inputBuffer = engine.createVertexBuffer(input.buffer, 0, input.byteLength, false); + const output = new Float32Array(count * 3); + const outputBuffer = engine.createVertexBuffer(output.buffer, 0, output.byteLength, false); + const vertexArray = engine.createVertexArray(); + engine.recordVertexBuffer(vertexArray, inputBuffer, position, 0, 12, 2, floatType, false, 0); + engine.recordVertexBuffer(vertexArray, inputBuffer, weight, 8, 12, 1, floatType, false, 0); + + const stream = new _native.NativeDataStream(function() {}); + engine.setCommandDataStream({_nativeDataStream: stream}); + const words = new Uint32Array([ + ..._native.Engine.COMMAND_SETPROGRAM, ...program, + ..._native.Engine.COMMAND_BINDVERTEXARRAY, ...vertexArray, + ..._native.Engine.COMMAND_DRAWTRANSFORMFEEDBACK, ...outputBuffer, 0, count, + ]); + stream.writeBuffer(words.buffer, words.length); + engine.readTransformFeedbackBuffer(outputBuffer, output.buffer, 0, output.byteLength); + for (let i = 0; i < count; ++i) { + const x = input[i * 3], y = input[i * 3 + 1], w = input[i * 3 + 2]; + const expected = [x + y + w, x * 2, y * 2]; + for (let c = 0; c < 3; ++c) { + if (output[i * 3 + c] !== expected[c]) { + return "vertex " + i + " component " + c + " is " + output[i * 3 + c] + ", expected " + expected[c]; + } + } + } + return ""; + }) + )", "transform-feedback-capture.js").As().Call({engine, Napi::Number::New(env, kCount), + Napi::Number::New(env, static_cast(bgfx::AttribType::Float))}); + completed.set_value(result.As().Utf8Value()); + } + catch (const std::exception& ex) + { + completed.set_value(ex.what()); + } + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30}; + while (future.wait_for(std::chrono::milliseconds{1}) != std::future_status::ready) + { + if (std::chrono::steady_clock::now() >= deadline) + { + std::cerr << "Timed out waiting for transform feedback capture" << std::endl; + std::quick_exit(1); + } + device.FinishRenderingCurrentFrame(); + device.StartRenderingCurrentFrame(); + } + EXPECT_EQ(future.get(), ""); + } + device.FinishRenderingCurrentFrame(); +#endif +} +#endif diff --git a/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp b/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp index a432c8857a..7b1a0bc7e4 100644 --- a/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp +++ b/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp @@ -10,6 +10,66 @@ #include #endif +TEST(NativeOptimizations, VertexTransformsAcceptArrayMatricesAndDefaultRanges) +{ +#ifndef HAS_NATIVE_OPTIMIZATIONS + GTEST_SKIP() << "NativeOptimizations is disabled"; +#else + std::promise completed; + auto completion = completed.get_future(); + Babylon::AppRuntime runtime{}; + runtime.Dispatch([&](Napi::Env env) { + try + { + Babylon::Plugins::NativeOptimizations::Initialize(env); + Napi::Eval(env, R"( + (function () { + const values = [1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0, 10, 11, 12, 1]; + const transforms = [ + ["_TransformVector3Coordinates", 3, [1, 2, 3], [40, 47, 54]], + ["_TransformVector3Normals", 3, [1, 2, 3], [30, 36, 42]], + ["_TransformVector4Normals", 4, [1, 2, 3, 4], [30, 36, 42, 4]], + ]; + for (const matrix of [new Float32Array(values), values]) { + for (const [name, stride, input, expected] of transforms) { + for (const range of [[], [undefined, undefined], [0, stride]]) { + const data = new Float32Array(input); + _native[name](data, { _m: matrix }, ...range); + if (Array.from(data).join(",") !== expected.join(",")) { + throw new Error(name + " produced " + Array.from(data).join(",")); + } + } + } + } + for (const Type of [Uint16Array, Uint32Array, Int32Array]) { + const indices = new Type([0, 1, 2, 3, 4, 5]); + _native._FlipFaces(indices); + if (Array.from(indices).join(",") !== "0,2,1,3,5,4") { + throw new Error("_FlipFaces did not default to the whole array"); + } + } + })(); + )", "native-vertex-transforms.js"); + completed.set_value(); + } + catch (const Napi::Error& error) + { + completed.set_exception(std::make_exception_ptr(std::runtime_error{Napi::GetErrorString(error)})); + } + catch (...) + { + completed.set_exception(std::current_exception()); + } + }); + if (completion.wait_for(std::chrono::seconds{30}) != std::future_status::ready) + { + ADD_FAILURE() << "Timed out waiting for native vertex transforms"; + std::quick_exit(1); + } + EXPECT_NO_THROW(completion.get()); +#endif +} + TEST(NativeOptimizations, SplatSortingAcceptsTypedAndNumberArrayMatrices) { #ifndef HAS_NATIVE_OPTIMIZATIONS diff --git a/Apps/UnitTests/Source/Tests.ShaderCache.TransformFeedback.cpp b/Apps/UnitTests/Source/Tests.ShaderCache.TransformFeedback.cpp new file mode 100644 index 0000000000..a4d52571a5 --- /dev/null +++ b/Apps/UnitTests/Source/Tests.ShaderCache.TransformFeedback.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include + +#include + +#include +#include +#include + +TEST(ShaderCache, TransformFeedbackEntriesRoundTripSeparatelyFromGraphics) +{ + Babylon::Plugins::ShaderCache::Enable(); + Babylon::Plugins::ShaderCache::Clear(); + const auto disableCache = gsl::finally([] { + Babylon::Plugins::ShaderCache::Clear(); + Babylon::Plugins::ShaderCache::Disable(); + }); + + const std::string vertexSource = "void main() { gl_Position = vec4(0.0); }"; + const std::string fragmentSource = "void main() { gl_FragColor = vec4(1.0); }"; + const std::string feedbackSource = "#version 300 es\nout vec4 outPosition;\nvoid main() { outPosition = vec4(1.0); }\n"; + const std::vector varyings{"outPosition"}; + + Babylon::Graphics::BgfxShaderInfo graphics{}; + graphics.VertexBytes = {1, 2, 3}; + graphics.FragmentBytes = {4, 5}; + graphics.VertexAttributeLocations = {{"position", 0}}; + graphics.UniformStages = {{"diffuseSampler", 2}}; + + Babylon::Graphics::BgfxShaderInfo compute{}; + compute.ComputeBytes = {9, 8, 7, 6}; + compute.UniformStages = {{"depthInput", 5}}; + compute.UniformNames = {{"_12_textureSize", "textureSize"}}; + + Babylon::Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, graphics); + Babylon::Plugins::ShaderCache::AddTransformFeedbackShader(feedbackSource, varyings, compute); + + // Transform feedback and graphics entries use separate key spaces, and varyings are part of the key. + EXPECT_EQ(Babylon::Plugins::ShaderCache::GetTransformFeedbackShader(vertexSource, varyings), nullptr); + EXPECT_EQ(Babylon::Plugins::ShaderCache::GetTransformFeedbackShader(feedbackSource, {"other"}), nullptr); + EXPECT_EQ(Babylon::Plugins::ShaderCache::GetShader(feedbackSource, ""), nullptr); + + std::stringstream stream{std::ios::in | std::ios::out | std::ios::binary}; + ASSERT_EQ(Babylon::Plugins::ShaderCache::Save(stream), 2u); + Babylon::Plugins::ShaderCache::Clear(); + EXPECT_EQ(Babylon::Plugins::ShaderCache::GetTransformFeedbackShader(feedbackSource, varyings), nullptr); + ASSERT_EQ(Babylon::Plugins::ShaderCache::Load(stream), 2u); + + std::string crlfFeedbackSource; + for (const char ch : feedbackSource) + { + if (ch == '\n') + { + crlfFeedbackSource.push_back('\r'); + } + crlfFeedbackSource.push_back(ch); + } + const auto loadedCompute = Babylon::Plugins::ShaderCache::GetTransformFeedbackShader(crlfFeedbackSource, varyings); + ASSERT_NE(loadedCompute, nullptr); + EXPECT_EQ(loadedCompute->ComputeBytes, compute.ComputeBytes); + EXPECT_TRUE(loadedCompute->VertexBytes.empty()); + EXPECT_TRUE(loadedCompute->FragmentBytes.empty()); + EXPECT_EQ(loadedCompute->UniformStages, compute.UniformStages); + EXPECT_EQ(loadedCompute->UniformNames, compute.UniformNames); + + const auto loadedGraphics = Babylon::Plugins::ShaderCache::GetShader(vertexSource, fragmentSource); + ASSERT_NE(loadedGraphics, nullptr); + EXPECT_EQ(loadedGraphics->VertexBytes, graphics.VertexBytes); + EXPECT_EQ(loadedGraphics->FragmentBytes, graphics.FragmentBytes); + EXPECT_TRUE(loadedGraphics->ComputeBytes.empty()); + EXPECT_EQ(loadedGraphics->VertexAttributeLocations, graphics.VertexAttributeLocations); + EXPECT_EQ(loadedGraphics->UniformStages, graphics.UniformStages); + + std::stringstream resaved{std::ios::in | std::ios::out | std::ios::binary}; + EXPECT_EQ(Babylon::Plugins::ShaderCache::Save(resaved), 2u); + EXPECT_EQ(resaved.str(), stream.str()); +} \ No newline at end of file diff --git a/Apps/UnitTests/Source/Tests.ShaderCompilation.FragCoord.cpp b/Apps/UnitTests/Source/Tests.ShaderCompilation.FragCoord.cpp index bcee0cb8bf..c663a04631 100644 --- a/Apps/UnitTests/Source/Tests.ShaderCompilation.FragCoord.cpp +++ b/Apps/UnitTests/Source/Tests.ShaderCompilation.FragCoord.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -429,6 +431,59 @@ namespace } } +TEST(ShaderCompilation, UniformNamesSurviveNativeBindingAndCache) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + Babylon::Plugins::ShaderCache::Enable(); + Babylon::Plugins::ShaderCache::Clear(); + const auto disableCache = gsl::finally([] { + Babylon::Plugins::ShaderCache::Clear(); + Babylon::Plugins::ShaderCache::Disable(); + }); + const auto render = [] { + return RenderFullScreenQuad(4, 4, + "attribute vec3 position; void main() { gl_Position = vec4(position, 1.0); }", + R"( + precision highp float; + uniform highp sampler2D inputSampler; + uniform vec2 textureSize; + uniform mat4 u_view; + uniform float bnUserUniform_textureSize; + uniform float bnSamplerState_inputSampler; + void main() { + gl_FragColor = vec4(textureSize.x / float(textureSize(inputSampler, 0).x), + u_view[0][0], bnUserUniform_textureSize * bnSamplerState_inputSampler, + texture2D(inputSampler, vec2(0.5)).a); + } + )", true, R"( + material.setVector2("textureSize", new BABYLON.Vector2(1, 2)); + material.setMatrix("u_view", BABYLON.Matrix.Scaling(0.5, 0.5, 0.5)); + material.setFloat("bnUserUniform_textureSize", 0.75); + material.setFloat("bnSamplerState_inputSampler", 1.0); + )"); + }; + const auto original = render(); + ASSERT_EQ(original.size(), 4u * 4u * 4u); + for (size_t offset = 0; offset < original.size(); offset += 4) + { + EXPECT_NEAR(original[offset], 64, 1); + EXPECT_NEAR(original[offset + 1], 128, 1); + EXPECT_NEAR(original[offset + 2], 191, 1); + EXPECT_EQ(original[offset + 3], 255); + } + std::stringstream stream{std::ios::in | std::ios::out | std::ios::binary}; + const auto count = Babylon::Plugins::ShaderCache::Save(stream); + ASSERT_GT(count, 0u); + Babylon::Plugins::ShaderCache::Clear(); + EXPECT_EQ(Babylon::Plugins::ShaderCache::Load(stream), count); + EXPECT_EQ(render(), original); + std::stringstream after; + EXPECT_EQ(Babylon::Plugins::ShaderCache::Save(after), count); +#endif +} + TEST(ShaderCompilation, FragCoordSetupAndPreparationFailuresPropagate) { #if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) @@ -540,6 +595,68 @@ TEST(ShaderCompilation, MultiRowMorphTextureMatchesVertexAttributes) #endif } +TEST(ShaderCompilation, NearestSamplerBoundariesPreserveLinearFilteringAndWrapModes) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) || defined(SKIP_FLIPPED_SAMPLING_TESTS) + GTEST_SKIP(); +#else + const std::array, 3> point{{ + {32, 32, 64, 128, 224, 224, 224}, + {224, 32, 64, 128, 224, 32, 64}, + {32, 32, 64, 128, 224, 224, 128}, + }}; + const std::array, 3> linear{{ + {32, 32, 48, 96, 176, 224, 224}, + {176, 128, 48, 96, 176, 128, 48}, + {48, 32, 48, 96, 176, 224, 176}, + }}; + for (int wrap = 0; wrap < 3; ++wrap) + { + for (const std::string sample : {"texture2D(source, uv)", "textureLod(source, uv, 0.0)", + "textureGrad(source, uv, vec2(0.0), vec2(0.0))"}) + { + SCOPED_TRACE(std::to_string(wrap) + ": " + sample); + const auto pixels = RenderFullScreenQuad(7, 1, + "attribute vec3 position; void main() { gl_Position = vec4(position, 1.0); }", + R"( + precision highp float; + uniform highp sampler2D pointSampler, linearSampler; + int calls = 0; + vec2 nextCoordinate() { + calls++; + return vec2(0.5, (floor(gl_FragCoord.x) - 1.0) / 4.0); + } + vec4 innerSample(sampler2D source, vec2 uv) { return )" + sample + R"(; } + vec4 outerSample(sampler2D source, vec2 uv) { return innerSample(source, uv); } + void main() { + vec2 uv = vec2(0.5, (floor(gl_FragCoord.x) - 1.0) / 4.0); + vec4 point = outerSample(pointSampler, nextCoordinate()); + vec4 linear = outerSample(linearSampler, uv); + gl_FragColor = vec4(point.r, linear.r, float(calls) * 0.25, 1.0); + } + )", false, + "var wrap = " + std::to_string(wrap) + R"(; + var data = new Uint8Array([32,0,0,255, 64,0,0,255, 128,0,0,255, 224,0,0,255]); + for (var i = 0; i < 2; ++i) { + var texture = BABYLON.RawTexture.CreateRGBATexture(data, 1, 4, scene, false, false, + i ? BABYLON.Texture.BILINEAR_SAMPLINGMODE : BABYLON.Texture.NEAREST_SAMPLINGMODE); + texture.wrapV = wrap; + material.setTexture(i ? "linearSampler" : "pointSampler", texture); + } + )"); + ASSERT_EQ(pixels.size(), 28u); + for (size_t index = 0; index < 7; ++index) + { + EXPECT_EQ(pixels[index * 4], point[wrap][index]); + EXPECT_NEAR(pixels[index * 4 + 1], linear[wrap][index], 1); + EXPECT_NEAR(pixels[index * 4 + 2], 64, 1); + EXPECT_EQ(pixels[index * 4 + 3], 255); + } + } + } +#endif +} + TEST(ShaderCompilation, VolumeCoordinateSideEffectsExecuteOnce) { #if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) @@ -573,6 +690,107 @@ TEST(ShaderCompilation, VolumeCoordinateSideEffectsExecuteOnce) #endif } +TEST(ShaderCompilation, RawArraySamplingPreservesRowsAndLayers) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) || defined(SKIP_FLIPPED_SAMPLING_TESTS) + GTEST_SKIP(); +#else + for (const std::string sample : { + "texture(arrayTexture, (vec3(coord) + vec3(0.5, 0.5, 0.0)) / vec3(2.0, 2.0, 1.0))", + "texelFetch(arrayTexture, coord, 0)", + "textureGrad(arrayTexture, (vec3(coord) + vec3(0.5, 0.5, 0.0)) / vec3(2.0, 2.0, 1.0), vec2(0.0), vec2(0.0))"}) + { + SCOPED_TRACE(sample); + const std::string fragmentShader = R"( + precision highp float; + uniform highp sampler2DArray arrayTexture; + void main() { + ivec3 coord = ivec3(int(gl_FragCoord.x) % 2, int(gl_FragCoord.y), int(gl_FragCoord.x) / 2); + gl_FragColor = )" + sample + R"(; + } + )"; + const auto pixels = RenderFullScreenQuad(4, 2, + "attribute vec3 position; void main() { gl_Position = vec4(position, 1.0); }", + fragmentShader, false, R"( + var data = new Uint8Array(32); + for (var layer = 0; layer < 2; ++layer) { + for (var y = 0; y < 2; ++y) { + for (var x = 0; x < 2; ++x) { + data.set([64 + layer * 128, 64 + y * 128, 32 + x * 64, 255], + ((layer * 2 + y) * 2 + x) * 4); + } + } + } + var original = Array.from(data).join(","); + var array = BABYLON.RawTexture2DArray.CreateRGBATexture( + data, 2, 2, 2, scene, false, false, BABYLON.Texture.NEAREST_SAMPLINGMODE); + if (Array.from(data).join(",") !== original) { + throw new Error("Array upload modified its source"); + } + material.setTexture("arrayTexture", array); + )"); + ASSERT_EQ(pixels.size(), 32u); + for (uint32_t row = 0; row < 2; ++row) + { + for (uint32_t x = 0; x < 4; ++x) + { + const auto offset = (row * 4 + x) * 4; + EXPECT_EQ(pixels[offset], 64 + (x / 2) * 128); + EXPECT_EQ(pixels[offset + 1], 64 + (1 - row) * 128); + EXPECT_EQ(pixels[offset + 2], 32 + (x % 2) * 64); + EXPECT_EQ(pixels[offset + 3], 255); + } + } + } +#endif +} + +TEST(ShaderCompilation, CompressedArraySamplingPreservesPartialRowsAndLayers) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) || defined(SKIP_FLIPPED_SAMPLING_TESTS) + GTEST_SKIP(); +#else + for (const std::string sample : { + "texture(arrayTexture, vec3(0.5, gl_FragCoord.y / 6.0, floor(gl_FragCoord.x)))", + "texelFetch(arrayTexture, ivec3(1, int(gl_FragCoord.y), int(gl_FragCoord.x)), 0)"}) + { + SCOPED_TRACE(sample); + const auto pixels = RenderFullScreenQuad(2, 6, + "attribute vec3 position; void main() { gl_Position = vec4(position, 1.0); }", + "precision highp float; uniform highp sampler2DArray arrayTexture; void main() { gl_FragColor = " + sample + "; }", + false, R"( + var data = new Uint8Array([ + 0x00, 0xf8, 0x00, 0xf8, 0, 0, 0, 0, + 0xe0, 0x07, 0xe0, 0x07, 0, 0, 0, 0, + 0x1f, 0x00, 0x1f, 0x00, 0, 0, 0, 0, + 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0 + ]); + var original = Array.from(data).join(","); + var array = new BABYLON.RawTexture2DArray( + data, 3, 6, 2, BABYLON.Constants.TEXTUREFORMAT_COMPRESSED_RGB_S3TC_DXT1, + scene, false, false, BABYLON.Texture.NEAREST_SAMPLINGMODE); + if (Array.from(data).join(",") !== original) { + throw new Error("Compressed array upload modified its source"); + } + material.setTexture("arrayTexture", array); + )"); + ASSERT_EQ(pixels.size(), 48u); + for (uint32_t row = 0; row < 6; ++row) + { + const auto offset = row * 8; + EXPECT_EQ(pixels[offset], row < 2 ? 0 : 255); + EXPECT_EQ(pixels[offset + 1], row < 2 ? 255 : 0); + EXPECT_EQ(pixels[offset + 2], 0); + EXPECT_EQ(pixels[offset + 3], 255); + EXPECT_EQ(pixels[offset + 4], row < 2 ? 255 : 0); + EXPECT_EQ(pixels[offset + 5], row < 2 ? 255 : 0); + EXPECT_EQ(pixels[offset + 6], 255); + EXPECT_EQ(pixels[offset + 7], 255); + } + } +#endif +} + TEST(ShaderCompilation, IntegerVolumeCoordinatesExecuteOnce) { #if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) @@ -660,23 +878,20 @@ TEST(NativeEngineReadback, FloatTexturesUsePinnedJavaScriptByteContract) var texture = BABYLON.RawTexture.CreateRGBATexture( new Float32Array([0.25, 0.5, 0.75, 1.0]), 1, 1, scene, false, false, BABYLON.Texture.NEAREST_SAMPLINGMODE, BABYLON.Constants.TEXTURETYPE_FLOAT); - return texture.readPixels().then(function (bytes) { - if (!(bytes instanceof Uint8Array) || bytes.length !== 4) { - throw new Error("Default float readback must contain four RGBA8 bytes"); - } + var handle = texture.getInternalTexture()._hardwareTexture.underlyingResource; + return engine._engine.readTexture(handle, 0, 0, 0, 1, 1, null, 0, 0, -1).then(function (buffer) { + if (buffer.byteLength !== 4) throw new Error("Legacy readTexture must return four RGBA8 bytes"); + var bytes = new Uint8Array(buffer); [64, 128, 191, 255].forEach(function (value, index) { - if (Math.abs(bytes[index] - value) > 1) { - throw new Error("Incorrect float-to-byte readback at channel " + index); - } + if (Math.abs(bytes[index] - value) > 1) throw new Error("Incorrect legacy readback"); }); var destination = new Uint8Array(12); destination.fill(91); - var region = destination.subarray(4, 8); - return texture.readPixels(0, 0, region).then(function (result) { - if (result !== region) throw new Error("Readback replaced the supplied buffer"); + return engine._engine.readTexture(handle, 0, 0, 0, 1, 1, destination.buffer, 4, 4, -1).then(function () { for (var i = 0; i < destination.length; ++i) { - var expected = i >= 4 && i < 8 ? bytes[i - 4] : 91; - if (destination[i] !== expected) throw new Error("Readback changed bytes outside its view"); + if (destination[i] !== (i >= 4 && i < 8 ? bytes[i - 4] : 91)) { + throw new Error("Legacy readback changed values outside its view"); + } } }); }); @@ -1165,3 +1380,391 @@ TEST(ShaderCompilation, InterfaceBlocksReserveAllMemberLocations) EXPECT_EQ(pixels[3], 255); #endif } + +TEST(NativeEngineShadows, PointLightCubeOrientationMatchesAllShadowLookups) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + constexpr uint32_t CELL_SIZE = 8; + constexpr uint32_t FACE_COUNT = 6; + constexpr uint32_t WIDTH = FACE_COUNT * 2 * CELL_SIZE; + constexpr uint32_t HEIGHT = CELL_SIZE; + + const std::string vertexShader = + "precision highp float;\n" + "attribute vec3 position;\n" + "attribute vec2 uv;\n" + "varying vec2 vUV;\n" + "void main(void) { vUV = uv; gl_Position = vec4(position, 1.0); }\n"; + + struct ShadowCase + { + int Filter; + const char* Name; + bool BackFaceCulling; + bool CullBackFaces; + bool ForceBackFacesOnly; + const char* Lookup; + }; + const ShadowCase cases[] = { + {0, "none/back-cull", true, true, false, "computeShadowCube(worldPos, vec3(0.0), shadowSampler, 0.0, vec2(0.1, 10.1))"}, + {0, "none/front-cull", true, false, false, "computeShadowCube(worldPos, vec3(0.0), shadowSampler, 0.0, vec2(0.1, 10.1))"}, + {0, "none/force-back-faces", true, true, true, "computeShadowCube(worldPos, vec3(0.0), shadowSampler, 0.0, vec2(0.1, 10.1))"}, + {1, "esm", true, true, false, "computeShadowWithESMCube(worldPos, vec3(0.0), shadowSampler, 0.0, 20.0, vec2(0.1, 10.1))"}, + {2, "poisson", true, true, false, "computeShadowWithPoissonSamplingCube(worldPos, vec3(0.0), shadowSampler, 1.0 / 64.0, 0.0, vec2(0.1, 10.1))"}, + {4, "close-esm", true, true, false, "computeShadowWithCloseESMCube(worldPos, vec3(0.0), shadowSampler, 0.0, 20.0, vec2(0.1, 10.1))"}, + }; + + for (const auto& shadowCase : cases) + { + SCOPED_TRACE(shadowCase.Name); + + // Each pair addresses one cube face in PointLight.getShadowDirection order: + // +X, -X, -Y, +Y, +Z, -Z. The first ray goes through an off-axis + // caster; the second reflects that ray across the face's projection-Y + // axis and must stay lit. The +/-Y faces use Z as their vertical axis. + const std::string fragmentShader = + "precision highp float;\n" + "varying vec2 vUV;\n" + "uniform samplerCube shadowSampler;\n" + "#define SHADOWS\n" + "#include\n" + "void main(void) {\n" + " float cell = floor(min(vUV.x, 0.999999) * 12.0);\n" + " vec3 direction;\n" + " if (cell < 0.5) direction = vec3( 1.00, 0.31, 0.17);\n" + " else if (cell < 1.5) direction = vec3( 1.00, -0.31, 0.17);\n" + " else if (cell < 2.5) direction = vec3(-1.00, -0.27, 0.19);\n" + " else if (cell < 3.5) direction = vec3(-1.00, 0.27, 0.19);\n" + " else if (cell < 4.5) direction = vec3( 0.23, -1.00, 0.37);\n" + " else if (cell < 5.5) direction = vec3( 0.23, -1.00, -0.37);\n" + " else if (cell < 6.5) direction = vec3(-0.21, 1.00, 0.33);\n" + " else if (cell < 7.5) direction = vec3(-0.21, 1.00, -0.33);\n" + " else if (cell < 8.5) direction = vec3( 0.29, 0.35, 1.00);\n" + " else if (cell < 9.5) direction = vec3( 0.29, -0.35, 1.00);\n" + " else if (cell < 10.5) direction = vec3(-0.25, -0.33, -1.00);\n" + " else direction = vec3(-0.25, 0.33, -1.00);\n" + " vec3 worldPos = normalize(direction) * 6.0;\n" + " float visibility = " + + std::string{shadowCase.Lookup} + + ";\n" + " gl_FragColor = vec4(vec3(visibility), 1.0);\n" + "}\n"; + + const std::string setupScript = + R"( + var filter = )" + + std::to_string(shadowCase.Filter) + + R"(; + var backFaceCulling = )" + + std::string{shadowCase.BackFaceCulling ? "true" : "false"} + + R"(; + var cullBackFaces = )" + + std::string{shadowCase.CullBackFaces ? "true" : "false"} + + R"(; + var forceBackFacesOnly = )" + + std::string{shadowCase.ForceBackFacesOnly ? "true" : "false"} + + R"(; + + camera.layerMask = 0x1; + quad.layerMask = 0x1; + quad.renderingGroupId = 1; + + var light = new BABYLON.PointLight("shadowLight", BABYLON.Vector3.Zero(), scene); + light.shadowMinZ = 0.1; + light.shadowMaxZ = 10.0; + + // Force the portable packed-RGBA shadow path so the lookup + // shader has one format on every Native backend. + var caps = engine.getCaps(); + var capNames = [ + "textureHalfFloatRender", + "textureHalfFloatLinearFiltering", + "textureFloatRender", + "textureFloatLinearFiltering" + ]; + var savedCaps = capNames.map(function (name) { return caps[name]; }); + var shadowGenerator; + try { + capNames.forEach(function (name) { caps[name] = false; }); + shadowGenerator = new BABYLON.ShadowGenerator(64, light); + } finally { + capNames.forEach(function (name, index) { caps[name] = savedCaps[index]; }); + } + shadowGenerator.filter = filter; + shadowGenerator.bias = 0.0; + shadowGenerator.depthScale = 20.0; + shadowGenerator.forceBackFacesOnly = forceBackFacesOnly; + + var casterMaterial = new BABYLON.StandardMaterial("casterMaterial", scene); + casterMaterial.disableLighting = true; + casterMaterial.backFaceCulling = backFaceCulling; + casterMaterial.cullBackFaces = cullBackFaces; + + var casterDirections = [ + new BABYLON.Vector3( 1.00, 0.31, 0.17), + new BABYLON.Vector3(-1.00, -0.27, 0.19), + new BABYLON.Vector3( 0.23, -1.00, 0.37), + new BABYLON.Vector3(-0.21, 1.00, 0.33), + new BABYLON.Vector3( 0.29, 0.35, 1.00), + new BABYLON.Vector3(-0.25, -0.33, -1.00) + ]; + casterDirections.forEach(function (direction, face) { + direction.normalize(); + var caster = BABYLON.MeshBuilder.CreateBox( + "caster" + face, + { width: 0.8, height: 0.8, depth: 0.8 }, + scene); + caster.position.copyFrom(direction.scale(3.0)); + caster.material = casterMaterial; + caster.layerMask = 0x2; + shadowGenerator.addShadowCaster(caster, false); + }); + + material.options.samplers.push("shadowSampler"); + material.setTexture("shadowSampler", shadowGenerator.getShadowMapForRendering()); + globalThis.__prepare = function () { + return Promise.all([ + shadowGenerator.forceCompilationAsync(), + material.forceCompilationAsync(quad) + ]); + }; + )"; + + const auto pixels = RenderFullScreenQuad(WIDTH, HEIGHT, vertexShader, fragmentShader, false, setupScript); + ASSERT_EQ(pixels.size(), static_cast(WIDTH) * HEIGHT * 4); + + const auto redAtCell = [&pixels](uint32_t cell) { + const uint32_t x = cell * CELL_SIZE + CELL_SIZE / 2; + const uint32_t y = HEIGHT / 2; + return static_cast(pixels[(static_cast(y) * WIDTH + x) * 4]); + }; + const char* faceNames[] = {"+X", "-X", "-Y", "+Y", "+Z", "-Z"}; + for (uint32_t face = 0; face < FACE_COUNT; ++face) + { + const int shadowed = redAtCell(face * 2); + const int lit = redAtCell(face * 2 + 1); + EXPECT_LT(shadowed, 64) + << faceNames[face] << " off-axis caster ray was not shadowed (visibility=" << shadowed << ")"; + EXPECT_GT(lit, 192) + << faceNames[face] << " projection-Y mirror ray was not lit (visibility=" << lit << ")"; + } + } +#endif +} + +TEST(NativeEngineTextureSampling, RawUploadsPreserveEarlierCameraDraws) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + const std::string vertexShader = + "precision highp float;\n" + "attribute vec3 position;\n" + "void main(void) { gl_Position = vec4(position, 1.0); }\n"; + const std::string fragmentShader = + "precision highp float;\n" + "uniform sampler2D inputSampler;\n" + "void main(void) { gl_FragColor = textureLod(inputSampler, vec2(0.5), 1.0); }\n"; + for (const bool mipmaps : {false, true}) + { + SCOPED_TRACE(mipmaps); + const std::string setupScript = std::string{R"( + var source = BABYLON.RawTexture.CreateRGBATexture( + new Uint8Array(16), 2, 2, scene, )"} + (mipmaps ? "true" : "false") + R"(, + false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE); + material.options.samplers.push("inputSampler"); + material.setTexture("inputSampler", source); + scene.autoClear = false; + scene.autoClearDepthAndStencil = false; + camera.viewport = new BABYLON.Viewport(0, 0, 0.5, 1); + var second = camera.clone("second"); + second.viewport = new BABYLON.Viewport(0.5, 0, 0.5, 1); + second.outputRenderTarget = outputTexture; + scene.activeCameras = [camera, second]; + scene.onBeforeCameraRenderObservable.add(function (active) { + var color = active === camera ? [255, 0, 0, 255] : [0, 255, 0, 255]; + source.update(new Uint8Array(color.concat(color, color, color))); + }); + )"; + const auto pixels = RenderFullScreenQuad(8, 4, vertexShader, fragmentShader, false, setupScript); + ASSERT_EQ(pixels.size(), 8u * 4u * 4u); + for (uint32_t y = 0; y < 4; ++y) + { + for (uint32_t x = 0; x < 8; ++x) + { + const auto offset = (y * 8 + x) * 4; + EXPECT_EQ(pixels[offset], x < 4 ? 255 : 0); + EXPECT_EQ(pixels[offset + 1], x < 4 ? 0 : 255); + EXPECT_EQ(pixels[offset + 2], 0); + EXPECT_EQ(pixels[offset + 3], 255); + } + } + } +#endif +} + +TEST(NativeEngineTextureSampling, NoMipSamplingPreservesFiltersAndModeChanges) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + const std::string vertexShader = + "precision highp float;\n" + "attribute vec3 position;\n" + "attribute vec2 uv;\n" + "varying vec2 vUV;\n" + "void main(void) { vUV = uv; gl_Position = vec4(position, 1.0); }\n"; + struct SamplingCase + { + int Mode; + int Anisotropy; + int Red; + int Green; + int MagnifiedRed; + }; + const SamplingCase cases[] = { + {1, 1, 0, 0, 255}, // Nearest, no mips. + {2, 1, 128, 0, 191}, // Linear, no mips. + {2, 4, 255, 0, 255}, // Anisotropic, constant-color base mip. + {3, 1, 0, 255, 191}, // Linear mip filtering restored. + {4, 1, 0, 255, 255}, // Point mip filtering restored. + }; + for (const auto& sample : cases) + { + SCOPED_TRACE(::testing::Message() << "mode=" << sample.Mode << ", anisotropy=" << sample.Anisotropy); + const std::string setupScript = R"( + var requestedMode = )" + std::to_string(sample.Mode) + R"(; + var anisotropy = )" + std::to_string(sample.Anisotropy) + R"(; + // Distinct lower mips separate mip selection from spatial filtering. + var sampled; + for (var mip = 0, size = 8; size >= 1; mip++, size /= 2) { + var pixels = new Uint8Array(size * size * 4); + var offset = 0; + for (var y = 0; y < size; y++) { + for (var x = 0; x < size; x++) { + pixels[offset++] = mip === 0 && (anisotropy > 1 || x % 2 === 0) ? 255 : 0; + pixels[offset++] = mip === 0 ? 0 : 255; + pixels[offset++] = 0; + pixels[offset++] = 255; + } + } + if (mip === 0) { + sampled = BABYLON.RawTexture.CreateRGBATexture(pixels, size, size, scene, true, false, 1); + } else { + engine.updateTextureData(sampled.getInternalTexture(), pixels, 0, 0, size, size, 0, mip); + } + } + sampled.anisotropicFilteringLevel = anisotropy; + sampled.updateSamplingMode(4); + sampled.updateSamplingMode(requestedMode); + material.setTexture("inputSampler", sampled); + )"; + for (const bool magnify : {false, true}) + { + SCOPED_TRACE(::testing::Message() << "magnify=" << magnify); + const std::string fragmentShader = + "precision highp float;\n" + "uniform sampler2D inputSampler;\n" + "varying vec2 vUV;\n" + "void main(void) { gl_FragColor = texture2D(inputSampler, vUV * " + + std::string{magnify ? "0.125" : "0.5"} + " + 0.25); }\n"; + const auto pixels = RenderFullScreenQuad(2, 2, vertexShader, fragmentShader, true, setupScript); + ASSERT_EQ(pixels.size(), 16u); + for (size_t offset = 0; offset < pixels.size(); offset += 4) + { + EXPECT_NEAR(pixels[offset], magnify ? sample.MagnifiedRed : sample.Red, 1); + EXPECT_NEAR(pixels[offset + 1], magnify ? 0 : sample.Green, 1); + EXPECT_EQ(pixels[offset + 2], 0); + EXPECT_EQ(pixels[offset + 3], 255); + } + } + } +#endif +} + +TEST(ShaderCompilation, PbrRoughnessSquareInNestedLoops) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + const std::string vertexShader = + "precision highp float;\n" + "attribute vec3 position;\n" + "void main(void) { gl_Position = vec4(position, 1.0); }\n"; + + // Use the production include: FXC can replace roughness squared with + // roughness when the saturated value is used inside nested dynamic loops. + const std::string fragmentShader = + "precision highp float;\n" + "uniform vec2 targetSize;\n" + "#include\n" + "#include\n" + "void main(void) {\n" + " vec3 result = vec3(0.0);\n" + " for (int i = 0; i < int(targetSize.x); ++i) {\n" + " for (int j = 0; j < int(targetSize.y); ++j) {\n" + " float roughness = clamp(1.0 / targetSize.x, 0.0, 1.0);\n" + " result = vec3(roughness, convertRoughnessToAverageSlope(roughness), sqrt(roughness));\n" + " }\n" + " }\n" + " gl_FragColor = vec4(result, 1.0);\n" + "}\n"; + + auto pixels = RenderFullScreenQuad(2, 1, vertexShader, fragmentShader, false); + ASSERT_EQ(pixels.size(), 8u); + for (size_t offset = 0; offset < pixels.size(); offset += 4) + { + EXPECT_NEAR(pixels[offset], 128, 1); + EXPECT_NEAR(pixels[offset + 1], 64, 1) << "roughness 0.5 must produce alphaG 0.2505, not 0.5005"; + EXPECT_NEAR(pixels[offset + 2], 180, 1); + EXPECT_EQ(pixels[offset + 3], 255); + } +#endif +} + +TEST(ShaderCompilation, SaturatedArithmeticInNestedLoops) +{ +#if defined(SKIP_EXTERNAL_TEXTURE_TESTS) || defined(SKIP_RENDER_TESTS) + GTEST_SKIP(); +#else + const std::string vertexShader = + "precision highp float;\n" + "attribute vec3 position;\n" + "void main(void) { gl_Position = vec4(position, 1.0); }\n"; + const std::string arithmetic = + "float value = clamp(1.0 / targetSize.x, 0.0, 1.0);\n" + "float squared = value * value;\n" + "float fifth = squared * squared * value;\n" + "float attenuation = clamp(1.0 - value, 0.0, 1.0);\n" + "attenuation *= attenuation;\n" + "result = vec3(squared, fifth, attenuation);\n"; + const std::vector> loops{ + {"for (int i = 0; i < int(targetSize.x); ++i) { for (int j = 0; j < int(targetSize.y); ++j) {\n", "}}\n"}, + {"int i = 0; while (i++ < int(targetSize.x)) { int j = 0; while (j++ < int(targetSize.y)) {\n", "}}\n"}, + {"int i = 0; do { int j = 0; do {\n", "} while (++j < int(targetSize.y)); } while (++i < int(targetSize.x));\n"}, + }; + for (const auto& loop : loops) + { + SCOPED_TRACE(loop.first); + const std::string fragmentShader = + "precision highp float;\n" + "uniform vec2 targetSize;\n" + "void main(void) {\n" + "vec3 result = vec3(0.0);\n" + + loop.first + arithmetic + loop.second + + "gl_FragColor = vec4(result, 1.0);\n" + "}\n"; + auto pixels = RenderFullScreenQuad(2, 1, vertexShader, fragmentShader, false); + ASSERT_EQ(pixels.size(), 8u); + for (size_t offset = 0; offset < pixels.size(); offset += 4) + { + EXPECT_NEAR(pixels[offset], 64, 1); + EXPECT_NEAR(pixels[offset + 1], 8, 1); + EXPECT_NEAR(pixels[offset + 2], 64, 1); + EXPECT_EQ(pixels[offset + 3], 255); + } + } +#endif +} diff --git a/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp b/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp index fc9fbfe81e..8cb49ca4e9 100644 --- a/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp +++ b/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp @@ -37,18 +37,45 @@ using namespace std::chrono_literals; extern Babylon::Graphics::Configuration g_deviceConfig; #if defined(BABYLON_NATIVE_GRAPHICS_API_D3D11) || defined(BABYLON_NATIVE_GRAPHICS_API_VULKAN) +#include +#include + namespace { - gsl::span ReadUniformFreeShader(const std::vector& bytes) + gsl::span ReadFixtureShaderCode(const std::vector& bytes) { - // These fixtures have no uniforms: bgfx v13's code length starts at byte 22. - constexpr size_t codeOffset = 26; - if (bytes.size() < codeOffset || bytes[3] != 13 || bytes[20] != 0 || bytes[21] != 0) + // bgfx v13: 20-byte header, uint16 uniform count, uniform table, then uint32 code length. + // The only uniform these fixtures may carry is the injected render target transform. + constexpr size_t countOffset = 20; + if (bytes.size() < countOffset + 2 || bytes[3] != 13) + { + throw std::runtime_error{"Expected a bgfx v13 shader"}; + } + uint16_t uniformCount{}; + std::memcpy(&uniformCount, bytes.data() + countOffset, sizeof(uniformCount)); + size_t offset = countOffset + sizeof(uniformCount); + for (uint16_t uniform = 0; uniform < uniformCount; ++uniform) { - throw std::runtime_error{"Expected a uniform-free bgfx v13 shader"}; + if (offset >= bytes.size()) + { + throw std::runtime_error{"Invalid shader uniform table"}; + } + const size_t nameSize = bytes[offset]; + if (offset + 1 + nameSize > bytes.size() || + std::string_view{reinterpret_cast(bytes.data() + offset + 1), nameSize} != Babylon::Graphics::RENDER_TARGET_TRANSFORM_UNIFORM_NAME) + { + throw std::runtime_error{"Expected only the render target transform uniform"}; + } + // name, type, count, register index, register count, texture component/dimension/format. + offset += 1 + nameSize + 1 + 1 + 2 + 2 + 1 + 1 + 2; + } + const size_t codeOffset = offset + sizeof(uint32_t); + if (bytes.size() < codeOffset) + { + throw std::runtime_error{"Invalid shader byte length"}; } uint32_t codeSize{}; - std::memcpy(&codeSize, bytes.data() + codeOffset - sizeof(codeSize), sizeof(codeSize)); + std::memcpy(&codeSize, bytes.data() + offset, sizeof(codeSize)); if (codeSize > bytes.size() - codeOffset) { throw std::runtime_error{"Invalid shader byte length"}; @@ -154,7 +181,7 @@ TEST(ShaderCompilation, InterfaceBlocksHaveMatchingD3D11Semantics) void main() { color = vec4(fragmentData.tint, 1.0); } )"); const auto readSignature = [](const std::vector& bytes, bool vertex) { - const auto code = ReadUniformFreeShader(bytes); + const auto code = ReadFixtureShaderCode(bytes); Microsoft::WRL::ComPtr reflection; if (FAILED(D3DReflect(code.data(), code.size(), IID_PPV_ARGS(&reflection)))) { @@ -212,6 +239,90 @@ TEST(ShaderCompilation, NativeCompilerAcceptsExistingVec4UniformArray) EXPECT_FALSE(shader.FragmentBytes.empty()); } +TEST(ShaderCompilation, UniformNamesDoNotCollideWithIntrinsicsOrBgfx) +{ + Babylon::Plugins::ShaderCompiler compiler{}; + const auto shader = compiler.Compile(R"(#version 310 es + in vec2 position; + uniform mat4 u_view; + void main() { gl_Position = u_view * vec4(position, 0.0, 1.0); } + )", R"(#version 310 es + precision highp float; + #define SIZE_NAME textureSize + uniform vec2 SIZE_NAME; + uniform mat4 u_view; + uniform float bnUserUniform_textureSize; + uniform highp sampler2D inputSampler; + out vec4 color; + void main() { + color = vec4(SIZE_NAME / vec2(textureSize(inputSampler, 0)), + u_view[0][0] + bnUserUniform_textureSize, 1.0); + } + )"); + ASSERT_EQ(shader.UniformNames.size(), 2u); + EXPECT_EQ(shader.UniformNames.at("bnUserUniform_textureSize1"), "textureSize"); + EXPECT_EQ(shader.UniformNames.at("bnUserUniform_u_view"), "u_view"); + EXPECT_FALSE(shader.FragmentBytes.empty()); +} + +TEST(ShaderCompilation, UniformNamesDoNotCollideWithRenderTargetTransform) +{ + Babylon::Plugins::ShaderCompiler compiler{}; + const auto shader = compiler.Compile(R"(#version 310 es + in vec2 position; + uniform vec4 bnRenderTargetTransform; + void main() { gl_Position = vec4(position, 0.0, 1.0) * bnRenderTargetTransform; } + )", R"(#version 310 es + precision highp float; + uniform vec4 bnRenderTargetTransform; + out vec4 color; + void main() { color = vec4(gl_FragCoord.y) * bnRenderTargetTransform; } + )"); + ASSERT_EQ(shader.UniformNames.size(), 1u); + EXPECT_EQ(shader.UniformNames.at("bnUserUniform_bnRenderTargetTransform"), "bnRenderTargetTransform"); + EXPECT_FALSE(shader.VertexBytes.empty()); + EXPECT_FALSE(shader.FragmentBytes.empty()); +} + +#if defined(BABYLON_NATIVE_GRAPHICS_API_D3D11) +TEST(ShaderCompilation, UnsupportedSamplerArrayReportsError) +{ + Babylon::Plugins::ShaderCompiler compiler{}; + EXPECT_THROW(compiler.Compile(R"( + precision highp float; + in vec3 position; + void main() { gl_Position = vec4(position, 1.0); } + )", R"( + precision highp float; + uniform sampler2D sources[2]; + out vec4 color; + void main() { color = texture(sources[0], vec2(0.5)) + texture(sources[1], vec2(0.5)); } + )"), std::runtime_error); +} +#endif + +TEST(ShaderCompilation, UnsignedShaderArgumentsAcceptIntegerConstants) +{ + Babylon::Plugins::ShaderCompiler compiler{}; + EXPECT_NO_THROW(compiler.Compile(R"( + in vec2 position; + void main() { gl_Position = vec4(position, 0.0, 1.0); } + )", R"( + precision highp float; + precision highp int; + #define NUM_SAMPLES 4 + out vec4 color; + float sampleValue(uint index, uint count) { return float(index) / float(count); } + void main() { + float sum = 0.0; + for (uint index = 0u; index < NUM_SAMPLES; ++index) { + sum += sampleValue(index, NUM_SAMPLES); + } + color = vec4(sum); + } + )")); +} + TEST(ShaderCompilation, SamplerCoordinatesAcceptConditionalExpressions) { Babylon::Plugins::ShaderCompiler compiler{}; @@ -522,7 +633,7 @@ namespace { spirv_cross::Compiler ReadVulkanShader(const std::vector& bytes) { - const auto code = ReadUniformFreeShader(bytes); + const auto code = ReadFixtureShaderCode(bytes); if (code.size() % sizeof(uint32_t) != 0) { throw std::runtime_error{"Invalid SPIR-V byte length"}; diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e3d09c6cb..af868f522b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ FetchContent_Declare(metal-cpp EXCLUDE_FROM_ALL) FetchContent_Declare(SPIRV-Cross GIT_REPOSITORY https://github.com/BabylonJS/SPIRV-Cross.git - GIT_TAG 7c8705749fc1e234119c20695f82179b30d90914 + GIT_TAG 2b078efc3ca2ca3f6ce0069339ac409c643b0136 EXCLUDE_FROM_ALL) # -------------------------------------------------- @@ -137,6 +137,11 @@ option(BABYLON_NATIVE_PLUGIN_NATIVEENCODING "Include Babylon Native Plugin Nativ option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE "Include Babylon Native Plugin NativeEngine." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES "Enable image loading in NativeEngine (LoadTexture, image bitmap, bimg)." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_COMPILESHADERS "Include Babylon Native Plugin NativeEngine - Compile Shaders." ON) +# Disable WebMin in glslang and SPIRV-Cross. WebMin strips those libraries down to the +# WebGL2 GLSL subset, which excludes compute shaders: referencing any compute built-in +# (e.g. gl_GlobalInvocationID) crashes the WebMin glslang. Compute support (GPUParticleSystem +# on the native path) requires the full builds, so this defaults ON here. +option(BABYLON_NATIVE_DISABLE_WEBMIN "Disable WebMin minimization in glslang and SPIRV-Cross (required for compute shaders)." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEINPUT "Include Babylon Native Plugin NativeInput." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT "Include Babylon Native Plugin NativeMeshopt." OFF) option(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS "Include Babylon Native Plugin NativeOptimizations." ON) diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/BgfxShaderInfo.h b/Core/Graphics/InternalInclude/Babylon/Graphics/BgfxShaderInfo.h index 29b0c47565..ecf782bef2 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/BgfxShaderInfo.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/BgfxShaderInfo.h @@ -62,17 +62,56 @@ namespace Babylon::Graphics } return false; } - /// Uniform declared in fragment shaders that read gl_FragCoord, holding the bound - /// framebuffer's width/height in .x/.y. Deliberately outside the u_ namespace Babylon.js - /// uses so it cannot collide with a shader uniform. - inline constexpr const char* FRAGCOORD_TARGET_SIZE_UNIFORM_NAME{"bnFragCoordTargetSize"}; + /// Per-draw render target orientation, injected on top-left-origin backends (D3D/Metal/Vulkan). + /// Like ANGLE's driver uniforms, it lets one compiled shader address either storage order: + /// .x/.y: window Y to GL Y, glY = x + y * windowY; also the dFdy sign (.y) + /// .z: gl_Position.y scale + /// Ordinary targets keep top-left rows, so NativeEngine writes (height, -1, 1, 0). Cube faces are + /// rendered upside down into GL row order, as on OpenGL, so they get (0, 1, -1, 0). + /// + /// The height must be the bound framebuffer's, not bgfx's u_viewRect, which + /// FrameBuffer::SetBgfxViewPortAndScissor narrows to the viewport whenever one is set, whereas + /// gl_FragCoord is relative to the whole render target. The name is deliberately outside the + /// u_ namespace Babylon.js uses for its own uniforms so it cannot collide with a shader uniform. + inline constexpr const char* RENDER_TARGET_TRANSFORM_UNIFORM_NAME{"bnRenderTargetTransform"}; + inline constexpr const char* SAMPLER_STATE_UNIFORM_PREFIX{"bnSamplerState_"}; + + /// WebGL2 transform feedback is emulated by compiling the capturing vertex shader into a + /// compute shader (one invocation per vertex). Its vertex inputs report attribute locations + /// starting at TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE, far above any bgfx attribute or + /// instance-data location, so vertex arrays can record them as raw storage-buffer inputs. + inline constexpr uint32_t TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE{0x10000}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_WORKGROUP_SIZE{64}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_INPUT_BINDING{0}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_OUTPUT_BINDING{1}; + /// Vertex inputs may come from up to TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS vertex buffers. The first + /// is bound at TRANSFORM_FEEDBACK_INPUT_BINDING, the rest from TRANSFORM_FEEDBACK_EXTRA_INPUT_BINDING. + inline constexpr uint32_t TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS{4}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_EXTRA_INPUT_BINDING{2}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_FIRST_SAMPLER_BINDING{TRANSFORM_FEEDBACK_EXTRA_INPUT_BINDING + TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS - 1}; + inline constexpr uint32_t TRANSFORM_FEEDBACK_MAX_SAMPLER_BINDING{16}; + /// x = vertex count, y = first vertex. + inline constexpr const char* TRANSFORM_FEEDBACK_PARAMS_UNIFORM{"bnTfParams"}; + /// Per vertex input: x = byte offset, y = byte stride, z = component count (0 = unbound), + /// w = buffer index * 16 + TransformFeedbackInputType * 2 + normalized. + inline constexpr const char* TRANSFORM_FEEDBACK_INPUT_LAYOUT_UNIFORM{"bnTfInputLayout"}; + enum class TransformFeedbackInputType : uint32_t + { + Float = 0, + Int8 = 1, + Uint8 = 2, + Int16 = 3, + Uint16 = 4, + }; struct BgfxShaderInfo { std::vector VertexBytes{}; std::vector FragmentBytes{}; + std::vector ComputeBytes{}; std::map VertexAttributeLocations{}; std::map BuiltInInstanceDataSlots{}; std::map UniformStages{}; + std::map UniformNames{}; }; } diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h b/Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h index 3ffda4f922..a6901a9ca2 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h @@ -39,17 +39,25 @@ namespace Babylon::Graphics // Floating-point colors preserve HDR values through bgfx's clear palette. // More than 16 distinct palette colors in a physical frame throws rather than overwriting pending clears. - void Clear(bgfx::Encoder& encoder, uint16_t flags, float r, float g, float b, float a, float depth, uint8_t stencil, uint8_t colorAttachmentMask = UINT8_MAX); + // ignoreScissor clears the whole framebuffer regardless of the current scissor. + void Clear(bgfx::Encoder& encoder, uint16_t flags, float r, float g, float b, float a, float depth, uint8_t stencil, uint8_t colorAttachmentMask = UINT8_MAX, bool ignoreScissor = false); void SetViewPort(float x, float y, float width, float height); void SetScissor(float x, float y, float width, float height); void Submit(bgfx::Encoder& encoder, bgfx::ProgramHandle programHandle, uint8_t flags); + void Compute(bgfx::Encoder& encoder, bgfx::ProgramHandle programHandle, uint32_t numX, uint32_t numY, uint32_t numZ); void SetStencil(bgfx::Encoder& encoder, uint32_t stencilState); void Blit(bgfx::Encoder& encoder, bgfx::TextureHandle dst, uint16_t dstX, uint16_t dstY, bgfx::TextureHandle src, uint16_t srcX = 0, uint16_t srcY = 0, uint16_t width = UINT16_MAX, uint16_t height = UINT16_MAX); bool HasDepth() const { return m_hasDepth; } bool HasStencil() const { return m_hasStencil; } + // Rendered upside down so rows are stored in GL order on a top-left-origin backend. + // Viewport and scissor rects are mirrored to match (see RENDER_TARGET_TRANSFORM_UNIFORM_NAME). + void SetGLRowOrder(bool glRowOrder) { m_glRowOrder = glRowOrder; } + bool GLRowOrder() const { return m_glRowOrder; } + private: + Rect ToTargetRect(const Rect& rect, float height) const; Rect GetBgfxScissor(float x, float y, float width, float height) const; void SetBgfxViewPortAndScissor(const Rect& viewPort, const Rect& scissor); @@ -80,6 +88,7 @@ namespace Babylon::Graphics Rect m_desiredScissor{}; bool m_disposed{}; + bool m_glRowOrder{}; int8_t m_depthStencilAttachmentIndex{-1}; }; } diff --git a/Core/Graphics/Source/FrameBuffer.cpp b/Core/Graphics/Source/FrameBuffer.cpp index a8a8e7d7fe..596d2d6fba 100644 --- a/Core/Graphics/Source/FrameBuffer.cpp +++ b/Core/Graphics/Source/FrameBuffer.cpp @@ -144,7 +144,7 @@ namespace Babylon::Graphics } } - void FrameBuffer::Clear(bgfx::Encoder& encoder, uint16_t flags, float r, float g, float b, float a, float depth, uint8_t stencil, uint8_t colorAttachmentMask) + void FrameBuffer::Clear(bgfx::Encoder& encoder, uint16_t flags, float r, float g, float b, float a, float depth, uint8_t stencil, uint8_t colorAttachmentMask, bool ignoreScissor) { const bool maskColorAttachments{colorAttachmentMask != UINT8_MAX && bgfx::isValid(m_handle) && SupportsClearAttachmentMasking()}; const std::array color{r, g, b, a}; @@ -182,20 +182,22 @@ namespace Babylon::Graphics // the view scissor is set. // // Note that the view rect and view scissor are reset to the desired dimensions before the encoder is submitted. - if (m_desiredScissor.X == 0.0f && m_desiredScissor.Y == 0.0f && m_desiredScissor.Width == 0.0f && m_desiredScissor.Height == 0.0f) + if (ignoreScissor || (m_desiredScissor.X == 0.0f && m_desiredScissor.Y == 0.0f && m_desiredScissor.Width == 0.0f && m_desiredScissor.Height == 0.0f)) { bgfx::setViewRect(m_viewId.value(), 0, 0, Width(), Height()); m_bgfxViewPort = {0, 0, 1, 1}; } else { + const Rect scissor{ToTargetRect(m_desiredScissor, static_cast(Height()))}; bgfx::setViewRect( m_viewId.value(), - static_cast(m_desiredScissor.X), - static_cast(m_desiredScissor.Y), - static_cast(m_desiredScissor.Width), - static_cast(m_desiredScissor.Height)); + static_cast(scissor.X), + static_cast(scissor.Y), + static_cast(scissor.Width), + static_cast(scissor.Height)); + // Kept in the desired (unmirrored) space it is compared against. m_bgfxViewPort = { m_desiredScissor.X / Width(), m_desiredScissor.Y / Height(), @@ -232,6 +234,17 @@ namespace Babylon::Graphics encoder.submit(m_viewId.value(), programHandle, 0, flags); } + void FrameBuffer::Compute(bgfx::Encoder& encoder, bgfx::ProgramHandle programHandle, uint32_t numX, uint32_t numY, uint32_t numZ) + { + // Each dispatch gets its own view, so views run it after earlier work on this frame buffer, and + // bgfx unbinds its storage buffers before the next one binds them the other way round (D3D11 + // drops a read binding of a buffer that is still bound for writing). Taking a view also moves + // the next draw on this frame buffer to a later view (see SetBgfxViewPortAndScissor). + const bgfx::ViewId viewId = m_deviceContext.AcquireNewViewId(); + bgfx::resetView(viewId); + encoder.dispatch(viewId, programHandle, numX, numY, numZ); + } + void FrameBuffer::Blit(bgfx::Encoder& encoder, bgfx::TextureHandle dst, uint16_t dstX, uint16_t dstY, bgfx::TextureHandle src, uint16_t srcX, uint16_t srcY, uint16_t width, uint16_t height) { // In order for Blit to work properly we need to force the creation of a new ViewID. @@ -313,19 +326,31 @@ namespace Babylon::Graphics bgfx::setViewFrameBuffer(m_viewId.value(), Handle()); m_bgfxViewPort = viewPort; + const Rect targetViewPort{ToTargetRect(viewPort, 1.0f)}; bgfx::setViewRect(m_viewId.value(), - static_cast(m_bgfxViewPort.X * Width()), - static_cast(m_bgfxViewPort.Y * Height()), - static_cast(m_bgfxViewPort.Width * Width()), - static_cast(m_bgfxViewPort.Height * Height())); + static_cast(targetViewPort.X * Width()), + static_cast(targetViewPort.Y * Height()), + static_cast(targetViewPort.Width * Width()), + static_cast(targetViewPort.Height * Height())); m_bgfxScissor = scissor; + const Rect targetScissor{ToTargetRect(scissor, static_cast(Height()))}; bgfx::setViewScissor( m_viewId.value(), - static_cast(m_bgfxScissor.X), - static_cast(m_bgfxScissor.Y), - static_cast(m_bgfxScissor.Width), - static_cast(m_bgfxScissor.Height)); + static_cast(targetScissor.X), + static_cast(targetScissor.Y), + static_cast(targetScissor.Width), + static_cast(targetScissor.Height)); + } + + Rect FrameBuffer::ToTargetRect(const Rect& rect, float height) const + { + // Rects are kept top-left; GL-row-order targets store GL's bottom row first. + if (!m_glRowOrder || (rect.Width == 0.0f && rect.Height == 0.0f)) + { + return rect; + } + return Rect{rect.X, height - rect.Y - rect.Height, rect.Width, rect.Height}; } bool Rect::Equals(const Rect& other) const diff --git a/Polyfills/Canvas/shaderc.cmake b/Dependencies/shaderc.cmake similarity index 87% rename from Polyfills/Canvas/shaderc.cmake rename to Dependencies/shaderc.cmake index 852e3f56ed..96897e19db 100644 --- a/Polyfills/Canvas/shaderc.cmake +++ b/Dependencies/shaderc.cmake @@ -1,3 +1,12 @@ +include_guard(GLOBAL) + +if(NOT IOS AND NOT VISIONOS AND NOT ANDROID) + set(SHADERC_PATH "" CACHE FILEPATH "Optional full path to shaderc built from bgfx.") + if(SHADERC_PATH AND NOT EXISTS "${SHADERC_PATH}") + message(FATAL_ERROR "'${SHADERC_PATH}' does not exist.") + endif() +endif() + # Copy of bgfx _bgfx_shaderc_parse function adapted for Babylon Native # Function is not guarded behind BGFX_TOOLS_SHADERC and shaderx path is set by SHADERC_PATH variable. # Usage: @@ -233,12 +242,14 @@ function(add_bgfx_shader FILE FOLDER) set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}Metal, ") # essl - if(NOT "${TYPE}" STREQUAL "COMPUTE") - set(ESSL_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/essl/${FILENAME}.h) + set(ESSL_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/essl/${FILENAME}.h) + if("${TYPE}" STREQUAL "COMPUTE") + _bn_shaderc_parse(ESSL ${COMMON} ANDROID PROFILE 310_es OUTPUT ${ESSL_OUTPUT} BIN2C "${FILENAME}_essl") + else() _bn_shaderc_parse(ESSL ${COMMON} ANDROID PROFILE 300_es OUTPUT ${ESSL_OUTPUT} BIN2C "${FILENAME}_essl") - list(APPEND OUTPUTS "ESSL") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}ESSL, ") endif() + list(APPEND OUTPUTS "ESSL") + set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}ESSL, ") # glsl set(GLSL_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/glsl/${FILENAME}.h) @@ -251,14 +262,12 @@ function(add_bgfx_shader FILE FOLDER) set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}GLSL, ") # spirv - if(NOT "${TYPE}" STREQUAL "COMPUTE") - set(SPIRV_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/spirv/${FILENAME}.h) - _bn_shaderc_parse(SPIRV ${COMMON} LINUX PROFILE spirv OUTPUT ${SPIRV_OUTPUT} BIN2C "${FILENAME}_spv") - list(APPEND OUTPUTS "SPIRV") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}SPIRV") - set(OUTPUT_FILES "") - set(COMMANDS "") - endif() + set(SPIRV_OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/spirv/${FILENAME}.h) + _bn_shaderc_parse(SPIRV ${COMMON} LINUX PROFILE spirv OUTPUT ${SPIRV_OUTPUT} BIN2C "${FILENAME}_spv") + list(APPEND OUTPUTS "SPIRV") + set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}SPIRV") + set(OUTPUT_FILES "") + set(COMMANDS "") foreach(OUT ${OUTPUTS}) list(APPEND OUTPUT_FILES ${${OUT}_OUTPUT}) @@ -270,7 +279,9 @@ function(add_bgfx_shader FILE FOLDER) file(RELATIVE_PATH PRINT_NAME ${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders ${FILE}) add_custom_command( MAIN_DEPENDENCY ${FILE} OUTPUT ${OUTPUT_FILES} ${COMMANDS} + DEPENDS "${BGFX_DIR}/src/bgfx_shader.sh" "${BGFX_DIR}/src/bgfx_compute.sh" COMMENT "Compiling shader ${PRINT_NAME} for ${OUTPUTS_PRETTY}" + VERBATIM ) endif() endfunction() diff --git a/Plugins/NativeEngine/CMakeLists.txt b/Plugins/NativeEngine/CMakeLists.txt index 3d46041954..2776e57a9c 100644 --- a/Plugins/NativeEngine/CMakeLists.txt +++ b/Plugins/NativeEngine/CMakeLists.txt @@ -1,7 +1,11 @@ +include(${CMAKE_CURRENT_LIST_DIR}/../../Dependencies/shaderc.cmake) + set(SOURCES "Include/Babylon/Plugins/NativeEngine.h" "Source/IndexBuffer.cpp" "Source/IndexBuffer.h" + "Source/InstanceRepacker.cpp" + "Source/InstanceRepacker.h" "Source/JsConsoleLogger.h" "Source/JsConsoleLogger.cpp" "Source/NativeDataStream.h" @@ -9,16 +13,29 @@ set(SOURCES "Source/NativeEngine.cpp" "Source/NativeEngine.h" "Source/PerFrameValue.h" + "Source/PrimitiveModeExpansion.cpp" + "Source/PrimitiveModeExpansion.h" "Source/Program.cpp" "Source/Program.h" "Source/ShaderProvider.h" "Source/ShaderProvider.cpp" + "Source/StorageBuffer.cpp" + "Source/StorageBuffer.h" "Source/VertexArray.cpp" "Source/VertexArray.h" "Source/VertexBuffer.cpp" "Source/VertexBuffer.h") -add_library(NativeEngine ${SOURCES}) +set(SHADERS "Source/Shaders/cs_instance_repack.sc") +foreach(BACKEND dxbc dxil essl glsl metal spirv) + list(APPEND SHADERS "Source/Shaders/${BACKEND}/cs_instance_repack.h") +endforeach() + +add_library(NativeEngine ${SOURCES} ${SHADERS}) + +if(SHADERC_PATH AND NOT IOS AND NOT VISIONOS AND NOT ANDROID) + add_bgfx_shader("${CMAKE_CURRENT_SOURCE_DIR}/Source/Shaders/cs_instance_repack.sc" "") +endif() warnings_as_errors(NativeEngine) diff --git a/Plugins/NativeEngine/Source/IndexBuffer.cpp b/Plugins/NativeEngine/Source/IndexBuffer.cpp index caf0d04234..3f83cbbd26 100644 --- a/Plugins/NativeEngine/Source/IndexBuffer.cpp +++ b/Plugins/NativeEngine/Source/IndexBuffer.cpp @@ -1,12 +1,15 @@ #include "IndexBuffer.h" #include "Babylon/Graphics/DeviceContext.h" +#include +#include + namespace Babylon { IndexBuffer::IndexBuffer(Graphics::DeviceContext& deviceContext, gsl::span bytes, uint16_t flags, bool dynamic) : m_deviceContext{deviceContext} , m_deviceID{deviceContext.GetDeviceId()} - , m_bytes{bytes.data(), bytes.data() + bytes.size()} + , m_data{bytes, (flags & BGFX_BUFFER_INDEX32) != 0} , m_flags{flags} , m_dynamic{dynamic} { @@ -36,7 +39,8 @@ namespace Babylon } } - m_bytes.clear(); + DestroyExpandedBuffers(); + m_data.Clear(); m_disposed = true; } @@ -48,34 +52,47 @@ namespace Babylon throw std::runtime_error{"Cannot update non-dynamic index buffer"}; } - if (bgfx::isValid(m_dynamicHandle)) + if (m_disposed) { - bgfx::update(m_dynamicHandle, startIndex, bgfx::copy(bytes.data(), static_cast(bytes.size()))); + throw std::runtime_error{"Cannot update disposed index buffer"}; } - else + + if (bytes.empty()) + { + return; + } + + const bool hasGpuBuffer = bgfx::isValid(m_dynamicHandle); + if (hasGpuBuffer && bytes.size() > std::numeric_limits::max()) { - const size_t byteStride = (m_flags & BGFX_BUFFER_INDEX32) ? 4 : 2; - const size_t byteOffset = startIndex * byteStride; + throw std::length_error{"Index buffer update is too large"}; + } - if (byteOffset + bytes.size() > m_bytes.size()) - { - throw std::runtime_error{"Failed to update index buffer: buffer overflow"}; - } + m_data.Update(bytes, startIndex); + + DestroyExpandedBuffers(); - std::memcpy(m_bytes.data() + byteOffset, bytes.data(), bytes.size()); + if (hasGpuBuffer) + { + bgfx::update(m_dynamicHandle, startIndex, bgfx::copy(bytes.data(), static_cast(bytes.size()))); } } void IndexBuffer::Build() { - if (!bgfx::isValid(m_handle)) + if (m_disposed) { - auto releaseFn = [](void*, void* userData) { - delete reinterpret_cast(userData); - }; + throw std::runtime_error{"Cannot build disposed index buffer"}; + } - auto* bytesPtr = new decltype(m_bytes){std::move(m_bytes)}; - const bgfx::Memory* memory = bgfx::makeRef(bytesPtr->data(), static_cast(bytesPtr->size()), releaseFn, bytesPtr); + if (!bgfx::isValid(m_handle)) + { + const auto bytes = m_data.Bytes(); + if (bytes.size() > std::numeric_limits::max()) + { + throw std::length_error{"Index buffer data is too large"}; + } + const bgfx::Memory* memory = bgfx::copy(bytes.data(), static_cast(bytes.size())); if (m_dynamic) { @@ -95,6 +112,11 @@ namespace Babylon void IndexBuffer::Set(bgfx::Encoder* encoder, uint32_t firstIndex, uint32_t numIndices) { + if (m_disposed) + { + throw std::runtime_error{"Cannot bind disposed index buffer"}; + } + if (m_dynamic) { encoder->setIndexBuffer(m_dynamicHandle, firstIndex, numIndices); @@ -104,4 +126,65 @@ namespace Babylon encoder->setIndexBuffer(m_handle, firstIndex, numIndices); } } + + bool IndexBuffer::SetExpanded(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t firstIndex, uint32_t numIndices) + { + if (m_disposed) + { + throw std::runtime_error{"Cannot bind disposed index buffer"}; + } + + const ExpansionKey key{mode, firstIndex, numIndices}; + auto existing = m_expandedBuffers.find(key); + if (existing == m_expandedBuffers.end()) + { + auto expanded = PrimitiveModeExpansion::ExpandIndexed(m_data, firstIndex, numIndices, mode); + if (expanded.IndexCount == 0) + { + return false; + } + + const uint16_t flags = expanded.Index32 ? BGFX_BUFFER_INDEX32 : 0; + const bgfx::Memory* memory = bgfx::copy(expanded.Bytes.data(), static_cast(expanded.Bytes.size())); + const bgfx::IndexBufferHandle handle = bgfx::createIndexBuffer(memory, flags); + if (!bgfx::isValid(handle)) + { + throw std::runtime_error{"Failed to create expanded primitive index buffer"}; + } + + try + { + existing = m_expandedBuffers.emplace(key, ExpandedBuffer{handle, expanded.IndexCount}).first; + } + catch (...) + { + bgfx::destroy(handle); + throw; + } + } + + encoder->setIndexBuffer(existing->second.Handle, 0, existing->second.IndexCount); + return true; + } + + bool IndexBuffer::ExpansionKey::operator<(const ExpansionKey& other) const + { + return std::tie(Mode, FirstIndex, IndexCount) < std::tie(other.Mode, other.FirstIndex, other.IndexCount); + } + + void IndexBuffer::DestroyExpandedBuffers() + { + if (m_deviceID == m_deviceContext.GetDeviceId()) + { + for (const auto& [key, buffer] : m_expandedBuffers) + { + static_cast(key); + if (bgfx::isValid(buffer.Handle)) + { + bgfx::destroy(buffer.Handle); + } + } + } + m_expandedBuffers.clear(); + } } diff --git a/Plugins/NativeEngine/Source/IndexBuffer.h b/Plugins/NativeEngine/Source/IndexBuffer.h index 7b9265e725..e2c46dd9a7 100644 --- a/Plugins/NativeEngine/Source/IndexBuffer.h +++ b/Plugins/NativeEngine/Source/IndexBuffer.h @@ -1,10 +1,12 @@ #pragma once +#include "PrimitiveModeExpansion.h" + #include #include #include -#include +#include namespace Babylon { @@ -30,14 +32,33 @@ namespace Babylon void Build(); void Set(bgfx::Encoder* encoder, uint32_t firstIndex, uint32_t numIndices); + bool SetExpanded(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t firstIndex, uint32_t numIndices); private: + struct ExpansionKey final + { + PrimitiveModeExpansion::Mode Mode{}; + uint32_t FirstIndex{}; + uint32_t IndexCount{}; + + bool operator<(const ExpansionKey& other) const; + }; + + struct ExpandedBuffer final + { + bgfx::IndexBufferHandle Handle{bgfx::kInvalidHandle}; + uint32_t IndexCount{}; + }; + + void DestroyExpandedBuffers(); + Graphics::DeviceContext& m_deviceContext; const uintptr_t m_deviceID{}; - std::vector m_bytes{}; + PrimitiveModeExpansion::IndexData m_data; const uint16_t m_flags{}; const bool m_dynamic{}; + std::map m_expandedBuffers{}; union { diff --git a/Plugins/NativeEngine/Source/InstanceRepacker.cpp b/Plugins/NativeEngine/Source/InstanceRepacker.cpp new file mode 100644 index 0000000000..6028e73163 --- /dev/null +++ b/Plugins/NativeEngine/Source/InstanceRepacker.cpp @@ -0,0 +1,183 @@ +#include "InstanceRepacker.h" + +#include "StorageBuffer.h" + +#include +#include + +#include +#include + +#include +#include + +#include "Shaders/dxbc/cs_instance_repack.h" +#include "Shaders/dxil/cs_instance_repack.h" +#include "Shaders/essl/cs_instance_repack.h" +#include "Shaders/glsl/cs_instance_repack.h" +#include "Shaders/metal/cs_instance_repack.h" +#include "Shaders/spirv/cs_instance_repack.h" + +namespace Babylon +{ + namespace + { + const bgfx::EmbeddedShader RepackShaders[]{ + BGFX_EMBEDDED_SHADER(cs_instance_repack), + BGFX_EMBEDDED_SHADER_END()}; + + constexpr uint32_t kSlotSize = 16; + constexpr uint32_t kParamsHeaderU = 4; + } + + InstanceRepacker::InstanceRepacker(Graphics::DeviceContext& deviceContext) + : m_deviceContext{deviceContext} + , m_deviceId{deviceContext.GetDeviceId()} + { + } + + InstanceRepacker::~InstanceRepacker() + { + for (auto& pair : m_perVertexArray) + { + DestroyDest(pair.second); + } + m_perVertexArray.clear(); + if (bgfx::isValid(m_program) && m_deviceId == m_deviceContext.GetDeviceId()) + { + bgfx::destroy(m_program); + } + } + + void InstanceRepacker::EnsureProgram() + { + if (bgfx::isValid(m_program)) + { + return; + } + + const auto shader = bgfx::createEmbeddedShader(RepackShaders, bgfx::getRendererType(), "cs_instance_repack"); + if (!bgfx::isValid(shader)) + { + throw std::runtime_error{"Failed to create embedded instance repack shader"}; + } + m_program = bgfx::createProgram(shader, true); + if (!bgfx::isValid(m_program)) + { + throw std::runtime_error{"Failed to create embedded instance repack program"}; + } + } + + void InstanceRepacker::DestroyDest(PerVertexArray& state) + { + if (state.DestStorage != nullptr) + { + state.DestStorage->Dispose(); + state.DestStorage.reset(); + } + state.Capacity = 0; + state.Stride = 0; + } + + void InstanceRepacker::Forget(VertexArray* vertexArray) + { + auto it = m_perVertexArray.find(vertexArray); + if (it != m_perVertexArray.end()) + { + DestroyDest(it->second); + m_perVertexArray.erase(it); + } + } + + bgfx::DynamicVertexBufferHandle InstanceRepacker::Repack(Graphics::FrameBuffer& frameBuffer, VertexArray* vertexArray, + const std::map& instances, const VertexBuffer::InstanceDataLayout& layout, + uint32_t instanceCount) + { + if (instances.empty() || instanceCount == 0) + { + return BGFX_INVALID_HANDLE; + } + + const uint32_t attributeCount = static_cast(instances.size()); + if (attributeCount > kMaxAttributes || layout.SlotCount > kMaxAttributes) + { + return BGFX_INVALID_HANDLE; + } + if (layout.Slots.size() != instances.size()) + { + throw std::runtime_error{"Instance-data layout does not cover every recorded attribute."}; + } + + StorageBuffer* source = instances.begin()->second.ResolveStorage(); + if (source == nullptr) + { + return BGFX_INVALID_HANDLE; + } + for (const auto& [location, instance] : instances) + { + if (instance.ResolveStorage() != source) + { + throw std::runtime_error{"GPU instance attributes must share a single storage buffer"}; + } + } + + EnsureProgram(); + const uint16_t instanceStride = static_cast(layout.SlotCount * kSlotSize); + if (instanceCount > UINT32_MAX / instanceStride) + { + throw std::runtime_error{"GPU instance count is too large"}; + } + const uint32_t destBytes = instanceCount * instanceStride; + + PerVertexArray& state = m_perVertexArray[vertexArray]; + + if (state.DestStorage == nullptr || state.Capacity < instanceCount || state.Stride != instanceStride + || state.DestStorage->ByteLength() < destBytes) + { + DestroyDest(state); + state.DestStorage = std::make_shared( + m_deviceContext, destBytes, /*byteStride*/ instanceStride, /*computeWrite*/ true); + state.Capacity = instanceCount; + state.Stride = instanceStride; + } + + // Build params blob: header + one uvec4 per attribute, written into the shader's layout slot. + const uint32_t paramsU = kParamsHeaderU + attributeCount * 4; + std::vector params(paramsU, 0); + params[0] = instanceCount; + params[1] = attributeCount; + params[2] = instanceStride / sizeof(float); + params[3] = 0; + + uint32_t attr = 0; + for (const auto& [location, element] : instances) + { + const uint32_t slot = layout.Slots.at(location); + const uint32_t base = kParamsHeaderU + attr++ * 4; + params[base + 0] = element.Offset / sizeof(float); + params[base + 1] = element.Stride / sizeof(float); + params[base + 2] = element.ElementSize / sizeof(float); + params[base + 3] = (slot * kSlotSize) / sizeof(float); + } + + bgfx::Encoder* computeEncoder = m_deviceContext.GetActiveEncoder(); + if (computeEncoder == nullptr) + { + return BGFX_INVALID_HANDLE; + } + + // bgfx applies all CPU updates of a frame before rendering it, so each dispatch needs its + // own params. Destruction is deferred until the frame that uses them has rendered. + const uint32_t paramsBytes = paramsU * sizeof(uint32_t); + StorageBuffer paramsBuffer{m_deviceContext, paramsBytes, /*byteStride*/ 16, /*computeWrite*/ false}; + paramsBuffer.Update(gsl::make_span(reinterpret_cast(params.data()), paramsBytes), 0); + paramsBuffer.SetCompute(computeEncoder, 0, bgfx::Access::Read); + source->SetCompute(computeEncoder, 1, bgfx::Access::Read); + state.DestStorage->SetCompute(computeEncoder, 2, bgfx::Access::ReadWrite); + + const uint32_t numGroups = (instanceCount + 63u) / 64u; + frameBuffer.Compute(*computeEncoder, m_program, numGroups, 1, 1); + + return state.DestStorage->Handle(); + } +} \ No newline at end of file diff --git a/Plugins/NativeEngine/Source/InstanceRepacker.h b/Plugins/NativeEngine/Source/InstanceRepacker.h new file mode 100644 index 0000000000..e6b802b277 --- /dev/null +++ b/Plugins/NativeEngine/Source/InstanceRepacker.h @@ -0,0 +1,66 @@ +#pragma once + +#include "VertexBuffer.h" + +#include + +#include +#include + +namespace Babylon +{ + namespace Graphics + { + class DeviceContext; + class FrameBuffer; + } + + class StorageBuffer; + class VertexArray; + + // Repacks GPU-written per-instance data (transform feedback output, packed float-by-float in a + // raw storage buffer) into bgfx's 16-byte-per-attribute i_data slot layout, entirely on the GPU. Attributes + // land in the slots of the given VertexBuffer::InstanceDataLayout, like BuildInstanceDataBuffer, so the + // existing instanced shader-variant path renders it unchanged. + class InstanceRepacker final + { + public: + explicit InstanceRepacker(Graphics::DeviceContext& deviceContext); + ~InstanceRepacker(); + + InstanceRepacker(const InstanceRepacker&) = delete; + InstanceRepacker& operator=(const InstanceRepacker&) = delete; + + // Dispatches the repack compute for the given vertex array's storage-backed instances on the + // frame buffer's (sequential) view so it is ordered before the subsequent draw, and returns + // the dynamic vertex buffer handle to bind via setInstanceDataBuffer. Returns an invalid + // handle if there is nothing to repack. + bgfx::DynamicVertexBufferHandle Repack(Graphics::FrameBuffer& frameBuffer, VertexArray* vertexArray, + const std::map& instances, const VertexBuffer::InstanceDataLayout& layout, + uint32_t instanceCount); + + // Releases the GPU resources associated with a vertex array (call on vertex-array deletion). + void Forget(VertexArray* vertexArray); + + private: + // Upper bound on bgfx's maxInstanceData slots. + static constexpr uint32_t kMaxAttributes = 32; + + struct PerVertexArray + { + // Destination is a StorageBuffer so the repack kernel can bind it as a RAW UAV + // (same path as the params and source). Handle() is then passed to setInstanceDataBuffer. + std::shared_ptr DestStorage; + uint32_t Capacity{}; // in instances + uint16_t Stride{}; // bytes per instance (== layout.SlotCount * 16) + }; + + void EnsureProgram(); + void DestroyDest(PerVertexArray& state); + + Graphics::DeviceContext& m_deviceContext; + const uintptr_t m_deviceId; + bgfx::ProgramHandle m_program{bgfx::kInvalidHandle}; + std::map m_perVertexArray; + }; +} diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index ba92d2f153..a29ef49878 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -6,6 +6,8 @@ #include #include "JsConsoleLogger.h" +#include "InstanceRepacker.h" +#include "StorageBuffer.h" #include #include @@ -55,6 +57,13 @@ namespace Babylon { namespace { + float PointSampleHeight(const Graphics::Texture& texture) + { + const auto filters = texture.SamplerFlags() & (BGFX_SAMPLER_MIN_MASK | BGFX_SAMPLER_MAG_MASK); + return filters == (BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT) && + (!texture.HasMips() || texture.SamplerMaxLod() == 0) ? static_cast(texture.Height()) : 0.0f; + } + uint32_t ReadUnsignedInteger(const Napi::Value& value, const char* name, uint32_t maximum, uint32_t minimum = 0, bool truncate = false) { double number = value.As().DoubleValue(); @@ -127,6 +136,27 @@ namespace Babylon constexpr uint32_t DARKEN = 5; } + constexpr bool IsFloatTextureFormat(bgfx::TextureFormat::Enum format) + { + switch (format) + { + case bgfx::TextureFormat::R16F: + case bgfx::TextureFormat::RG16F: + case bgfx::TextureFormat::RGBA16F: + case bgfx::TextureFormat::R32F: + case bgfx::TextureFormat::RG32F: + case bgfx::TextureFormat::RGBA32F: + case bgfx::TextureFormat::RGB9E5F: + case bgfx::TextureFormat::RG11B10F: + case bgfx::TextureFormat::D16F: + case bgfx::TextureFormat::D24F: + case bgfx::TextureFormat::D32F: + return true; + default: + return false; + } + } + void FlipImage(gsl::span image, uint32_t height) { const size_t rowPitch{image.size() / height}; @@ -144,6 +174,53 @@ namespace Babylon } } + // WebGL reads exactly the texels an upload needs and ignores trailing bytes. It also accepts + // 3-component RGB sources for formats that Native, like ANGLE on D3D, stores as RGBA. Returns the + // bytes to upload, widening tightly packed RGB data into `storage` with an opaque alpha, or an + // empty span when the data is too small for the upload. + gsl::span ResolveRawUpload(gsl::span data, uint64_t requiredSize, uint64_t texelCount, + bgfx::TextureFormat::Enum format, std::vector& storage) + { + if (requiredSize != 0 && data.size() >= requiredSize) + { + return data.first(static_cast(requiredSize)); + } + + uint32_t componentBytes{0}; + uint32_t alpha{0}; + switch (format) + { + case bgfx::TextureFormat::RGBA8: componentBytes = 1; alpha = 0xFF; break; + case bgfx::TextureFormat::RGBA8S: componentBytes = 1; alpha = 0x7F; break; + case bgfx::TextureFormat::RGBA8I: + case bgfx::TextureFormat::RGBA8U: componentBytes = 1; alpha = 1; break; + case bgfx::TextureFormat::RGBA16: componentBytes = 2; alpha = 0xFFFF; break; + case bgfx::TextureFormat::RGBA16S: componentBytes = 2; alpha = 0x7FFF; break; + case bgfx::TextureFormat::RGBA16F: componentBytes = 2; alpha = 0x3C00; break; + case bgfx::TextureFormat::RGBA16I: + case bgfx::TextureFormat::RGBA16U: componentBytes = 2; alpha = 1; break; + case bgfx::TextureFormat::RGBA32F: componentBytes = 4; alpha = 0x3F800000; break; + case bgfx::TextureFormat::RGBA32I: + case bgfx::TextureFormat::RGBA32U: componentBytes = 4; alpha = 1; break; + default: return {}; + } + + if (texelCount == 0 || data.size() != texelCount * 3 * componentBytes || texelCount * 4 * componentBytes != requiredSize) + { + return {}; + } + + storage.resize(static_cast(requiredSize)); + const size_t rgbBytes{3u * componentBytes}; + for (size_t texel = 0; texel < texelCount; ++texel) + { + uint8_t* destination{storage.data() + texel * 4 * componentBytes}; + std::memcpy(destination, data.data() + texel * rgbBytes, rgbBytes); + std::memcpy(destination + rgbBytes, &alpha, componentBytes); + } + return storage; + } + #ifdef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES static_assert(static_cast(bimg::TextureFormat::Count) == bgfx::TextureFormat::Count); static_assert(static_cast(bimg::TextureFormat::RGBA8) == bgfx::TextureFormat::RGBA8); @@ -1017,12 +1094,21 @@ namespace Babylon StaticValue("COMMAND_DRAWINDEXEDINSTANCED", Napi::FunctionPointer::Create(env, &NativeEngine::DrawIndexedInstanced)), StaticValue("COMMAND_DRAW", Napi::FunctionPointer::Create(env, &NativeEngine::Draw)), StaticValue("COMMAND_DRAWINSTANCED", Napi::FunctionPointer::Create(env, &NativeEngine::DrawInstanced)), + // Transform feedback is emulated with compute; without it Babylon.js falls back to CPU particles. + StaticValue("COMMAND_DRAWTRANSFORMFEEDBACK", 0 != (bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) + ? Napi::FunctionPointer::Create(env, &NativeEngine::DrawTransformFeedback) + : env.Undefined()), StaticValue("COMMAND_CLEAR", Napi::FunctionPointer::Create(env, &NativeEngine::Clear)), + StaticValue("COMMAND_CLEAR2", Napi::FunctionPointer::Create(env, &NativeEngine::Clear2)), StaticValue("COMMAND_SETSTENCIL", Napi::FunctionPointer::Create(env, &NativeEngine::SetStencil)), + StaticValue("COMMAND_SETSTENCIL2", Napi::FunctionPointer::Create(env, &NativeEngine::SetStencil2)), StaticValue("COMMAND_SETVIEWPORT", Napi::FunctionPointer::Create(env, &NativeEngine::SetViewPort)), StaticValue("COMMAND_SETSCISSOR", Napi::FunctionPointer::Create(env, &NativeEngine::SetScissor)), StaticValue("COMMAND_COPYTEXTURE", Napi::FunctionPointer::Create(env, &NativeEngine::CopyTexture)), + InstanceValue("supportsPrimitiveModeExpansion", Napi::Boolean::From(env, true)), + InstanceValue("supportsDynamicTextureMipMaps", Napi::Boolean::From(env, + 0 != (bgfx::getCaps()->formats[bgfx::TextureFormat::RGBA8] & BGFX_CAPS_FORMAT_TEXTURE_MIP_AUTOGEN))), InstanceMethod("dispose", &NativeEngine::Dispose), #ifdef BABYLON_NATIVE_NATIVEENGINE_TEST_HOOKS InstanceMethod("_disposeDrainTestSchedule", &NativeEngine::DisposeDrainTestSchedule), @@ -1039,6 +1125,7 @@ namespace Babylon InstanceMethod("createVertexBuffer", &NativeEngine::CreateVertexBuffer), InstanceMethod("recordVertexBuffer", &NativeEngine::RecordVertexBuffer), InstanceMethod("updateDynamicVertexBuffer", &NativeEngine::UpdateDynamicVertexBuffer), + InstanceMethod("readTransformFeedbackBuffer", &NativeEngine::ReadTransformFeedbackBuffer), InstanceMethod("createProgram", &NativeEngine::CreateProgram), InstanceMethod("createProgramAsync", &NativeEngine::CreateProgramAsync), @@ -1060,6 +1147,7 @@ namespace Babylon InstanceMethod("setTextureComparisonFunction", &NativeEngine::SetTextureComparisonFunction), InstanceMethod("deleteTexture", &NativeEngine::DeleteTexture), InstanceMethod("readTexture", &NativeEngine::ReadTexture), + InstanceMethod("readTexture2", &NativeEngine::ReadTexture2), InstanceMethod("createImageBitmap", &NativeEngine::CreateImageBitmap), InstanceMethod("resizeImageBitmap", &NativeEngine::ResizeImageBitmap), @@ -1102,7 +1190,8 @@ namespace Babylon , m_runtimeScheduler{runtime} , m_defaultFrameBuffer{m_deviceContext, BGFX_INVALID_HANDLE, 0, 0, true, true, true} , m_boundFrameBuffer{&m_defaultFrameBuffer} - , m_boundFrameBufferNeedsRebinding{m_deviceContext, *m_cancellationSource, true} + , m_boundFrameBufferNeedsRebinding{m_deviceContext, m_cancellationSource, true} + , m_defaultFrameBufferNeedsReset{m_deviceContext, m_cancellationSource, true} { // Set features supported by the NativeEngine from Babylon.js. if (!info[0].IsUndefined()) @@ -1176,7 +1265,12 @@ namespace Babylon void NativeEngine::DeleteVertexArray(NativeDataStream::Reader& data) { - data.ReadPointer()->Dispose(); + VertexArray* vertexArray = data.ReadPointer(); + if (m_instanceRepacker != nullptr) + { + m_instanceRepacker->Forget(vertexArray); + } + vertexArray->Dispose(); // TODO: should we clear the m_boundVertexArray if it gets deleted? //assert(vertexArray != m_boundVertexArray); } @@ -1340,16 +1434,171 @@ namespace Babylon } } + // WebGL's getBufferSubData after transform feedback is synchronous; ANGLE stalls on the GPU. Do the same: + // copy the captured storage buffer into an R32F image, read it back and run mid-frame flushes until the + // read completes. + void NativeEngine::ReadTransformFeedbackBuffer(const Napi::CallbackInfo& info) + { + VertexBuffer* vertexBuffer = info[0].As>().Get(); + const Napi::ArrayBuffer target = info[1].As(); + const uint32_t targetByteOffset = info[2].As().Uint32Value(); + const uint32_t targetByteLength = info[3].As().Uint32Value(); + if (static_cast(targetByteOffset) + targetByteLength > target.ByteLength()) + { + throw Napi::RangeError::New(info.Env(), "readTransformFeedbackBuffer target range is out of bounds"); + } + if ((bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) == 0) + { + throw Napi::Error::New(info.Env(), "readTransformFeedbackBuffer requires compute support"); + } + + if (m_commandStream) + { + SubmitCommands(info); + } + + StorageBuffer& storage = vertexBuffer->PromoteToGpuStorage(); + if (targetByteLength > storage.ByteLength()) + { + throw Napi::RangeError::New(info.Env(), "readTransformFeedbackBuffer source range is out of bounds"); + } + const uint32_t count = targetByteLength / static_cast(sizeof(float)); + if (count == 0) + { + return; + } + + constexpr uint32_t kWidth = 256; + const uint32_t height = (count + kWidth - 1) / kWidth; + if (height > bgfx::getCaps()->limits.maxTextureSize) + { + throw Napi::RangeError::New(info.Env(), "readTransformFeedbackBuffer buffer is too large"); + } + + if (m_bufferReadbackProgram == nullptr) + { + static constexpr const char* kSource = R"(#version 310 es +precision highp float; +precision highp int; +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; +layout(std430, binding = 0) readonly buffer Source { float sourceData[]; }; +layout(r32f, binding = 1) writeonly uniform highp image2D destination; +uniform vec4 readbackCount; +void main() { + uint i = gl_GlobalInvocationID.x; + if (i >= uint(readbackCount.x)) { return; } + imageStore(destination, ivec2(int(i % 256u), int(i / 256u)), vec4(sourceData[i], 0.0, 0.0, 0.0)); +} +)"; + auto program = std::make_shared(m_deviceContext); + program->InitializeCompute(m_shaderProvider.GetCompute(kSource)); + m_bufferReadbackProgram = std::move(program); + } + + // Keeps the render thread parked in FinishRenderingCurrentFrame so ForceMidFrameFlush can run. + auto frameScope = m_deviceContext.AcquireFrameCompletionScope(); + + const bgfx::TextureHandle texture = bgfx::createTexture2D(static_cast(kWidth), static_cast(height), + /*hasMips*/ false, /*numLayers*/ 1, bgfx::TextureFormat::R32F, BGFX_TEXTURE_COMPUTE_WRITE); + const bgfx::TextureHandle readbackTexture = bgfx::createTexture2D(static_cast(kWidth), static_cast(height), + /*hasMips*/ false, /*numLayers*/ 1, bgfx::TextureFormat::R32F, BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK); + const auto destroyTextures = gsl::finally([texture, readbackTexture] { + if (bgfx::isValid(texture)) + { + bgfx::destroy(texture); + } + if (bgfx::isValid(readbackTexture)) + { + bgfx::destroy(readbackTexture); + } + }); + if (!bgfx::isValid(texture) || !bgfx::isValid(readbackTexture)) + { + throw Napi::Error::New(info.Env(), "readTransformFeedbackBuffer could not allocate a readback texture"); + } + + bgfx::Encoder* encoder = GetEncoder(); + // An explicit count instead of .length(): Metal would need unbound buffer-size constants for it. + const float readbackCount[4]{static_cast(count), 0.0f, 0.0f, 0.0f}; + encoder->setUniform(m_bufferReadbackProgram->GetUniformInfo("readbackCount")->Handle, readbackCount); + encoder->setImage(1, texture, 0, bgfx::Access::Write, bgfx::TextureFormat::R32F); + storage.SetCompute(encoder, 0, bgfx::Access::Read); + GetBoundFrameBuffer().Compute(*encoder, m_bufferReadbackProgram->Handle(), (count + 63) / 64, 1, 1); + bgfx::TextureRegion destinationRegion{}; + destinationRegion.init(readbackTexture); + bgfx::TextureRegion sourceRegion{}; + sourceRegion.init(texture); + encoder->blit(m_deviceContext.AcquireNewViewId(), destinationRegion, sourceRegion); + RestoreBoundTextures(encoder); + + // The read may complete after this call if it times out, so the continuation shares ownership of the pixels. + struct Readback + { + std::vector Pixels; + std::atomic Done{false}; + std::atomic Failed{false}; + }; + auto readback = std::make_shared(); + readback->Pixels.resize(static_cast(kWidth) * height * sizeof(float)); + m_deviceContext.ReadTextureAsync(readbackTexture, readback->Pixels) + .then(arcana::inline_scheduler, arcana::cancellation::none(), [readback](const arcana::expected& result) { + readback->Failed = result.has_error(); + readback->Done = true; + }); + + constexpr int kMaxFlushes = 8; + for (int flush = 0; flush < kMaxFlushes && !readback->Done; ++flush) + { + if (!m_deviceContext.ForceMidFrameFlush()) + { + break; + } + } + + if (!readback->Done || readback->Failed) + { + throw Napi::Error::New(info.Env(), "readTransformFeedbackBuffer could not read the buffer back"); + } + + std::memcpy(static_cast(target.Data()) + targetByteOffset, readback->Pixels.data(), static_cast(count) * sizeof(float)); + } + + namespace + { + // Optional transform feedback varyings argument of createProgram/createProgramAsync. + std::vector ReadTransformFeedbackVaryings(const Napi::CallbackInfo& info, size_t index) + { + std::vector varyings; + if (info.Length() > index && info[index].IsArray()) + { + const auto array = info[index].As(); + for (uint32_t element = 0; element < array.Length(); ++element) + { + varyings.push_back(array.Get(element).As().Utf8Value()); + } + } + return varyings; + } + } + Napi::Value NativeEngine::CreateProgram(const Napi::CallbackInfo& info) { const std::string vertexSource = info[0].As().Utf8Value(); const std::string fragmentSource = info[1].As().Utf8Value(); + const std::vector varyings = ReadTransformFeedbackVaryings(info, 2); Program* program = new Program{m_deviceContext}; Napi::Value jsProgram = Napi::Pointer::Create(info.Env(), program, Napi::NapiPointerDeleter(program)); try { - program->SetSources(vertexSource, fragmentSource); - program->Initialize(m_shaderProvider.Get(vertexSource, fragmentSource)); + if (!varyings.empty()) + { + program->InitializeTransformFeedback(m_shaderProvider.GetTransformFeedback(vertexSource, varyings)); + } + else + { + program->SetSources(vertexSource, fragmentSource); + program->Initialize(m_shaderProvider.Get(vertexSource, fragmentSource)); + } } catch (const std::exception& ex) { @@ -1364,20 +1613,31 @@ namespace Babylon std::string fragmentSource = info[1].As().Utf8Value(); const Napi::Function onSuccess = info[2].As(); const Napi::Function onError = info[3].As(); + std::vector varyings = ReadTransformFeedbackVaryings(info, 4); Program* program = new Program{m_deviceContext}; Napi::Value jsProgram = Napi::Pointer::Create(info.Env(), program, Napi::NapiPointerDeleter(program)); - program->SetSources(vertexSource, fragmentSource); + if (varyings.empty()) + { + program->SetSources(vertexSource, fragmentSource); + } arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, - [this, vertexSource = std::move(vertexSource), fragmentSource = std::move(fragmentSource), program, cancellationSource{m_cancellationSource}, asyncTaskScope{TrackAsyncTask()}]() { + [this, vertexSource = std::move(vertexSource), fragmentSource = std::move(fragmentSource), varyings = std::move(varyings), program, cancellationSource{m_cancellationSource}, asyncTaskScope{TrackAsyncTask()}]() { // Skip touching graphics resources if teardown has already begun. if (cancellationSource->cancelled()) { return; } - program->Initialize(m_shaderProvider.Get(vertexSource, fragmentSource)); + if (!varyings.empty()) + { + program->InitializeTransformFeedback(m_shaderProvider.GetTransformFeedback(vertexSource, varyings)); + } + else + { + program->Initialize(m_shaderProvider.Get(vertexSource, fragmentSource)); + } }) .then(m_runtimeScheduler, *m_cancellationSource, [jsProgramRef{Napi::Persistent(jsProgram)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](const arcana::expected& result) { if (result.has_error()) @@ -1912,11 +2172,19 @@ namespace Babylon const auto invertY{info[6].As().Value()}; const auto srgb{info.Length() > 7 && !info[7].IsUndefined() ? info[7].As().Value() : false}; - const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; - if (data.ByteLength() != bimg::imageGetSize(nullptr, width, height, 1, false, false, 1, format)) + std::vector widened; + const auto upload{ResolveRawUpload({static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(), data.ByteLength()}, + bimg::imageGetSize(nullptr, width, height, 1, false, false, 1, format), static_cast(width) * height, Cast(format), widened)}; + if (upload.empty()) { throw Napi::Error::New(Env(), "The data size does not match width, height, and format"); } + const auto bytes{upload.data()}; + + if (m_commandStream) + { + SubmitCommands(info); + } bimg::ImageContainer* image{bimg::imageAlloc(&Graphics::DeviceContext::GetDefaultAllocator(), format, width, height, /*depth*/ 0, 1, false, false, bytes)}; @@ -1931,7 +2199,38 @@ namespace Babylon throw Napi::Error::New(Env(), exception.what()); } - LoadTextureFromImage(texture, image, srgb); + if (texture->IsValid() && m_deviceContext.PeekNextViewId() != 0) + { + if (texture->Width() != image->m_width || texture->Height() != image->m_height) + { + bimg::imageFree(image); + throw Napi::Error::New(Env(), "Cannot update texture from image of different size"); + } + + // bgfx uploads precede every draw in a frame. Stage repeated uploads and + // copy them in view order so earlier cameras retain their light data. + auto scope = m_deviceContext.AcquireFrameCompletionScope(); + auto* encoder = GetEncoder(); + Graphics::Texture staging{m_deviceContext}; + const auto mipCount = image->m_numMips; + LoadTextureFromImage(&staging, image, srgb); + const auto viewId = m_deviceContext.AcquireNewViewId(); + bgfx::resetView(viewId); + for (uint8_t mip = 0; mip < mipCount; ++mip) + { + bgfx::TextureRegion destination{}; + destination.init(texture->Handle()); + destination.mip = mip; + bgfx::TextureRegion source{}; + source.init(staging.Handle()); + source.mip = mip; + encoder->blit(viewId, destination, source); + } + } + else + { + LoadTextureFromImage(texture, image, srgb); + } #endif } @@ -1987,7 +2286,10 @@ namespace Babylon bgfx::TextureInfo textureInfo; bgfx::calcTextureSize(textureInfo, width, height, 1, false, false, 1, texture->Format()); const uint32_t requiredSize{textureInfo.storageSize}; - if (requiredSize == 0 || data.ByteLength() < requiredSize) + std::vector widened; + const auto upload{ResolveRawUpload({static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(), data.ByteLength()}, + requiredSize, static_cast(width) * height, texture->Format(), widened)}; + if (requiredSize == 0 || upload.empty()) { throw Napi::Error::New(info.Env(), "updateTextureData data size does not match width, height, and texture format"); } @@ -2006,37 +2308,99 @@ namespace Babylon throw Napi::Error::New(info.Env(), "updateTextureData does not support block-compressed texture formats"); } - const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; - // Cube uploads honor invertY directly; only 2D uploads compensate for the backend origin. const bool flip{texture->IsCube() ? invertY : (bgfx::getCaps()->originBottomLeft ? invertY : !invertY)}; - const uint16_t targetY{flip ? static_cast(mipHeight - y - height) : y}; - const bgfx::Memory* mem{bgfx::alloc(requiredSize)}; - if (flip) - { - const uint32_t rowBytes{requiredSize / height}; - for (uint16_t row = 0; row < height; ++row) + const auto uploadLevel = [texture, flip, layer](uint8_t level, uint16_t levelX, uint16_t levelY, uint16_t levelWidth, uint16_t levelHeight, const uint8_t* bytes, uint32_t size) { + const uint32_t levelMipHeight{std::max(1u, static_cast(texture->Height()) >> level)}; + const uint16_t targetY{flip ? static_cast(levelMipHeight - levelY - levelHeight) : levelY}; + const bgfx::Memory* mem{bgfx::alloc(size)}; + if (flip) + { + const uint32_t rowBytes{size / levelHeight}; + for (uint16_t row = 0; row < levelHeight; ++row) + { + std::memcpy(mem->data + static_cast(row) * rowBytes, bytes + static_cast(levelHeight - 1 - row) * rowBytes, rowBytes); + } + } + else { - std::memcpy(mem->data + static_cast(row) * rowBytes, bytes + static_cast(height - 1 - row) * rowBytes, rowBytes); + std::memcpy(mem->data, bytes, size); } + if (texture->IsCube()) + { + // bgfx addresses a cube texture by (array layer, side 0-5), but the JS side packs both into the + // single "layer" argument it also uses for 2D-array slices (6 consecutive faces per array layer, + // which is what the bounds check above allows). Decompose it, otherwise a cube-array face index + // above 5 would be forwarded as an out-of-range side and every update would land on array layer 0. + texture->UpdateCube(static_cast(layer / 6), static_cast(layer % 6), level, levelX, targetY, levelWidth, levelHeight, mem); + } + else + { + texture->Update2D(layer, level, levelX, targetY, levelWidth, levelHeight, mem); + } + }; + uploadLevel(mip, x, y, width, height, upload.data(), requiredSize); + + // Optional WebGL generateMipmap for this face/layer: rebuild the rest of the chain from the full + // level that was just uploaded. bgfx cannot generate mips for textures that are not render targets. + const bool generateMips{info.Length() > 9 && !info[9].IsUndefined() && info[9].As().Value()}; + if (!generateMips || mipChainInfo.numMips <= mip + 1) + { + return; } - else + if (x != 0 || y != 0 || width != mipWidth || height != mipHeight) { - std::memcpy(mem->data, bytes, requiredSize); + throw Napi::Error::New(info.Env(), "updateTextureData can only generate mips from a full mip level"); } - if (texture->IsCube()) + auto& allocator{Graphics::DeviceContext::GetDefaultAllocator()}; + using ImagePtr = std::unique_ptr; + const auto format{static_cast(texture->Format())}; + // gl.generateMipmap box-filters linear values; bimg's mip generator applies a gamma curve, so filter here. + ImagePtr source{bimg::imageAlloc(&allocator, format, width, height, 0, 1, false, false, upload.data()), bimg::imageFree}; + ImagePtr converted{source ? bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *source, false) : nullptr, bimg::imageFree}; + if (!converted) { - // bgfx addresses a cube texture by (array layer, side 0-5), but the JS side packs both into the - // single "layer" argument it also uses for 2D-array slices (6 consecutive faces per array layer, - // which is what the bounds check above allows). Decompose it, otherwise a cube-array face index - // above 5 would be forwarded as an out-of-range side and every update would land on array layer 0. - texture->UpdateCube(static_cast(layer / 6), static_cast(layer % 6), mip, x, targetY, width, height, mem); + throw Napi::Error::New(info.Env(), std::string{"updateTextureData cannot generate mips for format "} + bimg::getName(format)); } - else + + const float* levelSource{static_cast(converted->m_data)}; + std::vector levelTexels; + uint32_t levelWidth{width}; + uint32_t levelHeight{height}; + for (uint8_t level = static_cast(mip + 1); level < mipChainInfo.numMips; ++level) { - texture->Update2D(layer, mip, x, targetY, width, height, mem); + const uint32_t nextWidth{std::max(1u, levelWidth >> 1)}; + const uint32_t nextHeight{std::max(1u, levelHeight >> 1)}; + std::vector next(static_cast(nextWidth) * nextHeight * 4); + for (uint32_t row = 0; row < nextHeight; ++row) + { + for (uint32_t column = 0; column < nextWidth; ++column) + { + const uint32_t x0{std::min(column * 2, levelWidth - 1)}; + const uint32_t x1{std::min(column * 2 + 1, levelWidth - 1)}; + const uint32_t y0{std::min(row * 2, levelHeight - 1)}; + const uint32_t y1{std::min(row * 2 + 1, levelHeight - 1)}; + for (uint32_t channel = 0; channel < 4; ++channel) + { + const auto at = [&](uint32_t sx, uint32_t sy) { return levelSource[(static_cast(sy) * levelWidth + sx) * 4 + channel]; }; + next[(static_cast(row) * nextWidth + column) * 4 + channel] = (at(x0, y0) + at(x1, y0) + at(x0, y1) + at(x1, y1)) * 0.25f; + } + } + } + levelTexels = std::move(next); + levelSource = levelTexels.data(); + levelWidth = nextWidth; + levelHeight = nextHeight; + + const auto levelSize{static_cast(bimg::imageGetSize(nullptr, static_cast(levelWidth), static_cast(levelHeight), 1, false, false, 1, format))}; + std::vector levelBytes(levelSize); + if (!bimg::imageConvert(&allocator, levelBytes.data(), format, levelSource, bimg::TextureFormat::RGBA32F, levelWidth, levelHeight, 1)) + { + throw Napi::Error::New(info.Env(), std::string{"updateTextureData cannot convert generated mips to format "} + bimg::getName(format)); + } + uploadLevel(level, 0, 0, static_cast(levelWidth), static_cast(levelHeight), levelBytes.data(), levelSize); } } @@ -2096,8 +2460,13 @@ namespace Babylon // are ever uploaded or sampled (the shader indexes an explicit layer). const uint16_t allocLayers{depth > 1 ? depth : static_cast(2)}; + const bool flipRows = !bgfx::getCaps()->originBottomLeft; + const bool decodeBlocks = flipRows && bimg::isCompressed(format); + const auto uploadFormat = decodeBlocks + ? (bimg::isFloat(format) ? bimg::TextureFormat::RGBA32F : bimg::TextureFormat::RGBA8) + : format; uint64_t flags{BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE}; - texture->Create2D(width, height, generateMips, allocLayers, Cast(format), flags); + texture->Create2D(width, height, generateMips, allocLayers, Cast(uploadFormat), flags); if (!data.IsNull()) { @@ -2105,27 +2474,57 @@ namespace Babylon // 64-bit byte length so a crafted width/height/depth cannot wrap the // expected size and slip an undersized buffer past this check. const uint64_t expectedSize{bimg::imageGetSize(nullptr, width, height, 1, false, false, depth, format)}; - if (expectedSize == 0 || static_cast(data.ByteLength()) != expectedSize) + std::vector widened; + const auto resolved{ResolveRawUpload({static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(), data.ByteLength()}, + expectedSize, static_cast(width) * height * depth, Cast(format), widened)}; + if (expectedSize == 0 || resolved.empty()) { throw Napi::Error::New(Env(), "The data size does not match width, height, depth and format"); } - uint8_t* dataPtr = static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(); - size_t dataSize = data.ByteLength(); + const uint8_t* dataPtr = resolved.data(); + size_t dataSize = resolved.size(); size_t textureSize = dataSize / static_cast(depth); // bgfx::Memory uses a 32-bit size; reject a per-layer payload that // would be truncated by the bgfx::copy cast below. - if (textureSize > UINT32_MAX) + const auto uploadSize = bimg::imageGetSize(nullptr, width, height, 1, false, false, 1, uploadFormat); + if (textureSize > UINT32_MAX || uploadSize > UINT32_MAX) { throw Napi::Error::New(Env(), "The 2D texture array layer size is too large."); } for (uint16_t i = 0; i < depth; i++) { - uint8_t* begin = dataPtr + (textureSize * static_cast(i)); - const bgfx::Memory* dataCopy = bgfx::copy(begin, static_cast(textureSize)); // This is required since BGFX must manage the data the memory. + const uint8_t* begin = dataPtr + (textureSize * static_cast(i)); + using ImagePtr = std::unique_ptr; + ImagePtr decoded{nullptr, bimg::imageFree}; + gsl::span upload{begin, textureSize}; + if (decodeBlocks) + { + // Compressed blocks are not pixel rows; decode before flipping, including partial edge blocks. + auto& allocator = Graphics::DeviceContext::GetDefaultAllocator(); + ImagePtr source{bimg::imageAlloc(&allocator, format, width, height, 0, 1, false, false, begin), bimg::imageFree}; + if (!source) + { + throw Napi::Error::New(Env(), "Failed to allocate the compressed array layer."); + } + source->m_width = width; + source->m_height = height; + decoded.reset(bimg::imageConvert(&allocator, uploadFormat, *source, false)); + if (!decoded || decoded->m_size != uploadSize) + { + throw Napi::Error::New(Env(), "Failed to decode the compressed array layer."); + } + upload = {static_cast(decoded->m_data), decoded->m_size}; + } + const bgfx::Memory* dataCopy = bgfx::copy(upload.data(), static_cast(upload.size())); + if (flipRows) + { + // Match rendered array layers without modifying the caller's data. + FlipImage({dataCopy->data, upload.size()}, height); + } texture->Update2D(i, 0, 0, 0, width, height, dataCopy); } } @@ -2182,13 +2581,16 @@ namespace Babylon // crafted width/height/depth cannot wrap the expected size and slip an undersized buffer past // this check. A 3D texture is a single volume (numLayers = 1) whose depth is the 3rd argument. const uint64_t expectedSize{bimg::imageGetSize(nullptr, width, height, depth, false, false, 1, format)}; - if (expectedSize == 0 || static_cast(data.ByteLength()) != expectedSize) + std::vector widened; + const auto resolved{ResolveRawUpload({static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(), data.ByteLength()}, + expectedSize, static_cast(width) * height * depth, Cast(format), widened)}; + if (expectedSize == 0 || resolved.empty()) { throw Napi::Error::New(Env(), "The data size does not match width, height, depth and format"); } - uint8_t* dataPtr = static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(); - const size_t dataSize = data.ByteLength(); + const uint8_t* dataPtr = resolved.data(); + const size_t dataSize = resolved.size(); // bgfx::Memory uses a 32-bit size; reject a payload that would be truncated by the bgfx::copy // cast below. @@ -2478,10 +2880,19 @@ namespace Babylon const Graphics::Texture* texture = data.ReadPointer(); bgfx::Encoder* encoder = GetEncoder(); - const uint16_t firstLayer = texture->ViewNumLayers() != 0 ? texture->ViewFirstLayer() : 0; - const uint16_t numLayers = texture->ViewNumLayers() != 0 ? texture->ViewNumLayers() : UINT16_MAX; - encoder->setTexture(uniformInfo->Stage, uniformInfo->Handle, texture->Handle(), - firstLayer, numLayers, 0, UINT8_MAX, texture->SamplerFlags(), 0, texture->SamplerMaxLod()); + + BoundTexture bound{}; + bound.Handle = uniformInfo->Handle; + bound.Texture = texture->Handle(); + bound.Flags = texture->SamplerFlags(); + bound.FirstLayer = texture->ViewNumLayers() != 0 ? texture->ViewFirstLayer() : 0; + bound.NumLayers = texture->ViewNumLayers() != 0 ? texture->ViewNumLayers() : UINT16_MAX; + bound.MaxLod = texture->SamplerMaxLod(); + bound.PointSampleHeight = PointSampleHeight(*texture); + m_boundTextures[uniformInfo->Stage] = bound; + + encoder->setTexture(uniformInfo->Stage, bound.Handle, bound.Texture, + bound.FirstLayer, bound.NumLayers, 0, UINT8_MAX, bound.Flags, 0, bound.MaxLod); } void NativeEngine::UnsetTexture(NativeDataStream::Reader& data) @@ -2489,23 +2900,57 @@ namespace Babylon const UniformInfo* uniformInfo = data.ReadPointer(); bgfx::Encoder* encoder = GetEncoder(); + m_boundTextures.erase(uniformInfo->Stage); encoder->setTexture(uniformInfo->Stage, uniformInfo->Handle, BGFX_INVALID_HANDLE); } void NativeEngine::DiscardAllTextures(NativeDataStream::Reader&) { bgfx::Encoder* encoder = GetEncoder(); + m_boundTextures.clear(); encoder->discard(BGFX_DISCARD_BINDINGS); } + void NativeEngine::RestoreBoundTextures(bgfx::Encoder* encoder) + { + if (encoder == nullptr) + { + return; + } + for (const auto& [stage, bound] : m_boundTextures) + { + if (!bgfx::isValid(bound.Handle) || !bgfx::isValid(bound.Texture)) + { + continue; + } + encoder->setTexture(stage, bound.Handle, bound.Texture, + bound.FirstLayer, bound.NumLayers, 0, UINT8_MAX, bound.Flags, 0, bound.MaxLod); + } + } + void NativeEngine::DeleteTexture(const Napi::CallbackInfo& info) { Graphics::Texture* texture = info[0].As>().Get(); + // Deleting a bound WebGL texture unbinds it. + for (auto it = m_boundTextures.begin(); it != m_boundTextures.end();) + { + it = it->second.Texture.idx == texture->Handle().idx ? m_boundTextures.erase(it) : std::next(it); + } m_deviceContext.RemoveTexture(texture->Handle()); texture->Dispose(); } Napi::Value NativeEngine::ReadTexture(const Napi::CallbackInfo& info) + { + return ReadTextureImpl(info, false); + } + + Napi::Value NativeEngine::ReadTexture2(const Napi::CallbackInfo& info) + { + return ReadTextureImpl(info, true); + } + + Napi::Value NativeEngine::ReadTextureImpl(const Napi::CallbackInfo& info, bool preserveFloat) { const Napi::Env env{info.Env()}; @@ -2605,7 +3050,7 @@ namespace Babylon } // Check the full storage size before bgfx narrows it to uint32. - const auto targetTextureFormat{bgfx::TextureFormat::RGBA8}; + const auto targetTextureFormat{preserveFloat && IsFloatTextureFormat(sourceTextureFormat) ? bgfx::TextureFormat::RGBA32F : bgfx::TextureFormat::RGBA8}; const auto storageSize = [&](bgfx::TextureFormat::Enum format) { return bimg::imageGetSize(nullptr, width, height, 1, false, false, 1, static_cast(format)); }; @@ -2656,7 +3101,9 @@ namespace Babylon auto tempTexture = std::make_shared(false); // WebGL readPixels coordinates start at the bottom, unlike D3D/Metal/Vulkan blit coordinates. - const uint16_t blitY{bgfx::getCaps()->originBottomLeft ? y : static_cast(mipHeight - y - height)}; + // Cube faces are stored in GL row order on every backend, for uploads and rendering alike. + const bool glRowOrder{bgfx::getCaps()->originBottomLeft || texture->IsCube()}; + const uint16_t blitY{glRowOrder ? y : static_cast(mipHeight - y - height)}; // If the image needs to be cropped, the texture lacks the READ_BACK flag, or we are reading a // specific cube-map face, blit to a temp 2D texture. bgfx::read addresses a whole mip of one @@ -2685,7 +3132,7 @@ namespace Babylon // Read the source texture (async — completes after bgfx::frame). m_deviceContext.ReadTextureAsync(sourceTextureHandle, textureBuffer, mipLevel) - .then(arcana::inline_scheduler, *m_cancellationSource, [textureBuffer{std::move(textureBuffer)}, sourceTextureInfo, targetTextureInfo]() mutable { + .then(arcana::inline_scheduler, *m_cancellationSource, [textureBuffer{std::move(textureBuffer)}, sourceTextureInfo, targetTextureInfo, glRowOrder]() mutable { if (targetTextureInfo.format != sourceTextureInfo.format) { #ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES @@ -2701,7 +3148,7 @@ namespace Babylon } assert(textureBuffer.size() == targetTextureInfo.storageSize); // Return bottom-up rows, matching WebGL readPixels on every backend. - if (!bgfx::getCaps()->originBottomLeft) + if (!glRowOrder) { FlipImage(textureBuffer, targetTextureInfo.height); } @@ -3018,6 +3465,21 @@ namespace Babylon const bool isMultisampled = RenderTargetSamplesToBgfxRtFlag(samples) != BGFX_TEXTURE_RT; Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, hasDepthAttachment, generateStencilBuffer, frameBufferDepthOwnerIndex, isMultisampled, depthOneVolumeAttachmentMask); + // Like ANGLE on D3D, render cube faces upside down on top-left-origin backends so their rows land + // in GL order. Cube directions are sampled unflipped, so this keeps rendered faces consistent with + // uploaded faces and with WebGL without any Babylon.js compensation. 2D targets keep top-left rows, + // which the shader compiler's sampler coordinate flip already accounts for. + if (!caps->originBottomLeft) + { + bool cubeTarget{false}; + for (Graphics::Texture* texture : colorTextures) + { + cubeTarget = cubeTarget || (texture != nullptr && bgfx::isValid(texture->Handle()) && texture->IsCube()); + } + cubeTarget = cubeTarget || (explicitDepthTexture != nullptr && explicitDepthTexture->IsCube()); + frameBuffer->SetGLRowOrder(cubeTarget); + } + return Napi::Pointer::Create(env, frameBuffer, Napi::NapiPointerDeleter(frameBuffer)); } @@ -3073,13 +3535,31 @@ namespace Babylon const uint32_t indexCount = data.ReadUint32(); bgfx::Encoder* encoder = GetEncoder(); + // A pending back buffer reset clears the encoder, so run it before binding geometry. + GetBoundFrameBuffer(); const auto instanceDataLayout = GetInstanceDataLayout(); + bool shouldDraw = true; if (m_boundVertexArray != nullptr) { - m_boundVertexArray->SetIndexBuffer(encoder, indexStart, indexCount); + if (fillMode == 5 || fillMode == 8) + { + shouldDraw = m_boundVertexArray->SetExpandedIndexBuffer( + encoder, static_cast(fillMode), indexStart, indexCount); + } + else + { + m_boundVertexArray->SetIndexBuffer(encoder, indexStart, indexCount); + } m_boundVertexArray->SetVertexBuffers(encoder, 0, std::numeric_limits::max(), 0, instanceDataLayout); } - DrawInternal(encoder, fillMode, instanceDataLayout); + else if (fillMode == 5 || fillMode == 8) + { + throw std::runtime_error{"Primitive expansion requires a bound vertex array"}; + } + if (shouldDraw) + { + DrawInternal(encoder, fillMode, instanceDataLayout); + } } void NativeEngine::DrawIndexedInstanced(NativeDataStream::Reader& data) @@ -3090,13 +3570,37 @@ namespace Babylon const uint32_t instanceCount = data.ReadUint32(); bgfx::Encoder* encoder = GetEncoder(); + // A pending back buffer reset clears the encoder, so run it before binding geometry. + GetBoundFrameBuffer(); const auto instanceDataLayout = GetInstanceDataLayout(); + bool shouldDraw = true; if (m_boundVertexArray != nullptr) { - m_boundVertexArray->SetIndexBuffer(encoder, indexStart, indexCount); + const bgfx::DynamicVertexBufferHandle repacked = RepackStorageInstances(m_boundVertexArray, instanceDataLayout, instanceCount); + RestoreBoundTextures(encoder); + if (fillMode == 5 || fillMode == 8) + { + shouldDraw = m_boundVertexArray->SetExpandedIndexBuffer( + encoder, static_cast(fillMode), indexStart, indexCount); + } + else + { + m_boundVertexArray->SetIndexBuffer(encoder, indexStart, indexCount); + } m_boundVertexArray->SetVertexBuffers(encoder, 0, std::numeric_limits::max(), instanceCount, instanceDataLayout); + if (bgfx::isValid(repacked)) + { + encoder->setInstanceDataBuffer(repacked, 0, instanceCount); + } + } + else if (fillMode == 5 || fillMode == 8) + { + throw std::runtime_error{"Primitive expansion requires a bound vertex array"}; + } + if (shouldDraw) + { + DrawInternal(encoder, fillMode, instanceDataLayout); } - DrawInternal(encoder, fillMode, instanceDataLayout); } // Note: For legacy reasons JS might call this function for instance drawing. @@ -3108,12 +3612,27 @@ namespace Babylon const uint32_t verticesCount = data.ReadUint32(); bgfx::Encoder* encoder = GetEncoder(); + // A pending back buffer reset clears the encoder, so run it before binding geometry. + GetBoundFrameBuffer(); const auto instanceDataLayout = GetInstanceDataLayout(); + bool shouldDraw = true; if (m_boundVertexArray != nullptr) { m_boundVertexArray->SetVertexBuffers(encoder, verticesStart, verticesCount, 0, instanceDataLayout); + if (fillMode == 5 || fillMode == 8) + { + shouldDraw = m_boundVertexArray->SetExpandedUnindexedBuffer( + encoder, static_cast(fillMode), verticesCount); + } + } + else if (fillMode == 5 || fillMode == 8) + { + throw std::runtime_error{"Primitive expansion requires a bound vertex array"}; + } + if (shouldDraw) + { + DrawInternal(encoder, fillMode, instanceDataLayout); } - DrawInternal(encoder, fillMode, instanceDataLayout); } void NativeEngine::DrawInstanced(NativeDataStream::Reader& data) @@ -3124,15 +3643,48 @@ namespace Babylon const uint32_t instanceCount = data.ReadUint32(); bgfx::Encoder* encoder = GetEncoder(); + // A pending back buffer reset clears the encoder, so run it before binding geometry. + GetBoundFrameBuffer(); const auto instanceDataLayout = GetInstanceDataLayout(); + bool shouldDraw = true; if (m_boundVertexArray != nullptr) { + // GPU-written instance sources (transform feedback output) are repacked into bgfx i_data + // slots by a compute dispatch, which clears the encoder's texture bindings. + const bgfx::DynamicVertexBufferHandle repacked = RepackStorageInstances(m_boundVertexArray, instanceDataLayout, instanceCount); + RestoreBoundTextures(encoder); m_boundVertexArray->SetVertexBuffers(encoder, verticesStart, verticesCount, instanceCount, instanceDataLayout); + if (fillMode == 5 || fillMode == 8) + { + shouldDraw = m_boundVertexArray->SetExpandedUnindexedBuffer( + encoder, static_cast(fillMode), verticesCount); + } + if (bgfx::isValid(repacked)) + { + encoder->setInstanceDataBuffer(repacked, 0, instanceCount); + } + } + else if (fillMode == 5 || fillMode == 8) + { + throw std::runtime_error{"Primitive expansion requires a bound vertex array"}; + } + if (shouldDraw) + { + DrawInternal(encoder, fillMode, instanceDataLayout); } - DrawInternal(encoder, fillMode, instanceDataLayout); } void NativeEngine::Clear(NativeDataStream::Reader& data) + { + ClearImpl(data, false); + } + + void NativeEngine::Clear2(NativeDataStream::Reader& data) + { + ClearImpl(data, true); + } + + void NativeEngine::ClearImpl(NativeDataStream::Reader& data, bool hasAttachmentMask) { uint16_t flags{0}; @@ -3145,6 +3697,7 @@ namespace Babylon const float depth{data.ReadFloat32()}; const bool shouldClearStencil{static_cast(data.ReadUint32())}; const uint8_t stencil{static_cast(data.ReadUint32())}; + const uint8_t colorAttachmentMask{static_cast(hasAttachmentMask ? data.ReadUint32() : UINT8_MAX)}; bgfx::Encoder* encoder = GetEncoder(); @@ -3164,7 +3717,9 @@ namespace Babylon flags |= BGFX_CLEAR_STENCIL; } - GetBoundFrameBuffer().Clear(*encoder, flags, r, g, b, a, depth, stencil); + GetBoundFrameBuffer().Clear(*encoder, flags, r, g, b, a, depth, stencil, colorAttachmentMask); + // FrameBuffer::Clear touches its view, discarding bgfx bindings. WebGL clears retain them. + RestoreBoundTextures(encoder); } Napi::Value NativeEngine::GetRenderWidth(const Napi::CallbackInfo& info) @@ -3354,6 +3909,16 @@ namespace Babylon } void NativeEngine::SetStencil(NativeDataStream::Reader& data) + { + SetStencilImpl(data, false); + } + + void NativeEngine::SetStencil2(NativeDataStream::Reader& data) + { + SetStencilImpl(data, true); + } + + void NativeEngine::SetStencilImpl(NativeDataStream::Reader& data, bool hasComparisonMask) { const uint32_t writeMask{data.ReadUint32()}; const uint32_t stencilOpFail{data.ReadUint32()}; @@ -3361,8 +3926,9 @@ namespace Babylon const uint32_t depthOpPass{data.ReadUint32()}; const uint32_t func{data.ReadUint32()}; const uint32_t ref{data.ReadUint32()}; + const uint32_t funcMask{hasComparisonMask ? data.ReadUint32() : UINT8_MAX}; - m_stencilState = BGFX_STENCIL_FUNC_RMASK(0xFF); + m_stencilState = BGFX_STENCIL_FUNC_RMASK(funcMask & 0xFFu); m_stencilState |= stencilOpFail; m_stencilState |= depthOpFail; // bgfx write mask is always 0xFF, to not change stencil value when writemask is 0 @@ -3450,7 +4016,6 @@ namespace Babylon throw Napi::Error::New(info.Env(), exception); } } - void NativeEngine::PopulateFrameStats(const Napi::CallbackInfo& info) { const auto stats{bgfx::getStats()}; @@ -3472,9 +4037,125 @@ namespace Babylon // Nothing to do here. } + // Emulates a WebGL2 transform feedback draw (rasterizer discarded, POINTS, interleaved + // capture into one buffer). Stream layout: output VertexBuffer*, first vertex, vertex count. + // The current program must be a transform feedback program; the bound vertex array supplies + // its inputs, all from one vertex buffer. Both buffers move to GPU storage on first use, after + // which later renders read the captured data from there. + void NativeEngine::DrawTransformFeedback(NativeDataStream::Reader& data) + { + VertexBuffer* output = data.ReadPointer(); + const uint32_t first = data.ReadUint32(); + const uint32_t count = data.ReadUint32(); + + if (m_currentProgram == nullptr || !m_currentProgram->IsTransformFeedback()) + { + throw std::runtime_error{"Transform feedback draw requires a transform feedback program"}; + } + if (count == 0) + { + return; + } + + std::vector inputs; + const uint32_t inputCount = m_currentProgram->TransformFeedbackInputCount(); + std::vector inputLayout(std::max(inputCount, 1) * 4, 0.0f); + if (m_boundVertexArray != nullptr) + { + for (const auto& [index, record] : m_boundVertexArray->GetTransformFeedbackInputs()) + { + if (index >= inputCount) + { + continue; + } + if (record.Buffer == output) + { + throw std::runtime_error{"Transform feedback cannot capture into a buffer it reads from"}; + } + auto found = std::find(inputs.begin(), inputs.end(), record.Buffer); + if (found == inputs.end()) + { + if (inputs.size() == Graphics::TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS) + { + throw std::runtime_error{"Transform feedback inputs come from too many vertex buffers"}; + } + found = inputs.insert(inputs.end(), record.Buffer); + } + const uint32_t buffer = static_cast(found - inputs.begin()); + inputLayout[index * 4 + 0] = static_cast(record.ByteOffset); + inputLayout[index * 4 + 1] = static_cast(record.ByteStride); + inputLayout[index * 4 + 2] = static_cast(record.NumElements); + inputLayout[index * 4 + 3] = static_cast(buffer * 16 + static_cast(record.Type) * 2 + (record.Normalized ? 1 : 0)); + } + } + + StorageBuffer& outputStorage = output->PromoteToGpuStorage(); + + bgfx::Encoder* encoder = GetEncoder(); + RestoreBoundTextures(encoder); + for (const auto& [handle, value] : m_currentProgram->Uniforms()) + { + encoder->setUniform({handle}, value.Data.data(), value.ElementLength); + } + for (const auto& [stage, uniform] : m_currentProgram->SamplerStateUniforms()) + { + const auto texture = m_boundTextures.find(stage); + const float values[4]{ + texture == m_boundTextures.end() ? 0.0f : texture->second.PointSampleHeight, 0.0f, 0.0f, 0.0f}; + encoder->setUniform(uniform, values); + } + const float params[4]{static_cast(count), static_cast(first), 0.0f, 0.0f}; + encoder->setUniform(m_currentProgram->TransformFeedbackParams()->Handle, params); + if (const UniformInfo* layout = m_currentProgram->TransformFeedbackInputLayout()) + { + encoder->setUniform(layout->Handle, inputLayout.data(), static_cast(std::max(inputCount, 1))); + } + + // Buffers share bgfx binding slots with textures, so bind them after restoring textures. Every + // declared input buffer needs a binding (Vulkan descriptors), so unused ones repeat a bound + // buffer, or the output when there are no inputs. + for (uint32_t buffer = 0; buffer < Graphics::TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS; ++buffer) + { + const auto stage = static_cast(buffer == 0 ? Graphics::TRANSFORM_FEEDBACK_INPUT_BINDING : Graphics::TRANSFORM_FEEDBACK_EXTRA_INPUT_BINDING + buffer - 1); + if (inputs.empty()) + { + outputStorage.SetCompute(encoder, stage, bgfx::Access::Read); + } + else + { + inputs[buffer < inputs.size() ? buffer : 0]->SetComputeRead(encoder, stage); + } + } + outputStorage.SetCompute(encoder, static_cast(Graphics::TRANSFORM_FEEDBACK_OUTPUT_BINDING), bgfx::Access::Write); + + const uint32_t groups = (count + Graphics::TRANSFORM_FEEDBACK_WORKGROUP_SIZE - 1) / Graphics::TRANSFORM_FEEDBACK_WORKGROUP_SIZE; + GetBoundFrameBuffer().Compute(*encoder, m_currentProgram->Handle(), groups, 1, 1); + + // The dispatch clears the encoder's bindings. Later dispatches and draws in this frame are + // ordered after it (see FrameBuffer::Compute), so no flush is needed for them to see the writes. + RestoreBoundTextures(encoder); + } + + bgfx::DynamicVertexBufferHandle NativeEngine::RepackStorageInstances(VertexArray* vertexArray, + const VertexBuffer::InstanceDataLayout& instanceDataLayout, uint32_t instanceCount) + { + if (vertexArray == nullptr || instanceCount == 0 || !vertexArray->HasStorageInstances()) + { + return BGFX_INVALID_HANDLE; + } + + if (m_instanceRepacker == nullptr) + { + m_instanceRepacker = std::make_unique(m_deviceContext); + } + + return m_instanceRepacker->Repack(GetBoundFrameBuffer(), vertexArray, vertexArray->GetInstances(), instanceDataLayout, instanceCount); + } + void NativeEngine::DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode, const VertexBuffer::InstanceDataLayout& instanceDataLayout) { uint64_t fillModeState{0}; // indexed triangle list + switch (fillMode) { case 0: // MATERIAL_TriangleFillMode @@ -3496,7 +4177,7 @@ namespace Babylon } case 5: // MATERIAL_LineLoopDrawMode { - // TODO: unsupported mode + fillModeState = BGFX_STATE_PT_LINES; break; } case 6: // MATERIAL_LineStripDrawMode @@ -3511,7 +4192,7 @@ namespace Babylon } case 8: // MATERIAL_TriangleFanDrawMode { - // TODO: unsupported mode + fillModeState = 0; break; } } @@ -3521,19 +4202,27 @@ namespace Babylon const UniformValue& value = it.second; encoder->setUniform({it.first}, value.Data.data(), value.ElementLength); } + for (const auto& [stage, uniform] : m_currentProgram->SamplerStateUniforms()) + { + const auto texture = m_boundTextures.find(stage); + const float values[4]{ + texture == m_boundTextures.end() ? 0.0f : texture->second.PointSampleHeight, 0.0f, 0.0f, 0.0f}; + encoder->setUniform(uniform, values); + } - // Resolves the gl_FragCoord Y flip injected by ShaderCompilerTraversers::FlipFragCoordY. - // Must be the framebuffer's height, not the bgfx view rect's, which - // SetBgfxViewPortAndScissor narrows to the viewport when one is set. - if (const UniformInfo* fragCoordTargetSize = m_currentProgram->FragCoordTargetSizeUniform()) + // Resolve the injected render target transform (see RENDER_TARGET_TRANSFORM_UNIFORM_NAME). + // The height must be the bound framebuffer's, not the bgfx view rect's: FrameBuffer::SetBgfxViewPortAndScissor + // narrows the view rect to the viewport whenever one is set, while gl_FragCoord is relative to the whole target. + const Graphics::FrameBuffer& transformFrameBuffer = GetBoundFrameBuffer(); + const bool glRowOrder = transformFrameBuffer.GLRowOrder(); + if (const UniformInfo* renderTargetTransform = m_currentProgram->RenderTargetTransformUniform()) { - const Graphics::FrameBuffer& frameBuffer = GetBoundFrameBuffer(); - const float targetSize[4]{ - static_cast(frameBuffer.Width()), - static_cast(frameBuffer.Height()), - 0.0f, + const float transform[4]{ + glRowOrder ? 0.0f : static_cast(transformFrameBuffer.Height()), + glRowOrder ? 1.0f : -1.0f, + glRowOrder ? -1.0f : 1.0f, 0.0f}; - encoder->setUniform(fragCoordTargetSize->Handle, targetSize, 1); + encoder->setUniform(renderTargetTransform->Handle, transform, 1); } // Generic divisor-driven attributes are per-vertex inputs in the base shader. Recompile a @@ -3545,6 +4234,9 @@ namespace Babylon const auto& instances = m_boundVertexArray->GetInstances(); if (!instances.empty()) { + // bgfx delivers instance slot k at TEXCOORD(31 - k); the highest-location instance + // attribute is packed at byte offset 0 (i_data0 == TEXCOORD31 == + // INSTANCE_DATA_FIRST_LOCATION), matching BuildInstanceDataBuffer's reverse packing. std::map genericInstancedAttributes; const auto& attributeLocations = m_currentProgram->VertexAttributeLocations(); for (const auto& instance : instances) @@ -3577,14 +4269,17 @@ namespace Babylon auto& boundFrameBuffer = GetBoundFrameBuffer(); // D3D11 also uses this flag to select its line coverage algorithm on single-sample targets. const uint64_t multisampleMask = boundFrameBuffer.IsMultisampled() ? UINT64_MAX : ~BGFX_STATE_MSAA; + // The vertex Y flip for GL-row-order targets mirrors the winding; bgfx culls relative to the + // front face, so swapping it keeps both culling and gl_FrontFacing as WebGL defines them. + const uint64_t engineState = glRowOrder ? (m_engineState ^ BGFX_STATE_FRONT_CCW) : m_engineState; if (boundFrameBuffer.HasDepth()) { - const uint64_t drawState = (m_engineState | fillModeState); + const uint64_t drawState = (engineState | fillModeState); encoder->setState(drawState & multisampleMask); } else { - const uint64_t drawState = ((m_engineState & ~BGFX_STATE_WRITE_Z) | fillModeState); + const uint64_t drawState = ((engineState & ~BGFX_STATE_WRITE_Z) | fillModeState); encoder->setState(drawState & multisampleMask); } @@ -3608,9 +4303,40 @@ namespace Babylon } m_boundFrameBufferNeedsRebinding.Set(false); + + if (m_boundFrameBuffer == &m_defaultFrameBuffer && m_defaultFrameBufferNeedsReset.Get()) + { + ResetDefaultFrameBuffer(); + } + return *m_boundFrameBuffer; } + void NativeEngine::ResetDefaultFrameBuffer() + { + // Like a WebGL drawing buffer without preserveDrawingBuffer, the first use of the back buffer + // in a frame sees depth 1 and stencil 0 rather than whatever the previous frame left (or the + // zero-initialized surface when only render targets were ever cleared). Colour is preserved so + // content that is not redrawn every frame stays on screen. Not GetEncoder(): a view flush there + // would replace the encoder the caller already holds. + bgfx::Encoder* encoder = m_deviceContext.GetActiveEncoder(); + if (encoder == nullptr) + { + return; + } + + m_defaultFrameBufferNeedsReset.Set(false); + + const uint16_t flags = static_cast((m_defaultFrameBuffer.HasDepth() ? BGFX_CLEAR_DEPTH : 0) | (m_defaultFrameBuffer.HasStencil() ? BGFX_CLEAR_STENCIL : 0)); + if (flags == 0) + { + return; + } + + m_defaultFrameBuffer.Clear(*encoder, flags, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0, UINT8_MAX, true); + RestoreBoundTextures(encoder); + } + void NativeEngine::ScheduleRequestAnimationFrameCallbacks() { if (m_requestAnimationFrameCallbacksScheduled) diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index 5532c18f86..58098fcb4c 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -37,6 +37,8 @@ namespace Babylon class Texture; } + class InstanceRepacker; + class NativeEngine final : public Napi::ObjectWrap { static constexpr auto JS_CLASS_NAME = "_NativeEngine"; @@ -70,6 +72,7 @@ namespace Babylon void DeleteVertexBuffer(NativeDataStream::Reader& data); void RecordVertexBuffer(const Napi::CallbackInfo& info); void UpdateDynamicVertexBuffer(const Napi::CallbackInfo& info); + void ReadTransformFeedbackBuffer(const Napi::CallbackInfo& info); Napi::Value CreateProgram(const Napi::CallbackInfo& info); Napi::Value CreateProgramAsync(const Napi::CallbackInfo& info); Napi::Value GetUniforms(const Napi::CallbackInfo& info); @@ -123,6 +126,8 @@ namespace Babylon void DiscardAllTextures(NativeDataStream::Reader& data); void DeleteTexture(const Napi::CallbackInfo& info); Napi::Value ReadTexture(const Napi::CallbackInfo& info); + Napi::Value ReadTexture2(const Napi::CallbackInfo& info); + Napi::Value ReadTextureImpl(const Napi::CallbackInfo& info, bool preserveFloat); Napi::Value CreateFrameBuffer(const Napi::CallbackInfo& info); Napi::Value CreateMultiFrameBuffer(const Napi::CallbackInfo& info); Napi::Value CreateFrameBufferImpl(Napi::Env env, gsl::span colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples, uint16_t layer = 0, uint16_t mip = 0, gsl::span perAttachmentLayers = {}, Graphics::Texture* explicitDepthTexture = nullptr, bool autoGenerateMips = true, Graphics::Texture* depthStencilTexture = nullptr); @@ -134,7 +139,10 @@ namespace Babylon void DrawIndexedInstanced(NativeDataStream::Reader& data); void Draw(NativeDataStream::Reader& data); void DrawInstanced(NativeDataStream::Reader& data); + void DrawTransformFeedback(NativeDataStream::Reader& data); void Clear(NativeDataStream::Reader& data); + void Clear2(NativeDataStream::Reader& data); + void ClearImpl(NativeDataStream::Reader& data, bool hasAttachmentMask); Napi::Value GetRenderWidth(const Napi::CallbackInfo& info); Napi::Value GetRenderHeight(const Napi::CallbackInfo& info); Napi::Value GetHardwareScalingLevel(const Napi::CallbackInfo& info); @@ -144,6 +152,8 @@ namespace Babylon void GetFrameBufferData(const Napi::CallbackInfo& info); void SetRenderResetCallback(const Napi::CallbackInfo& info); void SetStencil(NativeDataStream::Reader& data); + void SetStencil2(NativeDataStream::Reader& data); + void SetStencilImpl(NativeDataStream::Reader& data, bool hasComparisonMask); void SetViewPort(NativeDataStream::Reader& data); void SetScissor(NativeDataStream::Reader& data); void SetCommandDataStream(const Napi::CallbackInfo& info); @@ -153,8 +163,16 @@ namespace Babylon void EndFrame(const Napi::CallbackInfo&); void DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode, const VertexBuffer::InstanceDataLayout& instanceDataLayout); + // Dispatches the GPU instance-data repack for a vertex array whose per-instance sources are + // GPU storage buffers, returning the dynamic vertex buffer to bind as instance data (or an + // invalid handle when there is nothing to repack). Must be called before the draw bindings + // are set, since dispatch resets the encoder's pending draw state. + bgfx::DynamicVertexBufferHandle RepackStorageInstances(VertexArray* vertexArray, + const VertexBuffer::InstanceDataLayout& instanceDataLayout, uint32_t instanceCount); + bgfx::Encoder* GetEncoder(); Graphics::FrameBuffer& GetBoundFrameBuffer(); + void ResetDefaultFrameBuffer(); std::shared_ptr m_cancellationSource{}; @@ -230,10 +248,32 @@ namespace Babylon std::vector m_requestAnimationFrameCallbacks{}; VertexArray* m_boundVertexArray{}; + std::unique_ptr m_instanceRepacker{}; + // Copies a storage buffer into an R32F image for readTransformFeedbackBuffer; created on first use. + std::shared_ptr m_bufferReadbackProgram{}; Graphics::FrameBuffer m_defaultFrameBuffer; Graphics::FrameBuffer* m_boundFrameBuffer{}; PerFrameValue m_boundFrameBufferNeedsRebinding; + // WebGL resets the drawing buffer's depth and stencil at the start of every frame. + PerFrameValue m_defaultFrameBufferNeedsReset; + + // Last material texture binds on the frame encoder. bgfx drops encoder bindings when work + // moves to another view (clears, resolves, blits, compute dispatches); WebGL keeps them, + // so they are replayed. + struct BoundTexture + { + bgfx::UniformHandle Handle{bgfx::kInvalidHandle}; + bgfx::TextureHandle Texture{bgfx::kInvalidHandle}; + uint32_t Flags{}; + uint16_t FirstLayer{}; + uint16_t NumLayers{}; + uint8_t MaxLod{UINT8_MAX}; + float PointSampleHeight{}; + }; + std::map m_boundTextures{}; + void RestoreBoundTextures(bgfx::Encoder* encoder); + // TODO: This should be changed to a non-owning ref once multi-update is available. NativeDataStream* m_commandStream{}; diff --git a/Plugins/NativeEngine/Source/PerFrameValue.h b/Plugins/NativeEngine/Source/PerFrameValue.h index 0932381a9f..c46e297eb1 100644 --- a/Plugins/NativeEngine/Source/PerFrameValue.h +++ b/Plugins/NativeEngine/Source/PerFrameValue.h @@ -4,6 +4,8 @@ #include +#include + namespace Babylon { // This type contains a per-frame value which automatically resets to @@ -12,9 +14,9 @@ namespace Babylon class PerFrameValue { public: - PerFrameValue(Graphics::DeviceContext& context, arcana::cancellation_source& cancellation, T defaultValue) + PerFrameValue(Graphics::DeviceContext& context, std::shared_ptr cancellation, T defaultValue) : m_context{context} - , m_cancellationSource{cancellation} + , m_cancellationSource{std::move(cancellation)} , m_defaultValue{defaultValue} , m_value{defaultValue} , m_isResetScheduled{false} @@ -31,7 +33,8 @@ namespace Babylon m_value = value; if (!m_isResetScheduled) { - arcana::make_task(m_context.AfterRenderScheduler(), m_cancellationSource, [this]() { + // The task may outlive this object, so it keeps the cancellation source alive. + arcana::make_task(m_context.AfterRenderScheduler(), *m_cancellationSource, [this, cancellationSource{m_cancellationSource}]() { m_value = m_defaultValue; m_isResetScheduled = false; }); @@ -41,7 +44,7 @@ namespace Babylon private: Graphics::DeviceContext& m_context; - arcana::cancellation_source& m_cancellationSource; + std::shared_ptr m_cancellationSource; const T m_defaultValue{}; T m_value{}; bool m_isResetScheduled{}; diff --git a/Plugins/NativeEngine/Source/PrimitiveModeExpansion.cpp b/Plugins/NativeEngine/Source/PrimitiveModeExpansion.cpp new file mode 100644 index 0000000000..0da009123b --- /dev/null +++ b/Plugins/NativeEngine/Source/PrimitiveModeExpansion.cpp @@ -0,0 +1,248 @@ +#include "PrimitiveModeExpansion.h" + +#include +#include +#include +#include + +namespace Babylon::PrimitiveModeExpansion +{ + namespace + { + template + T ReadIndex(gsl::span bytes, size_t index) + { + T value{}; + std::memcpy(&value, bytes.data() + index * sizeof(T), sizeof(T)); + return value; + } + + uint32_t CheckedIndexCount(uint64_t count, size_t indexStride) + { + if (count > std::numeric_limits::max() || + count > std::numeric_limits::max() / indexStride || + count > std::numeric_limits::max() / indexStride) + { + throw std::length_error{"Expanded primitive index data is too large"}; + } + + return static_cast(count); + } + + uint64_t PrimitiveIndexCount(uint64_t runLength, Mode mode) + { + switch (mode) + { + case Mode::LineLoop: + return runLength >= 2 ? runLength * 2 : 0; + case Mode::TriangleFan: + return runLength >= 3 ? (runLength - 2) * 3 : 0; + } + + throw std::invalid_argument{"Unsupported primitive expansion mode"}; + } + + template + Result ExpandIndexedTyped(const IndexData& data, uint32_t firstIndex, uint32_t indexCount, Mode mode) + { + const auto bytes = data.Bytes(); + if (bytes.size() % sizeof(T) != 0) + { + throw std::runtime_error{"Index buffer byte length is not aligned to its index type"}; + } + + const size_t totalIndexCount = bytes.size() / sizeof(T); + if (firstIndex > totalIndexCount || indexCount > totalIndexCount - firstIndex) + { + throw std::out_of_range{"Primitive expansion index range exceeds the index buffer"}; + } + + constexpr T RestartIndex = std::numeric_limits::max(); + uint64_t expandedCount{}; + uint64_t runLength{}; + for (uint64_t offset = 0; offset < indexCount; ++offset) + { + if (ReadIndex(bytes, static_cast(firstIndex) + static_cast(offset)) == RestartIndex) + { + expandedCount += PrimitiveIndexCount(runLength, mode); + runLength = 0; + } + else + { + ++runLength; + } + } + expandedCount += PrimitiveIndexCount(runLength, mode); + + Result result{}; + result.Index32 = std::is_same_v; + result.IndexCount = CheckedIndexCount(expandedCount, sizeof(T)); + + std::vector expanded{}; + expanded.reserve(result.IndexCount); + + const auto appendRun = [&](size_t runStart, size_t runEnd) { + const size_t length = runEnd - runStart; + if (mode == Mode::LineLoop) + { + if (length < 2) + { + return; + } + + for (size_t index = runStart; index + 1 < runEnd; ++index) + { + expanded.emplace_back(ReadIndex(bytes, index)); + expanded.emplace_back(ReadIndex(bytes, index + 1)); + } + expanded.emplace_back(ReadIndex(bytes, runEnd - 1)); + expanded.emplace_back(ReadIndex(bytes, runStart)); + } + else if (mode == Mode::TriangleFan) + { + if (length < 3) + { + return; + } + + const T root = ReadIndex(bytes, runStart); + for (size_t index = runStart + 1; index + 1 < runEnd; ++index) + { + expanded.emplace_back(root); + expanded.emplace_back(ReadIndex(bytes, index)); + expanded.emplace_back(ReadIndex(bytes, index + 1)); + } + } + else + { + throw std::invalid_argument{"Unsupported primitive expansion mode"}; + } + }; + + size_t runStart = firstIndex; + const size_t rangeEnd = static_cast(firstIndex) + indexCount; + for (size_t index = firstIndex; index < rangeEnd; ++index) + { + if (ReadIndex(bytes, index) == RestartIndex) + { + appendRun(runStart, index); + runStart = index + 1; + } + } + appendRun(runStart, rangeEnd); + + result.Bytes.resize(expanded.size() * sizeof(T)); + if (!expanded.empty()) + { + std::memcpy(result.Bytes.data(), expanded.data(), result.Bytes.size()); + } + return result; + } + + template + Result ExpandUnindexedTyped(uint32_t vertexCount, Mode mode) + { + const uint64_t expandedCount = PrimitiveIndexCount(vertexCount, mode); + + Result result{}; + result.Index32 = std::is_same_v; + result.IndexCount = CheckedIndexCount(expandedCount, sizeof(T)); + + std::vector expanded{}; + expanded.reserve(result.IndexCount); + if (mode == Mode::LineLoop && vertexCount >= 2) + { + for (uint32_t index = 0; index + 1 < vertexCount; ++index) + { + expanded.emplace_back(static_cast(index)); + expanded.emplace_back(static_cast(index + 1)); + } + expanded.emplace_back(static_cast(vertexCount - 1)); + expanded.emplace_back(T{}); + } + else if (mode == Mode::TriangleFan && vertexCount >= 3) + { + for (uint32_t index = 1; index + 1 < vertexCount; ++index) + { + expanded.emplace_back(T{}); + expanded.emplace_back(static_cast(index)); + expanded.emplace_back(static_cast(index + 1)); + } + } + + result.Bytes.resize(expanded.size() * sizeof(T)); + if (!expanded.empty()) + { + std::memcpy(result.Bytes.data(), expanded.data(), result.Bytes.size()); + } + return result; + } + } + + IndexData::IndexData(gsl::span bytes, bool index32) + : m_index32{index32} + { + if (!bytes.empty()) + { + m_bytes.assign(bytes.begin(), bytes.end()); + } + } + + void IndexData::Update(gsl::span bytes, uint32_t startIndex) + { + const size_t byteStride = m_index32 ? sizeof(uint32_t) : sizeof(uint16_t); + if (startIndex > std::numeric_limits::max() / byteStride) + { + throw std::out_of_range{"Failed to update index buffer: buffer overflow"}; + } + + const size_t byteOffset = static_cast(startIndex) * byteStride; + if (byteOffset > m_bytes.size() || bytes.size() > m_bytes.size() - byteOffset) + { + throw std::out_of_range{"Failed to update index buffer: buffer overflow"}; + } + + if (bytes.empty()) + { + return; + } + + std::memcpy(m_bytes.data() + byteOffset, bytes.data(), bytes.size()); + ++m_revision; + } + + void IndexData::Clear() + { + m_bytes.clear(); + ++m_revision; + } + + gsl::span IndexData::Bytes() const + { + return gsl::span{m_bytes.data(), m_bytes.size()}; + } + + bool IndexData::Is32Bit() const + { + return m_index32; + } + + uint64_t IndexData::Revision() const + { + return m_revision; + } + + Result ExpandIndexed(const IndexData& data, uint32_t firstIndex, uint32_t indexCount, Mode mode) + { + return data.Is32Bit() + ? ExpandIndexedTyped(data, firstIndex, indexCount, mode) + : ExpandIndexedTyped(data, firstIndex, indexCount, mode); + } + + Result ExpandUnindexed(uint32_t vertexCount, Mode mode) + { + return vertexCount <= std::numeric_limits::max() + ? ExpandUnindexedTyped(vertexCount, mode) + : ExpandUnindexedTyped(vertexCount, mode); + } +} diff --git a/Plugins/NativeEngine/Source/PrimitiveModeExpansion.h b/Plugins/NativeEngine/Source/PrimitiveModeExpansion.h new file mode 100644 index 0000000000..3b816a051f --- /dev/null +++ b/Plugins/NativeEngine/Source/PrimitiveModeExpansion.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include + +namespace Babylon +{ + namespace PrimitiveModeExpansion + { + enum class Mode : uint32_t + { + LineLoop = 5, + TriangleFan = 8, + }; + + class IndexData final + { + public: + IndexData(gsl::span bytes, bool index32); + + void Update(gsl::span bytes, uint32_t startIndex); + void Clear(); + + gsl::span Bytes() const; + bool Is32Bit() const; + uint64_t Revision() const; + + private: + std::vector m_bytes{}; + bool m_index32{}; + uint64_t m_revision{}; + }; + + struct Result final + { + std::vector Bytes{}; + bool Index32{}; + uint32_t IndexCount{}; + }; + + Result ExpandIndexed(const IndexData& data, uint32_t firstIndex, uint32_t indexCount, Mode mode); + Result ExpandUnindexed(uint32_t vertexCount, Mode mode); + } +} diff --git a/Plugins/NativeEngine/Source/Program.cpp b/Plugins/NativeEngine/Source/Program.cpp index 68867766ee..9a942a957f 100644 --- a/Plugins/NativeEngine/Source/Program.cpp +++ b/Plugins/NativeEngine/Source/Program.cpp @@ -10,8 +10,10 @@ namespace void InitUniformInfos( bgfx::ShaderHandle shader, const std::map& uniformStages, + const std::map& uniformNames, std::map& uniformInfos, - std::map& uniformNameToIndex) + std::map& uniformNameToIndex, + std::map& samplerStates) { uint16_t numUniforms = bgfx::getShaderUniforms(shader); std::vector uniforms{numUniforms}; @@ -25,7 +27,19 @@ namespace auto itStage = uniformStages.find(info.name); auto& handle = uniforms[index]; uniformInfos.emplace(std::make_pair(handle.idx, Babylon::UniformInfo{itStage == uniformStages.end() ? uint8_t{} : itStage->second, handle, info.type, info.num})); - uniformNameToIndex[info.name] = handleIndex; + const auto originalName = uniformNames.find(info.name); + const std::string_view compiledName{info.name}; + const std::string_view statePrefix{Babylon::Graphics::SAMPLER_STATE_UNIFORM_PREFIX}; + if (originalName == uniformNames.end() && compiledName.starts_with(statePrefix)) + { + const auto sampler = uniformStages.find(std::string{compiledName.substr(statePrefix.size())}); + if (sampler != uniformStages.end()) + { + samplerStates.emplace(sampler->second, handle); + continue; + } + } + uniformNameToIndex[originalName == uniformNames.end() ? info.name : originalName->second] = handleIndex; } } @@ -60,10 +74,10 @@ namespace Babylon arcana::trace_region region{"Program::Initialize"}; auto vertexShader = CreateShader(shaderInfo, shaderInfo->VertexBytes); - InitUniformInfos(vertexShader, shaderInfo->UniformStages, m_uniformInfos, m_uniformNameToIndex); + InitUniformInfos(vertexShader, shaderInfo->UniformStages, shaderInfo->UniformNames, m_uniformInfos, m_uniformNameToIndex, m_samplerStateUniforms); auto fragmentShader = CreateShader(shaderInfo, shaderInfo->FragmentBytes); - InitUniformInfos(fragmentShader, shaderInfo->UniformStages, m_uniformInfos, m_uniformNameToIndex); + InitUniformInfos(fragmentShader, shaderInfo->UniformStages, shaderInfo->UniformNames, m_uniformInfos, m_uniformNameToIndex, m_samplerStateUniforms); m_handle = bgfx::createProgram(vertexShader, fragmentShader, true); m_vertexAttributeLocations = shaderInfo->VertexAttributeLocations; @@ -104,8 +118,32 @@ namespace Babylon } } - // Cached because DrawInternal consults it on every draw. - m_fragCoordTargetSizeUniform = GetUniformInfo(Graphics::FRAGCOORD_TARGET_SIZE_UNIFORM_NAME); + // Cached rather than looked up per draw: DrawInternal consults this on every single draw, + // and m_uniformInfos is stable for the lifetime of the program. + m_renderTargetTransformUniform = GetUniformInfo(Graphics::RENDER_TARGET_TRANSFORM_UNIFORM_NAME); + } + + void Program::InitializeCompute(std::shared_ptr shaderInfo) + { + arcana::trace_region region{"Program::InitializeCompute"}; + + auto computeShader = CreateShader(shaderInfo, shaderInfo->ComputeBytes); + InitUniformInfos(computeShader, shaderInfo->UniformStages, shaderInfo->UniformNames, m_uniformInfos, m_uniformNameToIndex, m_samplerStateUniforms); + + m_handle = bgfx::createProgram(computeShader, true); + } + + void Program::InitializeTransformFeedback(std::shared_ptr shaderInfo) + { + InitializeCompute(shaderInfo); + m_vertexAttributeLocations = shaderInfo->VertexAttributeLocations; + m_transformFeedbackParams = GetUniformInfo(Graphics::TRANSFORM_FEEDBACK_PARAMS_UNIFORM); + m_transformFeedbackInputLayout = GetUniformInfo(Graphics::TRANSFORM_FEEDBACK_INPUT_LAYOUT_UNIFORM); + if (m_transformFeedbackParams == nullptr) + { + throw std::runtime_error{"Transform feedback program is missing its parameter uniform."}; + } + m_transformFeedbackInputCount = static_cast(m_vertexAttributeLocations.size()); } void Program::SetSources(std::string vertexSource, std::string fragmentSource) @@ -163,8 +201,12 @@ namespace Babylon m_uniforms.clear(); m_uniformNameToIndex.clear(); m_uniformInfos.clear(); + m_samplerStateUniforms.clear(); m_vertexAttributeLocations.clear(); m_builtInInstanceDataSlots.clear(); + m_transformFeedbackParams = nullptr; + m_transformFeedbackInputLayout = nullptr; + m_transformFeedbackInputCount = 0; } void Program::SetUniform(bgfx::UniformHandle handle, gsl::span data, size_t elementLength) diff --git a/Plugins/NativeEngine/Source/Program.h b/Plugins/NativeEngine/Source/Program.h index 669c36b76f..dfad7c8b8b 100644 --- a/Plugins/NativeEngine/Source/Program.h +++ b/Plugins/NativeEngine/Source/Program.h @@ -53,6 +53,19 @@ namespace Babylon ~Program(); void Initialize(std::shared_ptr shaderInfo); + + // Creates a bgfx compute program (single CSH shader) from shaderInfo->ComputeBytes. + void InitializeCompute(std::shared_ptr shaderInfo); + + // Creates the compute program that emulates a transform feedback vertex shader (see + // ShaderCompiler::CompileTransformFeedback). Vertex inputs keep their reported locations. + void InitializeTransformFeedback(std::shared_ptr shaderInfo); + bool IsTransformFeedback() const { return m_transformFeedbackParams != nullptr; } + const UniformInfo* TransformFeedbackParams() const { return m_transformFeedbackParams; } + // Null when the shader has no vertex inputs (glslang strips the unused array). + const UniformInfo* TransformFeedbackInputLayout() const { return m_transformFeedbackInputLayout; } + uint32_t TransformFeedbackInputCount() const { return m_transformFeedbackInputCount; } + void Dispose(); // Stores the original GLSL sources so divisor-driven instanced variants can be @@ -69,8 +82,11 @@ namespace Babylon bgfx::ProgramHandle Handle() const { return m_handle; } const std::map& Uniforms() const { return m_uniforms; } const std::map& VertexAttributeLocations() const { return m_vertexAttributeLocations; } - // Null for shaders that never read gl_FragCoord; the compiler omits the uniform there. - const UniformInfo* FragCoordTargetSizeUniform() const { return m_fragCoordTargetSizeUniform; } + // The render target orientation uniform the shader compiler injects on top-left-origin + // backends (see RENDER_TARGET_TRANSFORM_UNIFORM_NAME). Null on OpenGL and for compute. + // Resolved once at initialization because it is consulted on every draw. + const UniformInfo* RenderTargetTransformUniform() const { return m_renderTargetTransformUniform; } + const std::map& SamplerStateUniforms() const { return m_samplerStateUniforms; } // Compiler-assigned i_data slot for each built-in attribute location. const std::map& BuiltInInstanceDataSlots() const { return m_builtInInstanceDataSlots; } @@ -83,10 +99,14 @@ namespace Babylon std::map m_uniformNameToIndex; std::map m_uniformInfos; std::map m_vertexAttributeLocations; + const UniformInfo* m_renderTargetTransformUniform{nullptr}; + std::map m_samplerStateUniforms; std::map m_builtInInstanceDataSlots; - const UniformInfo* m_fragCoordTargetSizeUniform{nullptr}; std::string m_vertexSource; std::string m_fragmentSource; std::map, bgfx::ProgramHandle> m_instancedVariants; + const UniformInfo* m_transformFeedbackParams{nullptr}; + const UniformInfo* m_transformFeedbackInputLayout{nullptr}; + uint32_t m_transformFeedbackInputCount{}; }; } diff --git a/Plugins/NativeEngine/Source/ShaderProvider.cpp b/Plugins/NativeEngine/Source/ShaderProvider.cpp index bef621beab..90bfa9f26b 100644 --- a/Plugins/NativeEngine/Source/ShaderProvider.cpp +++ b/Plugins/NativeEngine/Source/ShaderProvider.cpp @@ -31,18 +31,13 @@ namespace namespace Babylon { - std::shared_ptr ShaderProvider::Get(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) + std::shared_ptr ShaderProvider::Get(std::string_view vertexSource, std::string_view fragmentSource, [[maybe_unused]] const std::map& instancedAttributes) { - // The shader cache is keyed only by source, so it must be bypassed when routing instanced - // attributes (otherwise a variant would collide with the base program's cached shader). - // Only referenced inside the SHADER_CACHE blocks below, so mark it maybe_unused for builds - // that compile with the ShaderCache plugin disabled. - [[maybe_unused]] const bool useCache = instancedAttributes.empty(); - #ifdef SHADER_CACHE - if (useCache && Plugins::ShaderCache::IsEnabled()) + // Instanced variants are keyed by their routed attributes as well as the sources. + if (Plugins::ShaderCache::IsEnabled()) { - const auto shaderInfo = Plugins::ShaderCache::GetShader(vertexSource, fragmentSource); + const auto shaderInfo = Plugins::ShaderCache::GetShader(vertexSource, fragmentSource, instancedAttributes); if (shaderInfo) { return shaderInfo; @@ -54,16 +49,55 @@ namespace Babylon CheckShaderCompilerAssumptions(); #ifdef SHADER_CACHE - if (useCache && Plugins::ShaderCache::IsEnabled()) + if (Plugins::ShaderCache::IsEnabled()) { auto compiledShaderInfo = m_shaderCompiler.Compile(vertexSource, fragmentSource, instancedAttributes); - return Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, compiledShaderInfo); + return Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, compiledShaderInfo, instancedAttributes); } #endif return std::make_shared(m_shaderCompiler.Compile(vertexSource, fragmentSource, instancedAttributes)); #else throw std::runtime_error{"Shader compiler is not available"}; +#endif + } + + std::shared_ptr ShaderProvider::GetCompute([[maybe_unused]] std::string_view computeSource) + { +#ifdef SHADER_COMPILER + CheckShaderCompilerAssumptions(); + return std::make_shared(m_shaderCompiler.CompileCompute(computeSource)); +#else + throw std::runtime_error{"Shader compiler is not available"}; +#endif + } + + std::shared_ptr ShaderProvider::GetTransformFeedback([[maybe_unused]] std::string_view vertexSource, [[maybe_unused]] const std::vector& varyings) + { +#ifdef SHADER_CACHE + if (Plugins::ShaderCache::IsEnabled()) + { + const auto shaderInfo = Plugins::ShaderCache::GetTransformFeedbackShader(vertexSource, varyings); + if (shaderInfo) + { + return shaderInfo; + } + } +#endif + +#ifdef SHADER_COMPILER + CheckShaderCompilerAssumptions(); + +#ifdef SHADER_CACHE + if (Plugins::ShaderCache::IsEnabled()) + { + return Plugins::ShaderCache::AddTransformFeedbackShader(vertexSource, varyings, m_shaderCompiler.CompileTransformFeedback(vertexSource, varyings)); + } +#endif + + return std::make_shared(m_shaderCompiler.CompileTransformFeedback(vertexSource, varyings)); +#else + throw std::runtime_error{"Shader compiler is not available"}; #endif } } diff --git a/Plugins/NativeEngine/Source/ShaderProvider.h b/Plugins/NativeEngine/Source/ShaderProvider.h index 6dd2c4924e..090a6c57b5 100644 --- a/Plugins/NativeEngine/Source/ShaderProvider.h +++ b/Plugins/NativeEngine/Source/ShaderProvider.h @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef SHADER_COMPILER #include @@ -19,6 +20,12 @@ namespace Babylon public: std::shared_ptr Get(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes = {}); + // Compiles a single GLSL compute shader into a bgfx CSH shader binary. + std::shared_ptr GetCompute(std::string_view computeSource); + + // Compiles a transform feedback vertex shader into its emulating compute shader. + std::shared_ptr GetTransformFeedback(std::string_view vertexSource, const std::vector& varyings); + private: #ifdef SHADER_COMPILER Plugins::ShaderCompiler m_shaderCompiler{}; diff --git a/Plugins/NativeEngine/Source/Shaders/cs_instance_repack.sc b/Plugins/NativeEngine/Source/Shaders/cs_instance_repack.sc new file mode 100644 index 0000000000..6fc3090e9f --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/cs_instance_repack.sc @@ -0,0 +1,31 @@ +#include + +BUFFER_RAW_RO(params, 0); +BUFFER_RAW_RO(source, 1); +BUFFER_RAW_WO(destination, 2); + +NUM_THREADS(64, 1, 1) +void main() +{ + uint instance = gl_GlobalInvocationID.x; + if (instance >= rawLoadUint(params, 0)) + { + return; + } + + uint attributeCount = rawLoadUint(params, 1); + uint destinationStride = rawLoadUint(params, 2); + for (uint attr = 0u; attr < attributeCount; ++attr) + { + uint base = 4u + attr * 4u; + uint sourceOffset = rawLoadUint(params, base); + uint sourceStride = rawLoadUint(params, base + 1u); + uint wordCount = rawLoadUint(params, base + 2u); + uint destinationOffset = rawLoadUint(params, base + 3u); + for (uint word = 0u; word < wordCount; ++word) + { + rawStoreUint(destination, destinationOffset + instance * destinationStride + word, + rawLoadUint(source, sourceOffset + instance * sourceStride + word)); + } + } +} diff --git a/Plugins/NativeEngine/Source/Shaders/dxbc/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/dxbc/cs_instance_repack.h new file mode 100644 index 0000000000..d5e7fe77d4 --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/dxbc/cs_instance_repack.h @@ -0,0 +1,56 @@ +static const uint8_t cs_instance_repack_dxbc[838] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, // CSH............. + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x44, 0x58, 0x42, 0x43, 0xa7, 0x1f, // ......(...DXBC.. + 0x9a, 0xb1, 0xe7, 0xfe, 0x8b, 0x9c, 0x1f, 0x2c, 0x4d, 0xf8, 0x50, 0xca, 0xe8, 0x06, 0x01, 0x00, // .......,M.P..... + 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x3c, 0x00, // ..(.......,...<. + 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x49, 0x53, 0x47, 0x4e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // ..L...ISGN...... + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4f, 0x53, 0x47, 0x4e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // ......OSGN...... + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x53, 0x48, 0x45, 0x58, 0xd4, 0x02, 0x00, 0x00, 0x50, 0x00, // ......SHEX....P. + 0x05, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x6a, 0x08, 0x00, 0x01, 0xa1, 0x00, 0x00, 0x03, 0x00, 0x70, // ......j........p + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x03, 0x00, 0x70, 0x10, 0x00, 0x01, 0x00, // ...........p.... + 0x00, 0x00, 0x9d, 0x00, 0x00, 0x03, 0x00, 0xe0, 0x11, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5f, 0x00, // .............._. + 0x00, 0x02, 0x12, 0x00, 0x02, 0x00, 0x68, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x9b, 0x00, // ......h......... + 0x00, 0x04, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa5, 0x00, // ..@............. + 0x00, 0x89, 0xc2, 0x02, 0x00, 0x80, 0x83, 0x99, 0x19, 0x00, 0x12, 0x00, 0x10, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x70, 0x10, 0x00, 0x00, 0x00, // ...@.......p.... + 0x00, 0x00, 0x50, 0x00, 0x00, 0x06, 0x12, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, // ..P............. + 0x02, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x04, 0x03, 0x0a, 0x00, // ................ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x01, 0x15, 0x00, 0x00, 0x01, 0xa5, 0x00, // ......>......... + 0x00, 0x89, 0xc2, 0x02, 0x00, 0x80, 0x83, 0x99, 0x19, 0x00, 0x32, 0x00, 0x10, 0x00, 0x00, 0x00, // ..........2..... + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x46, 0x70, 0x10, 0x00, 0x00, 0x00, // ...@......Fp.... + 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x42, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, // ..6...B........@ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x07, 0x82, 0x00, // ......0...P..... + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, // ......*......... + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x03, 0x3a, 0x00, 0x10, 0x00, 0x00, 0x00, // ..........:..... + 0x00, 0x00, 0x29, 0x00, 0x00, 0x07, 0x82, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, // ..)...........*. + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, // .......@........ + 0x00, 0x07, 0x82, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x00, 0x00, // ..........:..... + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x89, 0xc2, 0x02, // ...@............ + 0x00, 0x80, 0x83, 0x99, 0x19, 0x00, 0xf2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3a, 0x00, // ..............:. + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x7e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, // ......F~......#. + 0x00, 0x08, 0x82, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x02, 0x00, 0x1a, 0x00, // ................ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, // ......:.......#. + 0x00, 0x08, 0x12, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x02, 0x00, 0x1a, 0x00, // ................ + 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x36, 0x00, // ..............6. + 0x00, 0x05, 0x22, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, // .."........@.... + 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x07, 0x82, 0x00, 0x10, 0x00, 0x01, 0x00, // ..0...P......... + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x10, 0x00, 0x01, 0x00, // ..........*..... + 0x00, 0x00, 0x03, 0x00, 0x04, 0x03, 0x3a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, // ......:......... + 0x00, 0x07, 0x82, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x00, 0x00, // ..........:..... + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x07, 0x82, 0x00, // ..........)..... + 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x40, // ......:........@ + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x07, 0x12, 0x00, 0x10, 0x00, 0x02, 0x00, // ................ + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, // ................ + 0x00, 0x00, 0x29, 0x00, 0x00, 0x07, 0x12, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x00, // ..)............. + 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xa5, 0x00, // .......@........ + 0x00, 0x89, 0xc2, 0x02, 0x00, 0x80, 0x83, 0x99, 0x19, 0x00, 0x12, 0x00, 0x10, 0x00, 0x02, 0x00, // ................ + 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x70, 0x10, 0x00, 0x01, 0x00, // ...........p.... + 0x00, 0x00, 0xa6, 0x00, 0x00, 0x07, 0x12, 0xe0, 0x11, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3a, 0x00, // ..............:. + 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, // ................ + 0x00, 0x07, 0x22, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, 0x00, // .."............. + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x01, 0x1e, 0x00, // ...@............ + 0x00, 0x07, 0x42, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, // ..B.......*..... + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x01, 0x3e, 0x00, // ...@..........>. + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // ...... +}; diff --git a/Plugins/NativeEngine/Source/Shaders/dxil/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/dxil/cs_instance_repack.h new file mode 100644 index 0000000000..0cb7db5731 --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/dxil/cs_instance_repack.h @@ -0,0 +1,218 @@ +static const uint8_t cs_instance_repack_dxil[3438] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, // CSH............. + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x0d, 0x00, 0x00, 0x44, 0x58, 0x42, 0x43, 0x6e, 0xca, // ......P...DXBCn. + 0x68, 0x53, 0xbc, 0x49, 0xc0, 0x1c, 0xce, 0x84, 0xc4, 0x04, 0x98, 0xc0, 0x00, 0x56, 0x01, 0x00, // hS.I.........V.. + 0x00, 0x00, 0x50, 0x0d, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x4c, 0x00, // ..P.......<...L. + 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0xc0, 0x06, // ......l......... + 0x00, 0x00, 0xdc, 0x06, 0x00, 0x00, 0x53, 0x46, 0x49, 0x30, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // ......SFI0...... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x53, 0x47, 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // ......ISG1...... + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4f, 0x53, 0x47, 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // ......OSG1...... + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x50, 0x53, 0x56, 0x30, 0x9c, 0x00, 0x00, 0x00, 0x38, 0x00, // ......PSV0....8. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, // ......@......... + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, 0x00, // ................ + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, // ................ + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x6d, // ...............m + 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x54, 0x41, 0x54, 0xa8, 0x05, // ain.......STAT.. + 0x00, 0x00, 0x60, 0x00, 0x05, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x44, 0x58, 0x49, 0x4c, 0x00, 0x01, // ..`...j...DXIL.. + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0x42, 0x43, 0xc0, 0xde, 0x21, 0x0c, // ..........BC..!. + 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x0b, 0x82, 0x20, 0x00, 0x02, 0x00, 0x00, 0x00, 0x13, 0x00, // ..a..... ....... + 0x00, 0x00, 0x07, 0x81, 0x23, 0x91, 0x41, 0xc8, 0x04, 0x49, 0x06, 0x10, 0x32, 0x39, 0x92, 0x01, // ....#.A..I..29.. + 0x84, 0x0c, 0x25, 0x05, 0x08, 0x19, 0x1e, 0x04, 0x8b, 0x62, 0x80, 0x14, 0x45, 0x02, 0x42, 0x92, // ..%......b..E.B. + 0x0b, 0x42, 0xa4, 0x10, 0x32, 0x14, 0x38, 0x08, 0x18, 0x4b, 0x0a, 0x32, 0x52, 0x88, 0x48, 0x90, // .B..2.8..K.2R.H. + 0x14, 0x20, 0x43, 0x46, 0x88, 0xa5, 0x00, 0x19, 0x32, 0x42, 0xe4, 0x48, 0x0e, 0x90, 0x91, 0x22, // . CF....2B.H..." + 0xc4, 0x50, 0x41, 0x51, 0x81, 0x8c, 0xe1, 0x83, 0xe5, 0x8a, 0x04, 0x29, 0x46, 0x06, 0x51, 0x18, // .PAQ.......)F.Q. + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1b, 0x8c, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x07, 0x40, 0x02, // ..............@. + 0xa8, 0x0d, 0x86, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x03, 0x20, 0x01, 0xd5, 0x06, 0x62, 0xf8, 0xff, // ......... ...b.. + 0xff, 0xff, 0xff, 0x01, 0x90, 0x00, 0x49, 0x18, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x13, 0x82, // ......I......... + 0x60, 0x42, 0x20, 0x4c, 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x89, 0x20, 0x00, 0x00, 0x28, 0x00, // `B L....... ..(. + 0x00, 0x00, 0x32, 0x22, 0x48, 0x09, 0x20, 0x64, 0x85, 0x04, 0x93, 0x22, 0xa4, 0x84, 0x04, 0x93, // ..2"H. d...".... + 0x22, 0xe3, 0x84, 0xa1, 0x90, 0x14, 0x12, 0x4c, 0x8a, 0x8c, 0x0b, 0x84, 0xa4, 0x4c, 0x10, 0x5c, // "......L.....L.. + 0x23, 0x00, 0x25, 0x00, 0x14, 0xe6, 0x08, 0xc0, 0xa0, 0x0c, 0x63, 0x0c, 0x22, 0x73, 0x04, 0x08, // #.%.......c."s.. + 0x99, 0x7b, 0x86, 0xcb, 0x9f, 0xb0, 0x87, 0x90, 0xfc, 0x10, 0x68, 0x86, 0x85, 0x40, 0xc1, 0x99, // .{........h..@.. + 0x23, 0x08, 0x8a, 0x81, 0x86, 0x19, 0x23, 0x91, 0xba, 0x69, 0xb8, 0xfc, 0x09, 0x7b, 0x08, 0xc9, // #.....#..i...{.. + 0x5f, 0x09, 0x69, 0x25, 0x26, 0x1f, 0xb9, 0x6d, 0x54, 0x8c, 0x31, 0xc6, 0x28, 0x05, 0x1b, 0x68, // _.i%&..mT.1.(..h + 0x0c, 0x6a, 0x45, 0x01, 0x03, 0x8d, 0x31, 0xc6, 0x18, 0x86, 0xde, 0x40, 0xc0, 0x61, 0xd2, 0x14, // .jE...1....@.a.. + 0x51, 0xc2, 0xe4, 0x6f, 0xd8, 0x44, 0x68, 0xc3, 0x10, 0x11, 0x92, 0xb4, 0x51, 0x45, 0x41, 0x44, // Q..o.Dh.....QEAD + 0x28, 0x18, 0x24, 0x4f, 0x93, 0xa6, 0x88, 0x12, 0x26, 0x7f, 0x85, 0x37, 0x6c, 0x22, 0xb4, 0x61, // (.$O....&..7l".a + 0x88, 0x08, 0x49, 0xda, 0xa8, 0xa2, 0x20, 0x22, 0x14, 0x0c, 0xa2, 0x73, 0x04, 0xa0, 0x00, 0x00, // ..I... "...s.... + 0x00, 0x00, 0x13, 0x14, 0x72, 0xc0, 0x87, 0x74, 0x60, 0x87, 0x36, 0x68, 0x87, 0x79, 0x68, 0x03, // ....r..t`.6h.yh. + 0x72, 0xc0, 0x87, 0x0d, 0xaf, 0x50, 0x0e, 0x6d, 0xd0, 0x0e, 0x7a, 0x50, 0x0e, 0x6d, 0x00, 0x0f, // r....P.m..zP.m.. + 0x7a, 0x30, 0x07, 0x72, 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x71, 0xa0, 0x07, 0x73, // z0.r..s .m..q..s + 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x78, 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x71, 0x60, // .m..x..s .m..q` + 0x07, 0x7a, 0x30, 0x07, 0x72, 0xd0, 0x06, 0xe9, 0x30, 0x07, 0x72, 0xa0, 0x07, 0x73, 0x20, 0x07, // .z0.r...0.r..s . + 0x6d, 0x90, 0x0e, 0x76, 0x40, 0x07, 0x7a, 0x60, 0x07, 0x74, 0xd0, 0x06, 0xe6, 0x10, 0x07, 0x76, // m..v@.z`.t.....v + 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x60, 0x0e, 0x73, 0x20, 0x07, 0x7a, 0x30, 0x07, 0x72, 0xd0, // ..s .m`.s .z0.r. + 0x06, 0xe6, 0x60, 0x07, 0x74, 0xa0, 0x07, 0x76, 0x40, 0x07, 0x6d, 0xe0, 0x0e, 0x78, 0xa0, 0x07, // ..`.t..v@.m..x.. + 0x71, 0x60, 0x07, 0x7a, 0x30, 0x07, 0x72, 0xa0, 0x07, 0x76, 0x40, 0x07, 0x43, 0x9e, 0x00, 0x08, // q`.z0.r..v@.C... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x3c, 0x04, 0x10, 0x00, 0x01, 0x00, // ..........<..... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x79, 0x14, 0x20, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // .......y. ...... + 0x00, 0x00, 0x00, 0x18, 0xf2, 0x34, 0x40, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .....4@......... + 0x30, 0xe4, 0x79, 0x80, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0b, 0x04, // 0.y.......... .. + 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x32, 0x1e, 0x98, 0x14, 0x19, 0x11, 0x4c, 0x90, 0x8c, 0x09, // ......2.....L... + 0x26, 0x47, 0xc6, 0x04, 0x43, 0x1a, 0x25, 0x30, 0x02, 0x50, 0x0c, 0x65, 0x51, 0x04, 0x85, 0x50, // &G..C.%0.P.eQ..P + 0x80, 0x01, 0xa5, 0x50, 0x6a, 0x25, 0x52, 0x10, 0x65, 0x40, 0x69, 0x04, 0x80, 0x6c, 0x81, 0x50, // ...Pj%R.e@i..l.P + 0x9d, 0x01, 0xa0, 0x39, 0x03, 0x00, 0x79, 0x18, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x1a, 0x03, // ...9..y...f..... + 0x4c, 0x90, 0x46, 0x02, 0x13, 0xc4, 0x8f, 0x0c, 0x6f, 0xec, 0xed, 0x4d, 0x0c, 0x24, 0xc6, 0x25, // L.F.....o..M.$.% + 0x06, 0xc6, 0x45, 0xc6, 0x06, 0xa6, 0xc6, 0x65, 0xe6, 0x06, 0x04, 0x65, 0x8c, 0x86, 0x0c, 0xc7, // ..E....e...e.... + 0x8c, 0xc6, 0x2c, 0x27, 0x27, 0x65, 0x43, 0x10, 0x4c, 0x10, 0x86, 0x62, 0x82, 0x30, 0x18, 0x1b, // ..,''eC.L..b.0.. + 0x84, 0x81, 0xd8, 0x20, 0x10, 0x04, 0x85, 0xb1, 0xb9, 0x09, 0xc2, 0x70, 0x6c, 0x18, 0x0e, 0x84, // ... .......pl... + 0x98, 0x20, 0x4c, 0x14, 0x0d, 0xb8, 0x30, 0xb9, 0xb0, 0xb6, 0xb9, 0x09, 0xc2, 0x80, 0x6c, 0x48, // . L...0.......lH + 0x08, 0x65, 0x21, 0x88, 0x81, 0x21, 0x00, 0x1a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1b, 0x92, // .e!..!..source.. + 0x41, 0x71, 0x88, 0x61, 0x60, 0x08, 0x60, 0x83, 0xd0, 0x3c, 0x13, 0x84, 0x6a, 0xe2, 0x42, 0x56, // Aq.a`.`..<..j.BV + 0x36, 0x47, 0x97, 0xe6, 0x16, 0x46, 0x97, 0xf6, 0xe6, 0x36, 0x41, 0x18, 0x92, 0x09, 0x42, 0x12, // 6G...F...6A...B. + 0x6d, 0x58, 0x88, 0x48, 0x22, 0xa6, 0x81, 0xa1, 0x28, 0x0a, 0xd8, 0x10, 0x54, 0x1b, 0x08, 0xc8, // mX.H"...(...T... + 0x02, 0x80, 0x09, 0x82, 0x00, 0x6c, 0x00, 0x36, 0x0c, 0x44, 0x96, 0x6d, 0x08, 0xb4, 0x0d, 0xc3, // .....l.6.D.m.... + 0x80, 0x6d, 0x24, 0xda, 0xc2, 0xd2, 0xdc, 0x26, 0x08, 0x96, 0x34, 0x41, 0x18, 0x94, 0x09, 0xc2, // .m$....&..4A.... + 0xb0, 0x6c, 0x18, 0xc0, 0x60, 0x18, 0x36, 0x10, 0x84, 0xf7, 0x85, 0xc1, 0x86, 0x02, 0xeb, 0x80, // .l..`.6......... + 0x4b, 0x0c, 0x88, 0x88, 0xc9, 0x85, 0xb9, 0x8d, 0xa1, 0x95, 0xcd, 0x4d, 0x10, 0x06, 0x86, 0x45, // K..........M...E + 0x9a, 0xdb, 0x1c, 0xdd, 0xdc, 0x04, 0x61, 0x68, 0x48, 0xa4, 0xb9, 0xd1, 0xcd, 0x4d, 0x10, 0x06, // ......ahH....M.. + 0x87, 0x08, 0x5d, 0x19, 0xde, 0x17, 0xdb, 0x5b, 0x18, 0xd9, 0x04, 0x61, 0x78, 0x98, 0xd0, 0x95, // ..]....[...ax... + 0xe1, 0x7d, 0xcd, 0xd1, 0xbd, 0xc9, 0x95, 0xb1, 0xa8, 0x4b, 0x73, 0xa3, 0x9b, 0x9b, 0x20, 0x0c, // .}.......Ks... . + 0xd0, 0x06, 0x86, 0x0c, 0xca, 0xc0, 0x0c, 0xce, 0x00, 0x0d, 0xd2, 0x40, 0x0d, 0xd6, 0x80, 0x0d, // ...........@.... + 0x86, 0x36, 0x70, 0x83, 0x2a, 0x6c, 0x6c, 0x76, 0x6d, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, // .6p.*llvm.identS + 0x82, 0xa0, 0x0a, 0x19, 0x9e, 0x8b, 0x5d, 0x99, 0xdc, 0x5c, 0xda, 0x9b, 0xdb, 0x94, 0x80, 0x68, // ......]........h + 0x42, 0x86, 0xe7, 0x62, 0x17, 0xc6, 0x66, 0x57, 0x26, 0x37, 0x25, 0x28, 0xea, 0x90, 0xe1, 0xb9, // B..b..fW&7%(.... + 0xcc, 0xa1, 0x85, 0x91, 0x95, 0xc9, 0x35, 0xbd, 0x91, 0x95, 0xb1, 0x4d, 0x09, 0x90, 0x32, 0x64, // ......5....M..2d + 0x78, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x53, 0x02, 0xab, 0x12, 0x19, // x.resourcesS.... + 0x9e, 0x0b, 0x5d, 0x1e, 0x5c, 0x59, 0x90, 0x9b, 0xdb, 0x1b, 0x5d, 0x18, 0x5d, 0xda, 0x9b, 0xdb, // ..]..Y....].]... + 0xdc, 0x94, 0x60, 0xab, 0x43, 0x86, 0xe7, 0x52, 0xe6, 0x46, 0x27, 0x97, 0x07, 0xf5, 0x96, 0xe6, // ..`.C..R.F'..... + 0x46, 0x37, 0x37, 0x25, 0x10, 0x83, 0x2e, 0x64, 0x78, 0x2e, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, // F77%...dx.counte + 0x72, 0x73, 0x53, 0x02, 0x37, 0x00, 0x79, 0x18, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x33, 0x08, // rsS.7.y...Q...3. + 0x80, 0x1c, 0xc4, 0xe1, 0x1c, 0x66, 0x14, 0x01, 0x3d, 0x88, 0x43, 0x38, 0x84, 0xc3, 0x8c, 0x42, // .....f..=.C8...B + 0x80, 0x07, 0x79, 0x78, 0x07, 0x73, 0x98, 0x71, 0x0c, 0xe6, 0x00, 0x0f, 0xed, 0x10, 0x0e, 0xf4, // ..yx.s.q........ + 0x80, 0x0e, 0x33, 0x0c, 0x42, 0x1e, 0xc2, 0xc1, 0x1d, 0xce, 0xa1, 0x1c, 0x66, 0x30, 0x05, 0x3d, // ..3.B.......f0.= + 0x88, 0x43, 0x38, 0x84, 0x83, 0x1b, 0xcc, 0x03, 0x3d, 0xc8, 0x43, 0x3d, 0x8c, 0x03, 0x3d, 0xcc, // .C8.....=.C=..=. + 0x78, 0x8c, 0x74, 0x70, 0x07, 0x7b, 0x08, 0x07, 0x79, 0x48, 0x87, 0x70, 0x70, 0x07, 0x7a, 0x70, // x.tp.{..yH.pp.zp + 0x03, 0x76, 0x78, 0x87, 0x70, 0x20, 0x87, 0x19, 0xcc, 0x11, 0x0e, 0xec, 0x90, 0x0e, 0xe1, 0x30, // .vx.p .........0 + 0x0f, 0x6e, 0x30, 0x0f, 0xe3, 0xf0, 0x0e, 0xf0, 0x50, 0x0e, 0x33, 0x10, 0xc4, 0x1d, 0xde, 0x21, // .n0.....P.3....! + 0x1c, 0xd8, 0x21, 0x1d, 0xc2, 0x61, 0x1e, 0x66, 0x30, 0x89, 0x3b, 0xbc, 0x83, 0x3b, 0xd0, 0x43, // ..!..a.f0.;..;.C + 0x39, 0xb4, 0x03, 0x3c, 0xbc, 0x83, 0x3c, 0x84, 0x03, 0x3b, 0xcc, 0xf0, 0x14, 0x76, 0x60, 0x07, // 9..<..<..;...v`. + 0x7b, 0x68, 0x07, 0x37, 0x68, 0x87, 0x72, 0x68, 0x07, 0x37, 0x80, 0x87, 0x70, 0x90, 0x87, 0x70, // {h.7h.rh.7..p..p + 0x60, 0x07, 0x76, 0x28, 0x07, 0x76, 0xf8, 0x05, 0x76, 0x78, 0x87, 0x77, 0x80, 0x87, 0x5f, 0x08, // `.v(.v..vx.w.._. + 0x87, 0x71, 0x18, 0x87, 0x72, 0x98, 0x87, 0x79, 0x98, 0x81, 0x2c, 0xee, 0xf0, 0x0e, 0xee, 0xe0, // .q..r..y..,..... + 0x0e, 0xf5, 0xc0, 0x0e, 0xec, 0x30, 0x03, 0x62, 0xc8, 0xa1, 0x1c, 0xe4, 0xa1, 0x1c, 0xcc, 0xa1, // .....0.b........ + 0x1c, 0xe4, 0xa1, 0x1c, 0xdc, 0x61, 0x1c, 0xca, 0x21, 0x1c, 0xc4, 0x81, 0x1d, 0xca, 0x61, 0x06, // .....a..!.....a. + 0xd6, 0x90, 0x43, 0x39, 0xc8, 0x43, 0x39, 0x98, 0x43, 0x39, 0xc8, 0x43, 0x39, 0xb8, 0xc3, 0x38, // ..C9.C9.C9.C9..8 + 0x94, 0x43, 0x38, 0x88, 0x03, 0x3b, 0x94, 0xc3, 0x2f, 0xbc, 0x83, 0x3c, 0xfc, 0x82, 0x3b, 0xd4, // .C8..;../..<..;. + 0x03, 0x3b, 0xb0, 0xc3, 0x0c, 0xc4, 0x21, 0x07, 0x7c, 0x70, 0x03, 0x7a, 0x28, 0x87, 0x76, 0x80, // .;....!.|p.z(.v. + 0x87, 0x19, 0xd1, 0x43, 0x0e, 0xf8, 0xe0, 0x06, 0xe4, 0x20, 0x0e, 0xe7, 0xe0, 0x06, 0xf6, 0x10, // ...C..... ...... + 0x0e, 0xf2, 0xc0, 0x0e, 0xe1, 0x90, 0x0f, 0xef, 0x50, 0x0f, 0xf4, 0x30, 0x83, 0x81, 0xc8, 0x01, // ........P..0.... + 0x1f, 0xdc, 0x40, 0x1c, 0xe4, 0xa1, 0x1c, 0xc2, 0x61, 0x1d, 0xdc, 0x40, 0x1c, 0xe4, 0x01, 0x00, // ..@.....a..@.... + 0x00, 0x00, 0x71, 0x20, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x40, 0x0d, 0x97, 0xef, 0x3c, // ..q ......6@...< + 0x7e, 0x40, 0x15, 0x05, 0x11, 0x95, 0x0e, 0x30, 0xf8, 0xc8, 0x6d, 0x1b, 0x41, 0x35, 0x5c, 0xbe, // ~@.....0..m.A5.. + 0xf3, 0xf8, 0x01, 0x55, 0x14, 0x44, 0xc4, 0x4e, 0x4e, 0x44, 0xf8, 0xc8, 0x6d, 0x9b, 0x80, 0x34, // ...U.D.NND..m..4 + 0x5c, 0xbe, 0xf3, 0xf8, 0x42, 0x44, 0x00, 0x13, 0x11, 0x02, 0xcd, 0xb0, 0x10, 0x16, 0x20, 0x0d, // ....BD........ . + 0x97, 0xef, 0x3c, 0xfe, 0x74, 0x44, 0x04, 0x30, 0x88, 0x83, 0x8f, 0xdc, 0xb6, 0x01, 0x10, 0x0c, // ..<.tD.0........ + 0x80, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x41, 0x53, 0x48, 0x14, 0x00, // .4........HASH.. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xc9, 0xb8, 0x17, 0xcf, 0x41, 0xbc, 0x7d, 0x44, 0xf3, // ......x....A.}D. + 0x89, 0xf2, 0x03, 0xd6, 0xa6, 0xba, 0x44, 0x58, 0x49, 0x4c, 0x6c, 0x06, 0x00, 0x00, 0x60, 0x00, // ......DXILl...`. + 0x05, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x44, 0x58, 0x49, 0x4c, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // ......DXIL...... + 0x00, 0x00, 0x54, 0x06, 0x00, 0x00, 0x42, 0x43, 0xc0, 0xde, 0x21, 0x0c, 0x00, 0x00, 0x92, 0x01, // ..T...BC..!..... + 0x00, 0x00, 0x0b, 0x82, 0x20, 0x00, 0x02, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x07, 0x81, // .... ........... + 0x23, 0x91, 0x41, 0xc8, 0x04, 0x49, 0x06, 0x10, 0x32, 0x39, 0x92, 0x01, 0x84, 0x0c, 0x25, 0x05, // #.A..I..29....%. + 0x08, 0x19, 0x1e, 0x04, 0x8b, 0x62, 0x80, 0x14, 0x45, 0x02, 0x42, 0x92, 0x0b, 0x42, 0xa4, 0x10, // .....b..E.B..B.. + 0x32, 0x14, 0x38, 0x08, 0x18, 0x4b, 0x0a, 0x32, 0x52, 0x88, 0x48, 0x90, 0x14, 0x20, 0x43, 0x46, // 2.8..K.2R.H.. CF + 0x88, 0xa5, 0x00, 0x19, 0x32, 0x42, 0xe4, 0x48, 0x0e, 0x90, 0x91, 0x22, 0xc4, 0x50, 0x41, 0x51, // ....2B.H...".PAQ + 0x81, 0x8c, 0xe1, 0x83, 0xe5, 0x8a, 0x04, 0x29, 0x46, 0x06, 0x51, 0x18, 0x00, 0x00, 0x08, 0x00, // .......)F.Q..... + 0x00, 0x00, 0x1b, 0x8c, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x07, 0x40, 0x02, 0xa8, 0x0d, 0x86, 0xf0, // ..........@..... + 0xff, 0xff, 0xff, 0xff, 0x03, 0x20, 0x01, 0xd5, 0x06, 0x62, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x01, // ..... ...b...... + 0x90, 0x00, 0x49, 0x18, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x13, 0x82, 0x60, 0x42, 0x20, 0x4c, // ..I.........`B L + 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x89, 0x20, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x32, 0x22, // ....... ..(...2" + 0x48, 0x09, 0x20, 0x64, 0x85, 0x04, 0x93, 0x22, 0xa4, 0x84, 0x04, 0x93, 0x22, 0xe3, 0x84, 0xa1, // H. d..."...."... + 0x90, 0x14, 0x12, 0x4c, 0x8a, 0x8c, 0x0b, 0x84, 0xa4, 0x4c, 0x10, 0x60, 0x23, 0x00, 0x25, 0x00, // ...L.....L.`#.%. + 0x14, 0xe6, 0x08, 0xc0, 0xa0, 0x0c, 0x63, 0x0c, 0x22, 0x73, 0x04, 0x08, 0x99, 0x7b, 0x86, 0xcb, // ......c."s...{.. + 0x9f, 0xb0, 0x87, 0x90, 0xfc, 0x10, 0x68, 0x86, 0x85, 0x40, 0xc1, 0x99, 0x23, 0x08, 0x8a, 0x81, // ......h..@..#... + 0x86, 0x19, 0x23, 0x91, 0xba, 0x69, 0xb8, 0xfc, 0x09, 0x7b, 0x08, 0xc9, 0x5f, 0x09, 0x69, 0x25, // ..#..i...{.._.i% + 0x26, 0x1f, 0xb9, 0x6d, 0x54, 0x8c, 0x31, 0xc6, 0x28, 0x05, 0x1b, 0x68, 0x0c, 0x6a, 0x45, 0x01, // &..mT.1.(..h.jE. + 0x03, 0x8d, 0x31, 0xc6, 0x18, 0x86, 0xde, 0x40, 0xc0, 0x61, 0xd2, 0x14, 0x51, 0xc2, 0xe4, 0x6f, // ..1....@.a..Q..o + 0xd8, 0x44, 0x68, 0xc3, 0x10, 0x11, 0x92, 0xb4, 0x51, 0x45, 0x41, 0x44, 0x28, 0x18, 0x24, 0x4f, // .Dh.....QEAD(.$O + 0x93, 0xa6, 0x88, 0x12, 0x26, 0x7f, 0x85, 0x37, 0x6c, 0x22, 0xb4, 0x61, 0x88, 0x08, 0x49, 0xda, // ....&..7l".a..I. + 0xa8, 0xa2, 0x20, 0x22, 0x14, 0x0c, 0xa2, 0x73, 0x04, 0xa0, 0x30, 0x05, 0x00, 0x00, 0x13, 0x14, // .. "...s..0..... + 0x72, 0xc0, 0x87, 0x74, 0x60, 0x87, 0x36, 0x68, 0x87, 0x79, 0x68, 0x03, 0x72, 0xc0, 0x87, 0x0d, // r..t`.6h.yh.r... + 0xaf, 0x50, 0x0e, 0x6d, 0xd0, 0x0e, 0x7a, 0x50, 0x0e, 0x6d, 0x00, 0x0f, 0x7a, 0x30, 0x07, 0x72, // .P.m..zP.m..z0.r + 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x71, 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, // ..s .m..q..s .m. + 0x0e, 0x78, 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x71, 0x60, 0x07, 0x7a, 0x30, 0x07, // .x..s .m..q`.z0. + 0x72, 0xd0, 0x06, 0xe9, 0x30, 0x07, 0x72, 0xa0, 0x07, 0x73, 0x20, 0x07, 0x6d, 0x90, 0x0e, 0x76, // r...0.r..s .m..v + 0x40, 0x07, 0x7a, 0x60, 0x07, 0x74, 0xd0, 0x06, 0xe6, 0x10, 0x07, 0x76, 0xa0, 0x07, 0x73, 0x20, // @.z`.t.....v..s + 0x07, 0x6d, 0x60, 0x0e, 0x73, 0x20, 0x07, 0x7a, 0x30, 0x07, 0x72, 0xd0, 0x06, 0xe6, 0x60, 0x07, // .m`.s .z0.r...`. + 0x74, 0xa0, 0x07, 0x76, 0x40, 0x07, 0x6d, 0xe0, 0x0e, 0x78, 0xa0, 0x07, 0x71, 0x60, 0x07, 0x7a, // t..v@.m..x..q`.z + 0x30, 0x07, 0x72, 0xa0, 0x07, 0x76, 0x40, 0x07, 0x43, 0x9e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0.r..v@.C....... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x3c, 0x04, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // ......<......... + 0x00, 0x00, 0x0c, 0x79, 0x14, 0x20, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, // ...y. .......... + 0xf2, 0x34, 0x40, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xe4, 0x79, 0x80, // .4@.........0.y. + 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0b, 0x04, 0x00, 0x00, 0x0b, 0x00, // ......... ...... + 0x00, 0x00, 0x32, 0x1e, 0x98, 0x14, 0x19, 0x11, 0x4c, 0x90, 0x8c, 0x09, 0x26, 0x47, 0xc6, 0x04, // ..2.....L...&G.. + 0x43, 0x1a, 0x25, 0x30, 0x02, 0x50, 0x14, 0xc5, 0x50, 0x16, 0x45, 0x50, 0x08, 0x05, 0x18, 0x40, // C.%0.P..P.EP...@ + 0x69, 0x04, 0x80, 0x6c, 0x81, 0x50, 0x9d, 0x01, 0xa0, 0x39, 0x03, 0x00, 0x00, 0x00, 0x79, 0x18, // i..l.P...9....y. + 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x1a, 0x03, 0x4c, 0x90, 0x46, 0x02, 0x13, 0xc4, 0x8f, 0x0c, // ..?.....L.F..... + 0x6f, 0xec, 0xed, 0x4d, 0x0c, 0x24, 0xc6, 0x25, 0x06, 0xc6, 0x45, 0xc6, 0x06, 0xa6, 0xc6, 0x65, // o..M.$.%..E....e + 0xe6, 0x06, 0x04, 0x65, 0x8c, 0x86, 0x0c, 0xc7, 0x8c, 0xc6, 0x2c, 0x27, 0x27, 0x65, 0x43, 0x10, // ...e......,''eC. + 0x4c, 0x10, 0x86, 0x62, 0x82, 0x30, 0x18, 0x1b, 0x84, 0x81, 0x98, 0x20, 0x0c, 0xc7, 0x06, 0x61, // L..b.0..... ...a + 0x30, 0x28, 0x8c, 0xcd, 0x4d, 0x10, 0x06, 0x64, 0xc3, 0x80, 0x24, 0xc4, 0x04, 0x61, 0x82, 0x08, // 0(..M..d..$..a.. + 0x4c, 0x10, 0x86, 0x64, 0x43, 0x42, 0x2c, 0x0c, 0x41, 0x0c, 0x0d, 0x01, 0x6c, 0x48, 0x86, 0x85, // L..dCB,.A...lH.. + 0x21, 0x86, 0xa1, 0x21, 0x80, 0x0d, 0x82, 0xf3, 0x4c, 0x10, 0xaa, 0x67, 0x82, 0x30, 0x28, 0x13, // !..!....L..g.0(. + 0x84, 0xa4, 0xd9, 0xb0, 0x10, 0x11, 0x43, 0x48, 0x43, 0x33, 0x4d, 0x13, 0xb0, 0x21, 0xa0, 0x36, // ......CHC3M..!.6 + 0x10, 0x50, 0x05, 0x00, 0x13, 0x04, 0x01, 0x20, 0xd1, 0x16, 0x96, 0xe6, 0x36, 0x41, 0xb0, 0x9c, // .P..... ....6A.. + 0x09, 0xc2, 0xb0, 0x4c, 0x10, 0x06, 0x66, 0xc3, 0xb0, 0x0d, 0xc3, 0x06, 0x82, 0xc8, 0x34, 0x6e, // ...L..f.......4n + 0x43, 0x71, 0x61, 0x80, 0xd5, 0x55, 0x61, 0x63, 0xb3, 0x6b, 0x73, 0x49, 0x23, 0x2b, 0x73, 0xa3, // Cqa..Uac.ksI#+s. + 0x9b, 0x12, 0x04, 0x55, 0xc8, 0xf0, 0x5c, 0xec, 0xca, 0xe4, 0xe6, 0xd2, 0xde, 0xdc, 0xa6, 0x04, // ...U............ + 0x44, 0x13, 0x32, 0x3c, 0x17, 0xbb, 0x30, 0x36, 0xbb, 0x32, 0xb9, 0x29, 0x81, 0x51, 0x87, 0x0c, // D.2<..06.2.).Q.. + 0xcf, 0x65, 0x0e, 0x2d, 0x8c, 0xac, 0x4c, 0xae, 0xe9, 0x8d, 0xac, 0x8c, 0x6d, 0x4a, 0x90, 0x94, // .e.-..L.....mJ.. + 0x21, 0xc3, 0x73, 0x91, 0x2b, 0x9b, 0x7b, 0xab, 0x93, 0x1b, 0x2b, 0x9b, 0x9b, 0x12, 0x54, 0x75, // !.s.+.{...+...Tu + 0xc8, 0xf0, 0x5c, 0xca, 0xdc, 0xe8, 0xe4, 0xf2, 0xa0, 0xde, 0xd2, 0xdc, 0xe8, 0xe6, 0xa6, 0x04, // ................ + 0x1d, 0x00, 0x79, 0x18, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x33, 0x08, 0x80, 0x1c, 0xc4, 0xe1, // ..y...Q...3..... + 0x1c, 0x66, 0x14, 0x01, 0x3d, 0x88, 0x43, 0x38, 0x84, 0xc3, 0x8c, 0x42, 0x80, 0x07, 0x79, 0x78, // .f..=.C8...B..yx + 0x07, 0x73, 0x98, 0x71, 0x0c, 0xe6, 0x00, 0x0f, 0xed, 0x10, 0x0e, 0xf4, 0x80, 0x0e, 0x33, 0x0c, // .s.q..........3. + 0x42, 0x1e, 0xc2, 0xc1, 0x1d, 0xce, 0xa1, 0x1c, 0x66, 0x30, 0x05, 0x3d, 0x88, 0x43, 0x38, 0x84, // B.......f0.=.C8. + 0x83, 0x1b, 0xcc, 0x03, 0x3d, 0xc8, 0x43, 0x3d, 0x8c, 0x03, 0x3d, 0xcc, 0x78, 0x8c, 0x74, 0x70, // ....=.C=..=.x.tp + 0x07, 0x7b, 0x08, 0x07, 0x79, 0x48, 0x87, 0x70, 0x70, 0x07, 0x7a, 0x70, 0x03, 0x76, 0x78, 0x87, // .{..yH.pp.zp.vx. + 0x70, 0x20, 0x87, 0x19, 0xcc, 0x11, 0x0e, 0xec, 0x90, 0x0e, 0xe1, 0x30, 0x0f, 0x6e, 0x30, 0x0f, // p .........0.n0. + 0xe3, 0xf0, 0x0e, 0xf0, 0x50, 0x0e, 0x33, 0x10, 0xc4, 0x1d, 0xde, 0x21, 0x1c, 0xd8, 0x21, 0x1d, // ....P.3....!..!. + 0xc2, 0x61, 0x1e, 0x66, 0x30, 0x89, 0x3b, 0xbc, 0x83, 0x3b, 0xd0, 0x43, 0x39, 0xb4, 0x03, 0x3c, // .a.f0.;..;.C9..< + 0xbc, 0x83, 0x3c, 0x84, 0x03, 0x3b, 0xcc, 0xf0, 0x14, 0x76, 0x60, 0x07, 0x7b, 0x68, 0x07, 0x37, // ..<..;...v`.{h.7 + 0x68, 0x87, 0x72, 0x68, 0x07, 0x37, 0x80, 0x87, 0x70, 0x90, 0x87, 0x70, 0x60, 0x07, 0x76, 0x28, // h.rh.7..p..p`.v( + 0x07, 0x76, 0xf8, 0x05, 0x76, 0x78, 0x87, 0x77, 0x80, 0x87, 0x5f, 0x08, 0x87, 0x71, 0x18, 0x87, // .v..vx.w.._..q.. + 0x72, 0x98, 0x87, 0x79, 0x98, 0x81, 0x2c, 0xee, 0xf0, 0x0e, 0xee, 0xe0, 0x0e, 0xf5, 0xc0, 0x0e, // r..y..,......... + 0xec, 0x30, 0x03, 0x62, 0xc8, 0xa1, 0x1c, 0xe4, 0xa1, 0x1c, 0xcc, 0xa1, 0x1c, 0xe4, 0xa1, 0x1c, // .0.b............ + 0xdc, 0x61, 0x1c, 0xca, 0x21, 0x1c, 0xc4, 0x81, 0x1d, 0xca, 0x61, 0x06, 0xd6, 0x90, 0x43, 0x39, // .a..!.....a...C9 + 0xc8, 0x43, 0x39, 0x98, 0x43, 0x39, 0xc8, 0x43, 0x39, 0xb8, 0xc3, 0x38, 0x94, 0x43, 0x38, 0x88, // .C9.C9.C9..8.C8. + 0x03, 0x3b, 0x94, 0xc3, 0x2f, 0xbc, 0x83, 0x3c, 0xfc, 0x82, 0x3b, 0xd4, 0x03, 0x3b, 0xb0, 0xc3, // .;../..<..;..;.. + 0x0c, 0xc4, 0x21, 0x07, 0x7c, 0x70, 0x03, 0x7a, 0x28, 0x87, 0x76, 0x80, 0x87, 0x19, 0xd1, 0x43, // ..!.|p.z(.v....C + 0x0e, 0xf8, 0xe0, 0x06, 0xe4, 0x20, 0x0e, 0xe7, 0xe0, 0x06, 0xf6, 0x10, 0x0e, 0xf2, 0xc0, 0x0e, // ..... .......... + 0xe1, 0x90, 0x0f, 0xef, 0x50, 0x0f, 0xf4, 0x30, 0x83, 0x81, 0xc8, 0x01, 0x1f, 0xdc, 0x40, 0x1c, // ....P..0......@. + 0xe4, 0xa1, 0x1c, 0xc2, 0x61, 0x1d, 0xdc, 0x40, 0x1c, 0xe4, 0x01, 0x00, 0x00, 0x00, 0x71, 0x20, // ....a..@......q + 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x40, 0x0d, 0x97, 0xef, 0x3c, 0x7e, 0x40, 0x15, 0x05, // ......6@...<~@.. + 0x11, 0x95, 0x0e, 0x30, 0xf8, 0xc8, 0x6d, 0x1b, 0x41, 0x35, 0x5c, 0xbe, 0xf3, 0xf8, 0x01, 0x55, // ...0..m.A5.....U + 0x14, 0x44, 0xc4, 0x4e, 0x4e, 0x44, 0xf8, 0xc8, 0x6d, 0x9b, 0x80, 0x34, 0x5c, 0xbe, 0xf3, 0xf8, // .D.NND..m..4.... + 0x42, 0x44, 0x00, 0x13, 0x11, 0x02, 0xcd, 0xb0, 0x10, 0x16, 0x20, 0x0d, 0x97, 0xef, 0x3c, 0xfe, // BD........ ...<. + 0x74, 0x44, 0x04, 0x30, 0x88, 0x83, 0x8f, 0xdc, 0xb6, 0x01, 0x10, 0x0c, 0x80, 0x34, 0x00, 0x00, // tD.0.........4.. + 0x00, 0x00, 0x61, 0x20, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x13, 0x04, 0x4a, 0x2c, 0x10, 0x00, // ..a ..W.....J,.. + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x34, 0x66, 0x00, 0x0a, 0x31, 0xa0, 0xe4, 0x4a, 0x37, 0xa0, // ......4f..1..J7. + 0x20, 0x0a, 0xa4, 0x50, 0x0a, 0xa6, 0x70, 0x4a, 0x31, 0x80, 0x4c, 0x09, 0x8c, 0x00, 0x00, 0x00, // ..P..pJ1.L..... + 0x00, 0x00, 0x23, 0x06, 0x09, 0x00, 0x82, 0x60, 0xa0, 0x6c, 0x8a, 0x70, 0x4d, 0xd0, 0x88, 0x41, // ..#....`.l.pM..A + 0x02, 0x80, 0x20, 0x18, 0x28, 0xdc, 0x22, 0x64, 0x59, 0x34, 0x62, 0x90, 0x00, 0x20, 0x08, 0x06, // .. .(."dY4b.. .. + 0x4a, 0xc7, 0x0c, 0x59, 0x26, 0x8d, 0x18, 0x18, 0x00, 0x08, 0x82, 0x01, 0xf1, 0x31, 0xda, 0x88, // J..Y&........1.. + 0x01, 0x02, 0x80, 0x20, 0x18, 0x34, 0xde, 0x23, 0x6c, 0xd0, 0x68, 0x42, 0x00, 0x0c, 0x37, 0x0c, // ... .4.#l.hB..7. + 0x01, 0x19, 0xcc, 0x32, 0x04, 0x49, 0x30, 0x62, 0x80, 0x00, 0x20, 0x08, 0x06, 0x4d, 0x18, 0x48, // ...2.I0b.. ..M.H + 0x45, 0x36, 0x8d, 0x26, 0x04, 0xc0, 0x88, 0x01, 0x02, 0x80, 0x20, 0x18, 0x34, 0x63, 0x40, 0x1d, // E6.&...... .4c@. + 0x51, 0x35, 0x9a, 0x10, 0x00, 0xc3, 0x0d, 0x83, 0x18, 0x80, 0xc1, 0x2c, 0x43, 0x22, 0x04, 0xb3, // Q5.........,C".. + 0x04, 0xc3, 0x40, 0xc5, 0x90, 0x07, 0x87, 0x29, 0x08, 0x15, 0x7c, 0x57, 0x41, 0x05, 0x23, 0x06, // ..@....)..|WA.#. + 0x08, 0x00, 0x82, 0x60, 0xd0, 0xa4, 0x81, 0xd6, 0x04, 0xdb, 0x68, 0x42, 0x00, 0x14, 0x71, 0xc1, // ...`......hB..q. + 0x88, 0x01, 0x02, 0x80, 0x20, 0x18, 0x34, 0x6c, 0xd0, 0x41, 0x81, 0x37, 0x9a, 0x10, 0x00, 0x75, // .... .4l.A.7...u + 0x64, 0x30, 0x62, 0x80, 0x00, 0x20, 0x08, 0x06, 0xcd, 0x1b, 0x80, 0xc1, 0x14, 0x84, 0xc1, 0x68, // d0b.. .........h + 0x42, 0x00, 0x94, 0xb2, 0xc1, 0x88, 0x01, 0x02, 0x80, 0x20, 0x18, 0x34, 0x72, 0x30, 0x06, 0x56, // B........ .4r0.V + 0x40, 0x06, 0xa3, 0x09, 0x01, 0x30, 0xdc, 0x40, 0xc4, 0x01, 0x18, 0xcc, 0x32, 0x1c, 0x44, 0x30, // @....0.@....2.D0 + 0x4b, 0x50, 0x0c, 0x54, 0x0c, 0x57, 0x41, 0x0e, 0x44, 0x25, 0x99, 0x54, 0xd0, 0x40, 0x05, 0x03, // KP.T.WA.D%.T.@.. + 0x54, 0x20, 0x07, 0x37, 0x62, 0x80, 0x00, 0x20, 0x08, 0x06, 0x8d, 0x1e, 0xac, 0xc1, 0x17, 0xb0, // T .7b.. ........ + 0xc1, 0x68, 0x42, 0x00, 0x14, 0xf6, 0x49, 0x29, 0x01, 0x54, 0x90, 0x40, 0x05, 0x78, 0x70, 0x23, // .hB...I).T.@.xp# + 0x06, 0x0d, 0x00, 0x82, 0x60, 0xf0, 0xfc, 0x41, 0x1a, 0x98, 0x41, 0x20, 0x07, 0x85, 0x1c, 0xc8, // ....`..A..A .... + 0x81, 0x1c, 0xa0, 0x81, 0x2d, 0x7e, 0x00, 0x81, 0xe1, 0x86, 0x20, 0x02, 0x83, 0x59, 0x06, 0xa3, // ....-~.... ..Y.. + 0x08, 0x66, 0x09, 0x0e, 0xe3, 0x40, 0x01, 0x02, 0xc3, 0x0d, 0x41, 0x18, 0x80, 0xc1, 0x2c, 0x03, // .f...@....A...,. + 0x32, 0x04, 0xb3, 0x04, 0x09, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2............. +}; diff --git a/Plugins/NativeEngine/Source/Shaders/essl/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/essl/cs_instance_repack.h new file mode 100644 index 0000000000..d6af62b2aa --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/essl/cs_instance_repack.h @@ -0,0 +1,72 @@ +static const uint8_t cs_instance_repack_essl[1091] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CSH............. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, // ......(...layout + 0x28, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x78, 0x20, 0x3d, 0x20, // (local_size_x = + 0x36, 0x34, 0x2c, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x79, // 64, local_size_y + 0x20, 0x3d, 0x20, 0x31, 0x2c, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, // = 1, local_size + 0x5f, 0x7a, 0x20, 0x3d, 0x20, 0x31, 0x29, 0x20, 0x69, 0x6e, 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, // _z = 1) in;..lay + 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x30, 0x2c, // out(binding = 0, + 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, // std430) readonl + 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, // y buffer paramsB + 0x75, 0x66, 0x66, 0x65, 0x72, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, // uffer.{. uint + 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x5d, 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x32, 0x30, // params[];.} _20 + 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, // ;..layout(bindin + 0x67, 0x20, 0x3d, 0x20, 0x32, 0x2c, 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, 0x77, // g = 2, std430) w + 0x72, 0x69, 0x74, 0x65, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, // riteonly buffer + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x75, 0x66, 0x66, 0x65, // destinationBuffe + 0x72, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x64, 0x65, 0x73, // r.{. uint des + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5b, 0x5d, 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x38, // tination[];.} _8 + 0x37, 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, // 7;..layout(bindi + 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x31, 0x2c, 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, // ng = 1, std430) + 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, // readonly buffer + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x0a, 0x7b, 0x0a, 0x20, // sourceBuffer.{. + 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5b, 0x5d, // uint source[] + 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x39, 0x38, 0x3b, 0x0a, 0x0a, 0x76, 0x6f, 0x69, 0x64, 0x20, 0x6d, // ;.} _98;..void m + 0x61, 0x69, 0x6e, 0x28, 0x29, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x6f, 0x0a, 0x20, // ain().{. do. + 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, // {. if + 0x28, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, // (gl_GlobalInvoca + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x2e, 0x78, 0x20, 0x3e, 0x3d, 0x20, 0x5f, 0x32, 0x30, 0x2e, // tionID.x >= _20. + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x30, 0x5d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, // params[0]). + 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // break;. + 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, // }. for (u + 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x34, 0x33, 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, // int _143 = 0u; _ + 0x31, 0x34, 0x33, 0x20, 0x3c, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, // 143 < _20.params + 0x5b, 0x31, 0x5d, 0x3b, 0x20, 0x5f, 0x31, 0x34, 0x33, 0x2b, 0x2b, 0x29, 0x0a, 0x20, 0x20, 0x20, // [1]; _143++). + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x3d, 0x20, 0x5f, 0x31, // uint _51 = _1 + 0x34, 0x33, 0x20, 0x2a, 0x20, 0x34, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 43 * 4u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x35, 0x32, 0x20, 0x3d, 0x20, // uint _52 = + 0x34, 0x75, 0x20, 0x2b, 0x20, 0x5f, 0x35, 0x31, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 4u + _51;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x36, 0x30, 0x20, 0x3d, // uint _60 = + 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x35, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, // _51 + 5u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x36, 0x36, 0x20, // uint _66 + 0x3d, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x36, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, // = _51 + 6u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x37, 0x32, // uint _72 + 0x20, 0x3d, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x37, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, // = _51 + 7u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, 0x69, // for (ui + 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, 0x31, // nt _144 = 0u; _1 + 0x34, 0x34, 0x20, 0x3c, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, // 44 < _20.params[ + 0x5f, 0x36, 0x36, 0x5d, 0x3b, 0x20, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // _66]; ). + 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x38, 0x37, 0x2e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, // _87.destin + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5b, 0x28, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, // ation[(_20.param + 0x73, 0x5b, 0x5f, 0x37, 0x32, 0x5d, 0x20, 0x2b, 0x20, 0x28, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, // s[_72] + (gl_Glo + 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x2e, // balInvocationID. + 0x78, 0x20, 0x2a, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x32, // x * _20.params[2 + 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x5d, 0x20, 0x3d, 0x20, 0x5f, 0x39, // ])) + _144] = _9 + 0x38, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5b, 0x28, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, // 8.source[(_20.pa + 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x5f, 0x35, 0x32, 0x5d, 0x20, 0x2b, 0x20, 0x28, 0x67, 0x6c, 0x5f, // rams[_52] + (gl_ + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, // GlobalInvocation + 0x49, 0x44, 0x2e, 0x78, 0x20, 0x2a, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, // ID.x * _20.param + 0x73, 0x5b, 0x5f, 0x36, 0x30, 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x5d, // s[_60])) + _144] + 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // ;. + 0x20, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x2b, 0x2b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // _144++;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, // contin + 0x75, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // ue;. + 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, // }. }. + 0x20, 0x20, 0x20, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, // break;. } + 0x20, 0x77, 0x68, 0x69, 0x6c, 0x65, 0x28, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x7d, // while(false);.} + 0x0a, 0x0a, 0x00, // ... +}; diff --git a/Plugins/NativeEngine/Source/Shaders/glsl/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/glsl/cs_instance_repack.h new file mode 100644 index 0000000000..b4faa250d0 --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/glsl/cs_instance_repack.h @@ -0,0 +1,72 @@ +static const uint8_t cs_instance_repack_glsl[1091] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CSH............. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, // ......(...layout + 0x28, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x78, 0x20, 0x3d, 0x20, // (local_size_x = + 0x36, 0x34, 0x2c, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x79, // 64, local_size_y + 0x20, 0x3d, 0x20, 0x31, 0x2c, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, // = 1, local_size + 0x5f, 0x7a, 0x20, 0x3d, 0x20, 0x31, 0x29, 0x20, 0x69, 0x6e, 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, // _z = 1) in;..lay + 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x30, 0x2c, // out(binding = 0, + 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, // std430) readonl + 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, // y buffer paramsB + 0x75, 0x66, 0x66, 0x65, 0x72, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, // uffer.{. uint + 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x5d, 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x32, 0x30, // params[];.} _20 + 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, // ;..layout(bindin + 0x67, 0x20, 0x3d, 0x20, 0x32, 0x2c, 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, 0x77, // g = 2, std430) w + 0x72, 0x69, 0x74, 0x65, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, // riteonly buffer + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x75, 0x66, 0x66, 0x65, // destinationBuffe + 0x72, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x64, 0x65, 0x73, // r.{. uint des + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5b, 0x5d, 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x38, // tination[];.} _8 + 0x37, 0x3b, 0x0a, 0x0a, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x28, 0x62, 0x69, 0x6e, 0x64, 0x69, // 7;..layout(bindi + 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x31, 0x2c, 0x20, 0x73, 0x74, 0x64, 0x34, 0x33, 0x30, 0x29, 0x20, // ng = 1, std430) + 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, // readonly buffer + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x0a, 0x7b, 0x0a, 0x20, // sourceBuffer.{. + 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5b, 0x5d, // uint source[] + 0x3b, 0x0a, 0x7d, 0x20, 0x5f, 0x39, 0x38, 0x3b, 0x0a, 0x0a, 0x76, 0x6f, 0x69, 0x64, 0x20, 0x6d, // ;.} _98;..void m + 0x61, 0x69, 0x6e, 0x28, 0x29, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x6f, 0x0a, 0x20, // ain().{. do. + 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, // {. if + 0x28, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, // (gl_GlobalInvoca + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x2e, 0x78, 0x20, 0x3e, 0x3d, 0x20, 0x5f, 0x32, 0x30, 0x2e, // tionID.x >= _20. + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x30, 0x5d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, // params[0]). + 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // break;. + 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, // }. for (u + 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x34, 0x33, 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, // int _143 = 0u; _ + 0x31, 0x34, 0x33, 0x20, 0x3c, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, // 143 < _20.params + 0x5b, 0x31, 0x5d, 0x3b, 0x20, 0x5f, 0x31, 0x34, 0x33, 0x2b, 0x2b, 0x29, 0x0a, 0x20, 0x20, 0x20, // [1]; _143++). + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x3d, 0x20, 0x5f, 0x31, // uint _51 = _1 + 0x34, 0x33, 0x20, 0x2a, 0x20, 0x34, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 43 * 4u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x35, 0x32, 0x20, 0x3d, 0x20, // uint _52 = + 0x34, 0x75, 0x20, 0x2b, 0x20, 0x5f, 0x35, 0x31, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 4u + _51;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x36, 0x30, 0x20, 0x3d, // uint _60 = + 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x35, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, // _51 + 5u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x36, 0x36, 0x20, // uint _66 + 0x3d, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x36, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, // = _51 + 6u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x37, 0x32, // uint _72 + 0x20, 0x3d, 0x20, 0x5f, 0x35, 0x31, 0x20, 0x2b, 0x20, 0x37, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, // = _51 + 7u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, 0x69, // for (ui + 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, 0x31, // nt _144 = 0u; _1 + 0x34, 0x34, 0x20, 0x3c, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, // 44 < _20.params[ + 0x5f, 0x36, 0x36, 0x5d, 0x3b, 0x20, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // _66]; ). + 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x38, 0x37, 0x2e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, // _87.destin + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5b, 0x28, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, // ation[(_20.param + 0x73, 0x5b, 0x5f, 0x37, 0x32, 0x5d, 0x20, 0x2b, 0x20, 0x28, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, // s[_72] + (gl_Glo + 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x2e, // balInvocationID. + 0x78, 0x20, 0x2a, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x32, // x * _20.params[2 + 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x5d, 0x20, 0x3d, 0x20, 0x5f, 0x39, // ])) + _144] = _9 + 0x38, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5b, 0x28, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, // 8.source[(_20.pa + 0x72, 0x61, 0x6d, 0x73, 0x5b, 0x5f, 0x35, 0x32, 0x5d, 0x20, 0x2b, 0x20, 0x28, 0x67, 0x6c, 0x5f, // rams[_52] + (gl_ + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, // GlobalInvocation + 0x49, 0x44, 0x2e, 0x78, 0x20, 0x2a, 0x20, 0x5f, 0x32, 0x30, 0x2e, 0x70, 0x61, 0x72, 0x61, 0x6d, // ID.x * _20.param + 0x73, 0x5b, 0x5f, 0x36, 0x30, 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x5d, // s[_60])) + _144] + 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // ;. + 0x20, 0x20, 0x5f, 0x31, 0x34, 0x34, 0x2b, 0x2b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // _144++;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, // contin + 0x75, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // ue;. + 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, // }. }. + 0x20, 0x20, 0x20, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, // break;. } + 0x20, 0x77, 0x68, 0x69, 0x6c, 0x65, 0x28, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x7d, // while(false);.} + 0x0a, 0x0a, 0x00, // ... +}; diff --git a/Plugins/NativeEngine/Source/Shaders/metal/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/metal/cs_instance_repack.h new file mode 100644 index 0000000000..1e5b37e530 --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/metal/cs_instance_repack.h @@ -0,0 +1,76 @@ +static const uint8_t cs_instance_repack_mtl[1159] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CSH............. + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00, 0x01, 0x00, 0x63, 0x04, 0x00, 0x00, // ......@.....c... + 0x23, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x20, 0x3c, 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x5f, // #include .#include + 0x20, 0x3c, 0x73, 0x69, 0x6d, 0x64, 0x2f, 0x73, 0x69, 0x6d, 0x64, 0x2e, 0x68, 0x3e, 0x0a, 0x0a, // .. + 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, // using namespace + 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x70, // metal;..struct p + 0x61, 0x72, 0x61, 0x6d, 0x73, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, // arams.{. uint + 0x20, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5b, 0x31, 0x5d, 0x3b, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, // _data[1];.};..s + 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, // truct destinatio + 0x6e, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x64, 0x61, // n.{. uint _da + 0x74, 0x61, 0x5b, 0x31, 0x5d, 0x3b, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x6b, 0x65, 0x72, 0x6e, 0x65, // ta[1];.};..kerne + 0x6c, 0x20, 0x76, 0x6f, 0x69, 0x64, 0x20, 0x78, 0x6c, 0x61, 0x74, 0x4d, 0x74, 0x6c, 0x4d, 0x61, // l void xlatMtlMa + 0x69, 0x6e, 0x28, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, // in(device params + 0x26, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x20, 0x5b, 0x5b, 0x62, 0x75, 0x66, // & params_1 [[buf + 0x66, 0x65, 0x72, 0x28, 0x31, 0x29, 0x5d, 0x5d, 0x2c, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, // fer(1)]], device + 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x26, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, // params& source + 0x5b, 0x5b, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x28, 0x32, 0x29, 0x5d, 0x5d, 0x2c, 0x20, 0x64, // [[buffer(2)]], d + 0x65, 0x76, 0x69, 0x63, 0x65, 0x20, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, // evice destinatio + 0x6e, 0x26, 0x20, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x31, // n& destination_1 + 0x20, 0x5b, 0x5b, 0x62, 0x75, 0x66, 0x66, 0x65, 0x72, 0x28, 0x33, 0x29, 0x5d, 0x5d, 0x2c, 0x20, // [[buffer(3)]], + 0x75, 0x69, 0x6e, 0x74, 0x33, 0x20, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, // uint3 gl_GlobalI + 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x20, 0x5b, 0x5b, 0x74, 0x68, // nvocationID [[th + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, // read_position_in + 0x5f, 0x67, 0x72, 0x69, 0x64, 0x5d, 0x5d, 0x29, 0x0a, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, // _grid]]).{. d + 0x6f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // o. {. + 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x36, 0x33, 0x20, 0x3d, 0x20, 0x75, 0x69, 0x6e, 0x74, // uint _163 = uint + 0x28, 0x69, 0x6e, 0x74, 0x28, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, // (int(gl_GlobalIn + 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x2e, 0x78, 0x29, 0x29, 0x3b, 0x0a, // vocationID.x));. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x5f, 0x31, 0x36, 0x33, // if (_163 + 0x20, 0x3e, 0x3d, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x2e, 0x5f, 0x64, 0x61, // >= params_1._da + 0x74, 0x61, 0x5b, 0x30, 0x5d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, // ta[0]). { + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x72, 0x65, // . bre + 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, // ak;. }. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, 0x69, 0x6e, 0x74, 0x20, // for (uint + 0x5f, 0x32, 0x33, 0x30, 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, 0x32, 0x33, 0x30, 0x20, // _230 = 0u; _230 + 0x3c, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x2e, 0x5f, 0x64, 0x61, 0x74, 0x61, // < params_1._data + 0x5b, 0x31, 0x5d, 0x3b, 0x20, 0x5f, 0x32, 0x33, 0x30, 0x2b, 0x2b, 0x29, 0x0a, 0x20, 0x20, 0x20, // [1]; _230++). + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x38, 0x31, 0x20, 0x3d, 0x20, 0x5f, // uint _181 = _ + 0x32, 0x33, 0x30, 0x20, 0x2a, 0x20, 0x34, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 230 * 4u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x38, 0x32, 0x20, // uint _182 + 0x3d, 0x20, 0x34, 0x75, 0x20, 0x2b, 0x20, 0x5f, 0x31, 0x38, 0x31, 0x3b, 0x0a, 0x20, 0x20, 0x20, // = 4u + _181;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, // uint _1 + 0x38, 0x37, 0x20, 0x3d, 0x20, 0x5f, 0x31, 0x38, 0x31, 0x20, 0x2b, 0x20, 0x35, 0x75, 0x3b, 0x0a, // 87 = _181 + 5u;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, 0x69, 0x6e, 0x74, // uint + 0x20, 0x5f, 0x31, 0x39, 0x31, 0x20, 0x3d, 0x20, 0x5f, 0x31, 0x38, 0x31, 0x20, 0x2b, 0x20, 0x36, // _191 = _181 + 6 + 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75, // u;. u + 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x31, 0x39, 0x35, 0x20, 0x3d, 0x20, 0x5f, 0x31, 0x38, 0x31, 0x20, // int _195 = _181 + 0x2b, 0x20, 0x37, 0x75, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // + 7u;. + 0x20, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x5f, 0x32, 0x33, 0x31, // for (uint _231 + 0x20, 0x3d, 0x20, 0x30, 0x75, 0x3b, 0x20, 0x5f, 0x32, 0x33, 0x31, 0x20, 0x3c, 0x20, 0x70, 0x61, // = 0u; _231 < pa + 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x2e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5b, 0x5f, 0x31, 0x39, // rams_1._data[_19 + 0x31, 0x5d, 0x3b, 0x20, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 1]; ). + 0x20, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // {. + 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, // destination_ + 0x31, 0x2e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5b, 0x28, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, // 1._data[(params_ + 0x31, 0x2e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5b, 0x5f, 0x31, 0x39, 0x35, 0x5d, 0x20, 0x2b, 0x20, // 1._data[_195] + + 0x28, 0x5f, 0x31, 0x36, 0x33, 0x20, 0x2a, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, // (_163 * params_1 + 0x2e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5b, 0x32, 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x32, // ._data[2])) + _2 + 0x33, 0x31, 0x5d, 0x20, 0x3d, 0x20, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x5f, 0x64, 0x61, // 31] = source._da + 0x74, 0x61, 0x5b, 0x28, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x2e, 0x5f, 0x64, 0x61, // ta[(params_1._da + 0x74, 0x61, 0x5b, 0x5f, 0x31, 0x38, 0x32, 0x5d, 0x20, 0x2b, 0x20, 0x28, 0x5f, 0x31, 0x36, 0x33, // ta[_182] + (_163 + 0x20, 0x2a, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x31, 0x2e, 0x5f, 0x64, 0x61, 0x74, // * params_1._dat + 0x61, 0x5b, 0x5f, 0x31, 0x38, 0x37, 0x5d, 0x29, 0x29, 0x20, 0x2b, 0x20, 0x5f, 0x32, 0x33, 0x31, // a[_187])) + _231 + 0x5d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // ];. + 0x20, 0x20, 0x20, 0x5f, 0x32, 0x33, 0x31, 0x2b, 0x2b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, // _231++;. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, // conti + 0x6e, 0x75, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // nue;. + 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, // }. }. + 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, // break;. + 0x7d, 0x20, 0x77, 0x68, 0x69, 0x6c, 0x65, 0x28, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, // } while(false);. + 0x7d, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, // }...... +}; diff --git a/Plugins/NativeEngine/Source/Shaders/spirv/cs_instance_repack.h b/Plugins/NativeEngine/Source/Shaders/spirv/cs_instance_repack.h new file mode 100644 index 0000000000..ed9b0a83f3 --- /dev/null +++ b/Plugins/NativeEngine/Source/Shaders/spirv/cs_instance_repack.h @@ -0,0 +1,135 @@ +static const uint8_t cs_instance_repack_spv[2110] = +{ + 0x43, 0x53, 0x48, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CSH............. + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x41, 0x00, 0x02, // .......paramsA.. + 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, // ........destinat + 0x69, 0x6f, 0x6e, 0x01, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x73, 0x6f, // ion...........so + 0x75, 0x72, 0x63, 0x65, 0x41, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x07, // urceA........... + 0x00, 0x00, 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0xf0, 0x00, // ....#........... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, // ................ + 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, // ......GLSL.std.4 + 0x35, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, // 50.............. + 0x00, 0x00, 0x0f, 0x00, 0x06, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, // ..............ma + 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x00, // in....t......... + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, // ......@......... + 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x05, 0x00, // ................ + 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, // ......main...... + 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x06, 0x00, // ......params.... + 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x64, 0x61, 0x74, 0x61, 0x00, // ..........@data. + 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, // ..........params + 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x59, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, // ......Y...destin + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x06, 0x00, 0x05, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, // ation.....Y..... + 0x00, 0x00, 0x40, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x5b, 0x00, // ..@data.......[. + 0x00, 0x00, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x05, 0x00, // ..destination... + 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x00, 0x00, 0x05, 0x00, // ..c...source.... + 0x08, 0x00, 0x74, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, // ..t...gl_GlobalI + 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x00, 0x00, 0x00, 0x47, 0x00, // nvocationID...G. + 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, // ..............G. + 0x03, 0x00, 0x17, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, 0x17, 0x00, // ..........H..... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x17, 0x00, // ..........H..... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, // ......#.......G. + 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x19, 0x00, // ..........G..... + 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x19, 0x00, // ..!.......G..... + 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x58, 0x00, // ..".......G...X. + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x59, 0x00, // ..........G...Y. + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, // ......H...Y..... + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x5b, 0x00, // ..#.......G...[. + 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x5b, 0x00, // ..!.......G...[. + 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x63, 0x00, // ..".......G...c. + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, 0x21, 0x00, // ......G...c...!. + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, 0x22, 0x00, // ......G...c...". + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x74, 0x00, 0x00, 0x00, 0x0b, 0x00, // ......G...t..... + 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, // ..............!. + 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x06, 0x00, // ................ + 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, // .. ............. + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0d, 0x00, // .. .......+..... + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, 0x16, 0x00, // ................ + 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x03, 0x00, 0x17, 0x00, 0x00, 0x00, 0x16, 0x00, // ................ + 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x17, 0x00, // .. ............. + 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x02, 0x00, // ..;............. + 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, // ..+............. + 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0d, 0x00, // .. ............. + 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, // ..........+..... + 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, // ..$.......+..... + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0d, 0x00, // ..(.......+..... + 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, 0x58, 0x00, // ..5...........X. + 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x03, 0x00, 0x59, 0x00, 0x00, 0x00, 0x58, 0x00, // ..........Y...X. + 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x59, 0x00, // .. ...Z.......Y. + 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x02, 0x00, // ..;...Z...[..... + 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x02, 0x00, // ..;.......c..... + 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, // .. ...s......... + 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x01, 0x00, // ..;...s...t..... + 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x05, 0x00, // ..+............. + 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0x06, 0x00, // ..+............. + 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0x07, 0x00, // ..+............. + 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xef, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, // .. ............. + 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // ..6............. + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0xf7, 0x00, // ................ + 0x03, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x03, 0x00, 0x10, 0x00, // ................ + 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x41, 0x00, // ..............A. + 0x05, 0x00, 0xef, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x10, 0x00, // ..........t..... + 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa4, 0x00, // ..=............. + 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xa5, 0x00, // ..|............. + 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x19, 0x00, // ..A............. + 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, // ..........=..... + 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0xae, 0x00, 0x05, 0x00, 0x1e, 0x00, // ................ + 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xf7, 0x00, // ................ + 0x03, 0x00, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xaa, 0x00, // ................ + 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xab, 0x00, // ................ + 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xac, 0x00, // ................ + 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x19, 0x00, // ..A............. + 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, // ......$...=..... + 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, // ..........A..... + 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x28, 0x00, // ..............(. + 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0xaf, 0x00, // ..=............. + 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x00, // ................ + 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x10, 0x00, // ................ + 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xb0, 0x00, // ................ + 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xae, 0x00, // ................ + 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xe7, 0x00, // ................ + 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x80, 0x00, // ..........5..... + 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0xb8, 0x00, // ..........5..... + 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x19, 0x00, // ..A............. + 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, // ..........=..... + 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x41, 0x00, // ..............A. + 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1a, 0x00, // ................ + 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc0, 0x00, // ......=......... + 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc2, 0x00, // ................ + 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, // ..........A..... + 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xc2, 0x00, // ................ + 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xc3, 0x00, // ..=............. + 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xb8, 0x00, // ................ + 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xc7, 0x00, // ......A......... + 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x3d, 0x00, // ..............=. + 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xf9, 0x00, // ................ + 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xf5, 0x00, // ................ + 0x07, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xb6, 0x00, // ................ + 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x05, 0x00, 0x1e, 0x00, // ................ + 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xf6, 0x00, // ................ + 0x04, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, // ................ + 0x04, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xf8, 0x00, // ................ + 0x02, 0x00, 0xce, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd2, 0x00, // ................ + 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x80, 0x00, // ................ + 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xea, 0x00, // ................ + 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0xa6, 0x00, // ................ + 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xda, 0x00, // ................ + 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x41, 0x00, // ..............A. + 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x1a, 0x00, // ..........c..... + 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xde, 0x00, // ......=......... + 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xdf, 0x00, // ......A......... + 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0x3e, 0x00, // ..[...........>. + 0x03, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x0d, 0x00, // ................ + 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xf9, 0x00, // ..........$..... + 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xf9, 0x00, // ................ + 0x02, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x80, 0x00, // ................ + 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x24, 0x00, // ..............$. + 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe7, 0x00, // ................ + 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe8, 0x00, // ................ + 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // ......8....... +}; diff --git a/Plugins/NativeEngine/Source/StorageBuffer.cpp b/Plugins/NativeEngine/Source/StorageBuffer.cpp new file mode 100644 index 0000000000..ab7b3dab73 --- /dev/null +++ b/Plugins/NativeEngine/Source/StorageBuffer.cpp @@ -0,0 +1,191 @@ +#include "StorageBuffer.h" +#include "Babylon/Graphics/DeviceContext.h" +#include +#include +#include +#include + +namespace Babylon +{ + namespace + { + uint32_t RoundUp(uint32_t value, uint32_t multiple) + { + if (value == 0 || multiple == 0 || multiple > std::numeric_limits::max()) + { + throw std::invalid_argument{"StorageBuffer size and stride must be positive and the stride must fit uint16"}; + } + const uint32_t remainder = value % multiple; + const uint32_t padding = remainder == 0 ? 0 : multiple - remainder; + if (value > std::numeric_limits::max() - padding) + { + throw std::overflow_error{"StorageBuffer rounded size exceeds uint32"}; + } + return value + padding; + } + } + + StorageBuffer::StorageBuffer(Graphics::DeviceContext& deviceContext, uint32_t byteLength, uint32_t byteStride, bool computeWrite) + : m_deviceContext{deviceContext} + , m_deviceId{m_deviceContext.GetDeviceId()} + , m_byteLength{byteLength} + , m_computeWrite{computeWrite} + , m_byteStride{byteStride == 0 ? 16u : byteStride} + , m_shadow(RoundUp(byteLength, m_byteStride), 0) + { + } + + StorageBuffer::~StorageBuffer() + { + Dispose(); + } + + void StorageBuffer::Dispose() + { + if (m_disposed) + { + return; + } + + if (bgfx::isValid(m_handle) && m_deviceId == m_deviceContext.GetDeviceId()) + { + bgfx::destroy(m_handle); + } + m_handle = BGFX_INVALID_HANDLE; + + m_shadow.clear(); + m_disposed = true; + } + + void StorageBuffer::ValidateUpdate(size_t byteLength, uint32_t byteOffset) const + { + if (m_disposed) + { + throw std::runtime_error{"Cannot update a disposed StorageBuffer"}; + } + if (byteOffset > m_byteLength || byteLength > m_byteLength - byteOffset) + { + throw std::runtime_error{"StorageBuffer update out of range"}; + } + if (byteOffset % m_byteStride != 0) + { + throw std::runtime_error{"StorageBuffer update byteOffset must be a multiple of the buffer stride"}; + } + } + + void StorageBuffer::Update(gsl::span bytes, uint32_t byteOffset) + { + ValidateUpdate(bytes.size(), byteOffset); + if (bytes.empty()) + { + return; + } + + std::memcpy(m_shadow.data() + byteOffset, bytes.data(), bytes.size()); + if (!bgfx::isValid(m_handle)) + { + // Creation seeds the GPU buffer from the whole shadow. + EnsureCreated(); + if (m_computeWrite) + { + Upload(m_shadow, 0); + } + return; + } + + if (m_computeWrite) + { + Upload(bytes, byteOffset); + return; + } + + // bgfx addresses dynamic vertex buffers by element, so the offset must be stride-aligned. + const uint32_t startVertex = byteOffset / m_byteStride; + bgfx::update(m_handle, startVertex, bgfx::copy(bytes.data(), static_cast(bytes.size()))); + } + + void StorageBuffer::EnsureCreated() + { + if (bgfx::isValid(m_handle)) + { + return; + } + + bgfx::VertexLayout layout; + layout.begin(); + layout.m_stride = static_cast(m_byteStride); + layout.end(); + + // Buffers without COMPUTE_WRITE stay dynamic, so CPU updates through bgfx::update stick on D3D11. + const uint16_t flags = static_cast( + m_computeWrite ? BGFX_BUFFER_COMPUTE_READ_WRITE : BGFX_BUFFER_COMPUTE_READ); + + const uint32_t numVertices = static_cast(m_shadow.size()) / m_byteStride; + m_handle = bgfx::createDynamicVertexBuffer(numVertices, layout, flags); + + if (!bgfx::isValid(m_handle)) + { + throw std::runtime_error{"Failed to create storage buffer"}; + } + + // bgfx forbids CPU updates of COMPUTE_WRITE buffers; Update uploads those through Upload. + if (!m_computeWrite) + { + bgfx::update(m_handle, 0, bgfx::copy(m_shadow.data(), static_cast(m_shadow.size()))); + } + } + + void StorageBuffer::Upload(gsl::span bytes, uint32_t byteOffset) + { + bgfx::Encoder* encoder = m_deviceContext.GetActiveEncoder(); + if (encoder == nullptr) + { + throw std::runtime_error{"StorageBuffer upload requires an active frame"}; + } + + // A fresh staging buffer per upload keeps each blit's data distinct: bgfx applies all CPU + // updates of a frame before rendering it. Raw buffer views address whole 32-bit words. + const uint32_t size = static_cast(bytes.size()); + const uint32_t stagingSize = RoundUp(size, sizeof(uint32_t)); + const bgfx::Memory* memory = bgfx::alloc(stagingSize); + std::memcpy(memory->data, bytes.data(), size); + std::memset(memory->data + size, 0, stagingSize - size); + + bgfx::VertexLayout layout; + layout.begin(); + layout.m_stride = sizeof(uint32_t); + layout.end(); + const auto staging = bgfx::createDynamicVertexBuffer(memory, layout, BGFX_BUFFER_COMPUTE_READ); + if (!bgfx::isValid(staging)) + { + throw std::runtime_error{"Failed to create storage buffer staging"}; + } + + bgfx::BufferRegion destination{}; + destination.init(m_handle, byteOffset, size); + bgfx::BufferRegion source{}; + source.init(staging, 0, size); + encoder->blit(m_deviceContext.AcquireNewViewId(), destination, source); + + // bgfx defers destruction until the frame that records the blit has rendered. + bgfx::destroy(staging); + } + + void StorageBuffer::SetCompute(bgfx::Encoder* encoder, uint8_t stage, bgfx::Access::Enum access) + { + EnsureCreated(); + encoder->setBuffer(stage, m_handle, access); + } + + void StorageBuffer::SetVertex(bgfx::Encoder* encoder, uint8_t stream, uint32_t startVertex, uint32_t numVertices, bgfx::VertexLayoutHandle layout) + { + EnsureCreated(); + encoder->setVertexBuffer(stream, m_handle, startVertex, numVertices, layout); + } + + bgfx::DynamicVertexBufferHandle StorageBuffer::Handle() + { + EnsureCreated(); + return m_handle; + } +} diff --git a/Plugins/NativeEngine/Source/StorageBuffer.h b/Plugins/NativeEngine/Source/StorageBuffer.h new file mode 100644 index 0000000000..93180b6bbc --- /dev/null +++ b/Plugins/NativeEngine/Source/StorageBuffer.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +namespace Babylon +{ + namespace Graphics + { + class DeviceContext; + } + + // Wraps a bgfx dynamic vertex buffer that compute shaders bind as a raw (ByteAddressBuffer) + // buffer and draws bind as a vertex or instance stream. It is created lazily and seeded from a + // CPU shadow, so a buffer can be filled before its first use. + class StorageBuffer final + { + public: + // byteStride is the bgfx vertex-layout stride used when the buffer is bound as a vertex or + // instance stream; instance data destinations pass (numSlots * 16). computeWrite = false + // omits BGFX_BUFFER_COMPUTE_WRITE so the buffer stays CPU-updatable. + StorageBuffer(Graphics::DeviceContext& deviceContext, uint32_t byteLength, uint32_t byteStride = 16, bool computeWrite = true); + ~StorageBuffer(); + + // No copy or move semantics. + StorageBuffer(const StorageBuffer&) = delete; + StorageBuffer(StorageBuffer&&) = delete; + + void Dispose(); + + // Writes bytes into the CPU shadow and the GPU buffer at byteOffset, which must be a multiple of the stride. + void Update(gsl::span bytes, uint32_t byteOffset); + + // Binds the buffer as a compute resource on the given stage. + void SetCompute(bgfx::Encoder* encoder, uint8_t stage, bgfx::Access::Enum access); + + // Binds the buffer as a vertex stream. + void SetVertex(bgfx::Encoder* encoder, uint8_t stream, uint32_t startVertex, uint32_t numVertices, bgfx::VertexLayoutHandle layout); + + uint32_t ByteLength() const { return m_byteLength; } + gsl::span ShadowBytes() const { return gsl::make_span(m_shadow.data(), m_shadow.size()); } + bgfx::DynamicVertexBufferHandle Handle(); + + private: + void ValidateUpdate(size_t byteLength, uint32_t byteOffset) const; + void EnsureCreated(); + void Upload(gsl::span bytes, uint32_t byteOffset); + + Graphics::DeviceContext& m_deviceContext; + const uintptr_t m_deviceId{}; + + const uint32_t m_byteLength{}; + const bool m_computeWrite{true}; + const uint32_t m_byteStride{}; + + std::vector m_shadow{}; + bgfx::DynamicVertexBufferHandle m_handle{bgfx::kInvalidHandle}; + + bool m_disposed{}; + }; +} diff --git a/Plugins/NativeEngine/Source/VertexArray.cpp b/Plugins/NativeEngine/Source/VertexArray.cpp index 67ee32d2fc..7e24504c6b 100644 --- a/Plugins/NativeEngine/Source/VertexArray.cpp +++ b/Plugins/NativeEngine/Source/VertexArray.cpp @@ -1,6 +1,7 @@ #include "VertexArray.h" #include #include +#include #include "Babylon/Graphics/BgfxShaderInfo.h" #include "Babylon/Graphics/DeviceContext.h" @@ -38,6 +39,19 @@ namespace Babylon } m_vertexBufferRecords.clear(); m_vertexBufferInstances.clear(); + m_transformFeedbackInputs.clear(); + if (m_deviceId == m_deviceContext.GetDeviceId()) + { + for (const auto& [key, buffer] : m_unindexedExpandedBuffers) + { + static_cast(key); + if (bgfx::isValid(buffer.Handle)) + { + bgfx::destroy(buffer.Handle); + } + } + } + m_unindexedExpandedBuffers.clear(); m_disposed = true; } @@ -61,6 +75,48 @@ namespace Babylon auto attribType = static_cast(type); + if (location >= Graphics::TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE) + { + Graphics::TransformFeedbackInputType inputType{}; + uint32_t componentSize{}; + switch (attribType) + { + case bgfx::AttribType::Float: + inputType = Graphics::TransformFeedbackInputType::Float; + componentSize = 4; + break; + case bgfx::AttribType::Int8: + inputType = Graphics::TransformFeedbackInputType::Int8; + componentSize = 1; + break; + case bgfx::AttribType::Uint8: + inputType = Graphics::TransformFeedbackInputType::Uint8; + componentSize = 1; + break; + case bgfx::AttribType::Int16: + inputType = Graphics::TransformFeedbackInputType::Int16; + componentSize = 2; + break; + case bgfx::AttribType::Uint16: + inputType = Graphics::TransformFeedbackInputType::Uint16; + componentSize = 2; + break; + default: + throw std::runtime_error{"Unsupported transform feedback vertex input type"}; + } + if (divisor != 0) + { + throw std::runtime_error{"Transform feedback vertex inputs cannot be instanced"}; + } + if (byteOffset % componentSize != 0 || byteStride % componentSize != 0 || numElements == 0 || numElements > 4) + { + throw std::runtime_error{"Transform feedback vertex inputs must be aligned to their component size"}; + } + m_transformFeedbackInputs[location - Graphics::TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE] = { + vertexBuffer, byteOffset, byteStride != 0 ? byteStride : numElements * componentSize, numElements, inputType, normalized}; + return; + } + if (divisor == 1) { if (attribType != bgfx::AttribType::Float || normalized) @@ -124,6 +180,18 @@ namespace Babylon } } + bool VertexArray::HasStorageInstances() const + { + for (const auto& pair : m_vertexBufferInstances) + { + if (pair.second.ResolveStorage() != nullptr) + { + return true; + } + } + return false; + } + void VertexArray::SetIndexBuffer(bgfx::Encoder* encoder, uint32_t firstIndex, uint32_t numIndices) { if (m_indexBuffer != nullptr) @@ -132,17 +200,72 @@ namespace Babylon } } + bool VertexArray::SetExpandedIndexBuffer(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t firstIndex, uint32_t numIndices) + { + if (m_indexBuffer == nullptr) + { + throw std::runtime_error{"Indexed primitive expansion requires an index buffer"}; + } + + return m_indexBuffer->SetExpanded(encoder, mode, firstIndex, numIndices); + } + + bool VertexArray::SetExpandedUnindexedBuffer(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t numVertices) + { + const UnindexedExpansionKey key{mode, numVertices}; + auto existing = m_unindexedExpandedBuffers.find(key); + if (existing == m_unindexedExpandedBuffers.end()) + { + auto expanded = PrimitiveModeExpansion::ExpandUnindexed(numVertices, mode); + if (expanded.IndexCount == 0) + { + return false; + } + + const uint16_t flags = expanded.Index32 ? BGFX_BUFFER_INDEX32 : 0; + const bgfx::Memory* memory = bgfx::copy(expanded.Bytes.data(), static_cast(expanded.Bytes.size())); + const bgfx::IndexBufferHandle handle = bgfx::createIndexBuffer(memory, flags); + if (!bgfx::isValid(handle)) + { + throw std::runtime_error{"Failed to create expanded primitive index buffer"}; + } + + try + { + existing = m_unindexedExpandedBuffers.emplace(key, ExpandedBuffer{handle, expanded.IndexCount}).first; + } + catch (...) + { + bgfx::destroy(handle); + throw; + } + } + + encoder->setIndexBuffer(existing->second.Handle, 0, existing->second.IndexCount); + return true; + } + + bool VertexArray::UnindexedExpansionKey::operator<(const UnindexedExpansionKey& other) const + { + return std::tie(Mode, VertexCount) < std::tie(other.Mode, other.VertexCount); + } + void VertexArray::SetVertexBuffers(bgfx::Encoder* encoder, uint32_t startVertex, uint32_t numVertices, uint32_t instanceCount, const VertexBuffer::InstanceDataLayout& instanceDataLayout) { - if (!m_vertexBufferInstances.empty()) + if (!m_vertexBufferInstances.empty() && !HasStorageInstances()) { bgfx::InstanceDataBuffer instanceDataBuffer{}; VertexBuffer::BuildInstanceDataBuffer(instanceDataBuffer, m_vertexBufferInstances, instanceCount, instanceDataLayout); encoder->setInstanceDataBuffer(&instanceDataBuffer); } - else if (instanceCount > 0) + else if (instanceCount > 0 && !HasStorageInstances()) { - // These draws derive per-instance data from gl_InstanceID instead of an instance buffer. + // Attribute-less instancing: the draw requests multiple instances but has no per-instance + // vertex data (e.g. the clustered-light tile-mask proxies, which derive the light index, + // vertical batch offset and bit mask purely from gl_InstanceID). Without an instance data + // buffer bgfx would fall back to drawing a single instance, leaving the tile mask empty and + // breaking clustered lighting. Tell bgfx the instance count explicitly so gl_InstanceID + // iterates 0..instanceCount-1. encoder->setInstanceCount(instanceCount); } diff --git a/Plugins/NativeEngine/Source/VertexArray.h b/Plugins/NativeEngine/Source/VertexArray.h index b222c75214..76a227c52c 100644 --- a/Plugins/NativeEngine/Source/VertexArray.h +++ b/Plugins/NativeEngine/Source/VertexArray.h @@ -2,6 +2,7 @@ #include "IndexBuffer.h" #include "VertexBuffer.h" +#include #include #include @@ -22,14 +23,48 @@ namespace Babylon void RecordVertexBuffer(VertexBuffer* vertexBuffer, uint32_t location, uint32_t byteOffset, uint32_t byteStride, uint32_t numElements, uint32_t type, bool normalized, uint32_t divisor); void SetIndexBuffer(bgfx::Encoder* encoder, uint32_t firstIndex, uint32_t numIndices); + bool SetExpandedIndexBuffer(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t firstIndex, uint32_t numIndices); + bool SetExpandedUnindexedBuffer(bgfx::Encoder* encoder, PrimitiveModeExpansion::Mode mode, uint32_t numVertices); void SetVertexBuffers(bgfx::Encoder* encoder, uint32_t startVertex, uint32_t numVertices, uint32_t instanceCount, const VertexBuffer::InstanceDataLayout& instanceDataLayout); const std::map& GetInstances() const { return m_vertexBufferInstances; } + // True when any recorded instance source is a GPU storage buffer, in which case the CPU + // BuildInstanceDataBuffer path is skipped and NativeEngine binds a GPU-repacked buffer. + bool HasStorageInstances() const; + + // Vertex inputs of a transform feedback program (locations at or above + // TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE), keyed by input index. The emulating compute + // shader reads and decodes them from the vertex buffers bound as raw compute buffers. + struct TransformFeedbackInput + { + VertexBuffer* Buffer{}; + uint32_t ByteOffset{}; + uint32_t ByteStride{}; + uint32_t NumElements{}; + Graphics::TransformFeedbackInputType Type{}; + bool Normalized{}; + }; + const std::map& GetTransformFeedbackInputs() const { return m_transformFeedbackInputs; } private: + struct UnindexedExpansionKey final + { + PrimitiveModeExpansion::Mode Mode{}; + uint32_t VertexCount{}; + + bool operator<(const UnindexedExpansionKey& other) const; + }; + + struct ExpandedBuffer final + { + bgfx::IndexBufferHandle Handle{bgfx::kInvalidHandle}; + uint32_t IndexCount{}; + }; + Graphics::DeviceContext& m_deviceContext; const uintptr_t m_deviceId{}; IndexBuffer* m_indexBuffer{}; + std::map m_unindexedExpandedBuffers{}; struct VertexBufferRecord { @@ -48,6 +83,7 @@ namespace Babylon std::map m_vertexBufferRecords{}; std::map m_vertexBufferInstances; + std::map m_transformFeedbackInputs; bool m_disposed{}; }; diff --git a/Plugins/NativeEngine/Source/VertexBuffer.cpp b/Plugins/NativeEngine/Source/VertexBuffer.cpp index c93d1a7125..4fa74311bf 100644 --- a/Plugins/NativeEngine/Source/VertexBuffer.cpp +++ b/Plugins/NativeEngine/Source/VertexBuffer.cpp @@ -1,4 +1,5 @@ #include "VertexBuffer.h" +#include "StorageBuffer.h" #include "Babylon/Graphics/DeviceContext.h" #include #include @@ -26,6 +27,11 @@ namespace Babylon return; } + if (m_gpuStorage) + { + m_gpuStorage->Dispose(); + } + if (bgfx::isValid(m_handle) && m_deviceId == m_deviceContext.GetDeviceId()) { if (m_dynamic) @@ -45,6 +51,16 @@ namespace Babylon void VertexBuffer::Update(gsl::span bytes, size_t byteOffset) { + if (m_gpuStorage) + { + if (byteOffset > UINT32_MAX) + { + throw std::runtime_error{"Cannot update vertex buffer due to buffer overflow"}; + } + m_gpuStorage->Update(bytes, static_cast(byteOffset)); + return; + } + if (!m_dynamic) { throw std::runtime_error{"Cannot update non-dynamic vertex buffer"}; @@ -89,7 +105,7 @@ namespace Babylon throw std::runtime_error{"Attributes of a vertex buffer must have the same byte stride"}; } - if (!bgfx::isValid(m_handle)) + if (!m_gpuStorage && !bgfx::isValid(m_handle)) { auto releaseFn = [](void*, void* userData) { delete reinterpret_cast(userData); @@ -103,13 +119,18 @@ namespace Babylon layout.m_stride = static_cast(byteStride); layout.end(); + // As in WebGL, where any vertex buffer can feed transform feedback, keep built buffers readable + // by the compute shaders that emulate it. Raw views address whole 32-bit words. + m_computeReadable = (bgfx::getCaps()->supported & BGFX_CAPS_COMPUTE) != 0 && memory->size % sizeof(uint32_t) == 0; + const uint16_t flags = m_computeReadable ? BGFX_BUFFER_COMPUTE_READ : BGFX_BUFFER_NONE; + if (m_dynamic) { - m_dynamicHandle = bgfx::createDynamicVertexBuffer(memory, layout); + m_dynamicHandle = bgfx::createDynamicVertexBuffer(memory, layout, flags); } else { - m_handle = bgfx::createVertexBuffer(memory, layout); + m_handle = bgfx::createVertexBuffer(memory, layout, flags); } if (!bgfx::isValid(m_handle)) @@ -121,7 +142,11 @@ namespace Babylon void VertexBuffer::Set(bgfx::Encoder* encoder, uint8_t stream, uint32_t startVertex, uint32_t numVertices, bgfx::VertexLayoutHandle layout) { - if (m_dynamic) + if (m_gpuStorage) + { + m_gpuStorage->SetVertex(encoder, stream, startVertex, numVertices, layout); + } + else if (m_dynamic) { encoder->setVertexBuffer(stream, m_dynamicHandle, startVertex, numVertices, layout); } @@ -131,6 +156,66 @@ namespace Babylon } } + StorageBuffer& VertexBuffer::PromoteToGpuStorage() + { + if (m_gpuStorage) + { + return *m_gpuStorage; + } + if (m_disposed || m_deviceId != m_deviceContext.GetDeviceId()) + { + throw std::runtime_error{"Cannot use a disposed or stale vertex buffer for transform feedback"}; + } + if (bgfx::isValid(m_handle)) + { + // Build hands the CPU bytes to bgfx, so they can no longer seed the storage buffer. + throw std::runtime_error{"Transform feedback cannot use a vertex buffer already bound as a per-vertex stream"}; + } + if (m_bytes.empty() || m_bytes.size() > UINT32_MAX) + { + throw std::runtime_error{"Transform feedback vertex buffers must not be empty"}; + } + + // A 4-byte storage stride keeps any float-aligned update offset addressable. The storage + // pads byte- and short-component data to whole words. + auto storage = std::make_unique(m_deviceContext, static_cast(m_bytes.size()), static_cast(sizeof(float))); + storage->Update(m_bytes, 0); + m_gpuStorage = std::move(storage); + m_bytes.clear(); + m_bytes.shrink_to_fit(); + return *m_gpuStorage; + } + + uint32_t VertexBuffer::ByteLength() const + { + return m_gpuStorage ? m_gpuStorage->ByteLength() : static_cast(m_bytes.size()); + } + + void VertexBuffer::SetComputeRead(bgfx::Encoder* encoder, uint8_t stage) + { + if (m_gpuStorage || !bgfx::isValid(m_handle)) + { + PromoteToGpuStorage().SetCompute(encoder, stage, bgfx::Access::Read); + } + else if (!m_computeReadable) + { + throw std::runtime_error{"Transform feedback cannot read this vertex buffer on this renderer"}; + } + else if (m_dynamic) + { + encoder->setBuffer(stage, m_dynamicHandle, bgfx::Access::Read); + } + else + { + encoder->setBuffer(stage, m_handle, bgfx::Access::Read); + } + } + + StorageBuffer* VertexBuffer::InstanceInfo::ResolveStorage() const + { + return Buffer != nullptr ? Buffer->GpuStorage() : nullptr; + } + VertexBuffer::InstanceDataLayout VertexBuffer::CreateInstanceDataLayout( const std::map& instances, const std::map& builtInSlots, diff --git a/Plugins/NativeEngine/Source/VertexBuffer.h b/Plugins/NativeEngine/Source/VertexBuffer.h index 1841ac9d22..059d3773ae 100644 --- a/Plugins/NativeEngine/Source/VertexBuffer.h +++ b/Plugins/NativeEngine/Source/VertexBuffer.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,8 @@ namespace Babylon class DeviceContext; } + class StorageBuffer; + class VertexBuffer final { public: @@ -33,12 +36,27 @@ namespace Babylon void Set(bgfx::Encoder* encoder, uint8_t stream, uint32_t startVertex, uint32_t numVertices, bgfx::VertexLayoutHandle layout); + // Moves the contents into a GPU storage buffer so compute work (transform feedback + // emulation) can read and write it; later updates, vertex binds and instance repacks use the + // storage buffer. Must happen before the buffer is built as a regular vertex stream. + StorageBuffer& PromoteToGpuStorage(); + StorageBuffer* GpuStorage() const { return m_gpuStorage.get(); } + uint32_t ByteLength() const; + + // Binds the contents as a read-only raw compute buffer (transform feedback vertex input). A + // buffer that was never built is promoted to GPU storage; a built one must be compute readable. + void SetComputeRead(bgfx::Encoder* encoder, uint8_t stage); + struct InstanceInfo { VertexBuffer* Buffer{}; uint32_t Offset{}; uint32_t Stride{}; uint32_t ElementSize{}; + + // The GPU storage holding this attribute when Buffer received transform feedback output. + // Such instances are repacked on the GPU (see InstanceRepacker) instead of on the CPU. + StorageBuffer* ResolveStorage() const; }; struct InstanceDataLayout @@ -65,6 +83,7 @@ namespace Babylon std::vector m_bytes{}; const bool m_dynamic{}; uint32_t m_byteStride{}; + bool m_computeReadable{}; union { @@ -72,6 +91,8 @@ namespace Babylon bgfx::DynamicVertexBufferHandle m_dynamicHandle; }; + std::unique_ptr m_gpuStorage{}; + bool m_disposed{}; }; }; diff --git a/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp b/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp index 97b9f15279..3e60af8fa3 100644 --- a/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp +++ b/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp @@ -1,9 +1,34 @@ #include #include +#include #include namespace { + // Babylon.js stores matrices in a plain Array when useHighPrecisionMatrix is set. + std::array ReadMatrix(const Napi::Object& transform) + { + const auto value{transform.Get("_m")}; + std::array m{}; + if (value.IsArray()) + { + const auto elements{value.As()}; + for (uint32_t i = 0; i < m.size(); ++i) + { + m[i] = elements.Get(i).As().FloatValue(); + } + } + else + { + const auto elements{value.As()}; + for (uint32_t i = 0; i < m.size(); ++i) + { + m[i] = elements[i]; + } + } + return m; + } + void MatrixScaleAdd(const float inputMatrix[16], float scale, float outputMatrix[16]) { for (size_t i = 0; i < 16; ++i) @@ -39,9 +64,10 @@ namespace { auto coordinates{info[0].As()}; const auto transform{info[1].As()}; - const auto m{transform.Get("_m").As()}; - const auto offset{info[2].As().Uint32Value()}; - const auto length{info[3].As().Uint32Value()}; + const auto m{ReadMatrix(transform)}; + // offset and length are optional in Babylon.js, defaulting to the whole array. + const auto offset{info[2].IsUndefined() ? 0 : info[2].As().Uint32Value()}; + const auto length{info[3].IsUndefined() ? static_cast(coordinates.ElementLength()) : info[3].As().Uint32Value()}; for (size_t index = offset; index < offset + length; index += 3) { @@ -61,9 +87,10 @@ namespace { auto normals{info[0].As()}; const auto transform{info[1].As()}; - const auto m{transform.Get("_m").As()}; - const auto offset{info[2].As().Uint32Value()}; - const auto length{info[3].As().Uint32Value()}; + const auto m{ReadMatrix(transform)}; + // offset and length are optional in Babylon.js, defaulting to the whole array. + const auto offset{info[2].IsUndefined() ? 0 : info[2].As().Uint32Value()}; + const auto length{info[3].IsUndefined() ? static_cast(normals.ElementLength()) : info[3].As().Uint32Value()}; for (size_t index = offset; index < offset + length; index += 3) { @@ -79,9 +106,10 @@ namespace { auto normals{info[0].As()}; const auto transform{info[1].As()}; - const auto m{transform.Get("_m").As()}; - const auto offset{info[2].As().Uint32Value()}; - const auto length{info[3].As().Uint32Value()}; + const auto m{ReadMatrix(transform)}; + // offset and length are optional in Babylon.js, defaulting to the whole array. + const auto offset{info[2].IsUndefined() ? 0 : info[2].As().Uint32Value()}; + const auto length{info[3].IsUndefined() ? static_cast(normals.ElementLength()) : info[3].As().Uint32Value()}; for (size_t index = offset; index < offset + length; index += 4) { @@ -107,8 +135,8 @@ namespace void FlipFaces(const Napi::CallbackInfo& info) { auto indices{info[0].As()}; - const auto offset{info[1].As().Uint32Value()}; - const auto length{info[2].As().Uint32Value()}; + const auto offset{info[1].IsUndefined() ? 0 : info[1].As().Uint32Value()}; + const auto length{info[2].IsUndefined() ? static_cast(indices.ElementLength()) : info[2].As().Uint32Value()}; if (indices.TypedArrayType() == napi_typedarray_type::napi_int32_array) { diff --git a/Plugins/ShaderCache/InternalInclude/Babylon/Plugins/ShaderCacheInternal.h b/Plugins/ShaderCache/InternalInclude/Babylon/Plugins/ShaderCacheInternal.h index 7ffc110c94..d90a98ae51 100644 --- a/Plugins/ShaderCache/InternalInclude/Babylon/Plugins/ShaderCacheInternal.h +++ b/Plugins/ShaderCache/InternalInclude/Babylon/Plugins/ShaderCacheInternal.h @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -11,10 +12,19 @@ namespace Babylon::Plugins::ShaderCache { // Adds a shader to the cache with the given vertex and fragment shader sources and shader information. + // instancedAttributes identifies a divisor-driven instanced variant (see ShaderCompiler::Compile); + // an empty map is the base program. // Returns the added shader information from the cache. - std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo); + std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo, + const std::map& instancedAttributes = {}); // Gets the shader from the cache for the given vertex and fragment shader source code. // Returns the shader information from the cache or nullptr if not found. - std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource); + std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource, + const std::map& instancedAttributes = {}); + + // Adds/gets the compute shader emulating a transform feedback vertex shader that captures + // varyings (see ShaderCompiler::CompileTransformFeedback). Keyed separately from vertex/fragment programs. + std::shared_ptr AddTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings, Graphics::BgfxShaderInfo shaderInfo); + std::shared_ptr GetTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings); } diff --git a/Plugins/ShaderCache/Source/ShaderCache.cpp b/Plugins/ShaderCache/Source/ShaderCache.cpp index a3880dac59..32550be74b 100644 --- a/Plugins/ShaderCache/Source/ShaderCache.cpp +++ b/Plugins/ShaderCache/Source/ShaderCache.cpp @@ -51,23 +51,45 @@ namespace Babylon::Plugins::ShaderCache return ShaderCacheImpl::Instance->Load(stream); } - std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo) + std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo, + const std::map& instancedAttributes) { if (!ShaderCacheImpl::Instance) { throw std::runtime_error("ShaderCache is not enabled."); } - return ShaderCacheImpl::Instance->AddShader(vertexSource, fragmentSource, std::move(shaderInfo)); + return ShaderCacheImpl::Instance->AddShader(vertexSource, fragmentSource, std::move(shaderInfo), instancedAttributes); } - std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource) + std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource, + const std::map& instancedAttributes) { if (!ShaderCacheImpl::Instance) { throw std::runtime_error("ShaderCache is not enabled."); } - return ShaderCacheImpl::Instance->GetShader(vertexSource, fragmentSource); + return ShaderCacheImpl::Instance->GetShader(vertexSource, fragmentSource, instancedAttributes); + } + + std::shared_ptr AddTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings, Graphics::BgfxShaderInfo shaderInfo) + { + if (!ShaderCacheImpl::Instance) + { + throw std::runtime_error("ShaderCache is not enabled."); + } + + return ShaderCacheImpl::Instance->AddTransformFeedbackShader(vertexSource, varyings, std::move(shaderInfo)); + } + + std::shared_ptr GetTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings) + { + if (!ShaderCacheImpl::Instance) + { + throw std::runtime_error("ShaderCache is not enabled."); + } + + return ShaderCacheImpl::Instance->GetTransformFeedbackShader(vertexSource, varyings); } } diff --git a/Plugins/ShaderCache/Source/ShaderCacheImpl.cpp b/Plugins/ShaderCache/Source/ShaderCacheImpl.cpp index 077a132064..82638c9f2d 100644 --- a/Plugins/ShaderCache/Source/ShaderCacheImpl.cpp +++ b/Plugins/ShaderCache/Source/ShaderCacheImpl.cpp @@ -17,6 +17,21 @@ namespace stream.read(string.data(), stringSize); } + void SaveBytes(std::ostream& stream, const std::vector& bytes) + { + uint32_t byteCount{static_cast(bytes.size())}; + stream.write(reinterpret_cast(&byteCount), sizeof(uint32_t)); + stream.write(reinterpret_cast(bytes.data()), bytes.size()); + } + + void LoadBytes(std::istream& stream, std::vector& bytes) + { + uint32_t byteCount{}; + stream.read(reinterpret_cast(&byteCount), sizeof(uint32_t)); + bytes.resize(byteCount); + stream.read(reinterpret_cast(bytes.data()), bytes.size()); + } + std::string NormalizeLineEndings(std::string_view source) { std::string result; @@ -45,30 +60,53 @@ namespace Babylon::Plugins::ShaderCache // version-5 entries for those shaders skip compilation and omit the uniform. // 7: shader binaries are bgfx BGFX_SHADER_BIN_VERSION 13 and, on D3D, record each // sampler's register index and its paired texture's dimension. - static const uint32_t CACHE_VERSION = 7; + // 8: preserve application uniform aliases and inject nearest-sampler state. + // 9: bnFragCoordTargetSize became bnRenderTargetTransform, which vertex shaders also read + // to render cube faces in GL row order, and dFdy now scales by it. + // 10: persist ComputeBytes, append a separately keyed section of transform feedback + // compute shaders, and key instanced variants by their routed attributes. + static const uint32_t CACHE_VERSION = 10; void ShaderCacheImpl::Clear() { m_cache.clear(); + m_computeCache.clear(); } uint32_t ShaderCacheImpl::Save(std::ostream& stream) { uint32_t cacheVersion{CACHE_VERSION}; stream.write(reinterpret_cast(&cacheVersion), sizeof(uint32_t)); - uint32_t cacheSize{static_cast(m_cache.size())}; + SaveEntries(stream, m_cache); + SaveEntries(stream, m_computeCache); + return static_cast(m_cache.size() + m_computeCache.size()); + } + + uint32_t ShaderCacheImpl::Load(std::istream& stream) + { + uint32_t cacheVersion; + stream.read(reinterpret_cast(&cacheVersion), sizeof(uint32_t)); + if (cacheVersion != CACHE_VERSION) + { + return 0; + } + + const uint32_t programCount = LoadEntries(stream, m_cache); + const uint32_t computeCount = LoadEntries(stream, m_computeCache); + return programCount + computeCount; + } + + void ShaderCacheImpl::SaveEntries(std::ostream& stream, const ShaderMap& entries) + { + uint32_t cacheSize{static_cast(entries.size())}; stream.write(reinterpret_cast(&cacheSize), sizeof(uint32_t)); - for (auto& entry : m_cache) + for (auto& entry : entries) { stream.write(reinterpret_cast(&entry.first), sizeof(ShaderHash)); const auto& info = entry.second; - uint32_t vertexBytes{static_cast(info->VertexBytes.size())}; - stream.write(reinterpret_cast(&vertexBytes), sizeof(uint32_t)); - stream.write((const char*)info->VertexBytes.data(), info->VertexBytes.size()); - - uint32_t fragmentBytes{static_cast(info->FragmentBytes.size())}; - stream.write(reinterpret_cast(&fragmentBytes), sizeof(uint32_t)); - stream.write((const char*)info->FragmentBytes.data(), info->FragmentBytes.size()); + SaveBytes(stream, info->VertexBytes); + SaveBytes(stream, info->FragmentBytes); + SaveBytes(stream, info->ComputeBytes); uint32_t vertexAttributeLocationCount{static_cast(info->VertexAttributeLocations.size())}; stream.write(reinterpret_cast(&vertexAttributeLocationCount), sizeof(uint32_t)); @@ -93,34 +131,28 @@ namespace Babylon::Plugins::ShaderCache SaveString(stream, uniformStages.first); stream.write(reinterpret_cast(&uniformStages.second), sizeof(uint8_t)); } + const auto nameCount = static_cast(info->UniformNames.size()); + stream.write(reinterpret_cast(&nameCount), sizeof(nameCount)); + for (const auto& [compiledName, originalName] : info->UniformNames) + { + SaveString(stream, compiledName); + SaveString(stream, originalName); + } } - return cacheSize; } - uint32_t ShaderCacheImpl::Load(std::istream& stream) + uint32_t ShaderCacheImpl::LoadEntries(std::istream& stream, ShaderMap& entries) { - uint32_t cacheVersion; - stream.read(reinterpret_cast(&cacheVersion), sizeof(uint32_t)); - if (cacheVersion != CACHE_VERSION) - { - return 0; - } - - uint32_t cacheSize; + uint32_t cacheSize{}; stream.read(reinterpret_cast(&cacheSize), sizeof(uint32_t)); for (unsigned int i = 0; i < cacheSize; i++) { ShaderHash hash; stream.read(reinterpret_cast(&hash), sizeof(ShaderHash)); std::shared_ptr info = std::make_shared(); - uint32_t vertexBytes; - stream.read(reinterpret_cast(&vertexBytes), sizeof(uint32_t)); - info->VertexBytes.resize(vertexBytes); - stream.read(reinterpret_cast(info->VertexBytes.data()), info->VertexBytes.size()); - uint32_t fragmentBytes; - stream.read(reinterpret_cast(&fragmentBytes), sizeof(uint32_t)); - info->FragmentBytes.resize(fragmentBytes); - stream.read(reinterpret_cast(info->FragmentBytes.data()), info->FragmentBytes.size()); + LoadBytes(stream, info->VertexBytes); + LoadBytes(stream, info->FragmentBytes); + LoadBytes(stream, info->ComputeBytes); uint32_t vertexAttributeLocationCount; stream.read(reinterpret_cast(&vertexAttributeLocationCount), sizeof(uint32_t)); for (unsigned int vertexAttributeLocation = 0; vertexAttributeLocation < vertexAttributeLocationCount; vertexAttributeLocation++) @@ -154,27 +186,73 @@ namespace Babylon::Plugins::ShaderCache info->UniformStages[stageName] = stageIndex; } - m_cache.emplace(hash, std::move(info)); + uint32_t nameCount{}; + stream.read(reinterpret_cast(&nameCount), sizeof(nameCount)); + for (uint32_t index = 0; index < nameCount; ++index) + { + std::string compiledName; + std::string originalName; + LoadString(stream, compiledName); + LoadString(stream, originalName); + info->UniformNames.emplace(std::move(compiledName), std::move(originalName)); + } + entries.emplace(hash, std::move(info)); } return cacheSize; } - std::shared_ptr ShaderCacheImpl::AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo) + std::shared_ptr ShaderCacheImpl::AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo, const std::map& instancedAttributes) { - return m_cache.try_emplace(Hash(vertexSource, fragmentSource), std::make_shared(std::move(shaderInfo))).first->second; + return m_cache.try_emplace(Hash(vertexSource, fragmentSource, instancedAttributes), std::make_shared(std::move(shaderInfo))).first->second; } - std::shared_ptr ShaderCacheImpl::GetShader(std::string_view vertexSource, std::string_view fragmentSource) + std::shared_ptr ShaderCacheImpl::GetShader(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { - const auto iter = m_cache.find(Hash(vertexSource, fragmentSource)); + const auto iter = m_cache.find(Hash(vertexSource, fragmentSource, instancedAttributes)); return (iter == m_cache.end() ? nullptr : iter->second); } - ShaderCacheImpl::ShaderHash ShaderCacheImpl::Hash(std::string_view vertexSource, std::string_view fragmentSource) + std::shared_ptr ShaderCacheImpl::AddTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings, Graphics::BgfxShaderInfo shaderInfo) + { + return m_computeCache.try_emplace(Hash(vertexSource, varyings), std::make_shared(std::move(shaderInfo))).first->second; + } + + std::shared_ptr ShaderCacheImpl::GetTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings) + { + const auto iter = m_computeCache.find(Hash(vertexSource, varyings)); + return (iter == m_computeCache.end() ? nullptr : iter->second); + } + + ShaderCacheImpl::ShaderHash ShaderCacheImpl::Hash(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { std::string normalizeVertexSource = NormalizeLineEndings(vertexSource); + // Instanced variants share the base sources; the routed attributes select the variant. + // The NUL separator cannot occur in GLSL source, so variant keys never alias base keys. + if (!instancedAttributes.empty()) + { + normalizeVertexSource.push_back('\0'); + normalizeVertexSource += "instanced:"; + for (const auto& [name, location] : instancedAttributes) + { + normalizeVertexSource += name + "=" + std::to_string(location) + ";"; + } + } std::string normalizeFragmentSource = NormalizeLineEndings(fragmentSource); return {XXH3_64bits(normalizeVertexSource.data(), normalizeVertexSource.size()), XXH3_64bits(normalizeFragmentSource.data(), normalizeFragmentSource.size())}; } + + ShaderCacheImpl::ShaderHash ShaderCacheImpl::Hash(std::string_view vertexSource, const std::vector& varyings) + { + // The NUL-separated varyings suffix keeps programs capturing different varyings distinct. + std::string key = NormalizeLineEndings(vertexSource); + key.push_back('\0'); + key += "transform-feedback:"; + for (const auto& varying : varyings) + { + key += varying + ","; + } + const auto hash = XXH3_128bits(key.data(), key.size()); + return {hash.low64, hash.high64}; + } } diff --git a/Plugins/ShaderCache/Source/ShaderCacheImpl.h b/Plugins/ShaderCache/Source/ShaderCacheImpl.h index 60e14f1964..eccf56c89c 100644 --- a/Plugins/ShaderCache/Source/ShaderCacheImpl.h +++ b/Plugins/ShaderCache/Source/ShaderCacheImpl.h @@ -21,16 +21,25 @@ namespace Babylon::Plugins::ShaderCache void Clear(); - std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo); - std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource); + std::shared_ptr AddShader(std::string_view vertexSource, std::string_view fragmentSource, Graphics::BgfxShaderInfo shaderInfo, const std::map& instancedAttributes); + std::shared_ptr GetShader(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes); + + std::shared_ptr AddTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings, Graphics::BgfxShaderInfo shaderInfo); + std::shared_ptr GetTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings); static inline std::unique_ptr Instance; private: using ShaderHash = std::pair; + using ShaderMap = std::map>; + + ShaderHash Hash(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes); + ShaderHash Hash(std::string_view vertexSource, const std::vector& varyings); - ShaderHash Hash(std::string_view vertexSource, std::string_view fragmentSource); + static void SaveEntries(std::ostream& stream, const ShaderMap& entries); + static uint32_t LoadEntries(std::istream& stream, ShaderMap& entries); - std::map> m_cache; + ShaderMap m_cache; + ShaderMap m_computeCache; }; } diff --git a/Plugins/ShaderCompiler/InternalInclude/Babylon/Plugins/ShaderCompiler.h b/Plugins/ShaderCompiler/InternalInclude/Babylon/Plugins/ShaderCompiler.h index af8d24d68a..35e8f15e1c 100644 --- a/Plugins/ShaderCompiler/InternalInclude/Babylon/Plugins/ShaderCompiler.h +++ b/Plugins/ShaderCompiler/InternalInclude/Babylon/Plugins/ShaderCompiler.h @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace Babylon::Plugins @@ -23,5 +24,19 @@ namespace Babylon::Plugins /// each attribute from the slot bgfx fills. An empty map preserves the legacy per-vertex /// mapping for all non-built-in attributes. Graphics::BgfxShaderInfo Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes = {}); + + /// Compiles a single GLSL compute shader (GLSL ES 3.10 `layout(local_size_*)`) into a bgfx + /// CSH (compute) shader binary for the active backend (HLSL/DXBC, HLSL/DXIL, SPIR-V, MSL or + /// GLSL ES 3.10). Storage buffers/images are bound at dispatch time via bgfx::setBuffer/setImage + /// using the stage index that equals the GLSL binding; samplers need explicit bindings past + /// the storage buffer stages. Requires BGFX_CAPS_COMPUTE at runtime. + Graphics::BgfxShaderInfo CompileCompute(std::string_view computeSource); + + /// Emulates WebGL2 transform feedback: compiles the capturing GLSL ES 3.00 vertex shader into + /// a compute shader that runs once per vertex and writes `varyings` (interleaved, tightly + /// packed floats) into a storage buffer. Vertex inputs are read from a float storage buffer; + /// VertexAttributeLocations maps each input to TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE + i. + /// Supported wherever CompileCompute is. + Graphics::BgfxShaderInfo CompileTransformFeedback(std::string_view vertexSource, const std::vector& varyings); }; } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp index fb43e9daf6..d59c1144f1 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp @@ -1,6 +1,12 @@ #include "ShaderCompilerCommon.h" +#include #include #include +#include +#include +#include +#include +#include #define BGFX_UNIFORM_FRAGMENTBIT UINT8_C(0x10) // Copy-pasta from bgfx_p.h #define BGFX_UNIFORM_SAMPLERBIT UINT8_C(0x20) // Copy-pasta from bgfx_p.h @@ -13,6 +19,618 @@ namespace bgfx namespace Babylon::ShaderCompilerCommon { + namespace + { + struct ShaderToken + { + size_t Offset; + std::string_view Text; + }; + + std::vector TokenizeShader(std::string_view source) + { + std::vector tokens; + for (size_t offset = 0; offset < source.size();) + { + const auto start = offset; + const auto character = static_cast(source[offset]); + if (std::isspace(character)) + { + ++offset; + continue; + } + if (source.substr(offset, 2) == "//") + { + const auto end = source.find('\n', offset + 2); + offset = end == std::string_view::npos ? source.size() : end; + continue; + } + if (source.substr(offset, 2) == "/*") + { + const auto end = source.find("*/", offset + 2); + if (end == std::string_view::npos) + { + throw std::runtime_error{"Unterminated shader comment."}; + } + offset = end + 2; + continue; + } + if (character == '"') + { + ++offset; + while (offset < source.size() && source[offset] != '"') + { + offset += source[offset] == '\\' && offset + 1 < source.size() ? 2 : 1; + } + if (offset == source.size()) + { + throw std::runtime_error{"Unterminated shader string."}; + } + ++offset; + } + else if (std::isalpha(character) || character == '_') + { + do + { + ++offset; + } while (offset < source.size() && + (std::isalnum(static_cast(source[offset])) || source[offset] == '_')); + } + else + { + ++offset; + } + tokens.push_back({start, source.substr(start, offset - start)}); + } + return tokens; + } + } + + std::string PreprocessShader(EShLanguage stage, std::string_view source) + { + std::string input{source}; + size_t insertion{}; + const auto tokens = TokenizeShader(source); + if (tokens.size() >= 2 && tokens[0].Text == "#" && tokens[1].Text == "version") + { + const auto end = source.find('\n', tokens[0].Offset); + if (end == std::string_view::npos) + { + input += '\n'; + insertion = input.size(); + } + else + { + insertion = end + 1; + } + } + // Keep #version first, including after glslang serializes the preprocessed source. + input.insert(insertion, "#extension GL_EXT_shader_implicit_conversions : enable\n"); + glslang::TShader shader{stage}; + const char* data = input.data(); + const int length = gsl::narrow_cast(input.size()); + shader.setStringsWithLengths(&data, &length, 1); + glslang::TShader::ForbidIncluder includer; + std::string result; + if (!shader.preprocess(GetDefaultResources(), 310, EProfile::EEsProfile, true, true, EShMsgDefault, &result, includer)) + { + throw std::runtime_error{shader.getInfoLog()}; + } + return result; + } + + namespace + { + // Scalar or vector of float, int or uint: returns the component count, or 0 for other types. + uint32_t ComponentCount(std::string_view type) + { + if (type == "float" || type == "int" || type == "uint") + { + return 1; + } + for (const std::string_view prefix : {"vec", "ivec", "uvec"}) + { + if (type.size() == prefix.size() + 1 && type.starts_with(prefix) && type.back() >= '2' && type.back() <= '4') + { + return static_cast(type.back() - '0'); + } + } + return 0; + } + + // The bit cast that stores a captured component in the float output buffer unchanged. + std::string_view OutputBitCast(std::string_view type) + { + if (type == "int" || type.starts_with("ivec")) + { + return "intBitsToFloat"; + } + if (type == "uint" || type.starts_with("uvec")) + { + return "uintBitsToFloat"; + } + return ""; + } + + bool IsInterfaceQualifier(std::string_view token) + { + return token == "flat" || token == "smooth" || token == "noperspective" || token == "centroid" || + token == "invariant" || token == "highp" || token == "mediump" || token == "lowp"; + } + + size_t FindClosingParenthesis(const std::vector& tokens, size_t open) + { + int depth{}; + for (size_t index = open; index < tokens.size(); ++index) + { + if (tokens[index].Text == "(") + { + ++depth; + } + else if (tokens[index].Text == ")" && --depth == 0) + { + return index; + } + } + throw std::runtime_error{"Transform feedback shader has unbalanced parentheses."}; + } + + // Offsets of the top-level commas separating the arguments of the call whose parentheses + // span [open, close]. + std::vector FindArgumentSeparators(const std::vector& tokens, size_t open, size_t close) + { + std::vector separators; + int depth{}; + for (size_t index = open + 1; index < close; ++index) + { + const auto text = tokens[index].Text; + if (text == "(" || text == "[") + { + ++depth; + } + else if (text == ")" || text == "]") + { + --depth; + } + else if (text == "," && depth == 0) + { + separators.push_back(index); + } + } + return separators; + } + } + + TransformFeedbackShader GenerateTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings) + { + if (varyings.empty()) + { + throw std::runtime_error{"Transform feedback requires at least one varying."}; + } + + // Preprocessor directives (only #version and #extension survive preprocessing) are blanked + // in the tokenized copy so statement boundaries are not confused by them. + std::string tokenText{vertexSource}; + for (size_t lineStart = 0; lineStart < tokenText.size();) + { + auto lineEnd = tokenText.find('\n', lineStart); + lineEnd = lineEnd == std::string::npos ? tokenText.size() : lineEnd; + const auto first = tokenText.find_first_not_of(" \t\r", lineStart); + if (first != std::string::npos && first < lineEnd && tokenText[first] == '#') + { + std::fill(tokenText.begin() + lineStart, tokenText.begin() + lineEnd, ' '); + } + lineStart = lineEnd + 1; + } + const auto tokens = TokenizeShader(tokenText); + + struct Variable + { + std::string Type; + std::string Name; + }; + std::vector inputs; + std::map outputs; + + struct Replacement + { + size_t Begin; + size_t End; + std::string Text; + }; + std::vector replacements; + const auto replaceToken = [&](const ShaderToken& token, std::string text) { + replacements.push_back({token.Offset, token.Offset + token.Text.size(), std::move(text)}); + }; + + // Rewrite top-level declarations: vertex inputs and outputs become private globals, and + // samplers get explicit bindings above the two buffer bindings (glslang leaves them at 0). + uint32_t nextSamplerBinding{Graphics::TRANSFORM_FEEDBACK_FIRST_SAMPLER_BINDING}; + int depth{}; + size_t statementStart{}; + for (size_t index = 0; index < tokens.size(); ++index) + { + const auto text = tokens[index].Text; + if (text == "{" || text == "(") + { + ++depth; + continue; + } + if (text == "}" || text == ")") + { + if (--depth == 0 && text == "}") + { + statementStart = index + 1; + } + continue; + } + if (depth != 0 || text != ";") + { + continue; + } + + const size_t begin = statementStart; + const size_t end = index; + statementStart = index + 1; + + bool isInput{}; + bool isOutput{}; + bool isUniform{}; + bool hasLayout{}; + size_t typeIndex{begin}; + for (; typeIndex < end; ++typeIndex) + { + const auto qualifier = tokens[typeIndex].Text; + if (qualifier == "in") + { + isInput = true; + } + else if (qualifier == "out") + { + isOutput = true; + } + else if (qualifier == "uniform") + { + isUniform = true; + } + else if (qualifier == "layout") + { + hasLayout = true; + typeIndex = FindClosingParenthesis(tokens, typeIndex + 1); + } + else if (!IsInterfaceQualifier(qualifier)) + { + break; + } + } + + if (isUniform && typeIndex < end) + { + const auto type = tokens[typeIndex].Text; + const bool isSampler = type.starts_with("sampler") || type.starts_with("isampler") || type.starts_with("usampler"); + if (isSampler) + { + if (hasLayout) + { + throw std::runtime_error{"Transform feedback shaders cannot declare sampler layouts."}; + } + if (nextSamplerBinding >= Graphics::TRANSFORM_FEEDBACK_MAX_SAMPLER_BINDING) + { + throw std::runtime_error{"Transform feedback shader declares too many samplers."}; + } + replacements.push_back({tokens[begin].Offset, tokens[begin].Offset, + "layout(binding = " + std::to_string(nextSamplerBinding++) + ") "}); + } + continue; + } + + if (!isInput && !isOutput) + { + continue; + } + if (isInput && isOutput) + { + throw std::runtime_error{"Transform feedback shader has an invalid interface declaration."}; + } + if (typeIndex + 1 >= end) + { + throw std::runtime_error{"Transform feedback shader has an incomplete interface declaration."}; + } + + const std::string type{tokens[typeIndex].Text}; + if (ComponentCount(type) == 0) + { + throw std::runtime_error{"Transform feedback supports only scalar and vector float, int and uint attributes and varyings ('" + type + "')."}; + } + + std::string declaration = type + " "; + for (size_t nameIndex = typeIndex + 1; nameIndex < end; nameIndex += 2) + { + const auto name = tokens[nameIndex].Text; + if (nameIndex + 1 < end && tokens[nameIndex + 1].Text != ",") + { + throw std::runtime_error{"Transform feedback does not support array or initialized interface variables ('" + std::string{name} + "')."}; + } + if (isInput) + { + inputs.push_back({type, std::string{name}}); + } + else + { + outputs[std::string{name}] = type; + } + declaration += std::string{name} + (nameIndex + 2 < end ? ", " : ""); + } + replacements.push_back({tokens[begin].Offset, tokens[end].Offset, std::move(declaration)}); + } + + // Rewrite vertex-stage built-ins and implicit-LOD sampling, which compute shaders lack. + for (size_t index = 0; index < tokens.size(); ++index) + { + const auto& token = tokens[index]; + if (token.Text == "main") + { + replaceToken(token, "bnTfMain"); + } + else if (token.Text == "gl_VertexID") + { + replaceToken(token, "bnTfVertexID"); + } + else if (token.Text == "gl_InstanceID") + { + replaceToken(token, "bnTfInstanceID"); + } + else if (token.Text == "gl_Position") + { + replaceToken(token, "bnTfPosition"); + } + else if (token.Text == "gl_PointSize") + { + replaceToken(token, "bnTfPointSize"); + } + else if ((token.Text == "texture" || token.Text == "textureProj" || token.Text == "textureOffset") && + index + 1 < tokens.size() && tokens[index + 1].Text == "(") + { + const size_t close = FindClosingParenthesis(tokens, index + 1); + const auto separators = FindArgumentSeparators(tokens, index + 1, close); + const bool offset = token.Text == "textureOffset"; + if (separators.size() != (offset ? 2u : 1u)) + { + throw std::runtime_error{"Transform feedback shaders cannot use a texture bias ('" + std::string{token.Text} + "')."}; + } + replaceToken(token, offset ? "textureLodOffset" : std::string{token.Text} + "Lod"); + const size_t insertAt = offset ? tokens[separators[1]].Offset : tokens[close].Offset; + replacements.push_back({insertAt, insertAt, ", 0.0"}); + } + } + + std::sort(replacements.begin(), replacements.end(), [](const Replacement& left, const Replacement& right) { + return left.Begin < right.Begin || (left.Begin == right.Begin && left.End < right.End); + }); + + std::string body; + size_t copied{}; + for (const auto& replacement : replacements) + { + if (replacement.Begin < copied) + { + throw std::runtime_error{"Transform feedback shader rewrite overlaps."}; + } + body.append(vertexSource, copied, replacement.Begin - copied); + body += replacement.Text; + copied = replacement.End; + } + body.append(vertexSource, copied); + + // Replace #version and keep the remaining directives (#extension) ahead of declarations. + std::string directives; + std::string declarations; + for (size_t lineStart = 0; lineStart < body.size();) + { + auto lineEnd = body.find('\n', lineStart); + lineEnd = lineEnd == std::string::npos ? body.size() : lineEnd; + const std::string_view line{body.data() + lineStart, lineEnd - lineStart}; + const auto first = line.find_first_not_of(" \t\r"); + if (first != std::string_view::npos && line[first] == '#') + { + if (line.substr(first).starts_with("#version")) + { + directives.insert(0, "#version 310 es\n"); + } + else + { + directives.append(line).append("\n"); + } + } + else + { + declarations.append(line).append("\n"); + } + lineStart = lineEnd + 1; + } + if (!directives.starts_with("#version")) + { + directives.insert(0, "#version 310 es\n"); + } + + TransformFeedbackShader result{}; + const size_t layoutCount = std::max(inputs.size(), 1); + std::string source = directives; + source += "layout(local_size_x = " + std::to_string(Graphics::TRANSFORM_FEEDBACK_WORKGROUP_SIZE) + ", local_size_y = 1, local_size_z = 1) in;\n"; + const auto inputBuffer = [](uint32_t buffer) { + return "bnTfInput" + std::to_string(buffer); + }; + for (uint32_t buffer = 0; buffer < Graphics::TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS; ++buffer) + { + const uint32_t binding = buffer == 0 ? Graphics::TRANSFORM_FEEDBACK_INPUT_BINDING : Graphics::TRANSFORM_FEEDBACK_EXTRA_INPUT_BINDING + buffer - 1; + source += "layout(std430, binding = " + std::to_string(binding) + ") readonly buffer BnTfInputBuffer" + std::to_string(buffer) + + " { uint " + inputBuffer(buffer) + "[]; };\n"; + } + source += "layout(std430, binding = " + std::to_string(Graphics::TRANSFORM_FEEDBACK_OUTPUT_BINDING) + ") writeonly buffer BnTfOutput { float bnTfOutput[]; };\n"; + source += std::string{"uniform vec4 "} + Graphics::TRANSFORM_FEEDBACK_PARAMS_UNIFORM + ";\n"; + source += std::string{"uniform vec4 "} + Graphics::TRANSFORM_FEEDBACK_INPUT_LAYOUT_UNIFORM + "[" + std::to_string(layoutCount) + "];\n"; + source += "int bnTfVertexID;\nint bnTfInstanceID;\nvec4 bnTfPosition;\nfloat bnTfPointSize;\n"; + source += declarations; + + source += "uint bnTfReadWord(int bufferIndex, int word)\n{\n"; + for (uint32_t buffer = 1; buffer < Graphics::TRANSFORM_FEEDBACK_MAX_INPUT_BUFFERS; ++buffer) + { + source += " if (bufferIndex == " + std::to_string(buffer) + ") { return " + inputBuffer(buffer) + "[word]; }\n"; + } + source += " return " + inputBuffer(0) + "[word];\n}\n"; + + // Decodes one component the way a WebGL vertex fetch does: float, or (normalized) 8/16-bit integers. + source += "float bnTfReadInput(int slot, int component, float fallback)\n{\n"; + source += " vec4 inputLayout = " + std::string{Graphics::TRANSFORM_FEEDBACK_INPUT_LAYOUT_UNIFORM} + "[slot];\n"; + source += " if (float(component) >= inputLayout.z) { return fallback; }\n"; + source += " int info = int(inputLayout.w);\n"; + source += " int type = (info / 2) % 8;\n"; + source += " bool normalized = (info % 2) == 1;\n"; + source += " int size = type == 0 ? 4 : (type <= 2 ? 1 : 2);\n"; + source += " int address = int(inputLayout.x) + bnTfVertexID * int(inputLayout.y) + component * size;\n"; + source += " uint word = bnTfReadWord(info / 16, address / 4);\n"; + source += " int shift = (address % 4) * 8;\n"; + source += " if (type == 0) { return uintBitsToFloat(word); }\n"; + source += " int bits = size * 8;\n"; + source += " if (type == 1 || type == 3)\n {\n"; + source += " float value = float(bitfieldExtract(int(word), shift, bits));\n"; + source += " return normalized ? max(value / float((1 << (bits - 1)) - 1), -1.0) : value;\n }\n"; + source += " float value = float(bitfieldExtract(word, shift, bits));\n"; + source += " return normalized ? value / float((1 << bits) - 1) : value;\n}\n"; + + source += "void main()\n{\n"; + source += " uint index = gl_GlobalInvocationID.x;\n"; + source += " if (index >= uint(" + std::string{Graphics::TRANSFORM_FEEDBACK_PARAMS_UNIFORM} + ".x)) { return; }\n"; + source += " bnTfVertexID = int(index) + int(" + std::string{Graphics::TRANSFORM_FEEDBACK_PARAMS_UNIFORM} + ".y);\n"; + source += " bnTfInstanceID = 0;\n"; + static constexpr const char* fallbacks[]{"0.0", "0.0", "0.0", "1.0"}; + static constexpr const char* swizzles[]{".x", ".y", ".z", ".w"}; + for (size_t attribute = 0; attribute < inputs.size(); ++attribute) + { + const auto& input = inputs[attribute]; + const uint32_t components = ComponentCount(input.Type); + source += " " + input.Name + " = " + (components == 1 && input.Type == "float" ? "" : input.Type + "("); + for (uint32_t component = 0; component < components; ++component) + { + source += (component == 0 ? "" : ", ") + std::string{"bnTfReadInput("} + std::to_string(attribute) + ", " + + std::to_string(component) + ", " + fallbacks[component] + ")"; + } + source += std::string{components == 1 && input.Type == "float" ? "" : ")"} + ";\n"; + result.InputNames.push_back(input.Name); + } + source += " bnTfMain();\n"; + + uint32_t stride{}; + for (const auto& varying : varyings) + { + const auto output = outputs.find(varying); + if (output == outputs.end()) + { + throw std::runtime_error{"Transform feedback varying '" + varying + "' is not a vertex shader output."}; + } + stride += ComponentCount(output->second); + } + source += " int base = int(index) * " + std::to_string(stride) + ";\n"; + uint32_t offset{}; + for (const auto& varying : varyings) + { + const auto& type = outputs.at(varying); + const uint32_t components = ComponentCount(type); + const std::string bitCast{OutputBitCast(type)}; + for (uint32_t component = 0; component < components; ++component) + { + source += " bnTfOutput[base + " + std::to_string(offset++) + "] = " + bitCast + "(" + varying + + (components == 1 ? "" : swizzles[component]) + ");\n"; + } + } + source += "}\n"; + + result.Source = std::move(source); + return result; + } + + std::map RenameShaderUniforms(std::string& source, std::string* pairedSource) + { + const auto first = TokenizeShader(source); + const auto second = pairedSource ? TokenizeShader(*pairedSource) : std::vector{}; + std::set names; + std::set candidates; + std::set firstCandidates; + std::set secondCandidates; + constexpr std::string_view predefined[] = { + "u_viewRect", "u_viewTexel", "u_view", "u_invView", "u_proj", "u_invProj", + "u_viewProj", "u_invViewProj", "u_model", "u_modelView", "u_invModelView", + "u_modelViewProj", "u_alphaRef", "u_alphaRef4", "bgfx_indirectArgBase", + Graphics::RENDER_TARGET_TRANSFORM_UNIFORM_NAME}; + const auto collect = [&](const std::vector& tokens, std::set& stageCandidates) { + bool uniform{}; + for (const auto& token : tokens) + { + names.emplace(token.Text); + if (token.Text == "uniform") + { + uniform = true; + } + else if (token.Text == ";" || token.Text == "{") + { + uniform = false; + } + else if (uniform && (token.Text == "textureSize" || token.Text.starts_with(Graphics::SAMPLER_STATE_UNIFORM_PREFIX) || + std::find(std::begin(predefined), std::end(predefined), token.Text) != std::end(predefined))) + { + candidates.emplace(token.Text); + stageCandidates.emplace(token.Text); + } + } + }; + collect(first, firstCandidates); + collect(second, secondCandidates); + + std::map renamed; + std::map originalNames; + for (const auto& name : candidates) + { + const auto prefix = "bnUserUniform_" + name; + auto replacement = prefix; + for (size_t suffix = 1; names.count(replacement); ++suffix) + { + replacement = prefix + std::to_string(suffix); + } + names.emplace(replacement); + renamed.emplace(name, replacement); + originalNames.emplace(replacement, name); + } + const auto rewrite = [&](std::string& text, const std::vector& tokens, const std::set& stageCandidates) { + std::string result; + size_t copied{}; + for (size_t index = 0; index < tokens.size(); ++index) + { + const auto& token = tokens[index]; + const auto replacement = renamed.find(std::string{token.Text}); + if (replacement == renamed.end() || !stageCandidates.count(replacement->first) || + (token.Text == "textureSize" && index + 1 < tokens.size() && tokens[index + 1].Text == "(")) + { + continue; + } + result.append(text, copied, token.Offset - copied); + result += replacement->second; + copied = token.Offset + token.Text.size(); + } + result.append(text, copied); + return result; + }; + if (!renamed.empty()) + { + auto rewritten = rewrite(source, first, firstCandidates); + if (pairedSource) + { + *pairedSource = rewrite(*pairedSource, second, secondCandidates); + } + source = std::move(rewritten); + } + return originalNames; + } + // The synthetic instance-data locations must line up with the live bgfx::Attrib enum so that // per-instance inputs land on the top TEXCOORD semantics bgfx binds them to, and so that they // sort above every real vertex attribute (and can therefore be excluded from the shader's @@ -24,6 +642,7 @@ namespace Babylon::ShaderCompilerCommon // Patching shader code to append clip space coordinates for the current rendering API. // Can be done with glslang shader traversal. Done with string patching for now. + // Also flips clip-space Y for GL-row-order targets (see RENDER_TARGET_TRANSFORM_UNIFORM_NAME). std::string ProcessShaderCoordinates(std::string_view source) { size_t lastBrace = source.find_last_of('}'); @@ -32,13 +651,24 @@ namespace Babylon::ShaderCompilerCommon throw std::runtime_error{"ProcessShaderCoordinates: Could not find closing brace."}; } - return std::string{source}.substr(0, lastBrace) + "gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0; }"; + std::smatch match; + const std::string body{source.substr(0, lastBrace)}; + static const std::regex mainPattern{R"(\bvoid\s+main\s*\()"}; + if (!std::regex_search(body, match, mainPattern)) + { + throw std::runtime_error{"ProcessShaderCoordinates: Could not find main."}; + } + + const std::string uniformName{Graphics::RENDER_TARGET_TRANSFORM_UNIFORM_NAME}; + const auto mainOffset = static_cast(match.position(0)); + return body.substr(0, mainOffset) + "uniform vec4 " + uniformName + ";\n" + body.substr(mainOffset) + + "gl_Position.y *= " + uniformName + ".z; gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0; }"; } std::string ProcessSamplerFlip(std::string_view source) { - // The AST traverser flips 2D/volume coordinates, including explicit gradients and - // integer fetches, while preserving raw array rows and cube directions. + // The AST traverser flips 2D/array/volume coordinates, including explicit gradients + // and integer fetches, while preserving layers and cube directions. // Retain this identity passthrough for the existing backend call sites. return std::string{source}; } @@ -166,7 +796,13 @@ namespace Babylon::ShaderCompilerCommon // validate this, so the bug was GL-only). Each sampler now gets its own distinct unit, // mirroring WebGL's Effect._bindSamplerUniformToChannel. Keyed on the recovered // pre-transpile name (see above) so both stages agree on the same identifier. - if (stages.find(name) == stages.end()) + if (compiler.get_execution_model() == spv::ExecutionModelGLCompute) + { + // Compute samplers declare explicit bindings that sit after the storage buffer + // stages; sequential units would alias the buffers. + stages[name] = static_cast(compiler.get_decoration(sampler.id, spv::DecorationBinding)); + } + else if (stages.find(name) == stages.end()) { stages[name] = static_cast(stages.size()); } @@ -390,9 +1026,8 @@ Graphics::BgfxShaderInfo CreateBgfxShader( AppendBytes(vertexBytes, BX_MAKEFOURCC('V', 'S', 'H', BGFX_SHADER_BIN_VERSION)); AppendBytes(vertexBytes, vertexOutputsHash); AppendBytes(vertexBytes, fragmentInputsHash); - // Raw SRV/UAV masks (bgfx v12). BN runtime shaders don't expose raw buffers here. - AppendBytes(vertexBytes, static_cast(0)); - AppendBytes(vertexBytes, static_cast(0)); + AppendBytes(vertexBytes, vertexShaderInfo.RawSrvMask); + AppendBytes(vertexBytes, vertexShaderInfo.RawUavMask); AppendBytes(vertexBytes, static_cast(numUniforms)); AppendUniformBuffer(vertexBytes, uniformsInfo, false); @@ -448,9 +1083,8 @@ Graphics::BgfxShaderInfo CreateBgfxShader( AppendBytes(fragmentBytes, BX_MAKEFOURCC('F', 'S', 'H', BGFX_SHADER_BIN_VERSION)); AppendBytes(fragmentBytes, vertexOutputsHash); AppendBytes(fragmentBytes, fragmentInputsHash); - // Raw SRV/UAV masks (bgfx v12). BN runtime shaders don't expose raw buffers here. - AppendBytes(fragmentBytes, static_cast(0)); - AppendBytes(fragmentBytes, static_cast(0)); + AppendBytes(fragmentBytes, fragmentShaderInfo.RawSrvMask); + AppendBytes(fragmentBytes, fragmentShaderInfo.RawUavMask); AppendBytes(fragmentBytes, static_cast(numUniforms)); AppendUniformBuffer(fragmentBytes, uniformsInfo, true); @@ -468,4 +1102,113 @@ Graphics::BgfxShaderInfo CreateBgfxShader( return bgfxShaderInfo; } + +Graphics::BgfxShaderInfo CreateBgfxComputeShader(ShaderInfo computeShaderInfo) +{ + return CreateBgfxComputeShader(std::move(computeShaderInfo), SamplerResourceSet::SampledImages, AppendSamplers); +} + +Graphics::BgfxShaderInfo CreateBgfxComputeShader(ShaderInfo computeShaderInfo, SamplerResourceSet samplerResources, AppendSamplersFn appendSamplers) +{ + Graphics::BgfxShaderInfo bgfxShaderInfo{}; + + // Must match BGFX_SHADER_BIN_VERSION in bgfx tools/shaderc/shaderc.cpp. + // bgfx rejects anything older than v12; v13 makes it trust the samplers' texDimension. + constexpr uint8_t BGFX_SHADER_BIN_VERSION{13}; + + // Compute shaders have no vertex-varying interface, so both interface hashes are zero + // (bgfx's own shaderc writes 0 for the compute input hash). + constexpr uint32_t inputHash{0}; + constexpr uint32_t outputHash{0}; + + std::vector& computeBytes{bgfxShaderInfo.ComputeBytes}; + + const spirv_cross::Compiler& compiler{*computeShaderInfo.Compiler}; + const spirv_cross::ShaderResources resources{compiler.get_shader_resources()}; + const auto uniformsInfo{CollectNonSamplerUniforms(*computeShaderInfo.Parser, compiler)}; + // Compute shaders keep combined samplers everywhere except Vulkan, whose descriptor model + // needs separate images and samplers. A sampler's stage is its binding, which compute + // shaders declare explicitly so it doesn't collide with storage buffer stages. + const spirv_cross::SmallVector& samplers{SelectSamplers(resources, samplerResources)}; +#if VULKAN + // bgfx's Vulkan backend builds its descriptor set layout from the uniform table, so storage + // buffers and images get entries (type End, regCount = descriptor type id) as shaderc emits. + const size_t numStorage{resources.storage_buffers.size() + resources.storage_images.size()}; +#else + const size_t numStorage{0}; +#endif + const size_t numUniforms{uniformsInfo.Uniforms.size() + samplers.size() + numStorage}; + + AppendBytes(computeBytes, BX_MAKEFOURCC('C', 'S', 'H', BGFX_SHADER_BIN_VERSION)); + AppendBytes(computeBytes, inputHash); + AppendBytes(computeBytes, outputHash); + AppendBytes(computeBytes, computeShaderInfo.RawSrvMask); + AppendBytes(computeBytes, computeShaderInfo.RawUavMask); + + AppendBytes(computeBytes, static_cast(numUniforms)); + AppendUniformBuffer(computeBytes, uniformsInfo, false); + appendSamplers(computeBytes, compiler, computeShaderInfo.Parser->get_parsed_ir(), samplers, bgfxShaderInfo.UniformStages); +#if VULKAN + { + // Copy-pasta from bgfx_p.h and bgfx/src/shader.cpp (s_descriptorTypeToId). + constexpr uint8_t BGFX_UNIFORM_READONLYBIT{0x40}; + constexpr uint16_t DESCRIPTOR_TYPE_STORAGE_BUFFER_ID{0x0007}; + constexpr uint16_t DESCRIPTOR_TYPE_STORAGE_IMAGE_ID{0x0003}; + const auto appendStorage = [&](const spirv_cross::Resource& resource, uint16_t descriptorTypeId, uint8_t texDimension) { + const auto flags = descriptorTypeId == DESCRIPTOR_TYPE_STORAGE_BUFFER_ID + ? compiler.get_buffer_block_flags(resource.id) + : compiler.get_decoration_bitset(resource.id); + const bool readOnly = flags.get(spv::DecorationNonWritable); + AppendBytes(computeBytes, static_cast(resource.name.size())); + AppendBytes(computeBytes, resource.name); + AppendBytes(computeBytes, static_cast(bgfx::UniformType::End | (readOnly ? BGFX_UNIFORM_READONLYBIT : 0))); + AppendBytes(computeBytes, uint8_t{0}); + AppendBytes(computeBytes, static_cast(compiler.get_decoration(resource.id, spv::DecorationBinding))); + AppendBytes(computeBytes, descriptorTypeId); + AppendUniformTextureMeta(computeBytes, /*texComponent*/ 0, texDimension); + }; + for (const auto& buffer : resources.storage_buffers) + { + appendStorage(buffer, DESCRIPTOR_TYPE_STORAGE_BUFFER_ID, 0); + } + for (const auto& image : resources.storage_images) + { + appendStorage(image, DESCRIPTOR_TYPE_STORAGE_IMAGE_ID, TextureDimensionIdFromResource(compiler, image)); + } + } +#endif +#if __APPLE__ + // bgfx's Metal backend reads the threadgroup size from the binary; MSL doesn't carry it. + for (uint32_t dimension = 0; dimension < 3; ++dimension) + { + AppendBytes(computeBytes, static_cast(compiler.get_execution_mode_argument(spv::ExecutionModeLocalSize, dimension))); + } +#endif + + AppendBytes(computeBytes, static_cast(computeShaderInfo.Bytes.size())); + AppendBytes(computeBytes, computeShaderInfo.Bytes); + AppendBytes(computeBytes, static_cast(0)); + + // Compute shaders have no vertex attributes. + AppendBytes(computeBytes, static_cast(0)); + + AppendBytes(computeBytes, static_cast(uniformsInfo.ByteSize)); + + return bgfxShaderInfo; +} +} + +namespace Babylon::Plugins +{ + Graphics::BgfxShaderInfo ShaderCompiler::CompileTransformFeedback(std::string_view vertexSource, const std::vector& varyings) + { + const auto preprocessed = ShaderCompilerCommon::PreprocessShader(EShLangVertex, vertexSource); + const auto generated = ShaderCompilerCommon::GenerateTransformFeedbackShader(preprocessed, varyings); + auto result = CompileCompute(generated.Source); + for (uint32_t index = 0; index < generated.InputNames.size(); ++index) + { + result.VertexAttributeLocations[generated.InputNames[index]] = Graphics::TRANSFORM_FEEDBACK_ATTRIBUTE_LOCATION_BASE + index; + } + return result; + } } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.h b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.h index 60ea926dd5..b95adfa205 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.h +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.h @@ -5,11 +5,30 @@ #include #include #include +#include #include #include namespace Babylon::ShaderCompilerCommon { + std::string PreprocessShader(EShLanguage stage, std::string_view source); + + struct TransformFeedbackShader + { + std::string Source{}; + // Vertex inputs in the order of the generated bnTfInputLayout entries. + std::vector InputNames{}; + }; + + /// Rewrites a preprocessed GLSL ES 3.00 vertex shader that feeds WebGL2 transform feedback into + /// a GLSL ES 3.10 compute shader: one invocation per vertex reads the vertex inputs from a float + /// storage buffer (described by bnTfInputLayout) and writes the captured varyings, tightly + /// packed in `varyings` order (GL_INTERLEAVED_ATTRIBS), into a second float storage buffer. + TransformFeedbackShader GenerateTransformFeedbackShader(std::string_view vertexSource, const std::vector& varyings); + + // Returns compiled uniform names mapped to the names supplied by the application. + std::map RenameShaderUniforms(std::string& source, std::string* pairedSource = nullptr); + std::string ProcessShaderCoordinates(std::string_view source); std::string ProcessSamplerFlip(std::string_view source); @@ -97,6 +116,8 @@ namespace Babylon::ShaderCompilerCommon std::unique_ptr Compiler; gsl::span Bytes; std::map AttributeRenaming; + uint32_t RawSrvMask{}; + uint32_t RawUavMask{}; }; // Backend-default packaging (D3D separate_samplers / GL sampled_images / Metal separate_images). @@ -109,4 +130,13 @@ namespace Babylon::ShaderCompilerCommon std::map builtInInstanceDataSlots, SamplerResourceSet samplerResources, AppendSamplersFn appendSamplers); + + /// Assembles a bgfx CSH (compute) shader binary from a compiled compute shader. The binary + /// layout mirrors the per-shader block of CreateBgfxShader (fourcc 'CSH' + hashes + uniform + /// table + samplers + blob) but with zero vertex attributes. Raw-buffer masks select the + /// views used when storage buffers are bound at dispatch via bgfx::setBuffer(stage,...). + Graphics::BgfxShaderInfo CreateBgfxComputeShader(ShaderInfo computeShaderInfo); + + // Explicit sampler resource set + appender (Vulkan packages split samplers with its binding model). + Graphics::BgfxShaderInfo CreateBgfxComputeShader(ShaderInfo computeShaderInfo, SamplerResourceSet samplerResources, AppendSamplersFn appendSamplers); } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerDXBC.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerDXBC.cpp index f026337c2d..0e4039604f 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerDXBC.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerDXBC.cpp @@ -9,10 +9,48 @@ #include #include #include +#include #include namespace { + void ReflectRawBindings(Babylon::ShaderCompilerCommon::ShaderInfo& shaderInfo) + { + Microsoft::WRL::ComPtr reflection; + if (FAILED(D3DReflect(shaderInfo.Bytes.data(), shaderInfo.Bytes.size(), IID_PPV_ARGS(&reflection)))) + { + throw std::runtime_error{"D3DReflect failed while reading raw buffer bindings"}; + } + + D3D11_SHADER_DESC shaderDesc{}; + if (FAILED(reflection->GetDesc(&shaderDesc))) + { + throw std::runtime_error{"Failed to read compiled shader description"}; + } + + for (UINT i = 0; i < shaderDesc.BoundResources; ++i) + { + D3D11_SHADER_INPUT_BIND_DESC binding{}; + if (FAILED(reflection->GetResourceBindingDesc(i, &binding))) + { + throw std::runtime_error{"Failed to read compiled shader resource binding"}; + } + if (binding.Type != D3D_SIT_BYTEADDRESS && binding.Type != D3D_SIT_UAV_RWBYTEADDRESS) + { + continue; + } + if (binding.BindPoint >= 32 || binding.BindCount == 0 || binding.BindCount > 32 - binding.BindPoint) + { + throw std::runtime_error{"Raw buffer binding exceeds bgfx's 32-bit shader mask"}; + } + auto& mask = binding.Type == D3D_SIT_BYTEADDRESS ? shaderInfo.RawSrvMask : shaderInfo.RawUavMask; + for (UINT slot = binding.BindPoint; slot < binding.BindPoint + binding.BindCount; ++slot) + { + mask |= uint32_t{1} << slot; + } + } + } + void AddShader(glslang::TProgram& program, glslang::TShader& shader, std::string_view source) { const std::array sources{source.data()}; @@ -38,7 +76,9 @@ namespace auto compiler = std::make_unique(parser->get_parsed_ir()); - compiler->set_hlsl_options({40, true}); + spirv_cross::CompilerHLSL::Options options{40, true}; + options.enable_fxc_nested_loop_workaround = true; + compiler->set_hlsl_options(options); for (const auto& attribute : attributes) { @@ -83,6 +123,11 @@ namespace Babylon::Plugins Graphics::BgfxShaderInfo ShaderCompiler::Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { + auto vertexText = PreprocessShader(EShLangVertex, vertexSource); + auto fragmentText = PreprocessShader(EShLangFragment, fragmentSource); + auto uniformNames = RenameShaderUniforms(vertexText, &fragmentText); + vertexSource = vertexText; + fragmentSource = fragmentText; glslang::TProgram program; glslang::TShader vertexShader{EShLangVertex}; @@ -114,7 +159,6 @@ namespace Babylon::Plugins ShaderCompilerTraversers::SplitSamplersIntoSamplersAndTextures(program, ids); ShaderCompilerTraversers::SplitSamplerFunctionParameters(program, ids); ShaderCompilerTraversers::ZeroInitializeStructLocals(program); - ShaderCompilerTraversers::InvertYDerivativeOperands(program); // clang-format off static const spirv_cross::HLSLVertexAttributeRemap attributes[] = { @@ -170,6 +214,7 @@ namespace Babylon::Plugins std::move(vertexCompiler), gsl::make_span(static_cast(vertexBlob->GetBufferPointer()), vertexBlob->GetBufferSize()), std::move(vertexAttributeRenaming)}; + ReflectRawBindings(vertexShaderInfo); Microsoft::WRL::ComPtr fragmentBlob; auto [fragmentParser, fragmentCompiler] = CompileShader(program, EShLangFragment, {}, &fragmentBlob); @@ -178,7 +223,73 @@ namespace Babylon::Plugins std::move(fragmentCompiler), gsl::make_span(static_cast(fragmentBlob->GetBufferPointer()), fragmentBlob->GetBufferSize()), {}}; + ReflectRawBindings(fragmentShaderInfo); + + auto result = CreateBgfxShader(std::move(vertexShaderInfo), std::move(fragmentShaderInfo), std::move(builtInInstanceDataSlots)); + result.UniformNames = std::move(uniformNames); + return result; + } + + Graphics::BgfxShaderInfo ShaderCompiler::CompileCompute(std::string_view computeSource) + { + auto source = PreprocessShader(EShLangCompute, computeSource); + auto uniformNames = RenameShaderUniforms(source); + computeSource = source; + glslang::TProgram program; + + glslang::TShader computeShader{EShLangCompute}; + AddShader(program, computeShader, ProcessSamplerFlip(computeSource)); + + glslang::SpvVersion spv{}; + spv.spv = 0x10000; + computeShader.getIntermediate()->setSpv(spv); + + if (!program.link(EShMsgDefault)) + { + throw std::runtime_error{program.getInfoLog()}; + } + + // Compute skips the vertex/fragment traverser set; keep uniforms usable and sample + // coordinates consistent with the graphics path where applicable. + ShaderCompilerTraversers::IdGenerator ids{}; + ShaderCompilerTraversers::FlipSamplerCoordinates(program, ids); + auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids); + auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids); + ShaderCompilerTraversers::ZeroInitializeStructLocals(program); + + Microsoft::WRL::ComPtr computeBlob; + // CompileShader hard-codes vs/ps; compile HLSL inline for compute (cs_5_0). + std::vector spirv; + glslang::GlslangToSpv(*program.getIntermediate(EShLangCompute), spirv); + + auto parser = std::make_unique(std::move(spirv)); + parser->parse(); + auto compiler = std::make_unique(parser->get_parsed_ir()); + spirv_cross::CompilerHLSL::Options options{50, true}; + options.enable_fxc_nested_loop_workaround = true; + compiler->set_hlsl_options(options); + Babylon::ShaderCompilerCommon::AssignUniformBufferBindings(*compiler); + std::string hlsl = compiler->compile(); + + Microsoft::WRL::ComPtr errorMsgs; + UINT flags = 0; +#ifdef _DEBUG + flags |= D3DCOMPILE_DEBUG; +#endif + if (FAILED(D3DCompile(hlsl.data(), hlsl.size(), nullptr, nullptr, nullptr, "main", "cs_5_0", flags, 0, &computeBlob, &errorMsgs))) + { + throw std::runtime_error{static_cast(errorMsgs->GetBufferPointer())}; + } + + ShaderInfo computeShaderInfo{ + std::move(parser), + std::move(compiler), + gsl::make_span(static_cast(computeBlob->GetBufferPointer()), computeBlob->GetBufferSize()), + {}}; + ReflectRawBindings(computeShaderInfo); - return CreateBgfxShader(std::move(vertexShaderInfo), std::move(fragmentShaderInfo), std::move(builtInInstanceDataSlots)); + auto result = CreateBgfxComputeShader(std::move(computeShaderInfo)); + result.UniformNames = std::move(uniformNames); + return result; } } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerDXIL.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerDXIL.cpp index b63fce6d7e..fa88d1bd75 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerDXIL.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerDXIL.cpp @@ -86,7 +86,8 @@ namespace auto compiler = std::make_unique(parser->get_parsed_ir()); - compiler->set_hlsl_options({40, true}); + // Compute needs SM5 for (RW)ByteAddressBuffer storage buffers. + compiler->set_hlsl_options({stage == EShLangCompute ? 50u : 40u, true}); for (const auto& attribute : attributes) { @@ -99,7 +100,7 @@ namespace auto& dxc = GetDxcCompiler(); - const wchar_t* target = stage == EShLangVertex ? L"vs_6_0" : L"ps_6_0"; + const wchar_t* target = stage == EShLangVertex ? L"vs_6_0" : stage == EShLangFragment ? L"ps_6_0" : L"cs_6_0"; std::vector args = {L"-E", L"main", L"-T", target}; #ifdef _DEBUG @@ -141,6 +142,23 @@ namespace return {std::move(parser), std::move(compiler)}; } + + // SPIRV-Cross emits read-only storage buffers as ByteAddressBuffer (t#) and writable ones as + // RWByteAddressBuffer (u#), both at the GLSL binding. bgfx needs those slots flagged raw. + void ReflectRawBindings(Babylon::ShaderCompilerCommon::ShaderInfo& shaderInfo) + { + const auto& compiler = *shaderInfo.Compiler; + for (const auto& buffer : compiler.get_shader_resources(compiler.get_active_interface_variables()).storage_buffers) + { + const uint32_t binding = compiler.get_decoration(buffer.id, spv::DecorationBinding); + if (binding >= 32) + { + throw std::runtime_error{"Raw buffer binding exceeds bgfx's 32-bit shader mask"}; + } + auto& mask = compiler.get_buffer_block_flags(buffer.id).get(spv::DecorationNonWritable) ? shaderInfo.RawSrvMask : shaderInfo.RawUavMask; + mask |= uint32_t{1} << binding; + } + } } namespace Babylon::Plugins @@ -159,6 +177,11 @@ namespace Babylon::Plugins Graphics::BgfxShaderInfo ShaderCompiler::Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { + auto vertexText = PreprocessShader(EShLangVertex, vertexSource); + auto fragmentText = PreprocessShader(EShLangFragment, fragmentSource); + auto uniformNames = RenameShaderUniforms(vertexText, &fragmentText); + vertexSource = vertexText; + fragmentSource = fragmentText; glslang::TProgram program; glslang::TShader vertexShader{EShLangVertex}; @@ -190,7 +213,6 @@ namespace Babylon::Plugins ShaderCompilerTraversers::SplitSamplersIntoSamplersAndTextures(program, ids); ShaderCompilerTraversers::SplitSamplerFunctionParameters(program, ids); ShaderCompilerTraversers::ZeroInitializeStructLocals(program); - ShaderCompilerTraversers::InvertYDerivativeOperands(program); // clang-format off static const spirv_cross::HLSLVertexAttributeRemap attributes[] = { @@ -255,6 +277,47 @@ namespace Babylon::Plugins gsl::make_span(static_cast(fragmentBlob->GetBufferPointer()), fragmentBlob->GetBufferSize()), {}}; - return CreateBgfxShader(std::move(vertexShaderInfo), std::move(fragmentShaderInfo), std::move(builtInInstanceDataSlots)); + auto result = CreateBgfxShader(std::move(vertexShaderInfo), std::move(fragmentShaderInfo), std::move(builtInInstanceDataSlots)); + result.UniformNames = std::move(uniformNames); + return result; + } + + Graphics::BgfxShaderInfo ShaderCompiler::CompileCompute(std::string_view computeSource) + { + auto source = PreprocessShader(EShLangCompute, computeSource); + auto uniformNames = RenameShaderUniforms(source); + computeSource = source; + glslang::TProgram program; + + glslang::TShader computeShader{EShLangCompute}; + AddShader(program, computeShader, ProcessSamplerFlip(computeSource)); + + glslang::SpvVersion spv{}; + spv.spv = 0x10000; + computeShader.getIntermediate()->setSpv(spv); + + if (!program.link(EShMsgDefault)) + { + throw std::runtime_error{program.getInfoLog()}; + } + + ShaderCompilerTraversers::IdGenerator ids{}; + ShaderCompilerTraversers::FlipSamplerCoordinates(program, ids); + auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids); + auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids); + ShaderCompilerTraversers::ZeroInitializeStructLocals(program); + + Microsoft::WRL::ComPtr computeBlob; + auto [computeParser, computeCompiler] = CompileShader(program, EShLangCompute, {}, &computeBlob); + ShaderInfo computeShaderInfo{ + std::move(computeParser), + std::move(computeCompiler), + gsl::make_span(static_cast(computeBlob->GetBufferPointer()), computeBlob->GetBufferSize()), + {}}; + ReflectRawBindings(computeShaderInfo); + + auto result = CreateBgfxComputeShader(std::move(computeShaderInfo)); + result.UniformNames = std::move(uniformNames); + return result; } } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerMetal.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerMetal.cpp index ad1ddc3b68..f71971d38b 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerMetal.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerMetal.cpp @@ -48,6 +48,37 @@ namespace compiler->set_name(resource.id, "_mtl_u"); } + if (stage == EShLangCompute) + { + // bgfx's Metal compute binding: uniforms in buffer 0, storage buffer stage N in + // buffer N + 1, and a texture/sampler at its stage. Compute keeps combined samplers. + // Each resource kind gets its own descriptor set, so the generated uniform block (which + // has no binding, i.e. 0) and storage binding 0 do not share one remapping key. + const auto bind = [&](const spirv_cross::Resource& resource, uint32_t set, uint32_t buffer, uint32_t texture) { + compiler->set_decoration(resource.id, spv::DecorationDescriptorSet, set); + spirv_cross::MSLResourceBinding binding{}; + binding.stage = spv::ExecutionModelGLCompute; + binding.desc_set = set; + binding.binding = compiler->get_decoration(resource.id, spv::DecorationBinding); + binding.msl_buffer = buffer; + binding.msl_texture = texture; + binding.msl_sampler = texture; + compiler->add_msl_resource_binding(binding); + }; + for (const auto& resource : resources.uniform_buffers) + { + bind(resource, 0, 0, 0); + } + for (const auto& resource : resources.storage_buffers) + { + bind(resource, 1, compiler->get_decoration(resource.id, spv::DecorationBinding) + 1, 0); + } + for (const auto& resource : resources.sampled_images) + { + bind(resource, 2, 0, compiler->get_decoration(resource.id, spv::DecorationBinding)); + } + } + // WebGL shaders use a single sampler uniform, Metal separates it into a sampler and a texture, appending "Texture" and "Sampler" to the original WebGL names. // Remove 'Texture' from the uniform name to match JS expected names. for (auto& resource : resources.separate_images) @@ -65,7 +96,10 @@ namespace } } - compiler->rename_entry_point("main", "xlatMtlMain", (stage == EShLangVertex) ? spv::ExecutionModelVertex : spv::ExecutionModelFragment); + const auto executionModel = stage == EShLangVertex ? spv::ExecutionModelVertex + : stage == EShLangFragment ? spv::ExecutionModelFragment + : spv::ExecutionModelGLCompute; + compiler->rename_entry_point("main", "xlatMtlMain", executionModel); shaderResult = compiler->compile(); return {std::move(parser), std::move(compiler)}; @@ -88,6 +122,11 @@ namespace Babylon::Plugins Graphics::BgfxShaderInfo ShaderCompiler::Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { + auto vertexText = PreprocessShader(EShLangVertex, vertexSource); + auto fragmentText = PreprocessShader(EShLangFragment, fragmentSource); + auto uniformNames = RenameShaderUniforms(vertexText, &fragmentText); + vertexSource = vertexText; + fragmentSource = fragmentText; glslang::TProgram program; glslang::TShader vertexShader{EShLangVertex}; @@ -119,7 +158,6 @@ namespace Babylon::Plugins ShaderCompilerTraversers::SplitSamplersIntoSamplersAndTextures(program, ids); ShaderCompilerTraversers::SplitSamplerFunctionParameters(program, ids); ShaderCompilerTraversers::ZeroInitializeStructLocals(program); - ShaderCompilerTraversers::InvertYDerivativeOperands(program); std::string vertexMSL(vertexSource.data(), vertexSource.size()); auto [vertexParser, vertexCompiler] = CompileShader(program, EShLangVertex, vertexMSL); @@ -127,9 +165,45 @@ namespace Babylon::Plugins std::string fragmentMSL(fragmentSource.data(), fragmentSource.size()); auto [fragmentParser, fragmentCompiler] = CompileShader(program, EShLangFragment, fragmentMSL); - return CreateBgfxShader( + auto result = CreateBgfxShader( {std::move(vertexParser), std::move(vertexCompiler), gsl::make_span(reinterpret_cast(vertexMSL.data()), vertexMSL.size()), std::move(vertexAttributeRenaming)}, {std::move(fragmentParser), std::move(fragmentCompiler), gsl::make_span(reinterpret_cast(fragmentMSL.data()), fragmentMSL.size()), {}}, std::move(builtInInstanceDataSlots)); + result.UniformNames = std::move(uniformNames); + return result; + } + + Graphics::BgfxShaderInfo ShaderCompiler::CompileCompute(std::string_view computeSource) + { + auto source = PreprocessShader(EShLangCompute, computeSource); + auto uniformNames = RenameShaderUniforms(source); + computeSource = source; + glslang::TProgram program; + + glslang::TShader computeShader{EShLangCompute}; + AddShader(program, computeShader, ProcessSamplerFlip(computeSource)); + + glslang::SpvVersion spv{}; + spv.spv = 0x10000; + computeShader.getIntermediate()->setSpv(spv); + + if (!program.link(EShMsgDefault)) + { + throw std::runtime_error{program.getInfoLog()}; + } + + ShaderCompilerTraversers::IdGenerator ids{}; + ShaderCompilerTraversers::FlipSamplerCoordinates(program, ids); + auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids); + auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids); + ShaderCompilerTraversers::ZeroInitializeStructLocals(program); + + std::string computeMSL; + auto [computeParser, computeCompiler] = CompileShader(program, EShLangCompute, computeMSL); + + auto result = CreateBgfxComputeShader( + {std::move(computeParser), std::move(computeCompiler), gsl::make_span(reinterpret_cast(computeMSL.data()), computeMSL.size()), {}}); + result.UniformNames = std::move(uniformNames); + return result; } } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerOpenGL.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerOpenGL.cpp index bf9b207db0..403d8707ae 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerOpenGL.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerOpenGL.cpp @@ -38,11 +38,26 @@ namespace spirv_cross::CompilerGLSL::Options options = compiler->get_common_options(); - options.version = 300; + // Compute shaders and storage buffers need GLSL ES 3.10. + options.version = stage == EShLangCompute ? 310 : 300; options.es = true; compiler->set_common_options(options); + // GLSL ES 3.00 flattens interface blocks into varyings prefixed with the stage-local + // instance name, so name each instance after its block to keep the stages linked. + const auto resources = compiler->get_shader_resources(); + for (const auto* interfaceResources : {&resources.stage_inputs, &resources.stage_outputs}) + { + for (const auto& resource : *interfaceResources) + { + if (compiler->has_decoration(resource.base_type_id, spv::DecorationBlock)) + { + compiler->set_name(resource.id, compiler->get_name(resource.base_type_id)); + } + } + } + glsl = compiler->compile(); return {std::move(parser), std::move(compiler)}; @@ -65,6 +80,11 @@ namespace Babylon::Plugins Graphics::BgfxShaderInfo ShaderCompiler::Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { + auto vertexText = PreprocessShader(EShLangVertex, vertexSource); + auto fragmentText = PreprocessShader(EShLangFragment, fragmentSource); + auto uniformNames = RenameShaderUniforms(vertexText, &fragmentText); + vertexSource = vertexText; + fragmentSource = fragmentText; glslang::TProgram program; glslang::TShader vertexShader{EShLangVertex}; @@ -94,9 +114,42 @@ namespace Babylon::Plugins std::string fragmentGLSL(fragmentSource.data(), fragmentSource.size()); auto [fragmentParser, fragmentCompiler] = CompileShader(program, EShLangFragment, fragmentGLSL); - return CreateBgfxShader( + auto result = CreateBgfxShader( {std::move(vertexParser), std::move(vertexCompiler), gsl::make_span(reinterpret_cast(vertexGLSL.data()), vertexGLSL.size()), std::move(vertexAttributeRenaming)}, {std::move(fragmentParser), std::move(fragmentCompiler), gsl::make_span(reinterpret_cast(fragmentGLSL.data()), fragmentGLSL.size()), {}}, std::move(builtInInstanceDataSlots)); + result.UniformNames = std::move(uniformNames); + return result; + } + + Graphics::BgfxShaderInfo ShaderCompiler::CompileCompute(std::string_view computeSource) + { + auto source = PreprocessShader(EShLangCompute, computeSource); + auto uniformNames = RenameShaderUniforms(source); + computeSource = source; + glslang::TProgram program; + + glslang::TShader computeShader{EShLangCompute}; + AddShader(program, computeShader, computeSource); + + glslang::SpvVersion spv{}; + spv.spv = 0x10000; + computeShader.getIntermediate()->setSpv(spv); + + if (!program.link(EShMsgDefault)) + { + throw std::runtime_error{program.getInfoLog()}; + } + + ShaderCompilerTraversers::IdGenerator ids{}; + auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids); + + std::string computeGLSL; + auto [computeParser, computeCompiler] = CompileShader(program, EShLangCompute, computeGLSL); + + auto result = CreateBgfxComputeShader( + {std::move(computeParser), std::move(computeCompiler), gsl::make_span(reinterpret_cast(computeGLSL.data()), computeGLSL.size()), {}}); + result.UniformNames = std::move(uniformNames); + return result; } } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp index 9e74761382..dd98e5e666 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp @@ -124,6 +124,8 @@ namespace Babylon::ShaderCompilerTraversers auto* scope = new AllocationsScope(); Traverse(program.getIntermediate(EShLangVertex), ids, *scope); Traverse(program.getIntermediate(EShLangFragment), ids, *scope); + // Compute-only programs (GPU particles, etc.) have neither VS nor FS. + Traverse(program.getIntermediate(EShLangCompute), ids, *scope); return std::unique_ptr(scope); } @@ -167,6 +169,11 @@ namespace Babylon::ShaderCompilerTraversers static void Traverse(TIntermediate* intermediate, IdGenerator& ids, AllocationsScope& scope) { + if (intermediate == nullptr) + { + return; + } + NonSamplerUniformToStructTraverser traverser{}; intermediate->getTreeRoot()->traverse(&traverser); @@ -298,6 +305,8 @@ namespace Babylon::ShaderCompilerTraversers auto* scope = new AllocationsScope(); Traverse(program.getIntermediate(EShLangVertex), ids, *scope); Traverse(program.getIntermediate(EShLangFragment), ids, *scope); + // Compute-only programs (GPU particles, etc.) have neither VS nor FS. + Traverse(program.getIntermediate(EShLangCompute), ids, *scope); return std::unique_ptr(scope); } @@ -504,6 +513,11 @@ namespace Babylon::ShaderCompilerTraversers static void Traverse(TIntermediate* intermediate, IdGenerator&, AllocationsScope& scope) { + if (intermediate == nullptr) + { + return; + } + UniformTypeChangeTraverser traverser{intermediate, scope}; intermediate->getTreeRoot()->traverse(&traverser); } @@ -983,7 +997,9 @@ namespace Babylon::ShaderCompilerTraversers public: void visitSymbol(TIntermSymbol* symbol) override { - if (symbol->getType().getQualifier().storage == EvqUniform && symbol->getType().getBasicType() == EbtSampler) + // Storage images (compute only) are bound as images, not texture + sampler pairs. + if (symbol->getType().getQualifier().storage == EvqUniform && symbol->getType().getBasicType() == EbtSampler && + !symbol->getType().getSampler().isImage()) { // Collect all sampler uniform symbols into the relevant caches // later proccessing. Note that we treat linker object replacement @@ -1015,12 +1031,22 @@ namespace Babylon::ShaderCompilerTraversers // shaders that declare many sampler uniforms but only use a few. std::map nameToBinding{}; unsigned int nextBinding{0}; - Traverse(program.getIntermediate(EShLangVertex), ids, nameToBinding, nextBinding); - Traverse(program.getIntermediate(EShLangFragment), ids, nameToBinding, nextBinding); + for (auto stage : {EShLangVertex, EShLangFragment}) + { + if (auto* intermediate = program.getIntermediate(stage)) + { + Traverse(intermediate, ids, nameToBinding, nextBinding, false); + } + } + // Compute samplers declare explicit bindings past the storage buffer stages; keep them. + if (auto* intermediate = program.getIntermediate(EShLangCompute)) + { + Traverse(intermediate, ids, nameToBinding, nextBinding, true); + } } private: - static void Traverse(TIntermediate* intermediate, IdGenerator& ids, std::map& nameToBinding, unsigned int& nextBinding) + static void Traverse(TIntermediate* intermediate, IdGenerator& ids, std::map& nameToBinding, unsigned int& nextBinding, bool preserveBindings) { SamplerSplitterTraverser traverser{}; intermediate->getTreeRoot()->traverse(&traverser); @@ -1039,7 +1065,11 @@ namespace Babylon::ShaderCompilerTraversers // on the public Traverse() overload for why bindings are shared. unsigned int layoutBinding; const auto bindingIt = nameToBinding.find(name); - if (bindingIt != nameToBinding.end()) + if (preserveBindings && symbol->getType().getQualifier().hasBinding()) + { + layoutBinding = symbol->getType().getQualifier().layoutBinding; + } + else if (bindingIt != nameToBinding.end()) { layoutBinding = bindingIt->second; } @@ -1152,8 +1182,13 @@ namespace Babylon::ShaderCompilerTraversers static void Traverse(TProgram& program, IdGenerator& ids) { SamplerFunctionParameterSplitterTraverser pass{}; - pass.Traverse(program.getIntermediate(EShLangVertex), ids); - pass.Traverse(program.getIntermediate(EShLangFragment), ids); + for (auto stage : {EShLangVertex, EShLangFragment, EShLangCompute}) + { + if (auto* intermediate = program.getIntermediate(stage)) + { + pass.Traverse(intermediate, ids); + } + } } private: @@ -1597,6 +1632,7 @@ namespace Babylon::ShaderCompilerTraversers StructLocalZeroInitializerTraverser pass{}; pass.TraverseStage(program.getIntermediate(EShLangVertex)); pass.TraverseStage(program.getIntermediate(EShLangFragment)); + pass.TraverseStage(program.getIntermediate(EShLangCompute)); } private: @@ -2104,41 +2140,6 @@ namespace Babylon::ShaderCompilerTraversers std::vector> m_symbolsToParents{}; }; - class InvertYDerivativeOperandsTraverser : public TIntermTraverser - { - public: - static void Traverse(TProgram& program) - { - auto intermediate{program.getIntermediate(EShLangFragment)}; - InvertYDerivativeOperandsTraverser invertYDerivativeOperandsTraverser{intermediate}; - intermediate->getTreeRoot()->traverse(&invertYDerivativeOperandsTraverser); - } - - protected: - virtual bool visitUnary(TVisit visit, TIntermUnary* unary) override - { - if (visit == EvPreVisit) - { - auto op = unary->getOp(); - if (op == EOpDPdy || op == EOpDPdyFine || op == EOpDPdyCoarse) - { - unary->setOperand(m_intermediate->addUnaryNode(EOpNegative, unary->getOperand(), {})); - return false; - } - } - - return true; - } - - private: - InvertYDerivativeOperandsTraverser(TIntermediate* intermediate) - : m_intermediate{intermediate} - { - } - - TIntermediate* m_intermediate{}; - }; - class FlipSamplerCoordinatesTraverser : public TIntermTraverser { public: @@ -2150,6 +2151,7 @@ namespace Babylon::ShaderCompilerTraversers if (intermediate != nullptr) { FlipSamplerCoordinatesTraverser traverser{intermediate, ids}; + traverser.AddSamplerStates(); intermediate->getTreeRoot()->traverse(&traverser); } } @@ -2163,7 +2165,7 @@ namespace Babylon::ShaderCompilerTraversers auto& sequence = node->getSequence(); if (sequence.size() >= 2) { - // Raw array uploads keep their rows; leave array coordinates and cube directions unchanged. + // Raw uploads and rendered layers share the framebuffer origin; preserve cube directions. auto* sampler = sequence[0]->getAsTyped(); auto* coordinate = sequence[1]->getAsTyped(); if (sampler != nullptr && coordinate != nullptr && @@ -2173,9 +2175,9 @@ namespace Babylon::ShaderCompilerTraversers { const TSampler& samp = sampler->getType().getSampler(); const int vecSize = coordinate->getType().getVectorSize(); - if (!samp.arrayed && (samp.is2D() || samp.dim == Esd3D) && vecSize >= 2 && vecSize <= 4) + if ((samp.is2D() || samp.dim == Esd3D) && vecSize >= 2 && vecSize <= 4) { - sequence[1] = FlipVerticalCoordinate(coordinate); + sequence[1] = FlipSampleCoordinate(coordinate, sampler); if (node->getOp() == EOpTextureGrad) { sequence[2] = FlipVerticalCoordinate(sequence[2]->getAsTyped(), true); @@ -2185,6 +2187,25 @@ namespace Babylon::ShaderCompilerTraversers } } } + else if (visit == EvPostVisit && node->getOp() == EOpFunctionCall) + { + const auto function = m_functionStates.find(node->getName().c_str()); + if (function != m_functionStates.end()) + { + auto& arguments = node->getSequence(); + auto& qualifiers = node->getQualifierList(); + if (arguments.size() != qualifiers.size()) + { + throw std::runtime_error{"Sampler state call qualifiers do not match arguments."}; + } + for (const auto index : function->second.Parameters) + { + arguments.push_back(SamplerHeight(arguments[index]->getAsTyped())); + qualifiers.push_back(EvqIn); + } + node->setName(function->second.Name.c_str()); + } + } else if (visit == EvPostVisit && node->getOp() == EOpTextureFetch) { // Flip integer Y using the selected mip height, preserving volume depth. @@ -2200,7 +2221,8 @@ namespace Babylon::ShaderCompilerTraversers !coordinate->getType().isArray()) { const int vecSize = coordinate->getType().getVectorSize(); - if (vecSize == 2 || (vecSize == 3 && sampler->getType().getSampler().dim == Esd3D)) + const auto& samp = sampler->getType().getSampler(); + if (vecSize == 2 || (vecSize == 3 && (samp.dim == Esd3D || samp.arrayed))) { const auto& loc = coordinate->getLoc(); TIntermSymbol coordinateTemp{m_ids.Next(), "bnTexelCoordinate", TType{EbtInt, EvqTemporary, vecSize}}; @@ -2220,6 +2242,131 @@ namespace Babylon::ShaderCompilerTraversers } private: + struct FunctionState + { + std::string Name; + std::vector Parameters; + }; + + static bool NeedsSamplerState(const TType& type) + { + return type.getBasicType() == EbtSampler && !type.isArray() && type.getSampler().isCombined() && + (type.getSampler().is2D() || type.getSampler().dim == Esd3D); + } + + void AddSamplerStates() + { + auto* root = m_intermediate->getTreeRoot()->getAsAggregate(); + for (auto* node : root->getSequence()) + { + auto* aggregate = node->getAsAggregate(); + if (aggregate == nullptr) + { + continue; + } + if (aggregate->getOp() == EOpLinkerObjects) + { + TIntermSequence states; + for (auto* declaration : aggregate->getSequence()) + { + auto* sampler = declaration->getAsSymbolNode(); + if (sampler != nullptr && NeedsSamplerState(sampler->getType())) + { + const auto name = std::string{Graphics::SAMPLER_STATE_UNIFORM_PREFIX} + sampler->getName().c_str(); + auto* state = m_intermediate->addSymbol(TIntermSymbol{ + m_ids.Next(), name.c_str(), TType{EbtFloat, EvqUniform, 4}}); + m_samplerStates.emplace(sampler->getId(), state); + states.push_back(state); + } + } + aggregate->getSequence().insert(aggregate->getSequence().end(), states.begin(), states.end()); + } + else if (aggregate->getOp() == EOpFunction) + { + auto* parameters = aggregate->getSequence().front()->getAsAggregate(); + auto& sequence = parameters->getSequence(); + const auto count = sequence.size(); + FunctionState function; + for (size_t index = 0; index < count; ++index) + { + auto* sampler = sequence[index]->getAsSymbolNode(); + if (NeedsSamplerState(sampler->getType())) + { + const auto name = std::string{sampler->getName().c_str()} + "BnSamplerHeight"; + auto* state = m_intermediate->addSymbol(TIntermSymbol{ + m_ids.Next(), name.c_str(), TType{EbtFloat, EvqIn}}); + m_samplerStates.emplace(sampler->getId(), state); + sequence.push_back(state); + function.Parameters.push_back(index); + } + } + if (!function.Parameters.empty()) + { + const std::string originalName{aggregate->getName().c_str()}; + TString name{originalName.substr(0, originalName.find('(') + 1).c_str()}; + for (auto* parameter : sequence) + { + parameter->getAsTyped()->getType().appendMangledName(name); + } + function.Name = name.c_str(); + aggregate->setName(name); + m_functionStates.emplace(originalName, std::move(function)); + } + } + } + } + + TIntermTyped* SamplerHeight(TIntermTyped* sampler) + { + auto* symbol = sampler->getAsSymbolNode(); + if (symbol == nullptr || !m_samplerStates.count(symbol->getId())) + { + throw std::runtime_error{"Sampler filter state requires a standalone sampler uniform or function parameter."}; + } + auto* state = m_intermediate->addSymbol(*m_samplerStates.at(symbol->getId())); + if (state->getType().isVector()) + { + return Component(state, 0); + } + return state; + } + + TIntermTyped* Component(TIntermTyped* vector, int index) + { + const auto& loc = vector->getLoc(); + auto* result = m_intermediate->addIndex(EOpIndexDirect, vector, m_intermediate->addConstantUnion(index, loc), loc); + result->setType(TType{EbtFloat, EvqTemporary}); + return result; + } + + TIntermTyped* FlipSampleCoordinate(TIntermTyped* coordinate, TIntermTyped* sampler) + { + const auto& loc = coordinate->getLoc(); + const auto size = coordinate->getType().getVectorSize(); + TIntermSymbol temporary{m_ids.Next(), "bnSampleCoordinate", TType{EbtFloat, EvqTemporary, size}}; + auto* capture = m_intermediate->addAssign(EOpAssign, m_intermediate->addSymbol(temporary), coordinate, loc); + auto* height = SamplerHeight(sampler); + auto* y = Component(m_intermediate->addSymbol(temporary), 1); + auto* pixel = m_intermediate->addBinaryMath(EOpMul, y, height, loc); + auto* rounded = m_intermediate->addBuiltInFunctionCall(loc, EOpFloor, true, pixel, TType{EbtFloat, EvqTemporary}); + auto* center = m_intermediate->addBinaryMath(EOpAdd, rounded, m_intermediate->addConstantUnion(0.5, EbtFloat, loc), loc); + auto* normalized = m_intermediate->addBinaryMath(EOpDiv, center, SamplerHeight(sampler), loc); + auto* components = m_intermediate->makeAggregate(Component(m_intermediate->addSymbol(temporary), 0), loc); + components = m_intermediate->growAggregate(components, normalized, loc); + for (int index = 2; index < size; ++index) + { + components = m_intermediate->growAggregate(components, Component(m_intermediate->addSymbol(temporary), index), loc); + } + const auto operation = size == 2 ? EOpConstructVec2 : size == 3 ? EOpConstructVec3 : EOpConstructVec4; + auto* snapped = m_intermediate->setAggregateOperator(components, operation, temporary.getType(), loc); + auto* enabled = m_intermediate->addBinaryMath(EOpGreaterThan, SamplerHeight(sampler), + m_intermediate->addConstantUnion(0.0, EbtFloat, loc), loc); + // Select the GL nearest texel before changing origins. Flipping a boundary first + // selects the adjacent row. Linear and mip-filtered samples retain their coordinates. + auto* selected = m_intermediate->addSelection(enabled, snapped, m_intermediate->addSymbol(temporary), loc); + return m_intermediate->addComma(capture, FlipVerticalCoordinate(selected), loc); + } + // Rewrite texelFetch after nested samples have been visited. FlipSamplerCoordinatesTraverser(TIntermediate* intermediate, IdGenerator& ids) : TIntermTraverser{true, false, true} @@ -2378,11 +2525,16 @@ namespace Babylon::ShaderCompilerTraversers TIntermediate* m_intermediate{}; IdGenerator& m_ids; + std::map m_samplerStates; + std::map m_functionStates; }; - /// Convert gl_FragCoord to GL coordinates on top-left-origin backends. - /// Pixel centers require targetHeight - y, without a -1 term. - /// Use the full target height, not u_viewRect, which may describe a smaller viewport. + /// Convert gl_FragCoord.y and dFdy to GL orientation on top-left-origin backends, driven by the + /// per-draw RENDER_TARGET_TRANSFORM_UNIFORM_NAME so one shader serves both storage orders: + /// gl_FragCoord.y -> T.x + T.y * gl_FragCoord.y (targetHeight - y on ordinary targets) + /// dFdy(v) -> dFdy(v * T.y) (dFdy(-v) on ordinary targets, as bgfx_shader.sh) + /// Pixel centers require targetHeight - y, without a -1 term. Use the full target height, not + /// u_viewRect, which may describe a smaller viewport. class FragCoordYFlipTraverser final : private TIntermTraverser { public: @@ -2397,23 +2549,23 @@ namespace Babylon::ShaderCompilerTraversers FragCoordYFlipTraverser traverser{intermediate}; intermediate->getTreeRoot()->traverse(&traverser); - if (traverser.m_symbolsToParents.empty()) + if (traverser.m_symbolsToParents.empty() && traverser.m_derivatives.empty()) { return; } - // Declare before uniform collection so target size joins the Frame struct and uniform table. - TType targetSizeType{EbtFloat, EvqUniform, 4}; - TIntermSymbol* targetSize{intermediate->addSymbol(TIntermSymbol{ids.Next(), Graphics::FRAGCOORD_TARGET_SIZE_UNIFORM_NAME, targetSizeType})}; + // Declare before uniform collection so the transform joins the Frame struct and uniform table. + TType transformType{EbtFloat, EvqUniform, 4}; + TIntermSymbol* transform{intermediate->addSymbol(TIntermSymbol{ids.Next(), Graphics::RENDER_TARGET_TRANSFORM_UNIFORM_NAME, transformType})}; auto* linkerObjects = FindLinkerObjects(intermediate->getTreeRoot()->getAsAggregate()); if (linkerObjects == nullptr) { throw std::runtime_error{"FragCoordYFlip: fragment stage has no linker objects sequence."}; } - linkerObjects->getSequence().push_back(targetSize); + linkerObjects->getSequence().push_back(transform); - traverser.ApplyReplacements(targetSize); + traverser.ApplyReplacements(transform); } protected: @@ -2428,6 +2580,16 @@ namespace Babylon::ShaderCompilerTraversers m_symbolsToParents.emplace_back(symbol, getParentNode()); } + bool visitUnary(TVisit, TIntermUnary* unary) override + { + const auto op = unary->getOp(); + if (op == EOpDPdy || op == EOpDPdyFine || op == EOpDPdyCoarse) + { + m_derivatives.push_back(unary); + } + return true; + } + private: FragCoordYFlipTraverser(TIntermediate* intermediate) : TIntermTraverser{true, false, false} @@ -2454,17 +2616,35 @@ namespace Babylon::ShaderCompilerTraversers return nullptr; } - void ApplyReplacements(TIntermSymbol* targetSize) + void ApplyReplacements(TIntermSymbol* transform) { for (const auto& [symbol, parent] : m_symbolsToParents) { // Build a fresh replacement per occurrence to avoid sharing AST parents. - MakeReplacements({{"gl_FragCoord", BuildFlippedFragCoord(symbol, targetSize)}}, {{symbol, parent}}); + MakeReplacements({{"gl_FragCoord", BuildFlippedFragCoord(symbol, transform)}}, {{symbol, parent}}); + } + + for (auto* derivative : m_derivatives) + { + TIntermTyped* operand{derivative->getOperand()}; + TIntermTyped* scaled{m_intermediate->addBinaryMath(EOpMul, operand, TransformComponent(transform, 1, operand->getLoc()), operand->getLoc())}; + if (scaled == nullptr) + { + throw std::runtime_error{"FragCoordYFlip: could not scale a dFdy operand."}; + } + derivative->setOperand(scaled); } } + TIntermTyped* TransformComponent(TIntermSymbol* transform, int index, const TSourceLoc& loc) + { + TIntermTyped* component{m_intermediate->addIndex(EOpIndexDirect, m_intermediate->addSymbol(*transform), m_intermediate->addConstantUnion(index, loc), loc)}; + component->setType(TType{EbtFloat, EvqTemporary, 1}); + return component; + } + /// Reconstruct the vec4 to support whole-vector, swizzled, and indexed reads. - TIntermTyped* BuildFlippedFragCoord(TIntermSymbol* fragCoord, TIntermSymbol* targetSize) + TIntermTyped* BuildFlippedFragCoord(TIntermSymbol* fragCoord, TIntermSymbol* transform) { const TSourceLoc& loc{fragCoord->getLoc()}; TType floatType{EbtFloat, EvqTemporary, 1}; @@ -2478,10 +2658,8 @@ namespace Babylon::ShaderCompilerTraversers return element; }; - TIntermTyped* height{m_intermediate->addIndex(EOpIndexDirect, m_intermediate->addSymbol(*targetSize), m_intermediate->addConstantUnion(1, loc), loc)}; - height->setType(floatType); - - TIntermTyped* flippedY{m_intermediate->addBinaryMath(EOpSub, height, component(1), loc)}; + TIntermTyped* scaledY{m_intermediate->addBinaryMath(EOpMul, TransformComponent(transform, 1, loc), component(1), loc)}; + TIntermTyped* flippedY{m_intermediate->addBinaryMath(EOpAdd, TransformComponent(transform, 0, loc), scaledY, loc)}; TIntermAggregate* constructed{m_intermediate->makeAggregate(component(0), loc)}; constructed = m_intermediate->growAggregate(constructed, flippedY, loc); @@ -2492,6 +2670,7 @@ namespace Babylon::ShaderCompilerTraversers TIntermediate* m_intermediate{}; std::vector> m_symbolsToParents{}; + std::vector m_derivatives{}; }; /// Assign matching locations across stages; mutate symbols in place to retain single-parent ASTs. @@ -2655,11 +2834,6 @@ namespace Babylon::ShaderCompilerTraversers InterStageVaryingLocationTraverser::Traverse(program, ids); } - void InvertYDerivativeOperands(TProgram& program) - { - InvertYDerivativeOperandsTraverser::Traverse(program); - } - void FlipSamplerCoordinates(TProgram& program, IdGenerator& ids) { FlipSamplerCoordinatesTraverser::Traverse(program, ids); diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.h b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.h index cb0ac8010a..37d58a9cb6 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.h +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.h @@ -164,18 +164,13 @@ namespace Babylon::ShaderCompilerTraversers /// Reserve complete matrix, array and block-member ranges. Run after FlattenNarrowVaryingArrays. void AssignInterStageVaryingLocations(glslang::TProgram& program, IdGenerator& ids); - /// Invert dFdy operands similar to bgfx_shader.sh - /// https://github.com/bkaradzic/bgfx/blob/7be225bf490bb1cd231cfb4abf7e617bf35b59cb/src/bgfx_shader.sh#L44-L45 - /// https://github.com/bkaradzic/bgfx/blob/7be225bf490bb1cd231cfb4abf7e617bf35b59cb/src/bgfx_shader.sh#L62-L65 - void InvertYDerivativeOperands(glslang::TProgram& program); - /// Flip only texture-coordinate Y on D3D/Metal/Vulkan, including shadow and volume samplers. /// For textureGrad, also reflect gradient Y without translation. For texelFetch, capture /// coordinates and LOD once and use the selected mip height. Preserve other components and cube directions. /// Leave raw array coordinates unchanged. Do not apply on OpenGL. void FlipSamplerCoordinates(glslang::TProgram& program, IdGenerator& ids); - /// Convert fragment coordinates with bnFragCoordTargetSize on D3D/Metal/Vulkan. - /// Run before uniform collection; shaders without gl_FragCoord remain unchanged. + /// Convert gl_FragCoord.y and dFdy with bnRenderTargetTransform on D3D/Metal/Vulkan; dFdy follows + /// bgfx_shader.sh on ordinary targets. Run before uniform collection; shaders using neither are unchanged. void FlipFragCoordY(glslang::TProgram& program, IdGenerator& ids); } diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp index eace9900c0..7fced1ea70 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,46 @@ namespace // bgfx Vulkan m_bindInfo is indexed by sampler stage 0..15. constexpr unsigned kMaxBgfxTextureStages = 16; + // TIntermSymbol stores its TType by value, so a layout change on a linker object does not reach + // the uses in function bodies (which GlslangToSpv may visit first). Copy linker-object bindings + // to every symbol node with the same id. + class SymbolBindingPropagator final : public TIntermTraverser + { + public: + explicit SymbolBindingPropagator(std::map bindings) + : m_bindings{std::move(bindings)} + { + } + + void visitSymbol(TIntermSymbol* symbol) override + { + if (auto it = m_bindings.find(symbol->getId()); it != m_bindings.end()) + { + symbol->getWritableType().getQualifier().layoutBinding = it->second; + } + } + + private: + std::map m_bindings; + }; + + void PropagateLinkerObjectBindings(TIntermediate* intermediate) + { + auto* root = intermediate->getTreeRoot()->getAsAggregate(); + auto* linkerObjects = root->getSequence().back()->getAsAggregate(); + std::map bindings; + for (TIntermNode* node : linkerObjects->getSequence()) + { + auto* symbol = node->getAsSymbolNode(); + if (symbol != nullptr && symbol->getType().getQualifier().hasBinding()) + { + bindings[symbol->getId()] = symbol->getType().getQualifier().layoutBinding; + } + } + SymbolBindingPropagator propagator{std::move(bindings)}; + intermediate->getTreeRoot()->traverse(&propagator); + } + class VulkanInterfaceLocationTraverser final : public TIntermTraverser { public: @@ -187,6 +228,54 @@ namespace } } + void ApplyBgfxVulkanStageBindings(glslang::TIntermediate* intermediate, unsigned uboBinding) + { + std::vector textures; + std::vector pureSamplers; + std::vector ubos; + CollectStageUniforms(intermediate, textures, pureSamplers, ubos); + + // SamplerSplitterTraverser assigns one shared layoutBinding per sampler name + // across VS/FS. Preserve that slot and only apply bgfx's fixed shifts — + // do not compact per-stage (different subsets would desync the same + // sampler across stages and overwrite UniformStages). + for (auto* texture : textures) + { + const unsigned originalBinding = texture->getType().getQualifier().layoutBinding; + const unsigned imageBinding = kSpirvBindShift + originalBinding; + texture->getWritableType().getQualifier().layoutBinding = imageBinding; + + std::string texName = texture->getName().c_str(); + std::string baseName = texName; + constexpr char kSuffix[] = "Texture"; + constexpr size_t kSuffixLen = sizeof(kSuffix) - 1; + if (baseName.size() > kSuffixLen && baseName.compare(baseName.size() - kSuffixLen, kSuffixLen, kSuffix) == 0) + { + baseName.resize(baseName.size() - kSuffixLen); + } + + for (auto* samplerSymbol : pureSamplers) + { + if (std::string{samplerSymbol->getName().c_str()} == baseName) + { + samplerSymbol->getWritableType().getQualifier().layoutBinding = imageBinding + kSpirvSamplerShift; + break; + } + } + } + + for (auto* ubo : ubos) + { + // Frame is the UBO synthesized by MoveNonSamplerUniformsIntoStruct. + // Extra user UBOs are rare on the Vulkan path; leave their existing + // bindings alone so we do not collide with the image range. + if (std::string{ubo->getName().c_str()} == "Frame" || ubos.size() == 1) + { + ubo->getWritableType().getQualifier().layoutBinding = uboBinding; + } + } + } + void ApplyBgfxVulkanResourceBindings(glslang::TProgram& program) { // Count distinct sampler slots shared across VS/FS (SamplerSplitter assigns one @@ -212,55 +301,33 @@ namespace } } - auto applyStage = [](glslang::TIntermediate* intermediate, unsigned uboBinding) { - std::vector textures; - std::vector pureSamplers; - std::vector ubos; - CollectStageUniforms(intermediate, textures, pureSamplers, ubos); - - // SamplerSplitterTraverser assigns one shared layoutBinding per sampler name - // across VS/FS. Preserve that slot and only apply bgfx's fixed shifts — - // do not compact per-stage (different subsets would desync the same - // sampler across stages and overwrite UniformStages). - for (auto* texture : textures) - { - const unsigned originalBinding = texture->getType().getQualifier().layoutBinding; - const unsigned imageBinding = kSpirvBindShift + originalBinding; - texture->getWritableType().getQualifier().layoutBinding = imageBinding; - - std::string texName = texture->getName().c_str(); - std::string baseName = texName; - constexpr char kSuffix[] = "Texture"; - constexpr size_t kSuffixLen = sizeof(kSuffix) - 1; - if (baseName.size() > kSuffixLen && baseName.compare(baseName.size() - kSuffixLen, kSuffixLen, kSuffix) == 0) - { - baseName.resize(baseName.size() - kSuffixLen); - } + ApplyBgfxVulkanStageBindings(program.getIntermediate(EShLangVertex), kSpirvVertexBinding); + ApplyBgfxVulkanStageBindings(program.getIntermediate(EShLangFragment), kSpirvFragmentBinding); + } - for (auto* samplerSymbol : pureSamplers) - { - if (std::string{samplerSymbol->getName().c_str()} == baseName) - { - samplerSymbol->getWritableType().getQualifier().layoutBinding = imageBinding + kSpirvSamplerShift; - break; - } - } - } + void ApplyBgfxVulkanComputeResourceBindings(glslang::TProgram& program) + { + auto* intermediate = program.getIntermediate(EShLangCompute); + ApplyBgfxVulkanStageBindings(intermediate, kSpirvVertexBinding); - for (auto* ubo : ubos) + // Storage buffers share bgfx's stage space with textures: binding = stage + kSpirvBindShift. + auto* root = intermediate->getTreeRoot()->getAsAggregate(); + auto* linkerObjects = root->getSequence().back()->getAsAggregate(); + for (TIntermNode* node : linkerObjects->getSequence()) + { + auto* symbol = node->getAsSymbolNode(); + if (symbol != nullptr && symbol->getType().getQualifier().storage == glslang::EvqBuffer) { - // Frame is the UBO synthesized by MoveNonSamplerUniformsIntoStruct. - // Extra user UBOs are rare on the Vulkan path; leave their existing - // bindings alone so we do not collide with the image range. - if (std::string{ubo->getName().c_str()} == "Frame" || ubos.size() == 1) + auto& qualifier = symbol->getWritableType().getQualifier(); + if (!qualifier.hasBinding() || qualifier.layoutBinding >= kMaxBgfxTextureStages) { - ubo->getWritableType().getQualifier().layoutBinding = uboBinding; + throw std::runtime_error{"Vulkan compute storage buffers need an explicit binding below 16"}; } + qualifier.layoutBinding += kSpirvBindShift; } - }; + } - applyStage(program.getIntermediate(EShLangVertex), kSpirvVertexBinding); - applyStage(program.getIntermediate(EShLangFragment), kSpirvFragmentBinding); + PropagateLinkerObjectBindings(intermediate); } // Vulkan-only bgfx sampler packaging: separate_images with Texture-suffix stripped, @@ -355,6 +422,11 @@ namespace Babylon::Plugins Graphics::BgfxShaderInfo ShaderCompiler::Compile(std::string_view vertexSource, std::string_view fragmentSource, const std::map& instancedAttributes) { + auto vertexText = PreprocessShader(EShLangVertex, vertexSource); + auto fragmentText = PreprocessShader(EShLangFragment, fragmentSource); + auto uniformNames = RenameShaderUniforms(vertexText, &fragmentText); + vertexSource = vertexText; + fragmentSource = fragmentText; glslang::TProgram program; glslang::TShader vertexShader{EShLangVertex}; @@ -400,7 +472,6 @@ namespace Babylon::Plugins ShaderCompilerTraversers::SplitSamplerFunctionParameters(program, ids); ApplyBgfxVulkanResourceBindings(program); ShaderCompilerTraversers::ZeroInitializeStructLocals(program); - ShaderCompilerTraversers::InvertYDerivativeOperands(program); std::vector spirvVS; auto [vertexParser, vertexCompiler] = CompileShader(program, EShLangVertex, spirvVS); @@ -408,11 +479,52 @@ namespace Babylon::Plugins std::vector spirvFS; auto [fragmentParser, fragmentCompiler] = CompileShader(program, EShLangFragment, spirvFS); - return CreateBgfxShader( + auto result = CreateBgfxShader( {std::move(vertexParser), std::move(vertexCompiler), gsl::make_span(reinterpret_cast(spirvVS.data()), spirvVS.size() * sizeof(uint32_t)), std::move(vertexAttributeRenaming)}, {std::move(fragmentParser), std::move(fragmentCompiler), gsl::make_span(reinterpret_cast(spirvFS.data()), spirvFS.size() * sizeof(uint32_t)), {}}, std::move(builtInInstanceDataSlots), SamplerResourceSet::SeparateImages, AppendSamplersVulkan); + result.UniformNames = std::move(uniformNames); + return result; + } + + Graphics::BgfxShaderInfo ShaderCompiler::CompileCompute(std::string_view computeSource) + { + auto source = PreprocessShader(EShLangCompute, computeSource); + auto uniformNames = RenameShaderUniforms(source); + computeSource = source; + glslang::TProgram program; + + glslang::TShader computeShader{EShLangCompute}; + AddShader(program, computeShader, ProcessSamplerFlip(computeSource)); + + glslang::SpvVersion spv{}; + spv.spv = 0x10000; + computeShader.getIntermediate()->setSpv(spv); + + if (!program.link(EShMsgDefault)) + { + throw std::runtime_error{program.getInfoLog()}; + } + + ShaderCompilerTraversers::IdGenerator ids{}; + ShaderCompilerTraversers::FlipSamplerCoordinates(program, ids); + auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids); + auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids); + ShaderCompilerTraversers::SplitSamplersIntoSamplersAndTextures(program, ids); + ShaderCompilerTraversers::SplitSamplerFunctionParameters(program, ids); + ApplyBgfxVulkanComputeResourceBindings(program); + ShaderCompilerTraversers::ZeroInitializeStructLocals(program); + + std::vector spirvCS; + auto [computeParser, computeCompiler] = CompileShader(program, EShLangCompute, spirvCS); + + auto result = CreateBgfxComputeShader( + {std::move(computeParser), std::move(computeCompiler), gsl::make_span(reinterpret_cast(spirvCS.data()), spirvCS.size() * sizeof(uint32_t)), {}}, + SamplerResourceSet::SeparateImages, + AppendSamplersVulkan); + result.UniformNames = std::move(uniformNames); + return result; } } diff --git a/Plugins/ShaderTool/README.md b/Plugins/ShaderTool/README.md index a78ad543b7..f6335c716a 100644 --- a/Plugins/ShaderTool/README.md +++ b/Plugins/ShaderTool/README.md @@ -4,12 +4,14 @@ A cross-platform console tool that compiles GLSL shaders using the ShaderCompile ## Overview -This tool takes vertex and fragment shader source files (in GLSL format) as input, compiles them for the target graphics API using Babylon Native's ShaderCompiler, and saves the compiled shader data to an output file using the ShaderCache component. Multiple shader pairs can be compiled into a single cache file. +This tool takes vertex and fragment shader source files (in GLSL format) as input, compiles them for the target graphics API using Babylon Native's ShaderCompiler, and saves the compiled shader data to an output file using the ShaderCache component. Multiple shader pairs can be compiled into a single cache file. It can also precompile the shaders NativeEngine generates at run time for WebGL2 transform feedback programs and for divisor-driven instanced attributes. + +Shader sources must exactly match the strings Babylon Native receives at run time (`createProgram`); CR/LF differences are ignored. ## Usage ``` -ShaderTool -o [ ...] +ShaderTool -o [-v ]... [[-i ] ...] ``` ### Options @@ -17,6 +19,8 @@ ShaderTool -o [ ...] | Option | Description | |--------|-------------| | `-o ` | Path to the output compiled shader cache file (required) | +| `-v ` | Transform feedback vertex shader and its comma-separated captured varyings (interleaved, in capture order); may be repeated | +| `-i ` | Comma-separated attributes the next vertex/fragment pair reads with a vertex divisor; caches that instanced variant as well | ### Arguments @@ -36,6 +40,11 @@ Multiple shader pairs: ShaderTool -o cache.bin v1.glsl f1.glsl v2.glsl f2.glsl v3.glsl f3.glsl ``` +Transform feedback update shader plus an instanced render pair: +```bash +ShaderTool -o cache.bin -v update.glsl outPosition,outAge -i position,age v1.glsl f1.glsl +``` + ## Build Integration This tool can be integrated into your build process using CMake custom commands: diff --git a/Plugins/ShaderTool/Source/Main.cpp b/Plugins/ShaderTool/Source/Main.cpp index 9186524c2d..f79cbc28c4 100644 --- a/Plugins/ShaderTool/Source/Main.cpp +++ b/Plugins/ShaderTool/Source/Main.cpp @@ -5,7 +5,7 @@ * the ShaderCompiler component and saves the compiled results using ShaderCache. * * Usage: - * ShaderTool -o [ ...] + * ShaderTool -o [-v ]... [[-i ] ...] */ #include @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include namespace @@ -27,17 +29,85 @@ namespace { std::filesystem::path vertexPath; std::filesystem::path fragmentPath; + // Divisor-driven instanced attribute names; when set, the instanced variant is cached too. + std::vector instancedAttributes; }; + struct TransformFeedbackShader + { + std::filesystem::path vertexPath; + std::vector varyings; + }; + + std::vector SplitList(std::string_view list) + { + std::vector items; + std::stringstream stream{std::string{list}}; + std::string item; + while (std::getline(stream, item, ',')) + { + if (!item.empty()) + { + items.push_back(item); + } + } + return items; + } + + // Mirrors the runtime draw path (VertexBuffer::CreateInstanceDataLayout + NativeEngine::DrawInternal): + // built-in instance attributes keep their compiler-assigned slots, and the remaining instanced + // attributes take the following slots in descending attribute-location order. + std::map ResolveInstancedAttributes(const Babylon::Graphics::BgfxShaderInfo& baseShader, const std::vector& names) + { + uint32_t builtInCount = static_cast(baseShader.BuiltInInstanceDataSlots.size()); + if (builtInCount == 0) + { + for (const auto& [name, location] : baseShader.VertexAttributeLocations) + { + builtInCount += Babylon::Graphics::IsBuiltInInstanceAttributeName(name) ? 1 : 0; + } + } + + std::map generic; + for (const auto& name : names) + { + const auto location = baseShader.VertexAttributeLocations.find(name); + if (location == baseShader.VertexAttributeLocations.end()) + { + throw std::runtime_error("Instanced attribute is not a vertex shader input: " + name); + } + if (!Babylon::Graphics::IsBuiltInInstanceAttributeName(name)) + { + generic.emplace(location->second, name); + } + } + + std::map instancedAttributes; + uint32_t slot = builtInCount; + for (auto attribute = generic.rbegin(); attribute != generic.rend(); ++attribute, ++slot) + { + if (slot >= Babylon::Graphics::MAX_INSTANCE_DATA_SLOT_COUNT) + { + throw std::runtime_error("Too many instanced attributes"); + } + instancedAttributes.emplace(attribute->second, Babylon::Graphics::INSTANCE_DATA_FIRST_LOCATION - slot); + } + return instancedAttributes; + } + void PrintUsage(const char* programName) { std::cerr << "Babylon Native Shader Tool" << std::endl; std::cerr << std::endl; std::cerr << "Usage:" << std::endl; - std::cerr << " " << programName << " -o [ ...]" << std::endl; + std::cerr << " " << programName << " -o [-v ]... [[-i ] ...]" << std::endl; std::cerr << std::endl; std::cerr << "Options:" << std::endl; - std::cerr << " -o Path to the output compiled shader cache file" << std::endl; + std::cerr << " -o Path to the output compiled shader cache file" << std::endl; + std::cerr << " -v Transform feedback vertex shader and its comma-separated captured" << std::endl; + std::cerr << " varyings (interleaved, in capture order); may be repeated" << std::endl; + std::cerr << " -i Comma-separated attributes the next vertex/fragment pair reads with" << std::endl; + std::cerr << " a vertex divisor; caches that instanced variant as well" << std::endl; std::cerr << std::endl; std::cerr << "Arguments:" << std::endl; std::cerr << " Pairs of vertex and fragment shader source files (GLSL)" << std::endl; @@ -45,6 +115,7 @@ namespace std::cerr << "Examples:" << std::endl; std::cerr << " " << programName << " -o cache.bin vertex.glsl fragment.glsl" << std::endl; std::cerr << " " << programName << " -o cache.bin v1.glsl f1.glsl v2.glsl f2.glsl" << std::endl; + std::cerr << " " << programName << " -o cache.bin -v update.glsl outPosition,outAge -i position,age v1.glsl f1.glsl" << std::endl; } std::string ReadFileContents(const std::filesystem::path& filePath) @@ -96,39 +167,67 @@ namespace return true; } - bool ParseArguments(int argc, char* argv[], std::filesystem::path& outputPath, std::vector& shaderPairs) + bool ParseArguments(int argc, char* argv[], std::filesystem::path& outputPath, std::vector& shaderPairs, + std::vector& transformFeedbackShaders) { - // Find -o flag - int outputIndex = -1; - for (int i = 1; i < argc - 1; i++) + bool hasOutput = false; + std::vector remaining; + std::map> instancedAttributes; + for (int i = 1; i < argc; i++) { + if (std::strcmp(argv[i], "-v") == 0) + { + if (i + 2 >= argc) + { + std::cerr << "Error: -v requires a vertex shader and a varyings list" << std::endl; + return false; + } + TransformFeedbackShader shader{argv[i + 1], SplitList(argv[i + 2])}; + if (shader.varyings.empty()) + { + std::cerr << "Error: -v requires at least one varying" << std::endl; + return false; + } + transformFeedbackShaders.push_back(std::move(shader)); + i += 2; + continue; + } + if (std::strcmp(argv[i], "-i") == 0) + { + if (i + 1 >= argc || remaining.size() % 2 != 0) + { + std::cerr << "Error: -i requires an attribute list and must precede a vertex/fragment pair" << std::endl; + return false; + } + instancedAttributes[remaining.size() / 2] = SplitList(argv[++i]); + continue; + } if (std::strcmp(argv[i], "-o") == 0) { - outputIndex = i; - outputPath = argv[i + 1]; - break; + if (i + 1 >= argc) + { + std::cerr << "Error: Missing value for -o option" << std::endl; + return false; + } + if (hasOutput) + { + std::cerr << "Error: -o may only be specified once" << std::endl; + return false; + } + hasOutput = true; + outputPath = argv[++i]; + continue; } + remaining.push_back(argv[i]); } - if (outputIndex == -1) + if (!hasOutput) { std::cerr << "Error: Missing required -o option" << std::endl; return false; } - // Collect remaining arguments (excluding -o and its value) directly into shader pairs - std::vector remaining; - for (int i = 1; i < argc; i++) - { - if (i == outputIndex) - { - i++; // Skip -o and its value - continue; - } - remaining.push_back(argv[i]); - } - - if (remaining.empty()) + if (remaining.empty() && transformFeedbackShaders.empty()) { std::cerr << "Error: No shader files specified" << std::endl; return false; @@ -143,7 +242,17 @@ namespace for (size_t i = 0; i < remaining.size(); i += 2) { - shaderPairs.push_back({std::filesystem::path(remaining[i]), std::filesystem::path(remaining[i + 1])}); + shaderPairs.push_back({std::filesystem::path(remaining[i]), std::filesystem::path(remaining[i + 1]), {}}); + } + + for (auto& [pairIndex, names] : instancedAttributes) + { + if (pairIndex >= shaderPairs.size() || names.empty()) + { + std::cerr << "Error: -i must be followed by a vertex/fragment pair and list at least one attribute" << std::endl; + return false; + } + shaderPairs[pairIndex].instancedAttributes = std::move(names); } return true; @@ -154,9 +263,10 @@ int main(int argc, char* argv[]) { std::filesystem::path outputPath; std::vector shaderPairs; + std::vector transformFeedbackShaders; // Parse command line arguments - if (!ParseArguments(argc, argv, outputPath, shaderPairs)) + if (!ParseArguments(argc, argv, outputPath, shaderPairs, transformFeedbackShaders)) { std::cerr << std::endl; PrintUsage(argv[0]); @@ -179,6 +289,14 @@ int main(int argc, char* argv[]) } } + for (const auto& shader : transformFeedbackShaders) + { + if (!ValidateFilePath(shader.vertexPath, "Transform feedback vertex shader")) + { + return EXIT_FAILURE; + } + } + try { // Enable the shader cache @@ -194,7 +312,26 @@ int main(int argc, char* argv[]) std::string vertexSource = ReadFileContents(pair.vertexPath); std::string fragmentSource = ReadFileContents(pair.fragmentPath); - Babylon::Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, compiler.Compile(vertexSource, fragmentSource)); + const auto baseShader = Babylon::Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, compiler.Compile(vertexSource, fragmentSource)); + if (!pair.instancedAttributes.empty()) + { + const auto instancedAttributes = ResolveInstancedAttributes(*baseShader, pair.instancedAttributes); + if (!instancedAttributes.empty()) + { + std::cout << "Compiling instanced variant: " << pair.vertexPath.string() << std::endl; + Babylon::Plugins::ShaderCache::AddShader(vertexSource, fragmentSource, + compiler.Compile(vertexSource, fragmentSource, instancedAttributes), instancedAttributes); + } + } + } + + for (const auto& shader : transformFeedbackShaders) + { + std::cout << "Compiling transform feedback: " << shader.vertexPath.string() << std::endl; + + std::string vertexSource = ReadFileContents(shader.vertexPath); + Babylon::Plugins::ShaderCache::AddTransformFeedbackShader(vertexSource, shader.varyings, + compiler.CompileTransformFeedback(vertexSource, shader.varyings)); } // Save the shader cache to the output file diff --git a/Polyfills/Canvas/CMakeLists.txt b/Polyfills/Canvas/CMakeLists.txt index 99600c220e..3a6dfa115b 100644 --- a/Polyfills/Canvas/CMakeLists.txt +++ b/Polyfills/Canvas/CMakeLists.txt @@ -1,8 +1,4 @@ -include(${CMAKE_CURRENT_LIST_DIR}/shaderc.cmake) - -if(NOT IOS AND NOT VISIONOS AND NOT ANDROID) - set(SHADERC_PATH "" CACHE FILEPATH "Optional full path to shaderc built from bgfx.") -endif() +include(${CMAKE_CURRENT_LIST_DIR}/../../Dependencies/shaderc.cmake) set(SOURCES "Include/Babylon/Polyfills/Canvas.h" @@ -63,12 +59,6 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES) endif() if(SHADERC_PATH AND NOT IOS AND NOT VISIONOS AND NOT ANDROID) - if(EXISTS "${SHADERC_PATH}") - message(STATUS "Using shaderc from: ${SHADERC_PATH}") - else() - message(FATAL_ERROR " '${SHADERC_PATH}' does not exist.") - endif() - foreach(SHADER ${SHADERS}) add_bgfx_shader(${SHADER} "") endforeach() diff --git a/Polyfills/Canvas/Readme.md b/Polyfills/Canvas/Readme.md index e0c8022be9..5db46e0544 100644 --- a/Polyfills/Canvas/Readme.md +++ b/Polyfills/Canvas/Readme.md @@ -87,6 +87,9 @@ To obtain shaderc: If `SHADERC_PATH` is set but does not point to a valid executable, CMake will display a fatal error during project generation. When correctly configured, the BabylonNative project will automatically use shaderc to compile all .sc shader files. +Canvas and NativeEngine's fixed instance-repacking shaders share the generator in +`Dependencies/shaderc.cmake`. Commit the regenerated headers alongside their `.sc` +source; ordinary builds consume those headers without requiring shaderc. ## Embedding Shaders: diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index 707c3a8582..715f65036d 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -967,7 +967,12 @@ namespace Babylon::Polyfills::Internal nvgSetViewport(*m_nvg, float(width), float(height)); nvgSetFrameBufferAndEncoder(*m_nvg, frameBuffer, encoder); - nvgSetFrameBufferPool(*m_nvg, { acquire, release }); + const auto samplerFlags = [this] { + // Border colors share bgfx's palette with framebuffer clears. + const auto transparent = m_graphicsContext.AcquireClearPaletteIndex({0.f, 0.f, 0.f, 0.f}); + return BGFX_SAMPLER_U_BORDER | BGFX_SAMPLER_V_BORDER | BGFX_SAMPLER_BORDER_COLOR(transparent); + }; + nvgSetFrameBufferPool(*m_nvg, { acquire, release, samplerFlags }); nvgEndFrame(*m_nvg); ReleaseImagesAfterFlush(); ReleaseClipMasksAfterFlush(); diff --git a/Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp b/Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp index cc4d582726..ac27d551f5 100644 --- a/Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp +++ b/Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp @@ -799,7 +799,7 @@ namespace gl->encoder->setUniform(gl->u_params, params); gl->encoder->setState(gl->state); gl->encoder->setVertexBuffer(0, &vertices); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(input->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(input->Handle()), gl->frameBufferPool.samplerFlags()); gl->encoder->setTexture(1, gl->s_tex2, gl->texMissing); gl->encoder->setTexture(2, gl->s_clip, gl->thClip); output->Submit(*gl->encoder, gl->prog, BGFX_DISCARD_ALL); @@ -889,7 +889,7 @@ namespace }; std::function filterPass = [gl, call](bgfx::ProgramHandle prog, Babylon::Graphics::FrameBuffer *inBuffer, Babylon::Graphics::FrameBuffer *outBuffer) { gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -903,7 +903,7 @@ namespace gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA) | BGFX_STATE_BLEND_EQUATION(BGFX_STATE_BLEND_EQUATION_ADD)); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -955,7 +955,7 @@ namespace }; std::function filterPass = [gl, call](bgfx::ProgramHandle prog, Babylon::Graphics::FrameBuffer *inBuffer, Babylon::Graphics::FrameBuffer *outBuffer) { gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -969,7 +969,7 @@ namespace gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA) | BGFX_STATE_BLEND_EQUATION(BGFX_STATE_BLEND_EQUATION_ADD)); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -1003,7 +1003,7 @@ namespace }; std::function filterPass = [gl, call](bgfx::ProgramHandle prog, Babylon::Graphics::FrameBuffer *inBuffer, Babylon::Graphics::FrameBuffer *outBuffer) { gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -1017,7 +1017,7 @@ namespace gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA) | BGFX_STATE_BLEND_EQUATION(BGFX_STATE_BLEND_EQUATION_ADD)); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -1046,7 +1046,7 @@ namespace }; std::function filterPass = [gl, call](bgfx::ProgramHandle prog, Babylon::Graphics::FrameBuffer *inBuffer, Babylon::Graphics::FrameBuffer *outBuffer) { gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); @@ -1060,7 +1060,7 @@ namespace gl->encoder->setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA) | BGFX_STATE_BLEND_EQUATION(BGFX_STATE_BLEND_EQUATION_ADD)); - gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle())); + gl->encoder->setTexture(0, gl->s_tex, bgfx::getTexture(inBuffer->Handle()), gl->frameBufferPool.samplerFlags()); bool s_originBottomLeft = bgfx::getCaps()->originBottomLeft; screenSpaceQuad(gl->encoder, s_originBottomLeft); outBuffer->Submit(*gl->encoder, prog, BGFX_DISCARD_ALL); diff --git a/Polyfills/Canvas/Source/nanovg/nanovg_babylon.h b/Polyfills/Canvas/Source/nanovg/nanovg_babylon.h index 17c2afe84d..2144b03dc8 100644 --- a/Polyfills/Canvas/Source/nanovg/nanovg_babylon.h +++ b/Polyfills/Canvas/Source/nanovg/nanovg_babylon.h @@ -26,6 +26,7 @@ struct PoolInterface { std::function acquire; std::function release; + std::function samplerFlags; }; void nvgSetFrameBufferPool(NVGcontext* _ctx, PoolInterface pool);