From 49f6357f434572c5d90bfe52075cf1e3e9e051d4 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 10 Jun 2026 16:01:21 -0700 Subject: [PATCH 1/9] NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics loadCubeTexture now accepts a single self-contained cubemap container (all six faces + mips), decoded via bimg::imageParse, and uploads sides 0-5 x mips. ComputeCubeSphericalPolynomial derives the diffuse-IBL spherical harmonics from the top-mip faces (port of CubeMapToSphericalPolynomialTools) and returns the polynomial coefficients to JS. This is done natively because the WebGL upload and cube-readback paths are unimplemented on native and .dds stores no SH. The 6 prefiltered-environment PBR validation tests this unblocks stay excluded here; they need the paired Babylon.js change to ship in the babylonjs dependency first. Pairs with BabylonJS/Babylon.js#18560 (native createCubeTexture dispatch for single-URL containers). Depends on a babylonjs dependency bump including it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- Plugins/NativeEngine/Source/NativeEngine.cpp | 274 +++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 5a3b977c2..1effa2d0e 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #ifdef BABYLON_NATIVE_NATIVEENGINE_TEST_HOOKS #include @@ -419,6 +420,241 @@ namespace Babylon } } } + // Parse a single self-contained cubemap container (e.g. .dds / .ktx / + // .ktx2) that already holds all six faces and their mip chain. bimg + // decodes these natively, so there is no need to split into six images + // on the JS side. Unlike ParseImage (which targets single-face 2D images + // and asserts !m_cubeMap), this keeps the container as-is. + bimg::ImageContainer* ParseCubeImage(bx::AllocatorI& allocator, gsl::span data) + { + bx::ErrorIgnore parseError; + bimg::ImageContainer* image{bimg::imageParse(&allocator, data.data(), static_cast(data.size()), bimg::TextureFormat::Count, &parseError)}; + if (image == nullptr) + { + throw std::runtime_error{"Failed to parse cube image."}; + } + + if (!image->m_cubeMap) + { + bimg::imageFree(image); + throw std::runtime_error{"Image is not a cubemap."}; + } + + return image; + } + + // Port of Babylon.js CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial. + // Prefiltered .dds environments need diffuse-IBL spherical harmonics, which Babylon's WebGL + // path computes on the CPU from the top-mip faces. The native engine cannot read cube faces + // back from the GPU (_readTexturePixels throws for cube faces), so we compute the harmonics + // here from the bimg-decoded top mip. Returns the 9x3 polynomial coefficients in + // SphericalPolynomial.FromArray order: x, y, z, xx, yy, zz, yz, zx, xy. + std::array ComputeCubeSphericalPolynomial(bx::AllocatorI& allocator, bimg::ImageContainer* image) + { + std::array result{}; + + bimg::ImageContainer* f32{bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false)}; + if (f32 == nullptr) + { + return result; + } + + const uint32_t size{f32->m_width}; + constexpr double pi{3.14159265358979323846}; + + // Face orientations matching Babylon's _FileFaces, indexed by bimg cube side order + // (+X, -X, +Y, -Y, +Z, -Z): worldAxisForNormal, worldAxisForFileX, worldAxisForFileY. + struct FaceAxes + { + double n[3]; + double fx[3]; + double fy[3]; + }; + static const FaceAxes faces[6] = { + {{1, 0, 0}, {0, 0, -1}, {0, -1, 0}}, // +X right + {{-1, 0, 0}, {0, 0, 1}, {0, -1, 0}}, // -X left + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // +Y up + {{0, -1, 0}, {1, 0, 0}, {0, 0, -1}}, // -Y down + {{0, 0, 1}, {1, 0, 0}, {0, -1, 0}}, // +Z front + {{0, 0, -1}, {-1, 0, 0}, {0, -1, 0}}, // -Z back + }; + + const double shConst[9] = { + std::sqrt(1.0 / (4.0 * pi)), + -std::sqrt(3.0 / (4.0 * pi)), + std::sqrt(3.0 / (4.0 * pi)), + -std::sqrt(3.0 / (4.0 * pi)), + std::sqrt(15.0 / (4.0 * pi)), + -std::sqrt(15.0 / (4.0 * pi)), + std::sqrt(5.0 / (16.0 * pi)), + -std::sqrt(15.0 / (4.0 * pi)), + std::sqrt(15.0 / (16.0 * pi)), + }; + const double cosKernel[9] = {pi, 2.0 * pi / 3.0, 2.0 * pi / 3.0, 2.0 * pi / 3.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0}; + + const auto areaElement = [](double x, double y) { return std::atan2(x * y, std::sqrt(x * x + y * y + 1.0)); }; + + double sh[9][3] = {}; + double totalSolidAngle{0.0}; + + const double du{2.0 / static_cast(size)}; + const double halfTexel{0.5 * du}; + const double minUV{halfTexel - 1.0}; + const double maxHdri{4096.0}; + + for (uint16_t side = 0; side < 6; ++side) + { + bimg::ImageMip mip{}; + if (!bimg::imageGetRawData(*f32, side, 0, f32->m_data, f32->m_size, mip)) + { + continue; + } + + const float* data{reinterpret_cast(mip.m_data)}; + const FaceAxes& f{faces[side]}; + + double v{minUV}; + for (uint32_t y = 0; y < size; ++y) + { + double u{minUV}; + for (uint32_t x = 0; x < size; ++x) + { + double dir[3] = { + f.fx[0] * u + f.fy[0] * v + f.n[0], + f.fx[1] * u + f.fy[1] * v + f.n[1], + f.fx[2] * u + f.fy[2] * v + f.n[2], + }; + const double len{std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2])}; + dir[0] /= len; + dir[1] /= len; + dir[2] /= len; + + const double deltaSolidAngle{ + areaElement(u - halfTexel, v - halfTexel) - + areaElement(u - halfTexel, v + halfTexel) - + areaElement(u + halfTexel, v - halfTexel) + + areaElement(u + halfTexel, v + halfTexel)}; + + const size_t idx{(static_cast(y) * size + x) * 4}; + double rgb[3] = {data[idx + 0], data[idx + 1], data[idx + 2]}; + for (int c = 0; c < 3; ++c) + { + if (std::isnan(rgb[c])) + { + rgb[c] = 0.0; + } + rgb[c] = rgb[c] < 0.0 ? 0.0 : (rgb[c] > maxHdri ? maxHdri : rgb[c]); + } + + const double trig[9] = { + 1.0, + dir[1], + dir[2], + dir[0], + dir[0] * dir[1], + dir[1] * dir[2], + 3.0 * dir[2] * dir[2] - 1.0, + dir[0] * dir[2], + dir[0] * dir[0] - dir[1] * dir[1], + }; + for (int lm = 0; lm < 9; ++lm) + { + const double basis{shConst[lm] * trig[lm] * deltaSolidAngle}; + sh[lm][0] += rgb[0] * basis; + sh[lm][1] += rgb[1] * basis; + sh[lm][2] += rgb[2] * basis; + } + totalSolidAngle += deltaSolidAngle; + u += du; + } + v += du; + } + } + + bimg::imageFree(f32); + + if (totalSolidAngle <= 0.0) + { + return result; + } + + // scaleInPlace(correction) + convertIncidentRadianceToIrradiance + convertIrradianceToLambertianRadiance. + const double correction{(4.0 * pi) / totalSolidAngle}; + for (int lm = 0; lm < 9; ++lm) + { + const double scale{correction * cosKernel[lm] / pi}; + sh[lm][0] *= scale; + sh[lm][1] *= scale; + sh[lm][2] *= scale; + } + + // SphericalPolynomial.FromHarmonics (updateFromHarmonics then *1/pi). + for (int c = 0; c < 3; ++c) + { + const double l00{sh[0][c]}, l1_1{sh[1][c]}, l10{sh[2][c]}, l11{sh[3][c]}; + const double l2_2{sh[4][c]}, l2_1{sh[5][c]}, l20{sh[6][c]}, l21{sh[7][c]}, l22{sh[8][c]}; + const double invPi{1.0 / pi}; + result[0 * 3 + c] = static_cast(-1.02333 * l11 * invPi); // x + result[1 * 3 + c] = static_cast(-1.02333 * l1_1 * invPi); // y + result[2 * 3 + c] = static_cast(1.02333 * l10 * invPi); // z + result[3 * 3 + c] = static_cast((0.886277 * l00 - 0.247708 * l20 + 0.429043 * l22) * invPi); // xx + result[4 * 3 + c] = static_cast((0.886277 * l00 - 0.247708 * l20 - 0.429043 * l22) * invPi); // yy + result[5 * 3 + c] = static_cast((0.886277 * l00 + 0.495417 * l20) * invPi); // zz + result[6 * 3 + c] = static_cast(-0.858086 * l2_1 * invPi); // yz + result[7 * 3 + c] = static_cast(-0.858086 * l21 * invPi); // zx + result[8 * 3 + c] = static_cast(0.858086 * l2_2 * invPi); // xy + } + + return result; + } + + void LoadCubeTextureFromContainer(Graphics::Texture* texture, bimg::ImageContainer* image, bool srgb) + { + assert(image->m_cubeMap); + assert(image->m_width == image->m_height); + const uint32_t size{image->m_width}; + + if (texture->IsValid()) + { + if (texture->Width() != size || texture->Height() != size) + { + bimg::imageFree(image); + throw std::runtime_error{"Cannot update texture from image of different size"}; + } + } + else + { + const bool hasMips{image->m_numMips > 1}; + const bgfx::TextureFormat::Enum format{Cast(image->m_format)}; + const uint64_t flags{srgb ? BGFX_TEXTURE_SRGB : BGFX_TEXTURE_NONE}; + texture->CreateCube(static_cast(size), hasMips, 1, format, flags); + } + + // Every (side, mip) view points into the single container's backing + // store, so the allocation is released exactly once, after bgfx has + // consumed the final upload. + const uint8_t numMips{static_cast(image->m_numMips)}; + for (uint8_t side = 0; side < 6; ++side) + { + for (uint8_t mip = 0; mip < numMips; ++mip) + { + bimg::ImageMip imageMip{}; + if (bimg::imageGetRawData(*image, side, mip, image->m_data, image->m_size, imageMip)) + { + bgfx::ReleaseFn releaseFn{}; + if (side == 5 && mip == numMips - 1) + { + releaseFn = [](void*, void* userData) { + bimg::imageFree(static_cast(userData)); + }; + } + + const bgfx::Memory* mem{bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)}; + texture->UpdateCube(0, side, mip, 0, 0, static_cast(imageMip.m_width), static_cast(imageMip.m_height), mem); + } + } + } + } #endif // BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES auto RenderTargetSamplesToBgfxMsaaFlag(uint32_t renderTargetSamples) @@ -1559,6 +1795,44 @@ namespace Babylon const auto onSuccess{info[5].As()}; const auto onError{info[6].As()}; + // A single buffer means a self-contained cubemap container (.dds / .ktx / + // .ktx2) that already holds all six faces and their mip chain; hand it to + // bimg directly instead of expecting six pre-split face images. + if (data.Length() == 1) + { + const auto typedArray{data[0u].As()}; + const auto dataSpan{gsl::make_span(static_cast(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength())}; + auto dataRef{Napi::Persistent(typedArray)}; + arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [dataSpan]() { + return ParseCubeImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan); + }) + .then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](bimg::ImageContainer* image) { + // Compute the spherical harmonics from the decoded top mip before the upload + // hands the container's memory to bgfx. + auto sphericalPolynomial = ComputeCubeSphericalPolynomial(Graphics::DeviceContext::GetDefaultAllocator(), image); + LoadCubeTextureFromContainer(texture, image, srgb); + return sphericalPolynomial; + }) + .then(m_runtimeScheduler, *m_cancellationSource, [dataRef{std::move(dataRef)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected, std::exception_ptr> result) { + if (result.has_error()) + { + onErrorRef.Call({}); + } + else + { + const auto& sphericalPolynomial{result.value()}; + auto array{Napi::Float32Array::New(onSuccessRef.Env(), sphericalPolynomial.size())}; + float* dst{array.Data()}; + for (size_t i = 0; i < sphericalPolynomial.size(); ++i) + { + dst[i] = sphericalPolynomial[i]; + } + onSuccessRef.Call({array}); + } + }); + return; + } + std::array, 6> dataRefs; std::array, 6> tasks; for (uint32_t face = 0; face < data.Length(); face++) From efc4252bdbc549308491701621fed115b032c5eb Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 11 Jun 2026 07:54:33 -0700 Subject: [PATCH 2/9] 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 8aa8f48e2..53e2ef95b 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2211,8 +2211,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 1effa2d0e..09b5bdd28 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -954,6 +954,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), @@ -1690,6 +1691,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 e3ce8af04..3317f719a 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -105,6 +105,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 92aabb2d2afe4f49a95c8606ad4c890530bdcc8f Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 9 Jul 2026 12:51:40 -0700 Subject: [PATCH 3/9] NativeEngine: add updateTextureDirectly texture-loader sink Implement the native sink for Babylon's _uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly so single-file container textures (.dds/.ktx/.ktx2, plus Basis/IES/HDR/EXR/TGA) load through the same JS texture loaders WebGL/WebGPU use. The loaders upload one (face, mip) at a time WebGL texImage2D-style; bgfx needs the whole texture allocated first, so the underlying texture is created lazily on the first upload. Validates JS-provided dimensions against maxTextureSize before uint16 narrowing and the payload size against bimg::imageGetSize, uses bgfx::copy for async-owned upload memory, and matches the existing loader flip conventions (skipping row-flips for compressed formats). Addresses #218 (paired with the Babylon.js single-file cubemap loader change). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Plugins/NativeEngine/Source/NativeEngine.cpp | 136 +++++++++++++++++++ Plugins/NativeEngine/Source/NativeEngine.h | 1 + 2 files changed, 137 insertions(+) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 09b5bdd28..e4e6180c1 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -956,6 +956,7 @@ namespace Babylon InstanceMethod("loadRawTexture", &NativeEngine::LoadRawTexture), InstanceMethod("updateTextureData", &NativeEngine::UpdateTextureData), InstanceMethod("loadRawTexture2DArray", &NativeEngine::LoadRawTexture2DArray), + InstanceMethod("updateTextureDirectly", &NativeEngine::UpdateTextureDirectly), InstanceMethod("loadCubeTexture", &NativeEngine::LoadCubeTexture), InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips), InstanceMethod("getTextureWidth", &NativeEngine::GetTextureWidth), @@ -1854,6 +1855,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>().Get()}; + const auto data{info[1].As()}; + const auto faceIndexValue{info[2].As().Uint32Value()}; + const auto lodValue{info[3].As().Uint32Value()}; + const auto baseWidth{info[4].As().Uint32Value()}; + const auto baseHeight{info[5].As().Uint32Value()}; + const auto mipWidth{info[6].As().Uint32Value()}; + const auto mipHeight{info[7].As().Uint32Value()}; + const auto formatValue{info[8].As().Uint32Value()}; + const auto isCube{info[9].As().Value()}; + const auto hasMips{info[10].As().Value()}; + const auto invertY{info[11].As().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(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(baseWidth), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE); + } + else + { + texture->Create2D(static_cast(baseWidth), static_cast(baseHeight), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE); + } + } + + const uint64_t expectedSize{bimg::imageGetSize(nullptr, static_cast(mipWidth), static_cast(mipHeight), 1, false, false, 1, format)}; + if (expectedSize == 0 || static_cast(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(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(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(mipHeight)); + } + + if (isCube) + { + texture->UpdateCube(0, static_cast(faceIndexValue), static_cast(lodValue), 0, 0, static_cast(mipWidth), static_cast(mipHeight), mem); + } + else + { + texture->Update2D(0, static_cast(lodValue), 0, 0, static_cast(mipWidth), static_cast(mipHeight), mem); + } +#endif + } + void NativeEngine::LoadCubeTexture(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 3317f719a..bfa57b18d 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -107,6 +107,7 @@ namespace Babylon void LoadRawTexture(const Napi::CallbackInfo& info); void UpdateTextureData(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); From 865ce4a63d425ba6c1d314f584626f13124785fb Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 15 Jul 2026 20:13:38 -0700 Subject: [PATCH 4/9] NativeEngine: route cube-texture UpdateTextureData to bgfx::updateTextureCube A cube created with bgfx::createTextureCube must be updated with bgfx::updateTextureCube (per-face side index), not updateTexture2D. Add an IsCube() flag (set in Texture::CreateCube, cleared in Create2D/Attach) and, in NativeEngine::UpdateTextureData, branch cubes to Texture::UpdateCube(0, side, mip, ...). Widen the layer bounds check to 6*numLayers for cubes (the JS side passes the face index in the layer arg). This is the C++ half of the HDR createRawCubeTexture fix. The IBL tests it unblocks stay excluded here: they also need the Babylon.js-side half, which is not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- .../InternalInclude/Babylon/Graphics/Texture.h | 2 ++ Core/Graphics/Source/Texture.cpp | 8 ++++++++ Plugins/NativeEngine/Source/NativeEngine.cpp | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h index 8f4e2477c..af772ea0d 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h @@ -31,6 +31,7 @@ namespace Babylon::Graphics uint16_t Width() const; uint16_t Height() const; bool HasMips() const; + bool IsCube() const; uint16_t NumLayers() const; bgfx::TextureFormat::Enum Format() const; uint64_t Flags() const; @@ -55,6 +56,7 @@ namespace Babylon::Graphics uint16_t m_width{0}; uint16_t m_height{0}; bool m_hasMips{false}; + bool m_isCube{false}; uint16_t m_numLayers{0}; bgfx::TextureFormat::Enum m_format{bgfx::TextureFormat::Enum::Unknown}; uint64_t m_flags{BGFX_TEXTURE_NONE}; diff --git a/Core/Graphics/Source/Texture.cpp b/Core/Graphics/Source/Texture.cpp index a99006e7e..07328b7ff 100644 --- a/Core/Graphics/Source/Texture.cpp +++ b/Core/Graphics/Source/Texture.cpp @@ -64,6 +64,7 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; + m_isCube = false; } void Texture::Update2D(uint16_t layer, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -83,6 +84,7 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; + m_isCube = true; } void Texture::UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -103,6 +105,7 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; + m_isCube = false; } bgfx::TextureHandle Texture::Handle() const @@ -125,6 +128,11 @@ namespace Babylon::Graphics return m_hasMips; } + bool Texture::IsCube() const + { + return m_isCube; + } + uint16_t Texture::NumLayers() const { return m_numLayers; diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index e4e6180c1..eddcdaad0 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1722,10 +1722,13 @@ namespace Babylon mipHeight = 1; } const uint16_t numLayers{texture->NumLayers() > 0 ? texture->NumLayers() : static_cast(1)}; + // A cube texture is addressed by 6 faces per array layer (bgfx side index 0-5). The JS side passes the + // face in the same "layer" argument used for 2D-array slices, so the valid range is 6*numLayers. + const uint16_t maxLayers{texture->IsCube() ? static_cast(6 * numLayers) : numLayers}; if (width == 0 || height == 0 || static_cast(x) + width > mipWidth || static_cast(y) + height > mipHeight || - layer >= numLayers) + layer >= maxLayers) { throw Napi::Error::New(info.Env(), "updateTextureData region is out of bounds"); } @@ -1760,7 +1763,16 @@ namespace Babylon { std::memcpy(mem->data, bytes, requiredSize); } - texture->Update2D(layer, mip, x, targetY, width, height, mem); + if (texture->IsCube()) + { + // bgfx addresses a cube texture by (array layer, side 0-5). Only a single (non-array) cube is + // supported here, so the JS "layer" argument is the face/side index. + texture->UpdateCube(0, static_cast(layer), mip, x, targetY, width, height, mem); + } + else + { + texture->Update2D(layer, mip, x, targetY, width, height, mem); + } } void NativeEngine::LoadRawTexture2DArray(const Napi::CallbackInfo& info) From 6c932591de06832e22c8916b19cdd0e5344268e2 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 16 Jul 2026 09:08:46 -0700 Subject: [PATCH 5/9] Fix BC1/DXT1 texture-load crash in NativeEngine PrepareImage passed block-compressed / unsupported formats straight to bimg::imageGenerateMips, which only supports RGBA8/RGBA32F and returns NULL otherwise. The NULL image was then dereferenced in LoadTextureFromImage, crashing with an access violation. Tests 250/251/252 ("PBR shader code coverage 1/2/3", snippets #QI7TL3#63/64/65) load a 256x256 BC1 texture with generateMips=true and hit this. - PrepareImage: convert any non-RGBA8/RGBA32F format before imageGenerateMips (float/high-precision -> RGBA32F, everything else incl. BC1/DXT1 -> RGBA8), with a null-check on the imageConvert result. - LoadTexture: throw (routes to onError) instead of dereferencing a null image. The crash is eliminated on all three. They stay excluded here: they also load a single-file environment cubemap, which needs the Babylon.js-side change that is not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- Plugins/NativeEngine/Source/NativeEngine.cpp | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index eddcdaad0..5ee107f41 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -289,17 +289,34 @@ namespace Babylon if (generateMips) { - if (image->m_format == bimg::TextureFormat::RGB8) - { - bimg::ImageContainer* oldImage{image}; - image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA8, *image, false); - bimg::imageFree(oldImage); - } - else if (image->m_format == bimg::TextureFormat::RG16F || image->m_format == bimg::TextureFormat::RGBA16F || image->m_format == bimg::TextureFormat::RGBA16 || image->m_format == bimg::TextureFormat::R16) + // bimg::imageGenerateMips only supports RGBA8 and RGBA32F source data; for any + // other format it returns NULL. Convert first so mip generation (and the + // subsequent GPU upload) succeeds. High-precision / float formats are promoted to + // RGBA32F to preserve range; everything else - including single/dual-channel, + // RGB8/BGRA8 and block-compressed formats such as BC1/DXT1 - is decoded to RGBA8. + // Without this, e.g. a BC1 texture with generateMips produced a NULL image that the + // caller dereferenced, crashing the process. + if (image->m_format != bimg::TextureFormat::RGBA8 && + image->m_format != bimg::TextureFormat::RGBA32F) { + const bimg::TextureFormat::Enum dstFormat = + (image->m_format == bimg::TextureFormat::R16 || + image->m_format == bimg::TextureFormat::R16F || + image->m_format == bimg::TextureFormat::RG16F || + image->m_format == bimg::TextureFormat::RG32F || + image->m_format == bimg::TextureFormat::RGBA16 || + image->m_format == bimg::TextureFormat::RGBA16F) + ? bimg::TextureFormat::RGBA32F + : bimg::TextureFormat::RGBA8; + bimg::ImageContainer* oldImage{image}; - image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false); + image = bimg::imageConvert(&allocator, dstFormat, *image, false); bimg::imageFree(oldImage); + + if (image == nullptr) + { + return nullptr; + } } bimg::ImageContainer* oldImage{image}; @@ -307,7 +324,6 @@ namespace Babylon bimg::imageFree(oldImage); } - assert(image != nullptr); return image; } @@ -1630,6 +1646,10 @@ namespace Babylon arcana::trace_region loadRegion{"NativeEngine::LoadTexture"}; bimg::ImageContainer* image{ParseImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan)}; image = PrepareImage(Graphics::DeviceContext::GetDefaultAllocator(), image, invertY, srgb, generateMips); + if (image == nullptr) + { + throw std::runtime_error{"Failed to prepare image for texture (unsupported format or image conversion failure)."}; + } LoadTextureFromImage(texture, image, srgb); }) .then(m_runtimeScheduler, *m_cancellationSource, [dataRef{Napi::Persistent(data)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected result) { From 4c4abb3348508049b70932db50167fe8168d08d8 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 16 Jul 2026 09:58:23 -0700 Subject: [PATCH 6/9] Support cube-map face readback in NativeEngine.readTexture readTexture ignored the cube face and always read srcZ=0, and the JS _readTexturePixels threw for any cube faceIndex. As a result readPixels(face) returned null and ConvertCubeMapToSphericalPolynomial crashed with "Cannot read properties of null" for tests that compute diffuse-IBL spherical harmonics from a dynamically rendered cube ("Realtime Filtering", "Refraction local cube map PBR"). - readTexture now accepts an optional faceIndex (info[9], -1 = plain 2D). A cube-face read always routes through the blit path with srcZ = face (bgfx::readTexture cannot address an individual cube face). Both tests stay excluded here: they also require Babylon.js-side changes that are not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- Plugins/NativeEngine/Source/NativeEngine.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 5ee107f41..82febbb44 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -2273,6 +2273,10 @@ namespace Babylon auto buffer{info[6].As()}; uint32_t bufferOffset{info[7].As().Uint32Value()}; uint32_t bufferLength{info[8].As().Uint32Value()}; + // Optional cube-map face index (0-5). -1 (or absent) means a plain 2D read. + const int32_t faceIndex{(info.Length() > 9 && info[9].IsNumber()) ? info[9].As().Int32Value() : -1}; + const bool isCubeFace{faceIndex >= 0}; + const uint16_t srcZ{isCubeFace ? static_cast(faceIndex) : static_cast(0)}; const auto deferred{Napi::Promise::Deferred::New(env)}; @@ -2319,13 +2323,15 @@ namespace Babylon bgfx::TextureHandle sourceTextureHandle{texture->Handle()}; auto tempTexture = std::make_shared(false); - // If the image needs to be cropped or the texture lacks the READ_BACK flag, blit to a temp texture. - if (x != 0 || y != 0 || width != (texture->Width() >> mipLevel) || height != (texture->Height() >> mipLevel) || (texture->Flags() & BGFX_TEXTURE_READ_BACK) == 0) + // If the image needs to be cropped, the texture lacks the READ_BACK flag, or we are reading a + // specific cube-map face, blit to a temp 2D texture. bgfx::readTexture cannot address an + // individual cube face, so a cube-face read always goes through the blit (srcZ = face index). + if (isCubeFace || x != 0 || y != 0 || width != (texture->Width() >> mipLevel) || height != (texture->Height() >> mipLevel) || (texture->Flags() & BGFX_TEXTURE_READ_BACK) == 0) { const bgfx::TextureHandle blitTextureHandle{bgfx::createTexture2D(width, height, /*hasMips*/ false, /*numLayers*/ 1, sourceTextureFormat, BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK)}; bgfx::Encoder* encoder = GetEncoder(); - encoder->blit(static_cast(bgfx::getCaps()->limits.maxViews - 1), blitTextureHandle, /*dstMip*/ 0, /*dstX*/ 0, /*dstY*/ 0, /*dstZ*/ 0, sourceTextureHandle, mipLevel, x, y, /*srcZ*/ 0, width, height, /*depth*/ 0); + encoder->blit(static_cast(bgfx::getCaps()->limits.maxViews - 1), blitTextureHandle, /*dstMip*/ 0, /*dstX*/ 0, /*dstY*/ 0, /*dstZ*/ 0, sourceTextureHandle, mipLevel, x, y, srcZ, width, height, /*depth*/ 0); sourceTextureHandle = blitTextureHandle; *tempTexture = true; From a17b21b7f4aec3686a88c8a62b7151fc54d9601f Mon Sep 17 00:00:00 2001 From: bkaradzic-microsoft Date: Thu, 16 Jul 2026 21:17:23 -0700 Subject: [PATCH 7/9] Native raw 3D textures + sampler3D texelFetch fix Graphics::Texture gains Create3D/Update3D (bgfx createTexture3D/ updateTexture3D) and NativeEngine gains a loadRawTexture3D binding, giving Babylon.js createRawTexture3D/updateRawTexture3D real 3D volumes on Native. Fix the sampler3D shader compile that blocks HAL Lattice: the vertical texel-coordinate flip was applied by a preprocessor macro that forced every texelFetch coordinate through ivec2(...), so sampler3D fetches failed with 'no matching overloaded function'. Move that flip into the dimension-aware FlipSamplerCoordinates AST traverser (only 2-component integer coords are flipped; 3D/array left intact), cloning the sampler and lod operands so the injected textureSize() call does not alias the original texelFetch subtree (aliasing corrupted the AST and crashed unrelated async tests on dispose). The flip is built as ivec2(uv.x, textureSize(s, lod).y - 1 - uv.y), matching the expression the old macro expanded to. The tidier vector form uv * ivec2(1, -1) + ivec2(0, size.y - 1) must not be used: it emits SPIR-V OpIMul, which SPIRV-Cross drops entirely in the SPIRV_CROSS_WEBMIN configuration Babylon Native builds (the handler is compiled out to a release-mode no-op assert). The multiply then yields no HLSL/MSL expression and the whole shader fails to cross-compile with "Cannot resolve expression type" - which regressed "Gaussian Splatting Compressed ply SH", the only enabled test that texelFetches a usampler2D. Integer subtract and vector construction are both retained by that build. The texelFetch rewrite runs on post-visit because it references the coordinate subtree twice; rewriting on the way down would make the traverser descend into that subtree twice and double-flip any nested texture() call. HAL Lattice (idx 128) stays excluded: it additionally needs Babylon.js-side 3D texture support, which is not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- .../Babylon/Graphics/Texture.h | 7 ++ Core/Graphics/Source/Texture.cpp | 41 ++++++++ Plugins/NativeEngine/Source/NativeEngine.cpp | 73 ++++++++++++++ Plugins/NativeEngine/Source/NativeEngine.h | 1 + .../Source/ShaderCompilerCommon.cpp | 31 ++---- .../Source/ShaderCompilerTraversers.cpp | 98 ++++++++++++++++++- 6 files changed, 229 insertions(+), 22 deletions(-) diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h index af772ea0d..88d1ece81 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h @@ -22,6 +22,9 @@ namespace Babylon::Graphics void Create2D(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags); void Update2D(uint16_t layer, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch = UINT16_MAX); + void Create3D(uint16_t width, uint16_t height, uint16_t depth, bool hasMips, bgfx::TextureFormat::Enum format, uint64_t flags); + void Update3D(uint8_t mip, uint16_t x, uint16_t y, uint16_t z, uint16_t width, uint16_t height, uint16_t depth, const bgfx::Memory* mem); + void CreateCube(uint16_t size, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags); void UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch = UINT16_MAX); @@ -32,7 +35,9 @@ namespace Babylon::Graphics uint16_t Height() const; bool HasMips() const; bool IsCube() const; + bool Is3D() const; uint16_t NumLayers() const; + uint16_t Depth() const; bgfx::TextureFormat::Enum Format() const; uint64_t Flags() const; uint32_t SamplerFlags() const; @@ -57,7 +62,9 @@ namespace Babylon::Graphics uint16_t m_height{0}; bool m_hasMips{false}; bool m_isCube{false}; + bool m_is3D{false}; uint16_t m_numLayers{0}; + uint16_t m_depth{0}; bgfx::TextureFormat::Enum m_format{bgfx::TextureFormat::Enum::Unknown}; uint64_t m_flags{BGFX_TEXTURE_NONE}; uint32_t m_samplerFlags{BGFX_SAMPLER_NONE}; diff --git a/Core/Graphics/Source/Texture.cpp b/Core/Graphics/Source/Texture.cpp index 07328b7ff..4e8a232a5 100644 --- a/Core/Graphics/Source/Texture.cpp +++ b/Core/Graphics/Source/Texture.cpp @@ -65,6 +65,7 @@ namespace Babylon::Graphics m_format = format; m_flags = flags; m_isCube = false; + m_is3D = false; } void Texture::Update2D(uint16_t layer, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -72,6 +73,34 @@ namespace Babylon::Graphics bgfx::updateTexture2D(m_handle, layer, mip, x, y, width, height, mem, pitch); } + void Texture::Create3D(uint16_t width, uint16_t height, uint16_t depth, bool hasMips, bgfx::TextureFormat::Enum format, uint64_t flags) + { + Dispose(); + + m_handle = bgfx::createTexture3D(width, height, depth, hasMips, format, flags); + if (!bgfx::isValid(m_handle)) + { + throw std::runtime_error{"Failed to create 3D texture"}; + } + + m_ownsHandle = true; + m_width = width; + m_height = height; + m_depth = depth; + m_hasMips = hasMips; + m_numLayers = 1; + m_format = format; + m_flags = flags; + m_isCube = false; + m_is3D = false; + m_is3D = true; + } + + void Texture::Update3D(uint8_t mip, uint16_t x, uint16_t y, uint16_t z, uint16_t width, uint16_t height, uint16_t depth, const bgfx::Memory* mem) + { + bgfx::updateTexture3D(m_handle, mip, x, y, z, width, height, depth, mem); + } + void Texture::CreateCube(uint16_t size, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); @@ -85,6 +114,7 @@ namespace Babylon::Graphics m_format = format; m_flags = flags; m_isCube = true; + m_is3D = false; } void Texture::UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -106,6 +136,7 @@ namespace Babylon::Graphics m_format = format; m_flags = flags; m_isCube = false; + m_is3D = false; } bgfx::TextureHandle Texture::Handle() const @@ -133,11 +164,21 @@ namespace Babylon::Graphics return m_isCube; } + bool Texture::Is3D() const + { + return m_is3D; + } + uint16_t Texture::NumLayers() const { return m_numLayers; } + uint16_t Texture::Depth() const + { + return m_depth; + } + bgfx::TextureFormat::Enum Texture::Format() const { return m_format; diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 82febbb44..f114429e7 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -972,6 +972,7 @@ namespace Babylon InstanceMethod("loadRawTexture", &NativeEngine::LoadRawTexture), InstanceMethod("updateTextureData", &NativeEngine::UpdateTextureData), InstanceMethod("loadRawTexture2DArray", &NativeEngine::LoadRawTexture2DArray), + InstanceMethod("loadRawTexture3D", &NativeEngine::LoadRawTexture3D), InstanceMethod("updateTextureDirectly", &NativeEngine::UpdateTextureDirectly), InstanceMethod("loadCubeTexture", &NativeEngine::LoadCubeTexture), InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips), @@ -1887,6 +1888,78 @@ namespace Babylon #endif } + void NativeEngine::LoadRawTexture3D(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>().Get()}; + const auto data = info[1].As(); + const auto rawWidth{info[2].As().Uint32Value()}; + const auto rawHeight{info[3].As().Uint32Value()}; + const auto rawDepth{info[4].As().Uint32Value()}; + const auto format{static_cast(info[5].As().Uint32Value())}; + const auto generateMips = info[6].As().Value(); + const auto invertY = info[7].As().Value(); + + if (generateMips) + { + throw Napi::Error::New(Env(), "Texture 3D currently do not support mipmaps."); + } + + if (invertY) + { + throw Napi::Error::New(Env(), "Texture 3D currently do not support invert Y."); + } + + // width/height/depth originate from JS. Validate the raw 32-bit values against the GPU limits + // before narrowing to uint16_t, otherwise an out-of-range value (e.g. 70000) would wrap into an + // in-range uint16_t and slip past this check, driving an oversized allocation or an out-of-bounds + // read in the renderer. + const auto maxTextureSize = bgfx::getCaps()->limits.maxTextureSize; + if (rawWidth == 0 || rawHeight == 0 || rawDepth == 0 || + rawWidth > maxTextureSize || + rawHeight > maxTextureSize || + rawDepth > maxTextureSize) + { + throw Napi::Error::New(Env(), "Invalid width, height, or depth for the 3D texture."); + } + + const auto width{static_cast(rawWidth)}; + const auto height{static_cast(rawHeight)}; + const auto depth{static_cast(rawDepth)}; + + uint64_t flags{BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE}; + texture->Create3D(width, height, depth, false, Cast(format), flags); + + if (!data.IsNull()) + { + // imageGetSize returns the full size in 64-bit; compare against the 64-bit byte length so a + // crafted width/height/depth cannot wrap the expected size and slip an undersized buffer past + // this check. A 3D texture is a single volume (numLayers = 1) whose depth is the 3rd argument. + const uint64_t expectedSize{bimg::imageGetSize(nullptr, width, height, depth, false, false, 1, format)}; + if (expectedSize == 0 || static_cast(data.ByteLength()) != expectedSize) + { + throw Napi::Error::New(Env(), "The data size does not match width, height, depth and format"); + } + + uint8_t* dataPtr = static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(); + const size_t dataSize = data.ByteLength(); + + // bgfx::Memory uses a 32-bit size; reject a payload that would be truncated by the bgfx::copy + // cast below. + if (dataSize > UINT32_MAX) + { + throw Napi::Error::New(Env(), "The 3D texture volume size is too large."); + } + + // This is required since BGFX must manage the memory backing the update. + const bgfx::Memory* dataCopy = bgfx::copy(dataPtr, static_cast(dataSize)); + texture->Update3D(0, 0, 0, 0, width, height, depth, dataCopy); + } +#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, diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index bfa57b18d..fcae56fe1 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -107,6 +107,7 @@ namespace Babylon void LoadRawTexture(const Napi::CallbackInfo& info); void UpdateTextureData(const Napi::CallbackInfo& info); void LoadRawTexture2DArray(const Napi::CallbackInfo& info); + void LoadRawTexture3D(const Napi::CallbackInfo& info); void UpdateTextureDirectly(const Napi::CallbackInfo& info); void LoadCubeTexture(const Napi::CallbackInfo& info); void LoadCubeTextureWithMips(const Napi::CallbackInfo& info); diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp index 425dc359b..e2222792a 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp @@ -41,27 +41,16 @@ namespace Babylon::ShaderCompilerCommon std::string ProcessSamplerFlip(std::string_view source) { - static const std::string shaderNameDefineStr = "#define SHADER_NAME"; - const auto shaderNameDefine = source.find(shaderNameDefineStr); - if (shaderNameDefine == std::string::npos) - { - throw std::runtime_error{"ProcessSamplerFlip: Could not find shader name define."}; - } - - // The vertical (V) flip applied to texture()/textureLod() sample coordinates is performed by - // the FlipSamplerCoordinates AST traverser, not by a preprocessor macro. A 2-argument - // function-like macro (`#define texture(x,y) texture(x, flip(y))`) cannot match the - // 3-argument bias form `texture(sampler, uv, bias)` emitted by some Babylon.js shaders (e.g. - // GreasedLine), and glslang's preprocessor has no variadic-macro support, so those shaders - // failed to compile. texelFetch keeps its macro because it takes integer texel coordinates, - // which the float-coordinate AST flip does not handle. - static const auto textureSamplerFunctions = R"( - #define texelFetch(tex, uv, lod) texelFetch((tex), ivec2((uv).x, textureSize((tex), (lod)).y - 1 - (uv).y), (lod)) - #define SHADER_NAME)"; - - std::string result{source}; - result.replace(shaderNameDefine, shaderNameDefineStr.length(), textureSamplerFunctions); - return result; + // The vertical (V) flip for both float sample coordinates (texture()/textureLod()) and + // integer texel coordinates (texelFetch()) is now performed by the FlipSamplerCoordinates + // AST traverser, not by a preprocessor macro. The macro form + // #define texelFetch(tex, uv, lod) texelFetch((tex), ivec2(...), (lod)) + // forced every coordinate through ivec2(...), so it could not compile against sampler3D / + // sampler2DArray ('no matching overloaded function'). The AST traverser knows the sampler + // dimensionality and only flips 2-component coordinates, leaving 3D/array fetches intact. + // This function is retained as an identity passthrough so the backend call sites don't need + // to change. + return std::string{source}; } void AppendUniformBuffer(std::vector& bytes, const NonSamplerUniformsInfo& uniformBuffer, bool isFragment) diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp index 3289d86ce..0963a83f5 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp @@ -1760,13 +1760,41 @@ namespace Babylon::ShaderCompilerTraversers } } } + else if (visit == EvPostVisit && node->getOp() == EOpTextureFetch) + { + // texelFetch(sampler, ivec coord, lod). The vertical flip that used to be applied + // by a preprocessor macro in ProcessSamplerFlip is done here instead so that the + // sampler dimensionality is known: only 2-component integer coordinates + // (sampler2D-style) are flipped. sampler3D / sampler2DArray coordinates (ivec3) + // are left untouched — the old macro forced every coordinate through ivec2(...), + // which failed to compile against sampler3D ('no matching overloaded function'). + auto& sequence = node->getSequence(); + if (sequence.size() >= 3) + { + auto* sampler = sequence[0]->getAsTyped(); + auto* coordinate = sequence[1]->getAsTyped(); + auto* lod = sequence[2]->getAsTyped(); + if (sampler != nullptr && coordinate != nullptr && lod != nullptr && + coordinate->getType().getBasicType() == EbtInt && + !coordinate->getType().isArray() && + coordinate->getType().getVectorSize() == 2) + { + sequence[1] = FlipVerticalTexelCoordinate(coordinate, sampler, lod); + } + } + } return true; } private: + // Post-visit is enabled so that texelFetch coordinates can be rewritten after their + // children have been traversed: the rewrite references the coordinate subtree twice, + // and rewriting it on the way down would make the traverser descend into that subtree + // twice and flip any nested texture() call inside it twice. FlipSamplerCoordinatesTraverser(TIntermediate* intermediate) - : m_intermediate{intermediate} + : TIntermTraverser{true, false, true} + , m_intermediate{intermediate} { } @@ -1790,6 +1818,74 @@ namespace Babylon::ShaderCompilerTraversers return m_intermediate->addBinaryMath(EOpAdd, scaled, offset, loc); } + // Builds `ivec2(coordinate.x, textureSize(sampler, lod).y - 1 - coordinate.y)`, the + // integer-texel-coordinate equivalent of FlipVerticalCoordinate. This is exactly the + // expression the former ProcessSamplerFlip texelFetch macro expanded to, including its + // double evaluation of the coordinate operand. + // + // The obvious vector form `coordinate * ivec2(1, -1) + ivec2(0, size.y - 1)` must NOT + // be used: it emits SPIR-V OpIMul, which SPIRV-Cross omits entirely when built with + // SPIRV_CROSS_WEBMIN (the configuration Babylon Native ships). The multiply then + // silently produces no HLSL/MSL expression and the whole shader fails to cross-compile + // with "Cannot resolve expression type". Integer subtract and vector construction are + // both retained by that build, so express the flip with those only. + TIntermTyped* FlipVerticalTexelCoordinate(TIntermTyped* coordinate, TIntermTyped* sampler, TIntermTyped* lod) + { + const TSourceLoc& loc{coordinate->getLoc()}; + + // The sampler and lod operands are still referenced by the original texelFetch node. + // Reusing the same node pointers inside a second call (textureSize) would give those + // subtrees two parents in the AST, which later traversers (sampler splitting, SPIR-V + // generation) corrupt. Clone them so each reference is an independent node. If either + // operand is something we cannot safely clone, skip the flip rather than risk it. + TIntermTyped* samplerClone{CloneLeaf(sampler)}; + TIntermTyped* lodClone{CloneLeaf(lod)}; + if (samplerClone == nullptr || lodClone == nullptr) + { + return coordinate; + } + + TType ivec2Type{EbtInt, EvqTemporary, 2}; + TType intType{EbtInt, EvqTemporary, 1}; + + // textureSize(sampler, lod) -> ivec2 + TIntermAggregate* sizeArgs{m_intermediate->makeAggregate(samplerClone, loc)}; + sizeArgs = m_intermediate->growAggregate(sizeArgs, lodClone, loc); + TIntermTyped* size{m_intermediate->addBuiltInFunctionCall(loc, EOpTextureQuerySize, false, sizeArgs, ivec2Type)}; + + // textureSize(sampler, lod).y - 1 + TIntermTyped* sizeY{m_intermediate->addIndex(EOpIndexDirect, size, m_intermediate->addConstantUnion(1, loc), loc)}; + sizeY->setType(intType); + TIntermTyped* maxY{m_intermediate->addBinaryMath(EOpSub, sizeY, m_intermediate->addConstantUnion(1, loc), loc)}; + + // coordinate.x and coordinate.y + TIntermTyped* coordinateX{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(0, loc), loc)}; + coordinateX->setType(intType); + TIntermTyped* coordinateY{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(1, loc), loc)}; + coordinateY->setType(intType); + + // ivec2(coordinate.x, (textureSize(sampler, lod).y - 1) - coordinate.y) + TIntermTyped* flippedY{m_intermediate->addBinaryMath(EOpSub, maxY, coordinateY, loc)}; + TIntermAggregate* flipped{m_intermediate->makeAggregate(coordinateX, loc)}; + flipped = m_intermediate->growAggregate(flipped, flippedY, loc); + return m_intermediate->setAggregateOperator(flipped, EOpConstructIVec2, ivec2Type, loc); + } + + // Produces an independent copy of a leaf operand (a sampler symbol or a constant lod) so + // it can be referenced from a second call site without aliasing the original AST node. + TIntermTyped* CloneLeaf(TIntermTyped* node) + { + if (TIntermSymbol* symbol = node->getAsSymbolNode()) + { + return m_intermediate->addSymbol(*symbol); + } + if (TIntermConstantUnion* constant = node->getAsConstantUnion()) + { + return m_intermediate->addConstantUnion(constant->getConstArray(), constant->getType(), node->getLoc()); + } + return nullptr; + } + TIntermediate* m_intermediate{}; }; } From 5d8c2e1a97b85509f7400f313881e2b3f4e1fe9d Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Fri, 31 Jul 2026 15:30:02 -0700 Subject: [PATCH 8/9] Address Copilot review feedback Three fixes from review on #1808: - UpdateTextureData: the bounds check allows `layer` up to 6*numLayers for cube textures (six faces per array layer), but the call site passed that value straight through as the bgfx *side* and hardcoded array layer 0. For a cube array that meant a side index above 5 and every update landing on layer 0. Decompose `layer` into (layer / 6, layer % 6) so it matches the range the bounds check actually admits. - ReadTexture: the face/layer index was forwarded to encoder->blit as srcZ with no upper bound, so an out-of-range value could drive an out-of-bounds read inside bgfx. Validate it against the texture's srcZ extent. Note this is deliberately not a flat 0-5 check: Babylon.js passes this same argument for 2D arrays as a slice index (see BaseTexture.readPixels, which takes the faceIndex branch for `isCube || is2DArray`), where values above 5 are legitimate. The bound is 6*numLayers for cube textures and numLayers otherwise, matching UpdateTextureData. - Texture::Create3D: drop a duplicated `m_is3D = false;` store left over from copy/paste before the correct `m_is3D = true;`. Revalidated: ran=301 passed=301 failed=0 (RelWithDebInfo, Win32, D3D11). --- Core/Graphics/Source/Texture.cpp | 1 - Plugins/NativeEngine/Source/NativeEngine.cpp | 21 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Core/Graphics/Source/Texture.cpp b/Core/Graphics/Source/Texture.cpp index 4e8a232a5..558afeed6 100644 --- a/Core/Graphics/Source/Texture.cpp +++ b/Core/Graphics/Source/Texture.cpp @@ -92,7 +92,6 @@ namespace Babylon::Graphics m_format = format; m_flags = flags; m_isCube = false; - m_is3D = false; m_is3D = true; } diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index f114429e7..695801eaf 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1786,9 +1786,11 @@ namespace Babylon } if (texture->IsCube()) { - // bgfx addresses a cube texture by (array layer, side 0-5). Only a single (non-array) cube is - // supported here, so the JS "layer" argument is the face/side index. - texture->UpdateCube(0, static_cast(layer), mip, x, targetY, width, height, mem); + // bgfx addresses a cube texture by (array layer, side 0-5), but the JS side packs both into the + // single "layer" argument it also uses for 2D-array slices (6 consecutive faces per array layer, + // which is what the bounds check above allows). Decompose it, otherwise a cube-array face index + // above 5 would be forwarded as an out-of-range side and every update would land on array layer 0. + texture->UpdateCube(static_cast(layer / 6), static_cast(layer % 6), mip, x, targetY, width, height, mem); } else { @@ -2372,12 +2374,23 @@ namespace Babylon buffer = Napi::ArrayBuffer::New(env, bufferLength); } + // The face/layer index is JS-controlled and is forwarded to encoder->blit as srcZ, so validate it + // before it can drive an out-of-bounds read inside bgfx. Babylon.js passes this argument for both + // cube maps (face 0-5, six consecutive faces per array layer) and 2D arrays (slice index, which can + // legitimately exceed 5), and -1 for a plain 2D read -- so the bound is the texture's srcZ extent, + // not a flat 0-5. + const uint16_t numLayers{texture->NumLayers() > 0 ? texture->NumLayers() : static_cast(1)}; + const uint32_t maxSrcZ{texture->IsCube() ? 6u * numLayers : numLayers}; + if (isCubeFace && static_cast(faceIndex) >= maxSrcZ) + { + deferred.Reject(Napi::Error::New(env, "readTexture face/layer index is out of range for this texture.").Value()); + } // Make sure the buffer is big enough for the offset + length. Both // bufferOffset and bufferLength are JS-supplied uint32_t, so widen the // addition to 64-bit: computing it in 32-bit can wrap around (e.g. offset // 0xF0000000 + length 0x20000000), letting an out-of-range offset pass this // gate and overflow the ArrayBuffer backing store in the memcpy below. - if (buffer.ByteLength() < static_cast(bufferOffset) + bufferLength) + else if (buffer.ByteLength() < static_cast(bufferOffset) + bufferLength) { deferred.Reject(Napi::Error::New(env, "Provided buffer is too small for the specified offset and length.").Value()); } From a3241c16037492cd2b36c4101836147e17565ea0 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Tue, 4 Aug 2026 16:45:59 -0700 Subject: [PATCH 9/9] Address review feedback on texel flip, texture metadata and updateTextureData Clone whole texel coordinate expressions instead of leaves only. CloneLeaf handled symbols and constant unions and returned nullptr for everything else, and the caller treated nullptr as "skip the flip". Any texelFetch whose sampler or lod was not a bare leaf therefore sampled with an un-flipped Y and produced a wrong image with no diagnostic. Replace it with CloneExpression, a structural deep clone covering symbols, constant unions, binary, unary and aggregate nodes, which throws instead of silently skipping when it meets something it cannot copy. The coordinate itself was also referenced twice, for .x and .y, without being cloned. That is the same aliasing the surrounding comment cites as the reason for cloning the sampler and lod, so clone it too and let the original supply one reference and the clone the other, leaving every node with exactly one parent. Cover the new paths in the comprehensive GLSL compilation test with the coordinate shapes that actually occur in Babylon shaders: a constructor, a binary expression, a nested constructor over a float expression, a nested constructor over integer binaries, built-in calls, and a non-constant lod. These exercise the unary, binary and aggregate clone paths, none of which the old code could handle. Integer multiply, divide, modulo and bitwise operators are avoided in the test because glslang and SPIRV-Cross are built in their WEBMIN configurations here and reject them for unrelated reasons. Reset texture metadata in one place. m_depth was only ever assigned by Create3D, so a Texture re-created as 2D or cube after having been 3D kept reporting the old depth. Rather than add one more hand-written assignment to each Create*, give them a shared ResetMetadata() so a field that a given path does not set cannot survive from the previous, differently shaped texture. Reject block-compressed formats in updateTextureData. The vertical flip derived its row stride as requiredSize / height, which is only a row stride for uncompressed formats. For BC1 at 4x4 that yields 2 bytes against a real block row of 8. Even with the right stride, mirroring block rows cannot flip a texture vertically without re-encoding the texel rows packed inside each block, and bgfx additionally requires block-aligned coordinates here. Reject these formats with a clear error; the base upload path already handles compressed data, so nothing is lost. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae --- ...sts.shaderCompilation.comprehensiveGLSL.js | 21 ++++- ...sts.shaderCompilation.comprehensiveGLSL.ts | 21 ++++- .../Babylon/Graphics/Texture.h | 6 ++ Core/Graphics/Source/Texture.cpp | 23 +++-- Plugins/NativeEngine/Source/NativeEngine.cpp | 14 +++ .../Source/ShaderCompilerTraversers.cpp | 92 +++++++++++++++---- 6 files changed, 149 insertions(+), 28 deletions(-) diff --git a/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js b/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js index 1621956e7..19ef31d67 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js +++ b/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js @@ -780,7 +780,26 @@ var vertexSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2 // --------------------------------------------------------------------------- // Comprehensive WebGL2 GLSL ES 3.00 fragment shader // --------------------------------------------------------------------------- -var fragmentSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\nprecision highp samplerCube;\nprecision highp sampler2DArray;\nprecision highp sampler2DShadow;\nprecision highp samplerCubeShadow;\nprecision highp sampler2DArrayShadow;\nprecision highp isampler2D;\nprecision highp isampler3D;\nprecision highp isamplerCube;\nprecision highp isampler2DArray;\nprecision highp usampler2D;\nprecision highp usampler3D;\nprecision highp usamplerCube;\nprecision highp usampler2DArray;\n\n// \u2500\u2500 Struct (must match vertex) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nstruct LightInfo {\n vec3 position;\n vec3 color;\n float intensity;\n};\n\nstruct Material {\n vec4 diffuse;\n vec4 specular;\n float shininess;\n LightInfo mainLight;\n};\n\n// \u2500\u2500 Uniform blocks (std140) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(std140) uniform TransformBlock {\n mat4 uModel;\n mat4 uViewProj;\n mat4 uNormalMatrix;\n};\n\nlayout(std140) uniform SceneBlock {\n vec4 uAmbientColor;\n float uTime;\n int uFrameCount;\n uint uFlags;\n};\n\n// \u2500\u2500 All sampler types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform sampler2D uSampler2D;\nuniform sampler3D uSampler3D;\nuniform samplerCube uSamplerCube;\nuniform sampler2DArray uSampler2DArray;\nuniform sampler2DShadow uSampler2DShadow;\nuniform samplerCubeShadow uSamplerCubeShadow;\nuniform sampler2DArrayShadow uSampler2DArrayShadow;\nuniform isampler2D uISampler2D;\nuniform isampler3D uISampler3D;\nuniform isamplerCube uISamplerCube;\nuniform isampler2DArray uISampler2DArray;\nuniform usampler2D uUSampler2D;\nuniform usampler3D uUSampler3D;\nuniform usamplerCube uUSamplerCube;\nuniform usampler2DArray uUSampler2DArray;\n\n// \u2500\u2500 Plain uniforms \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform Material uMaterial;\nuniform float uCustomFloat;\n\n// \u2500\u2500 Fragment inputs (from vertex shader) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nin vec3 vWorldPos;\nin vec3 vNormal;\nin vec2 vUV;\nflat in int vVertexID;\nflat in uint vFlagsOut;\nsmooth in vec4 vColor;\ncentroid in vec2 vCentroidUV;\nin vec4 vSplatColor;\n\n// \u2500\u2500 Fragment output (MRT-capable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(location = 0) out vec4 fragColor;\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst float PI = 3.14159265359;\nconst float EPSILON = 1e-6;\n\n// \u2500\u2500 Helper: Blinn-Phong \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 blinnPhong(vec3 N, vec3 L, vec3 V, vec3 lightColor, float shininess) {\n float NdotL = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float NdotH = max(dot(N, H), 0.0);\n float specPower = pow(NdotH, shininess);\n return lightColor * (NdotL + specPower);\n}\n\n// \u2500\u2500 Helper: Fresnel-Schlick \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n}\n\n// \u2500\u2500 Helper: normal mapping simulation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 perturbNormal(vec3 N, vec3 texNormal) {\n // unpack from [0,1] to [-1,1]\n vec3 mapped = texNormal * 2.0 - 1.0;\n return normalize(N + mapped * 0.5);\n}\n\nvoid main() {\n // \u2500\u2500 Fragment built-in variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 fragCoord = gl_FragCoord;\n bool frontFacing = gl_FrontFacing;\n float fragDepth = gl_FragCoord.z;\n\n // \u2500\u2500 Derivative functions (fragment only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec2 dxUV = dFdx(vUV);\n vec2 dyUV = dFdy(vUV);\n vec2 fwUV = fwidth(vUV);\n \n // \u2500\u2500 Texture sampling: texture() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2d = texture(uSampler2D, vUV);\n vec4 tex3d = texture(uSampler3D, vec3(vUV, 0.0));\n vec4 texCube = texture(uSamplerCube, vNormal);\n vec4 texArr = texture(uSampler2DArray, vec3(vUV, 0.0));\n\n // \u2500\u2500 Texture sampling: textureLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dLod = textureLod(uSampler2D, vUV, 1.0);\n\n // \u2500\u2500 Texture sampling: textureOffset() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dOff = textureOffset(uSampler2D, vUV, ivec2(1, 0));\n\n // \u2500\u2500 Texture sampling: textureGrad() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dGrad = textureGrad(uSampler2D, vUV, dxUV, dyUV);\n /* textureProj not used in Babylon\n // \u2500\u2500 Texture sampling: textureProj() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProj = textureProj(uSampler2D, vec3(vUV, 1.0));\n vec4 tex2dProj4 = textureProj(uSampler2D, vec4(vUV, 0.0, 1.0));\n\n // \u2500\u2500 Texture sampling: textureProjLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProjL = textureProjLod(uSampler2D, vec3(vUV, 1.0), 0.0);\n */\n // \u2500\u2500 Texture sampling: texelFetch() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 texCoord = ivec2(gl_FragCoord.xy);\n vec4 fetched = texelFetch(uSampler2D, texCoord, 0);\n \n // \u2500\u2500 Texture sampling: textureSize() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 size2d = textureSize(uSampler2D, 0);\n ivec3 size3d = textureSize(uSampler3D, 0);\n \n ivec2 sizeCube = textureSize(uSamplerCube, 0);\n /* No 2D array support\n ivec3 sizeArr = textureSize(uSampler2DArray, 0);\n\n // \u2500\u2500 Shadow sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float shadow2d = texture(uSampler2DShadow, vec3(vUV, 0.5));\n float shadowCube = texture(uSamplerCubeShadow, vec4(vNormal, 0.5));\n float shadowArr = texture(uSampler2DArrayShadow, vec4(vUV, 0.0, 0.5));\n\n // \u2500\u2500 Integer sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec4 iTex2d = texture(uISampler2D, vUV);\n ivec4 iTex3d = texture(uISampler3D, vec3(vUV, 0.0));\n ivec4 iTexCube = texture(uISamplerCube, vNormal);\n ivec4 iTexArr = texture(uISampler2DArray, vec3(vUV, 0.0));\n uvec4 uTex2d = texture(uUSampler2D, vUV);\n uvec4 uTex3d = texture(uUSampler3D, vec3(vUV, 0.0));\n uvec4 uTexCube = texture(uUSamplerCube, vNormal);\n uvec4 uTexArr = texture(uUSampler2DArray, vec3(vUV, 0.0));\n */\n // \u2500\u2500 trunc / round (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float truncF = trunc(1.7);\n float roundF = round(1.5);\n float roundEven_ = roundEven(2.5);\n\n // \u2500\u2500 modf (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float integralPart;\n float fractionalPart = modf(3.75, integralPart);\n\n // \u2500\u2500 min/max/clamp on vec types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 minV = min(vNormal, vec3(0.5));\n vec3 maxV = max(vNormal, vec3(0.0));\n vec3 clampV = clamp(vNormal, vec3(0.0), vec3(1.0));\n \n // \u2500\u2500 mix with bvec selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 mixBvec = mix(vec3(0.0), vec3(1.0), bvec3(true, false, true));\n\n // \u2500\u2500 Control flow: if / else with discard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (tex2d.a < 0.01) {\n discard;\n }\n\n // \u2500\u2500 Control flow: for loop with early exit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 lightAccum = vec3(0.0);\n for (int j = 0; j < 4; j++) {\n LightInfo lt;\n lt.position = vec3(float(j) * 3.0, 5.0, 0.0);\n lt.color = vec3(1.0, 0.9, 0.8);\n lt.intensity = 1.0 / (1.0 + float(j));\n\n vec3 L = normalize(lt.position - vWorldPos);\n vec3 V = normalize(-vWorldPos);\n lightAccum += blinnPhong(normalize(vNormal), L, V, lt.color, uMaterial.shininess) * lt.intensity;\n\n if (length(lightAccum) > 3.0) break;\n }\n \n // \u2500\u2500 Control flow: while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float decay = 1.0;\n int wc = 0;\n while (decay > 0.01 && wc < 10) {\n decay *= 0.7;\n wc++;\n }\n\n // \u2500\u2500 Control flow: do-while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float buildUp = 0.0;\n int dc = 0;\n do {\n buildUp += 0.1;\n dc++;\n } while (buildUp < 0.5 && dc < 10);\n \n // \u2500\u2500 Control flow: switch / case \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 modeColor = vec3(0.0);\n switch (vVertexID % 4) {\n case 0: modeColor = vec3(1.0, 0.0, 0.0); break;\n case 1: modeColor = vec3(0.0, 1.0, 0.0); break;\n case 2: modeColor = vec3(0.0, 0.0, 1.0); break;\n default: modeColor = vec3(1.0); break;\n }\n\n // \u2500\u2500 Bitwise on uint flags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n bool flag0 = (vFlagsOut & 1u) != 0u;\n bool flag1 = (vFlagsOut & 2u) != 0u;\n uint shifted = vFlagsOut << 1u;\n uint masked = vFlagsOut & 0xF0u;\n \n // \u2500\u2500 Front-facing conditional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 normal = frontFacing ? normalize(vNormal) : -normalize(vNormal);\n\n // \u2500\u2500 Fresnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 viewDir = normalize(-vWorldPos);\n vec3 fresnel = fresnelSchlick(max(dot(normal, viewDir), 0.0), vec3(0.04));\n\n // \u2500\u2500 Normal perturbation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 perturbedN = perturbNormal(normal, tex2d.rgb);\n \n // \u2500\u2500 Final composition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 materialDiffuse = uMaterial.diffuse;\n vec4 materialSpecular = uMaterial.specular;\n vec3 albedo = tex2d.rgb * materialDiffuse.rgb * vColor.rgb;\n // using uAmbientColor.rgb crashes SpvBuilder because it assumes it's a single float\n // this case doesn't seem to be found anywhere in Babylon shader so won't fix\n vec3 ambient = /*uAmbientColor.rgb * */albedo;\n \n vec3 lit = ambient + lightAccum * albedo;\n vec3 specular = fresnel * materialSpecular.rgb;\n \n vec4 finalColor = vec4(lit + specular + modeColor * 0.01, tex2d.a * materialDiffuse.a);\n \n // \u2500\u2500 Tone mapping (Reinhard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = finalColor.rgb / (finalColor.rgb + vec3(1.0));\n\n // \u2500\u2500 Gamma correction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = pow(finalColor.rgb, vec3(1.0 / 2.2));\n\n // \u2500\u2500 Write fragment depth (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n gl_FragDepth = fragDepth;\n\n // \u2500\u2500 Output (blend in splat color contribution) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n fragColor = finalColor + vSplatColor * 0.001;\n}\n"; +var fragmentSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\nprecision highp samplerCube;\nprecision highp sampler2DArray;\nprecision highp sampler2DShadow;\nprecision highp samplerCubeShadow;\nprecision highp sampler2DArrayShadow;\nprecision highp isampler2D;\nprecision highp isampler3D;\nprecision highp isamplerCube;\nprecision highp isampler2DArray;\nprecision highp usampler2D;\nprecision highp usampler3D;\nprecision highp usamplerCube;\nprecision highp usampler2DArray;\n\n// \u2500\u2500 Struct (must match vertex) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nstruct LightInfo {\n vec3 position;\n vec3 color;\n float intensity;\n};\n\nstruct Material {\n vec4 diffuse;\n vec4 specular;\n float shininess;\n LightInfo mainLight;\n};\n\n// \u2500\u2500 Uniform blocks (std140) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(std140) uniform TransformBlock {\n mat4 uModel;\n mat4 uViewProj;\n mat4 uNormalMatrix;\n};\n\nlayout(std140) uniform SceneBlock {\n vec4 uAmbientColor;\n float uTime;\n int uFrameCount;\n uint uFlags;\n};\n\n// \u2500\u2500 All sampler types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform sampler2D uSampler2D;\nuniform sampler3D uSampler3D;\nuniform samplerCube uSamplerCube;\nuniform sampler2DArray uSampler2DArray;\nuniform sampler2DShadow uSampler2DShadow;\nuniform samplerCubeShadow uSamplerCubeShadow;\nuniform sampler2DArrayShadow uSampler2DArrayShadow;\nuniform isampler2D uISampler2D;\nuniform isampler3D uISampler3D;\nuniform isamplerCube uISamplerCube;\nuniform isampler2DArray uISampler2DArray;\nuniform usampler2D uUSampler2D;\nuniform usampler3D uUSampler3D;\nuniform usamplerCube uUSamplerCube;\nuniform usampler2DArray uUSampler2DArray;\n\n// \u2500\u2500 Plain uniforms \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform Material uMaterial;\nuniform float uCustomFloat;\n\n// \u2500\u2500 Fragment inputs (from vertex shader) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nin vec3 vWorldPos;\nin vec3 vNormal;\nin vec2 vUV;\nflat in int vVertexID;\nflat in uint vFlagsOut;\nsmooth in vec4 vColor;\ncentroid in vec2 vCentroidUV;\nin vec4 vSplatColor;\n\n// \u2500\u2500 Fragment output (MRT-capable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(location = 0) out vec4 fragColor;\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst float PI = 3.14159265359;\nconst float EPSILON = 1e-6;\n\n// \u2500\u2500 Helper: Blinn-Phong \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 blinnPhong(vec3 N, vec3 L, vec3 V, vec3 lightColor, float shininess) {\n float NdotL = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float NdotH = max(dot(N, H), 0.0);\n float specPower = pow(NdotH, shininess);\n return lightColor * (NdotL + specPower);\n}\n\n// \u2500\u2500 Helper: Fresnel-Schlick \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n}\n\n// \u2500\u2500 Helper: normal mapping simulation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 perturbNormal(vec3 N, vec3 texNormal) {\n // unpack from [0,1] to [-1,1]\n vec3 mapped = texNormal * 2.0 - 1.0;\n return normalize(N + mapped * 0.5);\n}\n\nvoid main() {\n // \u2500\u2500 Fragment built-in variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 fragCoord = gl_FragCoord;\n bool frontFacing = gl_FrontFacing;\n float fragDepth = gl_FragCoord.z;\n\n // \u2500\u2500 Derivative functions (fragment only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec2 dxUV = dFdx(vUV);\n vec2 dyUV = dFdy(vUV);\n vec2 fwUV = fwidth(vUV);\n \n // \u2500\u2500 Texture sampling: texture() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2d = texture(uSampler2D, vUV);\n vec4 tex3d = texture(uSampler3D, vec3(vUV, 0.0));\n vec4 texCube = texture(uSamplerCube, vNormal);\n vec4 texArr = texture(uSampler2DArray, vec3(vUV, 0.0));\n\n // \u2500\u2500 Texture sampling: textureLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dLod = textureLod(uSampler2D, vUV, 1.0);\n\n // \u2500\u2500 Texture sampling: textureOffset() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dOff = textureOffset(uSampler2D, vUV, ivec2(1, 0));\n\n // \u2500\u2500 Texture sampling: textureGrad() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dGrad = textureGrad(uSampler2D, vUV, dxUV, dyUV);\n /* textureProj not used in Babylon\n // \u2500\u2500 Texture sampling: textureProj() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProj = textureProj(uSampler2D, vec3(vUV, 1.0));\n vec4 tex2dProj4 = textureProj(uSampler2D, vec4(vUV, 0.0, 1.0));\n\n // \u2500\u2500 Texture sampling: textureProjLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProjL = textureProjLod(uSampler2D, vec3(vUV, 1.0), 0.0);\n */\n // \u2500\u2500 Texture sampling: texelFetch() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 texCoord = ivec2(gl_FragCoord.xy);\n vec4 fetched = texelFetch(uSampler2D, texCoord, 0);\n\n // texelFetch coordinates are flipped vertically by the shader compiler, which has to clone\n // the coordinate expression to reference it twice. Cover the operand shapes that appear in\n // Babylon shaders beyond a plain symbol: a constructor, a binary expression, a nested\n // constructor over a float expression, a nested constructor over integer binaries, built-in\n // calls, and a non-constant lod. Integer multiply, divide, modulo and bitwise operators are\n // deliberately avoided: Babylon Native builds glslang and SPIRV-Cross in their WEBMIN\n // configurations, which do not support them, so they would fail for reasons that have nothing\n // to do with texel coordinates.\n int fetchIndex = int(gl_FragCoord.x) + int(gl_FragCoord.y);\n int fetchLod = fetchIndex - fetchIndex;\n ivec2 fetchSize = textureSize(uSampler2D, 0);\n vec4 fetchedCtor = texelFetch(uSampler2D, ivec2(gl_FragCoord.xy), 0);\n vec4 fetchedOffset = texelFetch(uSampler2D, texCoord + ivec2(1, 1), 0);\n vec4 fetchedScaled = texelFetch(uSampler2D, ivec2(vUV * vec2(fetchSize)), 0);\n vec4 fetchedNested = texelFetch(uSampler2D, ivec2(fetchSize.x - texCoord.x, fetchSize.y - texCoord.y), 0);\n vec4 fetchedCall = texelFetch(uSampler2D, ivec2(abs(texCoord.x), abs(texCoord.y)), 0);\n vec4 fetchedLod = texelFetch(uSampler2D, texCoord.yx, fetchLod);\n vec4 fetchedComplex = fetchedCtor + fetchedOffset + fetchedScaled + fetchedNested + fetchedCall + fetchedLod;\n \n // \u2500\u2500 Texture sampling: textureSize() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 size2d = textureSize(uSampler2D, 0);\n ivec3 size3d = textureSize(uSampler3D, 0);\n \n ivec2 sizeCube = textureSize(uSamplerCube, 0);\n /* No 2D array support\n ivec3 sizeArr = textureSize(uSampler2DArray, 0);\n\n // \u2500\u2500 Shadow sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float shadow2d = texture(uSampler2DShadow, vec3(vUV, 0.5));\n float shadowCube = texture(uSamplerCubeShadow, vec4(vNormal, 0.5));\n float shadowArr = texture(uSampler2DArrayShadow, vec4(vUV, 0.0, 0.5));\n\n // \u2500\u2500 Integer sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec4 iTex2d = texture(uISampler2D, vUV);\n ivec4 iTex3d = texture(uISampler3D, vec3(vUV, 0.0));\n ivec4 iTexCube = texture(uISamplerCube, vNormal);\n ivec4 iTexArr = texture(uISampler2DArray, vec3(vUV, 0.0));\n uvec4 uTex2d = texture(uUSampler2D, vUV);\n uvec4 uTex3d = texture(uUSampler3D, vec3(vUV, 0.0));\n uvec4 uTexCube = texture(uUSamplerCube, vNormal);\n uvec4 uTexArr = texture(uUSampler2DArray, vec3(vUV, 0.0));\n */\n // \u2500\u2500 trunc / round (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float truncF = trunc(1.7);\n float roundF = round(1.5);\n float roundEven_ = roundEven(2.5);\n\n // \u2500\u2500 modf (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float integralPart;\n float fractionalPart = modf(3.75, integralPart);\n\n // \u2500\u2500 min/max/clamp on vec types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 minV = min(vNormal, vec3(0.5));\n vec3 maxV = max(vNormal, vec3(0.0));\n vec3 clampV = clamp(vNormal, vec3(0.0), vec3(1.0));\n \n // \u2500\u2500 mix with bvec selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 mixBvec = mix(vec3(0.0), vec3(1.0), bvec3(true, false, true));\n\n // \u2500\u2500 Control flow: if / else with discard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (tex2d.a < 0.01) {\n discard;\n }\n\n // \u2500\u2500 Control flow: for loop with early exit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 lightAccum = vec3(0.0);\n for (int j = 0; j < 4; j++) {\n LightInfo lt;\n lt.position = vec3(float(j) * 3.0, 5.0, 0.0);\n lt.color = vec3(1.0, 0.9, 0.8);\n lt.intensity = 1.0 / (1.0 + float(j));\n\n vec3 L = normalize(lt.position - vWorldPos);\n vec3 V = normalize(-vWorldPos);\n lightAccum += blinnPhong(normalize(vNormal), L, V, lt.color, uMaterial.shininess) * lt.intensity;\n\n if (length(lightAccum) > 3.0) break;\n }\n \n // \u2500\u2500 Control flow: while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float decay = 1.0;\n int wc = 0;\n while (decay > 0.01 && wc < 10) {\n decay *= 0.7;\n wc++;\n }\n\n // \u2500\u2500 Control flow: do-while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float buildUp = 0.0;\n int dc = 0;\n do {\n buildUp += 0.1;\n dc++;\n } while (buildUp < 0.5 && dc < 10);\n \n // \u2500\u2500 Control flow: switch / case \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 modeColor = vec3(0.0);\n switch (vVertexID % 4) {\n case 0: modeColor = vec3(1.0, 0.0, 0.0); break;\n case 1: modeColor = vec3(0.0, 1.0, 0.0); break;\n case 2: modeColor = vec3(0.0, 0.0, 1.0); break;\n default: modeColor = vec3(1.0); break;\n }\n\n // \u2500\u2500 Bitwise on uint flags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n bool flag0 = (vFlagsOut & 1u) != 0u;\n bool flag1 = (vFlagsOut & 2u) != 0u;\n uint shifted = vFlagsOut << 1u;\n uint masked = vFlagsOut & 0xF0u;\n \n // \u2500\u2500 Front-facing conditional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 normal = frontFacing ? normalize(vNormal) : -normalize(vNormal);\n\n // \u2500\u2500 Fresnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 viewDir = normalize(-vWorldPos);\n vec3 fresnel = fresnelSchlick(max(dot(normal, viewDir), 0.0), vec3(0.04));\n\n // \u2500\u2500 Normal perturbation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 perturbedN = perturbNormal(normal, tex2d.rgb);\n \n // \u2500\u2500 Final composition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 materialDiffuse = uMaterial.diffuse;\n vec4 materialSpecular = uMaterial.specular;\n vec3 albedo = tex2d.rgb * materialDiffuse.rgb * vColor.rgb;\n // using uAmbientColor.rgb crashes SpvBuilder because it assumes it's a single float\n // this case doesn't seem to be found anywhere in Babylon shader so won't fix\n vec3 ambient = /*uAmbientColor.rgb * */albedo;\n \n vec3 lit = ambient + lightAccum * albedo;\n vec3 specular = fresnel * materialSpecular.rgb;\n \n vec4 finalColor = vec4(lit + specular + modeColor * 0.01, tex2d.a * materialDiffuse.a);\n \n // \u2500\u2500 Tone mapping (Reinhard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = finalColor.rgb / (finalColor.rgb + vec3(1.0));\n\n // \u2500\u2500 Gamma correction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = pow(finalColor.rgb, vec3(1.0 / 2.2));\n\n // \u2500\u2500 Write fragment depth (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n gl_FragDepth = fragDepth;\n\n // \u2500\u2500 Output (blend in splat color contribution) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n fragColor = finalColor + vSplatColor * 0.001 + fetchedComplex * 0.0001;\n}\n"; + + + + + + + + + + + + + + + + + + + diff --git a/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts b/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts index bcb62581a..cb1fca550 100644 --- a/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts +++ b/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts @@ -826,6 +826,25 @@ void main() { // ── Texture sampling: texelFetch() ────────────────────────────── ivec2 texCoord = ivec2(gl_FragCoord.xy); vec4 fetched = texelFetch(uSampler2D, texCoord, 0); + + // texelFetch coordinates are flipped vertically by the shader compiler, which has to clone + // the coordinate expression to reference it twice. Cover the operand shapes that appear in + // Babylon shaders beyond a plain symbol: a constructor, a binary expression, a nested + // constructor over a float expression, a nested constructor over integer binaries, built-in + // calls, and a non-constant lod. Integer multiply, divide, modulo and bitwise operators are + // deliberately avoided: Babylon Native builds glslang and SPIRV-Cross in their WEBMIN + // configurations, which do not support them, so they would fail for reasons that have nothing + // to do with texel coordinates. + int fetchIndex = int(gl_FragCoord.x) + int(gl_FragCoord.y); + int fetchLod = fetchIndex - fetchIndex; + ivec2 fetchSize = textureSize(uSampler2D, 0); + vec4 fetchedCtor = texelFetch(uSampler2D, ivec2(gl_FragCoord.xy), 0); + vec4 fetchedOffset = texelFetch(uSampler2D, texCoord + ivec2(1, 1), 0); + vec4 fetchedScaled = texelFetch(uSampler2D, ivec2(vUV * vec2(fetchSize)), 0); + vec4 fetchedNested = texelFetch(uSampler2D, ivec2(fetchSize.x - texCoord.x, fetchSize.y - texCoord.y), 0); + vec4 fetchedCall = texelFetch(uSampler2D, ivec2(abs(texCoord.x), abs(texCoord.y)), 0); + vec4 fetchedLod = texelFetch(uSampler2D, texCoord.yx, fetchLod); + vec4 fetchedComplex = fetchedCtor + fetchedOffset + fetchedScaled + fetchedNested + fetchedCall + fetchedLod; // ── Texture sampling: textureSize() ───────────────────────────── ivec2 size2d = textureSize(uSampler2D, 0); @@ -951,7 +970,7 @@ void main() { gl_FragDepth = fragDepth; // ── Output (blend in splat color contribution) ──────────────────── - fragColor = finalColor + vSplatColor * 0.001; + fragColor = finalColor + vSplatColor * 0.001 + fetchedComplex * 0.0001; } `; diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h index 88d1ece81..60d3bd69d 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h @@ -56,6 +56,12 @@ namespace Babylon::Graphics void BlitViewId(bgfx::ViewId viewId) { m_blitViewId = viewId; } private: + // Resets every piece of shape metadata to its default. Each Create*/Attach calls this + // before assigning the subset that applies to it, so a field a given path does not set + // (m_depth is only meaningful for Create3D) cannot survive from the previous, differently + // shaped texture this object described. + void ResetMetadata(); + bgfx::TextureHandle m_handle{bgfx::kInvalidHandle}; bool m_ownsHandle{false}; uint16_t m_width{0}; diff --git a/Core/Graphics/Source/Texture.cpp b/Core/Graphics/Source/Texture.cpp index 558afeed6..bdd986273 100644 --- a/Core/Graphics/Source/Texture.cpp +++ b/Core/Graphics/Source/Texture.cpp @@ -43,9 +43,23 @@ namespace Babylon::Graphics return bgfx::isValid(m_handle); } + void Texture::ResetMetadata() + { + m_width = 0; + m_height = 0; + m_depth = 0; + m_hasMips = false; + m_isCube = false; + m_is3D = false; + m_numLayers = 0; + m_format = bgfx::TextureFormat::Enum::Unknown; + m_flags = BGFX_TEXTURE_NONE; + } + void Texture::Create2D(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); // make sure render targets are filled with 0 : https://registry.khronos.org/webgl/specs/latest/1.0/#TEXIMAGE2D const auto* mem = (flags & BGFX_TEXTURE_RT) ? GetZeroImageMemory(width, height, hasMips, numLayers, format) : nullptr; @@ -64,8 +78,6 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; - m_isCube = false; - m_is3D = false; } void Texture::Update2D(uint16_t layer, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -76,6 +88,7 @@ namespace Babylon::Graphics void Texture::Create3D(uint16_t width, uint16_t height, uint16_t depth, bool hasMips, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); m_handle = bgfx::createTexture3D(width, height, depth, hasMips, format, flags); if (!bgfx::isValid(m_handle)) @@ -91,7 +104,6 @@ namespace Babylon::Graphics m_numLayers = 1; m_format = format; m_flags = flags; - m_isCube = false; m_is3D = true; } @@ -103,6 +115,7 @@ namespace Babylon::Graphics void Texture::CreateCube(uint16_t size, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); m_handle = bgfx::createTextureCube(size, hasMips, numLayers, format, flags); m_ownsHandle = true; @@ -113,7 +126,6 @@ namespace Babylon::Graphics m_format = format; m_flags = flags; m_isCube = true; - m_is3D = false; } void Texture::UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -124,6 +136,7 @@ namespace Babylon::Graphics void Texture::Attach(bgfx::TextureHandle handle, bool ownsHandle, uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); assert(bgfx::isValid(handle)); m_handle = handle; @@ -134,8 +147,6 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; - m_isCube = false; - m_is3D = false; } bgfx::TextureHandle Texture::Handle() const diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 695801eaf..270fcae0c 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1764,6 +1764,20 @@ namespace Babylon throw Napi::Error::New(info.Env(), "updateTextureData data size does not match width, height, and texture format"); } + // Block-compressed formats are rejected outright. Their payload is a grid of 4x4 (or larger) + // blocks, so the row-reversal below would be wrong twice over: the stride it derives is + // requiredSize / height, which is a fraction of a block row rather than a whole one (an 8-byte + // BC1 block at 4x4 yields 2), and even with the right stride, mirroring block rows cannot flip a + // texture vertically without re-encoding the texel rows packed inside each block. bgfx also + // requires block-aligned x/y/width/height for these formats. The base upload path + // (loadTexture -> PrepareImage) already handles compressed data, so this is not a capability loss. + // In bgfx's TextureFormat enum every block-compressed format sorts before Unknown, which lets + // this be checked without bimg. + if (texture->Format() < bgfx::TextureFormat::Unknown) + { + throw Napi::Error::New(info.Env(), "updateTextureData does not support block-compressed texture formats"); + } + const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; // Match the vertical orientation the base upload applies (PrepareImage flips the whole image when diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp index 0963a83f5..2c2192c5f 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp @@ -1789,9 +1789,10 @@ namespace Babylon::ShaderCompilerTraversers private: // Post-visit is enabled so that texelFetch coordinates can be rewritten after their - // children have been traversed: the rewrite references the coordinate subtree twice, - // and rewriting it on the way down would make the traverser descend into that subtree - // twice and flip any nested texture() call inside it twice. + // children have been traversed. The rewrite copies the coordinate subtree, and doing + // that on the way down would leave the copy unvisited while the original still got + // flipped, so a nested texture() call inside the coordinate would be flipped in one + // reference but not the other. FlipSamplerCoordinatesTraverser(TIntermediate* intermediate) : TIntermTraverser{true, false, true} , m_intermediate{intermediate} @@ -1833,17 +1834,16 @@ namespace Babylon::ShaderCompilerTraversers { const TSourceLoc& loc{coordinate->getLoc()}; - // The sampler and lod operands are still referenced by the original texelFetch node. - // Reusing the same node pointers inside a second call (textureSize) would give those - // subtrees two parents in the AST, which later traversers (sampler splitting, SPIR-V - // generation) corrupt. Clone them so each reference is an independent node. If either - // operand is something we cannot safely clone, skip the flip rather than risk it. - TIntermTyped* samplerClone{CloneLeaf(sampler)}; - TIntermTyped* lodClone{CloneLeaf(lod)}; - if (samplerClone == nullptr || lodClone == nullptr) - { - return coordinate; - } + // Every operand referenced more than once below has to be an independent subtree. + // Reusing a node pointer would give it two parents in the AST, which later traversers + // (sampler splitting, SPIR-V generation) do not expect. The sampler and lod are each + // referenced twice because textureSize repeats them, and the coordinate is referenced + // twice because the flip reads both .x and .y, so all three need a copy. The original + // node is kept for one reference and the clone used for the other, leaving every node + // with exactly one parent. + TIntermTyped* samplerClone{CloneExpression(sampler)}; + TIntermTyped* lodClone{CloneExpression(lod)}; + TIntermTyped* coordinateClone{CloneExpression(coordinate)}; TType ivec2Type{EbtInt, EvqTemporary, 2}; TType intType{EbtInt, EvqTemporary, 1}; @@ -1858,10 +1858,11 @@ namespace Babylon::ShaderCompilerTraversers sizeY->setType(intType); TIntermTyped* maxY{m_intermediate->addBinaryMath(EOpSub, sizeY, m_intermediate->addConstantUnion(1, loc), loc)}; - // coordinate.x and coordinate.y + // coordinate.x and coordinate.y. The clone supplies the second reference so that + // neither subtree ends up with two parents. TIntermTyped* coordinateX{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(0, loc), loc)}; coordinateX->setType(intType); - TIntermTyped* coordinateY{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(1, loc), loc)}; + TIntermTyped* coordinateY{m_intermediate->addIndex(EOpIndexDirect, coordinateClone, m_intermediate->addConstantUnion(1, loc), loc)}; coordinateY->setType(intType); // ivec2(coordinate.x, (textureSize(sampler, lod).y - 1) - coordinate.y) @@ -1871,19 +1872,70 @@ namespace Babylon::ShaderCompilerTraversers return m_intermediate->setAggregateOperator(flipped, EOpConstructIVec2, ivec2Type, loc); } - // Produces an independent copy of a leaf operand (a sampler symbol or a constant lod) so - // it can be referenced from a second call site without aliasing the original AST node. - TIntermTyped* CloneLeaf(TIntermTyped* node) + // Produces an independent copy of an expression subtree so it can be referenced from a + // second call site without giving any original node two parents in the AST. + // + // The clone is structural: each node's operator, type and source location are copied + // verbatim rather than rebuilt through TIntermediate::add*, so no constant folding, + // type promotion or precision inference can make the copy diverge from the original. + // + // Anything not reachable in a texel coordinate expression (ternaries, array methods) + // is reported rather than silently skipped -- returning the coordinate unflipped would + // sample with an un-flipped Y and produce a wrong image with no diagnostic at all. + TIntermTyped* CloneExpression(TIntermTyped* node) { + if (node == nullptr) + { + throw std::runtime_error{"FlipSamplerCoordinates: missing operand in texelFetch."}; + } + if (TIntermSymbol* symbol = node->getAsSymbolNode()) { return m_intermediate->addSymbol(*symbol); } + if (TIntermConstantUnion* constant = node->getAsConstantUnion()) { return m_intermediate->addConstantUnion(constant->getConstArray(), constant->getType(), node->getLoc()); } - return nullptr; + + if (TIntermBinary* binary = node->getAsBinaryNode()) + { + auto* clone = new TIntermBinary{binary->getOp()}; + clone->setLeft(CloneExpression(binary->getLeft())); + clone->setRight(CloneExpression(binary->getRight())); + clone->setType(binary->getType()); + clone->setLoc(binary->getLoc()); + return clone; + } + + if (TIntermUnary* unary = node->getAsUnaryNode()) + { + auto* clone = new TIntermUnary{unary->getOp()}; + clone->setOperand(CloneExpression(unary->getOperand())); + clone->setType(unary->getType()); + clone->setLoc(unary->getLoc()); + return clone; + } + + if (TIntermAggregate* aggregate = node->getAsAggregate()) + { + auto* clone = new TIntermAggregate{aggregate->getOp()}; + for (TIntermNode* child : aggregate->getSequence()) + { + clone->getSequence().push_back(CloneExpression(child == nullptr ? nullptr : child->getAsTyped())); + } + clone->setType(aggregate->getType()); + clone->setLoc(aggregate->getLoc()); + clone->setName(aggregate->getName()); + if (aggregate->isUserDefined()) + { + clone->setUserDefined(); + } + return clone; + } + + throw std::runtime_error{"FlipSamplerCoordinates: unsupported expression in a texelFetch operand; cannot flip the texel coordinate."}; } TIntermediate* m_intermediate{};