Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions Plugins/NativeEngine/Source/NativeEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ namespace Babylon
InstanceMethod("loadTexture", &NativeEngine::LoadTexture),
InstanceMethod("loadRawTexture", &NativeEngine::LoadRawTexture),
InstanceMethod("loadRawTexture2DArray", &NativeEngine::LoadRawTexture2DArray),
InstanceMethod("updateTextureDirectly", &NativeEngine::UpdateTextureDirectly),
InstanceMethod("loadCubeTexture", &NativeEngine::LoadCubeTexture),
InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips),
InstanceMethod("getTextureWidth", &NativeEngine::GetTextureWidth),
Expand Down Expand Up @@ -1520,6 +1521,141 @@ namespace Babylon
#endif
}

// Implements the shared JS texture-loader sink (Babylon's _uploadDataToTextureDirectly /
// _uploadCompressedDataToTextureDirectly), letting DDS/KTX/KTX2/Basis/IES/HDR/EXR/TGA load
// through the same loaders WebGL/WebGPU use. The loaders upload one (face, mip) at a time,
// WebGL texImage2D-style; bgfx instead needs the whole texture allocated before any update,
// so the texture is created lazily on the first upload.
void NativeEngine::UpdateTextureDirectly(const Napi::CallbackInfo& info)
{
#ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES
throw Napi::Error::New(info.Env(), "Image loading is disabled in this build (BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES=OFF).");
#else
const auto texture{info[0].As<Napi::Pointer<Graphics::Texture>>().Get()};
const auto data{info[1].As<Napi::TypedArray>()};
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
const auto faceIndexValue{info[2].As<Napi::Number>().Uint32Value()};
const auto lodValue{info[3].As<Napi::Number>().Uint32Value()};
const auto baseWidth{info[4].As<Napi::Number>().Uint32Value()};
const auto baseHeight{info[5].As<Napi::Number>().Uint32Value()};
const auto mipWidth{info[6].As<Napi::Number>().Uint32Value()};
const auto mipHeight{info[7].As<Napi::Number>().Uint32Value()};
const auto formatValue{info[8].As<Napi::Number>().Uint32Value()};
const auto isCube{info[9].As<Napi::Boolean>().Value()};
const auto hasMips{info[10].As<Napi::Boolean>().Value()};
const auto invertY{info[11].As<Napi::Boolean>().Value()};

// Validate the JS-provided dimensions against GPU limits before narrowing to uint16_t,
// so an out-of-range value can't wrap into an in-range one and drive an OOB read/upload.
const auto maxTextureSize = bgfx::getCaps()->limits.maxTextureSize;
if (baseWidth == 0 || baseHeight == 0 || mipWidth == 0 || mipHeight == 0 ||
baseWidth > maxTextureSize || baseHeight > maxTextureSize ||
mipWidth > maxTextureSize || mipHeight > maxTextureSize)
{
throw Napi::Error::New(Env(), "Invalid base or mip dimensions for the texture.");
}

// The format is a raw JS enum value; validate it before narrowing so an out-of-range value
// can't index past bimg/bgfx's format tables in Create*/imageGetSize (OOB read).
if (formatValue >= bimg::TextureFormat::Count)
{
throw Napi::Error::New(Env(), "Invalid texture format.");
}
const auto format{static_cast<bimg::TextureFormat::Enum>(formatValue)};

// Cube textures must be square and address one of the six faces; 2D textures have a single
// face. Validate the JS-provided face index before narrowing it to uint8_t.
if (isCube)
{
if (baseWidth != baseHeight || mipWidth != mipHeight)
{
throw Napi::Error::New(Env(), "Cube texture dimensions must be square.");
}
if (faceIndexValue >= 6)
{
throw Napi::Error::New(Env(), "Cube face index must be in the range [0, 5].");
}
}
else if (faceIndexValue != 0)
{
throw Napi::Error::New(Env(), "Face index must be 0 for a 2D texture.");
}

// The lod must address a real mip level: level 0 only when the texture has no mipmaps,
// otherwise within the mip chain implied by the base dimensions.
uint32_t numMips{1};
if (hasMips)
{
uint32_t levelDim{baseWidth > baseHeight ? baseWidth : baseHeight};
while (levelDim > 1)
{
levelDim >>= 1;
++numMips;
}
}
if (lodValue >= numMips)
{
throw Napi::Error::New(Env(), "Lod exceeds the texture's mip level count.");
}

// The mip dimensions must match the level implied by (base >> lod); otherwise bgfx would
// update a region that doesn't match the allocated mip level (asserts / UB on some backends).
const uint32_t shiftedWidth{baseWidth >> lodValue};
const uint32_t shiftedHeight{baseHeight >> lodValue};
const uint32_t expectedMipWidth{shiftedWidth > 1 ? shiftedWidth : 1};
const uint32_t expectedMipHeight{shiftedHeight > 1 ? shiftedHeight : 1};
if (mipWidth != expectedMipWidth || mipHeight != expectedMipHeight)
{
throw Napi::Error::New(Env(), "Mip dimensions do not match the base dimensions and lod.");
}

if (!texture->IsValid())
{
if (isCube)
{
texture->CreateCube(static_cast<uint16_t>(baseWidth), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE);
}
else
{
texture->Create2D(static_cast<uint16_t>(baseWidth), static_cast<uint16_t>(baseHeight), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE);
}
}

const uint64_t expectedSize{bimg::imageGetSize(nullptr, static_cast<uint16_t>(mipWidth), static_cast<uint16_t>(mipHeight), 1, false, false, 1, format)};
if (expectedSize == 0 || static_cast<uint64_t>(data.ByteLength()) != expectedSize)
{
throw Napi::Error::New(Env(), "The data size does not match mip dimensions and format.");
}

// bgfx::copy takes a 32-bit size; reject anything that would truncate.
if (expectedSize > 0xFFFFFFFFull)
{
throw Napi::Error::New(Env(), "Texture upload size exceeds the maximum supported size.");
}

const auto bytes{static_cast<uint8_t*>(data.ArrayBuffer().Data()) + data.ByteOffset()};
// bgfx must own the upload buffer (released asynchronously after the GPU consumes it).
const bgfx::Memory* mem{bgfx::copy(bytes, static_cast<uint32_t>(data.ByteLength()))};

// Match the existing loader flip conventions: cube faces flip only on origin-bottom-left
// (OpenGL), like LoadCubeTextureFromImages; 2D follows the raw/loadTexture convention.
// Compressed block data cannot be row-flipped.
const bool flip{isCube ? bgfx::getCaps()->originBottomLeft : (bgfx::getCaps()->originBottomLeft ? invertY : !invertY)};
if (flip && !bimg::isCompressed(format))
{
FlipImage({mem->data, mem->size}, static_cast<uint32_t>(mipHeight));
}

if (isCube)
{
texture->UpdateCube(0, static_cast<uint8_t>(faceIndexValue), static_cast<uint8_t>(lodValue), 0, 0, static_cast<uint16_t>(mipWidth), static_cast<uint16_t>(mipHeight), mem);
}
else
{
texture->Update2D(0, static_cast<uint8_t>(lodValue), 0, 0, static_cast<uint16_t>(mipWidth), static_cast<uint16_t>(mipHeight), mem);
}
#endif
}

void NativeEngine::LoadCubeTexture(const Napi::CallbackInfo& info)
{
#ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES
Expand Down
1 change: 1 addition & 0 deletions Plugins/NativeEngine/Source/NativeEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ namespace Babylon
void CopyTexture(NativeDataStream::Reader& data);
void LoadRawTexture(const Napi::CallbackInfo& info);
void LoadRawTexture2DArray(const Napi::CallbackInfo& info);
void UpdateTextureDirectly(const Napi::CallbackInfo& info);
void LoadCubeTexture(const Napi::CallbackInfo& info);
void LoadCubeTextureWithMips(const Napi::CallbackInfo& info);
Napi::Value GetTextureWidth(const Napi::CallbackInfo& info);
Expand Down