diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index da7b7b23fe..685fa192c1 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -152,6 +152,7 @@ if(GRAPHICS_API STREQUAL "D3D12") endif() if(GRAPHICS_API STREQUAL "Vulkan") + target_link_libraries(UnitTests PRIVATE ShaderCompilerInternal spirv-cross-core glslang) # Vulkan packaging paths (e.g. the >16 sampler rejection) are gated on this define. # ExternalTexture / native-texture helpers are not implemented for Vulkan yet, so skip # those tests rather than failing at link or runtime. diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js index a5d9cda5bb..342b1910f7 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js +++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js @@ -1,6 +1,105 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ +/***/ "./src/tests.nativeEngine.attributeLessInstancing.ts" +/*!***********************************************************!*\ + !*** ./src/tests.nativeEngine.attributeLessInstancing.ts ***! + \***********************************************************/ +(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ registerAttributeLessInstancingTests: () => (/* binding */ registerAttributeLessInstancingTests) +/* 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 registerAttributeLessInstancingTests( +describe, +it, +enabled) +{ + describe("NativeEngine attribute-less instancing", function () { + (enabled ? it : it.skip)("draws every instance without an instance buffer", /*#__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, material, target, _i, _arr, unIndexed, mesh, _loop, _i2, _arr2;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0: + this.timeout(15000); + engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.NativeEngine(); + scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Scene(engine);_context2.prev = 1; + + scene.clearColor = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Color4(0, 0, 0, 1); + camera = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.FreeCamera("camera", new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3(0, 0, -5), scene); + camera.setTarget(_babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.Vector3.Zero()); + material = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.ShaderMaterial("instances", scene, { + vertexSource: "\n precision highp float;\n attribute vec3 position;\n varying vec3 instanceColor;\n void main() {\n float offset = (float(gl_InstanceID) - 1.0) * 0.6;\n gl_Position = vec4(position.x * 0.2 + offset, position.y * 0.2, 0.5, 1.0);\n instanceColor = vec3(float(gl_InstanceID == 0), float(gl_InstanceID == 1), float(gl_InstanceID == 2));\n }", + + + + + + + + + fragmentSource: "\n precision highp float;\n varying vec3 instanceColor;\n void main() { gl_FragColor = vec4(instanceColor, 1.0); }" + + + + }, { attributes: ["position"], uniforms: [] }); + material.backFaceCulling = false; + target = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.RenderTargetTexture("instances", 64, scene, false);_i = 0, _arr = + [false, true];case 2:if (!(_i < _arr.length)) {_context2.next = 7;break;}unIndexed = _arr[_i]; + mesh = _babylonjs_core__WEBPACK_IMPORTED_MODULE_3__.MeshBuilder.CreatePlane("plane", { size: 2 }, scene); + if (unIndexed) { + mesh.convertToUnIndexedMesh(); + } + mesh.material = material; + target.renderList = [mesh];_loop = /*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _loop() {var count, readinessDeadline, pixels, expected, centers, actual;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0: + count = _arr2[_i2]; + mesh.forcedInstanceCount = count;_context.next = 1;return ( + material.forceCompilationAsync(mesh, { useInstances: count > 0 }));case 1:_context.next = 2;return ( + scene.whenReadyAsync());case 2: + readinessDeadline = Date.now() + 10000;case 3:if ( + target.isReadyForRendering()) {_context.next = 6;break;}if (!( + Date.now() >= readinessDeadline)) {_context.next = 4;break;}throw ( + new Error("Render target did not become ready: unIndexed=".concat(unIndexed, ", count=").concat(count)));case 4:_context.next = 5;return ( + + new Promise(function (resolve) {return setTimeout(resolve, 10);}));case 5:_context.next = 3;break;case 6: + + target.render();_context.next = 7;return ( + target.readPixels());case 7:pixels = _context.sent;if ( + pixels instanceof Uint8Array) {_context.next = 8;break;}throw ( + new Error("Expected RGBA8 instance rendering readback"));case 8: + + expected = [ + [255, 0, 0, 255], + count === 3 ? [0, 255, 0, 255] : [0, 0, 0, 255], + count === 3 ? [0, 0, 255, 255] : [0, 0, 0, 255]]; + + centers = [13, 32, 51]; + actual = centers.map(function (center) { + var offset = (32 * 64 + center) * 4; + return Array.from(pixels.subarray(offset, offset + 4)); + }); + (0,chai__WEBPACK_IMPORTED_MODULE_2__.expect)(actual, "unIndexed=".concat(unIndexed, ", count=").concat(count)).to.deep.equal(expected);case 9:case "end":return _context.stop();}}, _loop);});_i2 = 0, _arr2 = [0, 1, 3];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: + + mesh.dispose();case 6:_i++;_context2.next = 2;break;case 7:_context2.prev = 7; + + + scene.dispose(); + engine.dispose();return _context2.finish(7);case 8:case "end":return _context2.stop();}}, _callee, this, [[1,, 7, 8]]);})) + + ); + }); +} + +/***/ }, + /***/ "./src/tests.nativeEngine.png.ts" /*!***************************************!*\ !*** ./src/tests.nativeEngine.png.ts ***! @@ -28521,6 +28620,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @babylonjs/materials */ "@babylonjs/core"); /* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_6__); /* 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_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_8__ = __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;} @@ -28703,6 +28803,7 @@ describe("Native texture readback", function () { ); }); +(0,_tests_nativeEngine_attributeLessInstancing__WEBPACK_IMPORTED_MODULE_8__.registerAttributeLessInstancingTests)(describe, it, hasAttributeLessInstancing); describe("RequestFile", function () { this.timeout(0); diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts index d861bf035d..fe267ba3eb 100644 --- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts +++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts @@ -24,6 +24,7 @@ import { } from "@babylonjs/core"; import { GradientMaterial } from "@babylonjs/materials"; import { registerPngTests } from "./tests.nativeEngine.png"; +import { registerAttributeLessInstancingTests } from "./tests.nativeEngine.attributeLessInstancing"; declare var describe: typeof Mocha.describe; declare var it: typeof Mocha.it; @@ -35,12 +36,14 @@ Mocha.reporter("spec"); declare const hostPlatform: string; declare const hasGpuRendering: boolean; declare const hasNativeImageLoading: boolean; +declare const hasAttributeLessInstancing: boolean; declare const setExitCode: (code: number) => void; declare const setImageReloadTestResponse: (bytes: Uint8Array) => void; declare const skipCanvasGpuTests: boolean; declare const _native: any; registerPngTests(describe, it, hasGpuRendering && hasNativeImageLoading); +registerAttributeLessInstancingTests(describe, it, hasAttributeLessInstancing); describe("Native texture readback", function () { this.timeout(10000); diff --git a/Apps/UnitTests/JavaScript/src/tests.nativeEngine.attributeLessInstancing.ts b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.attributeLessInstancing.ts new file mode 100644 index 0000000000..d2ae4bcd72 --- /dev/null +++ b/Apps/UnitTests/JavaScript/src/tests.nativeEngine.attributeLessInstancing.ts @@ -0,0 +1,82 @@ +import * as Mocha from "mocha"; +import { expect } from "chai"; +import { + Color4, FreeCamera, MeshBuilder, NativeEngine, RenderTargetTexture, + Scene, ShaderMaterial, Vector3 +} from "@babylonjs/core"; + +export function registerAttributeLessInstancingTests( + describe: typeof Mocha.describe, + it: typeof Mocha.it, + enabled: boolean +): void { + describe("NativeEngine attribute-less instancing", function () { + (enabled ? it : it.skip)("draws every instance without an instance buffer", async function () { + this.timeout(15000); + const engine = new NativeEngine(); + const scene = new Scene(engine); + try { + scene.clearColor = new Color4(0, 0, 0, 1); + const camera = new FreeCamera("camera", new Vector3(0, 0, -5), scene); + camera.setTarget(Vector3.Zero()); + const material = new ShaderMaterial("instances", scene, { + vertexSource: ` + precision highp float; + attribute vec3 position; + varying vec3 instanceColor; + void main() { + float offset = (float(gl_InstanceID) - 1.0) * 0.6; + gl_Position = vec4(position.x * 0.2 + offset, position.y * 0.2, 0.5, 1.0); + instanceColor = vec3(float(gl_InstanceID == 0), float(gl_InstanceID == 1), float(gl_InstanceID == 2)); + }`, + fragmentSource: ` + precision highp float; + varying vec3 instanceColor; + void main() { gl_FragColor = vec4(instanceColor, 1.0); }` + }, { attributes: ["position"], uniforms: [] }); + material.backFaceCulling = false; + const target = new RenderTargetTexture("instances", 64, scene, false); + for (const unIndexed of [false, true]) { + const mesh = MeshBuilder.CreatePlane("plane", { size: 2 }, scene); + if (unIndexed) { + mesh.convertToUnIndexedMesh(); + } + mesh.material = material; + target.renderList = [mesh]; + for (const count of [0, 1, 3]) { + mesh.forcedInstanceCount = count; + await material.forceCompilationAsync(mesh, { useInstances: count > 0 }); + await scene.whenReadyAsync(); + const readinessDeadline = Date.now() + 10000; + while (!target.isReadyForRendering()) { + if (Date.now() >= readinessDeadline) { + throw new Error(`Render target did not become ready: unIndexed=${unIndexed}, count=${count}`); + } + await new Promise(resolve => setTimeout(resolve, 10)); + } + target.render(); + const pixels = await target.readPixels(); + if (!(pixels instanceof Uint8Array)) { + throw new Error("Expected RGBA8 instance rendering readback"); + } + const expected = [ + [255, 0, 0, 255], + count === 3 ? [0, 255, 0, 255] : [0, 0, 0, 255], + count === 3 ? [0, 0, 255, 255] : [0, 0, 0, 255] + ]; + const centers = [13, 32, 51]; + const actual = centers.map(center => { + const offset = (32 * 64 + center) * 4; + return Array.from(pixels.subarray(offset, offset + 4)); + }); + expect(actual, `unIndexed=${unIndexed}, count=${count}`).to.deep.equal(expected); + } + mesh.dispose(); + } + } finally { + scene.dispose(); + engine.dispose(); + } + }); + }); +} diff --git a/Apps/UnitTests/Source/Tests.JavaScript.cpp b/Apps/UnitTests/Source/Tests.JavaScript.cpp index 2c48f6d36a..b413bba6a2 100644 --- a/Apps/UnitTests/Source/Tests.JavaScript.cpp +++ b/Apps/UnitTests/Source/Tests.JavaScript.cpp @@ -153,6 +153,11 @@ TEST(JavaScript, All) #else env.Global().Set("hasNativeImageLoading", false); #endif +#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS) || defined(TEST_INSTALLED_SDK) + env.Global().Set("hasAttributeLessInstancing", false); +#else + env.Global().Set("hasAttributeLessInstancing", true); +#endif #ifdef USE_NOOP_METAL_DEVICE env.Global().Set("hasGpuRendering", false); #else diff --git a/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp b/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp index f65978ea2c..dcc237fff4 100644 --- a/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp +++ b/Apps/UnitTests/Source/Tests.ShaderCompilation.cpp @@ -16,6 +16,13 @@ #include #include +#if defined(BABYLON_NATIVE_GRAPHICS_API_VULKAN) +#include +#include +#include +#include +#endif + using namespace std::chrono_literals; extern Babylon::Graphics::Configuration g_deviceConfig; @@ -319,6 +326,25 @@ TEST(ShaderCompilation, PartiallySharedStageSamplersCompile) #if defined(BABYLON_NATIVE_GRAPHICS_API_VULKAN) namespace { + spirv_cross::Compiler ReadVulkanShader(const std::vector& bytes) + { + // These fixtures have no uniforms: bgfx v12's code length starts at byte 22. + constexpr size_t codeOffset = 26; + if (bytes.size() < codeOffset || bytes[3] != 12 || bytes[20] != 0 || bytes[21] != 0) + { + throw std::runtime_error{"Expected a uniform-free bgfx v12 shader"}; + } + uint32_t codeSize{}; + std::memcpy(&codeSize, bytes.data() + codeOffset - sizeof(codeSize), sizeof(codeSize)); + if (codeSize % sizeof(uint32_t) != 0 || codeSize > bytes.size() - codeOffset) + { + throw std::runtime_error{"Invalid SPIR-V byte length"}; + } + std::vector words(codeSize / sizeof(uint32_t)); + std::memcpy(words.data(), bytes.data() + codeOffset, codeSize); + return spirv_cross::Compiler{std::move(words)}; + } + struct SamplerCompilationResult { bool IsReady; @@ -421,4 +447,196 @@ TEST(ShaderCompilation, VulkanRejectsMoreThanSixteenSamplers) EXPECT_FALSE(result.IsReady); EXPECT_NE(result.Error.find("Vulkan shader uses more than 16 distinct sampler textures"), std::string::npos) << result.Error; } + +TEST(ShaderCompilation, VulkanAssignsMatchingVaryingLocations) +{ + Babylon::Plugins::ShaderCompiler compiler; + for (unsigned explicitStages = 0; explicitStages < 4; ++explicitStages) + { + SCOPED_TRACE(explicitStages); + const std::string vertexLocation = (explicitStages & 1) ? "layout(location = 3) " : ""; + const std::string fragmentLocation = (explicitStages & 2) ? "layout(location = 3) " : ""; + const auto info = compiler.Compile( + "#version 310 es\nprecision highp float;\nin vec3 position;\n" + + vertexLocation + R"(out vec3 colorValue; + out vec2 extraValue; + void main() { + gl_Position = vec4(position, 1.0); + colorValue = position; + extraValue = position.xy; + })", + "#version 310 es\nprecision highp float;\nin vec2 extraValue;\n" + + fragmentLocation + R"(in vec3 colorValue; + layout(location = 0) out vec4 color; + void main() { color = vec4(colorValue.xy + extraValue, colorValue.z, 1.0); })"); + auto vertex = ReadVulkanShader(info.VertexBytes); + auto fragment = ReadVulkanShader(info.FragmentBytes); + const auto outputs = vertex.get_shader_resources().stage_outputs; + const auto inputs = fragment.get_shader_resources().stage_inputs; + ASSERT_EQ(outputs.size(), 2u); + ASSERT_EQ(inputs.size(), 2u); + EXPECT_NE(vertex.get_decoration(outputs[0].id, spv::DecorationLocation), + vertex.get_decoration(outputs[1].id, spv::DecorationLocation)); + for (const auto& output : outputs) + { + ASSERT_TRUE(vertex.has_decoration(output.id, spv::DecorationLocation)) << output.name; + bool matched{}; + for (const auto& input : inputs) + { + if (input.name == output.name) + { + ASSERT_TRUE(fragment.has_decoration(input.id, spv::DecorationLocation)) << input.name; + EXPECT_EQ(vertex.get_decoration(output.id, spv::DecorationLocation), + fragment.get_decoration(input.id, spv::DecorationLocation)); + if (explicitStages != 0 && output.name == "colorValue") + { + EXPECT_EQ(vertex.get_decoration(output.id, spv::DecorationLocation), 3u); + } + matched = true; + } + } + EXPECT_TRUE(matched) << output.name; + } + } +} + +TEST(ShaderCompilation, VulkanReservesMatrixArrayLocations) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto info = compiler.Compile( + R"(#version 310 es + precision highp float; + in vec3 position; + out mat2 matrixValue[2]; + out vec3 colorValue; + out vec2 vertexOnly; + void main() { + gl_Position = vec4(position, 1.0); + matrixValue[0] = mat2(1.0); + matrixValue[1] = mat2(2.0); + colorValue = position; + vertexOnly = position.xy; + })", + R"(#version 310 es + precision highp float; + layout(location = 3) in vec3 colorValue; + in mat2 matrixValue[2]; + out vec4 color; + void main() { color = vec4(matrixValue[0] * matrixValue[1] * colorValue.xy, colorValue.z, 1.0); })"); + auto vertex = ReadVulkanShader(info.VertexBytes); + auto fragment = ReadVulkanShader(info.FragmentBytes); + for (const auto* stage : {&vertex, &fragment}) + { + const auto resources = stage->get_shader_resources(); + const auto& varyings = stage == &vertex ? resources.stage_outputs : resources.stage_inputs; + ASSERT_EQ(varyings.size(), stage == &vertex ? 3u : 2u); + for (const auto& varying : varyings) + { + ASSERT_TRUE(stage->has_decoration(varying.id, spv::DecorationLocation)); + const auto location = stage->get_decoration(varying.id, spv::DecorationLocation); + if (varying.name == "matrixValue") + { + EXPECT_EQ(location, 4u); + } + else if (varying.name == "colorValue") + { + EXPECT_EQ(location, 3u); + } + else + { + EXPECT_EQ(varying.name, "vertexOnly"); + EXPECT_EQ(location, 0u); + } + } + } + const auto outputs = fragment.get_shader_resources().stage_outputs; + ASSERT_EQ(outputs.size(), 1u); + EXPECT_TRUE(fragment.has_decoration(outputs[0].id, spv::DecorationLocation)); + EXPECT_EQ(fragment.get_decoration(outputs[0].id, spv::DecorationLocation), 0u); + const auto inputs = vertex.get_shader_resources().stage_inputs; + ASSERT_EQ(inputs.size(), 1u); + EXPECT_EQ(vertex.get_decoration(inputs[0].id, spv::DecorationLocation), 0u); +} + +TEST(ShaderCompilation, VulkanLinksBlocksWithDifferentInstanceNames) +{ +#if defined(GLSLANG_WEB) + GTEST_SKIP() << "Interface blocks require BABYLON_NATIVE_DISABLE_WEBMIN"; +#else + Babylon::Plugins::ShaderCompiler compiler; + for (const std::string fragmentLocation : {"", "layout(location = 4) "}) + { + SCOPED_TRACE(fragmentLocation); + const auto info = compiler.Compile( + R"(#version 310 es + #extension GL_EXT_shader_io_blocks : require + precision highp float; + in vec3 position; + out Payload { mat2 transform; vec3 tint; } vertexData; + out vec2 extraValue; + void main() { + gl_Position = vec4(position, 1.0); + vertexData.transform = mat2(1.0); + vertexData.tint = position; + extraValue = position.xy; + })", + "#version 310 es\n#extension GL_EXT_shader_io_blocks : require\n" + "precision highp float;\nin vec2 extraValue;\n" + + fragmentLocation + R"(in Payload { mat2 transform; vec3 tint; } fragmentData; + layout(location = 0) out vec4 color; + void main() { color = vec4(fragmentData.transform * extraValue, fragmentData.tint.z, 1.0); })"); + auto vertex = ReadVulkanShader(info.VertexBytes); + auto fragment = ReadVulkanShader(info.FragmentBytes); + std::map vertexLocations; + std::map fragmentLocations; + for (const auto* stage : {&vertex, &fragment}) + { + const auto resources = stage->get_shader_resources(); + const auto& varyings = stage == &vertex ? resources.stage_outputs : resources.stage_inputs; + ASSERT_EQ(varyings.size(), 2u); + auto& locations = stage == &vertex ? vertexLocations : fragmentLocations; + for (const auto& varying : varyings) + { + ASSERT_TRUE(stage->has_decoration(varying.id, spv::DecorationLocation)); + locations[varying.name] = stage->get_decoration(varying.id, spv::DecorationLocation); + } + } + ASSERT_EQ(vertexLocations.count("Payload"), 1u); + ASSERT_EQ(vertexLocations.count("extraValue"), 1u); + EXPECT_EQ(vertexLocations, fragmentLocations); + const auto blockLocation = vertexLocations.at("Payload"); + const auto extraLocation = vertexLocations.at("extraValue"); + EXPECT_TRUE(extraLocation < blockLocation || extraLocation >= blockLocation + 3); + if (!fragmentLocation.empty()) + { + EXPECT_EQ(blockLocation, 4u); + } + } +#endif +} + +TEST(ShaderCompilation, VulkanUsesVertexAndInstanceIndexBuiltins) +{ + Babylon::Plugins::ShaderCompiler compiler; + const auto info = compiler.Compile( + R"(#version 310 es + precision highp float; + void main() { gl_Position = vec4(float(gl_VertexID), float(gl_InstanceID), 0.0, 1.0); })", + R"(#version 310 es + precision highp float; + layout(location = 0) out vec4 color; + void main() { color = vec4(1.0); })"); + auto vertex = ReadVulkanShader(info.VertexBytes); + bool vertexIndex{}; + bool instanceIndex{}; + for (const auto& input : vertex.get_shader_resources().builtin_inputs) + { + EXPECT_NE(input.builtin, spv::BuiltInVertexId); + EXPECT_NE(input.builtin, spv::BuiltInInstanceId); + vertexIndex |= input.builtin == spv::BuiltInVertexIndex; + instanceIndex |= input.builtin == spv::BuiltInInstanceIndex; + } + EXPECT_TRUE(vertexIndex); + EXPECT_TRUE(instanceIndex); +} #endif diff --git a/Install/Test/CMakeLists.txt b/Install/Test/CMakeLists.txt index c0bd87ee91..8a57a6b334 100644 --- a/Install/Test/CMakeLists.txt +++ b/Install/Test/CMakeLists.txt @@ -65,6 +65,7 @@ else() endif() add_executable(TestInstall ${SOURCES}) +target_compile_definitions(TestInstall PRIVATE TEST_INSTALLED_SDK) message(STATUS "Install include directory: ${INSTALL_DIR}/include") target_include_directories(TestInstall PRIVATE "${INSTALL_DIR}/include") diff --git a/Plugins/NativeEngine/Source/VertexArray.cpp b/Plugins/NativeEngine/Source/VertexArray.cpp index 92b5c5e89c..27430f6b05 100644 --- a/Plugins/NativeEngine/Source/VertexArray.cpp +++ b/Plugins/NativeEngine/Source/VertexArray.cpp @@ -140,6 +140,11 @@ namespace Babylon VertexBuffer::BuildInstanceDataBuffer(instanceDataBuffer, m_vertexBufferInstances, instanceCount, instanceDataLayout); encoder->setInstanceDataBuffer(&instanceDataBuffer); } + else if (instanceCount > 0) + { + // These draws derive per-instance data from gl_InstanceID instead of an instance buffer. + encoder->setInstanceCount(instanceCount); + } uint8_t stream = 0; for (const auto& pair : m_vertexBufferRecords) diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp index 4f519224c0..c925f2a950 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp @@ -35,6 +35,102 @@ namespace // bgfx Vulkan m_bindInfo is indexed by sampler stage 0..15. constexpr unsigned kMaxBgfxTextureStages = 16; + class VulkanInterfaceLocationTraverser final : public TIntermTraverser + { + public: + void Collect(TIntermediate& intermediate, TStorageQualifier storage) + { + m_storage = storage; + intermediate.getTreeRoot()->traverse(this); + } + + void AssignLocations() + { + std::map locations; + std::set reserved; + for (const auto& [name, symbols] : m_symbols) + { + for (const auto* symbol : symbols) + { + const auto& qualifier = symbol->getQualifier(); + if (qualifier.hasLocation()) + { + locations[name] = qualifier.layoutLocation; + const auto count = TIntermediate::computeTypeLocationSize(symbol->getType(), EShLangFragment); + for (int offset = 0; offset < count; ++offset) + { + reserved.insert(qualifier.layoutLocation + offset); + } + } + } + } + for (const auto& [name, symbols] : m_symbols) + { + if (locations.find(name) == locations.end()) + { + const auto count = TIntermediate::computeTypeLocationSize(symbols.front()->getType(), EShLangFragment); + unsigned location{}; + for (int offset = 0; offset < count; ++offset) + { + if (reserved.count(location + offset)) + { + location += offset + 1; + offset = -1; + } + } + locations[name] = location; + for (int offset = 0; offset < count; ++offset) + { + reserved.insert(location + offset); + } + } + for (auto* symbol : symbols) + { + symbol->getWritableType().getQualifier().layoutLocation = locations.at(name); + } + } + } + + private: + void visitSymbol(TIntermSymbol* symbol) override + { + const auto& type = symbol->getType(); + if (type.getQualifier().storage != m_storage || type.isBuiltIn()) + { + return; + } + if (type.isStruct() && !type.getStruct()->empty() && type.getStruct()->front().type->isBuiltIn()) + { + return; + } + // Block instance names are stage-local; the block type identifies the interface. + const auto& name = type.getBasicType() == EbtBlock ? type.getTypeName() : symbol->getName(); + m_symbols[name.c_str()].push_back(symbol); + } + + TStorageQualifier m_storage{}; + std::map> m_symbols; + }; + + class VulkanBuiltInTraverser final : public TIntermTraverser + { + void visitSymbol(TIntermSymbol* symbol) override + { + auto& qualifier = symbol->getWritableType().getQualifier(); + switch (qualifier.builtIn) + { + case EbvVertexId: + qualifier.builtIn = EbvVertexIndex; + break; + case EbvInstanceId: + qualifier.builtIn = EbvInstanceIndex; + break; + default: + break; + } + } + }; + void CollectStageUniforms( glslang::TIntermediate* intermediate, std::vector& textures, @@ -277,6 +373,20 @@ namespace Babylon::Plugins throw std::runtime_error{program.getInfoLog()}; } + // The GLSLANG_WEB build omits mapIO. Match stage interfaces by name and + // reserve explicit locations before allocating implicit ones. + VulkanInterfaceLocationTraverser varyings; + varyings.Collect(*program.getIntermediate(EShLangVertex), EvqVaryingOut); + varyings.Collect(*program.getIntermediate(EShLangFragment), EvqVaryingIn); + varyings.AssignLocations(); + VulkanInterfaceLocationTraverser outputs; + outputs.Collect(*program.getIntermediate(EShLangFragment), EvqVaryingOut); + outputs.AssignLocations(); + + // Parse Babylon's ordinary GLSL, but emit Vulkan's index built-ins. + VulkanBuiltInTraverser builtIns; + program.getIntermediate(EShLangVertex)->getTreeRoot()->traverse(&builtIns); + ShaderCompilerTraversers::IdGenerator ids{}; // Flip 2D texture sample coordinates (replaces the former ProcessSamplerFlip texture() macro). ShaderCompilerTraversers::FlipSamplerCoordinates(program);