Skip to content

NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 7 tests)#1748

Open
bkaradzic-microsoft wants to merge 4 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:native-cube-dds-ktx-loading
Open

NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 7 tests)#1748
bkaradzic-microsoft wants to merge 4 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:native-cube-dds-ktx-loading

Conversation

@bkaradzic-microsoft

@bkaradzic-microsoft bkaradzic-microsoft commented Jun 10, 2026

Copy link
Copy Markdown
Member

NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + compute spherical harmonics

Status

What

Adds native support for loading a self-contained cubemap container (.dds / .ktx / .ktx2,
the format produced by CubeTexture.CreateFromPrefilteredData(...)) and re-enables six PBR
environment validation tests.

This PR also folds in #1747 (re-enabling blur-cube-with-the-effect-renderer) — see
"Folded-in change" below — so seven validation tests are re-enabled in total.

Plugins/NativeEngine/Source/NativeEngine.cpp:

  • loadCubeTexture now accepts a single buffer (all six faces + mips in one container).
    ParseCubeImage runs it through bimg::imageParse (which decodes DDS/KTX/KTX2 cubemaps), and
    LoadCubeTextureFromContainer uploads sides 0–5 × mips.
  • ComputeCubeSphericalPolynomial derives the diffuse-IBL spherical harmonics from the top-mip
    faces (a port of Babylon's CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial)
    and returns the 9×3 polynomial coefficients to JS.

Apps/Playground/Scripts/config.json: re-enables the six tests this unblocks (NMEGLTF,
Anisotropic, Clear Coat, PBRMetallicRoughnessMaterial, PBRSpecularGlossinessMaterial, PBR),
plus blur-cube-with-the-effect-renderer (folded-in #1747).

Why this shape

These scenes load a prefiltered environment via a single .dds. WebGL gets the cube faces and
computes the spherical harmonics on the CPU through the texture-loader path. On native that path
is unavailable: _uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly and cube
_readTexturePixels are unimplemented stubs. The .dds also stores no SH (Babylon computes it from
the faces). So the SH must be computed from the decoded faces, in native C++, alongside the upload —
no .env swap, no JS polyfill, no GPU readback.

Folded-in change (was #1747)

Re-enables blur-cube-with-the-effect-renderer. The test rendered an EffectRenderer fullscreen
pass that produced an all-black frame on native: EffectRenderer disables depth via
engine.depthCullingState.depthTest = false, but the native engine only honored depth state set
through the explicit setDepthBuffer() command and never flushed depthCullingState at draw time,
so the fullscreen quad kept the previous draw's depth test and every fragment was discarded. Fixed
on the Babylon.js side (see paired changes). Combined here because it's another cube validation
re-enable gated on a paired Babylon.js dependency bump that requires the same full rebuild.

Paired Babylon.js changes

Draft: depends on a babylonjs dependency bump that includes both #18560 and #18558. CI goes
green once those are in the bundled babylonjs. Verified locally against from-source
babylon.max.js builds containing each change.

Verification (local)

  • Against a from-source babylon.max.js built with #18560, the six cube-environment tests run via
    the normal path (no --include-excluded) and pass, matching their reference images. The SH
    integration is correct (total solid angle = 4π).
  • blur-cube-with-the-effect-renderer passes against a babylon.max.js patched with #18558
    (Test 'blur-cube-with-the-effect-renderer' validated). On the unpatched bundled engine it still
    renders all-black, as expected, until the dependency bump lands.
  • Scope note: Prepass SSAO + depth of field also references a .dds env — its cube now loads, but
    it still fails on unrelated SSAO/DoF/prepass differences, so it remains excluded (separate issue).

Related PRs & landing order

Co-dependent; land in this order:

  1. Babylon.js #18560 first — adds the single-URL container cube dispatch + spherical-harmonics wiring; no WebGL behavior change.
  2. A babylonjs npm release ships that TS change.
  3. BabylonNative NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 7 tests) #1748 last — bumps the bundled babylonjs and re-enables the 6 prefiltered-.dds PBR validation tests, which only pass once the paired JS is present in the bundled engine. The folded-in blur-cube-with-the-effect-renderer test additionally requires [Native] Honor depthCullingState.depthTest on the native engine Babylon.js#18558 to be in the same babylonjs release.

@bkaradzic-microsoft bkaradzic-microsoft force-pushed the native-cube-dds-ktx-loading branch from 1649a36 to 383b8a3 Compare June 12, 2026 22:35
@bkaradzic-microsoft bkaradzic-microsoft changed the title NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 6 PBR tests) NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 7 tests) Jun 12, 2026
@bkaradzic-microsoft bkaradzic-microsoft marked this pull request as ready for review June 12, 2026 22:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds native support in Babylon Native’s NativeEngine for loading self-contained cubemap containers (.dds / .ktx / .ktx2) and computing diffuse-IBL spherical harmonics on the native side, enabling previously excluded environment/PBR validation tests to run again.

Changes:

  • Added cubemap-container parsing and upload path for single-buffer cube textures in NativeEngine::LoadCubeTexture.
  • Implemented CPU spherical harmonics computation from decoded top-mip cubemap faces and returned coefficients back to JS.
  • Re-enabled 7 Playground validation tests by removing excludeFromAutomaticTesting entries.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
Plugins/NativeEngine/Source/NativeEngine.cpp Adds single-buffer cubemap container loading, uploads all faces/mips, and computes spherical-harmonics coefficients for diffuse IBL.
Apps/Playground/Scripts/config.json Re-enables previously excluded validation tests now unblocked by the native cubemap+SH support (plus the EffectRenderer blur-cube test).

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 802dd1e. The single-buffer path's inline continuation (which calls LoadCubeTextureFromContainer and touches texture/bgfx) now captures asyncTaskScope{TrackAsyncTask()}, matching the multi-buffer path, so Dispose() drains it before teardown frees the graphics resources.

Comment on lines +634 to +654
const uint8_t numMips{static_cast<uint8_t>(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<bimg::ImageContainer*>(userData));
};
}

const bgfx::Memory* mem{bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)};
texture->UpdateCube(0, side, mip, 0, 0, static_cast<uint16_t>(imageMip.m_width), static_cast<uint16_t>(imageMip.m_height), mem);
}
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 802dd1e. LoadCubeTextureFromContainer now validates that every (side, mip) is present up front and fails fast — freeing the container and throwing — before any upload. That guarantees the single release callback on the last (side 5, last mip) upload always fires, so a truncated/malformed container can no longer leak the ImageContainer or leave the texture partially initialized.

@bghgary bghgary left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Reviewed by Copilot on behalf of @bghgary]

This extends the existing native createCubeTexture override to single-file .dds/.ktx/.ktx2 and computes the diffuse-IBL spherical harmonics in C++. The cube-container loading is consistent with how native already bypasses the texture loaders (.env, six-file). The SH computation, though, is a ~150-line C++ port of CubeMapToSphericalPolynomialTools that has to stay in lockstep with the JS original — new, drift-prone duplication, and the bulk of this PR.

This is the texture-loader bypass tracked in #218. Native can't reuse Babylon's _DDSTextureLoader (which parses on CPU and computes the same SH in JS — no GPU readback) only because of the unimplemented _uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly stubs. Native already has the GPU-upload machinery (Create2D/Update2D/CreateCube/UpdateCube + makeRef), so implementing those as a thin wrapper would light up the shared JS loader path — DDS/KTX/ENV + SH in one place, WebGL parity, future formats free — and delete the C++ SH port instead of maintaining it.

I'd rather not absorb the C++ SH duplication as a stopgap just to re-enable test coverage. Can we scope the _uploadDataToTextureDirectly route (#218) instead?

@bkaradzic-microsoft

Copy link
Copy Markdown
Member Author

@bghgary you're right, and digging into it the payoff is bigger than I'd scoped. I checked the bundled engine:

  • The only thing standing in the way is the two stubs in thinNativeEngine.pure.js (_uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly) that just throw new Error("...not implemented"). WebGL/WebGPU implement both.
  • The JS _DDSTextureLoader already does the whole thing on CPU and computes the SH in JS with no GPU readback — dds.pure.js:520 calls CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial(...) after uploading the faces. So the ~150-line C++ port really is redundant duplication of that, and implementing the two stubs lets me delete it.

And it's not just DDS/SH. The same two methods are the upload sink for the shared loader path used by DDS, KTX, KTX2, Basis, IES, HDR, EXR, TGA. Concretely, implementing them also fixes a batch of currently-excluded native hangs/failures that I separately traced to exactly these throwing stubs:

  • IES Profile / IES Profile2iesTextureLoader.js:36_uploadDataToTextureDirectly → throws → the texture onLoad never fires → createScene Promise never resolves → hang.
  • Basis loaderbasis.pure.js:211 → same throw ("...not implemented").

So #218 is the right move: implement _uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly as thin wrappers over the existing Update2D/UpdateCube + makeRef machinery, light up the shared JS loader path (DDS/KTX/ENV/SH in one place, WebGL parity, future formats free), and drop the C++ SH port and the bespoke single-file cube path here.

I'll re-scope this PR to the #218 route. The folded-in blur-cube-with-the-effect-renderer re-enable is independent (it's the depthCullingState.depthTest fix paired with BabylonJS/Babylon.js#18558) — I'll split that back out so it isn't blocked behind the loader rework.

@bkaradzic-microsoft

Copy link
Copy Markdown
Member Author

Split the folded-in blur-cube-with-the-effect-renderer re-enable out into its own PR so it isn't blocked behind this PR's loader rework: #1763 (it's the depthCullingState.depthTest fix paired with BabylonJS/Babylon.js#18558, independent of the loader work). Drop the blur-cube commit here when re-scoping.

bkaradzic and others added 4 commits June 18, 2026 08:22
…armonics

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.

Re-enables 6 prefiltered-environment PBR validation tests.

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>
This EffectRenderer fullscreen-pass test rendered all-black on Native because the
native engine did not honor depth-test toggles made through
engine.depthCullingState.depthTest (used by EffectRenderer). Fixed upstream in the
native engine (BabylonJS/Babylon.js#18558); re-enable the test here.

Depends on a babylonjs dependency bump that includes the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…faces/mips

- loadCubeTexture single-buffer path: capture TrackAsyncTask() into the inline
  continuation that uploads to bgfx, so NativeEngine::Dispose() drains it before
  teardown frees the graphics resources it touches (matches the multi-buffer path).
- LoadCubeTextureFromContainer: validate every (side, mip) is present before
  uploading and fail fast (freeing the container) if not, so the single release
  callback attached to the last upload always fires - no leak or partially
  initialized texture on a truncated container.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… upload loops

Use the same or (uint8_t mip = 0, numMips = image->m_numMips; ...) idiom as LoadTextureFromImage / LoadCubeTextureFromImages instead of a standalone cached local, for consistency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bkaradzic-microsoft bkaradzic-microsoft force-pushed the native-cube-dds-ktx-loading branch from 6cfb04b to ea0d1eb Compare June 18, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants