Add RenderingStatistics for GPU memory tracking and RenderTargetPool for RT reuse (#2910)

* feat: add RenderingInfo class for tracking rendering memory usage and RenderTargetPool for RT reuse
This commit is contained in:
ChenMo
2026-03-03 18:25:14 +08:00
committed by GitHub
parent 1491cfc995
commit ea2f92595f
24 changed files with 867 additions and 121 deletions

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 = <Texture2D>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 = <Texture2D>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
);
}
}

View File

@@ -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;
}
}

View File

@@ -15,6 +15,7 @@ export abstract class GraphicsResource extends ReferResource {
protected constructor(engine: Engine) {
super(engine);
engine.resourceManager._addGraphicResource(this);
this._isContentLost = engine._isDeviceLost;
}
/**

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -170,6 +170,7 @@ export class ResourceManager {
*/
gc(): void {
this._gc(false);
this.engine._renderTargetPool.gc();
this.engine._pendingGC();
}

View File

@@ -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();
}
}

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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 = <Texture | null | TextureFormat>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 = <TextureFormat>depth;
// Depth format always needs a RBO
memorySize += TextureUtils.getMipLevelByteCount(<TextureFormat>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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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<void>((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<void>((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<void>((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<void>((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();
});
});
});
});

View File

@@ -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);
});
});