Skip to content

Expose bound texture layer count via NativeEngine.getTextureLayerCount - #1733

Open
bghgary wants to merge 3 commits into
BabylonJS:masterfrom
bghgary:expose-texture-layer-count
Open

Expose bound texture layer count via NativeEngine.getTextureLayerCount#1733
bghgary wants to merge 3 commits into
BabylonJS:masterfrom
bghgary:expose-texture-layer-count

Conversation

@bghgary

@bghgary bghgary commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Change

Adds NativeEngine::GetTextureLayerCount(handle) so wrapNativeTexture can populate is2DArray/depth on a wrapped Texture2DArray (today they stay at 2D defaults). Returns the bound layer count — Texture::ViewNumLayers() for a single-slice view (e.g. an ExternalTexture wrapped with a layerIndex), else Texture::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 three ExternalTextures end to end and asserts the InternalTexture:

  • whole array (arraySize 2, no layerIndex) → is2DArray, depth=baseDepth=2
  • single slice (arraySize 2, layerIndex 0) → plain 2D (one NV12 array slice)
  • single layer (arraySize 1) → plain 2D

Skips when the BJS consumer is absent, staying green across the cross-repo landing order.

[Created by Copilot on behalf of @bghgary]

bghgary added a commit to bghgary/Babylon.js that referenced this pull request Jun 4, 2026
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>
@bghgary
bghgary force-pushed the expose-texture-layer-count branch from f9ca23c to efa0d70 Compare June 17, 2026 16:23
@bghgary bghgary changed the title Expose Texture::NumLayers via NativeEngine.getTextureLayerCount Expose bound texture layer count via NativeEngine.getTextureLayerCount Jun 17, 2026
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>
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>
@bghgary
bghgary marked this pull request as ready for review August 14, 2026 21:35
@bghgary
bghgary requested review from bkaradzic-microsoft and ryantrem and a lite review from Copilot and removed request for Copilot August 14, 2026 21:35

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 bkaradzic-microsoft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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.

3 participants