@viewscript/renderer 0.1.0-202605140639

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. package/vitest.config.ts +8 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * WASM Resource Manager
3
+ *
4
+ * This module provides explicit lifecycle management for WASM GPU resources.
5
+ * JavaScript's garbage collector cannot free WASM heap memory - we must call
6
+ * delete() explicitly on GPU objects.
7
+ *
8
+ * ## Problem
9
+ *
10
+ * ```
11
+ * const paint = canvasKit.Paint(); // Allocates on WASM heap
12
+ * paint = null; // JS reference gone, but WASM memory leaked!
13
+ * ```
14
+ *
15
+ * ## Solution: FinalizationRegistry
16
+ *
17
+ * We track all WASM resources and ensure delete() is called when the JS wrapper
18
+ * is garbage collected, OR when explicitly released via the resource pool.
19
+ *
20
+ * ## Resource Categories
21
+ *
22
+ * | Resource | Lifetime | Release Strategy |
23
+ * |----------|----------|------------------|
24
+ * | PaintSpec | Entity | On entity remove / HMR update |
25
+ * | PathEntity | Entity | On path change / entity remove |
26
+ * | GpuImage | Async | On image unload / src change |
27
+ * | GpuFont | Global | On font unload (rare) |
28
+ */
29
+
30
+ // =============================================================================
31
+ // Types
32
+ // =============================================================================
33
+
34
+ /**
35
+ * Any GPU WASM object with a delete() method.
36
+ */
37
+ interface Deletable {
38
+ delete(): void;
39
+ isDeleted?(): boolean;
40
+ }
41
+
42
+ /**
43
+ * Resource metadata for tracking.
44
+ */
45
+ interface ResourceEntry {
46
+ type: ResourceType;
47
+ entityId: number | null;
48
+ createdAt: number;
49
+ accessed: number;
50
+ }
51
+
52
+ type ResourceType = 'paint' | 'path' | 'image' | 'font' | 'shader' | 'surface';
53
+
54
+ /**
55
+ * Resource pool statistics.
56
+ */
57
+ export interface ResourceStats {
58
+ totalAllocated: number;
59
+ totalReleased: number;
60
+ currentActive: number;
61
+ byType: Record<ResourceType, number>;
62
+ leakSuspects: number;
63
+ }
64
+
65
+ // =============================================================================
66
+ // Resource Manager
67
+ // =============================================================================
68
+
69
+ export class WASMResourceManager {
70
+ /** FinalizationRegistry for automatic cleanup on GC */
71
+ private registry: FinalizationRegistry<CleanupToken>;
72
+
73
+ /** Active resources (strong references for explicit management) */
74
+ private resources: Map<symbol, { ref: WeakRef<Deletable>; entry: ResourceEntry }>;
75
+
76
+ /** Statistics */
77
+ private stats: {
78
+ allocated: number;
79
+ released: number;
80
+ };
81
+
82
+ /** Cleanup queue (for batch processing) */
83
+ private cleanupQueue: CleanupToken[] = [];
84
+ private cleanupScheduled = false;
85
+
86
+ constructor() {
87
+ this.resources = new Map();
88
+ this.stats = { allocated: 0, released: 0 };
89
+
90
+ // Create registry with cleanup callback
91
+ this.registry = new FinalizationRegistry((token: CleanupToken) => {
92
+ this.queueCleanup(token);
93
+ });
94
+ }
95
+
96
+ // ===========================================================================
97
+ // Public API
98
+ // ===========================================================================
99
+
100
+ /**
101
+ * Register a WASM resource for tracking.
102
+ *
103
+ * @param resource - GPU WASM object with delete() method
104
+ * @param type - Resource category
105
+ * @param entityId - Associated entity (optional)
106
+ * @returns Symbol token for explicit release
107
+ */
108
+ register<T extends Deletable>(
109
+ resource: T,
110
+ type: ResourceType,
111
+ entityId: number | null = null,
112
+ ): symbol {
113
+ const token = Symbol(`wasm-${type}-${this.stats.allocated}`);
114
+
115
+ const entry: ResourceEntry = {
116
+ type,
117
+ entityId,
118
+ createdAt: performance.now(),
119
+ accessed: performance.now(),
120
+ };
121
+
122
+ // Store weak reference for tracking
123
+ this.resources.set(token, {
124
+ ref: new WeakRef(resource),
125
+ entry,
126
+ });
127
+
128
+ // Register for finalization (GC-triggered cleanup)
129
+ const cleanupToken: CleanupToken = { token, type };
130
+ this.registry.register(resource, cleanupToken, resource);
131
+
132
+ this.stats.allocated++;
133
+
134
+ return token;
135
+ }
136
+
137
+ /**
138
+ * Explicitly release a resource by token.
139
+ * Preferred over waiting for GC.
140
+ */
141
+ release(token: symbol): boolean {
142
+ const entry = this.resources.get(token);
143
+ if (!entry) return false;
144
+
145
+ const resource = entry.ref.deref();
146
+ if (resource && !resource.isDeleted?.()) {
147
+ try {
148
+ resource.delete();
149
+ this.registry.unregister(resource);
150
+ } catch (e) {
151
+ console.warn('[WASM] Failed to delete resource:', e);
152
+ }
153
+ }
154
+
155
+ this.resources.delete(token);
156
+ this.stats.released++;
157
+
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Release all resources associated with an entity.
163
+ * Called on entity removal or HMR update.
164
+ */
165
+ releaseByEntity(entityId: number): number {
166
+ let released = 0;
167
+
168
+ for (const [token, { entry }] of this.resources) {
169
+ if (entry.entityId === entityId) {
170
+ if (this.release(token)) {
171
+ released++;
172
+ }
173
+ }
174
+ }
175
+
176
+ return released;
177
+ }
178
+
179
+ /**
180
+ * Release all resources of a specific type.
181
+ */
182
+ releaseByType(type: ResourceType): number {
183
+ let released = 0;
184
+
185
+ for (const [token, { entry }] of this.resources) {
186
+ if (entry.type === type) {
187
+ if (this.release(token)) {
188
+ released++;
189
+ }
190
+ }
191
+ }
192
+
193
+ return released;
194
+ }
195
+
196
+ /**
197
+ * Force cleanup of all queued finalizations.
198
+ * Call after GC to ensure WASM memory is freed.
199
+ */
200
+ flushCleanupQueue(): number {
201
+ const count = this.cleanupQueue.length;
202
+
203
+ for (const token of this.cleanupQueue) {
204
+ this.release(token.token);
205
+ }
206
+
207
+ this.cleanupQueue = [];
208
+ return count;
209
+ }
210
+
211
+ /**
212
+ * Release ALL resources. Use with caution.
213
+ */
214
+ releaseAll(): number {
215
+ let released = 0;
216
+
217
+ for (const token of this.resources.keys()) {
218
+ if (this.release(token)) {
219
+ released++;
220
+ }
221
+ }
222
+
223
+ return released;
224
+ }
225
+
226
+ /**
227
+ * Get current resource statistics.
228
+ */
229
+ getStats(): ResourceStats {
230
+ const byType: Record<ResourceType, number> = {
231
+ paint: 0,
232
+ path: 0,
233
+ image: 0,
234
+ font: 0,
235
+ shader: 0,
236
+ surface: 0,
237
+ };
238
+
239
+ let leakSuspects = 0;
240
+ const now = performance.now();
241
+ const LEAK_THRESHOLD_MS = 60000; // 1 minute without access
242
+
243
+ for (const { ref, entry } of this.resources.values()) {
244
+ const resource = ref.deref();
245
+ if (resource) {
246
+ byType[entry.type]++;
247
+
248
+ // Check for potential leaks (old, unaccessed resources)
249
+ if (now - entry.accessed > LEAK_THRESHOLD_MS) {
250
+ leakSuspects++;
251
+ }
252
+ }
253
+ }
254
+
255
+ return {
256
+ totalAllocated: this.stats.allocated,
257
+ totalReleased: this.stats.released,
258
+ currentActive: this.resources.size,
259
+ byType,
260
+ leakSuspects,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Mark a resource as recently accessed (resets leak timer).
266
+ */
267
+ touch(token: symbol): void {
268
+ const entry = this.resources.get(token);
269
+ if (entry) {
270
+ entry.entry.accessed = performance.now();
271
+ }
272
+ }
273
+
274
+ // ===========================================================================
275
+ // Private Methods
276
+ // ===========================================================================
277
+
278
+ /**
279
+ * Queue a cleanup token for batch processing.
280
+ */
281
+ private queueCleanup(token: CleanupToken): void {
282
+ this.cleanupQueue.push(token);
283
+
284
+ // Schedule batch cleanup on next microtask
285
+ if (!this.cleanupScheduled) {
286
+ this.cleanupScheduled = true;
287
+ queueMicrotask(() => {
288
+ this.flushCleanupQueue();
289
+ this.cleanupScheduled = false;
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Token passed to FinalizationRegistry callback.
297
+ */
298
+ interface CleanupToken {
299
+ token: symbol;
300
+ type: ResourceType;
301
+ }
302
+
303
+ // =============================================================================
304
+ // Global Instance
305
+ // =============================================================================
306
+
307
+ /**
308
+ * Singleton resource manager for the renderer.
309
+ */
310
+ export const wasmResources = new WASMResourceManager();
311
+
312
+ // =============================================================================
313
+ // Helper: Scoped Resource Guard
314
+ // =============================================================================
315
+
316
+ /**
317
+ * RAII-style guard for temporary WASM resources.
318
+ *
319
+ * Usage:
320
+ * ```
321
+ * using(canvasKit.Paint(), paint => {
322
+ * canvas.drawRect(rect, paint);
323
+ * }); // paint.delete() called automatically
324
+ * ```
325
+ */
326
+ export function using<T extends Deletable, R>(
327
+ resource: T,
328
+ fn: (resource: T) => R,
329
+ ): R {
330
+ try {
331
+ return fn(resource);
332
+ } finally {
333
+ resource.delete();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Async version of using().
339
+ */
340
+ export async function usingAsync<T extends Deletable, R>(
341
+ resource: T,
342
+ fn: (resource: T) => Promise<R>,
343
+ ): Promise<R> {
344
+ try {
345
+ return await fn(resource);
346
+ } finally {
347
+ resource.delete();
348
+ }
349
+ }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * wgpu Renderer Adapter
3
+ *
4
+ * This module provides a `CanvasRenderer` implementation that delegates to
5
+ * `WasmGpuRenderer` from the vsc-wasm crate.
6
+ *
7
+ * ## Phase F: wgpu Migration
8
+ *
9
+ * This adapter implements the wgpu-based renderer:
10
+ *
11
+ * ```text
12
+ * Before (legacy):
13
+ * RenderLoop → GpuRenderer → PathEntity → WebGL
14
+ *
15
+ * After (wgpu):
16
+ * RenderLoop → WgpuRendererAdapter → WasmGpuRenderer → wgpu → WebGPU/WebGL
17
+ * ```
18
+ *
19
+ * ## Key Differences from legacy renderer
20
+ *
21
+ * 1. **No Resource Management**: wgpu resources are managed on the Rust side.
22
+ * TypeScript does NOT need to call delete() on GPU objects.
23
+ *
24
+ * 2. **JSON Serialization**: DrawCommands are converted to CanvasNode JSON
25
+ * and passed to WASM. This adds serialization overhead but simplifies
26
+ * the boundary significantly.
27
+ *
28
+ * 3. **Batched Rendering**: draw() accumulates commands; flush() sends them
29
+ * all to the GPU in a single render pass.
30
+ */
31
+
32
+ import type { EntityId, RasterBounds, CanvasNode, RenderableEntity, Rational } from '../ast/types.js';
33
+
34
+ // =============================================================================
35
+ // JSON Serialization Helpers
36
+ // =============================================================================
37
+
38
+ /**
39
+ * Custom JSON replacer for WASM boundary serialization.
40
+ *
41
+ * ## Problem
42
+ *
43
+ * JavaScript's `JSON.stringify()` does not support `bigint`:
44
+ * ```
45
+ * JSON.stringify({ n: 100n }) // TypeError: BigInt value can't be serialized
46
+ * ```
47
+ *
48
+ * ## Solution
49
+ *
50
+ * This replacer converts:
51
+ * - `bigint` → string (e.g., `100n` → `"100"`)
52
+ * - `Rational` object → `"numerator/denominator"` string
53
+ *
54
+ * The Rust side expects Rational as `"num/den"` string format
55
+ * (see `vsc-core/src/types.rs` Deserialize impl).
56
+ */
57
+ function wasmBoundaryReplacer(_key: string, value: unknown): unknown {
58
+ // Handle raw bigint (shouldn't occur in well-typed code, but safety first)
59
+ if (typeof value === 'bigint') {
60
+ return value.toString();
61
+ }
62
+
63
+ // Handle Rational objects: { numerator: bigint, denominator: bigint }
64
+ if (isRational(value)) {
65
+ return `${value.numerator}/${value.denominator}`;
66
+ }
67
+
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * Type guard for Rational objects.
73
+ */
74
+ function isRational(value: unknown): value is Rational {
75
+ return (
76
+ value !== null &&
77
+ typeof value === 'object' &&
78
+ 'numerator' in value &&
79
+ 'denominator' in value &&
80
+ typeof (value as Rational).numerator === 'bigint' &&
81
+ typeof (value as Rational).denominator === 'bigint'
82
+ );
83
+ }
84
+
85
+ // =============================================================================
86
+ // Types
87
+ // =============================================================================
88
+
89
+ /**
90
+ * Canvas draw command (batched for wgpu).
91
+ */
92
+ interface DrawCommand {
93
+ entityId: EntityId;
94
+ type: 'path' | 'text' | 'image' | 'group';
95
+ bounds: RasterBounds;
96
+ payload: unknown;
97
+ }
98
+
99
+ /**
100
+ * CanvasRenderer interface from render-loop.ts
101
+ */
102
+ interface CanvasRenderer {
103
+ getEntity(id: EntityId): RenderableEntity | undefined;
104
+ draw(command: DrawCommand): void;
105
+ flush(): void;
106
+ }
107
+
108
+ /**
109
+ * WasmGpuRenderer interface (from vsc-wasm with "gpu" feature).
110
+ *
111
+ * This is the TypeScript-side view of the Rust struct.
112
+ */
113
+ interface WasmGpuRenderer {
114
+ render(nodes_json: string): void;
115
+ resize(width: number, height: number): void;
116
+ readonly width: number;
117
+ readonly height: number;
118
+ }
119
+
120
+ /**
121
+ * Factory function for WasmGpuRenderer.
122
+ */
123
+ interface WasmGpuRendererStatic {
124
+ create(canvas: HTMLCanvasElement): Promise<WasmGpuRenderer>;
125
+ }
126
+
127
+ // =============================================================================
128
+ // Adapter Implementation
129
+ // =============================================================================
130
+
131
+ /**
132
+ * wgpu-based CanvasRenderer implementation.
133
+ *
134
+ * This adapter accumulates DrawCommands during a frame, converts them to
135
+ * CanvasNode JSON, and sends them to WasmGpuRenderer on flush().
136
+ *
137
+ * ## Usage
138
+ *
139
+ * ```typescript
140
+ * import { createWgpuRendererAdapter } from './wgpu-renderer-adapter';
141
+ *
142
+ * const canvas = document.getElementById('viewport') as HTMLCanvasElement;
143
+ * const adapter = await createWgpuRendererAdapter(canvas, entityStore);
144
+ *
145
+ * // In render loop:
146
+ * adapter.draw(command1);
147
+ * adapter.draw(command2);
148
+ * adapter.flush(); // Sends all commands to GPU
149
+ * ```
150
+ */
151
+ export class WgpuRendererAdapter implements CanvasRenderer {
152
+ /** WASM GPU renderer instance */
153
+ private renderer: WasmGpuRenderer;
154
+
155
+ /** Entity store for getEntity() lookups */
156
+ private entityStore: Map<EntityId, RenderableEntity>;
157
+
158
+ /** Accumulated draw commands for current frame */
159
+ private pendingCommands: DrawCommand[] = [];
160
+
161
+ /** Canvas element for resize handling */
162
+ private canvas: HTMLCanvasElement;
163
+
164
+ private constructor(
165
+ renderer: WasmGpuRenderer,
166
+ canvas: HTMLCanvasElement,
167
+ entityStore: Map<EntityId, RenderableEntity>,
168
+ ) {
169
+ this.renderer = renderer;
170
+ this.canvas = canvas;
171
+ this.entityStore = entityStore;
172
+ }
173
+
174
+ /**
175
+ * Create a new wgpu renderer adapter.
176
+ *
177
+ * @param canvas - HTML canvas element to render to
178
+ * @param entityStore - Map of entities for getEntity() lookups
179
+ * @param wasmModule - WASM module containing WasmGpuRenderer
180
+ */
181
+ static async create(
182
+ canvas: HTMLCanvasElement,
183
+ entityStore: Map<EntityId, RenderableEntity>,
184
+ wasmModule: { WasmGpuRenderer: WasmGpuRendererStatic },
185
+ ): Promise<WgpuRendererAdapter> {
186
+ const renderer = await wasmModule.WasmGpuRenderer.create(canvas);
187
+ return new WgpuRendererAdapter(renderer, canvas, entityStore);
188
+ }
189
+
190
+ // ===========================================================================
191
+ // CanvasRenderer Interface
192
+ // ===========================================================================
193
+
194
+ /**
195
+ * Get a renderable entity by ID.
196
+ */
197
+ getEntity(id: EntityId): RenderableEntity | undefined {
198
+ return this.entityStore.get(id);
199
+ }
200
+
201
+ /**
202
+ * Queue a draw command for the current frame.
203
+ *
204
+ * Commands are accumulated until flush() is called.
205
+ */
206
+ draw(command: DrawCommand): void {
207
+ this.pendingCommands.push(command);
208
+ }
209
+
210
+ /**
211
+ * Flush all pending commands to the GPU.
212
+ *
213
+ * This converts accumulated DrawCommands to CanvasNode JSON and
214
+ * sends them to the WASM renderer in a single call.
215
+ *
216
+ * ## Serialization
217
+ *
218
+ * Uses `wasmBoundaryReplacer` to handle:
219
+ * - `bigint` values (not supported by standard JSON.stringify)
220
+ * - `Rational` objects → `"num/den"` string format (Rust expectation)
221
+ */
222
+ flush(): void {
223
+ if (this.pendingCommands.length === 0) {
224
+ return;
225
+ }
226
+
227
+ // Convert DrawCommands to CanvasNodes
228
+ const nodes = this.commandsToCanvasNodes(this.pendingCommands);
229
+
230
+ // Serialize with custom replacer for Rational/bigint handling
231
+ const json = JSON.stringify(nodes, wasmBoundaryReplacer);
232
+ this.renderer.render(json);
233
+
234
+ // Clear pending commands
235
+ this.pendingCommands = [];
236
+ }
237
+
238
+ // ===========================================================================
239
+ // Public Utilities
240
+ // ===========================================================================
241
+
242
+ /**
243
+ * Handle canvas resize.
244
+ *
245
+ * Call this when the canvas element is resized.
246
+ */
247
+ resize(width: number, height: number): void {
248
+ this.renderer.resize(width, height);
249
+ }
250
+
251
+ /**
252
+ * Get the current render surface dimensions.
253
+ */
254
+ getDimensions(): { width: number; height: number } {
255
+ return {
256
+ width: this.renderer.width,
257
+ height: this.renderer.height,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Update the entity store reference.
263
+ *
264
+ * Call this when the entity store is replaced (e.g., after HMR).
265
+ */
266
+ setEntityStore(store: Map<EntityId, RenderableEntity>): void {
267
+ this.entityStore = store;
268
+ }
269
+
270
+ // ===========================================================================
271
+ // Private Methods
272
+ // ===========================================================================
273
+
274
+ /**
275
+ * Convert DrawCommands to CanvasNodes.
276
+ *
277
+ * This is the key translation layer between the render loop's command
278
+ * abstraction and the GPU renderer's scene graph.
279
+ */
280
+ private commandsToCanvasNodes(commands: DrawCommand[]): CanvasNode[] {
281
+ const nodes: CanvasNode[] = [];
282
+
283
+ for (const cmd of commands) {
284
+ const entity = this.entityStore.get(cmd.entityId);
285
+ if (!entity?.canvas) {
286
+ continue;
287
+ }
288
+
289
+ // Use the entity's CanvasNode directly
290
+ // The payload in DrawCommand may contain overrides, but for now
291
+ // we use the entity's stored canvas representation
292
+ nodes.push(entity.canvas);
293
+ }
294
+
295
+ return nodes;
296
+ }
297
+ }
298
+
299
+ // =============================================================================
300
+ // Factory Function
301
+ // =============================================================================
302
+
303
+ /**
304
+ * Create a wgpu renderer adapter.
305
+ *
306
+ * This is the main entry point for creating a wgpu-based renderer.
307
+ *
308
+ * @param canvas - HTML canvas element to render to
309
+ * @param entityStore - Map of entities for getEntity() lookups
310
+ * @param wasmModule - WASM module containing WasmGpuRenderer
311
+ */
312
+ export async function createWgpuRendererAdapter(
313
+ canvas: HTMLCanvasElement,
314
+ entityStore: Map<EntityId, RenderableEntity>,
315
+ wasmModule: { WasmGpuRenderer: WasmGpuRendererStatic },
316
+ ): Promise<WgpuRendererAdapter> {
317
+ return WgpuRendererAdapter.create(canvas, entityStore, wasmModule);
318
+ }