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);