Expose bound texture layer count via NativeEngine.getTextureLayerCount - #1733
Expose bound texture layer count via NativeEngine.getTextureLayerCount#1733bghgary wants to merge 3 commits into
Conversation
Mark `INativeEngine.getTextureLayerCount` optional and feature-detect (`typeof === "function"`) before calling, so older Babylon Native builds that don't expose the binding keep working: - `wrapNativeTexture`: if the binding is absent, skip auto-populating `is2DArray` / `depth`. The wrapped InternalTexture stays at the defaults, matching pre-existing behavior. - `updateWrappedNativeTexture`: if the binding is absent, skip the layer-count validation. Dimensions are still validated. Removes the runtime dependency on BabylonJS/BabylonNative#1733, so this PR can land independently. Hosts running on an updated native engine get the auto-detect for free; hosts on older native engines see no change. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Babylon.js side wraps native textures with engine.wrapNativeTexture and sets InternalTexture properties from the engine bindings. Today the binding only exposes width and height, so consumers cannot detect that a wrapped texture is a Texture2DArray and InternalTexture.is2DArray / .depth stay at their defaults. Add getTextureLayerCount so Babylon.js can populate is2DArray and depth on the wrapped InternalTexture automatically. It returns the bound layer count: ViewNumLayers() when the texture is wrapped as a single-slice view (e.g. an ExternalTexture created with a layerIndex), otherwise NumLayers(). This lets wrapNativeTexture distinguish a whole-array wrap (is2DArray) from a single slice of an array texture (a plain 2D texture), so a single-slice view is not misreported as an array. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f9ca23c to
efa0d70
Compare
Exercises the cross-repo path end to end: getTextureLayerCount feeds Babylon.js wrapNativeTexture (BabylonJS/Babylon.js#18535), which sets is2DArray / depth / baseDepth when a wrapped texture presents more than one layer. Wraps three ExternalTextures and asserts the resulting InternalTexture: - whole array (arraySize 2, no layerIndex) -> is2DArray, depth=baseDepth=2 - single slice (arraySize 2, layerIndex 0) -> plain 2D (one slice of an NV12 array) - single layer (arraySize 1) -> plain 2D The single-slice case guards against regressing a single array slice into a 2D array. Skips cleanly when the native binding or the consuming Babylon.js change is absent so it stays green across the cross-repo landing order. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
efa0d70 to
7aa2d93
Compare
Brings Babylon.js 9.15.0, which carries the consuming wrapNativeTexture change (BabylonJS/Babylon.js#18535). Until now this branch pinned 9.9.1, which predates that change, so ExternalTexture.WrapNativeTextureLayerCount took its skip path and its assertions never ran in CI. Verified locally on Windows/D3D11 Debug from clean committed state: the test now runs and passes, and the full UnitTests suite is 22/22 with zero skips (previously 17 passed + 1 skipped). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Not for this PR, but we made this WebPack to align with BJS, but BJS has moved to Rollup, so we really should do the same here.
bkaradzic-microsoft
left a comment
There was a problem hiding this comment.
Read the diff along with the surrounding Graphics::Texture / ExternalTexture machinery. The core change looks correct and well scoped: 12 lines of production code, shaped exactly like its sibling getters, with complete build wiring (CMake TEST_ASSETS + SOURCES, webpack entry). The viewNumLayers > 0 ? viewNumLayers : NumLayers() reduction is sound given ViewNumLayers is only ever assigned 0 or 1, at ExternalTexture_Shared.h:61 and ExternalTexture_Base.h:88.
Four observations, roughly in order of how much they matter. Only the third is one I would actually hold the PR for, and it is a reorder.
1. The reported value is mutable, but the consumer snapshots it
getTextureLayerCount reads ViewNumLayers(), which is not fixed at creation — ExternalTexture::Update(ptr, overrideFormat, layerIndex) rewrites it on already-wrapped textures via UpdateTextures (ExternalTexture_Base.h:87-88). wrapNativeTexture, though, resolves is2DArray / depth / baseDepth once at wrap time and bakes them into the InternalTexture, and nothing re-notifies JS afterwards.
So a host can wrap with layerIndex = 0 (→ is2DArray = false, plain 2D), then later call Update(..., std::nullopt), which flips ViewNumLayers 1 → 0. Native then binds the whole array while the JS InternalTexture still describes a plain 2D texture, and nothing detects the divergence.
Not necessarily something to fix here, but it is an implicit contract ("wrap after the last Update, and re-wrap if the layer selection changes") that is currently written down nowhere. A sentence on GetTextureLayerCount would probably save someone a confusing debugging session.
2. UpdateTextures applies one layerIndex to every registered texture
This one is pre-existing, not introduced by this PR, but it bears directly on the scenario used to justify the design.
CreateForJavaScript(env, layerIndex) lets several Graphics::Textures be created from a single ExternalTexture with different layer indices — which is exactly the "host wraps one slice of an NV12 array per video plane" case cited in the description. But UpdateTextures loops over all of m_textures and applies the single layerIndex from the Update call to every one of them:
for (auto* texture : m_textures)
{
...
texture->ViewFirstLayer(layerIndex.value_or(0));
texture->ViewNumLayers(layerIndex.has_value() ? 1 : 0);
}After the first Update, both planes point at the same slice. The layer selection looks like it wants to be per-texture state captured at CreateTexture time rather than a per-Update argument. Worth a follow-up issue if you agree.
3. The skip ordering hides the logic this PR actually adds
In Tests.ExternalTexture.LayerCount.cpp, the BJS-capability skip sits between case (A) and cases (B)/(C):
if (arrayInfo.rawLayerCount == 2 && !arrayInfo.is2DArray)
{
GTEST_SKIP() << "wrapNativeTexture did not populate is2DArray ... requires ... #18535.";
}Cases (B) and (C) are the only ones asserting rawLayerCount == 1, i.e. the single-slice ViewNumLayers branch that is the novel part of this change. Because the skip precedes them, any time the BJS side is absent or regressed, the native-only assertions silently disappear too — the branch most worth protecting is the least protected.
Running all three inspect() calls first, asserting the three rawLayerCounts unconditionally, and then guarding only the is2DArray / depth / baseDepth assertions behind the capability check would keep the native contract covered regardless of which @babylonjs/core resolves.
Related, and the reason I would not leave this as-is: the skip infers "BJS too old" from the assertion outcome, so it cannot distinguish that from "BJS regressed". With "@babylonjs/core": "^9.3.4" floating to the latest 9.x, a future regression in wrapNativeTexture would turn this test green-by-skip rather than red. Keying the skip off an explicit capability probe would make it fail for the right reasons.
4. Only meaningful for 2D textures (minor)
CreateCube sets m_numLayers = numLayers (so a cube array reports its array size, and a consumer applying the > 1 → is2DArray rule would flag a cube as a 2D array), and Create3D sets m_numLayers = 1 while the meaningful extent lives in Depth(). Neither is reachable through ExternalTexture, which always goes through createTexture2D — but getTextureLayerCount is a general engine binding and nothing stops a future caller from handing it a cube or 3D handle. A short comment scoping it to 2D/2D-array textures would be enough.
One thing I deliberately would not change: the absent null/argument check. GetTextureWidth and GetTextureHeight are identical in that respect, and a lone validating outlier among them would be worse than the consistent status quo. If that pattern is ever worth revisiting it should be done across all three at once.
Change
Adds
NativeEngine::GetTextureLayerCount(handle)sowrapNativeTexturecan populateis2DArray/depthon a wrappedTexture2DArray(today they stay at 2D defaults). Returns the bound layer count —Texture::ViewNumLayers()for a single-slice view (e.g. anExternalTexturewrapped with alayerIndex), elseTexture::NumLayers()— distinguishing a whole-array wrap (is2DArray) from a single array slice (plain 2D). Pure addition; existing callers unaffected.Single-slice reporting keeps hosts correct: a host that wraps one slice of an NV12 array per video plane needs that plane to stay 2D, not be flagged
is2DArray.Consumer
BJS #18535 consumes this in
wrapNativeTexture.Test
ExternalTexture.WrapNativeTextureLayerCount(UnitTests) wraps threeExternalTextures end to end and asserts theInternalTexture:is2DArray,depth=baseDepth=2Skips when the BJS consumer is absent, staying green across the cross-repo landing order.
[Created by Copilot on behalf of @bghgary]