Skip to content

Commit 5adc34d

Browse files
bkaradzic-microsoftbkaradzicCopilot
authored
[Native] Add cube render target support to the native engine (#18561)
Paired native PR: BabylonJS/BabylonNative#1750 ## What Adds cube render target support to the Babylon Native engine. Previously the native engine had no `createRenderTargetCubeTexture` override and `bindFramebuffer(faceIndex)` threw, so any feature that renders into a cube map — `ReflectionProbe`, point-light cube shadow maps — fell through to the WebGL code path and dereferenced the null `_gl` context (`TEXTURE_CUBE_MAP undefined`). ## Changes (`packages/dev/core/src/Engines`) - `thinNativeEngine.pure.ts` - `createRenderTargetCubeTexture`: creates a native cube color texture and one framebuffer per face (the native side binds the matching cube layer). - `bindFramebuffer`: binds the per-face framebuffer for cube render targets. - `generateMipMapsForCubemap`: no-op on Native — bgfx auto-generates the mip chain on render-target resolve, the same way 2D RTTs get their mips. - `Native/nativeRenderTargetWrapper.ts`: tracks per-face framebuffers and releases them on dispose. - `Native/nativeInterfaces.ts`: threads the cube/layer params through `initializeTexture` and `createFrameBuffer`. ## Paired native change Requires the matching BabylonNative C++ change (cube color texture + per-face attachment). Draft until both land. ## Testing Built `babylon.max.js` and ran the BabylonNative Playground validation suite (D3D11). The "Shadows with instances" tests (left/right handed), which previously crashed, now pass; the ReflectionProbe and point-light-shadow scenes render with correct geometry and orientation. --- ## Related PRs & landing order - **Babylon.js (engine / TS):** #18561 - **BabylonNative (C++ engine + test re-enable):** BabylonJS/BabylonNative#1750 Co-dependent; land in this order: 1. **Babylon.js #18561 first** — adds the cube render-target TS overrides; no WebGL behavior change. 2. A **`babylonjs` npm release** ships that TS change. 3. **BabylonNative #1750 last** — bumps the bundled `babylonjs` and re-enables the 2 `Shadows with instances` (left/right handed) validation tests, which only pass once the paired JS is present in the bundled engine. --------- Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4d4dad2 commit 5adc34d

3 files changed

Lines changed: 168 additions & 16 deletions

File tree

packages/dev/core/src/Engines/Native/nativeInterfaces.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,17 @@ export interface INativeEngine {
6161
getAttributes(shaderProgram: NativeProgram, attributeNames: string[]): number[];
6262

6363
createTexture(): NativeTexture;
64-
initializeTexture(texture: NativeTexture, width: number, height: number, hasMips: boolean, format: number, renderTarget: boolean, srgb: boolean, samples: number): void;
64+
initializeTexture(
65+
texture: NativeTexture,
66+
width: number,
67+
height: number,
68+
hasMips: boolean,
69+
format: number,
70+
renderTarget: boolean,
71+
srgb: boolean,
72+
samples: number,
73+
isCube?: boolean
74+
): void;
6575
loadTexture(texture: NativeTexture, data: ArrayBufferView, generateMips: boolean, invertY: boolean, srgb: boolean, onSuccess: () => void, onError: () => void): void;
6676
loadRawTexture(texture: NativeTexture, data: ArrayBufferView, width: number, height: number, format: number, generateMips: boolean, invertY: boolean): void;
6777
loadRawTexture2DArray(
@@ -101,7 +111,8 @@ export interface INativeEngine {
101111
height: number,
102112
generateStencilBuffer: boolean,
103113
generateDepthBuffer: boolean,
104-
samples: number
114+
samples: number,
115+
layer?: number
105116
): NativeFramebuffer;
106117

107118
getRenderWidth(): number;

packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper {
1111
private __framebuffer: Nullable<NativeFramebuffer> = null;
1212
// eslint-disable-next-line @typescript-eslint/naming-convention
1313
private __framebufferDepthStencil: Nullable<NativeFramebuffer> = null;
14+
// Per-face framebuffers for cube render targets (index = cube face 0..5).
15+
// eslint-disable-next-line @typescript-eslint/naming-convention
16+
private __framebuffers: Nullable<NativeFramebuffer[]> = null;
1417

1518
public get _framebuffer(): Nullable<NativeFramebuffer> {
1619
return this.__framebuffer;
@@ -23,6 +26,21 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper {
2326
this.__framebuffer = framebuffer;
2427
}
2528

29+
public get _framebuffers(): Nullable<NativeFramebuffer[]> {
30+
return this.__framebuffers;
31+
}
32+
33+
public set _framebuffers(framebuffers: Nullable<NativeFramebuffer[]>) {
34+
if (this.__framebuffers) {
35+
for (const framebuffer of this.__framebuffers) {
36+
this._engine._releaseFramebufferObjects(framebuffer);
37+
}
38+
}
39+
this.__framebuffers = framebuffers;
40+
// Keep _framebuffer pointing at face 0 so single-target code paths still work.
41+
this.__framebuffer = framebuffers ? framebuffers[0] : null;
42+
}
43+
2644
public get _framebufferDepthStencil(): Nullable<NativeFramebuffer> {
2745
return this.__framebufferDepthStencil;
2846
}
@@ -40,7 +58,13 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper {
4058
}
4159

4260
public override dispose(disposeOnlyFramebuffers = false): void {
43-
this._framebuffer = null;
61+
if (this.__framebuffers) {
62+
// Releases all six per-face framebuffers (face 0 is aliased by __framebuffer, so
63+
// clear that alias here without releasing it again).
64+
this._framebuffers = null;
65+
} else {
66+
this._framebuffer = null;
67+
}
4468
this._framebufferDepthStencil = null;
4569

4670
super.dispose(disposeOnlyFramebuffers);

packages/dev/core/src/Engines/thinNativeEngine.pure.ts

Lines changed: 130 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,90 @@ export class ThinNativeEngine extends ThinEngine {
23592359
return rtWrapper;
23602360
}
23612361

2362+
public override createRenderTargetCubeTexture(size: number, options?: RenderTargetCreationOptions): RenderTargetWrapper {
2363+
const rtWrapper = this._createHardwareRenderTargetWrapper(false, true, size) as NativeRenderTargetWrapper;
2364+
2365+
let generateDepthBuffer = true;
2366+
let generateStencilBuffer = false;
2367+
let generateMipMaps = false;
2368+
let type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
2369+
let samplingMode = Constants.TEXTURE_TRILINEAR_SAMPLINGMODE;
2370+
let format = Constants.TEXTUREFORMAT_RGBA;
2371+
let samples = 1;
2372+
let label: string | undefined;
2373+
if (options !== undefined && typeof options === "object") {
2374+
generateDepthBuffer = options.generateDepthBuffer ?? true;
2375+
generateStencilBuffer = !!options.generateStencilBuffer;
2376+
generateMipMaps = !!options.generateMipMaps;
2377+
type = options.type ?? Constants.TEXTURETYPE_UNSIGNED_BYTE;
2378+
samplingMode = options.samplingMode ?? Constants.TEXTURE_TRILINEAR_SAMPLINGMODE;
2379+
format = options.format ?? Constants.TEXTUREFORMAT_RGBA;
2380+
samples = options.samples ?? 1;
2381+
label = options.label;
2382+
}
2383+
2384+
// Match _createInternalTexture: float/half-float RTTs that the platform can't linearly filter fall
2385+
// back to NEAREST so the cube RTT never carries an unsupported sampling mode.
2386+
if (type === Constants.TEXTURETYPE_FLOAT && !this._caps.textureFloatLinearFiltering) {
2387+
samplingMode = Constants.TEXTURE_NEAREST_SAMPLINGMODE;
2388+
} else if (type === Constants.TEXTURETYPE_HALF_FLOAT && !this._caps.textureHalfFloatLinearFiltering) {
2389+
samplingMode = Constants.TEXTURE_NEAREST_SAMPLINGMODE;
2390+
}
2391+
if (type === Constants.TEXTURETYPE_FLOAT && !this._caps.textureFloat) {
2392+
type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
2393+
Logger.Warn("Float textures are not supported. Type forced to TEXTURETYPE_UNSIGNED_BYTE");
2394+
}
2395+
2396+
const texture = new InternalTexture(this, InternalTextureSource.RenderTarget);
2397+
texture.isCube = true;
2398+
texture.baseWidth = size;
2399+
texture.baseHeight = size;
2400+
texture.width = size;
2401+
texture.height = size;
2402+
texture.isReady = true;
2403+
texture.samples = samples;
2404+
texture.generateMipMaps = generateMipMaps;
2405+
texture.samplingMode = samplingMode;
2406+
texture.type = type;
2407+
texture.format = format;
2408+
texture.label = label;
2409+
2410+
const nativeTexture = texture._hardwareTexture!.underlyingResource;
2411+
const nativeTextureFormat = getNativeTextureFormat(format, type);
2412+
// See the createRenderTargetTexture MSAA/mips note: avoid the mips + samples combo on bgfx.
2413+
const hasMips = samples > 1 ? false : generateMipMaps;
2414+
this._engine.initializeTexture(nativeTexture, size, size, hasMips, nativeTextureFormat, /*renderTarget*/ true, /*srgb*/ false, samples, /*isCube*/ true);
2415+
this._setTextureSampling(nativeTexture, getNativeSamplingMode(samplingMode));
2416+
2417+
// The native engine cannot render to all six faces through one framebuffer, so create one
2418+
// framebuffer per face (the C++ side binds the matching cube layer); bindFramebuffer(faceIndex)
2419+
// then selects the right one.
2420+
const framebuffers: NativeFramebuffer[] = [];
2421+
for (let face = 0; face < 6; face++) {
2422+
framebuffers.push(this._engine.createFrameBuffer(nativeTexture, size, size, generateStencilBuffer, generateDepthBuffer, samples, face));
2423+
}
2424+
2425+
rtWrapper._framebuffers = framebuffers;
2426+
rtWrapper._generateDepthBuffer = generateDepthBuffer;
2427+
rtWrapper._generateStencilBuffer = generateStencilBuffer;
2428+
rtWrapper._samples = samples;
2429+
2430+
rtWrapper.setTextures(texture);
2431+
2432+
// Track the hand-built cube RTT texture the same way _createInternalTexture tracks 2D textures so it
2433+
// participates in engine-wide lifecycle management (dispose iteration, context rebuild, stats).
2434+
this._internalTexturesCache.push(texture);
2435+
2436+
return rtWrapper;
2437+
}
2438+
2439+
public override generateMipMapsForCubemap(_texture: InternalTexture, _unbind = true): void {
2440+
// The WebGL path rebinds gl.TEXTURE_CUBE_MAP and calls gl.generateMipmap; both deref _gl, which is
2441+
// null on Native. bgfx auto-generates the mip chain when a render target texture created with mips is
2442+
// resolved (the same way 2D RTTs get their mips here -- unBindFramebuffer issues no explicit mipgen),
2443+
// so this is a no-op on Native.
2444+
}
2445+
23622446
public override updateRenderTargetTextureSampleCount(rtWrapper: RenderTargetWrapper, samples: number): number {
23632447
if (rtWrapper.samples === samples) {
23642448
return samples;
@@ -2404,19 +2488,51 @@ export class ThinNativeEngine extends ThinEngine {
24042488
// underlying bgfx resource has 1 mip level. Remove this guard once a fixed bgfx is in stable BN npm.
24052489
const hasMips = samples > 1 ? false : texture.generateMipMaps;
24062490
const nativeTextureFormat = getNativeTextureFormat(texture.format, texture.type);
2407-
this._engine.initializeTexture(nativeTexture, texture.baseWidth, texture.baseHeight, hasMips, nativeTextureFormat, /*renderTarget*/ true, texture._useSRGBBuffer, samples);
2408-
2409-
// NativeRenderTargetWrapper._framebuffer setter releases the old framebuffer before assigning,
2410-
// so no manual _releaseFramebufferObjects call is needed (and would double-delete the handle).
2411-
nativeRTWrapper._framebuffer = this._engine.createFrameBuffer(
2491+
const isCube = texture.isCube;
2492+
this._engine.initializeTexture(
24122493
nativeTexture,
24132494
texture.baseWidth,
24142495
texture.baseHeight,
2415-
rtWrapper._generateStencilBuffer,
2416-
rtWrapper._generateDepthBuffer,
2417-
samples
2496+
hasMips,
2497+
nativeTextureFormat,
2498+
/*renderTarget*/ true,
2499+
texture._useSRGBBuffer,
2500+
samples,
2501+
isCube
24182502
);
24192503

2504+
if (isCube) {
2505+
// Cube RTTs render through one framebuffer per face (see createRenderTargetCubeTexture). The
2506+
// underlying bgfx handle was just rotated, so recreate all six attachments; the _framebuffers
2507+
// setter releases the stale ones and keeps _framebuffer aliased to face 0 for single-target paths.
2508+
const framebuffers: NativeFramebuffer[] = [];
2509+
for (let face = 0; face < 6; face++) {
2510+
framebuffers.push(
2511+
this._engine.createFrameBuffer(
2512+
nativeTexture,
2513+
texture.baseWidth,
2514+
texture.baseHeight,
2515+
rtWrapper._generateStencilBuffer,
2516+
rtWrapper._generateDepthBuffer,
2517+
samples,
2518+
face
2519+
)
2520+
);
2521+
}
2522+
nativeRTWrapper._framebuffers = framebuffers;
2523+
} else {
2524+
// NativeRenderTargetWrapper._framebuffer setter releases the old framebuffer before assigning,
2525+
// so no manual _releaseFramebufferObjects call is needed (and would double-delete the handle).
2526+
nativeRTWrapper._framebuffer = this._engine.createFrameBuffer(
2527+
nativeTexture,
2528+
texture.baseWidth,
2529+
texture.baseHeight,
2530+
rtWrapper._generateStencilBuffer,
2531+
rtWrapper._generateDepthBuffer,
2532+
samples
2533+
);
2534+
}
2535+
24202536
rtWrapper._samples = samples;
24212537
texture.samples = samples;
24222538
return samples;
@@ -2440,15 +2556,16 @@ export class ThinNativeEngine extends ThinEngine {
24402556

24412557
this._currentRenderTarget = texture;
24422558

2443-
if (faceIndex) {
2444-
throw new Error("Cuboid frame buffers are not yet supported in NativeEngine.");
2445-
}
2446-
24472559
if (requiredWidth || requiredHeight) {
24482560
throw new Error("Required width/height for frame buffers not yet supported in NativeEngine.");
24492561
}
24502562

2451-
if (nativeRTWrapper._framebufferDepthStencil) {
2563+
if (nativeRTWrapper._framebuffers) {
2564+
// Cube render target: bind the framebuffer for the requested face.
2565+
this._bindUnboundFramebuffer(nativeRTWrapper._framebuffers[faceIndex ?? 0]);
2566+
} else if (faceIndex) {
2567+
throw new Error("Cuboid frame buffers are not yet supported in NativeEngine.");
2568+
} else if (nativeRTWrapper._framebufferDepthStencil) {
24522569
this._bindUnboundFramebuffer(nativeRTWrapper._framebufferDepthStencil);
24532570
} else {
24542571
this._bindUnboundFramebuffer(nativeRTWrapper._framebuffer);

0 commit comments

Comments
 (0)