Skip to content

Commit 2024ae1

Browse files
GuoLei1990claude
andcommitted
refactor: clean architecture for TransformFeedbackPrimitive
- Move VAO management to GLTransformFeedbackPrimitive (RHI layer) - Add IPlatformTransformFeedbackPrimitive interface - Remove GL calls from core layer TransformFeedbackPrimitive - Split draw into beginDraw/draw/endDraw (no array allocation) - Use VertexElement/VertexBufferBinding directly (no intermediate types) - Remove unused drawArrays/VAO methods from WebGLGraphicDevice - Fix TF buffer residual binding causing GL conflict on VAO rebuild Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fcc73e5 commit 2024ae1

File tree

6 files changed

+239
-150
lines changed

6 files changed

+239
-150
lines changed
Lines changed: 65 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,129 @@
11
import { Engine } from "../Engine";
2+
import { IPlatformTransformFeedbackPrimitive } from "../renderingHardwareInterface";
23
import { ShaderProgram } from "../shader/ShaderProgram";
34
import { Buffer } from "./Buffer";
45
import { BufferBindFlag } from "./enums/BufferBindFlag";
56
import { BufferUsage } from "./enums/BufferUsage";
7+
import { MeshTopology } from "./enums/MeshTopology";
68
import { TransformFeedback } from "./TransformFeedback";
79
import { VertexBufferBinding } from "./VertexBufferBinding";
810
import { VertexElement } from "./VertexElement";
9-
import { MeshTopology } from "./enums/MeshTopology";
1011

1112
/**
1213
* @internal
13-
* A primitive that manages ping-pong buffers and VAOs for Transform Feedback rendering.
14-
* Handles buffer creation, VAO setup, TF draw with bindBufferRange, and ping-pong swap.
14+
* Transform Feedback primitive that manages buffers, draw calls, and state for GPU-side data capture.
1515
*/
1616
export class TransformFeedbackPrimitive {
17+
/** @internal */
18+
_platformPrimitive: IPlatformTransformFeedbackPrimitive;
19+
1720
private _engine: Engine;
21+
private _transformFeedback: TransformFeedback;
1822
private _readBuffer: Buffer;
1923
private _writeBuffer: Buffer;
20-
private _transformFeedback: TransformFeedback;
21-
22-
// VAO pair for current program (one per ping-pong direction)
23-
private _vaoA: WebGLVertexArrayObject;
24-
private _vaoB: WebGLVertexArrayObject;
25-
private _useA = true;
26-
private _lastProgramId = -1;
27-
2824
private _renderBufferBinding: VertexBufferBinding;
2925
private _byteStride: number;
30-
private _vertexCount = 0;
31-
private _initialized = false;
26+
private _readIsFirst = true;
3227

28+
/**
29+
* Buffer binding for the render pass (points to the latest TF output).
30+
*/
3331
get currentRenderBufferBinding(): VertexBufferBinding {
3432
return this._renderBufferBinding;
3533
}
3634

35+
/**
36+
* The current read buffer (TF input / render source).
37+
*/
3738
get readBuffer(): Buffer {
3839
return this._readBuffer;
3940
}
4041

42+
/**
43+
* The current write buffer (TF output target).
44+
*/
4145
get writeBuffer(): Buffer {
4246
return this._writeBuffer;
4347
}
4448

45-
get initialized(): boolean {
46-
return this._initialized;
47-
}
48-
49+
/**
50+
* @param engine - Engine instance
51+
* @param byteStride - Bytes per vertex in the TF buffer
52+
*/
4953
constructor(engine: Engine, byteStride: number) {
5054
this._engine = engine;
5155
this._byteStride = byteStride;
5256
this._transformFeedback = new TransformFeedback(engine);
5357
this._transformFeedback.isGCIgnored = true;
58+
this._platformPrimitive = engine._hardwareRenderer.createPlatformTransformFeedbackPrimitive();
5459
}
5560

5661
/**
57-
* Resize ping-pong buffers for the given vertex count.
62+
* Resize buffers.
63+
* @param vertexCount - Number of vertices to allocate
5864
*/
5965
resize(vertexCount: number): void {
60-
if (vertexCount === this._vertexCount && this._initialized) return;
61-
62-
const engine = this._engine;
63-
const byteLength = this._byteStride * vertexCount;
64-
6566
this._readBuffer?.destroy();
6667
this._writeBuffer?.destroy();
6768

68-
const readBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false);
69+
const byteLength = this._byteStride * vertexCount;
70+
const readBuffer = new Buffer(this._engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false);
6971
readBuffer.isGCIgnored = true;
70-
const writeBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false);
72+
const writeBuffer = new Buffer(this._engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false);
7173
writeBuffer.isGCIgnored = true;
7274

73-
const floatStride = this._byteStride / 4;
74-
const zeroData = new Float32Array(vertexCount * floatStride);
75-
readBuffer.setData(zeroData);
76-
writeBuffer.setData(zeroData);
77-
7875
this._readBuffer = readBuffer;
7976
this._writeBuffer = writeBuffer;
80-
this._renderBufferBinding = new VertexBufferBinding(this._readBuffer, this._byteStride);
81-
this._vertexCount = vertexCount;
82-
this._initialized = true;
83-
// Force VAO rebuild on next updateVAOs
84-
this._lastProgramId = -1;
77+
this._renderBufferBinding = new VertexBufferBinding(readBuffer, this._byteStride);
8578
}
8679

8780
/**
88-
* Ensure VAOs are up-to-date. Automatically rebuilds when program or buffers change.
81+
* Prepare for drawing. Updates attribute bindings if needed and binds state.
82+
* @param program - The shader program
83+
* @param feedbackElements - Vertex elements for the feedback buffer
84+
* @param inputBinding - Input buffer binding (e.g., instance data)
85+
* @param inputElements - Vertex elements for the input buffer
8986
*/
90-
updateVAOs(
87+
beginDraw(
9188
program: ShaderProgram,
92-
tfElements: VertexElement[],
93-
extraBindings: { binding: VertexBufferBinding; elements: VertexElement[] }[]
89+
feedbackElements: VertexElement[],
90+
inputBinding: VertexBufferBinding,
91+
inputElements: VertexElement[]
9492
): void {
95-
if (program.id === this._lastProgramId) return;
96-
97-
const gl = this._engine._hardwareRenderer.gl as WebGL2RenderingContext;
98-
99-
this._deleteVAOs(gl);
100-
101-
this._vaoA = this._createVAO(gl, program, this._readBuffer, tfElements, extraBindings);
102-
this._vaoB = this._createVAO(gl, program, this._writeBuffer, tfElements, extraBindings);
103-
this._lastProgramId = program.id;
104-
105-
gl.bindVertexArray(null);
93+
this._platformPrimitive.update(
94+
program,
95+
this._readBuffer,
96+
this._writeBuffer,
97+
this._byteStride,
98+
feedbackElements,
99+
inputBinding,
100+
inputElements
101+
);
102+
this._platformPrimitive.bind(this._readIsFirst);
106103
}
107104

108105
/**
109-
* Bind the current VAO for TF drawing.
106+
* Execute a single TF draw call for a vertex range.
107+
* @param mode - Primitive topology
108+
* @param first - First vertex index
109+
* @param count - Number of vertices to process
110110
*/
111-
bindVAO(): void {
112-
const gl = this._engine._hardwareRenderer.gl as WebGL2RenderingContext;
113-
gl.bindVertexArray(this._useA ? this._vaoA : this._vaoB);
111+
draw(mode: MeshTopology, first: number, count: number): void {
112+
const transformFeedback = this._transformFeedback;
113+
transformFeedback.bind();
114+
transformFeedback.bindBufferRange(0, this._writeBuffer, first * this._byteStride, count * this._byteStride);
115+
transformFeedback.begin(mode);
116+
this._platformPrimitive.draw(mode, first, count);
117+
transformFeedback.end();
118+
transformFeedback.unbindBuffer(0);
119+
transformFeedback.unbind();
114120
}
115121

116122
/**
117-
* Unbind VAO after TF drawing.
123+
* Finish drawing and unbind state.
118124
*/
119-
unbindVAO(): void {
120-
const gl = this._engine._hardwareRenderer.gl as WebGL2RenderingContext;
121-
gl.bindVertexArray(null);
122-
}
123-
124-
/**
125-
* Execute a TF draw call for a range of vertices.
126-
*/
127-
draw(rhi: any, mode: MeshTopology, first: number, count: number): void {
128-
const byteOffset = first * this._byteStride;
129-
const byteSize = count * this._byteStride;
130-
this._transformFeedback.bind();
131-
this._transformFeedback.bindBufferRange(0, this._writeBuffer, byteOffset, byteSize);
132-
this._transformFeedback.begin(mode);
133-
rhi.drawArrays(mode, first, count);
134-
this._transformFeedback.end();
135-
// Unbind TF buffer from TRANSFORM_FEEDBACK_BUFFER target to avoid
136-
// conflicts when the same buffer is bound as ARRAY_BUFFER in render pass.
137-
this._transformFeedback.unbindBuffer(0);
138-
this._transformFeedback.unbind();
125+
endDraw(): void {
126+
this._platformPrimitive.unbind();
139127
}
140128

141129
/**
@@ -145,66 +133,14 @@ export class TransformFeedbackPrimitive {
145133
const temp = this._readBuffer;
146134
this._readBuffer = this._writeBuffer;
147135
this._writeBuffer = temp;
148-
this._useA = !this._useA;
136+
this._readIsFirst = !this._readIsFirst;
149137
this._renderBufferBinding = new VertexBufferBinding(this._readBuffer, this._byteStride);
150138
}
151139

152140
destroy(): void {
153-
const gl = this._engine._hardwareRenderer.gl as WebGL2RenderingContext;
154-
this._deleteVAOs(gl);
141+
this._platformPrimitive?.destroy();
155142
this._readBuffer?.destroy();
156143
this._writeBuffer?.destroy();
157144
this._transformFeedback?.destroy();
158145
}
159-
160-
private _deleteVAOs(gl: WebGL2RenderingContext): void {
161-
if (this._vaoA) {
162-
gl.deleteVertexArray(this._vaoA);
163-
this._vaoA = null;
164-
}
165-
if (this._vaoB) {
166-
gl.deleteVertexArray(this._vaoB);
167-
this._vaoB = null;
168-
}
169-
}
170-
171-
private _createVAO(
172-
gl: WebGL2RenderingContext,
173-
program: ShaderProgram,
174-
tfReadBuffer: Buffer,
175-
tfElements: VertexElement[],
176-
extraBindings: { binding: VertexBufferBinding; elements: VertexElement[] }[]
177-
): WebGLVertexArrayObject {
178-
const vao = gl.createVertexArray();
179-
gl.bindVertexArray(vao);
180-
181-
const attribs = program.attributeLocation;
182-
183-
tfReadBuffer.bind();
184-
this._bindElements(gl, attribs, tfElements, this._byteStride);
185-
186-
for (const { binding, elements } of extraBindings) {
187-
binding.buffer.bind();
188-
this._bindElements(gl, attribs, elements, binding.stride);
189-
}
190-
191-
gl.bindBuffer(gl.ARRAY_BUFFER, null);
192-
return vao;
193-
}
194-
195-
private _bindElements(
196-
gl: WebGL2RenderingContext,
197-
attribs: Record<string, number>,
198-
elements: VertexElement[],
199-
stride: number
200-
): void {
201-
for (const element of elements) {
202-
const loc = attribs[element.attribute];
203-
if (loc !== undefined && loc !== -1) {
204-
const info = element._formatMetaInfo;
205-
gl.enableVertexAttribArray(loc);
206-
gl.vertexAttribPointer(loc, info.size, info.type, info.normalized, stride, element.offset);
207-
}
208-
}
209-
}
210146
}

packages/core/src/particle/ParticleTransformFeedbackSimulator.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class ParticleTransformFeedbackSimulator {
2626
private static readonly _deltaTimeProperty = ShaderProperty.getByName("renderer_DeltaTime");
2727

2828
// TF read buffer vertex elements
29-
private static readonly _tfElements: VertexElement[] = [
29+
private static readonly _feedbackElements: VertexElement[] = [
3030
new VertexElement("a_TFPosition", 0, VertexElementFormat.Vector3, 0),
3131
new VertexElement("a_TFVelocity", 12, VertexElementFormat.Vector3, 0)
3232
];
@@ -106,12 +106,6 @@ export class ParticleTransformFeedbackSimulator {
106106

107107
if (!this._tfProgram || !this._tfProgram.isValid) return;
108108

109-
// Update VAOs (auto-rebuilds if program or buffers changed)
110-
const instanceBinding = new VertexBufferBinding(instanceBuffer, ParticleBufferUtils.instanceVertexStride);
111-
this._primitive.updateVAOs(this._tfProgram, ParticleTransformFeedbackSimulator._tfElements, [
112-
{ binding: instanceBinding, elements: ParticleTransformFeedbackSimulator._instanceElements }
113-
]);
114-
115109
// --- TF pass ---
116110
this._tfProgram.bind();
117111
rhi.enableRasterizerDiscard();
@@ -121,20 +115,25 @@ export class ParticleTransformFeedbackSimulator {
121115
this._tfProgram.uploadUniforms(this._tfProgram.rendererUniformBlock, shaderData);
122116
this._tfProgram.uploadUniforms(this._tfProgram.otherUniformBlock, shaderData);
123117

124-
// Bind VAO and execute TF for alive particles
125-
this._primitive.bindVAO();
118+
// Draw alive particles (ring buffer may wrap into two segments)
119+
const instanceBinding = new VertexBufferBinding(instanceBuffer, ParticleBufferUtils.instanceVertexStride);
120+
this._primitive.beginDraw(
121+
this._tfProgram,
122+
ParticleTransformFeedbackSimulator._feedbackElements,
123+
instanceBinding,
124+
ParticleTransformFeedbackSimulator._instanceElements
125+
);
126126

127-
const POINTS = MeshTopology.Points;
128127
if (firstActive < firstFree) {
129-
this._primitive.draw(rhi, POINTS, firstActive, firstFree - firstActive);
128+
this._primitive.draw(MeshTopology.Points, firstActive, firstFree - firstActive);
130129
} else {
131-
this._primitive.draw(rhi, POINTS, firstActive, particleCount - firstActive);
130+
this._primitive.draw(MeshTopology.Points, firstActive, particleCount - firstActive);
132131
if (firstFree > 0) {
133-
this._primitive.draw(rhi, POINTS, 0, firstFree);
132+
this._primitive.draw(MeshTopology.Points, 0, firstFree);
134133
}
135134
}
136135

137-
this._primitive.unbindVAO();
136+
this._primitive.endDraw();
138137
rhi.disableRasterizerDiscard();
139138

140139
// Swap ping-pong
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Platform interface for Transform Feedback primitive operations.
3+
* @internal
4+
*/
5+
export interface IPlatformTransformFeedbackPrimitive {
6+
/**
7+
* Update attribute bindings for the given program. Auto-rebuilds when program changes.
8+
* @param program - Shader program (for attribute locations)
9+
* @param readBuffer - Current read buffer (ping-pong A)
10+
* @param writeBuffer - Current write buffer (ping-pong B)
11+
* @param tfStride - TF buffer byte stride
12+
* @param feedbackElements - Vertex elements for TF buffer
13+
* @param inputBindings - Additional buffers with their vertex elements
14+
*/
15+
update(
16+
program: any,
17+
readBuffer: any,
18+
writeBuffer: any,
19+
feedbackStride: number,
20+
feedbackElements: any[],
21+
inputBinding: any,
22+
inputElements: any[]
23+
): void;
24+
25+
/**
26+
* Bind attribute state for the given ping-pong direction.
27+
* @param useA - Whether to bind direction A or B
28+
*/
29+
bind(useA: boolean): void;
30+
31+
/**
32+
* Unbind attribute state.
33+
*/
34+
unbind(): void;
35+
36+
/**
37+
* Issue a draw call.
38+
* @param mode - Primitive topology
39+
* @param first - First vertex index
40+
* @param count - Number of vertices
41+
*/
42+
draw(mode: number, first: number, count: number): void;
43+
44+
/**
45+
* Destroy native resources.
46+
*/
47+
destroy(): void;
48+
}

packages/core/src/renderingHardwareInterface/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export type { IPlatformTexture2D } from "./IPlatformTexture2D";
55
export type { IPlatformTexture2DArray } from "./IPlatformTexture2DArray";
66
export type { IPlatformTextureCube } from "./IPlatformTextureCube";
77
export type { IPlatformTransformFeedback } from "./IPlatformTransformFeedback";
8+
export type { IPlatformTransformFeedbackPrimitive } from "./IPlatformTransformFeedbackPrimitive";

0 commit comments

Comments
 (0)