@viewscript/renderer 0.1.0-202605140721 → 0.1.0-202605141229
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/runtime/__tests__/ffi-dispatcher.test.d.ts +11 -0
- package/dist/runtime/__tests__/ffi-dispatcher.test.js +408 -0
- package/dist/runtime/__tests__/ffi-integration.test.d.ts +9 -0
- package/dist/runtime/__tests__/ffi-integration.test.js +514 -0
- package/dist/runtime/__tests__/wasm-solver-bridge.test.d.ts +9 -0
- package/dist/runtime/__tests__/wasm-solver-bridge.test.js +214 -0
- package/dist/runtime/ffi-dispatcher.d.ts +217 -0
- package/dist/runtime/ffi-dispatcher.js +291 -0
- package/dist/runtime/render-loop.d.ts +46 -1
- package/dist/runtime/render-loop.js +67 -1
- package/dist/runtime/wasm-solver-bridge.d.ts +122 -0
- package/dist/runtime/wasm-solver-bridge.js +168 -0
- package/package.json +1 -1
- package/src/runtime/__tests__/ffi-dispatcher.test.ts +519 -0
- package/src/runtime/__tests__/ffi-integration.test.ts +650 -0
- package/src/runtime/__tests__/wasm-solver-bridge.test.ts +276 -0
- package/src/runtime/ffi-dispatcher.ts +451 -0
- package/src/runtime/render-loop.ts +90 -2
- package/src/runtime/wasm-solver-bridge.ts +268 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFI Dispatcher for ViewScript Runtime
|
|
3
|
+
*
|
|
4
|
+
* Evaluates pending FFI calls after frame commit (Phase 8) and buffers
|
|
5
|
+
* results for consumption in the next frame's QSnapshot merge (Phase 0-1).
|
|
6
|
+
*
|
|
7
|
+
* ## Design Rationale
|
|
8
|
+
*
|
|
9
|
+
* FFI function execution time is non-deterministic (user-defined functions).
|
|
10
|
+
* By executing after commitFrame(), we:
|
|
11
|
+
* 1. Keep the rendering critical path (Phase 2-7) free of unpredictable latency
|
|
12
|
+
* 2. Utilize the idle time between commitFrame() and the next rAF callback
|
|
13
|
+
* 3. Maintain 1-frame latency for FFI results (Axiom 2: Ouroboros Binding)
|
|
14
|
+
*
|
|
15
|
+
* ## Data Flow
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* Frame N:
|
|
19
|
+
* Phase 2: tick() → TickResult { pending_ffi_calls }
|
|
20
|
+
* Phase 8: dispatch(pending_ffi_calls) → buffer results
|
|
21
|
+
*
|
|
22
|
+
* Frame N+1:
|
|
23
|
+
* Phase 0-1: drainResults() → QSnapshot.values merge
|
|
24
|
+
* Phase 2: solver sees FFI results as Q-dimension input
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// FFI Dispatcher
|
|
29
|
+
// =============================================================================
|
|
30
|
+
export class FfiDispatcher {
|
|
31
|
+
/** Function registry: ffi_id -> entry */
|
|
32
|
+
registry = new Map();
|
|
33
|
+
/** Pending modules awaiting registration */
|
|
34
|
+
pendingModules = new Set();
|
|
35
|
+
/** Buffered results for next frame */
|
|
36
|
+
resultBuffer = [];
|
|
37
|
+
/** In-flight async calls (prevents duplicate dispatch while awaiting) */
|
|
38
|
+
inflight = new Set();
|
|
39
|
+
/** Entity coordinate resolver (injected) */
|
|
40
|
+
coordResolver = null;
|
|
41
|
+
/** Q-variable resolver (injected) */
|
|
42
|
+
qResolver = null;
|
|
43
|
+
// ===========================================================================
|
|
44
|
+
// Public API
|
|
45
|
+
// ===========================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Load FFI manifest and build function registry.
|
|
48
|
+
*
|
|
49
|
+
* @param manifest - Parsed FfiManifest JSON
|
|
50
|
+
*/
|
|
51
|
+
loadManifest(manifest) {
|
|
52
|
+
this.registry.clear();
|
|
53
|
+
this.pendingModules.clear();
|
|
54
|
+
// Register bindings
|
|
55
|
+
for (const binding of manifest.bindings) {
|
|
56
|
+
this.registry.set(binding.ffi_id, {
|
|
57
|
+
ffi_id: binding.ffi_id,
|
|
58
|
+
bind_name: binding.bind_name,
|
|
59
|
+
module_path: binding.module_path,
|
|
60
|
+
export_name: binding.export_name,
|
|
61
|
+
args: binding.args,
|
|
62
|
+
fn: null,
|
|
63
|
+
});
|
|
64
|
+
this.pendingModules.add(binding.module_path);
|
|
65
|
+
}
|
|
66
|
+
// Register triggers (each trigger has its own function reference)
|
|
67
|
+
for (const trigger of manifest.triggers) {
|
|
68
|
+
if (!this.registry.has(trigger.ffi_id)) {
|
|
69
|
+
this.registry.set(trigger.ffi_id, {
|
|
70
|
+
ffi_id: trigger.ffi_id,
|
|
71
|
+
bind_name: `trigger_${trigger.trigger_id}`, // Triggers don't have bind names
|
|
72
|
+
module_path: trigger.module_path,
|
|
73
|
+
export_name: trigger.export_name,
|
|
74
|
+
args: trigger.args,
|
|
75
|
+
fn: null,
|
|
76
|
+
});
|
|
77
|
+
this.pendingModules.add(trigger.module_path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register a dynamically imported ESM module.
|
|
83
|
+
*
|
|
84
|
+
* Call this after `import(modulePath)` resolves.
|
|
85
|
+
*
|
|
86
|
+
* @param modulePath - Resolved module path (must match manifest's module_path)
|
|
87
|
+
* @param module - The imported module object
|
|
88
|
+
*/
|
|
89
|
+
registerModule(modulePath, module) {
|
|
90
|
+
for (const entry of this.registry.values()) {
|
|
91
|
+
if (entry.module_path === modulePath) {
|
|
92
|
+
const fn = module[entry.export_name];
|
|
93
|
+
if (typeof fn === 'function') {
|
|
94
|
+
entry.fn = fn;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.warn(`[FfiDispatcher] Export '${entry.export_name}' from '${modulePath}' is not a function`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.pendingModules.delete(modulePath);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set the coordinate resolver for EntityCoord arguments.
|
|
105
|
+
*/
|
|
106
|
+
setCoordinateResolver(resolver) {
|
|
107
|
+
this.coordResolver = resolver;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Set the Q-variable resolver for QRef arguments.
|
|
111
|
+
*/
|
|
112
|
+
setQResolver(resolver) {
|
|
113
|
+
this.qResolver = resolver;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get list of module paths that need to be imported.
|
|
117
|
+
*/
|
|
118
|
+
getPendingModules() {
|
|
119
|
+
return [...this.pendingModules];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if all modules have been registered.
|
|
123
|
+
*/
|
|
124
|
+
isReady() {
|
|
125
|
+
return this.pendingModules.size === 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Dispatch pending FFI calls (Phase 8).
|
|
129
|
+
*
|
|
130
|
+
* Evaluates JS functions and buffers results. Supports both sync and async
|
|
131
|
+
* functions. For async functions, results are buffered when the Promise
|
|
132
|
+
* resolves (may be multiple frames later).
|
|
133
|
+
*
|
|
134
|
+
* In-flight guard: If an async call for a given ffi_id is still pending,
|
|
135
|
+
* subsequent dispatch requests for the same ffi_id are skipped until the
|
|
136
|
+
* previous call completes.
|
|
137
|
+
*
|
|
138
|
+
* @param pendingCalls - Array of PendingFfiCall from TickResult
|
|
139
|
+
*/
|
|
140
|
+
dispatch(pendingCalls) {
|
|
141
|
+
for (const call of pendingCalls) {
|
|
142
|
+
const entry = this.registry.get(call.ffi_id);
|
|
143
|
+
if (!entry) {
|
|
144
|
+
console.warn(`[FfiDispatcher] Unknown ffi_id: ${call.ffi_id}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!entry.fn) {
|
|
148
|
+
console.warn(`[FfiDispatcher] Function not registered for ffi_id ${call.ffi_id} ` +
|
|
149
|
+
`(${entry.module_path}::${entry.export_name})`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// In-flight guard: skip if previous async call hasn't completed
|
|
153
|
+
if (this.inflight.has(call.ffi_id)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
// Resolve arguments
|
|
158
|
+
const resolvedArgs = this.resolveArgs(entry.args, call.args);
|
|
159
|
+
// Call the function
|
|
160
|
+
const result = entry.fn(...resolvedArgs);
|
|
161
|
+
// Handle async (Promise) results
|
|
162
|
+
if (result instanceof Promise) {
|
|
163
|
+
this.inflight.add(call.ffi_id);
|
|
164
|
+
result
|
|
165
|
+
.then((value) => {
|
|
166
|
+
this.bufferResult(entry.bind_name, value);
|
|
167
|
+
})
|
|
168
|
+
.catch((error) => {
|
|
169
|
+
console.error(`[FfiDispatcher] Async error in ${entry.export_name}:`, error);
|
|
170
|
+
})
|
|
171
|
+
.finally(() => {
|
|
172
|
+
this.inflight.delete(call.ffi_id);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Handle sync results
|
|
177
|
+
this.bufferResult(entry.bind_name, result);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error(`[FfiDispatcher] Error calling ${entry.export_name}:`, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Buffer a result value if it's numeric.
|
|
187
|
+
*/
|
|
188
|
+
bufferResult(bindName, result) {
|
|
189
|
+
const timestamp = performance.now();
|
|
190
|
+
if (typeof result === 'number') {
|
|
191
|
+
this.resultBuffer.push({
|
|
192
|
+
name: bindName,
|
|
193
|
+
value: result,
|
|
194
|
+
timestamp,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (result !== undefined && result !== null) {
|
|
198
|
+
// Attempt numeric coercion for non-number results
|
|
199
|
+
const numValue = Number(result);
|
|
200
|
+
if (!isNaN(numValue)) {
|
|
201
|
+
this.resultBuffer.push({
|
|
202
|
+
name: bindName,
|
|
203
|
+
value: numValue,
|
|
204
|
+
timestamp,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Non-numeric results are silently ignored (side-effect only functions)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Drain buffered results for QSnapshot merge (Phase 0-1).
|
|
212
|
+
*
|
|
213
|
+
* Returns all buffered results and clears the buffer.
|
|
214
|
+
* Call this at the start of the next frame.
|
|
215
|
+
*
|
|
216
|
+
* @returns Array of FfiResult for QSnapshot.values merge
|
|
217
|
+
*/
|
|
218
|
+
drainResults() {
|
|
219
|
+
const results = this.resultBuffer;
|
|
220
|
+
this.resultBuffer = [];
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get the number of buffered results (for diagnostics).
|
|
225
|
+
*/
|
|
226
|
+
getBufferedCount() {
|
|
227
|
+
return this.resultBuffer.length;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get the number of in-flight async calls (for diagnostics/testing).
|
|
231
|
+
*/
|
|
232
|
+
getInflightCount() {
|
|
233
|
+
return this.inflight.size;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if a specific ffi_id has an in-flight async call.
|
|
237
|
+
*/
|
|
238
|
+
isInflight(ffiId) {
|
|
239
|
+
return this.inflight.has(ffiId);
|
|
240
|
+
}
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
// Internal Helpers
|
|
243
|
+
// ===========================================================================
|
|
244
|
+
/**
|
|
245
|
+
* Resolve FfiArg array to concrete values.
|
|
246
|
+
*
|
|
247
|
+
* @param argSpecs - Argument specifications from manifest
|
|
248
|
+
* @param runtimeArgs - Runtime argument overrides from PendingFfiCall
|
|
249
|
+
*/
|
|
250
|
+
resolveArgs(argSpecs, runtimeArgs) {
|
|
251
|
+
// If runtime args are provided, use them directly (pre-resolved by WASM)
|
|
252
|
+
if (runtimeArgs.length > 0) {
|
|
253
|
+
return runtimeArgs;
|
|
254
|
+
}
|
|
255
|
+
// Otherwise, resolve from specs
|
|
256
|
+
return argSpecs.map((spec) => this.resolveArg(spec));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Resolve a single FfiArg to its concrete value.
|
|
260
|
+
*/
|
|
261
|
+
resolveArg(spec) {
|
|
262
|
+
switch (spec.type) {
|
|
263
|
+
case 'static':
|
|
264
|
+
return spec.value;
|
|
265
|
+
case 'q_ref':
|
|
266
|
+
if (this.qResolver) {
|
|
267
|
+
return this.qResolver.getQValue(spec.name);
|
|
268
|
+
}
|
|
269
|
+
console.warn(`[FfiDispatcher] No Q resolver, cannot resolve ${spec.name}`);
|
|
270
|
+
return 0;
|
|
271
|
+
case 'entity_coord':
|
|
272
|
+
if (this.coordResolver) {
|
|
273
|
+
return this.coordResolver.getEntityCoord(spec.entity_id, spec.component);
|
|
274
|
+
}
|
|
275
|
+
console.warn(`[FfiDispatcher] No coord resolver, cannot resolve entity ${spec.entity_id}.${spec.component}`);
|
|
276
|
+
return 0;
|
|
277
|
+
default:
|
|
278
|
+
console.warn(`[FfiDispatcher] Unknown arg type: ${spec.type}`);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// =============================================================================
|
|
284
|
+
// Factory Function
|
|
285
|
+
// =============================================================================
|
|
286
|
+
/**
|
|
287
|
+
* Create a new FFI dispatcher instance.
|
|
288
|
+
*/
|
|
289
|
+
export function createFfiDispatcher() {
|
|
290
|
+
return new FfiDispatcher();
|
|
291
|
+
}
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
* We NEVER touch: width, height, top, left, margin, padding (reflow triggers)
|
|
36
36
|
*/
|
|
37
37
|
import type { EntityId, RenderableEntity, RasterBounds, PVectorBounds } from '../ast/types';
|
|
38
|
+
import type { FfiDispatcher, PendingFfiCall } from './ffi-dispatcher.js';
|
|
39
|
+
import type { WasmSolverBridge } from './wasm-solver-bridge.js';
|
|
38
40
|
/**
|
|
39
41
|
* Semantic T-vector state keys.
|
|
40
42
|
* Mirrors the type from event-backpressure.ts.
|
|
@@ -91,6 +93,23 @@ export declare class AtomicRenderLoop {
|
|
|
91
93
|
* Injected via setEventBuffer() to support mergeAsyncEvents() at tick start.
|
|
92
94
|
*/
|
|
93
95
|
private eventBuffer;
|
|
96
|
+
/**
|
|
97
|
+
* FFI dispatcher for Phase 8 function evaluation.
|
|
98
|
+
*
|
|
99
|
+
* Injected via setFfiDispatcher() to support JS function calls after frame commit.
|
|
100
|
+
*/
|
|
101
|
+
private ffiDispatcher;
|
|
102
|
+
/**
|
|
103
|
+
* Latest tick result from WASM solver (for Phase 8 FFI dispatch).
|
|
104
|
+
*/
|
|
105
|
+
private latestTickResult;
|
|
106
|
+
/**
|
|
107
|
+
* Concrete solver bridge for FFI result injection (Phase 0.5).
|
|
108
|
+
*
|
|
109
|
+
* This is the concrete type of constraintSolver, used only for
|
|
110
|
+
* injectFfiResults(). The ConstraintSolver interface remains FFI-agnostic.
|
|
111
|
+
*/
|
|
112
|
+
private solverBridge;
|
|
94
113
|
constructor(config: Partial<RenderLoopConfig>, constraintSolver: ConstraintSolver, topologyRounder: TopologyRounder, canvasRenderer: CanvasRenderer, domLayer: DOMLayer);
|
|
95
114
|
/**
|
|
96
115
|
* Start the render loop.
|
|
@@ -112,6 +131,21 @@ export declare class AtomicRenderLoop {
|
|
|
112
131
|
* at the start of each tick, ensuring deterministic event ordering.
|
|
113
132
|
*/
|
|
114
133
|
setEventBuffer(buffer: EventBufferInterface): void;
|
|
134
|
+
/**
|
|
135
|
+
* Set the FFI dispatcher for Phase 8 function evaluation.
|
|
136
|
+
*
|
|
137
|
+
* The FFI dispatcher evaluates JS functions after frame commit and buffers
|
|
138
|
+
* results for the next frame's QSnapshot merge.
|
|
139
|
+
*/
|
|
140
|
+
setFfiDispatcher(dispatcher: FfiDispatcher): void;
|
|
141
|
+
/**
|
|
142
|
+
* Set the concrete solver bridge for FFI result injection.
|
|
143
|
+
*
|
|
144
|
+
* The solver bridge is the concrete implementation of ConstraintSolver
|
|
145
|
+
* that supports injectFfiResults(). This must be the same instance
|
|
146
|
+
* as the constraintSolver passed to the constructor.
|
|
147
|
+
*/
|
|
148
|
+
setSolverBridge(bridge: WasmSolverBridge): void;
|
|
115
149
|
/**
|
|
116
150
|
* Execute a single frame tick.
|
|
117
151
|
*
|
|
@@ -176,8 +210,19 @@ export declare class AtomicRenderLoop {
|
|
|
176
210
|
*
|
|
177
211
|
* This ensures the constraint graph is the single source of truth.
|
|
178
212
|
*/
|
|
213
|
+
/**
|
|
214
|
+
* Result of constraint solver evaluation.
|
|
215
|
+
*
|
|
216
|
+
* Contains both P-dimension bounds and pending FFI calls from WASM tick().
|
|
217
|
+
*/
|
|
218
|
+
interface SolverEvaluationResult {
|
|
219
|
+
/** P-dimension bounds for all entities */
|
|
220
|
+
bounds: Map<EntityId, PVectorBounds>;
|
|
221
|
+
/** Pending FFI calls from trigger evaluation (may be empty) */
|
|
222
|
+
pendingFfiCalls: PendingFfiCall[];
|
|
223
|
+
}
|
|
179
224
|
interface ConstraintSolver {
|
|
180
|
-
evaluate(mutations: TStateMutation[]):
|
|
225
|
+
evaluate(mutations: TStateMutation[]): SolverEvaluationResult;
|
|
181
226
|
}
|
|
182
227
|
interface TopologyRounder {
|
|
183
228
|
round(bounds: Map<EntityId, PVectorBounds>): {
|
|
@@ -54,6 +54,23 @@ export class AtomicRenderLoop {
|
|
|
54
54
|
* Injected via setEventBuffer() to support mergeAsyncEvents() at tick start.
|
|
55
55
|
*/
|
|
56
56
|
eventBuffer = null;
|
|
57
|
+
/**
|
|
58
|
+
* FFI dispatcher for Phase 8 function evaluation.
|
|
59
|
+
*
|
|
60
|
+
* Injected via setFfiDispatcher() to support JS function calls after frame commit.
|
|
61
|
+
*/
|
|
62
|
+
ffiDispatcher = null;
|
|
63
|
+
/**
|
|
64
|
+
* Latest tick result from WASM solver (for Phase 8 FFI dispatch).
|
|
65
|
+
*/
|
|
66
|
+
latestTickResult = null;
|
|
67
|
+
/**
|
|
68
|
+
* Concrete solver bridge for FFI result injection (Phase 0.5).
|
|
69
|
+
*
|
|
70
|
+
* This is the concrete type of constraintSolver, used only for
|
|
71
|
+
* injectFfiResults(). The ConstraintSolver interface remains FFI-agnostic.
|
|
72
|
+
*/
|
|
73
|
+
solverBridge = null;
|
|
57
74
|
constructor(config, constraintSolver, topologyRounder, canvasRenderer, domLayer) {
|
|
58
75
|
this.config = {
|
|
59
76
|
targetFPS: 60,
|
|
@@ -110,6 +127,25 @@ export class AtomicRenderLoop {
|
|
|
110
127
|
setEventBuffer(buffer) {
|
|
111
128
|
this.eventBuffer = buffer;
|
|
112
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Set the FFI dispatcher for Phase 8 function evaluation.
|
|
132
|
+
*
|
|
133
|
+
* The FFI dispatcher evaluates JS functions after frame commit and buffers
|
|
134
|
+
* results for the next frame's QSnapshot merge.
|
|
135
|
+
*/
|
|
136
|
+
setFfiDispatcher(dispatcher) {
|
|
137
|
+
this.ffiDispatcher = dispatcher;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Set the concrete solver bridge for FFI result injection.
|
|
141
|
+
*
|
|
142
|
+
* The solver bridge is the concrete implementation of ConstraintSolver
|
|
143
|
+
* that supports injectFfiResults(). This must be the same instance
|
|
144
|
+
* as the constraintSolver passed to the constructor.
|
|
145
|
+
*/
|
|
146
|
+
setSolverBridge(bridge) {
|
|
147
|
+
this.solverBridge = bridge;
|
|
148
|
+
}
|
|
113
149
|
// ===========================================================================
|
|
114
150
|
// Frame Execution (Atomic Commit)
|
|
115
151
|
// ===========================================================================
|
|
@@ -140,13 +176,31 @@ export class AtomicRenderLoop {
|
|
|
140
176
|
this.eventBuffer.mergeAsyncEvents();
|
|
141
177
|
}
|
|
142
178
|
// -------------------------------------------------------------------------
|
|
179
|
+
// Phase 0.5: Merge FFI Results from Previous Frame
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
// FFI function results from Phase 8 of the previous frame are drained
|
|
182
|
+
// here and injected into the solver bridge. The bridge will include these
|
|
183
|
+
// values in the QSnapshot when evaluate() is called in Phase 2.
|
|
184
|
+
//
|
|
185
|
+
// This maintains the 1-frame latency required by Axiom 2 (Ouroboros Binding):
|
|
186
|
+
// FFI results flow Q→T→P, never directly into P-dimension.
|
|
187
|
+
if (this.ffiDispatcher && this.solverBridge) {
|
|
188
|
+
const ffiResults = this.ffiDispatcher.drainResults();
|
|
189
|
+
if (ffiResults.length > 0) {
|
|
190
|
+
this.solverBridge.injectFfiResults(ffiResults);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
143
194
|
// Phase 1: Flush Pending Mutations (Backpressure-Limited)
|
|
144
195
|
// -------------------------------------------------------------------------
|
|
145
196
|
const mutations = this.flushMutations();
|
|
146
197
|
// -------------------------------------------------------------------------
|
|
147
198
|
// Phase 2: Evaluate Constraint Graph
|
|
148
199
|
// -------------------------------------------------------------------------
|
|
149
|
-
const
|
|
200
|
+
const solverResult = this.constraintSolver.evaluate(mutations);
|
|
201
|
+
const pVectorBounds = solverResult.bounds;
|
|
202
|
+
// Store pending FFI calls for Phase 8 (after commit)
|
|
203
|
+
this.latestTickResult = { pending_ffi_calls: solverResult.pendingFfiCalls };
|
|
150
204
|
// -------------------------------------------------------------------------
|
|
151
205
|
// Phase 3: Topology-Preserving Rounding (with Error Distribution)
|
|
152
206
|
// -------------------------------------------------------------------------
|
|
@@ -168,6 +222,18 @@ export class AtomicRenderLoop {
|
|
|
168
222
|
// -------------------------------------------------------------------------
|
|
169
223
|
this.commitFrame(drawCommands, domMutations);
|
|
170
224
|
// -------------------------------------------------------------------------
|
|
225
|
+
// Phase 8: FFI Dispatch (Post-Commit)
|
|
226
|
+
// -------------------------------------------------------------------------
|
|
227
|
+
// FFI functions are evaluated AFTER frame commit to keep the rendering
|
|
228
|
+
// critical path (Phase 2-7) free of unpredictable latency. Results are
|
|
229
|
+
// buffered for consumption in the next frame's Phase 0.5.
|
|
230
|
+
//
|
|
231
|
+
// This utilizes the idle time between commitFrame() and the next rAF.
|
|
232
|
+
if (this.ffiDispatcher && this.latestTickResult) {
|
|
233
|
+
this.ffiDispatcher.dispatch(this.latestTickResult.pending_ffi_calls);
|
|
234
|
+
this.latestTickResult = null;
|
|
235
|
+
}
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
171
237
|
// Swap Buffers
|
|
172
238
|
// -------------------------------------------------------------------------
|
|
173
239
|
this.swapBuffers();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM Solver Bridge
|
|
3
|
+
*
|
|
4
|
+
* Implements ConstraintSolver by delegating to WasmViewScriptEngine.tick().
|
|
5
|
+
* Handles QSnapshot construction and TickResult parsing.
|
|
6
|
+
*
|
|
7
|
+
* ## Responsibilities
|
|
8
|
+
*
|
|
9
|
+
* 1. Convert TStateMutation[] to QSnapshot JSON format
|
|
10
|
+
* 2. Inject FFI results into QSnapshot.values (via injectFfiResults)
|
|
11
|
+
* 3. Call WasmViewScriptEngine.tick() with JSON payload
|
|
12
|
+
* 4. Parse TickResult and extract bounds + pendingFfiCalls
|
|
13
|
+
*
|
|
14
|
+
* ## Data Flow
|
|
15
|
+
*
|
|
16
|
+
* ```
|
|
17
|
+
* TStateMutation[] + FfiResult[]
|
|
18
|
+
* │
|
|
19
|
+
* ▼
|
|
20
|
+
* buildQSnapshot()
|
|
21
|
+
* │
|
|
22
|
+
* ▼
|
|
23
|
+
* QSnapshot JSON ─────► WasmViewScriptEngine.tick()
|
|
24
|
+
* │
|
|
25
|
+
* ▼
|
|
26
|
+
* TickResult JSON
|
|
27
|
+
* │
|
|
28
|
+
* ▼
|
|
29
|
+
* SolverEvaluationResult
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import type { EntityId, PVectorBounds } from '../ast/types.js';
|
|
33
|
+
import type { FfiResult, PendingFfiCall } from './ffi-dispatcher.js';
|
|
34
|
+
/**
|
|
35
|
+
* Semantic T-vector state keys.
|
|
36
|
+
*/
|
|
37
|
+
type TStateKey = 'hover' | 'pressed' | 'focused' | 'scroll_x' | 'scroll_y' | 'drag_progress' | 'animation_t' | 'gesture_phase';
|
|
38
|
+
/**
|
|
39
|
+
* T-vector state mutation from Q-dimension event.
|
|
40
|
+
*/
|
|
41
|
+
export interface TStateMutation {
|
|
42
|
+
entityId: EntityId;
|
|
43
|
+
state: TStateKey;
|
|
44
|
+
value: number;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Result of constraint solver evaluation.
|
|
49
|
+
*/
|
|
50
|
+
export interface SolverEvaluationResult {
|
|
51
|
+
bounds: Map<EntityId, PVectorBounds>;
|
|
52
|
+
pendingFfiCalls: PendingFfiCall[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* ConstraintSolver interface.
|
|
56
|
+
*
|
|
57
|
+
* Note: This interface is FFI-agnostic. FFI result injection
|
|
58
|
+
* is handled by the concrete WasmSolverBridge implementation.
|
|
59
|
+
*/
|
|
60
|
+
export interface ConstraintSolver {
|
|
61
|
+
evaluate(mutations: TStateMutation[]): SolverEvaluationResult;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* WASM engine interface (subset used by bridge).
|
|
65
|
+
*/
|
|
66
|
+
export interface WasmEngine {
|
|
67
|
+
tick(inputJson: string): string;
|
|
68
|
+
}
|
|
69
|
+
export declare class WasmSolverBridge implements ConstraintSolver {
|
|
70
|
+
private engine;
|
|
71
|
+
/** Pending FFI results to inject into next QSnapshot */
|
|
72
|
+
private pendingFfiResults;
|
|
73
|
+
/** Entity name to ID mapping (for mutation conversion) */
|
|
74
|
+
private entityNameToId;
|
|
75
|
+
/** Q-variable name to entity+state mapping */
|
|
76
|
+
private qVarMapping;
|
|
77
|
+
constructor(engine: WasmEngine);
|
|
78
|
+
/**
|
|
79
|
+
* Inject FFI results for the next evaluate() call.
|
|
80
|
+
*
|
|
81
|
+
* Called from render-loop Phase 0.5. Results are consumed
|
|
82
|
+
* and cleared when evaluate() runs.
|
|
83
|
+
*
|
|
84
|
+
* @param results - FFI function results from previous frame
|
|
85
|
+
*/
|
|
86
|
+
injectFfiResults(results: FfiResult[]): void;
|
|
87
|
+
/**
|
|
88
|
+
* Register entity name to ID mapping.
|
|
89
|
+
*
|
|
90
|
+
* Called during initialization to enable mutation conversion.
|
|
91
|
+
*/
|
|
92
|
+
registerEntity(name: string, id: EntityId): void;
|
|
93
|
+
/**
|
|
94
|
+
* Register Q-variable mapping.
|
|
95
|
+
*
|
|
96
|
+
* Maps Q-variable names to entity+state for mutation routing.
|
|
97
|
+
*/
|
|
98
|
+
registerQVariable(name: string, entityId: EntityId, state: TStateKey): void;
|
|
99
|
+
/**
|
|
100
|
+
* Evaluate constraints and return solver result.
|
|
101
|
+
*
|
|
102
|
+
* Implements ConstraintSolver interface.
|
|
103
|
+
*/
|
|
104
|
+
evaluate(mutations: TStateMutation[]): SolverEvaluationResult;
|
|
105
|
+
/**
|
|
106
|
+
* Build QSnapshot from mutations and FFI results.
|
|
107
|
+
*/
|
|
108
|
+
private buildQSnapshot;
|
|
109
|
+
/**
|
|
110
|
+
* Get Q-variable name for an entity's T-state.
|
|
111
|
+
*
|
|
112
|
+
* Convention: `{entityId}_{state}` (e.g., "1_hover", "2_scroll_y")
|
|
113
|
+
*/
|
|
114
|
+
private getQVarName;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create a WASM solver bridge instance.
|
|
118
|
+
*
|
|
119
|
+
* @param engine - WasmViewScriptEngine instance
|
|
120
|
+
*/
|
|
121
|
+
export declare function createWasmSolverBridge(engine: WasmEngine): WasmSolverBridge;
|
|
122
|
+
export {};
|