Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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