diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index 63d51ca1a..72030c18c 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -16,9 +16,11 @@ import { Entity } from "./Entity"; import { BatcherManager } from "./RenderPipeline/BatcherManager"; import { RenderContext } from "./RenderPipeline/RenderContext"; import { RenderElement } from "./RenderPipeline/RenderElement"; +import { RenderTargetPool } from "./RenderPipeline/RenderTargetPool"; import { SubRenderElement } from "./RenderPipeline/SubRenderElement"; import { Scene } from "./Scene"; import { SceneManager } from "./SceneManager"; +import { RenderingStatistics } from "./asset/RenderingStatistics"; import { ResourceManager } from "./asset/ResourceManager"; import { EngineObject, EventDispatcher, Logger, Time } from "./base"; import { GLCapabilityType } from "./base/Constant"; @@ -64,6 +66,10 @@ export class Engine extends EventDispatcher { /** XR manager of Engine. */ readonly xrManager: XRManager; + /** @internal */ + _renderingStatistics: RenderingStatistics = new RenderingStatistics(); + /** @internal */ + _isDeviceLost: boolean = false; /** @internal */ _batcherManager: BatcherManager; @@ -81,6 +87,8 @@ export class Engine extends EventDispatcher { /* @internal */ _hardwareRenderer: IHardwareRenderer; /* @internal */ + _renderTargetPool: RenderTargetPool; + /* @internal */ _lastRenderState: RenderState = new RenderState(); /* @internal */ @@ -183,6 +191,13 @@ export class Engine extends EventDispatcher { return this._time; } + /** + * Rendering statistics. + */ + get renderingStatistics(): RenderingStatistics { + return this._renderingStatistics; + } + /** * Whether the engine is paused. */ @@ -243,6 +258,7 @@ export class Engine extends EventDispatcher { this._textDefaultFont.isGCIgnored = true; this._batcherManager = new BatcherManager(this); + this._renderTargetPool = new RenderTargetPool(this); this.inputManager = new InputManager(this, configuration.input); const { xrDevice } = configuration; @@ -494,6 +510,7 @@ export class Engine extends EventDispatcher { this.inputManager._destroy(); this._batcherManager.destroy(); + this._renderTargetPool.gc(); this.xrManager?._destroy(); this.dispatch("shutdown", this); @@ -648,8 +665,10 @@ export class Engine extends EventDispatcher { } private _onDeviceLost(): void { + this._isDeviceLost = true; // Lose graphic resources this.resourceManager._lostGraphicResources(); + this._renderingStatistics._reset(); console.log("Device lost."); this.dispatch("devicelost", this); } @@ -664,6 +683,7 @@ export class Engine extends EventDispatcher { const { resourceManager } = this; // Restore graphic resources resourceManager._restoreGraphicResources(); + this._isDeviceLost = false; console.log("Graphic resource restored."); // Restore resources content diff --git a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts index 73050d4de..dee2231a0 100644 --- a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts +++ b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts @@ -72,6 +72,19 @@ export class BasicRenderPipeline { */ destroy(): void { this._cullingResults.destroy(); + + const pool = this._camera.engine._renderTargetPool; + + if (this._internalColorTarget) { + pool.freeRenderTarget(this._internalColorTarget); + this._internalColorTarget = null; + } + + if (this._copyBackgroundTexture) { + pool.freeTexture(this._copyBackgroundTexture); + this._copyBackgroundTexture = null; + } + this._camera = null; } @@ -188,13 +201,13 @@ export class BasicRenderPipeline { } else { const internalColorTarget = this._internalColorTarget; const copyBackgroundTexture = this._copyBackgroundTexture; + const pool = engine._renderTargetPool; if (internalColorTarget) { - internalColorTarget.getColorTexture(0)?.destroy(true); - internalColorTarget.destroy(true); + pool.freeRenderTarget(internalColorTarget); this._internalColorTarget = null; } if (copyBackgroundTexture) { - copyBackgroundTexture.destroy(true); + pool.freeTexture(copyBackgroundTexture); this._copyBackgroundTexture = null; } } diff --git a/packages/core/src/RenderPipeline/DepthOnlyPass.ts b/packages/core/src/RenderPipeline/DepthOnlyPass.ts index 84aa8c74c..5e30ad850 100644 --- a/packages/core/src/RenderPipeline/DepthOnlyPass.ts +++ b/packages/core/src/RenderPipeline/DepthOnlyPass.ts @@ -63,8 +63,7 @@ export class DepthOnlyPass extends PipelinePass { release(): void { const renderTarget = this.renderTarget; if (renderTarget) { - renderTarget.depthTexture?.destroy(true); - renderTarget.destroy(true); + this.engine._renderTargetPool.freeRenderTarget(renderTarget); this.renderTarget = null; } } diff --git a/packages/core/src/RenderPipeline/PipelineUtils.ts b/packages/core/src/RenderPipeline/PipelineUtils.ts index 50cc808eb..e957d57f4 100644 --- a/packages/core/src/RenderPipeline/PipelineUtils.ts +++ b/packages/core/src/RenderPipeline/PipelineUtils.ts @@ -8,19 +8,6 @@ import { RenderTarget, Texture2D, TextureFilterMode, TextureFormat, TextureWrapM export class PipelineUtils { static readonly defaultViewport = new Vector4(0, 0, 1, 1); - /** - * Recreate texture if needed. - * @param engine - Engine - * @param currentTexture - Current texture - * @param width - Need texture width - * @param height - Need texture height - * @param format - Need texture format - * @param mipmap - Need texture mipmap - * @param isSRGBColorSpace - Whether to use sRGB color space - * @param textureWrapMode - Texture wrap mode - * @param textureFilterMode - Texture filter mode - * @returns Texture - */ static recreateTextureIfNeeded( engine: Engine, currentTexture: Texture2D | null, @@ -32,44 +19,26 @@ export class PipelineUtils { textureWrapMode: TextureWrapMode, textureFilterMode: TextureFilterMode ): Texture2D { - if (currentTexture) { - if ( - currentTexture.width !== width || - currentTexture.height !== height || - currentTexture.format !== format || - currentTexture.isSRGBColorSpace !== isSRGBColorSpace || - currentTexture.mipmapCount > 1 !== mipmap - ) { - currentTexture.destroy(true); - currentTexture = new Texture2D(engine, width, height, format, mipmap, isSRGBColorSpace); - currentTexture.isGCIgnored = true; - } - } else { - currentTexture = new Texture2D(engine, width, height, format, mipmap, isSRGBColorSpace); - currentTexture.isGCIgnored = true; + if ( + currentTexture && + currentTexture.width === width && + currentTexture.height === height && + currentTexture.format === format && + currentTexture.isSRGBColorSpace === isSRGBColorSpace && + currentTexture.mipmapCount > 1 === mipmap + ) { + currentTexture.wrapModeU = currentTexture.wrapModeV = textureWrapMode; + currentTexture.filterMode = textureFilterMode; + return currentTexture; } - currentTexture.wrapModeU = currentTexture.wrapModeV = textureWrapMode; - currentTexture.filterMode = textureFilterMode; - - return currentTexture; + const pool = engine._renderTargetPool; + if (currentTexture) { + pool.freeTexture(currentTexture); + } + return pool.allocateTexture(width, height, format, mipmap, isSRGBColorSpace, textureWrapMode, textureFilterMode); } - /** - * Recreate render target if needed. - * @param engine - Engine - * @param currentRenderTarget - Current render target - * @param width - Need render target width - * @param height - Need render target height - * @param colorFormat - Need render target color format - * @param depthFormat - Need render target depth format - * @param mipmap - Need render target mipmap - * @param isSRGBColorSpace - Whether to use sRGB color space - * @param antiAliasing - Need render target anti aliasing - * @param textureWrapMode - Texture wrap mode - * @param textureFilterMode - Texture filter mode - * @returns Render target - */ static recreateRenderTargetIfNeeded( engine: Engine, currentRenderTarget: RenderTarget | null, @@ -84,55 +53,70 @@ export class PipelineUtils { textureWrapMode: TextureWrapMode, textureFilterMode: TextureFilterMode ): RenderTarget { - const currentColorTexture = currentRenderTarget?.getColorTexture(0); - const colorTexture = - colorFormat != null - ? PipelineUtils.recreateTextureIfNeeded( - engine, - currentColorTexture, - width, - height, - colorFormat, - mipmap, - isSRGBColorSpace, - textureWrapMode, - textureFilterMode - ) - : null; + if (currentRenderTarget) { + const colorTexture = currentRenderTarget.getColorTexture(0) as Texture2D; + const depthTexture = currentRenderTarget.depthTexture as Texture2D; - if (needDepthTexture) { - const currentDepthTexture = currentRenderTarget?.depthTexture; - const needDepthTexture = depthFormat - ? PipelineUtils.recreateTextureIfNeeded( - engine, - currentDepthTexture, - width, - height, - depthFormat, - mipmap, - isSRGBColorSpace, - textureWrapMode, - textureFilterMode - ) - : null; + let matched = true; - if (currentColorTexture !== colorTexture || currentDepthTexture !== needDepthTexture) { - currentRenderTarget?.destroy(true); - currentRenderTarget = new RenderTarget(engine, width, height, colorTexture, needDepthTexture, antiAliasing); - currentRenderTarget.isGCIgnored = true; + if (colorFormat != null) { + if ( + !colorTexture || + colorTexture.width !== width || + colorTexture.height !== height || + colorTexture.format !== colorFormat || + colorTexture.isSRGBColorSpace !== isSRGBColorSpace || + colorTexture.mipmapCount > 1 !== mipmap + ) { + matched = false; + } + } else if (colorTexture) { + matched = false; } - } else { - if ( - currentColorTexture !== colorTexture || - currentRenderTarget?._depthFormat !== depthFormat || - currentRenderTarget.antiAliasing !== antiAliasing - ) { - currentRenderTarget?.destroy(true); - currentRenderTarget = new RenderTarget(engine, width, height, colorTexture, depthFormat, antiAliasing); - currentRenderTarget.isGCIgnored = true; + + if (matched && currentRenderTarget.antiAliasing !== antiAliasing) { + matched = false; } + + if (matched) { + if (needDepthTexture) { + if (depthFormat) { + if ( + !depthTexture || + depthTexture.width !== width || + depthTexture.height !== height || + depthTexture.format !== depthFormat + ) { + matched = false; + } + } else if (depthTexture) { + matched = false; + } + } else { + if (currentRenderTarget._depthFormat !== depthFormat) { + matched = false; + } + } + } + + if (matched) { + return currentRenderTarget; + } + + engine._renderTargetPool.freeRenderTarget(currentRenderTarget); } - return currentRenderTarget; + return engine._renderTargetPool.allocateRenderTarget( + width, + height, + colorFormat, + depthFormat, + needDepthTexture, + mipmap, + isSRGBColorSpace, + antiAliasing, + textureWrapMode, + textureFilterMode + ); } } diff --git a/packages/core/src/RenderPipeline/RenderTargetPool.ts b/packages/core/src/RenderPipeline/RenderTargetPool.ts new file mode 100644 index 000000000..3eae130d6 --- /dev/null +++ b/packages/core/src/RenderPipeline/RenderTargetPool.ts @@ -0,0 +1,199 @@ +import { Engine } from "../Engine"; +import { RenderTarget, Texture2D, TextureFilterMode, TextureFormat, TextureWrapMode } from "../texture"; + +/** + * @internal + */ +export class RenderTargetPool { + private _freeRenderTargets: RenderTarget[] = []; + private _freeTextures: Texture2D[] = []; + private _engine: Engine; + + constructor(engine: Engine) { + this._engine = engine; + } + + allocateRenderTarget( + width: number, + height: number, + colorFormat: TextureFormat | null, + depthFormat: TextureFormat | null, + needDepthTexture: boolean, + mipmap: boolean, + isSRGBColorSpace: boolean, + antiAliasing: number, + wrapMode: TextureWrapMode, + filterMode: TextureFilterMode + ): RenderTarget { + const freeRenderTargets = this._freeRenderTargets; + for (let i = freeRenderTargets.length - 1; i >= 0; i--) { + const renderTarget = freeRenderTargets[i]; + if ( + RenderTargetPool._matchRenderTarget( + renderTarget, + width, + height, + colorFormat, + depthFormat, + needDepthTexture, + mipmap, + isSRGBColorSpace, + antiAliasing + ) + ) { + freeRenderTargets[i] = freeRenderTargets[freeRenderTargets.length - 1]; + freeRenderTargets.length--; + const colorTexture = renderTarget.getColorTexture(0) as Texture2D; + if (colorTexture) { + colorTexture.wrapModeU = colorTexture.wrapModeV = wrapMode; + colorTexture.filterMode = filterMode; + } + const depthTexture = renderTarget.depthTexture as Texture2D; + if (depthTexture) { + depthTexture.wrapModeU = depthTexture.wrapModeV = wrapMode; + depthTexture.filterMode = filterMode; + } + return renderTarget; + } + } + + const engine = this._engine; + let colorTexture: Texture2D = null; + if (colorFormat != null) { + colorTexture = new Texture2D(engine, width, height, colorFormat, mipmap, isSRGBColorSpace); + colorTexture.isGCIgnored = true; + colorTexture.wrapModeU = colorTexture.wrapModeV = wrapMode; + colorTexture.filterMode = filterMode; + } + + let renderTarget: RenderTarget; + if (needDepthTexture) { + let depthTexture: Texture2D = null; + if (depthFormat) { + depthTexture = new Texture2D(engine, width, height, depthFormat, mipmap, isSRGBColorSpace); + depthTexture.isGCIgnored = true; + depthTexture.wrapModeU = depthTexture.wrapModeV = wrapMode; + depthTexture.filterMode = filterMode; + } + renderTarget = new RenderTarget(engine, width, height, colorTexture, depthTexture, antiAliasing); + } else { + renderTarget = new RenderTarget(engine, width, height, colorTexture, depthFormat, antiAliasing); + } + renderTarget.isGCIgnored = true; + + return renderTarget; + } + + allocateTexture( + width: number, + height: number, + format: TextureFormat, + mipmap: boolean, + isSRGBColorSpace: boolean, + wrapMode: TextureWrapMode, + filterMode: TextureFilterMode + ): Texture2D { + const freeTextures = this._freeTextures; + for (let i = freeTextures.length - 1; i >= 0; i--) { + const texture = freeTextures[i]; + if ( + texture.width === width && + texture.height === height && + texture.format === format && + texture.mipmapCount > 1 === mipmap && + texture.isSRGBColorSpace === isSRGBColorSpace + ) { + freeTextures[i] = freeTextures[freeTextures.length - 1]; + freeTextures.length--; + texture.wrapModeU = texture.wrapModeV = wrapMode; + texture.filterMode = filterMode; + return texture; + } + } + + const texture = new Texture2D(this._engine, width, height, format, mipmap, isSRGBColorSpace); + texture.isGCIgnored = true; + texture.wrapModeU = texture.wrapModeV = wrapMode; + texture.filterMode = filterMode; + + return texture; + } + + freeRenderTarget(renderTarget: RenderTarget): void { + if (!renderTarget || renderTarget.destroyed) return; + this._freeRenderTargets.push(renderTarget); + } + + freeTexture(texture: Texture2D): void { + if (!texture || texture.destroyed) return; + this._freeTextures.push(texture); + } + + gc(): void { + const freeRenderTargets = this._freeRenderTargets; + for (let i = 0, n = freeRenderTargets.length; i < n; i++) { + const renderTarget = freeRenderTargets[i]; + const colorTexture = renderTarget.getColorTexture(0); + const depthTexture = renderTarget.depthTexture; + renderTarget.destroy(true); + colorTexture?.destroy(true); + if (depthTexture && depthTexture !== colorTexture) { + depthTexture.destroy(true); + } + } + freeRenderTargets.length = 0; + + const freeTextures = this._freeTextures; + for (let i = 0, n = freeTextures.length; i < n; i++) { + freeTextures[i].destroy(true); + } + freeTextures.length = 0; + } + + private static _matchRenderTarget( + renderTarget: RenderTarget, + width: number, + height: number, + colorFormat: TextureFormat | null, + depthFormat: TextureFormat | null, + needDepthTexture: boolean, + mipmap: boolean, + isSRGBColorSpace: boolean, + antiAliasing: number + ): boolean { + if (renderTarget.width !== width || renderTarget.height !== height || renderTarget.antiAliasing !== antiAliasing) { + return false; + } + + const colorTexture = renderTarget.getColorTexture(0) as Texture2D; + if (colorFormat != null) { + if ( + !colorTexture || + colorTexture.format !== colorFormat || + colorTexture.mipmapCount > 1 !== mipmap || + colorTexture.isSRGBColorSpace !== isSRGBColorSpace + ) { + return false; + } + } else if (colorTexture) { + return false; + } + + const depthTexture = renderTarget.depthTexture; + if (needDepthTexture) { + if (depthFormat) { + if (!depthTexture || (depthTexture as Texture2D).format !== depthFormat) { + return false; + } + } else if (depthTexture) { + return false; + } + } else { + if (renderTarget._depthFormat !== depthFormat) { + return false; + } + } + + return true; + } +} diff --git a/packages/core/src/asset/GraphicsResource.ts b/packages/core/src/asset/GraphicsResource.ts index 2be9c4b85..ee27f449b 100644 --- a/packages/core/src/asset/GraphicsResource.ts +++ b/packages/core/src/asset/GraphicsResource.ts @@ -15,6 +15,7 @@ export abstract class GraphicsResource extends ReferResource { protected constructor(engine: Engine) { super(engine); engine.resourceManager._addGraphicResource(this); + this._isContentLost = engine._isDeviceLost; } /** diff --git a/packages/core/src/asset/ReferResource.ts b/packages/core/src/asset/ReferResource.ts index 8f78a3dba..1460d048e 100644 --- a/packages/core/src/asset/ReferResource.ts +++ b/packages/core/src/asset/ReferResource.ts @@ -37,7 +37,7 @@ export abstract class ReferResource extends EngineObject implements IReferable { override destroy(force: boolean, isGC: boolean): boolean; override destroy(force: boolean = false, isGC?: boolean): boolean { - if (!force) { + if (!this._pendingDestroy && !force) { if (this._refCount !== 0) { return false; } diff --git a/packages/core/src/asset/RenderingStatistics.ts b/packages/core/src/asset/RenderingStatistics.ts new file mode 100644 index 000000000..8ba643678 --- /dev/null +++ b/packages/core/src/asset/RenderingStatistics.ts @@ -0,0 +1,38 @@ +/** + * Rendering statistics. + */ +export class RenderingStatistics { + /** @internal */ + _textureMemory: number = 0; + /** @internal */ + _bufferMemory: number = 0; + + /** + * Memory used by all textures, in bytes. + */ + get textureMemory(): number { + return this._textureMemory; + } + + /** + * Memory used by all buffers, in bytes. + */ + get bufferMemory(): number { + return this._bufferMemory; + } + + /** + * Total memory used, in bytes. + */ + get totalMemory(): number { + return this._textureMemory + this._bufferMemory; + } + + /** + * @internal + */ + _reset(): void { + this._textureMemory = 0; + this._bufferMemory = 0; + } +} diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 85ae63907..c2fc4b9eb 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -170,6 +170,7 @@ export class ResourceManager { */ gc(): void { this._gc(false); + this.engine._renderTargetPool.gc(); this.engine._pendingGC(); } diff --git a/packages/core/src/graphic/Buffer.ts b/packages/core/src/graphic/Buffer.ts index a5ae2e06f..3a8cd911e 100644 --- a/packages/core/src/graphic/Buffer.ts +++ b/packages/core/src/graphic/Buffer.ts @@ -125,6 +125,10 @@ export class Buffer extends GraphicsResource { this._data = new Uint8Array(buffer); } } + + if (!engine._isDeviceLost) { + engine._renderingStatistics._bufferMemory += this._byteLength; + } } /** @@ -238,6 +242,7 @@ export class Buffer extends GraphicsResource { this._bufferUsage ); this._platformBuffer = platformBuffer; + this._engine._renderingStatistics._bufferMemory += this._byteLength; } /** @@ -245,6 +250,9 @@ export class Buffer extends GraphicsResource { */ protected override _onDestroy() { super._onDestroy(); + if (!this._engine._isDeviceLost) { + this._engine._renderingStatistics._bufferMemory -= this._byteLength; + } this._platformBuffer.destroy(); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bc936042a..ccd80cb11 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,6 +24,7 @@ export { request } from "./asset/request"; export type { RequestConfig } from "./asset/request"; export { Loader } from "./asset/Loader"; export { ContentRestorer } from "./asset/ContentRestorer"; +export { RenderingStatistics } from "./asset/RenderingStatistics"; export { ResourceManager, resourceLoader } from "./asset/ResourceManager"; export { AssetPromise } from "./asset/AssetPromise"; export type { LoadItem } from "./asset/LoadItem"; diff --git a/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts b/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts index 14c67d91a..697e6e265 100644 --- a/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts +++ b/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts @@ -160,14 +160,13 @@ export class ScalableAmbientObscurancePass extends PipelinePass { } release(): void { + const pool = this.engine._renderTargetPool; if (this._saoRenderTarget) { - this._saoRenderTarget.getColorTexture(0)?.destroy(true); - this._saoRenderTarget.destroy(true); + pool.freeRenderTarget(this._saoRenderTarget); this._saoRenderTarget = null; } if (this._blurRenderTarget) { - this._blurRenderTarget.getColorTexture(0)?.destroy(true); - this._blurRenderTarget.destroy(true); + pool.freeRenderTarget(this._blurRenderTarget); this._blurRenderTarget = null; } this._depthRenderTarget = null; diff --git a/packages/core/src/postProcess/FinalPass.ts b/packages/core/src/postProcess/FinalPass.ts index cba60a35b..1342f3ab3 100644 --- a/packages/core/src/postProcess/FinalPass.ts +++ b/packages/core/src/postProcess/FinalPass.ts @@ -86,8 +86,7 @@ export class FinalPass extends PipelinePass { release(): void { const srgbRenderTarget = this._srgbRenderTarget; if (srgbRenderTarget) { - srgbRenderTarget.getColorTexture(0)?.destroy(true); - srgbRenderTarget.destroy(true); + this.engine._renderTargetPool.freeRenderTarget(srgbRenderTarget); this._srgbRenderTarget = null; } this._inputRenderTarget = null; diff --git a/packages/core/src/postProcess/PostProcessManager.ts b/packages/core/src/postProcess/PostProcessManager.ts index 8b6058bad..a2dcd674d 100644 --- a/packages/core/src/postProcess/PostProcessManager.ts +++ b/packages/core/src/postProcess/PostProcessManager.ts @@ -195,8 +195,7 @@ export class PostProcessManager { _releaseSwapRenderTarget(): void { const swapRenderTarget = this._swapRenderTarget; if (swapRenderTarget) { - swapRenderTarget.getColorTexture(0)?.destroy(true); - swapRenderTarget.destroy(true); + this.scene.engine._renderTargetPool.freeRenderTarget(swapRenderTarget); this._swapRenderTarget = null; } } @@ -207,8 +206,7 @@ export class PostProcessManager { _releaseOutputRenderTarget(): void { const outputRenderTarget = this._outputRenderTarget; if (outputRenderTarget) { - outputRenderTarget.getColorTexture(0)?.destroy(true); - outputRenderTarget.destroy(true); + this.scene.engine._renderTargetPool.freeRenderTarget(outputRenderTarget); this._outputRenderTarget = null; } } diff --git a/packages/core/src/postProcess/PostProcessUberPass.ts b/packages/core/src/postProcess/PostProcessUberPass.ts index cd1202e84..41ba06666 100644 --- a/packages/core/src/postProcess/PostProcessUberPass.ts +++ b/packages/core/src/postProcess/PostProcessUberPass.ts @@ -247,17 +247,16 @@ export class PostProcessUberPass extends PostProcessPass { } private _releaseBloomRenderTargets(): void { + const pool = this.engine._renderTargetPool; const length = this._mipDownRT.length; for (let i = 0; i < length; i++) { const downRT = this._mipDownRT[i]; const upRT = this._mipUpRT[i]; if (downRT) { - downRT.getColorTexture(0).destroy(true); - downRT.destroy(true); + pool.freeRenderTarget(downRT); } if (upRT) { - upRT.getColorTexture(0).destroy(true); - upRT.destroy(true); + pool.freeRenderTarget(upRT); } } this._mipDownRT.length = 0; diff --git a/packages/core/src/texture/RenderTarget.ts b/packages/core/src/texture/RenderTarget.ts index 965b5fc2f..8ed286859 100644 --- a/packages/core/src/texture/RenderTarget.ts +++ b/packages/core/src/texture/RenderTarget.ts @@ -1,9 +1,12 @@ import { GraphicsResource } from "../asset/GraphicsResource"; +import { Logger } from "../base/Logger"; import { Engine } from "../Engine"; import { IPlatformRenderTarget } from "../renderingHardwareInterface"; import { RenderBufferDepthFormat } from "./enums/RenderBufferDepthFormat"; import { TextureFormat } from "./enums/TextureFormat"; import { Texture } from "./Texture"; +import { TextureCube } from "./TextureCube"; +import { TextureUtils } from "./TextureUtils"; /** * The render target used for off-screen rendering. @@ -24,6 +27,7 @@ export class RenderTarget extends GraphicsResource { private _height: number; private _colorTextures: Texture[]; private _depthTexture: Texture | null = null; + private _memorySize: number = 0; /** * Whether to automatically generate multi-level textures. @@ -160,9 +164,16 @@ export class RenderTarget extends GraphicsResource { this._width = width; this._height = height; - this._antiAliasing = antiAliasing; this._depth = depth; + const maxAntiAliasing = engine._hardwareRenderer.capability.maxAntiAliasing; + if (antiAliasing > maxAntiAliasing) { + Logger.warn(`MSAA antiAliasing exceeds the limit and is automatically downgraded to:${maxAntiAliasing}`); + antiAliasing = maxAntiAliasing; + } + this._antiAliasing = antiAliasing; + + let memorySize = 0; if (renderTexture) { const colorTextures = renderTexture instanceof Array ? renderTexture.slice() : [renderTexture]; for (let i = 0, n = colorTextures.length; i < n; i++) { @@ -171,6 +182,9 @@ export class RenderTarget extends GraphicsResource { throw "Render texture can't use depth format."; } colorTexture._addReferCount(1); + if (antiAliasing > 1) { + memorySize += TextureUtils.getMipLevelByteCount(colorTexture.format, width, height); + } } this._colorTextures = colorTextures; } else { @@ -184,11 +198,25 @@ export class RenderTarget extends GraphicsResource { this._depthTexture = depth; this._depthTexture._addReferCount(1); this._depthFormat = depth.format; + // MSAA depth RBO or non-MSAA cube depth RBO + if (antiAliasing > 1 || depth instanceof TextureCube) { + memorySize += TextureUtils.getMipLevelByteCount(depth.format, width, height); + } } else if (typeof depth === "number") { this._depthFormat = depth; + // Depth format always needs a RBO + memorySize += TextureUtils.getMipLevelByteCount(depth, width, height); + } + + if (antiAliasing > 1) { + memorySize *= antiAliasing; } this._platformRenderTarget = engine._hardwareRenderer.createPlatformRenderTarget(this); + this._memorySize = memorySize; + if (!engine._isDeviceLost) { + engine._renderingStatistics._textureMemory += memorySize; + } } /** @@ -218,6 +246,9 @@ export class RenderTarget extends GraphicsResource { */ protected override _onDestroy(): void { super._onDestroy(); + if (!this._engine._isDeviceLost) { + this._engine._renderingStatistics._textureMemory -= this._memorySize; + } this._platformRenderTarget.destroy(); const { _colorTextures: colorTextures } = this; for (let i = 0, n = colorTextures.length; i < n; i++) { @@ -241,5 +272,6 @@ export class RenderTarget extends GraphicsResource { */ override _rebuild(): void { this._platformRenderTarget = this._engine._hardwareRenderer.createPlatformRenderTarget(this); + this._engine._renderingStatistics._textureMemory += this._memorySize; } } diff --git a/packages/core/src/texture/Texture.ts b/packages/core/src/texture/Texture.ts index 8fdfdbe92..d775f7e0c 100644 --- a/packages/core/src/texture/Texture.ts +++ b/packages/core/src/texture/Texture.ts @@ -21,6 +21,8 @@ export abstract class Texture extends GraphicsResource { _mipmap: boolean; /** @internal */ _isDepthTexture: boolean = false; + /** @internal */ + _memorySize: number = 0; protected _format: TextureFormat; protected _width: number; @@ -236,6 +238,9 @@ export abstract class Texture extends GraphicsResource { */ protected override _onDestroy() { super._onDestroy(); + if (!this._engine._isDeviceLost) { + this._engine._renderingStatistics._textureMemory -= this._memorySize; + } this._platformTexture.destroy(); this._platformTexture = null; } diff --git a/packages/core/src/texture/Texture2D.ts b/packages/core/src/texture/Texture2D.ts index 94476d377..11f820822 100644 --- a/packages/core/src/texture/Texture2D.ts +++ b/packages/core/src/texture/Texture2D.ts @@ -5,6 +5,7 @@ import { TextureFormat } from "./enums/TextureFormat"; import { TextureUsage } from "./enums/TextureUsage"; import { TextureWrapMode } from "./enums/TextureWrapMode"; import { Texture } from "./Texture"; +import { TextureUtils } from "./TextureUtils"; /** * Two-dimensional texture. @@ -36,6 +37,11 @@ export class Texture2D extends Texture { this._platformTexture = engine._hardwareRenderer.createPlatformTexture2D(this); this.filterMode = this._isIntFormat() ? TextureFilterMode.Point : TextureFilterMode.Bilinear; this.wrapModeU = this.wrapModeV = TextureWrapMode.Repeat; + + this._memorySize = TextureUtils.getTextureByteCount(format, width, height, this._mipmapCount, 1); + if (!engine._isDeviceLost) { + engine._renderingStatistics._textureMemory += this._memorySize; + } } /** @@ -168,6 +174,7 @@ export class Texture2D extends Texture { */ override _rebuild(): void { this._platformTexture = this._engine._hardwareRenderer.createPlatformTexture2D(this); + this._engine._renderingStatistics._textureMemory += this._memorySize; super._rebuild(); } } diff --git a/packages/core/src/texture/Texture2DArray.ts b/packages/core/src/texture/Texture2DArray.ts index 9dc9d9d57..6f9b88d00 100644 --- a/packages/core/src/texture/Texture2DArray.ts +++ b/packages/core/src/texture/Texture2DArray.ts @@ -4,6 +4,7 @@ import { TextureFilterMode } from "./enums/TextureFilterMode"; import { TextureFormat } from "./enums/TextureFormat"; import { TextureWrapMode } from "./enums/TextureWrapMode"; import { Texture } from "./Texture"; +import { TextureUtils } from "./TextureUtils"; /** * Two-dimensional texture array. @@ -47,6 +48,11 @@ export class Texture2DArray extends Texture { this._platformTexture = engine._hardwareRenderer.createPlatformTexture2DArray(this); this.filterMode = TextureFilterMode.Bilinear; this.wrapModeU = this.wrapModeV = TextureWrapMode.Repeat; + + this._memorySize = TextureUtils.getTextureByteCount(format, width, height, this._mipmapCount, length); + if (!engine._isDeviceLost) { + engine._renderingStatistics._textureMemory += this._memorySize; + } } /** @@ -219,6 +225,7 @@ export class Texture2DArray extends Texture { */ override _rebuild(): void { this._platformTexture = this._engine._hardwareRenderer.createPlatformTexture2DArray(this); + this._engine._renderingStatistics._textureMemory += this._memorySize; super._rebuild(); } } diff --git a/packages/core/src/texture/TextureCube.ts b/packages/core/src/texture/TextureCube.ts index 372fd6c32..27ec3652a 100644 --- a/packages/core/src/texture/TextureCube.ts +++ b/packages/core/src/texture/TextureCube.ts @@ -5,6 +5,7 @@ import { TextureFilterMode } from "./enums/TextureFilterMode"; import { TextureFormat } from "./enums/TextureFormat"; import { TextureWrapMode } from "./enums/TextureWrapMode"; import { Texture } from "./Texture"; +import { TextureUtils } from "./TextureUtils"; /** * Cube texture. @@ -24,6 +25,11 @@ export class TextureCube extends Texture { this._platformTexture = engine._hardwareRenderer.createPlatformTextureCube(this); this.filterMode = TextureFilterMode.Bilinear; this.wrapModeU = this.wrapModeV = TextureWrapMode.Clamp; + + this._memorySize = TextureUtils.getTextureByteCount(format, size, size, this._mipmapCount, 6); + if (!engine._isDeviceLost) { + engine._renderingStatistics._textureMemory += this._memorySize; + } } /** @@ -188,6 +194,7 @@ export class TextureCube extends Texture { */ override _rebuild(): void { this._platformTexture = this._engine._hardwareRenderer.createPlatformTextureCube(this); + this._engine._renderingStatistics._textureMemory += this._memorySize; super._rebuild(); } } diff --git a/packages/core/src/texture/TextureUtils.ts b/packages/core/src/texture/TextureUtils.ts index 0e99e9513..3a3fa9447 100644 --- a/packages/core/src/texture/TextureUtils.ts +++ b/packages/core/src/texture/TextureUtils.ts @@ -66,4 +66,106 @@ export class TextureUtils { return true; } + + static getTextureByteCount( + format: TextureFormat, + width: number, + height: number, + mipmapCount: number, + faceCount: number + ): number { + let totalBytes = 0; + for (let i = 0; i < mipmapCount; i++) { + const mipWidth = Math.max(1, width >> i); + const mipHeight = Math.max(1, height >> i); + totalBytes += TextureUtils.getMipLevelByteCount(format, mipWidth, mipHeight); + } + return totalBytes * faceCount; + } + + static getMipLevelByteCount(format: TextureFormat, width: number, height: number): number { + switch (format) { + // Uncompressed formats + case TextureFormat.R8: + case TextureFormat.Alpha8: + return width * height; + case TextureFormat.R8G8: + case TextureFormat.LuminanceAlpha: + return width * height * 2; + case TextureFormat.R8G8B8: + return width * height * 3; + case TextureFormat.R8G8B8A8: + case TextureFormat.R11G11B10_UFloat: + return width * height * 4; + case TextureFormat.R4G4B4A4: + case TextureFormat.R5G5B5A1: + case TextureFormat.R5G6B5: + return width * height * 2; + case TextureFormat.R16G16B16A16: + return width * height * 8; + case TextureFormat.R32G32B32A32: + case TextureFormat.R32G32B32A32_UInt: + return width * height * 16; + + // BC compressed (4x4 blocks) + case TextureFormat.BC1: + return Math.ceil(width / 4) * Math.ceil(height / 4) * 8; + case TextureFormat.BC3: + case TextureFormat.BC7: + case TextureFormat.BC6H: + return Math.ceil(width / 4) * Math.ceil(height / 4) * 16; + + // ETC compressed (4x4 blocks) + case TextureFormat.ETC1_RGB: + case TextureFormat.ETC2_RGB: + case TextureFormat.ETC2_RGBA5: + return Math.ceil(width / 4) * Math.ceil(height / 4) * 8; + case TextureFormat.ETC2_RGBA8: + return Math.ceil(width / 4) * Math.ceil(height / 4) * 16; + + // PVRTC compressed + case TextureFormat.PVRTC_RGB2: + case TextureFormat.PVRTC_RGBA2: + return (Math.max(width, 16) * Math.max(height, 8) * 2) / 8; + case TextureFormat.PVRTC_RGB4: + case TextureFormat.PVRTC_RGBA4: + return (Math.max(width, 8) * Math.max(height, 8) * 4) / 8; + + // ASTC compressed (16 bytes per block) + case TextureFormat.ASTC_4x4: + return Math.ceil(width / 4) * Math.ceil(height / 4) * 16; + case TextureFormat.ASTC_5x5: + return Math.ceil(width / 5) * Math.ceil(height / 5) * 16; + case TextureFormat.ASTC_6x6: + return Math.ceil(width / 6) * Math.ceil(height / 6) * 16; + case TextureFormat.ASTC_8x8: + return Math.ceil(width / 8) * Math.ceil(height / 8) * 16; + case TextureFormat.ASTC_10x10: + return Math.ceil(width / 10) * Math.ceil(height / 10) * 16; + case TextureFormat.ASTC_12x12: + return Math.ceil(width / 12) * Math.ceil(height / 12) * 16; + + // Depth / stencil formats + case TextureFormat.Depth16: + return width * height * 2; + case TextureFormat.Depth24: + case TextureFormat.Depth24Stencil8: + case TextureFormat.Depth32: + return width * height * 4; + case TextureFormat.Depth32Stencil8: + return width * height * 8; + + // STENCIL_INDEX8: 1 byte per pixel + case TextureFormat.Stencil: + return width * height; + + // Auto depth/stencil (conservative estimate) + case TextureFormat.Depth: + case TextureFormat.DepthStencil: + return width * height * 4; + + default: + return width * height * 4; + } + } } diff --git a/packages/rhi-webgl/src/GLRenderTarget.ts b/packages/rhi-webgl/src/GLRenderTarget.ts index 0ae8338ec..1265dc62c 100644 --- a/packages/rhi-webgl/src/GLRenderTarget.ts +++ b/packages/rhi-webgl/src/GLRenderTarget.ts @@ -1,7 +1,6 @@ import { GLCapabilityType, IPlatformRenderTarget, - Logger, RenderTarget, Texture, TextureCube, @@ -76,14 +75,6 @@ export class GLRenderTarget implements IPlatformRenderTarget { throw new Error("MRT+Cube+[,MSAA] is not supported"); } - const maxAntiAliasing = rhi.capability.maxAntiAliasing; - if (target.antiAliasing > maxAntiAliasing) { - Logger.warn(`MSAA antiAliasing exceeds the limit and is automatically downgraded to:${maxAntiAliasing}`); - - /** @ts-ignore */ - target._antiAliasing = maxAntiAliasing; - } - this._frameBuffer = this._gl.createFramebuffer(); // bind main FBO diff --git a/tests/src/core/RenderingStatistics.test.ts b/tests/src/core/RenderingStatistics.test.ts new file mode 100644 index 000000000..9fc79281f --- /dev/null +++ b/tests/src/core/RenderingStatistics.test.ts @@ -0,0 +1,278 @@ +import { + Buffer, + BufferBindFlag, + BufferUsage, + RenderTarget, + Texture2D, + TextureCube, + TextureFormat +} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { beforeAll, describe, expect, it } from "vitest"; + +describe("RenderingStatistics", () => { + const canvas = document.createElement("canvas"); + + let engine: WebGLEngine; + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas }); + }); + + describe("textureMemory", () => { + it("Texture2D creation increases textureMemory", () => { + const before = engine.renderingStatistics.textureMemory; + const texture = new Texture2D(engine, 256, 256, TextureFormat.R8G8B8A8, false, false); + const after = engine.renderingStatistics.textureMemory; + + // 256 * 256 * 4 = 262144 + expect(after - before).to.equal(256 * 256 * 4); + + texture.destroy(); + }); + + it("Texture2D with mipmaps accounts for all levels", () => { + const before = engine.renderingStatistics.textureMemory; + const texture = new Texture2D(engine, 256, 256, TextureFormat.R8G8B8A8, true, false); + const after = engine.renderingStatistics.textureMemory; + + // With mipmaps: 256x256 + 128x128 + 64x64 + ... + 1x1, all * 4 bytes + expect(after - before).to.be.greaterThan(256 * 256 * 4); + + texture.destroy(); + }); + + it("TextureCube accounts for 6 faces", () => { + const before = engine.renderingStatistics.textureMemory; + const texture = new TextureCube(engine, 64, TextureFormat.R8G8B8A8, false); + const after = engine.renderingStatistics.textureMemory; + + // 64 * 64 * 4 * 6 faces = 98304 + expect(after - before).to.equal(64 * 64 * 4 * 6); + + texture.destroy(); + }); + + it("Texture destroy decreases textureMemory", () => { + const before = engine.renderingStatistics.textureMemory; + const texture = new Texture2D(engine, 128, 128, TextureFormat.R8G8B8A8, false, false); + texture.destroy(); + const after = engine.renderingStatistics.textureMemory; + + expect(after).to.equal(before); + }); + }); + + describe("bufferMemory", () => { + it("Buffer creation increases bufferMemory", () => { + const before = engine.renderingStatistics.bufferMemory; + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 1024, BufferUsage.Static); + const after = engine.renderingStatistics.bufferMemory; + + expect(after - before).to.equal(1024); + + buffer.destroy(); + }); + + it("Buffer destroy decreases bufferMemory", () => { + const before = engine.renderingStatistics.bufferMemory; + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 2048, BufferUsage.Static); + buffer.destroy(); + const after = engine.renderingStatistics.bufferMemory; + + expect(after).to.equal(before); + }); + }); + + describe("totalMemory", () => { + it("totalMemory equals textureMemory + bufferMemory", () => { + const texture = new Texture2D(engine, 64, 64, TextureFormat.R8G8B8A8, false, false); + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 512, BufferUsage.Static); + + expect(engine.renderingStatistics.totalMemory).to.equal( + engine.renderingStatistics.textureMemory + engine.renderingStatistics.bufferMemory + ); + + texture.destroy(); + buffer.destroy(); + }); + }); + + describe("RenderTarget renderbuffer", () => { + it("RenderTarget with depth format tracks renderbuffer memory", () => { + const colorTexture = new Texture2D(engine, 256, 256, TextureFormat.R8G8B8A8, false, false); + const afterTexture = engine.renderingStatistics.textureMemory; + + const renderTarget = new RenderTarget(engine, 256, 256, colorTexture, TextureFormat.Depth24Stencil8); + const afterRT = engine.renderingStatistics.textureMemory; + + // Depth renderbuffer: 256 * 256 * 4 + expect(afterRT - afterTexture).to.equal(256 * 256 * 4); + + renderTarget.destroy(); + colorTexture.destroy(); + }); + + it("RenderTarget destroy decreases renderbuffer memory", () => { + const colorTexture = new Texture2D(engine, 128, 128, TextureFormat.R8G8B8A8, false, false); + const before = engine.renderingStatistics.textureMemory; + + const renderTarget = new RenderTarget(engine, 128, 128, colorTexture, TextureFormat.Depth24Stencil8); + renderTarget.destroy(); + + const after = engine.renderingStatistics.textureMemory; + expect(after).to.equal(before); + + colorTexture.destroy(); + }); + + it("RenderTarget with depth texture in non-MSAA mode does not add depth renderbuffer memory", () => { + const colorTexture = new Texture2D(engine, 128, 128, TextureFormat.R8G8B8A8, false, false); + const depthTexture = new Texture2D(engine, 128, 128, TextureFormat.Depth24Stencil8, false, false); + const before = engine.renderingStatistics.textureMemory; + + const renderTarget = new RenderTarget(engine, 128, 128, colorTexture, depthTexture, 1); + const after = engine.renderingStatistics.textureMemory; + expect(after).to.equal(before); + + renderTarget.destroy(); + colorTexture.destroy(); + depthTexture.destroy(); + }); + }); + + describe("device loss", () => { + it("counters reset on device lost and recover on rebuild", async () => { + const texture = new Texture2D(engine, 64, 64, TextureFormat.R8G8B8A8, false, false); + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 512, BufferUsage.Static); + + const totalBefore = engine.renderingStatistics.totalMemory; + const resourceSize = 64 * 64 * 4 + 512; + expect(totalBefore).to.be.greaterThan(0); + + await new Promise((resolve) => { + engine.once("devicelost", () => { + expect(engine.renderingStatistics.textureMemory).to.equal(0); + expect(engine.renderingStatistics.bufferMemory).to.equal(0); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + engine.once("devicerestored", () => { + expect(engine.renderingStatistics.totalMemory).to.equal(totalBefore); + resolve(); + }); + // @ts-ignore + engine._onDeviceRestored(); + }); + // @ts-ignore + engine._onDeviceLost(); + }); + + const beforeDestroy = engine.renderingStatistics.totalMemory; + texture.destroy(); + buffer.destroy(); + expect(engine.renderingStatistics.totalMemory).to.equal(beforeDestroy - resourceSize); + }); + + it("failed restore keeps device-lost state and avoids negative counters on destroy", async () => { + const texture = new Texture2D(engine, 64, 64, TextureFormat.R8G8B8A8, false, false); + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 512, BufferUsage.Static); + + // @ts-ignore + const resourceManager = engine.resourceManager; + const originalRestoreGraphicResources = resourceManager._restoreGraphicResources; + // @ts-ignore + resourceManager._restoreGraphicResources = () => { + throw new Error("mock restore failure"); + }; + + try { + // @ts-ignore + engine._onDeviceLost(); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + // @ts-ignore + expect(engine._isDeviceLost).to.equal(true); + + expect(() => { + // @ts-ignore + engine._onDeviceRestored(); + }).toThrow("mock restore failure"); + // @ts-ignore + expect(engine._isDeviceLost).to.equal(true); + } finally { + // @ts-ignore + resourceManager._restoreGraphicResources = originalRestoreGraphicResources; + } + + texture.destroy(); + buffer.destroy(); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + // Restore engine state for subsequent tests + // @ts-ignore + engine._onDeviceRestored(); + await new Promise((resolve) => { + engine.once("devicerestored", () => resolve()); + }); + }); + + it("resources created and destroyed during device lost should not affect counters", async () => { + const baseTexture = new Texture2D(engine, 64, 64, TextureFormat.R8G8B8A8, false, false); + const baseBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 512, BufferUsage.Static); + const totalBefore = engine.renderingStatistics.totalMemory; + + await new Promise((resolve) => { + engine.once("devicelost", () => { + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + const transientTexture = new Texture2D(engine, 32, 32, TextureFormat.R8G8B8A8, false, false); + const transientBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 256, BufferUsage.Static); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + transientTexture.destroy(); + transientBuffer.destroy(); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + engine.once("devicerestored", () => { + expect(engine.renderingStatistics.totalMemory).to.equal(totalBefore); + resolve(); + }); + // @ts-ignore + engine._onDeviceRestored(); + }); + // @ts-ignore + engine._onDeviceLost(); + }); + + baseTexture.destroy(); + baseBuffer.destroy(); + }); + + it("resources created during device lost should be counted once after restore", async () => { + // Record baseline before device loss (includes engine internal resources) + const baselineTotal = engine.renderingStatistics.totalMemory; + const resourceSize = 32 * 32 * 4 + 256; + + await new Promise((resolve) => { + engine.once("devicelost", () => { + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + const texture = new Texture2D(engine, 32, 32, TextureFormat.R8G8B8A8, false, false); + const buffer = new Buffer(engine, BufferBindFlag.VertexBuffer, 256, BufferUsage.Static); + expect(engine.renderingStatistics.totalMemory).to.equal(0); + + engine.once("devicerestored", () => { + // Should include engine internal resources + newly created resources + expect(engine.renderingStatistics.totalMemory).to.equal(baselineTotal + resourceSize); + texture.destroy(); + buffer.destroy(); + expect(engine.renderingStatistics.totalMemory).to.equal(baselineTotal); + resolve(); + }); + // @ts-ignore + engine._onDeviceRestored(); + }); + // @ts-ignore + engine._onDeviceLost(); + }); + }); + }); +}); diff --git a/tests/src/core/resource/ReferResource.test.ts b/tests/src/core/resource/ReferResource.test.ts new file mode 100644 index 000000000..e022775e8 --- /dev/null +++ b/tests/src/core/resource/ReferResource.test.ts @@ -0,0 +1,58 @@ +import { Texture2D } from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { beforeAll, describe, expect, it } from "vitest"; + +describe("ReferResource", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + engine.run(); + }); + + it("pending destroy should still destroy even if refCount increases during delay", () => { + const texture = new Texture2D(engine, 1, 1); + + // @ts-ignore + engine._frameInProcess = true; + texture.destroy(); + // @ts-ignore + engine._frameInProcess = false; + + expect(texture.pendingDestroy).eq(true); + + // During pending period, refCount increases (someone references it again) + // @ts-ignore + texture._addReferCount(1); + expect(texture.refCount).eq(1); + + // End-of-frame: should still destroy (destroy decision is final) + // @ts-ignore + engine._processPendingDestroyObjects(); + + expect(texture.destroyed).eq(true); + }); + + it("pending force destroy should not fail due to lost force flag at end of frame", () => { + const texture = new Texture2D(engine, 1, 1); + // @ts-ignore + texture._addReferCount(3); + + // In-frame: destroy(true) bypasses refCount check, but gets deferred + // @ts-ignore + engine._frameInProcess = true; + texture.destroy(true); + // @ts-ignore + engine._frameInProcess = false; + + expect(texture.pendingDestroy).eq(true); + expect(texture.destroyed).eq(false); + + // End-of-frame: _processPendingDestroyObjects calls destroy() without args, + // force flag must not be lost, otherwise refCount=3 would block destruction + // @ts-ignore + engine._processPendingDestroyObjects(); + + expect(texture.destroyed).eq(true); + }); +});