@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.
- package/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
// Resource Manager
|
|
31
|
+
// =============================================================================
|
|
32
|
+
export class WASMResourceManager {
|
|
33
|
+
/** FinalizationRegistry for automatic cleanup on GC */
|
|
34
|
+
registry;
|
|
35
|
+
/** Active resources (strong references for explicit management) */
|
|
36
|
+
resources;
|
|
37
|
+
/** Statistics */
|
|
38
|
+
stats;
|
|
39
|
+
/** Cleanup queue (for batch processing) */
|
|
40
|
+
cleanupQueue = [];
|
|
41
|
+
cleanupScheduled = false;
|
|
42
|
+
constructor() {
|
|
43
|
+
this.resources = new Map();
|
|
44
|
+
this.stats = { allocated: 0, released: 0 };
|
|
45
|
+
// Create registry with cleanup callback
|
|
46
|
+
this.registry = new FinalizationRegistry((token) => {
|
|
47
|
+
this.queueCleanup(token);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
// Public API
|
|
52
|
+
// ===========================================================================
|
|
53
|
+
/**
|
|
54
|
+
* Register a WASM resource for tracking.
|
|
55
|
+
*
|
|
56
|
+
* @param resource - GPU WASM object with delete() method
|
|
57
|
+
* @param type - Resource category
|
|
58
|
+
* @param entityId - Associated entity (optional)
|
|
59
|
+
* @returns Symbol token for explicit release
|
|
60
|
+
*/
|
|
61
|
+
register(resource, type, entityId = null) {
|
|
62
|
+
const token = Symbol(`wasm-${type}-${this.stats.allocated}`);
|
|
63
|
+
const entry = {
|
|
64
|
+
type,
|
|
65
|
+
entityId,
|
|
66
|
+
createdAt: performance.now(),
|
|
67
|
+
accessed: performance.now(),
|
|
68
|
+
};
|
|
69
|
+
// Store weak reference for tracking
|
|
70
|
+
this.resources.set(token, {
|
|
71
|
+
ref: new WeakRef(resource),
|
|
72
|
+
entry,
|
|
73
|
+
});
|
|
74
|
+
// Register for finalization (GC-triggered cleanup)
|
|
75
|
+
const cleanupToken = { token, type };
|
|
76
|
+
this.registry.register(resource, cleanupToken, resource);
|
|
77
|
+
this.stats.allocated++;
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Explicitly release a resource by token.
|
|
82
|
+
* Preferred over waiting for GC.
|
|
83
|
+
*/
|
|
84
|
+
release(token) {
|
|
85
|
+
const entry = this.resources.get(token);
|
|
86
|
+
if (!entry)
|
|
87
|
+
return false;
|
|
88
|
+
const resource = entry.ref.deref();
|
|
89
|
+
if (resource && !resource.isDeleted?.()) {
|
|
90
|
+
try {
|
|
91
|
+
resource.delete();
|
|
92
|
+
this.registry.unregister(resource);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.warn('[WASM] Failed to delete resource:', e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.resources.delete(token);
|
|
99
|
+
this.stats.released++;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Release all resources associated with an entity.
|
|
104
|
+
* Called on entity removal or HMR update.
|
|
105
|
+
*/
|
|
106
|
+
releaseByEntity(entityId) {
|
|
107
|
+
let released = 0;
|
|
108
|
+
for (const [token, { entry }] of this.resources) {
|
|
109
|
+
if (entry.entityId === entityId) {
|
|
110
|
+
if (this.release(token)) {
|
|
111
|
+
released++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return released;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Release all resources of a specific type.
|
|
119
|
+
*/
|
|
120
|
+
releaseByType(type) {
|
|
121
|
+
let released = 0;
|
|
122
|
+
for (const [token, { entry }] of this.resources) {
|
|
123
|
+
if (entry.type === type) {
|
|
124
|
+
if (this.release(token)) {
|
|
125
|
+
released++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return released;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Force cleanup of all queued finalizations.
|
|
133
|
+
* Call after GC to ensure WASM memory is freed.
|
|
134
|
+
*/
|
|
135
|
+
flushCleanupQueue() {
|
|
136
|
+
const count = this.cleanupQueue.length;
|
|
137
|
+
for (const token of this.cleanupQueue) {
|
|
138
|
+
this.release(token.token);
|
|
139
|
+
}
|
|
140
|
+
this.cleanupQueue = [];
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Release ALL resources. Use with caution.
|
|
145
|
+
*/
|
|
146
|
+
releaseAll() {
|
|
147
|
+
let released = 0;
|
|
148
|
+
for (const token of this.resources.keys()) {
|
|
149
|
+
if (this.release(token)) {
|
|
150
|
+
released++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return released;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get current resource statistics.
|
|
157
|
+
*/
|
|
158
|
+
getStats() {
|
|
159
|
+
const byType = {
|
|
160
|
+
paint: 0,
|
|
161
|
+
path: 0,
|
|
162
|
+
image: 0,
|
|
163
|
+
font: 0,
|
|
164
|
+
shader: 0,
|
|
165
|
+
surface: 0,
|
|
166
|
+
};
|
|
167
|
+
let leakSuspects = 0;
|
|
168
|
+
const now = performance.now();
|
|
169
|
+
const LEAK_THRESHOLD_MS = 60000; // 1 minute without access
|
|
170
|
+
for (const { ref, entry } of this.resources.values()) {
|
|
171
|
+
const resource = ref.deref();
|
|
172
|
+
if (resource) {
|
|
173
|
+
byType[entry.type]++;
|
|
174
|
+
// Check for potential leaks (old, unaccessed resources)
|
|
175
|
+
if (now - entry.accessed > LEAK_THRESHOLD_MS) {
|
|
176
|
+
leakSuspects++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
totalAllocated: this.stats.allocated,
|
|
182
|
+
totalReleased: this.stats.released,
|
|
183
|
+
currentActive: this.resources.size,
|
|
184
|
+
byType,
|
|
185
|
+
leakSuspects,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Mark a resource as recently accessed (resets leak timer).
|
|
190
|
+
*/
|
|
191
|
+
touch(token) {
|
|
192
|
+
const entry = this.resources.get(token);
|
|
193
|
+
if (entry) {
|
|
194
|
+
entry.entry.accessed = performance.now();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ===========================================================================
|
|
198
|
+
// Private Methods
|
|
199
|
+
// ===========================================================================
|
|
200
|
+
/**
|
|
201
|
+
* Queue a cleanup token for batch processing.
|
|
202
|
+
*/
|
|
203
|
+
queueCleanup(token) {
|
|
204
|
+
this.cleanupQueue.push(token);
|
|
205
|
+
// Schedule batch cleanup on next microtask
|
|
206
|
+
if (!this.cleanupScheduled) {
|
|
207
|
+
this.cleanupScheduled = true;
|
|
208
|
+
queueMicrotask(() => {
|
|
209
|
+
this.flushCleanupQueue();
|
|
210
|
+
this.cleanupScheduled = false;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// Global Instance
|
|
217
|
+
// =============================================================================
|
|
218
|
+
/**
|
|
219
|
+
* Singleton resource manager for the renderer.
|
|
220
|
+
*/
|
|
221
|
+
export const wasmResources = new WASMResourceManager();
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Helper: Scoped Resource Guard
|
|
224
|
+
// =============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* RAII-style guard for temporary WASM resources.
|
|
227
|
+
*
|
|
228
|
+
* Usage:
|
|
229
|
+
* ```
|
|
230
|
+
* using(canvasKit.Paint(), paint => {
|
|
231
|
+
* canvas.drawRect(rect, paint);
|
|
232
|
+
* }); // paint.delete() called automatically
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function using(resource, fn) {
|
|
236
|
+
try {
|
|
237
|
+
return fn(resource);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
resource.delete();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Async version of using().
|
|
245
|
+
*/
|
|
246
|
+
export async function usingAsync(resource, fn) {
|
|
247
|
+
try {
|
|
248
|
+
return await fn(resource);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
resource.delete();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
import type { EntityId, RasterBounds, RenderableEntity } from '../ast/types.js';
|
|
32
|
+
/**
|
|
33
|
+
* Canvas draw command (batched for wgpu).
|
|
34
|
+
*/
|
|
35
|
+
interface DrawCommand {
|
|
36
|
+
entityId: EntityId;
|
|
37
|
+
type: 'path' | 'text' | 'image' | 'group';
|
|
38
|
+
bounds: RasterBounds;
|
|
39
|
+
payload: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* CanvasRenderer interface from render-loop.ts
|
|
43
|
+
*/
|
|
44
|
+
interface CanvasRenderer {
|
|
45
|
+
getEntity(id: EntityId): RenderableEntity | undefined;
|
|
46
|
+
draw(command: DrawCommand): void;
|
|
47
|
+
flush(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* WasmGpuRenderer interface (from vsc-wasm with "gpu" feature).
|
|
51
|
+
*
|
|
52
|
+
* This is the TypeScript-side view of the Rust struct.
|
|
53
|
+
*/
|
|
54
|
+
interface WasmGpuRenderer {
|
|
55
|
+
render(nodes_json: string): void;
|
|
56
|
+
resize(width: number, height: number): void;
|
|
57
|
+
readonly width: number;
|
|
58
|
+
readonly height: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Factory function for WasmGpuRenderer.
|
|
62
|
+
*/
|
|
63
|
+
interface WasmGpuRendererStatic {
|
|
64
|
+
create(canvas: HTMLCanvasElement): Promise<WasmGpuRenderer>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* wgpu-based CanvasRenderer implementation.
|
|
68
|
+
*
|
|
69
|
+
* This adapter accumulates DrawCommands during a frame, converts them to
|
|
70
|
+
* CanvasNode JSON, and sends them to WasmGpuRenderer on flush().
|
|
71
|
+
*
|
|
72
|
+
* ## Usage
|
|
73
|
+
*
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { createWgpuRendererAdapter } from './wgpu-renderer-adapter';
|
|
76
|
+
*
|
|
77
|
+
* const canvas = document.getElementById('viewport') as HTMLCanvasElement;
|
|
78
|
+
* const adapter = await createWgpuRendererAdapter(canvas, entityStore);
|
|
79
|
+
*
|
|
80
|
+
* // In render loop:
|
|
81
|
+
* adapter.draw(command1);
|
|
82
|
+
* adapter.draw(command2);
|
|
83
|
+
* adapter.flush(); // Sends all commands to GPU
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare class WgpuRendererAdapter implements CanvasRenderer {
|
|
87
|
+
/** WASM GPU renderer instance */
|
|
88
|
+
private renderer;
|
|
89
|
+
/** Entity store for getEntity() lookups */
|
|
90
|
+
private entityStore;
|
|
91
|
+
/** Accumulated draw commands for current frame */
|
|
92
|
+
private pendingCommands;
|
|
93
|
+
/** Canvas element for resize handling */
|
|
94
|
+
private canvas;
|
|
95
|
+
private constructor();
|
|
96
|
+
/**
|
|
97
|
+
* Create a new wgpu renderer adapter.
|
|
98
|
+
*
|
|
99
|
+
* @param canvas - HTML canvas element to render to
|
|
100
|
+
* @param entityStore - Map of entities for getEntity() lookups
|
|
101
|
+
* @param wasmModule - WASM module containing WasmGpuRenderer
|
|
102
|
+
*/
|
|
103
|
+
static create(canvas: HTMLCanvasElement, entityStore: Map<EntityId, RenderableEntity>, wasmModule: {
|
|
104
|
+
WasmGpuRenderer: WasmGpuRendererStatic;
|
|
105
|
+
}): Promise<WgpuRendererAdapter>;
|
|
106
|
+
/**
|
|
107
|
+
* Get a renderable entity by ID.
|
|
108
|
+
*/
|
|
109
|
+
getEntity(id: EntityId): RenderableEntity | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Queue a draw command for the current frame.
|
|
112
|
+
*
|
|
113
|
+
* Commands are accumulated until flush() is called.
|
|
114
|
+
*/
|
|
115
|
+
draw(command: DrawCommand): void;
|
|
116
|
+
/**
|
|
117
|
+
* Flush all pending commands to the GPU.
|
|
118
|
+
*
|
|
119
|
+
* This converts accumulated DrawCommands to CanvasNode JSON and
|
|
120
|
+
* sends them to the WASM renderer in a single call.
|
|
121
|
+
*
|
|
122
|
+
* ## Serialization
|
|
123
|
+
*
|
|
124
|
+
* Uses `wasmBoundaryReplacer` to handle:
|
|
125
|
+
* - `bigint` values (not supported by standard JSON.stringify)
|
|
126
|
+
* - `Rational` objects → `"num/den"` string format (Rust expectation)
|
|
127
|
+
*/
|
|
128
|
+
flush(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Handle canvas resize.
|
|
131
|
+
*
|
|
132
|
+
* Call this when the canvas element is resized.
|
|
133
|
+
*/
|
|
134
|
+
resize(width: number, height: number): void;
|
|
135
|
+
/**
|
|
136
|
+
* Get the current render surface dimensions.
|
|
137
|
+
*/
|
|
138
|
+
getDimensions(): {
|
|
139
|
+
width: number;
|
|
140
|
+
height: number;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Update the entity store reference.
|
|
144
|
+
*
|
|
145
|
+
* Call this when the entity store is replaced (e.g., after HMR).
|
|
146
|
+
*/
|
|
147
|
+
setEntityStore(store: Map<EntityId, RenderableEntity>): void;
|
|
148
|
+
/**
|
|
149
|
+
* Convert DrawCommands to CanvasNodes.
|
|
150
|
+
*
|
|
151
|
+
* This is the key translation layer between the render loop's command
|
|
152
|
+
* abstraction and the GPU renderer's scene graph.
|
|
153
|
+
*/
|
|
154
|
+
private commandsToCanvasNodes;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Create a wgpu renderer adapter.
|
|
158
|
+
*
|
|
159
|
+
* This is the main entry point for creating a wgpu-based renderer.
|
|
160
|
+
*
|
|
161
|
+
* @param canvas - HTML canvas element to render to
|
|
162
|
+
* @param entityStore - Map of entities for getEntity() lookups
|
|
163
|
+
* @param wasmModule - WASM module containing WasmGpuRenderer
|
|
164
|
+
*/
|
|
165
|
+
export declare function createWgpuRendererAdapter(canvas: HTMLCanvasElement, entityStore: Map<EntityId, RenderableEntity>, wasmModule: {
|
|
166
|
+
WasmGpuRenderer: WasmGpuRendererStatic;
|
|
167
|
+
}): Promise<WgpuRendererAdapter>;
|
|
168
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
// JSON Serialization Helpers
|
|
33
|
+
// =============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Custom JSON replacer for WASM boundary serialization.
|
|
36
|
+
*
|
|
37
|
+
* ## Problem
|
|
38
|
+
*
|
|
39
|
+
* JavaScript's `JSON.stringify()` does not support `bigint`:
|
|
40
|
+
* ```
|
|
41
|
+
* JSON.stringify({ n: 100n }) // TypeError: BigInt value can't be serialized
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ## Solution
|
|
45
|
+
*
|
|
46
|
+
* This replacer converts:
|
|
47
|
+
* - `bigint` → string (e.g., `100n` → `"100"`)
|
|
48
|
+
* - `Rational` object → `"numerator/denominator"` string
|
|
49
|
+
*
|
|
50
|
+
* The Rust side expects Rational as `"num/den"` string format
|
|
51
|
+
* (see `vsc-core/src/types.rs` Deserialize impl).
|
|
52
|
+
*/
|
|
53
|
+
function wasmBoundaryReplacer(_key, value) {
|
|
54
|
+
// Handle raw bigint (shouldn't occur in well-typed code, but safety first)
|
|
55
|
+
if (typeof value === 'bigint') {
|
|
56
|
+
return value.toString();
|
|
57
|
+
}
|
|
58
|
+
// Handle Rational objects: { numerator: bigint, denominator: bigint }
|
|
59
|
+
if (isRational(value)) {
|
|
60
|
+
return `${value.numerator}/${value.denominator}`;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type guard for Rational objects.
|
|
66
|
+
*/
|
|
67
|
+
function isRational(value) {
|
|
68
|
+
return (value !== null &&
|
|
69
|
+
typeof value === 'object' &&
|
|
70
|
+
'numerator' in value &&
|
|
71
|
+
'denominator' in value &&
|
|
72
|
+
typeof value.numerator === 'bigint' &&
|
|
73
|
+
typeof value.denominator === 'bigint');
|
|
74
|
+
}
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Adapter Implementation
|
|
77
|
+
// =============================================================================
|
|
78
|
+
/**
|
|
79
|
+
* wgpu-based CanvasRenderer implementation.
|
|
80
|
+
*
|
|
81
|
+
* This adapter accumulates DrawCommands during a frame, converts them to
|
|
82
|
+
* CanvasNode JSON, and sends them to WasmGpuRenderer on flush().
|
|
83
|
+
*
|
|
84
|
+
* ## Usage
|
|
85
|
+
*
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { createWgpuRendererAdapter } from './wgpu-renderer-adapter';
|
|
88
|
+
*
|
|
89
|
+
* const canvas = document.getElementById('viewport') as HTMLCanvasElement;
|
|
90
|
+
* const adapter = await createWgpuRendererAdapter(canvas, entityStore);
|
|
91
|
+
*
|
|
92
|
+
* // In render loop:
|
|
93
|
+
* adapter.draw(command1);
|
|
94
|
+
* adapter.draw(command2);
|
|
95
|
+
* adapter.flush(); // Sends all commands to GPU
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export class WgpuRendererAdapter {
|
|
99
|
+
/** WASM GPU renderer instance */
|
|
100
|
+
renderer;
|
|
101
|
+
/** Entity store for getEntity() lookups */
|
|
102
|
+
entityStore;
|
|
103
|
+
/** Accumulated draw commands for current frame */
|
|
104
|
+
pendingCommands = [];
|
|
105
|
+
/** Canvas element for resize handling */
|
|
106
|
+
canvas;
|
|
107
|
+
constructor(renderer, canvas, entityStore) {
|
|
108
|
+
this.renderer = renderer;
|
|
109
|
+
this.canvas = canvas;
|
|
110
|
+
this.entityStore = entityStore;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a new wgpu renderer adapter.
|
|
114
|
+
*
|
|
115
|
+
* @param canvas - HTML canvas element to render to
|
|
116
|
+
* @param entityStore - Map of entities for getEntity() lookups
|
|
117
|
+
* @param wasmModule - WASM module containing WasmGpuRenderer
|
|
118
|
+
*/
|
|
119
|
+
static async create(canvas, entityStore, wasmModule) {
|
|
120
|
+
const renderer = await wasmModule.WasmGpuRenderer.create(canvas);
|
|
121
|
+
return new WgpuRendererAdapter(renderer, canvas, entityStore);
|
|
122
|
+
}
|
|
123
|
+
// ===========================================================================
|
|
124
|
+
// CanvasRenderer Interface
|
|
125
|
+
// ===========================================================================
|
|
126
|
+
/**
|
|
127
|
+
* Get a renderable entity by ID.
|
|
128
|
+
*/
|
|
129
|
+
getEntity(id) {
|
|
130
|
+
return this.entityStore.get(id);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Queue a draw command for the current frame.
|
|
134
|
+
*
|
|
135
|
+
* Commands are accumulated until flush() is called.
|
|
136
|
+
*/
|
|
137
|
+
draw(command) {
|
|
138
|
+
this.pendingCommands.push(command);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Flush all pending commands to the GPU.
|
|
142
|
+
*
|
|
143
|
+
* This converts accumulated DrawCommands to CanvasNode JSON and
|
|
144
|
+
* sends them to the WASM renderer in a single call.
|
|
145
|
+
*
|
|
146
|
+
* ## Serialization
|
|
147
|
+
*
|
|
148
|
+
* Uses `wasmBoundaryReplacer` to handle:
|
|
149
|
+
* - `bigint` values (not supported by standard JSON.stringify)
|
|
150
|
+
* - `Rational` objects → `"num/den"` string format (Rust expectation)
|
|
151
|
+
*/
|
|
152
|
+
flush() {
|
|
153
|
+
if (this.pendingCommands.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Convert DrawCommands to CanvasNodes
|
|
157
|
+
const nodes = this.commandsToCanvasNodes(this.pendingCommands);
|
|
158
|
+
// Serialize with custom replacer for Rational/bigint handling
|
|
159
|
+
const json = JSON.stringify(nodes, wasmBoundaryReplacer);
|
|
160
|
+
this.renderer.render(json);
|
|
161
|
+
// Clear pending commands
|
|
162
|
+
this.pendingCommands = [];
|
|
163
|
+
}
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
// Public Utilities
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
/**
|
|
168
|
+
* Handle canvas resize.
|
|
169
|
+
*
|
|
170
|
+
* Call this when the canvas element is resized.
|
|
171
|
+
*/
|
|
172
|
+
resize(width, height) {
|
|
173
|
+
this.renderer.resize(width, height);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get the current render surface dimensions.
|
|
177
|
+
*/
|
|
178
|
+
getDimensions() {
|
|
179
|
+
return {
|
|
180
|
+
width: this.renderer.width,
|
|
181
|
+
height: this.renderer.height,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Update the entity store reference.
|
|
186
|
+
*
|
|
187
|
+
* Call this when the entity store is replaced (e.g., after HMR).
|
|
188
|
+
*/
|
|
189
|
+
setEntityStore(store) {
|
|
190
|
+
this.entityStore = store;
|
|
191
|
+
}
|
|
192
|
+
// ===========================================================================
|
|
193
|
+
// Private Methods
|
|
194
|
+
// ===========================================================================
|
|
195
|
+
/**
|
|
196
|
+
* Convert DrawCommands to CanvasNodes.
|
|
197
|
+
*
|
|
198
|
+
* This is the key translation layer between the render loop's command
|
|
199
|
+
* abstraction and the GPU renderer's scene graph.
|
|
200
|
+
*/
|
|
201
|
+
commandsToCanvasNodes(commands) {
|
|
202
|
+
const nodes = [];
|
|
203
|
+
for (const cmd of commands) {
|
|
204
|
+
const entity = this.entityStore.get(cmd.entityId);
|
|
205
|
+
if (!entity?.canvas) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Use the entity's CanvasNode directly
|
|
209
|
+
// The payload in DrawCommand may contain overrides, but for now
|
|
210
|
+
// we use the entity's stored canvas representation
|
|
211
|
+
nodes.push(entity.canvas);
|
|
212
|
+
}
|
|
213
|
+
return nodes;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// Factory Function
|
|
218
|
+
// =============================================================================
|
|
219
|
+
/**
|
|
220
|
+
* Create a wgpu renderer adapter.
|
|
221
|
+
*
|
|
222
|
+
* This is the main entry point for creating a wgpu-based renderer.
|
|
223
|
+
*
|
|
224
|
+
* @param canvas - HTML canvas element to render to
|
|
225
|
+
* @param entityStore - Map of entities for getEntity() lookups
|
|
226
|
+
* @param wasmModule - WASM module containing WasmGpuRenderer
|
|
227
|
+
*/
|
|
228
|
+
export async function createWgpuRendererAdapter(canvas, entityStore, wasmModule) {
|
|
229
|
+
return WgpuRendererAdapter.create(canvas, entityStore, wasmModule);
|
|
230
|
+
}
|