@vulfram/engine 0.5.6-alpha
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/package.json +17 -0
- package/src/engine/api.ts +282 -0
- package/src/engine/bridge/dispatch.ts +115 -0
- package/src/engine/bridge/guards.ts +28 -0
- package/src/engine/bridge/protocol.ts +130 -0
- package/src/engine/ecs/index.ts +516 -0
- package/src/engine/errors.ts +10 -0
- package/src/engine/state.ts +135 -0
- package/src/engine/systems/command-intent.ts +74 -0
- package/src/engine/systems/core-command-builder.ts +265 -0
- package/src/engine/systems/diagnostics.ts +42 -0
- package/src/engine/systems/index.ts +7 -0
- package/src/engine/systems/input-mirror.ts +152 -0
- package/src/engine/systems/resource-upload.ts +143 -0
- package/src/engine/systems/response-decode.ts +28 -0
- package/src/engine/systems/utils.ts +147 -0
- package/src/engine/systems/world-lifecycle.ts +164 -0
- package/src/engine/world/entities.ts +516 -0
- package/src/index.ts +9 -0
- package/src/types/cmds/camera.ts +76 -0
- package/src/types/cmds/environment.ts +45 -0
- package/src/types/cmds/geometry.ts +144 -0
- package/src/types/cmds/gizmo.ts +18 -0
- package/src/types/cmds/index.ts +231 -0
- package/src/types/cmds/light.ts +69 -0
- package/src/types/cmds/material.ts +98 -0
- package/src/types/cmds/model.ts +59 -0
- package/src/types/cmds/shadow.ts +22 -0
- package/src/types/cmds/system.ts +15 -0
- package/src/types/cmds/texture.ts +63 -0
- package/src/types/cmds/window.ts +263 -0
- package/src/types/events/gamepad.ts +34 -0
- package/src/types/events/index.ts +19 -0
- package/src/types/events/keyboard.ts +225 -0
- package/src/types/events/pointer.ts +105 -0
- package/src/types/events/system.ts +13 -0
- package/src/types/events/window.ts +96 -0
- package/src/types/index.ts +3 -0
- package/src/types/kinds.ts +111 -0
- package/tsconfig.json +29 -0
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vulfram/engine",
|
|
3
|
+
"version": "0.5.6-alpha",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@vulfram/transport-types": "^0.2.1",
|
|
10
|
+
"gl-matrix": "^3.4.4",
|
|
11
|
+
"glob": "^13.0.0",
|
|
12
|
+
"msgpackr": "^1.11.8"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "^1.3.6"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { EngineTransportFactory } from '@vulfram/transport-types';
|
|
2
|
+
import {
|
|
3
|
+
collectCommands,
|
|
4
|
+
routeEvents,
|
|
5
|
+
routeResponses,
|
|
6
|
+
} from './bridge/dispatch';
|
|
7
|
+
import { requireInitialized } from './bridge/guards';
|
|
8
|
+
import {
|
|
9
|
+
deserializeEvents,
|
|
10
|
+
deserializeResponses,
|
|
11
|
+
serializeBatch,
|
|
12
|
+
} from './bridge/protocol';
|
|
13
|
+
import type { ComponentSchema, System, SystemContext, SystemStep } from './ecs';
|
|
14
|
+
import { EngineError } from './errors';
|
|
15
|
+
import { engineState, REQUIRED_SYSTEMS, type WorldState } from './state';
|
|
16
|
+
import * as CoreSystems from './systems';
|
|
17
|
+
import type { UploadType } from '../types/kinds';
|
|
18
|
+
|
|
19
|
+
export * from './world/entities';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the engine runtime and registers core systems.
|
|
23
|
+
* Call once before creating worlds or issuing commands.
|
|
24
|
+
*/
|
|
25
|
+
export function initEngine(config: {
|
|
26
|
+
/** Transport factory for the runtime (WASM, Bun, N-API, etc.). */
|
|
27
|
+
transport: EngineTransportFactory;
|
|
28
|
+
/** Enables verbose debug logs and extra diagnostics. */
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
}): void {
|
|
31
|
+
if (engineState.status === 'initialized') {
|
|
32
|
+
throw new EngineError('AlreadyInitialized', 'Engine already initialized.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reset Engine State for fresh start (important if re-initializing after dispose)
|
|
36
|
+
engineState.worlds.clear();
|
|
37
|
+
engineState.commandBatch = [];
|
|
38
|
+
engineState.commandTracker.clear();
|
|
39
|
+
engineState.nextEntityId = 1;
|
|
40
|
+
engineState.nextCommandId = 1;
|
|
41
|
+
engineState.registry.systems.input = [];
|
|
42
|
+
engineState.registry.systems.update = [];
|
|
43
|
+
engineState.registry.systems.preRender = [];
|
|
44
|
+
engineState.registry.systems.postRender = [];
|
|
45
|
+
|
|
46
|
+
engineState.flags.debugEnabled = config.debug ?? false;
|
|
47
|
+
|
|
48
|
+
const transport = config.transport();
|
|
49
|
+
const result = transport.vulframInit();
|
|
50
|
+
if (result !== 0) {
|
|
51
|
+
throw new EngineError(
|
|
52
|
+
'InitFailed',
|
|
53
|
+
`vulframInit failed with code ${result}.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
engineState.transport = transport;
|
|
58
|
+
engineState.status = 'initialized';
|
|
59
|
+
|
|
60
|
+
// Register Core Systems
|
|
61
|
+
registerSystem('input', CoreSystems.InputMirrorSystem);
|
|
62
|
+
registerSystem('update', CoreSystems.CommandIntentSystem);
|
|
63
|
+
registerSystem('update', CoreSystems.WorldLifecycleSystem);
|
|
64
|
+
registerSystem('update', CoreSystems.ResourceUploadSystem);
|
|
65
|
+
registerSystem('preRender', CoreSystems.CoreCommandBuilderSystem);
|
|
66
|
+
registerSystem('postRender', CoreSystems.ResponseDecodeSystem);
|
|
67
|
+
registerSystem('postRender', CoreSystems.DiagnosticsSystem);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Disposes the engine and releases the active transport.
|
|
72
|
+
* After dispose, you must call initEngine again to use the engine.
|
|
73
|
+
*/
|
|
74
|
+
export function disposeEngine(): void {
|
|
75
|
+
requireInitialized();
|
|
76
|
+
const transport = engineState.transport;
|
|
77
|
+
if (transport) {
|
|
78
|
+
transport.vulframDispose();
|
|
79
|
+
}
|
|
80
|
+
engineState.transport = null;
|
|
81
|
+
engineState.worlds.clear();
|
|
82
|
+
engineState.commandBatch = [];
|
|
83
|
+
engineState.commandTracker.clear();
|
|
84
|
+
engineState.status = 'disposed';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Registers a custom component schema for editor tooling or extensions.
|
|
89
|
+
*/
|
|
90
|
+
export function registerComponent(name: string, schema: ComponentSchema): void {
|
|
91
|
+
requireInitialized();
|
|
92
|
+
engineState.registry.components.set(name, schema);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Registers a system to run at a specific pipeline step.
|
|
97
|
+
*/
|
|
98
|
+
export function registerSystem(step: SystemStep, system: System): void {
|
|
99
|
+
requireInitialized();
|
|
100
|
+
engineState.registry.systems[step].push(system);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function uploadTypeToId(type: UploadType): number {
|
|
104
|
+
switch (type) {
|
|
105
|
+
case 'raw':
|
|
106
|
+
return 0;
|
|
107
|
+
case 'shader-source':
|
|
108
|
+
return 1;
|
|
109
|
+
case 'geometry-data':
|
|
110
|
+
return 2;
|
|
111
|
+
case 'vertex-data':
|
|
112
|
+
return 3;
|
|
113
|
+
case 'index-data':
|
|
114
|
+
return 4;
|
|
115
|
+
case 'image-data':
|
|
116
|
+
return 5;
|
|
117
|
+
case 'binary-asset':
|
|
118
|
+
return 6;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Uploads a raw buffer to the engine's GPU memory or internal storage.
|
|
124
|
+
* @param bufferId Unique ID for this buffer.
|
|
125
|
+
* @param type Type of data in the buffer (ImageData, VertexData, etc).
|
|
126
|
+
* @param data The raw binary data.
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* Uploads a raw buffer to the core for later use (textures, geometry, etc.).
|
|
130
|
+
*/
|
|
131
|
+
export function uploadBuffer(
|
|
132
|
+
bufferId: number,
|
|
133
|
+
type: UploadType,
|
|
134
|
+
data: Uint8Array | Buffer,
|
|
135
|
+
): void {
|
|
136
|
+
requireInitialized();
|
|
137
|
+
const transport = engineState.transport!;
|
|
138
|
+
const bufferData = data instanceof Uint8Array ? Buffer.from(data) : data;
|
|
139
|
+
const result = transport.vulframUploadBuffer(
|
|
140
|
+
bufferId,
|
|
141
|
+
uploadTypeToId(type),
|
|
142
|
+
bufferData,
|
|
143
|
+
);
|
|
144
|
+
if (result !== 0) {
|
|
145
|
+
throw new EngineError(
|
|
146
|
+
'UploadFailed',
|
|
147
|
+
`vulframUploadBuffer failed for ID ${bufferId} with code ${result}.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a world bound to a window ID. Returns the world ID.
|
|
154
|
+
*/
|
|
155
|
+
export function createWorld(windowId: number): number {
|
|
156
|
+
requireInitialized();
|
|
157
|
+
if (engineState.worlds.has(windowId)) {
|
|
158
|
+
throw new EngineError('WorldExists', `World ${windowId} already exists.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const world: WorldState = {
|
|
162
|
+
windowId,
|
|
163
|
+
entities: new Set(),
|
|
164
|
+
components: new Map(),
|
|
165
|
+
nextCoreId: 100,
|
|
166
|
+
systems: [...REQUIRED_SYSTEMS],
|
|
167
|
+
pendingIntents: [],
|
|
168
|
+
internalEvents: [],
|
|
169
|
+
pendingCommands: [],
|
|
170
|
+
inboundEvents: [],
|
|
171
|
+
inboundResponses: [],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
engineState.worlds.set(windowId, world);
|
|
175
|
+
return windowId;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Main engine tick.
|
|
180
|
+
* Orchestrates event routing, world updates, command collection, and core synchronization.
|
|
181
|
+
*/
|
|
182
|
+
/**
|
|
183
|
+
* Advances the engine by one frame.
|
|
184
|
+
* Call once per frame with monotonic time and delta in milliseconds.
|
|
185
|
+
*/
|
|
186
|
+
export function tick(timeMs: number, deltaMs: number): void {
|
|
187
|
+
requireInitialized();
|
|
188
|
+
const transport = engineState.transport!;
|
|
189
|
+
|
|
190
|
+
// 1. Engine Phase: Receive from Core (Input Pipeline)
|
|
191
|
+
// We process events and responses received since last frame
|
|
192
|
+
const eventsResult = transport.vulframReceiveEvents();
|
|
193
|
+
if (eventsResult.result === 0 && eventsResult.buffer.length > 0) {
|
|
194
|
+
const events = deserializeEvents(eventsResult.buffer);
|
|
195
|
+
routeEvents(events);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const responsesResult = transport.vulframReceiveQueue();
|
|
199
|
+
if (responsesResult.result === 0 && responsesResult.buffer.length > 0) {
|
|
200
|
+
const responses = deserializeResponses(responsesResult.buffer);
|
|
201
|
+
routeResponses(responses);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. Engine Phase: Clock & Context Preparation
|
|
205
|
+
const cappedDelta = Math.min(deltaMs, 100);
|
|
206
|
+
engineState.clock.lastTime = timeMs;
|
|
207
|
+
engineState.clock.lastDelta = cappedDelta;
|
|
208
|
+
engineState.clock.frameCount++;
|
|
209
|
+
|
|
210
|
+
const context: SystemContext = {
|
|
211
|
+
dt: cappedDelta / 1000,
|
|
212
|
+
time: timeMs / 1000,
|
|
213
|
+
worldId: 0,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 3. World Phase: System Execution
|
|
217
|
+
engineState.flags.isExecutingSystems = true;
|
|
218
|
+
try {
|
|
219
|
+
for (const [worldId, world] of engineState.worlds) {
|
|
220
|
+
context.worldId = worldId;
|
|
221
|
+
executeSystemStep(world, context, 'input');
|
|
222
|
+
executeSystemStep(world, context, 'update');
|
|
223
|
+
executeSystemStep(world, context, 'preRender');
|
|
224
|
+
executeSystemStep(world, context, 'postRender');
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
engineState.flags.isExecutingSystems = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 4. Engine Phase: Collect & Send (Output Pipeline)
|
|
231
|
+
collectCommands();
|
|
232
|
+
|
|
233
|
+
if (engineState.commandBatch.length > 0) {
|
|
234
|
+
const batchBuffer = serializeBatch(engineState.commandBatch);
|
|
235
|
+
const result = transport.vulframSendQueue(batchBuffer);
|
|
236
|
+
if (result !== 0) {
|
|
237
|
+
console.error(
|
|
238
|
+
`[Vulfram] vulframSendQueue failed with result ${result}. This usually indicates a MessagePack serialization mismatch between Host and Core.`,
|
|
239
|
+
);
|
|
240
|
+
for (const cmd of engineState.commandBatch) {
|
|
241
|
+
engineState.commandTracker.delete(cmd.id);
|
|
242
|
+
}
|
|
243
|
+
if (engineState.flags.debugEnabled) {
|
|
244
|
+
console.group('[Vulfram Debug] Failed Batch');
|
|
245
|
+
console.debug('Result:', result);
|
|
246
|
+
console.debug('Batch Size:', batchBuffer.length, 'bytes');
|
|
247
|
+
console.debug(
|
|
248
|
+
'Command Types:',
|
|
249
|
+
engineState.commandBatch.map((c) => c.type),
|
|
250
|
+
);
|
|
251
|
+
console.groupEnd();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
engineState.commandBatch = [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 5. Core Phase: Execute Tick
|
|
258
|
+
transport.vulframTick(timeMs, deltaMs);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Executes a specific group of systems with error handling.
|
|
263
|
+
*/
|
|
264
|
+
function executeSystemStep(
|
|
265
|
+
world: WorldState,
|
|
266
|
+
context: SystemContext,
|
|
267
|
+
step: SystemStep,
|
|
268
|
+
): void {
|
|
269
|
+
const systems = engineState.registry.systems[step];
|
|
270
|
+
for (const system of systems) {
|
|
271
|
+
try {
|
|
272
|
+
system(world, context);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(
|
|
275
|
+
`[SystemError] World ${context.worldId} | Step: ${step} | System: ${
|
|
276
|
+
system.name || 'anonymous'
|
|
277
|
+
}\n`,
|
|
278
|
+
err,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CommandResponseEnvelope, EngineCmd } from '../../types/cmds';
|
|
2
|
+
import type { EngineEvent } from '../../types/events';
|
|
3
|
+
import { engineState } from '../state';
|
|
4
|
+
import { getWorldOrThrow, requireInitialized } from './guards';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Limits for Backpressure management.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_BATCH_COMMANDS = 2048;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Enqueues a command to be sent to the core in the next tick.
|
|
13
|
+
* Ensures the command is routed through the specific world.
|
|
14
|
+
*/
|
|
15
|
+
export function enqueueCommand<T extends EngineCmd['type']>(
|
|
16
|
+
worldId: number,
|
|
17
|
+
type: T,
|
|
18
|
+
content: Extract<EngineCmd, { type: T }>['content'],
|
|
19
|
+
): number {
|
|
20
|
+
requireInitialized();
|
|
21
|
+
const world = getWorldOrThrow(worldId);
|
|
22
|
+
const id = engineState.nextCommandId++;
|
|
23
|
+
|
|
24
|
+
// Contract: windowId must match worldId (windowId) if present in content
|
|
25
|
+
const typedContent = content as Record<string, unknown>;
|
|
26
|
+
if ('windowId' in typedContent) {
|
|
27
|
+
typedContent.windowId = world.windowId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
world.pendingCommands.push({ id, type, content });
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Collects pending commands from all worlds into the global engine batch.
|
|
36
|
+
* Implements BatchAggregatorSystem and basic BackpressureSystem logic.
|
|
37
|
+
*/
|
|
38
|
+
export function collectCommands(): void {
|
|
39
|
+
let collectedCount = 0;
|
|
40
|
+
|
|
41
|
+
for (const [worldId, world] of engineState.worlds) {
|
|
42
|
+
if (collectedCount >= MAX_BATCH_COMMANDS) {
|
|
43
|
+
// Backpressure: If we reached the limit, stop collecting for this frame.
|
|
44
|
+
// Remaining commands will wait for the next frame.
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (world.pendingCommands.length > 0) {
|
|
49
|
+
// Determine how many we can take from this world
|
|
50
|
+
const remainingSpace = MAX_BATCH_COMMANDS - collectedCount;
|
|
51
|
+
const amountToTake = Math.min(
|
|
52
|
+
world.pendingCommands.length,
|
|
53
|
+
remainingSpace,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const cmdsToPush = world.pendingCommands.splice(0, amountToTake);
|
|
57
|
+
for (const cmd of cmdsToPush) {
|
|
58
|
+
engineState.commandBatch.push(cmd);
|
|
59
|
+
engineState.commandTracker.set(cmd.id, worldId);
|
|
60
|
+
}
|
|
61
|
+
collectedCount += amountToTake;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Routes core events to their respective worlds based on windowId.
|
|
68
|
+
*/
|
|
69
|
+
export function routeEvents(events: EngineEvent[]): void {
|
|
70
|
+
for (const event of events) {
|
|
71
|
+
// Determine windowId from event content
|
|
72
|
+
// Events have structure: { type, content: { event, data: { windowId, ... } } }
|
|
73
|
+
let windowId: number | undefined;
|
|
74
|
+
|
|
75
|
+
if ('data' in event.content && event.content.data) {
|
|
76
|
+
const eventData = event.content.data as unknown as Record<
|
|
77
|
+
string,
|
|
78
|
+
unknown
|
|
79
|
+
>;
|
|
80
|
+
windowId =
|
|
81
|
+
typeof eventData.windowId === 'number' ? eventData.windowId : undefined;
|
|
82
|
+
} else if ('windowId' in event.content) {
|
|
83
|
+
const eventContent = event.content as Record<string, unknown>;
|
|
84
|
+
windowId =
|
|
85
|
+
typeof eventContent.windowId === 'number'
|
|
86
|
+
? eventContent.windowId
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (windowId !== undefined) {
|
|
91
|
+
const world = engineState.worlds.get(windowId);
|
|
92
|
+
if (world) {
|
|
93
|
+
world.inboundEvents.push(event);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Global events (if any) could be handled here
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Routes core responses back to the world that initiated the command.
|
|
103
|
+
*/
|
|
104
|
+
export function routeResponses(responses: CommandResponseEnvelope[]): void {
|
|
105
|
+
for (const res of responses) {
|
|
106
|
+
const worldId = engineState.commandTracker.get(res.id);
|
|
107
|
+
if (worldId !== undefined) {
|
|
108
|
+
const world = engineState.worlds.get(worldId);
|
|
109
|
+
if (world) {
|
|
110
|
+
world.inboundResponses.push(res);
|
|
111
|
+
}
|
|
112
|
+
engineState.commandTracker.delete(res.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EngineError } from '../errors';
|
|
2
|
+
import { engineState, type WorldState } from '../state';
|
|
3
|
+
|
|
4
|
+
export function requireInitialized(): void {
|
|
5
|
+
if (engineState.status === 'disposed') {
|
|
6
|
+
throw new EngineError('Disposed', 'Engine has been disposed.');
|
|
7
|
+
}
|
|
8
|
+
if (engineState.status !== 'initialized') {
|
|
9
|
+
throw new EngineError('NotInitialized', 'Engine is not initialized.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getWorldOrThrow(worldId: number): WorldState {
|
|
14
|
+
const world = engineState.worlds.get(worldId);
|
|
15
|
+
if (!world) {
|
|
16
|
+
throw new EngineError('WorldNotFound', `World ${worldId} not found.`);
|
|
17
|
+
}
|
|
18
|
+
return world;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureEntity(world: WorldState, entityId: number): void {
|
|
22
|
+
if (!world.entities.has(entityId)) {
|
|
23
|
+
throw new EngineError(
|
|
24
|
+
'EntityNotFound',
|
|
25
|
+
`Entity ${entityId} not found in World ${world.windowId}.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Packr, Unpackr } from 'msgpackr';
|
|
2
|
+
import type {
|
|
3
|
+
CommandResponseEnvelope,
|
|
4
|
+
EngineCmdEnvelope,
|
|
5
|
+
} from '../../types/cmds';
|
|
6
|
+
import type { EngineEvent } from '../../types/events';
|
|
7
|
+
import { engineState } from '../state';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Vulfram Core/ABI Invariants:
|
|
11
|
+
*
|
|
12
|
+
* 1. Threading:
|
|
13
|
+
* - The Core expects to be ticked and communicated with from a single host thread (Main Thread).
|
|
14
|
+
* - FFI/N-API calls are synchronous but the logic they trigger in the core (GPU work) is asynchronous.
|
|
15
|
+
*
|
|
16
|
+
* 2. Temporal Stability (Tick):
|
|
17
|
+
* - `vulframTick` must be called once per frame.
|
|
18
|
+
* - Incorrect or skipped ticks can lead to uneven animations, input lag, or resource starvation.
|
|
19
|
+
*
|
|
20
|
+
* 3. Command Batching:
|
|
21
|
+
* - All High-Level Logic commands are batched into a single MessagePack-encoded buffer.
|
|
22
|
+
* - The batch is sent once per frame via `vulframSendQueue`.
|
|
23
|
+
* - Maximum batch size is defined by the Core (currently 64KB for safety, but adjustable).
|
|
24
|
+
*
|
|
25
|
+
* 4. Events & Responses:
|
|
26
|
+
* - Responses: Every command sent in a batch will eventually produce a response in `vulframReceiveQueue`.
|
|
27
|
+
* Responses are matched back to commands using the `id` field in the envelope.
|
|
28
|
+
* - Events: System-level events (Keyboard, Mouse, Window resize) are polled via `vulframReceiveEvents`.
|
|
29
|
+
*
|
|
30
|
+
* 5. Resource Uploads:
|
|
31
|
+
* - Larger data (Textures, Geometry data) are sent via `vulframUploadBuffer` to avoid bloating the command batch.
|
|
32
|
+
* - These are out-of-band and acknowledged via specific events or response IDs.
|
|
33
|
+
*
|
|
34
|
+
* 6. ID Management:
|
|
35
|
+
* - The Host (this ECS) is responsible for generating and managing Logical IDs for all entities, components, and resources.
|
|
36
|
+
* - The Core uses these IDs to track objects. Reusing an ID without disposing of the previous object will cause errors or undefined behavior.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Batch Scheme:
|
|
41
|
+
* A batch is simply an array of EngineCmdEnvelope objects.
|
|
42
|
+
*/
|
|
43
|
+
export type CoreCommandBatch = EngineCmdEnvelope[];
|
|
44
|
+
|
|
45
|
+
const packr = new Packr({ useRecords: false });
|
|
46
|
+
const unpackr = new Unpackr({ useRecords: false });
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Minimal Command Payload Requirement:
|
|
50
|
+
* Commands that interact with the scene or windows MUST include a `windowId`.
|
|
51
|
+
* This allows the Core to route the command to the correct world/surface.
|
|
52
|
+
*/
|
|
53
|
+
export interface BaseCommandArgs {
|
|
54
|
+
windowId: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Serializes a batch of commands into a Buffer ready for vulframSendQueue.
|
|
59
|
+
*/
|
|
60
|
+
export function serializeBatch(batch: CoreCommandBatch): Buffer {
|
|
61
|
+
if (engineState.flags.debugEnabled) {
|
|
62
|
+
const types = batch.map((cmd) => cmd.type);
|
|
63
|
+
const hasModel =
|
|
64
|
+
types.includes('cmd-model-create') || types.includes('cmd-model-update');
|
|
65
|
+
const hasResource = types.some(
|
|
66
|
+
(type) =>
|
|
67
|
+
type === 'cmd-primitive-geometry-create' ||
|
|
68
|
+
type === 'cmd-geometry-create' ||
|
|
69
|
+
type === 'cmd-material-create' ||
|
|
70
|
+
type === 'cmd-texture-create-from-buffer' ||
|
|
71
|
+
type === 'cmd-texture-create-solid-color',
|
|
72
|
+
);
|
|
73
|
+
if (hasModel || hasResource) {
|
|
74
|
+
console.debug('[Debug] BatchTypes', types);
|
|
75
|
+
const modelCreate = batch.find((cmd) => cmd.type === 'cmd-model-create');
|
|
76
|
+
if (modelCreate) {
|
|
77
|
+
console.debug(
|
|
78
|
+
'[Debug] ModelCreatePayload',
|
|
79
|
+
JSON.stringify(modelCreate.content),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const primitiveGeo = batch.find(
|
|
83
|
+
(cmd) => cmd.type === 'cmd-primitive-geometry-create',
|
|
84
|
+
);
|
|
85
|
+
if (primitiveGeo) {
|
|
86
|
+
console.debug(
|
|
87
|
+
'[Debug] GeometryPayload',
|
|
88
|
+
JSON.stringify(primitiveGeo.content),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const materialCreate = batch.find(
|
|
92
|
+
(cmd) => cmd.type === 'cmd-material-create',
|
|
93
|
+
);
|
|
94
|
+
if (materialCreate) {
|
|
95
|
+
console.debug(
|
|
96
|
+
'[Debug] MaterialPayload',
|
|
97
|
+
JSON.stringify(materialCreate.content),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const textureCreate = batch.find(
|
|
101
|
+
(cmd) => cmd.type === 'cmd-texture-create-from-buffer',
|
|
102
|
+
);
|
|
103
|
+
if (textureCreate) {
|
|
104
|
+
console.debug(
|
|
105
|
+
'[Debug] TexturePayload',
|
|
106
|
+
JSON.stringify(textureCreate.content),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return packr.pack(batch);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deserializes responses from vulframReceiveQueue.
|
|
116
|
+
*/
|
|
117
|
+
export function deserializeResponses(
|
|
118
|
+
buffer: Buffer,
|
|
119
|
+
): CommandResponseEnvelope[] {
|
|
120
|
+
if (buffer.length === 0) return [];
|
|
121
|
+
return unpackr.unpack(buffer);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deserializes events from vulframReceiveEvents.
|
|
126
|
+
*/
|
|
127
|
+
export function deserializeEvents(buffer: Buffer): EngineEvent[] {
|
|
128
|
+
if (buffer.length === 0) return [];
|
|
129
|
+
return unpackr.unpack(buffer);
|
|
130
|
+
}
|