From 283bcb54798f374c7278f14f5e2e94634e188a7c Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 11 Jun 2026 07:54:33 -0700 Subject: [PATCH 1/2] NativeEngine: implement updateTextureData (re-enables Test updateTextureData) updateTextureData previously threw "not implemented" on Native. Implement it so sub-rectangle texture updates work. - Add NativeEngine::UpdateTextureData: upload the requested sub-rectangle via bgfx::updateTexture2D (Texture::Update2D). Validates the JS-controlled rect against the mip extents, sizes the copy with bgfx::calcTextureSize (no bimg dependency, so it also works in no-image-loading builds), and mirrors the vertical flip the base texture upload applies so the sub-rect lines up on top-left-origin backends (e.g. D3D11). - Re-enable the "Test updateTextureData" validation test. Pairs with the Babylon.js change (engine.name = "Native" so name-gated WebGL _gl access skips Native, plus the updateTextureData override). CI stays red until a babylonjs npm with that change is published and the dependency bumped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/Playground/Scripts/config.json | 2 - Plugins/NativeEngine/Source/NativeEngine.cpp | 72 ++++++++++++++++++++ Plugins/NativeEngine/Source/NativeEngine.h | 1 + 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 3b7389558..d79dd1f41 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2212,8 +2212,6 @@ { "title": "Test updateTextureData", "playgroundId": "#EVX1DH#80", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "testUpdateTextureData.png" }, { diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 864031710..651d3d425 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -714,6 +714,7 @@ namespace Babylon InstanceMethod("initializeTexture", &NativeEngine::InitializeTexture), InstanceMethod("loadTexture", &NativeEngine::LoadTexture), InstanceMethod("loadRawTexture", &NativeEngine::LoadRawTexture), + InstanceMethod("updateTextureData", &NativeEngine::UpdateTextureData), InstanceMethod("loadRawTexture2DArray", &NativeEngine::LoadRawTexture2DArray), InstanceMethod("loadCubeTexture", &NativeEngine::LoadCubeTexture), InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips), @@ -1441,6 +1442,77 @@ namespace Babylon #endif } + void NativeEngine::UpdateTextureData(const Napi::CallbackInfo& info) + { + const auto texture{info[0].As>().Get()}; + const auto data{info[1].As()}; + const auto x{static_cast(info[2].As().Uint32Value())}; + const auto y{static_cast(info[3].As().Uint32Value())}; + const auto width{static_cast(info[4].As().Uint32Value())}; + const auto height{static_cast(info[5].As().Uint32Value())}; + const uint16_t layer{info.Length() > 6 && !info[6].IsUndefined() ? static_cast(info[6].As().Uint32Value()) : static_cast(0)}; + const uint8_t mip{info.Length() > 7 && !info[7].IsUndefined() ? static_cast(info[7].As().Uint32Value()) : static_cast(0)}; + const bool invertY{info.Length() > 8 && !info[8].IsUndefined() ? info[8].As().Value() : false}; + + if (texture == nullptr || !texture->IsValid()) + { + throw Napi::Error::New(info.Env(), "updateTextureData called on an invalid texture"); + } + + // Validate the (JS-controlled) update rectangle against the mip-level extents before handing it to + // bgfx, so an out-of-range origin/size can't drive an out-of-bounds read of the source buffer below. + uint32_t mipWidth{static_cast(texture->Width()) >> mip}; + uint32_t mipHeight{static_cast(texture->Height()) >> mip}; + if (mipWidth == 0) + { + mipWidth = 1; + } + if (mipHeight == 0) + { + mipHeight = 1; + } + const uint16_t numLayers{texture->NumLayers() > 0 ? texture->NumLayers() : static_cast(1)}; + if (width == 0 || height == 0 || + static_cast(x) + width > mipWidth || + static_cast(y) + height > mipHeight || + layer >= numLayers) + { + throw Napi::Error::New(info.Env(), "updateTextureData region is out of bounds"); + } + + // Size of the source rectangle in the texture's own format. bgfx is always linked (bimg is not, in + // builds without image loading), so size the upload with bgfx::calcTextureSize rather than bimg. + 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) + { + throw Napi::Error::New(info.Env(), "updateTextureData data size does not match width, height, and texture format"); + } + + const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; + + // Match the vertical orientation the base upload applies (PrepareImage flips the whole image when + // originBottomLeft ? invertY : !invertY). To land a sub-rectangle at the same place, flip it to the + // mirrored Y origin and reverse its rows so row 0 of the source lines up with the flipped base data. + const bool flip{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) + { + std::memcpy(mem->data + static_cast(row) * rowBytes, bytes + static_cast(height - 1 - row) * rowBytes, rowBytes); + } + } + else + { + std::memcpy(mem->data, bytes, requiredSize); + } + texture->Update2D(layer, mip, x, targetY, width, height, mem); + } + void NativeEngine::LoadRawTexture2DArray(const Napi::CallbackInfo& info) { #ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index 229ef0e3a..3fad975fa 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -101,6 +101,7 @@ namespace Babylon void LoadTexture(const Napi::CallbackInfo& info); void CopyTexture(NativeDataStream::Reader& data); void LoadRawTexture(const Napi::CallbackInfo& info); + void UpdateTextureData(const Napi::CallbackInfo& info); void LoadRawTexture2DArray(const Napi::CallbackInfo& info); void LoadCubeTexture(const Napi::CallbackInfo& info); void LoadCubeTextureWithMips(const Napi::CallbackInfo& info); From 82a45bd9e9dfc1c80722da9356aade071c55591c Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Mon, 6 Jul 2026 08:15:05 -0700 Subject: [PATCH 2/2] Bump babylonjs to 9.15.0 to pick up merged native-engine fixes The re-enabled validation tests depend on native-engine fixes that landed in babylonjs 9.14.0+ (depthCullingState.depthTest, cube render targets, updateTextureData / engine.name). The Apps lockfile was pinned to 9.9.1, so 'npm ci' installed an engine without those fixes and the tests crashed on the old WebGL-only paths. Bump to 9.15.0 (protocol-compatible: PROTOCOL_VERSION 9). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/package-lock.json | 70 +++++++++++++++++++++--------------------- Apps/package.json | 14 ++++----- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Apps/package-lock.json b/Apps/package-lock.json index 2156eb656..d8adf7009 100644 --- a/Apps/package-lock.json +++ b/Apps/package-lock.json @@ -11,13 +11,13 @@ "UnitTests/JavaScript" ], "dependencies": { - "babylonjs": "^9.3.4", - "babylonjs-addons": "^9.3.4", - "babylonjs-gltf2interface": "^9.3.4", - "babylonjs-gui": "^9.3.4", - "babylonjs-loaders": "^9.3.4", - "babylonjs-materials": "^9.3.4", - "babylonjs-serializers": "^9.3.4", + "babylonjs": "^9.15.0", + "babylonjs-addons": "^9.15.0", + "babylonjs-gltf2interface": "^9.15.0", + "babylonjs-gui": "^9.15.0", + "babylonjs-loaders": "^9.15.0", + "babylonjs-materials": "^9.15.0", + "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" } @@ -2596,63 +2596,63 @@ } }, "node_modules/babylonjs": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.9.1.tgz", - "integrity": "sha512-zMH8r0l/il9PJhy2+uS+/EyeLBy/ElZg0lKLj2d+ZrUarOanjMISaACw0HUx7C1EgYoXUFSMQLYcpXguleh8GA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.15.0.tgz", + "integrity": "sha512-IJQhrxsxxj4KCg4aSsB5chLidzAfbghr8rnzo6sRLFin6ipfeayCxg7MevjaMzqdVmc/BOMU6sj49nSNrBq+yQ==", "hasInstallScript": true, "license": "Apache-2.0" }, "node_modules/babylonjs-addons": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-addons/-/babylonjs-addons-9.9.1.tgz", - "integrity": "sha512-Hi9QZqeanqG+4VAAmJmHJh7JKf1bc/Y5Ml+wuv+W+3f/dKltZpy/pii/QN4vhYRyrU0n+S8OJc+kVeaUUFjHEg==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-addons/-/babylonjs-addons-9.15.0.tgz", + "integrity": "sha512-7vxm0ffFXWHCZWDlYYleifb6mg1iSH7nNo+sA9706z7QIfpNnLS9+9cTYadWobvhURwaD/DHAKnm7Z8ckq9YAA==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-gltf2interface": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.9.1.tgz", - "integrity": "sha512-qHE7pASEWbRORU+NEPSSL9fyVh6gBNqC1ugV94YL9Dz0HnLcH8YNcxdi6Pf+oXgFHRrXWZiJATn37HynsF7/0g==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.15.0.tgz", + "integrity": "sha512-Yr/WvOvnsZFN2LoyNU/ZZbT+lXMdNA9OiZzE4RibI1tnYxQjpPKT6X1cxQ/ACJPVzKwudxzJxQ/f/Tdba4TLDg==", "license": "Apache-2.0" }, "node_modules/babylonjs-gui": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-9.9.1.tgz", - "integrity": "sha512-w6HSvuHSqLYUOuxvko+gkjAQMvatf7gnOPufLRslm+P4tC+5KMEd0hyC+1vHCJuQmZXXkoc/GnozzFeaqllHZw==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-9.15.0.tgz", + "integrity": "sha512-klF2ywhA040JMQ3Z5XaGkN4S0GIOshtnvtUd8kbYqlLYEu5i6/y/zvB8V3O4geslzV0kWcBe2mIthlr7M7p4og==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-loaders": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-9.9.1.tgz", - "integrity": "sha512-bFSe4TQTi7aA7RsWmuZHQLX4mBMoza5D+AFRD2tVrizGyGCfQJALKnKze7Z3OetFyeo72838GXJAXmyiOWcxAA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-9.15.0.tgz", + "integrity": "sha512-k5Kg9wmuy0n4ZAWu9woFk3C1yEwSvQHRv0Ct2GOQtuRvIHUIyRcW4KEKOsW0GOiApSjXh9nQcabdJTnUo8IBNw==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1", - "babylonjs-gltf2interface": "9.9.1" + "babylonjs": "9.15.0", + "babylonjs-gltf2interface": "9.15.0" } }, "node_modules/babylonjs-materials": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-9.9.1.tgz", - "integrity": "sha512-2ai5hk59dzHRUN28/5/Nijknn+IungSuoBsrHrNZE2T/yIT+/T/IrU0ebm53nRYliv6NG0IaxSCfnh6euZUj7A==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-9.15.0.tgz", + "integrity": "sha512-E9C5EB0nZJrGvT7Mb38gypAnzi1q8vr/Gi+fTeqRQilIU8mdk+YzD5unfKOROEv8arRFCXGWrtBqkIN8k2PFxA==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-serializers": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-serializers/-/babylonjs-serializers-9.9.1.tgz", - "integrity": "sha512-nZJPplzHprwJhp4JVLgB3dEpJR2HvEElPIDvOoAFf1IcaKwcDja2tZTGXIbARdWlTX547pN0qN/vEd6dYLrnDA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-serializers/-/babylonjs-serializers-9.15.0.tgz", + "integrity": "sha512-iLYwPzZrUr2SnwHcsXjHJHYdJIVC+boW6z/olE0mE5GvqmfKXTtqFr9wRGrsq85BarhHQkfVlfQdFwhFq/JG4w==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1", - "babylonjs-gltf2interface": "9.9.1" + "babylonjs": "9.15.0", + "babylonjs-gltf2interface": "9.15.0" } }, "node_modules/balanced-match": { diff --git a/Apps/package.json b/Apps/package.json index f6a7e31e7..632285ede 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -9,13 +9,13 @@ "getNightly": "node scripts/getNightly.js" }, "dependencies": { - "babylonjs": "^9.3.4", - "babylonjs-addons": "^9.3.4", - "babylonjs-gltf2interface": "^9.3.4", - "babylonjs-gui": "^9.3.4", - "babylonjs-loaders": "^9.3.4", - "babylonjs-materials": "^9.3.4", - "babylonjs-serializers": "^9.3.4", + "babylonjs": "^9.15.0", + "babylonjs-addons": "^9.15.0", + "babylonjs-gltf2interface": "^9.15.0", + "babylonjs-gui": "^9.15.0", + "babylonjs-loaders": "^9.15.0", + "babylonjs-materials": "^9.15.0", + "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" }