@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,435 @@
1
+ /**
2
+ * Atomic Render Loop for ViewScript
3
+ *
4
+ * This module implements a frame-synchronous render pipeline that guarantees
5
+ * Canvas and DOM layers are updated atomically within a single animation frame.
6
+ *
7
+ * ## Architectural Invariant: No Frame Tearing
8
+ *
9
+ * If a button's visual representation moves at frame N, its DOM hit region
10
+ * MUST also move at frame N. Any desynchronization causes Q-dimension inputs
11
+ * to "hit the void" - a catastrophic UX failure.
12
+ *
13
+ * ## Strategy: Double-Buffered Dirty Tracking
14
+ *
15
+ * ```
16
+ * Frame N:
17
+ * ┌─────────────────────────────────────────────────────────────────┐
18
+ * │ 1. Flush pending T-vector mutations (from Q-dimension events) │
19
+ * │ 2. Evaluate constraint graph (P-dimension solver) │
20
+ * │ 3. Topology-preserving rounding (with error distribution) │
21
+ * │ 4. Diff against previous frame's RasterBounds │
22
+ * │ 5. Batch Canvas draw commands (wgpu) │
23
+ * │ 6. Batch DOM style mutations (transform only, no reflow) │
24
+ * │ 7. Commit: GpuRenderer.flush() + requestAnimationFrame boundary│
25
+ * └─────────────────────────────────────────────────────────────────┘
26
+ * ```
27
+ *
28
+ * ## Reflow Prevention Strategy
29
+ *
30
+ * DOM mutations are restricted to compositor-only properties:
31
+ * - transform: translate3d(x, y, 0) - GPU-accelerated, no reflow
32
+ * - opacity - compositor-only
33
+ * - will-change: transform - hint to browser
34
+ *
35
+ * We NEVER touch: width, height, top, left, margin, padding (reflow triggers)
36
+ */
37
+ // =============================================================================
38
+ // Core Render Loop
39
+ // =============================================================================
40
+ export class AtomicRenderLoop {
41
+ config;
42
+ frameState;
43
+ pendingMutations = [];
44
+ isRunning = false;
45
+ rafHandle = null;
46
+ // External dependencies (injected)
47
+ constraintSolver;
48
+ topologyRounder;
49
+ canvasRenderer;
50
+ domLayer;
51
+ /**
52
+ * Event buffer for async event atomicity.
53
+ *
54
+ * Injected via setEventBuffer() to support mergeAsyncEvents() at tick start.
55
+ */
56
+ eventBuffer = null;
57
+ constructor(config, constraintSolver, topologyRounder, canvasRenderer, domLayer) {
58
+ this.config = {
59
+ targetFPS: 60,
60
+ debugTiming: false,
61
+ maxMutationsPerFrame: 100,
62
+ ...config,
63
+ };
64
+ this.frameState = {
65
+ previousBounds: new Map(),
66
+ currentBounds: new Map(),
67
+ dirtyEntities: new Set(),
68
+ frameNumber: 0,
69
+ };
70
+ this.constraintSolver = constraintSolver;
71
+ this.topologyRounder = topologyRounder;
72
+ this.canvasRenderer = canvasRenderer;
73
+ this.domLayer = domLayer;
74
+ }
75
+ // ===========================================================================
76
+ // Public API
77
+ // ===========================================================================
78
+ /**
79
+ * Start the render loop.
80
+ */
81
+ start() {
82
+ if (this.isRunning)
83
+ return;
84
+ this.isRunning = true;
85
+ this.scheduleFrame();
86
+ }
87
+ /**
88
+ * Stop the render loop.
89
+ */
90
+ stop() {
91
+ this.isRunning = false;
92
+ if (this.rafHandle !== null) {
93
+ cancelAnimationFrame(this.rafHandle);
94
+ this.rafHandle = null;
95
+ }
96
+ }
97
+ /**
98
+ * Queue a T-vector mutation from Q-dimension event.
99
+ * Called from event handlers (backpressure-controlled).
100
+ */
101
+ queueMutation(mutation) {
102
+ this.pendingMutations.push(mutation);
103
+ }
104
+ /**
105
+ * Set the event buffer for async event atomicity.
106
+ *
107
+ * The event buffer is used to merge async events (from fetch, setTimeout, etc.)
108
+ * at the start of each tick, ensuring deterministic event ordering.
109
+ */
110
+ setEventBuffer(buffer) {
111
+ this.eventBuffer = buffer;
112
+ }
113
+ // ===========================================================================
114
+ // Frame Execution (Atomic Commit)
115
+ // ===========================================================================
116
+ /**
117
+ * Execute a single frame tick.
118
+ *
119
+ * CRITICAL: This function executes entirely within a single rAF callback,
120
+ * ensuring Canvas and DOM updates are committed atomically before the
121
+ * browser's compositor thread runs.
122
+ */
123
+ tick(timestamp) {
124
+ if (!this.isRunning)
125
+ return;
126
+ const frameStart = performance.now();
127
+ this.frameState.frameNumber++;
128
+ // -------------------------------------------------------------------------
129
+ // Phase 0: Merge Async Events (MUST be first - async atomicity)
130
+ // -------------------------------------------------------------------------
131
+ // Events from async callbacks (fetch, setTimeout, promises) are isolated
132
+ // in a separate buffer to prevent race conditions. They are merged here
133
+ // at the START of the tick, before any sync event processing.
134
+ //
135
+ // This ensures:
136
+ // 1. Deterministic ordering: async events processed before sync events
137
+ // 2. Atomicity: no async events can arrive mid-tick
138
+ // 3. Consistency: the tick sees a complete snapshot of async state
139
+ if (this.eventBuffer) {
140
+ this.eventBuffer.mergeAsyncEvents();
141
+ }
142
+ // -------------------------------------------------------------------------
143
+ // Phase 1: Flush Pending Mutations (Backpressure-Limited)
144
+ // -------------------------------------------------------------------------
145
+ const mutations = this.flushMutations();
146
+ // -------------------------------------------------------------------------
147
+ // Phase 2: Evaluate Constraint Graph
148
+ // -------------------------------------------------------------------------
149
+ const pVectorBounds = this.constraintSolver.evaluate(mutations);
150
+ // -------------------------------------------------------------------------
151
+ // Phase 3: Topology-Preserving Rounding (with Error Distribution)
152
+ // -------------------------------------------------------------------------
153
+ const rasterResult = this.topologyRounder.round(pVectorBounds);
154
+ // -------------------------------------------------------------------------
155
+ // Phase 4: Compute Dirty Set (Diff Against Previous Frame)
156
+ // -------------------------------------------------------------------------
157
+ this.computeDirtySet(rasterResult.bounds);
158
+ // -------------------------------------------------------------------------
159
+ // Phase 5: Batch Canvas Draw Commands
160
+ // -------------------------------------------------------------------------
161
+ const drawCommands = this.buildDrawCommands();
162
+ // -------------------------------------------------------------------------
163
+ // Phase 6: Batch DOM Mutations (Transform Only)
164
+ // -------------------------------------------------------------------------
165
+ const domMutations = this.buildDOMMutations();
166
+ // -------------------------------------------------------------------------
167
+ // Phase 7: Atomic Commit
168
+ // -------------------------------------------------------------------------
169
+ this.commitFrame(drawCommands, domMutations);
170
+ // -------------------------------------------------------------------------
171
+ // Swap Buffers
172
+ // -------------------------------------------------------------------------
173
+ this.swapBuffers();
174
+ // -------------------------------------------------------------------------
175
+ // Schedule Next Frame
176
+ // -------------------------------------------------------------------------
177
+ const frameEnd = performance.now();
178
+ if (this.config.debugTiming) {
179
+ console.log(`Frame ${this.frameState.frameNumber}: ${(frameEnd - frameStart).toFixed(2)}ms`);
180
+ }
181
+ this.scheduleFrame();
182
+ }
183
+ scheduleFrame() {
184
+ this.rafHandle = requestAnimationFrame((ts) => this.tick(ts));
185
+ }
186
+ // ===========================================================================
187
+ // Phase Implementations
188
+ // ===========================================================================
189
+ /**
190
+ * Phase 1: Flush pending mutations with backpressure limit.
191
+ */
192
+ flushMutations() {
193
+ const limit = this.config.maxMutationsPerFrame;
194
+ const flushed = this.pendingMutations.splice(0, limit);
195
+ return flushed;
196
+ }
197
+ /**
198
+ * Phase 4: Compute which entities changed this frame.
199
+ */
200
+ computeDirtySet(currentBounds) {
201
+ this.frameState.dirtyEntities.clear();
202
+ this.frameState.currentBounds = currentBounds;
203
+ for (const [entityId, bounds] of currentBounds) {
204
+ const prev = this.frameState.previousBounds.get(entityId);
205
+ if (!prev || !boundsEqual(prev, bounds)) {
206
+ this.frameState.dirtyEntities.add(entityId);
207
+ }
208
+ }
209
+ // Detect removed entities
210
+ for (const entityId of this.frameState.previousBounds.keys()) {
211
+ if (!currentBounds.has(entityId)) {
212
+ this.frameState.dirtyEntities.add(entityId);
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Phase 5: Build Canvas draw commands for dirty entities only.
218
+ */
219
+ buildDrawCommands() {
220
+ const commands = [];
221
+ for (const entityId of this.frameState.dirtyEntities) {
222
+ const bounds = this.frameState.currentBounds.get(entityId);
223
+ if (!bounds)
224
+ continue;
225
+ // Get entity's canvas node and build draw command
226
+ const entity = this.canvasRenderer.getEntity(entityId);
227
+ if (entity?.canvas) {
228
+ commands.push({
229
+ entityId,
230
+ type: entity.canvas.kind,
231
+ bounds,
232
+ payload: entity.canvas,
233
+ });
234
+ }
235
+ }
236
+ return commands;
237
+ }
238
+ /**
239
+ * Phase 6: Build DOM mutations (transform-only, no reflow).
240
+ *
241
+ * CRITICAL: We use transform: translate3d() which is compositor-only.
242
+ * This means the GPU handles the positioning without triggering
243
+ * the browser's layout engine.
244
+ */
245
+ buildDOMMutations() {
246
+ const mutations = [];
247
+ for (const entityId of this.frameState.dirtyEntities) {
248
+ const bounds = this.frameState.currentBounds.get(entityId);
249
+ if (!bounds)
250
+ continue;
251
+ const element = this.domLayer.getElement(entityId);
252
+ if (!element)
253
+ continue;
254
+ // Use translate3d for GPU acceleration
255
+ // Note: Element must have position: absolute and will-change: transform
256
+ const transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
257
+ mutations.push({
258
+ entityId,
259
+ element,
260
+ transform,
261
+ });
262
+ }
263
+ return mutations;
264
+ }
265
+ /**
266
+ * Phase 7: Atomic commit of Canvas and DOM updates.
267
+ *
268
+ * This function guarantees that by the time rAF callback returns:
269
+ * 1. All wgpu draw commands have been flushed to GPU
270
+ * 2. All DOM transforms have been written
271
+ * 3. Browser's compositor will see consistent state
272
+ *
273
+ * ## Strict DOM Proxy Enforcement
274
+ *
275
+ * DOM elements are initialized with `initializeDOMProxyElement()` which
276
+ * installs a Proxy guard. Only `transform` can be modified here.
277
+ */
278
+ commitFrame(drawCommands, domMutations) {
279
+ // --- Canvas Commit ---
280
+ // GPU renderer batches internally; we just need to issue commands and flush
281
+ for (const cmd of drawCommands) {
282
+ this.canvasRenderer.draw(cmd);
283
+ }
284
+ this.canvasRenderer.flush();
285
+ // --- DOM Commit (Single Write Batch) ---
286
+ // Using updateDOMProxyTransform which is the only allowed mutation
287
+ for (const mutation of domMutations) {
288
+ updateDOMProxyTransform(mutation.element, this.frameState.currentBounds.get(mutation.entityId)?.x ?? 0, this.frameState.currentBounds.get(mutation.entityId)?.y ?? 0);
289
+ }
290
+ // No reads after writes in this function = no reflow triggered
291
+ }
292
+ /**
293
+ * Swap frame buffers for next frame's diff.
294
+ */
295
+ swapBuffers() {
296
+ this.frameState.previousBounds = new Map(this.frameState.currentBounds);
297
+ }
298
+ }
299
+ // =============================================================================
300
+ // Helper Functions
301
+ // =============================================================================
302
+ function boundsEqual(a, b) {
303
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
304
+ }
305
+ // =============================================================================
306
+ // DOM Proxy Enforcement (Architect Directive: Strict DOM Proxy)
307
+ // =============================================================================
308
+ /**
309
+ * CSS properties allowed on DOM proxy elements.
310
+ *
311
+ * These are compositor-only properties that do NOT trigger browser reflow.
312
+ * Any attempt to set other properties (margin, padding, display, etc.)
313
+ * will throw a runtime error.
314
+ */
315
+ const ALLOWED_CSS_PROPERTIES = new Set([
316
+ 'transform',
317
+ 'opacity',
318
+ 'pointerEvents',
319
+ 'pointer-events', // Allow both camelCase and kebab-case
320
+ 'willChange',
321
+ 'will-change',
322
+ ]);
323
+ /**
324
+ * Initialize a DOM element as a strict ViewScript proxy.
325
+ *
326
+ * This function:
327
+ * 1. Sets all required CSS properties for compositor-only updates
328
+ * 2. Wraps the style object in a Proxy to prevent layout-triggering mutations
329
+ * 3. Ensures the element is a "transparent tactile proxy" with no visual rendering
330
+ *
331
+ * ## Architect Directive: Zero Layout Engine Intervention
332
+ *
333
+ * The browser's layout engine (Reflow) must NEVER be invoked by DOM proxy
334
+ * elements. All positioning is done via GPU-accelerated `transform: translate3d()`.
335
+ */
336
+ export function initializeDOMProxyElement(element, bounds) {
337
+ // Store original style object for Proxy wrapping
338
+ const originalStyle = element.style;
339
+ // Apply mandatory CSS for compositor-only updates
340
+ originalStyle.position = 'absolute';
341
+ originalStyle.top = '0';
342
+ originalStyle.left = '0';
343
+ originalStyle.width = `${bounds.width}px`;
344
+ originalStyle.height = `${bounds.height}px`;
345
+ originalStyle.opacity = '0'; // Visually transparent
346
+ originalStyle.pointerEvents = 'auto'; // Tactile proxy active
347
+ originalStyle.willChange = 'transform'; // GPU layer hint
348
+ originalStyle.margin = '0'; // Explicit zero
349
+ originalStyle.padding = '0'; // Explicit zero
350
+ originalStyle.border = 'none'; // Explicit none
351
+ originalStyle.boxSizing = 'border-box'; // Predictable sizing
352
+ originalStyle.overflow = 'hidden'; // No scrollbars
353
+ originalStyle.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
354
+ // Create a Proxy to guard against layout-triggering mutations
355
+ const guardedStyle = new Proxy(originalStyle, {
356
+ set(target, property, value) {
357
+ // Allow setting allowed properties
358
+ if (ALLOWED_CSS_PROPERTIES.has(property)) {
359
+ return Reflect.set(target, property, value);
360
+ }
361
+ // Block all other properties with a clear error
362
+ throw new Error(`[ViewScript DOM Proxy Violation] Cannot set CSS property '${property}' on DOM proxy element. ` +
363
+ `Only compositor-only properties are allowed: ${[...ALLOWED_CSS_PROPERTIES].join(', ')}. ` +
364
+ `This restriction prevents browser layout engine intervention.`);
365
+ },
366
+ get(target, property) {
367
+ return Reflect.get(target, property);
368
+ },
369
+ });
370
+ // Replace the element's style with the guarded proxy
371
+ // Note: We can't directly assign to element.style, but we can use defineProperty
372
+ Object.defineProperty(element, 'style', {
373
+ value: guardedStyle,
374
+ writable: false,
375
+ configurable: false,
376
+ });
377
+ }
378
+ /**
379
+ * Update a DOM proxy element's transform (position).
380
+ *
381
+ * This is the ONLY way to change a proxy element's position after initialization.
382
+ * It uses compositor-only transform, avoiding layout recalculation.
383
+ */
384
+ export function updateDOMProxyTransform(element, x, y) {
385
+ // Direct property access bypasses our Proxy guard (intentionally)
386
+ // because this is an internal, trusted call
387
+ element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
388
+ }
389
+ // =============================================================================
390
+ // Control Flow Summary (for Architect)
391
+ // =============================================================================
392
+ /**
393
+ * ## tick() Control Flow (Pseudocode)
394
+ *
395
+ * ```
396
+ * function tick(timestamp):
397
+ * // 1. Backpressure: Take at most N mutations from queue
398
+ * mutations = pendingMutations.splice(0, MAX_PER_FRAME)
399
+ *
400
+ * // 2. P-dimension: Solve constraints with new T-vector values
401
+ * pBounds = constraintSolver.evaluate(mutations)
402
+ *
403
+ * // 3. Rasterize: Rational → Integer with topology preservation
404
+ * rBounds = topologyRounder.round(pBounds)
405
+ *
406
+ * // 4. Diff: Find entities that changed since last frame
407
+ * dirtySet = diff(previousBounds, rBounds)
408
+ *
409
+ * // 5. Canvas: Build draw commands for dirty entities only
410
+ * drawCmds = buildDrawCommands(dirtySet)
411
+ *
412
+ * // 6. DOM: Build transform mutations (no width/height!)
413
+ * domMuts = buildDOMMutations(dirtySet)
414
+ *
415
+ * // 7. ATOMIC COMMIT (no interleaved reads/writes)
416
+ * for cmd in drawCmds: canvasRenderer.draw(cmd)
417
+ * canvasRenderer.flush() // GPU sync point
418
+ * for mut in domMuts: mut.element.style.transform = mut.transform
419
+ *
420
+ * // 8. Swap buffers
421
+ * previousBounds = rBounds
422
+ *
423
+ * // 9. Schedule next frame
424
+ * requestAnimationFrame(tick)
425
+ * ```
426
+ *
427
+ * ## Reflow Prevention Strategy
428
+ *
429
+ * 1. DOM elements are created once with fixed dimensions (via CSS)
430
+ * 2. Position changes use ONLY transform: translate3d()
431
+ * 3. translate3d() is compositor-only (GPU, no layout recalc)
432
+ * 4. will-change: transform hints browser to promote layer
433
+ * 5. All style writes happen before any reads (no thrashing)
434
+ * 6. Size changes require re-creating the element (rare)
435
+ */
@@ -0,0 +1,122 @@
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
+ * Any GPU WASM object with a delete() method.
31
+ */
32
+ interface Deletable {
33
+ delete(): void;
34
+ isDeleted?(): boolean;
35
+ }
36
+ type ResourceType = 'paint' | 'path' | 'image' | 'font' | 'shader' | 'surface';
37
+ /**
38
+ * Resource pool statistics.
39
+ */
40
+ export interface ResourceStats {
41
+ totalAllocated: number;
42
+ totalReleased: number;
43
+ currentActive: number;
44
+ byType: Record<ResourceType, number>;
45
+ leakSuspects: number;
46
+ }
47
+ export declare class WASMResourceManager {
48
+ /** FinalizationRegistry for automatic cleanup on GC */
49
+ private registry;
50
+ /** Active resources (strong references for explicit management) */
51
+ private resources;
52
+ /** Statistics */
53
+ private stats;
54
+ /** Cleanup queue (for batch processing) */
55
+ private cleanupQueue;
56
+ private cleanupScheduled;
57
+ constructor();
58
+ /**
59
+ * Register a WASM resource for tracking.
60
+ *
61
+ * @param resource - GPU WASM object with delete() method
62
+ * @param type - Resource category
63
+ * @param entityId - Associated entity (optional)
64
+ * @returns Symbol token for explicit release
65
+ */
66
+ register<T extends Deletable>(resource: T, type: ResourceType, entityId?: number | null): symbol;
67
+ /**
68
+ * Explicitly release a resource by token.
69
+ * Preferred over waiting for GC.
70
+ */
71
+ release(token: symbol): boolean;
72
+ /**
73
+ * Release all resources associated with an entity.
74
+ * Called on entity removal or HMR update.
75
+ */
76
+ releaseByEntity(entityId: number): number;
77
+ /**
78
+ * Release all resources of a specific type.
79
+ */
80
+ releaseByType(type: ResourceType): number;
81
+ /**
82
+ * Force cleanup of all queued finalizations.
83
+ * Call after GC to ensure WASM memory is freed.
84
+ */
85
+ flushCleanupQueue(): number;
86
+ /**
87
+ * Release ALL resources. Use with caution.
88
+ */
89
+ releaseAll(): number;
90
+ /**
91
+ * Get current resource statistics.
92
+ */
93
+ getStats(): ResourceStats;
94
+ /**
95
+ * Mark a resource as recently accessed (resets leak timer).
96
+ */
97
+ touch(token: symbol): void;
98
+ /**
99
+ * Queue a cleanup token for batch processing.
100
+ */
101
+ private queueCleanup;
102
+ }
103
+ /**
104
+ * Singleton resource manager for the renderer.
105
+ */
106
+ export declare const wasmResources: WASMResourceManager;
107
+ /**
108
+ * RAII-style guard for temporary WASM resources.
109
+ *
110
+ * Usage:
111
+ * ```
112
+ * using(canvasKit.Paint(), paint => {
113
+ * canvas.drawRect(rect, paint);
114
+ * }); // paint.delete() called automatically
115
+ * ```
116
+ */
117
+ export declare function using<T extends Deletable, R>(resource: T, fn: (resource: T) => R): R;
118
+ /**
119
+ * Async version of using().
120
+ */
121
+ export declare function usingAsync<T extends Deletable, R>(resource: T, fn: (resource: T) => Promise<R>): Promise<R>;
122
+ export {};