From a08fa49c436ffd1e0acadeddf2a74ff9865d5970 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 2 Jun 2026 09:00:13 -0700 Subject: [PATCH 1/6] Auto-populate is2DArray/depth in wrapNativeTexture from layer count Babylon Native used to require consumers to thread a texture's layer count through their own bridge to detect when a wrapped native texture was a Texture2DArray, because `wrapNativeTexture` only read width and height from the engine binding. Defaulting `is2DArray` and `depth` to unset/0 caused downstream code that picks a sampler variant by `InternalTexture.is2DArray` to mismatch the bgfx SRV view dimension and hit undefined behavior on some D3D11 drivers. With the matching `getTextureLayerCount` binding on the Babylon Native side, `wrapNativeTexture` can populate `is2DArray` and `depth` itself, removing the host-side plumbing. Also tighten `updateWrappedNativeTexture` to validate that the new handle's layer count matches the wrapped texture's recorded layer count, matching the existing dimension check. Requires the matching getTextureLayerCount binding in Babylon Native; older Babylon Native builds throw on the missing method. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dev/core/src/Engines/Native/nativeInterfaces.ts | 1 + .../dev/core/src/Engines/thinNativeEngine.pure.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index d2e96383a99..79849f407c0 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -69,6 +69,7 @@ export interface INativeEngine { loadCubeTextureWithMips(texture: NativeTexture, data: Array>, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; getTextureWidth(texture: NativeTexture): number; getTextureHeight(texture: NativeTexture): number; + getTextureLayerCount(texture: NativeTexture): number; deleteTexture(texture: NativeTexture): void; readTexture( texture: NativeTexture, diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index 23d0f45ffd9..ede12d5e295 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -1993,6 +1993,11 @@ export class ThinNativeEngine extends ThinEngine { internalTexture.baseHeight = this._engine.getTextureHeight(texture); internalTexture.width = internalTexture.baseWidth; internalTexture.height = internalTexture.baseHeight; + const layerCount = this._engine.getTextureLayerCount(texture); + if (layerCount > 1) { + internalTexture.is2DArray = true; + internalTexture.depth = layerCount; + } internalTexture.isReady = true; internalTexture.useMipMaps = hasMipMaps; this.updateTextureSamplingMode(samplingMode, internalTexture); @@ -2031,6 +2036,13 @@ export class ThinNativeEngine extends ThinEngine { `updateWrappedNativeTexture: new handle dimensions (${newWidth}x${newHeight}) must match the wrapped texture's dimensions (${internalTexture.baseWidth}x${internalTexture.baseHeight}).` ); } + const newLayerCount = this._engine.getTextureLayerCount(texture); + const oldLayerCount = internalTexture.is2DArray ? internalTexture.depth : 1; + if (newLayerCount !== oldLayerCount) { + throw new Error( + `updateWrappedNativeTexture: new handle layer count (${newLayerCount}) must match the wrapped texture's layer count (${oldLayerCount}).` + ); + } // Pre-validate before mutating any state so a thrown precondition leaves the InternalTexture untouched. // Note: rtWrapper.texture only returns _textures[0]; walk every attachment to catch the multi-RT case where From 385917502031e9745d262d7fe1318a88b330fb85 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 4 Jun 2026 08:29:26 -0700 Subject: [PATCH 2/6] Make getTextureLayerCount usage backward-compatible 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> --- .../src/Engines/Native/nativeInterfaces.ts | 7 +++++- .../core/src/Engines/thinNativeEngine.pure.ts | 24 +++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 79849f407c0..b60e4b81fc9 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -69,7 +69,12 @@ export interface INativeEngine { loadCubeTextureWithMips(texture: NativeTexture, data: Array>, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; getTextureWidth(texture: NativeTexture): number; getTextureHeight(texture: NativeTexture): number; - getTextureLayerCount(texture: NativeTexture): number; + /** + * Returns the number of array layers in a native texture. + * Optional: not present on older Babylon Native builds; callers must + * feature-detect (typeof === "function") before invoking. + */ + getTextureLayerCount?(texture: NativeTexture): number; deleteTexture(texture: NativeTexture): void; readTexture( texture: NativeTexture, diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index ede12d5e295..0f3a609e437 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -1993,10 +1993,12 @@ export class ThinNativeEngine extends ThinEngine { internalTexture.baseHeight = this._engine.getTextureHeight(texture); internalTexture.width = internalTexture.baseWidth; internalTexture.height = internalTexture.baseHeight; - const layerCount = this._engine.getTextureLayerCount(texture); - if (layerCount > 1) { - internalTexture.is2DArray = true; - internalTexture.depth = layerCount; + if (typeof this._engine.getTextureLayerCount === "function") { + const layerCount = this._engine.getTextureLayerCount(texture); + if (layerCount > 1) { + internalTexture.is2DArray = true; + internalTexture.depth = layerCount; + } } internalTexture.isReady = true; internalTexture.useMipMaps = hasMipMaps; @@ -2036,12 +2038,14 @@ export class ThinNativeEngine extends ThinEngine { `updateWrappedNativeTexture: new handle dimensions (${newWidth}x${newHeight}) must match the wrapped texture's dimensions (${internalTexture.baseWidth}x${internalTexture.baseHeight}).` ); } - const newLayerCount = this._engine.getTextureLayerCount(texture); - const oldLayerCount = internalTexture.is2DArray ? internalTexture.depth : 1; - if (newLayerCount !== oldLayerCount) { - throw new Error( - `updateWrappedNativeTexture: new handle layer count (${newLayerCount}) must match the wrapped texture's layer count (${oldLayerCount}).` - ); + if (typeof this._engine.getTextureLayerCount === "function") { + const newLayerCount = this._engine.getTextureLayerCount(texture); + const oldLayerCount = internalTexture.is2DArray ? internalTexture.depth : 1; + if (newLayerCount !== oldLayerCount) { + throw new Error( + `updateWrappedNativeTexture: new handle layer count (${newLayerCount}) must match the wrapped texture's layer count (${oldLayerCount}).` + ); + } } // Pre-validate before mutating any state so a thrown precondition leaves the InternalTexture untouched. From dbd66583b0878789dd8a27d5a8146e53bb7e8705 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 4 Jun 2026 08:30:49 -0700 Subject: [PATCH 3/6] Use truthy check instead of typeof for getTextureLayerCount presence Optional method on the interface is either a function (truthy) or undefined (falsy); the typeof === 'function' guard adds no value over a plain truthy check. [Created by Copilot on behalf of @bghgary] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/dev/core/src/Engines/Native/nativeInterfaces.ts | 2 +- packages/dev/core/src/Engines/thinNativeEngine.pure.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index b60e4b81fc9..14030aa0124 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -72,7 +72,7 @@ export interface INativeEngine { /** * Returns the number of array layers in a native texture. * Optional: not present on older Babylon Native builds; callers must - * feature-detect (typeof === "function") before invoking. + * check for presence before invoking. */ getTextureLayerCount?(texture: NativeTexture): number; deleteTexture(texture: NativeTexture): void; diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index 0f3a609e437..395a25fb6e4 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -1993,7 +1993,7 @@ export class ThinNativeEngine extends ThinEngine { internalTexture.baseHeight = this._engine.getTextureHeight(texture); internalTexture.width = internalTexture.baseWidth; internalTexture.height = internalTexture.baseHeight; - if (typeof this._engine.getTextureLayerCount === "function") { + if (this._engine.getTextureLayerCount) { const layerCount = this._engine.getTextureLayerCount(texture); if (layerCount > 1) { internalTexture.is2DArray = true; @@ -2038,7 +2038,7 @@ export class ThinNativeEngine extends ThinEngine { `updateWrappedNativeTexture: new handle dimensions (${newWidth}x${newHeight}) must match the wrapped texture's dimensions (${internalTexture.baseWidth}x${internalTexture.baseHeight}).` ); } - if (typeof this._engine.getTextureLayerCount === "function") { + if (this._engine.getTextureLayerCount) { const newLayerCount = this._engine.getTextureLayerCount(texture); const oldLayerCount = internalTexture.is2DArray ? internalTexture.depth : 1; if (newLayerCount !== oldLayerCount) { From f4f9e195be7f61f0e1ae84a1872a549ac873b421 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 16 Jun 2026 14:45:59 -0700 Subject: [PATCH 4/6] Set baseDepth on wrapped 2D-array textures and collapse long throw - wrapNativeTexture: set baseDepth alongside depth so a wrapped Texture2DArray reports its initial depth consistently with the rest of the engine (baseDepth = depth = layerCount). - updateWrappedNativeTexture: put the layer-count mismatch error on a single line to satisfy prettier (printWidth 180). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/dev/core/src/Engines/thinNativeEngine.pure.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index 395a25fb6e4..1fb39839bb3 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -1997,7 +1997,7 @@ export class ThinNativeEngine extends ThinEngine { const layerCount = this._engine.getTextureLayerCount(texture); if (layerCount > 1) { internalTexture.is2DArray = true; - internalTexture.depth = layerCount; + internalTexture.baseDepth = internalTexture.depth = layerCount; } } internalTexture.isReady = true; @@ -2042,9 +2042,7 @@ export class ThinNativeEngine extends ThinEngine { const newLayerCount = this._engine.getTextureLayerCount(texture); const oldLayerCount = internalTexture.is2DArray ? internalTexture.depth : 1; if (newLayerCount !== oldLayerCount) { - throw new Error( - `updateWrappedNativeTexture: new handle layer count (${newLayerCount}) must match the wrapped texture's layer count (${oldLayerCount}).` - ); + throw new Error(`updateWrappedNativeTexture: new handle layer count (${newLayerCount}) must match the wrapped texture's layer count (${oldLayerCount}).`); } } From 961b41941d660517ce06d0545e96a2fb56382062 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 16 Jun 2026 14:49:11 -0700 Subject: [PATCH 5/6] Drop redundant JSDoc on getTextureLayerCount Match the surrounding INativeEngine members (getTextureWidth/Height etc.), which carry no doc comments; the optional `?` already conveys availability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/dev/core/src/Engines/Native/nativeInterfaces.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 14030aa0124..7ba87f981c7 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -69,11 +69,6 @@ export interface INativeEngine { loadCubeTextureWithMips(texture: NativeTexture, data: Array>, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; getTextureWidth(texture: NativeTexture): number; getTextureHeight(texture: NativeTexture): number; - /** - * Returns the number of array layers in a native texture. - * Optional: not present on older Babylon Native builds; callers must - * check for presence before invoking. - */ getTextureLayerCount?(texture: NativeTexture): number; deleteTexture(texture: NativeTexture): void; readTexture( From ffad9e4e24e051e13ca6f6ef450bb15438ea1595 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Wed, 17 Jun 2026 14:32:00 -0700 Subject: [PATCH 6/6] Re-trigger CI (transient npm ECONNRESET on prior run) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>