NativeEngine: load single-file .dds/.ktx/.ktx2 cubemaps + spherical harmonics (re-enables 7 tests)#1748
Conversation
1649a36 to
383b8a3
Compare
There was a problem hiding this comment.
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
excludeFromAutomaticTestingentries.
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) { |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
[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?
|
@bghgary you're right, and digging into it the payoff is bigger than I'd scoped. I checked the bundled engine:
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:
So I'll re-scope this PR to the |
|
Split the folded-in |
…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>
6cfb04b to
ea0d1eb
Compare
NativeEngine: load single-file
.dds/.ktx/.ktx2cubemaps + compute spherical harmonicsStatus
master(base now includes the async-texture UAF fix NativeEngine: fix use-after-free in texture upload mip loop (regression of #1628) #1758).(#18560 and #18558) and stay red on the current bundled
babylonjsuntil those land — theypass against from-source
babylon.max.jsbuilds containing each change (see Verification).What
Adds native support for loading a self-contained cubemap container (
.dds/.ktx/.ktx2,the format produced by
CubeTexture.CreateFromPrefilteredData(...)) and re-enables six PBRenvironment 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:loadCubeTexturenow accepts a single buffer (all six faces + mips in one container).ParseCubeImageruns it throughbimg::imageParse(which decodes DDS/KTX/KTX2 cubemaps), andLoadCubeTextureFromContaineruploads sides 0–5 × mips.ComputeCubeSphericalPolynomialderives the diffuse-IBL spherical harmonics from the top-mipfaces (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 andcomputes the spherical harmonics on the CPU through the texture-loader path. On native that path
is unavailable:
_uploadDataToTextureDirectly/_uploadCompressedDataToTextureDirectlyand cube_readTexturePixelsare unimplemented stubs. The.ddsalso stores no SH (Babylon computes it fromthe faces). So the SH must be computed from the decoded faces, in native C++, alongside the upload —
no
.envswap, no JS polyfill, no GPU readback.Folded-in change (was #1747)
Re-enables
blur-cube-with-the-effect-renderer. The test rendered anEffectRendererfullscreenpass that produced an all-black frame on native:
EffectRendererdisables depth viaengine.depthCullingState.depthTest = false, but the native engine only honored depth state setthrough the explicit
setDepthBuffer()command and never flusheddepthCullingStateat 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
createCubeTexturedispatch for single-URLcontainers and sets
_sphericalPolynomialfrom the returned coefficients (cube-loading change).depthCullingState.depthTestat drawtime (
blur-cube-with-the-effect-rendererfix).Verification (local)
babylon.max.jsbuilt with #18560, the six cube-environment tests run viathe normal path (no
--include-excluded) and pass, matching their reference images. The SHintegration is correct (total solid angle = 4π).
blur-cube-with-the-effect-rendererpasses against ababylon.max.jspatched with #18558(
Test 'blur-cube-with-the-effect-renderer' validated). On the unpatched bundled engine it stillrenders all-black, as expected, until the dependency bump lands.
Prepass SSAO + depth of fieldalso references a.ddsenv — its cube now loads, butit still fails on unrelated SSAO/DoF/prepass differences, so it remains excluded (separate issue).
Related PRs & landing order
Co-dependent; land in this order:
babylonjsnpm release ships that TS change.babylonjsand re-enables the 6 prefiltered-.ddsPBR validation tests, which only pass once the paired JS is present in the bundled engine. The folded-inblur-cube-with-the-effect-renderertest additionally requires [Native] Honor depthCullingState.depthTest on the native engine Babylon.js#18558 to be in the samebabylonjsrelease.