diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index 0ec5cd4289..394fbdbaa2 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -46,6 +46,7 @@ set(SOURCES "Source/Tests.NativeEngine.DepthTextures.cpp" "Source/Tests.NativeEngine.Teardown.cpp" "Source/Tests.NativeEngine.TextureFormats.cpp" + "Source/Tests.NativeOptimizations.cpp" "Source/Tests.ShaderCache.cpp" "Source/Tests.ShaderCompilation.cpp" "Source/Tests.ShaderCompilation.FragCoord.cpp" @@ -136,6 +137,11 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT) target_compile_definitions(UnitTests PRIVATE HAS_NATIVE_MESHOPT) endif() +if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) + target_link_libraries(UnitTests PRIVATE NativeOptimizations) + target_compile_definitions(UnitTests PRIVATE HAS_NATIVE_OPTIMIZATIONS) +endif() + if(GRAPHICS_API STREQUAL "D3D12") target_compile_definitions(UnitTests PRIVATE SKIP_RENDER_TESTS BABYLON_NATIVE_GRAPHICS_API_D3D12) endif() diff --git a/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp b/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp new file mode 100644 index 0000000000..a432c8857a --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeOptimizations.cpp @@ -0,0 +1,106 @@ +#include +#ifdef HAS_NATIVE_OPTIMIZATIONS +#include +#include +#include + +#include +#include +#include +#include +#endif + +TEST(NativeOptimizations, SplatSortingAcceptsTypedAndNumberArrayMatrices) +{ +#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 positions = new Float32Array([ + 1, 0, 0, 1, + 0, 1, 0, 1, + 0, 0, 1, 1, + 1, 1, 1, 1, + -1, 0, -1, 1 + ]); + for (const typed of [true, false]) { + const matrix = typed ? new Float32Array(16) : new Array(16).fill(0); + matrix[2] = 2; + matrix[6] = -3; + matrix[10] = 5; + const modelView = { _m: matrix }; + for (const rightHanded of [false, true]) { + const indices = new Float32Array(5); + _native.sortSplats(modelView, positions, indices, rightHanded); + // Distinct depths [2, -3, 5, 4, -7] give an algorithm-independent ordering. + const expected = rightHanded ? [4, 1, 0, 3, 2] : [2, 3, 0, 1, 4]; + for (let index = 0; index < indices.length; ++index) { + if (indices[index] !== expected[index]) { + throw new Error("Unexpected splat order for typed=" + typed + + ", rightHanded=" + rightHanded + ", index=" + index); + } + } + const single = new Float32Array([99]); + _native.sortSplats(modelView, positions.subarray(0, 4), single, rightHanded); + if (single[0] !== 0) { + throw new Error("Unexpected single-splat index"); + } + _native.sortSplats(modelView, new Float32Array(0), new Float32Array(0), rightHanded); + } + } + function expectMatrixError(matrix, message) { + let error; + try { + _native.sortSplats({ _m: matrix }, positions, new Float32Array(5), false); + } catch (caught) { + error = caught; + } + // JSI wraps exceptions thrown from host functions with this prefix. + if (!(error instanceof Error) || + (error.message !== message && error.message !== "Exception in HostFunction: " + message)) { + throw new Error("Invalid matrix must report: " + message + + "; received: " + String(error)); + } + } + for (const matrix of [undefined, null, {}, 42, new Uint8Array(16), new Float64Array(16)]) { + expectMatrixError(matrix, "sortSplats requires modelView._m to be a Float32Array or Array."); + } + for (const component of [2, 6, 10]) { + const matrix = new Array(16).fill(0); + delete matrix[component]; + expectMatrixError(matrix, "sortSplats requires modelView._m[2], [6], [10] to be numbers."); + for (const value of [undefined, null, "2", false, {}]) { + matrix[component] = value; + expectMatrixError(matrix, "sortSplats requires modelView._m[2], [6], [10] to be numbers."); + } + } + })(); + )", "native-splat-matrix-storage.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) + { + // AppRuntime teardown joins the worker; returning would hang if that worker is stuck. + ADD_FAILURE() << "Timed out waiting for NativeOptimizations matrix storage regression"; + std::quick_exit(1); + } + EXPECT_NO_THROW(completion.get()); +#endif +} diff --git a/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp b/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp index 69d4bdc5de..97b9f15279 100644 --- a/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp +++ b/Plugins/NativeOptimizations/Source/NativeOptimizations.cpp @@ -291,7 +291,13 @@ namespace void sortSplats(const Napi::CallbackInfo& info) { const auto modelView{ info[0].As() }; - const auto m{ modelView.Get("_m").As() }; + const auto matrixStorage{ modelView.Get("_m") }; + if (!matrixStorage.IsArray() && + !(matrixStorage.IsTypedArray() && matrixStorage.As().TypedArrayType() == napi_float32_array)) + { + throw Napi::TypeError::New(info.Env(), "sortSplats requires modelView._m to be a Float32Array or Array."); + } + const auto m{ matrixStorage.As() }; auto positions{ info[1].As() }; @@ -306,7 +312,19 @@ namespace } const auto splatCount = indices.ElementLength(); - float vp[3] = { m[2u], m[6u], m[10u] }; + // High-precision Babylon matrices use ordinary number arrays. + const auto m2{m.Get(2u)}; + const auto m6{m.Get(6u)}; + const auto m10{m.Get(10u)}; + if (!m2.IsNumber() || !m6.IsNumber() || !m10.IsNumber()) + { + throw Napi::TypeError::New(info.Env(), "sortSplats requires modelView._m[2], [6], [10] to be numbers."); + } + float vp[3] = { + m2.As().FloatValue(), + m6.As().FloatValue(), + m10.As().FloatValue() + }; static std::vector depthMix; depthMix.resize(splatCount * 2);