Skip to content
Draft
157 changes: 157 additions & 0 deletions examples/src/text-chunk-leak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @title Text Chunk Leak
* @category 2D
*/
import {
Camera,
Color,
Entity,
Script,
WebGLEngine,
TextHorizontalAlignment,
} from "@galacean/engine";
import {
CanvasRenderMode,
registerGUI,
Text,
UICanvas,
UITransform,
} from "@galacean/engine-ui";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prettier violations here will break lint checks.

Static analysis already flags import formatting (Line 5–19) and a trailing comma in WebGLEngine.create (Line 72).

🧹 Suggested formatting fix
 import {
-  Camera,
-  Color,
-  Entity,
-  Script,
-  WebGLEngine,
-  TextHorizontalAlignment,
+  Camera, Color, Entity, Script, WebGLEngine, TextHorizontalAlignment
 } from "@galacean/engine";
 import {
-  CanvasRenderMode,
-  registerGUI,
-  Text,
-  UICanvas,
-  UITransform,
+  CanvasRenderMode, registerGUI, Text, UICanvas, UITransform
 } from "@galacean/engine-ui";

 async function main() {
   const engine = await WebGLEngine.create({
-    canvas: document.getElementById("canvas") as HTMLCanvasElement,
+    canvas: document.getElementById("canvas") as HTMLCanvasElement
   });

Also applies to: 72-72

🧰 Tools
🪛 ESLint

[error] 5-12: Replace ⏎··Camera,⏎··Color,⏎··Entity,⏎··Script,⏎··WebGLEngine,⏎··TextHorizontalAlignment,⏎ with ·Camera,·Color,·Entity,·Script,·WebGLEngine,·TextHorizontalAlignment·

(prettier/prettier)


[error] 13-19: Replace ⏎··CanvasRenderMode,⏎··registerGUI,⏎··Text,⏎··UICanvas,⏎··UITransform,⏎ with ·CanvasRenderMode,·registerGUI,·Text,·UICanvas,·UITransform·

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/src/text-chunk-leak.ts` around lines 5 - 19, The import block
(Camera, Color, Entity, Script, WebGLEngine, TextHorizontalAlignment and
CanvasRenderMode, registerGUI, Text, UICanvas, UITransform) is misformatted and
there is a trailing comma in the WebGLEngine.create(...) call; run Prettier or
manually reformat the multi-line import statements to match project style (one
import spec list per module, consistent commas/spacing) and remove the
extraneous trailing comma from the WebGLEngine.create invocation so
lint/Prettier checks pass.


registerGUI();

/**
* Bug reproduction:
*
* Text components share a single PrimitiveChunk (vertex buffer).
* When entity.isActive = false, _onDisableInScene does NOT call
* _freeTextChunks(), so the inactive Text's vertices remain in the
* shared buffer and are still drawn.
*
* Steps:
* 1. Text A ("得分:48") renders at top (allocates SubChunk in shared buffer)
* 2. After 3s, Text A's parent is disabled (isActive=false)
* → SubChunk NOT freed, vertices remain in buffer
* 3. Text B ("历史最高:48") activates at center
* → allocates new SubChunk in SAME PrimitiveChunk
* 4. Draw call submits entire buffer → Text A's old vertices still drawn
*/
class Controller extends Script {
textA_parent: Entity;
textB_parent: Entity;
textAScore: Text;

private _elapsed = 0;
private _score = 0;
private _switched = false;

onUpdate(dt: number) {
this._elapsed += dt;

if (!this._switched) {
// Update score every 0.3s to trigger chunk reallocation
if (this._elapsed > 0.3) {
this._elapsed -= 0.3;
this._score += Math.floor(Math.random() * 10) + 1;
this.textAScore.text = "" + this._score;
}
// After score > 40, switch panels
if (this._score > 40) {
this._switched = true;
// Hide panel A → chunks NOT freed (the bug)
this.textA_parent.isActive = false;
// Show panel B
this.textB_parent.isActive = true;
}
}
}
}

async function main() {
const engine = await WebGLEngine.create({
canvas: document.getElementById("canvas") as HTMLCanvasElement,
});
engine.canvas.resizeByClientSize();

const scene = engine.sceneManager.activeScene;
scene.background.solidColor.set(0.35, 0.3, 0.25, 1);
const rootEntity = scene.createRootEntity();

const cameraEntity = rootEntity.createChild("camera");
cameraEntity.transform.setPosition(0, 0, 10);
const camera = cameraEntity.addComponent(Camera);
camera.isOrthographic = true;
camera.orthographicSize = 5;

// UICanvas
const canvasEntity = rootEntity.createChild("Canvas");
const uiCanvas = canvasEntity.addComponent(UICanvas);
uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay;
uiCanvas.referenceResolutionX = 750;
uiCanvas.referenceResolutionY = 1334;

// ========== Panel A: HUD (visible initially) ==========
const panelA = canvasEntity.createChild("panelA");

const labelA = panelA.createChild("labelA");
labelA.getComponent(UITransform).setPosition(-40, 300, 0);
const textA = labelA.addComponent(Text);
textA.text = "得分:";
textA.fontSize = 36;
textA.color = new Color(1, 1, 1, 1);
textA.enableWrapping = true;

const scoreA = panelA.createChild("scoreA");
scoreA.getComponent(UITransform).setPosition(60, 300, 0);
const textAScore = scoreA.addComponent(Text);
textAScore.text = "0";
textAScore.fontSize = 36;
textAScore.color = new Color(1, 1, 1, 1);
textAScore.enableWrapping = true;

// ========== Panel B: GameOver (hidden initially) ==========
const panelB = canvasEntity.createChild("panelB");
panelB.isActive = false;

const labelB1 = panelB.createChild("labelB1");
labelB1.getComponent(UITransform).setPosition(-60, 50, 0);
const textB1 = labelB1.addComponent(Text);
textB1.text = "历史最高:";
textB1.fontSize = 40;
textB1.color = new Color(1, 1, 1, 1);
textB1.enableWrapping = true;

const scoreB1 = panelB.createChild("scoreB1");
scoreB1.getComponent(UITransform).setPosition(120, 50, 0);
const textBScore1 = scoreB1.addComponent(Text);
textBScore1.text = "99";
textBScore1.fontSize = 40;
textBScore1.color = new Color(1, 1, 1, 1);
textBScore1.enableWrapping = true;

const labelB2 = panelB.createChild("labelB2");
labelB2.getComponent(UITransform).setPosition(-60, -50, 0);
const textB2 = labelB2.addComponent(Text);
textB2.text = "当前得分:";
textB2.fontSize = 40;
textB2.color = new Color(1, 1, 1, 1);
textB2.enableWrapping = true;

const scoreB2 = panelB.createChild("scoreB2");
scoreB2.getComponent(UITransform).setPosition(120, -50, 0);
const textBScore2 = scoreB2.addComponent(Text);
textBScore2.text = "0";
textBScore2.fontSize = 40;
textBScore2.color = new Color(1, 1, 1, 1);
textBScore2.enableWrapping = true;

// ========== Controller ==========
const ctrl = rootEntity.addComponent(Controller);
ctrl.textA_parent = panelA;
ctrl.textB_parent = panelB;
ctrl.textAScore = textAScore;

engine.run();
}

main();
20 changes: 13 additions & 7 deletions packages/core/src/2d/assembler/ISpriteAssembler.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { Matrix, Vector2 } from "@galacean/engine-math";
import { ISpriteRenderer } from "./ISpriteRenderer";
import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math";
import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager";
import { SpriteTileMode } from "../enums/SpriteTileMode";
import { SpritePrimitive } from "../sprite/SpritePrimitive";

/**
* Interface for sprite assembler.
*/
export interface ISpriteAssembler {
resetData(renderer: ISpriteRenderer, vertexCount?: number): void;
resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager, vertexCount?: number): void;
updatePositions(
renderer: ISpriteRenderer,
primitive: SpritePrimitive,
chunkManager: PrimitiveChunkManager,
worldMatrix: Matrix,
width: number,
height: number,
pivot: Vector2,
flipX: boolean,
flipY: boolean,
referenceResolutionPerUnit?: number
outBounds: BoundingBox,
referenceResolutionPerUnit?: number,
tileMode?: SpriteTileMode,
tiledAdaptiveThreshold?: number
): void;
updateUVs(renderer: ISpriteRenderer): void;
updateColor(renderer: ISpriteRenderer, alpha: number): void;
updateUVs(primitive: SpritePrimitive): void;
updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void;
}
17 changes: 0 additions & 17 deletions packages/core/src/2d/assembler/ISpriteRenderer.ts

This file was deleted.

40 changes: 21 additions & 19 deletions packages/core/src/2d/assembler/SimpleSpriteAssembler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BoundingBox, Matrix, Vector2 } from "@galacean/engine-math";
import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math";
import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement";
import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager";
import { SpritePrimitive } from "../sprite/SpritePrimitive";
import { ISpriteAssembler } from "./ISpriteAssembler";
import { ISpriteRenderer } from "./ISpriteRenderer";

/**
* Assemble vertex data for the sprite renderer in simple mode.
Expand All @@ -11,25 +12,26 @@ export class SimpleSpriteAssembler {
private static _rectangleTriangles = [0, 1, 2, 2, 1, 3];
private static _matrix = new Matrix();

static resetData(renderer: ISpriteRenderer): void {
const manager = renderer._getChunkManager();
const lastSubChunk = renderer._subChunk;
lastSubChunk && manager.freeSubChunk(lastSubChunk);
const subChunk = manager.allocateSubChunk(4);
static resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager): void {
const lastSubChunk = primitive.subChunk;
lastSubChunk && chunkManager.freeSubChunk(lastSubChunk);
const subChunk = chunkManager.allocateSubChunk(4);
subChunk.indices = SimpleSpriteAssembler._rectangleTriangles;
renderer._subChunk = subChunk;
primitive.subChunk = subChunk;
}

static updatePositions(
renderer: ISpriteRenderer,
primitive: SpritePrimitive,
chunkManager: PrimitiveChunkManager,
worldMatrix: Matrix,
width: number,
height: number,
pivot: Vector2,
flipX: boolean,
flipY: boolean
flipY: boolean,
outBounds: BoundingBox
): void {
const { sprite } = renderer;
const { sprite } = primitive;
const { x: pivotX, y: pivotY } = pivot;
// Position to World
const modelMatrix = SimpleSpriteAssembler._matrix;
Expand All @@ -52,7 +54,7 @@ export class SimpleSpriteAssembler {
// ---------------
// Update positions
const spritePositions = sprite._getPositions();
const subChunk = renderer._subChunk;
const subChunk = primitive.subChunk;
const vertices = subChunk.chunk.vertices;
for (let i = 0, o = subChunk.vertexArea.start; i < 4; ++i, o += 9) {
const { x, y } = spritePositions[i];
Expand All @@ -62,14 +64,14 @@ export class SimpleSpriteAssembler {
}

// @ts-ignore
BoundingBox.transform(sprite._getBounds(), modelMatrix, renderer._bounds);
BoundingBox.transform(sprite._getBounds(), modelMatrix, outBounds);
}

static updateUVs(renderer: ISpriteRenderer): void {
const spriteUVs = renderer.sprite._getUVs();
static updateUVs(primitive: SpritePrimitive): void {
const spriteUVs = primitive.sprite._getUVs();
const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[3];
const subChunk = renderer._subChunk;
const subChunk = primitive.subChunk;
const vertices = subChunk.chunk.vertices;
const offset = subChunk.vertexArea.start + 3;
vertices[offset] = left;
Expand All @@ -82,9 +84,9 @@ export class SimpleSpriteAssembler {
vertices[offset + 28] = top;
}

static updateColor(renderer: ISpriteRenderer, alpha: number): void {
const subChunk = renderer._subChunk;
const { r, g, b, a } = renderer.color;
static updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void {
const subChunk = primitive.subChunk;
const { r, g, b, a } = color;
const finalAlpha = a * alpha;
const vertices = subChunk.chunk.vertices;
for (let i = 0, o = subChunk.vertexArea.start + 5; i < 4; ++i, o += 9) {
Expand Down
44 changes: 22 additions & 22 deletions packages/core/src/2d/assembler/SlicedSpriteAssembler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Matrix, Vector2 } from "@galacean/engine-math";
import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math";
import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement";
import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager";
import { SpritePrimitive } from "../sprite/SpritePrimitive";
import { ISpriteAssembler } from "./ISpriteAssembler";
import { ISpriteRenderer } from "./ISpriteRenderer";

/**
* Assemble vertex data for the sprite renderer in sliced mode.
Expand All @@ -16,26 +17,27 @@ export class SlicedSpriteAssembler {
private static _row = new Array<number>(4);
private static _column = new Array<number>(4);

static resetData(renderer: ISpriteRenderer): void {
const manager = renderer._getChunkManager();
const lastSubChunk = renderer._subChunk;
lastSubChunk && manager.freeSubChunk(lastSubChunk);
const subChunk = manager.allocateSubChunk(16);
static resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager): void {
const lastSubChunk = primitive.subChunk;
lastSubChunk && chunkManager.freeSubChunk(lastSubChunk);
const subChunk = chunkManager.allocateSubChunk(16);
subChunk.indices = SlicedSpriteAssembler._rectangleTriangles;
renderer._subChunk = subChunk;
primitive.subChunk = subChunk;
}

static updatePositions(
renderer: ISpriteRenderer,
primitive: SpritePrimitive,
chunkManager: PrimitiveChunkManager,
worldMatrix: Matrix,
width: number,
height: number,
pivot: Vector2,
flipX: boolean,
flipY: boolean,
outBounds: BoundingBox,
referenceResolutionPerUnit: number = 1
): void {
const { sprite } = renderer;
const { sprite } = primitive;
const { border } = sprite;
// Update local positions.
const spritePositions = sprite._getPositions();
Expand Down Expand Up @@ -106,7 +108,7 @@ export class SlicedSpriteAssembler {
// 0 - 4 - 8 - 12
// ------------------------
// Assemble position and uv.
const subChunk = renderer._subChunk;
const subChunk = primitive.subChunk;
const vertices = subChunk.chunk.vertices;
for (let i = 0, o = subChunk.vertexArea.start; i < 4; i++) {
const rowValue = row[i];
Expand All @@ -118,17 +120,15 @@ export class SlicedSpriteAssembler {
}
}

// @ts-ignore
const bounds = renderer._bounds;
bounds.min.set(row[0], column[0], 0);
bounds.max.set(row[3], column[3], 0);
bounds.transform(modelMatrix);
outBounds.min.set(row[0], column[0], 0);
outBounds.max.set(row[3], column[3], 0);
outBounds.transform(modelMatrix);
}

static updateUVs(renderer: ISpriteRenderer): void {
const subChunk = renderer._subChunk;
static updateUVs(primitive: SpritePrimitive): void {
const subChunk = primitive.subChunk;
const vertices = subChunk.chunk.vertices;
const spriteUVs = renderer.sprite._getUVs();
const spriteUVs = primitive.sprite._getUVs();
for (let i = 0, o = subChunk.vertexArea.start + 3; i < 4; i++) {
const rowU = spriteUVs[i].x;
for (let j = 0; j < 4; j++, o += 9) {
Expand All @@ -138,9 +138,9 @@ export class SlicedSpriteAssembler {
}
}

static updateColor(renderer: ISpriteRenderer, alpha: number): void {
const subChunk = renderer._subChunk;
const { r, g, b, a } = renderer.color;
static updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void {
const subChunk = primitive.subChunk;
const { r, g, b, a } = color;
const finalAlpha = a * alpha;
const vertices = subChunk.chunk.vertices;
for (let i = 0, o = subChunk.vertexArea.start + 5; i < 16; ++i, o += 9) {
Expand Down
Loading
Loading