@forgeax/engine-runtime 0.1.26 → 0.1.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-runtime",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,37 +26,39 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-animation": "0.1.26",
30
- "@forgeax/engine-assets-runtime": "0.1.26",
31
- "@forgeax/engine-audio-webaudio": "0.1.26",
32
- "@forgeax/engine-codec": "0.1.26",
33
- "@forgeax/engine-debug-draw": "0.1.26",
34
- "@forgeax/engine-ecs": "0.1.26",
35
- "@forgeax/engine-font": "0.1.26",
36
- "@forgeax/engine-geometry": "0.1.26",
37
- "@forgeax/engine-graphics-extras": "0.1.26",
38
- "@forgeax/engine-image": "0.1.26",
39
- "@forgeax/engine-input": "0.1.26",
40
- "@forgeax/engine-math": "0.1.26",
41
- "@forgeax/engine-pack": "0.1.26",
42
- "@forgeax/engine-profiler": "0.1.26",
43
- "@forgeax/engine-render": "0.1.26",
44
- "@forgeax/engine-render-graph": "0.1.26",
45
- "@forgeax/engine-rhi": "0.1.26",
46
- "@forgeax/engine-rhi-null": "0.1.26",
47
- "@forgeax/engine-rhi-webgpu": "0.1.26",
48
- "@forgeax/engine-rhi-wgpu": "0.1.26",
49
- "@forgeax/engine-scene": "0.1.26",
50
- "@forgeax/engine-shader": "0.1.26",
51
- "@forgeax/engine-skinning": "0.1.26",
52
- "@forgeax/engine-types": "0.1.26",
53
- "@forgeax/engine-vfx": "0.1.26"
29
+ "@forgeax/engine-animation": "0.1.28",
30
+ "@forgeax/engine-assets-runtime": "0.1.28",
31
+ "@forgeax/engine-audio-webaudio": "0.1.28",
32
+ "@forgeax/engine-codec": "0.1.28",
33
+ "@forgeax/engine-debug-draw": "0.1.28",
34
+ "@forgeax/engine-ecs": "0.1.28",
35
+ "@forgeax/engine-font": "0.1.28",
36
+ "@forgeax/engine-geometry": "0.1.28",
37
+ "@forgeax/engine-graphics-extras": "0.1.28",
38
+ "@forgeax/engine-image": "0.1.28",
39
+ "@forgeax/engine-input": "0.1.28",
40
+ "@forgeax/engine-math": "0.1.28",
41
+ "@forgeax/engine-pack": "0.1.28",
42
+ "@forgeax/engine-profiler": "0.1.28",
43
+ "@forgeax/engine-render": "0.1.28",
44
+ "@forgeax/engine-render-graph": "0.1.28",
45
+ "@forgeax/engine-rhi": "0.1.28",
46
+ "@forgeax/engine-rhi-null": "0.1.28",
47
+ "@forgeax/engine-rhi-webgpu": "0.1.28",
48
+ "@forgeax/engine-rhi-wgpu": "0.1.28",
49
+ "@forgeax/engine-scene": "0.1.28",
50
+ "@forgeax/engine-shader": "0.1.28",
51
+ "@forgeax/engine-skinning": "0.1.28",
52
+ "@forgeax/engine-types": "0.1.28",
53
+ "@forgeax/engine-vfx": "0.1.28"
54
54
  },
55
55
  "devDependencies": {
56
- "@forgeax/engine-import": "0.1.26",
57
- "@forgeax/engine-naga": "0.1.26",
58
- "@forgeax/engine-state": "0.1.26",
59
- "@forgeax/engine-vite-plugin-shader": "0.1.26",
56
+ "@forgeax/engine-import": "0.1.28",
57
+ "@forgeax/engine-naga": "0.1.28",
58
+ "@forgeax/engine-physics": "0.1.28",
59
+ "@forgeax/engine-physics-rapier3d": "0.1.28",
60
+ "@forgeax/engine-state": "0.1.28",
61
+ "@forgeax/engine-vite-plugin-shader": "0.1.28",
60
62
  "@types/node": "^20.14.0"
61
63
  },
62
64
  "forgeax": {
@@ -105,6 +105,7 @@ import type {
105
105
  } from '@forgeax/engine-types';
106
106
  import { toShared } from '@forgeax/engine-types';
107
107
  import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
108
+ import { componentId } from '../../../ecs/src/component';
108
109
  import { worldInternal } from '../../../ecs/src/world-internal';
109
110
  import {
110
111
  SPRITE_PLAYBACK_MODE_CLAMP,
@@ -489,7 +490,10 @@ type __MergedKeep =
489
490
  if (archetype === undefined || record === undefined) throw new Error('child row missing');
490
491
  const tableRow = archetype.rows[record.archetypeRow];
491
492
  if (tableRow === undefined) throw new Error('child table row missing');
492
- world[worldInternal].writeRow(archetype, ChildOf, tableRow, { parent: ghost });
493
+ const table = world[worldInternal].getGraph().tables[archetype.tableId];
494
+ const parentColumn = table?.storage.get(componentId(ChildOf))?.fields.get('parent');
495
+ if (parentColumn === undefined) throw new Error('ChildOf.parent column missing');
496
+ parentColumn.view[tableRow] = ghost as number;
493
497
 
494
498
  const r = propagateTransforms(world);
495
499
  expect(r.ok).toBe(false);
@@ -25,6 +25,7 @@ import { World } from '@forgeax/engine-ecs';
25
25
  import { readRenderArrayView } from '@forgeax/engine-ecs/projection';
26
26
  import { ChildOf, GlobalTransform, propagateTransforms, Transform } from '@forgeax/engine-scene';
27
27
  import { describe, expect, it } from 'vitest';
28
+ import { componentId } from '../../../ecs/src/component';
28
29
  import { worldInternal } from '../../../ecs/src/world-internal';
29
30
  import { TileLayer, Tilemap } from '../../../render/src/components';
30
31
 
@@ -258,7 +259,10 @@ describe('propagate-transforms.dawn - root-down DFS + stale ChildOf fail-fast (A
258
259
  if (archetype === undefined || record === undefined) throw new Error('orphan row missing');
259
260
  const tableRow = archetype.rows[record.archetypeRow];
260
261
  if (tableRow === undefined) throw new Error('orphan table row missing');
261
- world[worldInternal].writeRow(archetype, ChildOf, tableRow, { parent: ghost });
262
+ const table = world[worldInternal].getGraph().tables[archetype.tableId];
263
+ const parentColumn = table?.storage.get(componentId(ChildOf))?.fields.get('parent');
264
+ if (parentColumn === undefined) throw new Error('ChildOf.parent column missing');
265
+ parentColumn.view[tableRow] = ghost as number;
262
266
 
263
267
  const r = propagateTransforms(world);
264
268
  expect(r.ok).toBe(false);
@@ -13,6 +13,7 @@ import { AssetGuid } from '@forgeax/engine-pack/guid';
13
13
  import {
14
14
  Camera,
15
15
  DirectionalLight,
16
+ Materials,
16
17
  MeshFilter,
17
18
  MeshRenderer,
18
19
  PointLight,
@@ -284,6 +285,52 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
284
285
  );
285
286
  });
286
287
 
288
+ it('keeps custom Standard Surface fields in the extracted material ABI', () => {
289
+ const world = new World();
290
+ spawnCamera(world);
291
+ const assets = new AssetRegistry(makeMockShaderRegistry());
292
+ const mesh = registerMesh(world);
293
+ const material = world.allocSharedRef<'MaterialAsset', MaterialAsset>(
294
+ 'MaterialAsset',
295
+ Materials.standard({
296
+ surfaceModule: 'game_3d::rusted_iron_surface',
297
+ parameters: [
298
+ { name: 'ironColor', type: 'color' },
299
+ { name: 'rustDark', type: 'color' },
300
+ { name: 'rustBright', type: 'color' },
301
+ { name: 'noiseScale', type: 'f32' },
302
+ ],
303
+ values: {
304
+ ironColor: [0.4, 0.45, 0.47, 1],
305
+ rustDark: [0.42, 0.085, 0.018, 1],
306
+ rustBright: [0.95, 0.34, 0.055, 1],
307
+ noiseScale: 1.85,
308
+ },
309
+ }),
310
+ );
311
+ world
312
+ .spawn(
313
+ { component: Transform, data: { ...identity(), pos: [0, 0, -2] } },
314
+ { component: MeshFilter, data: { assetHandle: mesh } },
315
+ { component: MeshRenderer, data: { materials: [material] } },
316
+ )
317
+ .unwrap();
318
+
319
+ const frame = extractFrame(world, prepareExtractContext(world, { assets }));
320
+ const snapshot = frame.renderables[0]?.material;
321
+ const schemaNames = snapshot?.materialParamSchema?.map((entry) => entry.name) ?? [];
322
+
323
+ expect(schemaNames).toEqual(
324
+ expect.arrayContaining(['ironColor', 'rustDark', 'rustBright', 'noiseScale']),
325
+ );
326
+ expect(snapshot?.paramSnapshot).toMatchObject({
327
+ ironColor: [0.4, 0.45, 0.47].map(srgbChannelToLinear).concat(1),
328
+ rustDark: [0.42, 0.085, 0.018].map(srgbChannelToLinear).concat(1),
329
+ rustBright: [0.95, 0.34, 0.055].map(srgbChannelToLinear).concat(1),
330
+ noiseScale: 1.85,
331
+ });
332
+ });
333
+
287
334
  it('mesh-walk: RenderableSnapshot.transform.world is the resolved world mat4 (parent x child), zero per-snapshot compose', () => {
288
335
  const world = new World();
289
336
  world
@@ -0,0 +1,326 @@
1
+ import { createWorldContext, FixedTime, World } from '@forgeax/engine-ecs';
2
+ import { createBoxGeometry, meshFromInterleaved } from '@forgeax/engine-geometry';
3
+ import { vec3 } from '@forgeax/engine-math';
4
+ import { RigidBody, RigidBodyTypeValue } from '@forgeax/engine-physics';
5
+ import {
6
+ createRapier3DPhysicsWorld,
7
+ loadRapier3D,
8
+ registerPhysicsSystems,
9
+ } from '@forgeax/engine-physics-rapier3d';
10
+ import {
11
+ ANTIALIAS_TAA,
12
+ Camera,
13
+ DirectionalLight,
14
+ type DynamicGeometryCandidate,
15
+ Materials,
16
+ MeshFilter,
17
+ MeshRenderer,
18
+ } from '@forgeax/engine-render';
19
+ import { ChildOf, propagateTransforms, scenePlugin, Transform } from '@forgeax/engine-scene';
20
+ import { err, type MeshAsset, ok } from '@forgeax/engine-types';
21
+ import { expect, it } from 'vitest';
22
+ import { page } from 'vitest/browser';
23
+ import { createRenderer } from '../createRenderer';
24
+
25
+ // Deterministic external geometry output: four boxes form an aperture.
26
+ // The Engine consumes an ordinary MeshAsset, without a voxel renderer.
27
+ function aperture(): MeshAsset {
28
+ const vertices: number[] = [];
29
+ const indices: number[] = [];
30
+ for (const [x, y, w, h] of [
31
+ [-0.8, 0, 0.4, 2],
32
+ [0.8, 0, 0.4, 2],
33
+ [0, -0.8, 1.2, 0.4],
34
+ [0, 0.8, 1.2, 0.4],
35
+ ] as const) {
36
+ const box = createBoxGeometry(w, h, 0.5).unwrap();
37
+ const offset = vertices.length / 8;
38
+ const { position, normal, uv } = box.attributes;
39
+ if (position === undefined || normal === undefined || uv === undefined)
40
+ throw new Error('box attribute closure');
41
+ for (let i = 0; i < position.length / 3; i++) {
42
+ vertices.push(
43
+ (position[i * 3] ?? 0) + x,
44
+ (position[i * 3 + 1] ?? 0) + y,
45
+ position[i * 3 + 2] ?? 0,
46
+ normal[i * 3] ?? 0,
47
+ normal[i * 3 + 1] ?? 0,
48
+ normal[i * 3 + 2] ?? 0,
49
+ uv[i * 2] ?? 0,
50
+ uv[i * 2 + 1] ?? 0,
51
+ );
52
+ }
53
+ for (const index of box.indices) indices.push(index + offset);
54
+ }
55
+ return meshFromInterleaved(new Float32Array(vertices), new Uint16Array(indices)).unwrap();
56
+ }
57
+
58
+ it('atomically publishes physics and PBR apertures with shadow, parent motion and fresh TAA history', async () => {
59
+ const canvas = document.createElement('canvas');
60
+ canvas.width = 384;
61
+ canvas.height = 384;
62
+ canvas.style.width = '384px';
63
+ canvas.style.height = '384px';
64
+ document.body.appendChild(canvas);
65
+ const renderer = (
66
+ await createRenderer(canvas, {}, { shaderManifestUrl: '/shaders/manifest.json' })
67
+ ).unwrap();
68
+ const errors: string[] = [];
69
+ const unsubscribe = renderer.subscribe((event) => {
70
+ if (event.kind === 'error') errors.push(event.error.code);
71
+ });
72
+ const world = new World();
73
+ const scene = await createWorldContext(world, [scenePlugin()]);
74
+ const rapier = await loadRapier3D();
75
+ if ('code' in rapier) throw rapier;
76
+ const physics = createRapier3DPhysicsWorld(rapier);
77
+ world.insertResource('PhysicsWorld', physics);
78
+ const releasePhysics = registerPhysicsSystems(world);
79
+ try {
80
+ const material = world.allocSharedRef(
81
+ 'MaterialAsset',
82
+ Materials.standard({ baseColor: [0.7, 0.24, 0.06, 1], metallic: 0.15, roughness: 0.45 }),
83
+ );
84
+ const gray = world.allocSharedRef(
85
+ 'MaterialAsset',
86
+ Materials.standard({ baseColor: [0.55, 0.6, 0.65, 1], metallic: 0, roughness: 0.8 }),
87
+ );
88
+ const closedMesh = createBoxGeometry(2, 2, 0.5).unwrap();
89
+ const openMesh = aperture();
90
+ const closedHandle = world.allocSharedRef('MeshAsset', closedMesh);
91
+ const openHandle = world.allocSharedRef('MeshAsset', openMesh);
92
+ const parent = world
93
+ .spawn(
94
+ { component: Transform, data: {} },
95
+ { component: RigidBody, data: { type: RigidBodyTypeValue.static } },
96
+ )
97
+ .unwrap();
98
+ const entity = world
99
+ .spawn(
100
+ { component: Transform, data: {} },
101
+ { component: ChildOf, data: { parent } },
102
+ { component: MeshFilter, data: { assetHandle: closedHandle } },
103
+ { component: MeshRenderer, data: { materials: [material] } },
104
+ )
105
+ .unwrap();
106
+ const floor = world.allocSharedRef('MeshAsset', createBoxGeometry(7, 0.2, 7).unwrap());
107
+ world
108
+ .spawn(
109
+ { component: Transform, data: { pos: [0, -1.25, 0] } },
110
+ { component: MeshFilter, data: { assetHandle: floor } },
111
+ { component: MeshRenderer, data: { materials: [gray] } },
112
+ )
113
+ .unwrap();
114
+ const light = world
115
+ .spawn({
116
+ component: DirectionalLight,
117
+ data: { direction: [-0.5, -0.9, -0.7], intensity: 3, castShadow: true },
118
+ })
119
+ .unwrap();
120
+ const camera = world
121
+ .spawn(
122
+ { component: Transform, data: { pos: [0, 1, 7] } },
123
+ { component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 40 } },
124
+ )
125
+ .unwrap();
126
+ const lease = renderer.attach(world).unwrap();
127
+ const physicsInput = (revision: number, opened: boolean) => {
128
+ const cells: [number, number, number][] = [];
129
+ for (let x = -2; x <= 2; x++)
130
+ for (let y = -2; y <= 2; y++) {
131
+ if (!opened || Math.abs(x) === 2 || Math.abs(y) === 2) cells.push([x, y, 0]);
132
+ }
133
+ return {
134
+ entity: parent,
135
+ revision,
136
+ sourceKey: 'wave1:slab',
137
+ worldIdentity: world,
138
+ bodyType: 'static' as const,
139
+ shapes: [
140
+ {
141
+ id: 'slab',
142
+ revision,
143
+ cells,
144
+ voxelSize: [0.4, 0.4, 0.5] as const,
145
+ origin: [-0.2, -0.2, -0.25] as const,
146
+ },
147
+ ],
148
+ };
149
+ };
150
+ const centerHit = () => physics.raycast(vec3.create(0, 0, 4), vec3.create(0, 0, -1), 8);
151
+ world.update(1 / 60).unwrap();
152
+ physics
153
+ .admitDerivedShapeCandidate(
154
+ physics.prepareDerivedShapeCandidate(physicsInput(1, false)).unwrap(),
155
+ )
156
+ .unwrap();
157
+ world.update(1 / 60).unwrap();
158
+ expect(centerHit()?.entity).toBe(parent);
159
+ const draw = async () => {
160
+ propagateTransforms(world).unwrap();
161
+ const receipt = renderer
162
+ .draw({ leases: [lease], camera: { lease }, environment: { lease } })
163
+ .unwrap();
164
+ (await receipt.completed).unwrap();
165
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
166
+ return receipt;
167
+ };
168
+ const shot = async (name: string) => {
169
+ const value = await page
170
+ .elementLocator(canvas)
171
+ .screenshot({ path: `wave1-${name}.png`, base64: true });
172
+ return typeof value === 'string' ? value : value.base64;
173
+ };
174
+ await draw();
175
+ const closed = await shot('closed');
176
+ const candidate = renderer
177
+ .prepareDynamicGeometry({
178
+ world,
179
+ entity,
180
+ physicsEntity: parent,
181
+ mesh: openMesh,
182
+ meshHandle: openHandle,
183
+ revision: 2,
184
+ })
185
+ .unwrap();
186
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(closedHandle);
187
+ await draw();
188
+ expect(await shot('prepared')).toBe(closed);
189
+ let accepted: DynamicGeometryCandidate | undefined;
190
+ physics
191
+ .admitDerivedShapeCandidate(
192
+ physics.prepareDerivedShapeCandidate(physicsInput(2, true)).unwrap(),
193
+ () => {
194
+ const result = renderer.acceptDynamicGeometry(candidate, {
195
+ world,
196
+ fixedStep: world.getResource(FixedTime).tick,
197
+ });
198
+ if (!result.ok) return err(result.error);
199
+ accepted = result.value;
200
+ expect(
201
+ renderer.draw({ leases: [lease], camera: { lease }, environment: { lease } }),
202
+ ).toMatchObject({ ok: false, error: { code: 'frame-input-invalid' } });
203
+ return ok(undefined);
204
+ },
205
+ )
206
+ .unwrap();
207
+ world.update(0).unwrap();
208
+ expect(accepted).toBeUndefined();
209
+ expect(centerHit()?.entity).toBe(parent);
210
+ world.update(2 / 60).unwrap();
211
+ expect(accepted).toBeDefined();
212
+ if (accepted === undefined) throw new Error('paired admission did not run');
213
+ expect(centerHit()).toBeUndefined();
214
+ const frame = await draw();
215
+ expect(renderer.dynamicGeometryReceipt(accepted)?.frameId).toBe(frame.frameId);
216
+ const opened = await shot('open');
217
+ expect(opened).not.toBe(closed);
218
+ world.set(light, DirectionalLight, { castShadow: false }).unwrap();
219
+ await draw();
220
+ expect(await shot('no-shadow')).not.toBe(opened);
221
+ world.set(light, DirectionalLight, { castShadow: true }).unwrap();
222
+ world.set(parent, Transform, { pos: [0.75, 0, 0] }).unwrap();
223
+ world.update(1 / 60).unwrap();
224
+ await draw();
225
+ expect(await shot('moved')).not.toBe(opened);
226
+ const replacement = renderer
227
+ .prepareDynamicGeometry({
228
+ world,
229
+ entity,
230
+ physicsEntity: parent,
231
+ mesh: closedMesh,
232
+ meshHandle: closedHandle,
233
+ materialIdentity: String(material),
234
+ revision: 3,
235
+ })
236
+ .unwrap();
237
+ world.set(entity, MeshRenderer, { materials: [gray] }).unwrap();
238
+ physics
239
+ .admitDerivedShapeCandidate(
240
+ physics.prepareDerivedShapeCandidate(physicsInput(3, false)).unwrap(),
241
+ () => {
242
+ const result = renderer.acceptDynamicGeometry(replacement, {
243
+ world,
244
+ fixedStep: world.getResource(FixedTime).tick,
245
+ });
246
+ return result.ok ? ok(undefined) : err(result.error);
247
+ },
248
+ )
249
+ .unwrap();
250
+ world.update(1 / 60).unwrap();
251
+ expect(physics.getDerivedPublication(parent)?.revision).toBe(2);
252
+ expect(physics.getDerivedFailure(parent)?.recovery).toBe('old-state-retained');
253
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(openHandle);
254
+ renderer.cancelDynamicGeometry(replacement).unwrap();
255
+ world.set(entity, MeshRenderer, { materials: [material] }).unwrap();
256
+ const sealedCandidate = renderer
257
+ .prepareDynamicGeometry({
258
+ world,
259
+ entity,
260
+ physicsEntity: parent,
261
+ mesh: closedMesh,
262
+ meshHandle: closedHandle,
263
+ revision: 4,
264
+ })
265
+ .unwrap();
266
+ physics
267
+ .admitDerivedShapeCandidate(
268
+ physics.prepareDerivedShapeCandidate(physicsInput(4, false)).unwrap(),
269
+ () => {
270
+ const result = renderer.acceptDynamicGeometry(sealedCandidate, {
271
+ world,
272
+ fixedStep: world.getResource(FixedTime).tick,
273
+ });
274
+ return result.ok ? ok(undefined) : err(result.error);
275
+ },
276
+ )
277
+ .unwrap();
278
+ world.update(1 / 60).unwrap();
279
+ expect(physics.getDerivedPublication(parent)?.revision).toBe(4);
280
+ await draw();
281
+ renderer.retireDynamicGeometry(accepted as DynamicGeometryCandidate).unwrap();
282
+ await shot('sealed');
283
+ world.set(camera, Camera, { antialias: ANTIALIAS_TAA }).unwrap();
284
+ for (let i = 0; i < 8; i++) await draw();
285
+ await shot('taa-sealed');
286
+ expect(renderer.inspect().temporal.mode).toBe('taa');
287
+ expect(renderer.inspect().temporal.historyValid).toBe(true);
288
+ expect(renderer.inspect().temporal.frameIndex).toBeGreaterThanOrEqual(7);
289
+ const taaCandidate = renderer
290
+ .prepareDynamicGeometry({
291
+ world,
292
+ entity,
293
+ physicsEntity: parent,
294
+ mesh: openMesh,
295
+ meshHandle: openHandle,
296
+ revision: 5,
297
+ })
298
+ .unwrap();
299
+ physics
300
+ .admitDerivedShapeCandidate(
301
+ physics.prepareDerivedShapeCandidate(physicsInput(5, true)).unwrap(),
302
+ () => {
303
+ const result = renderer.acceptDynamicGeometry(taaCandidate, {
304
+ world,
305
+ fixedStep: world.getResource(FixedTime).tick,
306
+ });
307
+ return result.ok ? ok(undefined) : err(result.error);
308
+ },
309
+ )
310
+ .unwrap();
311
+ world.update(1 / 60).unwrap();
312
+ await draw();
313
+ expect(renderer.inspect().temporal.frameIndex).toBe(0);
314
+ await shot('taa-open-first');
315
+ expect(errors).toEqual([]);
316
+ } finally {
317
+ unsubscribe();
318
+ await renderer.dispose();
319
+ releasePhysics();
320
+ const entities = [...world.query({ read: [Transform] }).unwrap()].map((row) => row.entity);
321
+ for (const entity of entities.reverse()) world.despawn(entity).unwrap();
322
+ await scene.fiber.dispose();
323
+ physics.dispose();
324
+ canvas.remove();
325
+ }
326
+ }, 60000);