mirror of
https://github.com/galacean/engine.git
synced 2026-06-02 16:52:48 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
199
packages/core/src/RenderPipeline/RenderTargetPool.ts
Normal file
199
packages/core/src/RenderPipeline/RenderTargetPool.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export abstract class GraphicsResource extends ReferResource {
|
||||
protected constructor(engine: Engine) {
|
||||
super(engine);
|
||||
engine.resourceManager._addGraphicResource(this);
|
||||
this._isContentLost = engine._isDeviceLost;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
38
packages/core/src/asset/RenderingStatistics.ts
Normal file
38
packages/core/src/asset/RenderingStatistics.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@ export class ResourceManager {
|
||||
*/
|
||||
gc(): void {
|
||||
this._gc(false);
|
||||
this.engine._renderTargetPool.gc();
|
||||
this.engine._pendingGC();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
278
tests/src/core/RenderingStatistics.test.ts
Normal file
278
tests/src/core/RenderingStatistics.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
58
tests/src/core/resource/ReferResource.test.ts
Normal file
58
tests/src/core/resource/ReferResource.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user