diff --git a/packages/dev/core/src/Engines/Native/nativeHelpers.ts b/packages/dev/core/src/Engines/Native/nativeHelpers.ts index 751262a02794..9bacb6774291 100644 --- a/packages/dev/core/src/Engines/Native/nativeHelpers.ts +++ b/packages/dev/core/src/Engines/Native/nativeHelpers.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { ErrorCodes, RuntimeError } from "core/Misc/error"; +import { Logger } from "core/Misc/logger"; import { Constants } from "../constants"; import { type INative } from "./nativeInterfaces"; import { VertexBuffer } from "core/Buffers/buffer.pure"; @@ -299,6 +300,18 @@ export function getNativeStencilDepthPass(opPass: number): number { } } +const _warnedUnsupportedAlphaModes = new Set(); + +// Some alpha modes were introduced alongside newer Babylon Native features. When running against an older +// native binary the corresponding _native.Engine constant is undefined; warn once and fall back to ALPHA_ONEONE. +function _getFallbackAlphaMode(mode: number, unsupportedName: string): number { + if (!_warnedUnsupportedAlphaModes.has(mode)) { + _warnedUnsupportedAlphaModes.add(mode); + Logger.Warn(`Alpha mode ${unsupportedName} is not supported by this version of Babylon Native; falling back to ALPHA_ONEONE.`); + } + return _native.Engine.ALPHA_ONEONE; +} + export function getNativeAlphaMode(mode: number): number { switch (mode) { case Constants.ALPHA_DISABLE: @@ -315,6 +328,10 @@ export function getNativeAlphaMode(mode: number): number { return _native.Engine.ALPHA_MAXIMIZED; case Constants.ALPHA_ONEONE: return _native.Engine.ALPHA_ONEONE; + case Constants.ALPHA_ONEONE_ONEONE: + return _native.Engine.ALPHA_ONEONE_ONEONE ?? _getFallbackAlphaMode(mode, "ALPHA_ONEONE_ONEONE"); + case Constants.ALPHA_LAYER_ACCUMULATE: + return _native.Engine.ALPHA_LAYER_ACCUMULATE ?? _getFallbackAlphaMode(mode, "ALPHA_LAYER_ACCUMULATE"); case Constants.ALPHA_PREMULTIPLIED: return _native.Engine.ALPHA_PREMULTIPLIED; case Constants.ALPHA_PREMULTIPLIED_PORTERDUFF: diff --git a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts index 90e077e9decd..60f519f0abf2 100644 --- a/packages/dev/core/src/Engines/Native/nativeInterfaces.ts +++ b/packages/dev/core/src/Engines/Native/nativeInterfaces.ts @@ -125,6 +125,14 @@ export interface INativeEngine { samples: number, layer?: number ): NativeFramebuffer; + createMultiFrameBuffer?( + textures: NativeTexture[], + width: number, + height: number, + generateStencilBuffer: boolean, + generateDepthBuffer: boolean, + samples: number + ): NativeFramebuffer; getRenderWidth(): number; getRenderHeight(): number; @@ -293,6 +301,8 @@ interface INativeEngineConstructor { readonly ALPHA_MULTIPLY: number; readonly ALPHA_MAXIMIZED: number; readonly ALPHA_ONEONE: number; + readonly ALPHA_ONEONE_ONEONE?: number; + readonly ALPHA_LAYER_ACCUMULATE?: number; readonly ALPHA_PREMULTIPLIED: number; readonly ALPHA_PREMULTIPLIED_PORTERDUFF: number; readonly ALPHA_INTERPOLATE: number; diff --git a/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts b/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts index 37df65b7f764..825e513564cb 100644 --- a/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts +++ b/packages/dev/core/src/Engines/Native/nativeRenderTargetWrapper.ts @@ -1,4 +1,5 @@ import { type Nullable } from "../../types"; +import { type InternalTexture } from "../../Materials/Textures/internalTexture"; import { type TextureSize } from "../../Materials/Textures/textureCreationOptions"; import { RenderTargetWrapper } from "../renderTargetWrapper"; import { type NativeFramebuffer } from "./nativeInterfaces"; @@ -57,6 +58,19 @@ export class NativeRenderTargetWrapper extends RenderTargetWrapper { this._engine = engine; } + public override setTexture(texture: InternalTexture, index: number = 0, disposePrevious: boolean = true): void { + const previous = this.textures?.[index]; + super.setTexture(texture, index, disposePrevious); + + // bgfx binds a fixed attachment set when a framebuffer is created and cannot re-point an individual + // attachment the way GL's framebufferTexture2D can. When a multi render target attachment is swapped + // after creation (e.g. the OIT depth-peeling renderer replaces every attachment via + // MultiRenderTarget.setInternalTexture), recreate the whole framebuffer from the new attachment set. + if (this.isMulti && this.textures?.[index] !== previous) { + this._engine._createMultiRenderTargetFramebuffer(this); + } + } + public override dispose(disposeOnlyFramebuffers = false): void { if (this.__framebuffers) { // Releases all six per-face framebuffers (face 0 is aliased by __framebuffer, so diff --git a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts index bfc26f3b8bc5..fd6aafb89296 100644 --- a/packages/dev/core/src/Engines/thinNativeEngine.pure.ts +++ b/packages/dev/core/src/Engines/thinNativeEngine.pure.ts @@ -19,6 +19,7 @@ import { type InternalTextureCreationOptions, } from "../Materials/Textures/textureCreationOptions"; import { type IPipelineContext } from "./IPipelineContext"; +import { type IMultiRenderTargetOptions } from "../Materials/Textures/multiRenderTarget.pure"; import { type IColor3Like, type IColor4Like, type IViewportLike } from "../Maths/math.like"; import { Logger } from "../Misc/logger"; import { Constants } from "./constants"; @@ -350,7 +351,7 @@ export class ThinNativeEngine extends ThinEngine { textureHalfFloatRender: true, textureLOD: true, texelFetch: false, - drawBuffersExtension: false, + drawBuffersExtension: true, depthTextureExtension: false, vertexArrayObject: true, instancedArrays: true, @@ -576,8 +577,16 @@ export class ThinNativeEngine extends ThinEngine { } public override clear(color: Nullable, backBuffer: boolean, depth: boolean, stencil: boolean = false, stencilClearValue = 0): void { - if (this.useReverseDepthBuffer) { - throw new Error("reverse depth buffer is not currently implemented"); + if (depth && this.useReverseDepthBuffer) { + // Reverse-Z: the scene is rendered with a flipped projection (near maps to 1, far to 0), so the + // depth buffer is cleared to 0 and the comparison must accept greater values. The WebGL engine + // sets depthCullingState.depthFunc = GEQUAL here and relies on applyStates() running the depth + // comparison to the GL context before each draw. The native draw path does not call applyStates() + // (only depth-test enable/disable is reconciled in _flushDepthTestState()), so setting the shared + // state alone would never reach the backend. Route the comparison through the native command path + // as well -- mirroring the WebGPU engine's clear path, which calls setDepthFunctionToGreaterOrEqual(). + this._depthCullingState.depthFunc = Constants.GEQUAL; + this.setDepthFunction(Constants.GEQUAL); } this._commandBufferEncoder.startEncodingCommand(_native.Engine.COMMAND_CLEAR); @@ -587,7 +596,7 @@ export class ThinNativeEngine extends ThinEngine { this._commandBufferEncoder.encodeCommandArgAsFloat32(color ? color.b : 0); this._commandBufferEncoder.encodeCommandArgAsFloat32(color ? color.a : 1); this._commandBufferEncoder.encodeCommandArgAsUInt32(depth ? 1 : 0); - this._commandBufferEncoder.encodeCommandArgAsFloat32(1); + this._commandBufferEncoder.encodeCommandArgAsFloat32(depth && this.useReverseDepthBuffer ? 0 : 1); this._commandBufferEncoder.encodeCommandArgAsUInt32(stencil ? 1 : 0); this._commandBufferEncoder.encodeCommandArgAsUInt32(stencilClearValue); this._commandBufferEncoder.finishEncodingCommand(); @@ -1102,6 +1111,20 @@ export class ThinNativeEngine extends ThinEngine { } } + public override applyStates(): void { + // The base ThinEngine.applyStates() drives the WebGL context (this._gl) directly, which is null on + // Native. Flush the depth-culling state through the native command path instead, so callers that + // mutate engine.depthCullingState directly and then call applyStates() (e.g. the depth-peeling / OIT + // renderer) take effect. Alpha and stencil state are applied on Native via their dedicated setters + // (setAlphaMode / setStencil*), which encode their commands when called. + const depthCullingState = this._depthCullingState; + if (depthCullingState.depthFunc !== null && depthCullingState.depthFunc !== undefined) { + this.setDepthFunction(depthCullingState.depthFunc); + } + this.setDepthBuffer(depthCullingState.depthTest); + this.setDepthWrite(depthCullingState.depthMask); + } + /** * Gets a boolean indicating if depth writing is enabled * @returns the current depth writing state @@ -2440,6 +2463,174 @@ export class ThinNativeEngine extends ThinEngine { return rtWrapper; } + public override createMultipleRenderTarget(size: TextureSize, options: IMultiRenderTargetOptions, _initializeBuffers = true): RenderTargetWrapper { + const rtWrapper = this._createHardwareRenderTargetWrapper(true, false, size) as NativeRenderTargetWrapper; + + let generateMipMaps = false; + let generateDepthBuffer = true; + let generateStencilBuffer = false; + let generateDepthTexture = false; + let textureCount = 1; + let samples = 1; + let types: number[] = []; + let samplingModes: number[] = []; + let formats: number[] = []; + let targets: number[] = []; + let faceIndex: number[] = []; + let layerIndex: number[] = []; + let labels: string[] = []; + let dontCreateTextures = false; + + if (options !== undefined) { + generateMipMaps = options.generateMipMaps ?? false; + generateDepthBuffer = options.generateDepthBuffer ?? true; + generateStencilBuffer = options.generateStencilBuffer ?? false; + generateDepthTexture = options.generateDepthTexture ?? false; + textureCount = options.textureCount ?? 1; + samples = options.samples ?? 1; + types = options.types || types; + samplingModes = options.samplingModes || samplingModes; + formats = options.formats || formats; + targets = options.targetTypes || targets; + faceIndex = options.faceIndex || faceIndex; + layerIndex = options.layerIndex || layerIndex; + labels = options.labels || labels; + dontCreateTextures = options.dontCreateTextures ?? false; + } + + rtWrapper.label = options?.label ?? "MultiRenderTargetWrapper"; + + const width = (<{ width: number; height: number }>size).width ?? size; + const height = (<{ width: number; height: number }>size).height ?? size; + + const textures: InternalTexture[] = []; + const attachments: number[] = []; + + for (let i = 0; i < textureCount; i++) { + const samplingMode = samplingModes[i] || Constants.TEXTURE_TRILINEAR_SAMPLINGMODE; + let type = types[i] || Constants.TEXTURETYPE_UNSIGNED_BYTE; + const format = formats[i] || Constants.TEXTUREFORMAT_RGBA; + const target = targets[i] || Constants.TEXTURE_2D; + + attachments.push(i); + + // target === -1 marks an attachment with no engine-created texture; dontCreateTextures defers them. + if (target === -1 || dontCreateTextures) { + continue; + } + + if (type === Constants.TEXTURETYPE_FLOAT && !this._caps.textureFloat) { + type = Constants.TEXTURETYPE_UNSIGNED_BYTE; + Logger.Warn("Float textures are not supported. Multi render target attachment forced to TEXTURETYPE_UNSIGNED_BYTE type"); + } + + const texture = new InternalTexture(this, InternalTextureSource.MultiRenderTarget); + const nativeTexture = texture._hardwareTexture!.underlyingResource; + if (target !== Constants.TEXTURE_2D) { + // Native multi render targets currently only attach 2D color textures; cube / 2D-array + // attachments are created as 2D so the attachment still exists and mrt.textures[i] is populated. + Logger.Warn("Multi render target attachment target " + target + " is not supported on Native; using a 2D texture."); + } + // bgfx auto-generates the mip chain on resolve; avoid the mips + MSAA combo (see createRenderTargetTexture). + const hasMips = samples > 1 ? false : generateMipMaps; + this._engine.initializeTexture(nativeTexture, width, height, hasMips, getNativeTextureFormat(format, type), /*renderTarget*/ true, /*srgb*/ false, samples); + this._setTextureSampling(nativeTexture, getNativeSamplingMode(samplingMode)); + + texture.baseWidth = width; + texture.baseHeight = height; + texture.width = width; + texture.height = height; + texture.isReady = true; + texture.samples = samples; + texture.generateMipMaps = generateMipMaps; + texture.samplingMode = samplingMode; + texture.type = type; + texture.format = format; + texture.label = labels[i] ?? rtWrapper.label + "-Texture" + i; + + textures[i] = texture; + this._internalTexturesCache.push(texture); + } + + rtWrapper._generateDepthBuffer = generateDepthBuffer || generateDepthTexture; + rtWrapper._generateStencilBuffer = generateStencilBuffer; + rtWrapper._samples = samples; + rtWrapper._attachments = attachments; + + rtWrapper.setTextures(textures); + rtWrapper.setLayerAndFaceIndices(layerIndex, faceIndex); + + // The native engine creates one framebuffer with all the color attachments (bgfx writes to every + // attachment of the bound framebuffer, so there is no drawBuffers equivalent to issue per draw). + this._createMultiRenderTargetFramebuffer(rtWrapper); + + return rtWrapper; + } + + /** + * Creates (or recreates) the native framebuffer of a multi render target from the color attachment + * textures currently held by the wrapper. bgfx binds a fixed attachment set when a framebuffer is + * created and cannot re-point an individual attachment the way GL's framebufferTexture2D can, so the + * whole framebuffer has to be recreated whenever an attachment is swapped after creation (e.g. the OIT + * depth-peeling renderer replaces every attachment via MultiRenderTarget.setInternalTexture). + * @param rtWrapper The multi render target wrapper to build the framebuffer for. + * @internal + */ + public _createMultiRenderTargetFramebuffer(rtWrapper: NativeRenderTargetWrapper): void { + const textures = rtWrapper.textures; + if (!textures) { + return; + } + + const colorHandles: NativeTexture[] = []; + for (const texture of textures) { + const handle = texture?._hardwareTexture?.underlyingResource; + if (handle) { + colorHandles.push(handle); + } + } + + // bgfx framebuffers use a dense, ordered attachment list, so only (re)build once every expected color + // attachment is present. Building from a partial set (e.g. attachments deferred via dontCreateTextures / + // targetTypes[i] === -1, or filled out of order) would compress the attachment indices -- attachment 2 + // would become attachment 1, etc. -- and produce a framebuffer that does not match the MRT layout. The + // count match guarantees the collected handles are contiguous and in attachment order. + const expectedCount = rtWrapper._attachments ? rtWrapper._attachments.length : colorHandles.length; + if (colorHandles.length === 0 || colorHandles.length !== expectedCount) { + return; + } + + const width = rtWrapper.width; + const height = rtWrapper.height; + + if (this._engine.createMultiFrameBuffer) { + rtWrapper._framebuffer = this._engine.createMultiFrameBuffer( + colorHandles, + width, + height, + rtWrapper._generateStencilBuffer, + rtWrapper._generateDepthBuffer, + rtWrapper._samples + ); + } else { + // Older Babylon Native binaries (predating multi render target support) do not expose createMultiFrameBuffer. + // Fall back to a single-attachment framebuffer bound to the first color target so the scene keeps rendering. + // Warn once (limit 1): the framebuffer can be rebuilt many times during attachment setup/swaps. + Logger.Warn( + "createMultiFrameBuffer is not supported by this version of Babylon Native; multi render targets are unavailable. Falling back to a single-attachment framebuffer bound to the first color target.", + 1 + ); + rtWrapper._framebuffer = this._engine.createFrameBuffer( + colorHandles[0], + width, + height, + rtWrapper._generateStencilBuffer, + rtWrapper._generateDepthBuffer, + rtWrapper._samples + ); + } + } + 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 @@ -2447,6 +2638,91 @@ export class ThinNativeEngine extends ThinEngine { // so this is a no-op on Native. } + public override bindAttachments(_attachments: number[]): void { + // No-op on Native: bgfx renders to every color attachment of the bound framebuffer, so there is + // no gl.drawBuffers equivalent to select a subset. + } + + public override buildTextureLayout(textureStatus: boolean[], _backBufferLayout = false): number[] { + // Native has no gl draw-buffer enums; return a per-attachment index list (consumers only use the + // length/order, and bindAttachments is a no-op). + const result: number[] = []; + for (let i = 0; i < textureStatus.length; i++) { + result.push(textureStatus[i] ? i : -1); + } + return result; + } + + public override restoreSingleAttachment(): void { + // No-op on Native (see bindAttachments). + } + + public override restoreSingleAttachmentForRenderTarget(): void { + // No-op on Native (see bindAttachments). + } + + public override generateMipMapsMultiFramebuffer(_texture: RenderTargetWrapper): void { + // No-op on Native: bgfx auto-generates mips on render-target resolve (as for 2D/cube RTTs). + } + + public override resolveMultiFramebuffer(_texture: RenderTargetWrapper): void { + // No-op on Native: bgfx resolves MSAA render targets automatically. + } + + public override unBindMultiColorAttachmentFramebuffer(_rtWrapper: RenderTargetWrapper, _disableGenerateMipMaps = false, onBeforeUnbind?: () => void): void { + this._currentRenderTarget = null; + if (onBeforeUnbind) { + onBeforeUnbind(); + } + this._bindUnboundFramebuffer(null); + } + + public override updateMultipleRenderTargetTextureSampleCount(rtWrapper: Nullable, samples: number, _initializeBuffers = true): number { + if (!rtWrapper || rtWrapper.samples === samples) { + return samples; + } + + const textures = rtWrapper.textures; + if (!textures) { + return rtWrapper.samples; + } + + const nativeRTWrapper = rtWrapper as NativeRenderTargetWrapper; + + // bgfx couples MSAA to the texture creation flags (see updateRenderTargetTextureSampleCount), so + // changing the sample count after the fact requires reissuing every color attachment's underlying + // bgfx handle with the new MSAA flag. initializeTexture disposes the old handle and allocates a fresh + // one while preserving the InternalTexture / Graphics::Texture identity; only the internal bgfx handle + // rotates. Afterwards the framebuffer is recreated so its attachment list refers to the new handles. + for (const texture of textures) { + // Wrapped (External-source) textures own an opaque external handle (format=-1); reinitializing + // would destroy it and getNativeTextureFormat would throw, so leave those attachments untouched. + if (!texture?._hardwareTexture || texture.source === InternalTextureSource.External) { + continue; + } + + const nativeTexture = texture._hardwareTexture.underlyingResource; + // See the bgfx-msaa-mips workaround in updateRenderTargetTextureSampleCount (BabylonNative#1714). + const hasMips = samples > 1 ? false : texture.generateMipMaps; + this._engine.initializeTexture( + nativeTexture, + texture.baseWidth, + texture.baseHeight, + hasMips, + getNativeTextureFormat(texture.format, texture.type), + /*renderTarget*/ true, + texture._useSRGBBuffer, + samples + ); + texture.samples = samples; + } + + nativeRTWrapper._samples = samples; + this._createMultiRenderTargetFramebuffer(nativeRTWrapper); + + return samples; + } + public override updateRenderTargetTextureSampleCount(rtWrapper: RenderTargetWrapper, samples: number): number { if (rtWrapper.samples === samples) { return samples;