From 1860aea82cc4aa85160f25c48c47f21a3e38564a Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 10 Jun 2026 10:40:38 -0700 Subject: [PATCH 1/6] [Native] Honor depthCullingState.depthTest on the native engine EffectRenderer (and ThinDepthPeelingRenderer) disable depth testing by setting engine.depthCullingState.depthTest directly. On WebGL/WebGPU this is flushed by applyStates() at draw time, but the native engine only honored depth state set through the explicit setDepthBuffer() command and never flushed depthCullingState before a draw, so the toggle was silently dropped. A fullscreen EffectWrapper pass then kept the previous draw's depth test and every fragment was discarded, producing an all-black target. Reconcile depthCullingState.depthTest before each native draw to match the cross-engine contract: setDepthBuffer() keeps the state object authoritative and _flushDepthTestState() emits the command when the direct toggle differs from what was last encoded. No public API change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/src/Engines/thinNativeEngine.pure.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index 2ff13889962..fd858b8239e 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -227,6 +227,7 @@ export class ThinNativeEngine extends ThinEngine { private _frameStats: NativeFrameStats; private _boundBuffersVertexArray: any; private _currentDepthTest: number; + private _depthTestEnabled: boolean; private _stencilTest: boolean; private _stencilMask: number; private _stencilFunc: number; @@ -268,6 +269,7 @@ export class ThinNativeEngine extends ThinEngine { this._frameStats = { gpuTimeNs: Number.NaN }; this._boundBuffersVertexArray = null; this._currentDepthTest = _native.Engine.DEPTH_TEST_LEQUAL; + this._depthTestEnabled = true; this._stencilTest; this._stencilMask = 255; this._stencilFunc = Constants.ALWAYS; @@ -735,6 +737,7 @@ export class ThinNativeEngine extends ThinEngine { return; } // Apply states + this._flushDepthTestState(); this._drawCalls.addCount(1, false); if (instancesCount) { @@ -765,6 +768,7 @@ export class ThinNativeEngine extends ThinEngine { return; } // Apply states + this._flushDepthTestState(); this._drawCalls.addCount(1, false); if (instancesCount) { @@ -1069,11 +1073,31 @@ export class ThinNativeEngine extends ThinEngine { * @param enable defines the state to set */ public override setDepthBuffer(enable: boolean): void { + // Keep the shared depth-culling state in sync so that code paths which toggle + // depth testing through engine.depthCullingState.depthTest (for example + // EffectRenderer.applyEffectWrapper) are also honored on the native side. The + // native draw path does not go through the WebGL applyStates() flush, so the + // value is reconciled in _flushDepthTestState() right before each draw. + this._depthCullingState.depthTest = enable; + this._encodeDepthTest(enable); + } + + private _encodeDepthTest(enable: boolean): void { + this._depthTestEnabled = enable; this._commandBufferEncoder.startEncodingCommand(_native.Engine.COMMAND_SETDEPTHTEST); this._commandBufferEncoder.encodeCommandArgAsUInt32(enable ? this._currentDepthTest : _native.Engine.DEPTH_TEST_ALWAYS); this._commandBufferEncoder.finishEncodingCommand(); } + private _flushDepthTestState(): void { + // Unlike the WebGL engine, the native engine does not call applyStates() before + // a draw, so depth-test toggles made directly on engine.depthCullingState are + // flushed here to match the cross-engine contract. + if (this._depthCullingState.depthTest !== this._depthTestEnabled) { + this._encodeDepthTest(this._depthCullingState.depthTest); + } + } + /** * Gets a boolean indicating if depth writing is enabled * @returns the current depth writing state From 6584e1774c8eb3ddfc49721d7deab31c99b4caad Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 10 Jun 2026 16:00:22 -0700 Subject: [PATCH 2/6] [Native] Load single-file .dds/.ktx/.ktx2 cubemaps on the native engine The native createCubeTexture override only handled a single .env file or six face files; a single self-contained cubemap container (.dds/.ktx/.ktx2, as produced by CubeTexture.CreateFromPrefilteredData) threw "Cannot load cubemap because 6 files were not defined". The generic WebGL loader route is unusable on native (its texture-upload and cube-readback entry points are unimplemented). Route a single-URL container cubemap to engine.loadCubeTexture with the raw buffer; the native engine decodes it (bimg) and, when polynomials are requested, returns spherical-harmonics coefficients computed from the top mip, which are set as the texture's spherical polynomial. loadCubeTexture's onSuccess now optionally carries those coefficients. The .env and six-file paths are unchanged. Pairs with a Babylon Native NativeEngine change implementing the single-buffer cube decode and spherical-harmonics computation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../nativeEngine.cubeTexture.pure.ts | 111 ++++++++++++++---- .../src/Engines/Native/nativeInterfaces.ts | 34 +++++- 2 files changed, 119 insertions(+), 26 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts b/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts index bdb35b2e32e..b37c0b44196 100644 --- a/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts +++ b/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts @@ -4,6 +4,7 @@ import { InternalTextureSource, InternalTexture } from "../../../Materials/Textu import { Texture } from "../../../Materials/Textures/texture.pure"; import { CreateRadianceImageDataArrayBufferViews, GetEnvInfo, UploadEnvSpherical } from "../../../Misc/environmentTextureTools.pure"; import { type IWebRequest } from "../../../Misc/interfaces/iWebRequest"; +import { SphericalPolynomial } from "../../../Maths/sphericalPolynomial"; import { type Scene } from "../../../scene.pure"; import { type Nullable } from "../../../types"; import { Constants } from "../../constants"; @@ -118,34 +119,94 @@ export function RegisterNativeEngineCubeTexture(): void { ); } } else { - if (!files || files.length !== 6) { - throw new Error("Cannot load cubemap because 6 files were not defined"); - } - - // Reorder from [+X, +Y, +Z, -X, -Y, -Z] to [+X, -X, +Y, -Y, +Z, -Z]. - const reorderedFiles = [files[0], files[3], files[1], files[4], files[2], files[5]]; - // eslint-disable-next-line github/no-then - Promise.all(reorderedFiles.map(async (file) => await this._loadFileAsync(file, undefined, true).then((data) => new Uint8Array(data, 0, data.byteLength)))) + if (files && files.length === 6) { + // Reorder from [+X, +Y, +Z, -X, -Y, -Z] to [+X, -X, +Y, -Y, +Z, -Z]. + const reorderedFiles = [files[0], files[3], files[1], files[4], files[2], files[5]]; // eslint-disable-next-line github/no-then - .then(async (data) => { - return await new Promise((resolve, reject) => { - this._engine.loadCubeTexture(texture._hardwareTexture!.underlyingResource, data, !noMipmap, true, texture._useSRGBBuffer, resolve, reject); - }); - }) - // eslint-disable-next-line github/no-then - .then( - () => { - texture.isReady = true; - if (onLoad) { - onLoad(); + Promise.all(reorderedFiles.map(async (file) => await this._loadFileAsync(file, undefined, true).then((data) => new Uint8Array(data, 0, data.byteLength)))) + // eslint-disable-next-line github/no-then + .then(async (data) => { + return await new Promise((resolve, reject) => { + this._engine.loadCubeTexture(texture._hardwareTexture!.underlyingResource, data, !noMipmap, true, texture._useSRGBBuffer, resolve, reject); + }); + }) + // eslint-disable-next-line github/no-then + .then( + () => { + texture.isReady = true; + if (onLoad) { + onLoad(); + } + }, + (error) => { + if (onError) { + onError(`Failed to load cubemap: ${error.message}`, error); + } } - }, - (error) => { - if (onError) { - onError(`Failed to load cubemap: ${error.message}`, error); + ); + } else if (files) { + throw new Error("Cannot load cubemap because 6 files were not defined"); + } else { + // Single self-contained cubemap container (.dds / .ktx / .ktx2) holding all six + // faces and their mip chain. The native engine decodes it with bimg and, when + // polynomials are requested, returns the spherical harmonics it computed from the + // top mip (native cannot read cube faces back from the GPU to derive them lazily). + const onContainerLoaded = (data: ArrayBufferView) => { + this._engine.loadCubeTexture( + texture._hardwareTexture!.underlyingResource, + [data], + !noMipmap, + true, + texture._useSRGBBuffer, + (sphericalPolynomialCoefficients?: Float32Array) => { + if (createPolynomials && sphericalPolynomialCoefficients && sphericalPolynomialCoefficients.length === 27) { + const c = sphericalPolynomialCoefficients; + texture._sphericalPolynomial = SphericalPolynomial.FromArray([ + [c[0], c[1], c[2]], + [c[3], c[4], c[5]], + [c[6], c[7], c[8]], + [c[9], c[10], c[11]], + [c[12], c[13], c[14]], + [c[15], c[16], c[17]], + [c[18], c[19], c[20]], + [c[21], c[22], c[23]], + [c[24], c[25], c[26]], + ]); + } + texture.isReady = true; + if (onLoad) { + onLoad(); + } + }, + () => { + if (onError) { + onError("Could not load a native cube texture."); + } } - } - ); + ); + }; + + if (buffer) { + onContainerLoaded(buffer); + } else { + const onInternalError = (request?: IWebRequest, exception?: any) => { + if (onError && request) { + onError(request.status + " " + request.statusText, exception); + } + }; + + this._loadFile( + rootUrl, + (data) => { + onContainerLoaded(new Uint8Array(data as ArrayBuffer, 0, (data as ArrayBuffer).byteLength)); + }, + undefined, + undefined, + true, + onInternalError + ); + } + } } this._internalTexturesCache.push(texture); diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index ff6b7a8e656..803f5876786 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -74,7 +74,15 @@ export interface INativeEngine { generateMipMaps: boolean, invertY: boolean ): void; - loadCubeTexture(texture: NativeTexture, data: Array, generateMips: boolean, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; + loadCubeTexture( + texture: NativeTexture, + data: Array, + generateMips: boolean, + invertY: boolean, + srgb: boolean, + onSuccess: (sphericalPolynomial?: Float32Array) => void, + onError: () => void + ): void; loadCubeTextureWithMips(texture: NativeTexture, data: Array>, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; getTextureWidth(texture: NativeTexture): number; getTextureHeight(texture: NativeTexture): number; @@ -433,21 +441,45 @@ export const enum NativeTraceLevel { /** @internal */ export interface INative { // NativeEngine plugin + /** + * + */ Engine: INativeEngineConstructor; + /** + * + */ NativeDataStream: INativeDataStreamConstructor; // NativeCamera plugin + /** + * + */ Camera?: INativeCameraConstructor; // NativeCanvas plugin + /** + * + */ Canvas?: INativeCanvasConstructor; + /** + * + */ Image?: INativeImageConstructor; + /** + * + */ Path2D?: INativePath2DConstructor; // Native XMLHttpRequest polyfill + /** + * + */ XMLHttpRequest?: typeof XMLHttpRequest; // NativeInput plugin + /** + * + */ DeviceInputSystem?: IDeviceInputSystemConstructor; // NativeTracing plugin From 2cd48d22f8c0c2b11efa0f5ac298467dfa313c38 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 10 Jun 2026 16:44:06 -0700 Subject: [PATCH 3/6] Fix native build: wrap loadCubeTexture resolve/reject for the SH-typed onSuccess The six-file path passed the Promise resolve directly, which is not assignable to the widened onSuccess signature (sphericalPolynomial?: Float32Array). Wrap with () => resolve() / () => reject(new Error(...)). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Native/Extensions/nativeEngine.cubeTexture.pure.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts b/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts index b37c0b44196..9fa99a13fd1 100644 --- a/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts +++ b/packages/dev/core/src/Engines/Native/Extensions/nativeEngine.cubeTexture.pure.ts @@ -127,7 +127,15 @@ export function RegisterNativeEngineCubeTexture(): void { // eslint-disable-next-line github/no-then .then(async (data) => { return await new Promise((resolve, reject) => { - this._engine.loadCubeTexture(texture._hardwareTexture!.underlyingResource, data, !noMipmap, true, texture._useSRGBBuffer, resolve, reject); + this._engine.loadCubeTexture( + texture._hardwareTexture!.underlyingResource, + data, + !noMipmap, + true, + texture._useSRGBBuffer, + () => resolve(), + () => reject(new Error("Failed to load native cubemap")) + ); }); }) // eslint-disable-next-line github/no-then From f590bad7dc05692576bd3f0d9b9aab4fd5ba493a Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 10 Jun 2026 21:24:56 -0700 Subject: [PATCH 4/6] [Native] Add cube render target support to the native engine NativeEngine had no createRenderTargetCubeTexture / per-face bindFramebuffer override, so ReflectionProbe and point-light cube shadows fell through to the WebGL path and dereferenced the null _gl context (TEXTURE_CUBE_MAP undefined). - createRenderTargetCubeTexture: native cube color texture + one framebuffer per face (the native side binds the matching cube layer). - bindFramebuffer: bind the per-face framebuffer for cube render targets. - generateMipMapsForCubemap: no-op on Native (bgfx auto-generates the mip chain on render-target resolve, like 2D RTTs). - NativeRenderTargetWrapper: track per-face framebuffers, release on dispose. - nativeInterfaces: thread cube/layer params through initializeTexture and createFrameBuffer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Engines/Native/nativeInterfaces.ts | 15 +++- .../Native/nativeRenderTargetWrapper.ts | 26 +++++- .../core/src/Engines/thinNativeEngine.pure.ts | 81 +++++++++++++++++-- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 803f5876786..2be3b48b9c5 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -61,7 +61,17 @@ export interface INativeEngine { getAttributes(shaderProgram: NativeProgram, attributeNames: string[]): number[]; createTexture(): NativeTexture; - initializeTexture(texture: NativeTexture, width: number, height: number, hasMips: boolean, format: number, renderTarget: boolean, srgb: boolean, samples: number): void; + initializeTexture( + texture: NativeTexture, + width: number, + height: number, + hasMips: boolean, + format: number, + renderTarget: boolean, + srgb: boolean, + samples: number, + isCube?: boolean + ): void; loadTexture(texture: NativeTexture, data: ArrayBufferView, generateMips: boolean, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; loadRawTexture(texture: NativeTexture, data: ArrayBufferView, width: number, height: number, format: number, generateMips: boolean, invertY: boolean): void; loadRawTexture2DArray( @@ -108,7 +118,8 @@ export interface INativeEngine { height: number, generateStencilBuffer: boolean, generateDepthBuffer: boolean, - samples: number + samples: number, + layer?: number ): NativeFramebuffer; getRenderWidth(): number; diff --git a/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts b/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts index f1ba367132a..37df65b7f76 100644 --- a/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts +++ b/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts @@ -11,6 +11,9 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper { private __framebuffer: Nullable = null; // eslint-disable-next-line @typescript-eslint/naming-convention private __framebufferDepthStencil: Nullable = null; + // Per-face framebuffers for cube render targets (index = cube face 0..5). + // eslint-disable-next-line @typescript-eslint/naming-convention + private __framebuffers: Nullable = null; public get _framebuffer(): Nullable { return this.__framebuffer; @@ -23,6 +26,21 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper { this.__framebuffer = framebuffer; } + public get _framebuffers(): Nullable { + return this.__framebuffers; + } + + public set _framebuffers(framebuffers: Nullable) { + if (this.__framebuffers) { + for (const framebuffer of this.__framebuffers) { + this._engine._releaseFramebufferObjects(framebuffer); + } + } + this.__framebuffers = framebuffers; + // Keep _framebuffer pointing at face 0 so single-target code paths still work. + this.__framebuffer = framebuffers ? framebuffers[0] : null; + } + public get _framebufferDepthStencil(): Nullable { return this.__framebufferDepthStencil; } @@ -40,7 +58,13 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper { } public override dispose(disposeOnlyFramebuffers = false): void { - this._framebuffer = null; + if (this.__framebuffers) { + // Releases all six per-face framebuffers (face 0 is aliased by __framebuffer, so + // clear that alias here without releasing it again). + this._framebuffers = null; + } else { + this._framebuffer = null; + } this._framebufferDepthStencil = null; super.dispose(disposeOnlyFramebuffers); diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index fd858b8239e..4ce576aaf59 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -2341,6 +2341,76 @@ export class ThinNativeEngine extends ThinEngine { return rtWrapper; } + public override createRenderTargetCubeTexture(size: number, options?: RenderTargetCreationOptions): RenderTargetWrapper { + const rtWrapper = this._createHardwareRenderTargetWrapper(false, true, size) as NativeRenderTargetWrapper; + + let generateDepthBuffer = true; + let generateStencilBuffer = false; + let generateMipMaps = false; + let type = Constants.TEXTURETYPE_UNSIGNED_BYTE; + let samplingMode = Constants.TEXTURE_TRILINEAR_SAMPLINGMODE; + let format = Constants.TEXTUREFORMAT_RGBA; + let samples = 1; + if (options !== undefined && typeof options === "object") { + generateDepthBuffer = options.generateDepthBuffer ?? true; + generateStencilBuffer = !!options.generateStencilBuffer; + generateMipMaps = !!options.generateMipMaps; + type = options.type ?? Constants.TEXTURETYPE_UNSIGNED_BYTE; + samplingMode = options.samplingMode ?? Constants.TEXTURE_TRILINEAR_SAMPLINGMODE; + format = options.format ?? Constants.TEXTUREFORMAT_RGBA; + samples = options.samples ?? 1; + } + + if (type === Constants.TEXTURETYPE_FLOAT && !this._caps.textureFloat) { + type = Constants.TEXTURETYPE_UNSIGNED_BYTE; + Logger.Warn("Float textures are not supported. Type forced to TEXTURETYPE_UNSIGNED_BYTE"); + } + + const texture = new InternalTexture(this, InternalTextureSource.RenderTarget); + texture.isCube = true; + texture.baseWidth = size; + texture.baseHeight = size; + texture.width = size; + texture.height = size; + texture.isReady = true; + texture.samples = samples; + texture.generateMipMaps = generateMipMaps; + texture.samplingMode = samplingMode; + texture.type = type; + texture.format = format; + + const nativeTexture = texture._hardwareTexture!.underlyingResource; + const nativeTextureFormat = getNativeTextureFormat(format, type); + // See the createRenderTargetTexture MSAA/mips note: avoid the mips + samples combo on bgfx. + const hasMips = samples > 1 ? false : generateMipMaps; + this._engine.initializeTexture(nativeTexture, size, size, hasMips, nativeTextureFormat, /*renderTarget*/ true, /*srgb*/ false, samples, /*isCube*/ true); + this._setTextureSampling(nativeTexture, getNativeSamplingMode(samplingMode)); + + // The native engine cannot render to all six faces through one framebuffer, so create one + // framebuffer per face (the C++ side binds the matching cube layer); bindFramebuffer(faceIndex) + // then selects the right one. + const framebuffers: NativeFramebuffer[] = []; + for (let face = 0; face < 6; face++) { + framebuffers.push(this._engine.createFrameBuffer(nativeTexture, size, size, generateStencilBuffer, generateDepthBuffer, samples, face)); + } + + rtWrapper._framebuffers = framebuffers; + rtWrapper._generateDepthBuffer = generateDepthBuffer; + rtWrapper._generateStencilBuffer = generateStencilBuffer; + rtWrapper._samples = samples; + + rtWrapper.setTextures(texture); + + return rtWrapper; + } + + public override generateMipMapsForCubemap(_texture: InternalTexture, _unbind = true): void { + // The WebGL path rebinds gl.TEXTURE_CUBE_MAP and calls gl.generateMipmap; both deref _gl, which is + // null on Native. bgfx auto-generates the mip chain when a render target texture created with mips is + // resolved (the same way 2D RTTs get their mips here -- unBindFramebuffer issues no explicit mipgen), + // so this is a no-op on Native. + } + public override updateRenderTargetTextureSampleCount(rtWrapper: RenderTargetWrapper, samples: number): number { if (rtWrapper.samples === samples) { return samples; @@ -2422,15 +2492,16 @@ export class ThinNativeEngine extends ThinEngine { this._currentRenderTarget = texture; - if (faceIndex) { - throw new Error("Cuboid frame buffers are not yet supported in NativeEngine."); - } - if (requiredWidth || requiredHeight) { throw new Error("Required width/height for frame buffers not yet supported in NativeEngine."); } - if (nativeRTWrapper._framebufferDepthStencil) { + if (nativeRTWrapper._framebuffers) { + // Cube render target: bind the framebuffer for the requested face. + this._bindUnboundFramebuffer(nativeRTWrapper._framebuffers[faceIndex ?? 0]); + } else if (faceIndex) { + throw new Error("Cuboid frame buffers are not yet supported in NativeEngine."); + } else if (nativeRTWrapper._framebufferDepthStencil) { this._bindUnboundFramebuffer(nativeRTWrapper._framebufferDepthStencil); } else { this._bindUnboundFramebuffer(nativeRTWrapper._framebuffer); From aee206d9eac6512ddf9a7805e9240fdcdbf32e91 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 11 Jun 2026 07:54:50 -0700 Subject: [PATCH 5/6] [Native] Report engine.name "Native" and implement updateTextureData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native engine inherited engine.name === "WebGL" from ThinEngine, so code that guards WebGL-only _gl access with ngine.name === "WebGL" ran on the native engine and dereferenced the null _gl context (e.g. TEXTURE_2D undefined). updateTextureData also threw "not implemented". - Report a distinct engine name ("Native"), mirroring how the WebGPU engine reports "WebGPU", so name-gated WebGL-only code paths skip the native engine. - Implement updateTextureData: forward the sub-rectangle (plus invertY, so the native side can match the base texture's vertical orientation) to the native engine. generateMipMaps is ignored (mip regeneration after a partial update is not supported on Native). Pairs with the BabylonNative change adding NativeEngine::UpdateTextureData. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/src/Engines/Native/nativeInterfaces.ts | 11 +++++++++++ .../dev/core/src/Engines/thinNativeEngine.pure.ts | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 2be3b48b9c5..0b72bdfec84 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -74,6 +74,17 @@ export interface INativeEngine { ): void; loadTexture(texture: NativeTexture, data: ArrayBufferView, generateMips: boolean, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void; loadRawTexture(texture: NativeTexture, data: ArrayBufferView, width: number, height: number, format: number, generateMips: boolean, invertY: boolean): void; + updateTextureData( + texture: NativeTexture, + data: ArrayBufferView, + xOffset: number, + yOffset: number, + width: number, + height: number, + faceIndex: number, + lod: number, + invertY: boolean + ): void; loadRawTexture2DArray( texture: NativeTexture, data: Nullable, diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index 4ce576aaf59..3e60ca368ec 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -307,6 +307,10 @@ export class ThinNativeEngine extends ThinEngine { this._webGLVersion = 2; this.disableUniformBuffers = true; this._shaderPlatformName = "NATIVE"; + // Babylon Native is not WebGL and has no _gl context. Report a distinct engine name (like + // WebGPU reports "WebGPU") so application/feature code that branches on engine.name === "WebGL" + // to touch the WebGL-only _gl context skips the native engine instead of dereferencing null. + this._name = "Native"; // TODO: Initialize this more correctly based on the hardware capabilities. // Init caps @@ -2757,7 +2761,15 @@ export class ThinNativeEngine extends ThinEngine { lod: number = 0, generateMipMaps = false ): void { - throw new Error("updateTextureData not implemented."); + if (!texture._hardwareTexture) { + return; + } + + // bgfx updates the requested sub-rectangle of the existing texture (faceIndex selects the cube + // face / array layer, lod selects the mip level). invertY is forwarded so the native side can match + // the vertical orientation the base texture upload uses. Mip regeneration after a partial update is + // not supported on Native, so generateMipMaps is ignored (consistent with the other raw-texture paths). + this._engine.updateTextureData(texture._hardwareTexture.underlyingResource, imageData, xOffset, yOffset, width, height, faceIndex, lod, texture.invertY); } /** From bbac444bde5bb2f6f2ae26806655e8e4dbb837bd Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 11 Jun 2026 08:13:33 -0700 Subject: [PATCH 6/6] Strip eslint --fix doc-stub noise from INative (combined branch cleanup) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Engines/Native/nativeInterfaces.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 0b72bdfec84..4d84dba0de9 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -463,45 +463,21 @@ export const enum NativeTraceLevel { /** @internal */ export interface INative { // NativeEngine plugin - /** - * - */ Engine: INativeEngineConstructor; - /** - * - */ NativeDataStream: INativeDataStreamConstructor; // NativeCamera plugin - /** - * - */ Camera?: INativeCameraConstructor; // NativeCanvas plugin - /** - * - */ Canvas?: INativeCanvasConstructor; - /** - * - */ Image?: INativeImageConstructor; - /** - * - */ Path2D?: INativePath2DConstructor; // Native XMLHttpRequest polyfill - /** - * - */ XMLHttpRequest?: typeof XMLHttpRequest; // NativeInput plugin - /** - * - */ DeviceInputSystem?: IDeviceInputSystemConstructor; // NativeTracing plugin