@multiplekex/shallot 0.1.4 → 0.1.6
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 +2 -2
- package/src/core/builder.ts +1 -1
- package/src/extras/arrows/index.ts +64 -45
- package/src/extras/lines/index.ts +14 -2
- package/src/extras/orbit/index.ts +16 -2
- package/src/extras/text/index.ts +29 -13
- package/src/standard/compute/graph.ts +11 -3
- package/src/standard/compute/index.ts +12 -0
- package/src/standard/compute/inspect.ts +205 -0
- package/src/standard/compute/readback.ts +88 -0
- package/src/standard/compute/timing.ts +139 -0
- package/src/standard/input/index.ts +61 -17
- package/src/standard/render/camera.ts +14 -2
- package/src/standard/render/light.ts +2 -2
- package/src/standard/render/postprocess.ts +23 -24
- package/src/standard/render/scene.ts +12 -0
- package/src/standard/tween/index.ts +1 -1
- package/src/standard/tween/sequence.ts +4 -0
- package/src/standard/tween/tween.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplekex/shallot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"rust/transforms/pkg"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build:wasm": "./rust/transforms/build.sh",
|
|
16
|
+
"build:wasm": "bash ./rust/transforms/build.sh",
|
|
17
17
|
"test": "bun test"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
package/src/core/builder.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { State } from "./state";
|
|
|
2
2
|
import type { Plugin, Loading } from "./types";
|
|
3
3
|
import { toposort, type System } from "./scheduler";
|
|
4
4
|
import { registerComponent } from "./component";
|
|
5
|
+
import { initRuntime } from "./runtime";
|
|
5
6
|
|
|
6
7
|
export class StateBuilder {
|
|
7
8
|
static defaultPlugins: readonly Plugin[] = [];
|
|
@@ -118,7 +119,6 @@ export class StateBuilder {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
if (this._scenes.length > 0) {
|
|
121
|
-
const { initRuntime } = await import("./runtime");
|
|
122
122
|
const { loadSceneFile } = await import("./xml");
|
|
123
123
|
const runtime = await initRuntime();
|
|
124
124
|
for (const scenePath of this._scenes) {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
import { Line, Lines, LinesPlugin } from "../lines";
|
|
13
13
|
|
|
@@ -125,65 +125,72 @@ fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> Ver
|
|
|
125
125
|
let line = lines[eid];
|
|
126
126
|
let transform = matrices[eid];
|
|
127
127
|
|
|
128
|
+
// Extract scale from transform matrix
|
|
129
|
+
let scaleX = length(transform[0].xyz);
|
|
130
|
+
let scaleY = length(transform[1].xyz);
|
|
131
|
+
let scaleZ = length(transform[2].xyz);
|
|
132
|
+
let avgScale = (scaleX + scaleY + scaleZ) / 3.0;
|
|
133
|
+
|
|
134
|
+
// Skip rendering if scale is near zero
|
|
135
|
+
if avgScale < 0.001 {
|
|
136
|
+
var out: VertexOutput;
|
|
137
|
+
out.position = vec4(0.0, 0.0, -2.0, 1.0);
|
|
138
|
+
out.color = vec4(0.0);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
let start = transform[3].xyz;
|
|
129
143
|
let rotation = mat3x3<f32>(transform[0].xyz, transform[1].xyz, transform[2].xyz);
|
|
130
144
|
let end = start + rotation * line.offset;
|
|
131
145
|
|
|
132
|
-
//
|
|
133
|
-
let
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
// Convert to NDC
|
|
137
|
-
let startNDC = startClip.xy / startClip.w;
|
|
138
|
-
let endNDC = endClip.xy / endClip.w;
|
|
139
|
-
|
|
140
|
-
// Direction in screen space
|
|
141
|
-
let dir = endNDC - startNDC;
|
|
142
|
-
let len = length(dir);
|
|
143
|
-
let normDir = select(vec2(1.0, 0.0), dir / len, len > 0.0001);
|
|
146
|
+
// Line direction in world space
|
|
147
|
+
let lineVec = end - start;
|
|
148
|
+
let lineLen = length(lineVec);
|
|
149
|
+
let lineDir = select(vec3(1.0, 0.0, 0.0), lineVec / lineLen, lineLen > 0.0001);
|
|
144
150
|
|
|
145
|
-
//
|
|
146
|
-
let
|
|
151
|
+
// Camera position and direction to arrow
|
|
152
|
+
let cameraPos = scene.cameraWorld[3].xyz;
|
|
153
|
+
let anchorWorld = select(start, end, isEnd);
|
|
154
|
+
let toCamera = normalize(cameraPos - anchorWorld);
|
|
155
|
+
|
|
156
|
+
// Billboard perpendicular: perpendicular to both line and view direction
|
|
157
|
+
var perp = cross(lineDir, toCamera);
|
|
158
|
+
let perpLen = length(perp);
|
|
159
|
+
if perpLen < 0.001 {
|
|
160
|
+
// Line pointing at camera, pick arbitrary perpendicular
|
|
161
|
+
let up = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(lineDir.y) > 0.9);
|
|
162
|
+
perp = normalize(cross(lineDir, up));
|
|
163
|
+
} else {
|
|
164
|
+
perp = perp / perpLen;
|
|
165
|
+
}
|
|
147
166
|
|
|
148
167
|
// Arrow size scales with sqrt of line thickness and world-space distance to camera
|
|
149
|
-
let
|
|
150
|
-
let midpoint = (start + end) * 0.5;
|
|
151
|
-
let distToCamera = length(cameraPos - midpoint);
|
|
168
|
+
let distToCamera = length(cameraPos - anchorWorld);
|
|
152
169
|
let refDist = 15.0;
|
|
153
170
|
let rawScale = distToCamera / refDist;
|
|
154
171
|
let maxScale = 3.0;
|
|
155
172
|
let zoomScale = max(1.0, rawScale / (1.0 + rawScale / maxScale));
|
|
156
|
-
let baseSize = sqrt(line.thickness) * 0.
|
|
157
|
-
let arrowLength = arrow.size * baseSize *
|
|
158
|
-
let arrowWidth = arrow.size * baseSize *
|
|
159
|
-
|
|
160
|
-
// Pick anchor position and direction based on which end
|
|
161
|
-
var anchorNDC: vec2<f32>;
|
|
162
|
-
var anchorDepth: f32;
|
|
163
|
-
var arrowDir: vec2<f32>;
|
|
164
|
-
|
|
165
|
-
if isEnd {
|
|
166
|
-
anchorNDC = endNDC;
|
|
167
|
-
anchorDepth = endClip.z / endClip.w;
|
|
168
|
-
arrowDir = normDir;
|
|
169
|
-
} else {
|
|
170
|
-
anchorNDC = startNDC;
|
|
171
|
-
anchorDepth = startClip.z / startClip.w;
|
|
172
|
-
arrowDir = -normDir;
|
|
173
|
-
}
|
|
173
|
+
let baseSize = sqrt(line.thickness) * 0.1 * zoomScale;
|
|
174
|
+
let arrowLength = arrow.size * baseSize * 2.0;
|
|
175
|
+
let arrowWidth = arrow.size * baseSize * 0.8;
|
|
174
176
|
|
|
175
|
-
//
|
|
176
|
-
let
|
|
177
|
-
|
|
177
|
+
// Arrow direction along line
|
|
178
|
+
let arrowDir = select(-lineDir, lineDir, isEnd);
|
|
179
|
+
|
|
180
|
+
// Build triangle vertices in world space
|
|
181
|
+
var worldPos: vec3<f32>;
|
|
178
182
|
switch vid {
|
|
179
|
-
case 0u: {
|
|
180
|
-
case 1u: {
|
|
181
|
-
case 2u: {
|
|
182
|
-
default: {
|
|
183
|
+
case 0u: { worldPos = anchorWorld + arrowDir * arrowLength * 0.5; }
|
|
184
|
+
case 1u: { worldPos = anchorWorld - arrowDir * arrowLength * 0.5 + perp * arrowWidth; }
|
|
185
|
+
case 2u: { worldPos = anchorWorld - arrowDir * arrowLength * 0.5 - perp * arrowWidth; }
|
|
186
|
+
default: { worldPos = anchorWorld; }
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
// Project to clip space
|
|
190
|
+
let clipPos = scene.viewProj * vec4(worldPos, 1.0);
|
|
191
|
+
|
|
185
192
|
var out: VertexOutput;
|
|
186
|
-
out.position =
|
|
193
|
+
out.position = clipPos;
|
|
187
194
|
out.color = vec4(line.color.rgb, line.color.a * line.opacity);
|
|
188
195
|
return out;
|
|
189
196
|
}
|
|
@@ -230,6 +237,11 @@ export function createArrowsPipeline(
|
|
|
230
237
|
primitive: {
|
|
231
238
|
topology: "triangle-list",
|
|
232
239
|
},
|
|
240
|
+
depthStencil: {
|
|
241
|
+
format: DEPTH_FORMAT,
|
|
242
|
+
depthCompare: "less",
|
|
243
|
+
depthWriteEnabled: false,
|
|
244
|
+
},
|
|
233
245
|
});
|
|
234
246
|
}
|
|
235
247
|
|
|
@@ -239,7 +251,7 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
239
251
|
|
|
240
252
|
return {
|
|
241
253
|
id: "arrows",
|
|
242
|
-
phase: "
|
|
254
|
+
phase: "transparent",
|
|
243
255
|
inputs: [],
|
|
244
256
|
outputs: [],
|
|
245
257
|
|
|
@@ -267,6 +279,8 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
267
279
|
});
|
|
268
280
|
}
|
|
269
281
|
|
|
282
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
283
|
+
|
|
270
284
|
const pass = encoder.beginRenderPass({
|
|
271
285
|
colorAttachments: [
|
|
272
286
|
{
|
|
@@ -275,6 +289,11 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
275
289
|
storeOp: "store" as const,
|
|
276
290
|
},
|
|
277
291
|
],
|
|
292
|
+
depthStencilAttachment: {
|
|
293
|
+
view: depthView,
|
|
294
|
+
depthLoadOp: "load" as const,
|
|
295
|
+
depthStoreOp: "store" as const,
|
|
296
|
+
},
|
|
278
297
|
});
|
|
279
298
|
|
|
280
299
|
pass.setPipeline(pipeline);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
|
|
13
13
|
export const LineData = {
|
|
@@ -245,6 +245,11 @@ export function createLinesPipeline(
|
|
|
245
245
|
primitive: {
|
|
246
246
|
topology: "triangle-list",
|
|
247
247
|
},
|
|
248
|
+
depthStencil: {
|
|
249
|
+
format: DEPTH_FORMAT,
|
|
250
|
+
depthCompare: "less",
|
|
251
|
+
depthWriteEnabled: false,
|
|
252
|
+
},
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
255
|
|
|
@@ -254,7 +259,7 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
254
259
|
|
|
255
260
|
return {
|
|
256
261
|
id: "lines",
|
|
257
|
-
phase: "
|
|
262
|
+
phase: "transparent",
|
|
258
263
|
inputs: [],
|
|
259
264
|
outputs: [],
|
|
260
265
|
|
|
@@ -281,6 +286,8 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
281
286
|
});
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
290
|
+
|
|
284
291
|
const pass = encoder.beginRenderPass({
|
|
285
292
|
colorAttachments: [
|
|
286
293
|
{
|
|
@@ -289,6 +296,11 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
289
296
|
storeOp: "store" as const,
|
|
290
297
|
},
|
|
291
298
|
],
|
|
299
|
+
depthStencilAttachment: {
|
|
300
|
+
view: depthView,
|
|
301
|
+
depthLoadOp: "load" as const,
|
|
302
|
+
depthStoreOp: "store" as const,
|
|
303
|
+
},
|
|
292
304
|
});
|
|
293
305
|
|
|
294
306
|
pass.setPipeline(pipeline);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { setTraits } from "../../core/component";
|
|
2
|
-
import { clamp, lookAt, type State, type System, type Plugin } from "../../core";
|
|
2
|
+
import { clamp, lookAt, type State, type System, type Plugin, type MouseState } from "../../core";
|
|
3
3
|
import { Transform } from "../../standard/transforms";
|
|
4
4
|
import { Input, InputPlugin } from "../../standard/input";
|
|
5
5
|
|
|
6
6
|
const Tau = Math.PI * 2;
|
|
7
7
|
|
|
8
|
+
export const OrbitButton = {
|
|
9
|
+
Left: 0,
|
|
10
|
+
Middle: 1,
|
|
11
|
+
Right: 2,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
8
14
|
export const Orbit = {
|
|
9
15
|
target: [] as number[],
|
|
10
16
|
yaw: [] as number[],
|
|
@@ -20,6 +26,7 @@ export const Orbit = {
|
|
|
20
26
|
smoothness: [] as number[],
|
|
21
27
|
sensitivity: [] as number[],
|
|
22
28
|
zoomSpeed: [] as number[],
|
|
29
|
+
button: [] as number[],
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
setTraits(Orbit, {
|
|
@@ -38,6 +45,7 @@ setTraits(Orbit, {
|
|
|
38
45
|
smoothness: 0.3,
|
|
39
46
|
sensitivity: 0.005,
|
|
40
47
|
zoomSpeed: 0.025,
|
|
48
|
+
button: OrbitButton.Right,
|
|
41
49
|
}),
|
|
42
50
|
});
|
|
43
51
|
|
|
@@ -54,6 +62,12 @@ function angleDiff(from: number, to: number): number {
|
|
|
54
62
|
return diff > Math.PI ? diff - Tau : diff;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
function isOrbitButton(mouse: Readonly<MouseState>, button: number): boolean {
|
|
66
|
+
if (button === OrbitButton.Left) return mouse.left;
|
|
67
|
+
if (button === OrbitButton.Middle) return mouse.middle;
|
|
68
|
+
return mouse.right;
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
export const OrbitSystem: System = {
|
|
58
72
|
group: "simulation",
|
|
59
73
|
|
|
@@ -70,7 +84,7 @@ export const OrbitSystem: System = {
|
|
|
70
84
|
const maxDistance = Orbit.maxDistance[eid];
|
|
71
85
|
const smoothness = Orbit.smoothness[eid];
|
|
72
86
|
|
|
73
|
-
if (input
|
|
87
|
+
if (input && isOrbitButton(input.mouse, Orbit.button[eid])) {
|
|
74
88
|
Orbit.targetYaw[eid] -= input.mouse.deltaX * sensitivity;
|
|
75
89
|
Orbit.targetPitch[eid] = clamp(
|
|
76
90
|
Orbit.targetPitch[eid] + input.mouse.deltaY * sensitivity,
|
package/src/extras/text/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
|
|
13
13
|
const MAX_GLYPHS = 50000;
|
|
@@ -304,7 +304,7 @@ struct GlyphInstance {
|
|
|
304
304
|
posX: f32,
|
|
305
305
|
posY: f32,
|
|
306
306
|
posZ: f32,
|
|
307
|
-
|
|
307
|
+
entityId: u32,
|
|
308
308
|
width: f32,
|
|
309
309
|
height: f32,
|
|
310
310
|
_pad1: vec2<f32>,
|
|
@@ -319,6 +319,7 @@ struct GlyphInstance {
|
|
|
319
319
|
@group(0) @binding(1) var<storage, read> glyphs: array<GlyphInstance>;
|
|
320
320
|
@group(0) @binding(2) var atlasTexture: texture_2d<f32>;
|
|
321
321
|
@group(0) @binding(3) var atlasSampler: sampler;
|
|
322
|
+
@group(0) @binding(4) var<storage, read> matrices: array<mat4x4<f32>>;
|
|
322
323
|
|
|
323
324
|
struct VertexOutput {
|
|
324
325
|
@builtin(position) position: vec4<f32>,
|
|
@@ -369,14 +370,17 @@ fn vs(@builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
let
|
|
373
|
+
let localPos3 = vec3(
|
|
373
374
|
glyph.posX + localPos.x * glyph.width,
|
|
374
375
|
glyph.posY + localPos.y * glyph.height,
|
|
375
376
|
glyph.posZ
|
|
376
377
|
);
|
|
377
378
|
|
|
379
|
+
let transform = matrices[glyph.entityId];
|
|
380
|
+
let worldPos = transform * vec4(localPos3, 1.0);
|
|
381
|
+
|
|
378
382
|
var out: VertexOutput;
|
|
379
|
-
out.position = scene.viewProj *
|
|
383
|
+
out.position = scene.viewProj * worldPos;
|
|
380
384
|
out.uv = uv;
|
|
381
385
|
out.color = glyph.color;
|
|
382
386
|
return out;
|
|
@@ -429,6 +433,11 @@ function createTextPipeline(device: GPUDevice, format: GPUTextureFormat): GPURen
|
|
|
429
433
|
topology: "triangle-list",
|
|
430
434
|
cullMode: "none",
|
|
431
435
|
},
|
|
436
|
+
depthStencil: {
|
|
437
|
+
format: DEPTH_FORMAT,
|
|
438
|
+
depthCompare: "less",
|
|
439
|
+
depthWriteEnabled: false,
|
|
440
|
+
},
|
|
432
441
|
});
|
|
433
442
|
}
|
|
434
443
|
|
|
@@ -437,6 +446,7 @@ export interface TextConfig {
|
|
|
437
446
|
glyphs: GPUBuffer;
|
|
438
447
|
atlas: GPUTextureView;
|
|
439
448
|
sampler: GPUSampler;
|
|
449
|
+
matrices: GPUBuffer;
|
|
440
450
|
getCount: () => number;
|
|
441
451
|
}
|
|
442
452
|
|
|
@@ -446,7 +456,7 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
446
456
|
|
|
447
457
|
return {
|
|
448
458
|
id: "text",
|
|
449
|
-
phase: "
|
|
459
|
+
phase: "transparent",
|
|
450
460
|
inputs: [],
|
|
451
461
|
outputs: [],
|
|
452
462
|
|
|
@@ -469,10 +479,13 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
469
479
|
{ binding: 1, resource: { buffer: config.glyphs } },
|
|
470
480
|
{ binding: 2, resource: config.atlas },
|
|
471
481
|
{ binding: 3, resource: config.sampler },
|
|
482
|
+
{ binding: 4, resource: { buffer: config.matrices } },
|
|
472
483
|
],
|
|
473
484
|
});
|
|
474
485
|
}
|
|
475
486
|
|
|
487
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
488
|
+
|
|
476
489
|
const pass = encoder.beginRenderPass({
|
|
477
490
|
colorAttachments: [
|
|
478
491
|
{
|
|
@@ -481,6 +494,11 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
481
494
|
storeOp: "store" as const,
|
|
482
495
|
},
|
|
483
496
|
],
|
|
497
|
+
depthStencilAttachment: {
|
|
498
|
+
view: depthView,
|
|
499
|
+
depthLoadOp: "load" as const,
|
|
500
|
+
depthStoreOp: "store" as const,
|
|
501
|
+
},
|
|
484
502
|
});
|
|
485
503
|
|
|
486
504
|
pass.setPipeline(pipeline);
|
|
@@ -523,6 +541,7 @@ const TextSystem: System = {
|
|
|
523
541
|
|
|
524
542
|
const { device } = compute;
|
|
525
543
|
const { atlas, staging, content } = text;
|
|
544
|
+
const stagingU32 = new Uint32Array(staging.buffer);
|
|
526
545
|
|
|
527
546
|
let glyphCount = 0;
|
|
528
547
|
|
|
@@ -542,10 +561,6 @@ const TextSystem: System = {
|
|
|
542
561
|
const offsetX = -layout.width * anchorX;
|
|
543
562
|
const offsetY = -layout.height * anchorY;
|
|
544
563
|
|
|
545
|
-
const baseX = Transform.posX[eid] + offsetX;
|
|
546
|
-
const baseY = Transform.posY[eid] + offsetY;
|
|
547
|
-
const baseZ = Transform.posZ[eid];
|
|
548
|
-
|
|
549
564
|
const color = Text.color[eid];
|
|
550
565
|
const r = ((color >> 16) & 0xff) / 255;
|
|
551
566
|
const g = ((color >> 8) & 0xff) / 255;
|
|
@@ -557,10 +572,10 @@ const TextSystem: System = {
|
|
|
557
572
|
|
|
558
573
|
const offset = glyphCount * GLYPH_FLOATS;
|
|
559
574
|
|
|
560
|
-
staging[offset + 0] =
|
|
561
|
-
staging[offset + 1] =
|
|
562
|
-
staging[offset + 2] =
|
|
563
|
-
|
|
575
|
+
staging[offset + 0] = offsetX + glyph.x;
|
|
576
|
+
staging[offset + 1] = offsetY + glyph.y;
|
|
577
|
+
staging[offset + 2] = 0;
|
|
578
|
+
stagingU32[offset + 3] = eid;
|
|
564
579
|
|
|
565
580
|
staging[offset + 4] = glyph.width;
|
|
566
581
|
staging[offset + 5] = glyph.height;
|
|
@@ -634,6 +649,7 @@ export const TextPlugin: Plugin = {
|
|
|
634
649
|
glyphs: textState.buffer,
|
|
635
650
|
atlas: atlas.textureView,
|
|
636
651
|
sampler,
|
|
652
|
+
matrices: render.matrices,
|
|
637
653
|
getCount: () => textState.count,
|
|
638
654
|
})
|
|
639
655
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { CycleError } from "../../core";
|
|
2
|
+
import { inspect as inspectGraph, type GraphInspection } from "./inspect";
|
|
2
3
|
|
|
3
4
|
export type ResourceId = string;
|
|
4
5
|
export type NodeId = string;
|
|
5
|
-
export type Phase = "
|
|
6
|
+
export type Phase = "opaque" | "transparent" | "postprocess";
|
|
6
7
|
|
|
7
|
-
const PHASE_ORDER: Phase[] = ["
|
|
8
|
+
const PHASE_ORDER: Phase[] = ["opaque", "transparent", "postprocess"];
|
|
8
9
|
|
|
9
10
|
export interface ResourceRef {
|
|
10
11
|
id: ResourceId;
|
|
@@ -21,6 +22,9 @@ export interface ExecutionContext {
|
|
|
21
22
|
getTexture(id: ResourceId): GPUTexture | null;
|
|
22
23
|
getTextureView(id: ResourceId): GPUTextureView | null;
|
|
23
24
|
getBuffer(id: ResourceId): GPUBuffer | null;
|
|
25
|
+
setTexture(id: ResourceId, texture: GPUTexture): void;
|
|
26
|
+
setTextureView(id: ResourceId, view: GPUTextureView): void;
|
|
27
|
+
setBuffer(id: ResourceId, buffer: GPUBuffer): void;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export interface ComputeNode {
|
|
@@ -115,7 +119,7 @@ function compile(nodes: ComputeNode[]): ExecutionPlan {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
for (const node of nodes) {
|
|
118
|
-
const phase = node.phase ?? "
|
|
122
|
+
const phase = node.phase ?? "opaque";
|
|
119
123
|
byPhase.get(phase)!.push(node);
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -162,4 +166,8 @@ export class ComputeGraph {
|
|
|
162
166
|
}
|
|
163
167
|
return this._plan;
|
|
164
168
|
}
|
|
169
|
+
|
|
170
|
+
inspect(): GraphInspection {
|
|
171
|
+
return inspectGraph(Array.from(this.nodes.values()));
|
|
172
|
+
}
|
|
165
173
|
}
|
|
@@ -2,6 +2,9 @@ import { resource, type Plugin, type State, type System } from "../../core";
|
|
|
2
2
|
import { ComputeGraph, type ExecutionContext, type ResourceId } from "./graph";
|
|
3
3
|
|
|
4
4
|
export * from "./graph";
|
|
5
|
+
export * from "./inspect";
|
|
6
|
+
export * from "./readback";
|
|
7
|
+
export * from "./timing";
|
|
5
8
|
|
|
6
9
|
const MIN_CANVAS_SIZE = 1;
|
|
7
10
|
|
|
@@ -102,6 +105,15 @@ export const ComputeSystem: System = {
|
|
|
102
105
|
getBuffer(id: ResourceId) {
|
|
103
106
|
return resources.buffers.get(id) ?? null;
|
|
104
107
|
},
|
|
108
|
+
setTexture(id: ResourceId, texture: GPUTexture) {
|
|
109
|
+
resources.textures.set(id, texture);
|
|
110
|
+
},
|
|
111
|
+
setTextureView(id: ResourceId, view: GPUTextureView) {
|
|
112
|
+
resources.textureViews.set(id, view);
|
|
113
|
+
},
|
|
114
|
+
setBuffer(id: ResourceId, buffer: GPUBuffer) {
|
|
115
|
+
resources.buffers.set(id, buffer);
|
|
116
|
+
},
|
|
105
117
|
};
|
|
106
118
|
|
|
107
119
|
for (const node of plan.sorted) {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ComputeNode, NodeId, Phase, ResourceId, ResourceRef } from "./graph";
|
|
2
|
+
|
|
3
|
+
export interface NodeInfo {
|
|
4
|
+
readonly id: NodeId;
|
|
5
|
+
readonly phase: Phase;
|
|
6
|
+
readonly inputs: readonly ResourceRef[];
|
|
7
|
+
readonly outputs: readonly ResourceRef[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EdgeInfo {
|
|
11
|
+
readonly from: NodeId;
|
|
12
|
+
readonly to: NodeId;
|
|
13
|
+
readonly resource: ResourceId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GraphInspection {
|
|
17
|
+
readonly nodes: readonly NodeInfo[];
|
|
18
|
+
readonly edges: readonly EdgeInfo[];
|
|
19
|
+
readonly executionOrder: readonly NodeId[];
|
|
20
|
+
readonly byPhase: ReadonlyMap<Phase, readonly NodeId[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PHASE_ORDER: Phase[] = ["opaque", "transparent", "postprocess"];
|
|
24
|
+
|
|
25
|
+
export function inspect(nodes: readonly ComputeNode[]): GraphInspection {
|
|
26
|
+
const nodeInfos: NodeInfo[] = nodes.map((n) => ({
|
|
27
|
+
id: n.id,
|
|
28
|
+
phase: n.phase ?? "opaque",
|
|
29
|
+
inputs: n.inputs,
|
|
30
|
+
outputs: n.outputs,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const producers = new Map<ResourceId, NodeId>();
|
|
34
|
+
for (const n of nodes) {
|
|
35
|
+
for (const out of n.outputs) {
|
|
36
|
+
producers.set(out.id, n.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const edges: EdgeInfo[] = [];
|
|
41
|
+
for (const n of nodes) {
|
|
42
|
+
for (const inp of n.inputs) {
|
|
43
|
+
const producer = producers.get(inp.id);
|
|
44
|
+
if (producer) {
|
|
45
|
+
edges.push({
|
|
46
|
+
from: producer,
|
|
47
|
+
to: n.id,
|
|
48
|
+
resource: inp.id,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const byPhase = new Map<Phase, NodeId[]>();
|
|
55
|
+
for (const phase of PHASE_ORDER) {
|
|
56
|
+
byPhase.set(phase, []);
|
|
57
|
+
}
|
|
58
|
+
for (const n of nodeInfos) {
|
|
59
|
+
byPhase.get(n.phase)!.push(n.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const executionOrder = topoSortIds(nodes);
|
|
63
|
+
|
|
64
|
+
return { nodes: nodeInfos, edges, executionOrder, byPhase };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function topoSortIds(nodes: readonly ComputeNode[]): NodeId[] {
|
|
68
|
+
if (nodes.length === 0) return [];
|
|
69
|
+
|
|
70
|
+
const producers = new Map<ResourceId, ComputeNode>();
|
|
71
|
+
for (const node of nodes) {
|
|
72
|
+
for (const output of node.outputs) {
|
|
73
|
+
producers.set(output.id, node);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const adjacency = new Map<ComputeNode, ComputeNode[]>();
|
|
78
|
+
const inDegree = new Map<ComputeNode, number>();
|
|
79
|
+
|
|
80
|
+
for (const node of nodes) {
|
|
81
|
+
adjacency.set(node, []);
|
|
82
|
+
inDegree.set(node, 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
for (const input of node.inputs) {
|
|
87
|
+
const producer = producers.get(input.id);
|
|
88
|
+
if (producer) {
|
|
89
|
+
adjacency.get(producer)!.push(node);
|
|
90
|
+
inDegree.set(node, inDegree.get(node)! + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const byPhase = new Map<Phase, ComputeNode[]>();
|
|
96
|
+
for (const phase of PHASE_ORDER) {
|
|
97
|
+
byPhase.set(phase, []);
|
|
98
|
+
}
|
|
99
|
+
for (const node of nodes) {
|
|
100
|
+
const phase = node.phase ?? "opaque";
|
|
101
|
+
byPhase.get(phase)!.push(node);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sorted: NodeId[] = [];
|
|
105
|
+
for (const phase of PHASE_ORDER) {
|
|
106
|
+
const phaseNodes = byPhase.get(phase)!;
|
|
107
|
+
const phaseInDegree = new Map<ComputeNode, number>();
|
|
108
|
+
|
|
109
|
+
for (const node of phaseNodes) {
|
|
110
|
+
let degree = 0;
|
|
111
|
+
for (const input of node.inputs) {
|
|
112
|
+
const producer = producers.get(input.id);
|
|
113
|
+
if (producer && phaseNodes.includes(producer)) {
|
|
114
|
+
degree++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
phaseInDegree.set(node, degree);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const queue: ComputeNode[] = [];
|
|
121
|
+
for (const node of phaseNodes) {
|
|
122
|
+
if (phaseInDegree.get(node) === 0) {
|
|
123
|
+
queue.push(node);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let i = 0;
|
|
128
|
+
while (i < queue.length) {
|
|
129
|
+
const node = queue[i++];
|
|
130
|
+
sorted.push(node.id);
|
|
131
|
+
|
|
132
|
+
for (const dep of phaseNodes) {
|
|
133
|
+
for (const input of dep.inputs) {
|
|
134
|
+
const producer = producers.get(input.id);
|
|
135
|
+
if (producer === node) {
|
|
136
|
+
const newDegree = phaseInDegree.get(dep)! - 1;
|
|
137
|
+
phaseInDegree.set(dep, newDegree);
|
|
138
|
+
if (newDegree === 0) {
|
|
139
|
+
queue.push(dep);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sorted;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function formatGraph(info: GraphInspection): string {
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push("=== Compute Graph ===");
|
|
153
|
+
lines.push("");
|
|
154
|
+
|
|
155
|
+
const nodeMap = new Map(info.nodes.map((n) => [n.id, n]));
|
|
156
|
+
|
|
157
|
+
for (const phase of PHASE_ORDER) {
|
|
158
|
+
const nodeIds = info.byPhase.get(phase) ?? [];
|
|
159
|
+
if (nodeIds.length === 0) continue;
|
|
160
|
+
|
|
161
|
+
lines.push(`[${phase}]`);
|
|
162
|
+
for (const id of nodeIds) {
|
|
163
|
+
const node = nodeMap.get(id)!;
|
|
164
|
+
const ins = node.inputs.map((r) => r.id).join(", ") || "none";
|
|
165
|
+
const outs = node.outputs.map((r) => r.id).join(", ") || "none";
|
|
166
|
+
lines.push(` ${id}: ${ins} -> ${outs}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
lines.push("Execution Order:");
|
|
172
|
+
info.executionOrder.forEach((id, i) => {
|
|
173
|
+
lines.push(` ${i + 1}. ${id}`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function toDot(info: GraphInspection): string {
|
|
180
|
+
const lines: string[] = ["digraph ComputeGraph {"];
|
|
181
|
+
lines.push(" rankdir=LR;");
|
|
182
|
+
lines.push(" node [shape=box];");
|
|
183
|
+
lines.push("");
|
|
184
|
+
|
|
185
|
+
for (const phase of PHASE_ORDER) {
|
|
186
|
+
const nodeIds = info.byPhase.get(phase) ?? [];
|
|
187
|
+
if (nodeIds.length === 0) continue;
|
|
188
|
+
|
|
189
|
+
lines.push(` subgraph cluster_${phase} {`);
|
|
190
|
+
lines.push(` label="${phase}";`);
|
|
191
|
+
for (const id of nodeIds) {
|
|
192
|
+
lines.push(` "${id}";`);
|
|
193
|
+
}
|
|
194
|
+
lines.push(" }");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push("");
|
|
198
|
+
|
|
199
|
+
for (const edge of info.edges) {
|
|
200
|
+
lines.push(` "${edge.from}" -> "${edge.to}" [label="${edge.resource}"];`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push("}");
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export async function readBuffer(
|
|
2
|
+
device: GPUDevice,
|
|
3
|
+
source: GPUBuffer,
|
|
4
|
+
size: number
|
|
5
|
+
): Promise<ArrayBuffer> {
|
|
6
|
+
const staging = device.createBuffer({
|
|
7
|
+
label: "staging-readback",
|
|
8
|
+
size,
|
|
9
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const encoder = device.createCommandEncoder();
|
|
13
|
+
encoder.copyBufferToBuffer(source, 0, staging, 0, size);
|
|
14
|
+
device.queue.submit([encoder.finish()]);
|
|
15
|
+
|
|
16
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
17
|
+
const data = staging.getMappedRange().slice(0);
|
|
18
|
+
staging.unmap();
|
|
19
|
+
staging.destroy();
|
|
20
|
+
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readFloat32(
|
|
25
|
+
device: GPUDevice,
|
|
26
|
+
buffer: GPUBuffer,
|
|
27
|
+
count: number
|
|
28
|
+
): Promise<Float32Array> {
|
|
29
|
+
const data = await readBuffer(device, buffer, count * 4);
|
|
30
|
+
return new Float32Array(data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readUint32(
|
|
34
|
+
device: GPUDevice,
|
|
35
|
+
buffer: GPUBuffer,
|
|
36
|
+
count: number
|
|
37
|
+
): Promise<Uint32Array> {
|
|
38
|
+
const data = await readBuffer(device, buffer, count * 4);
|
|
39
|
+
return new Uint32Array(data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nextPowerOf2(n: number): number {
|
|
43
|
+
const min = 256;
|
|
44
|
+
return Math.pow(2, Math.ceil(Math.log2(Math.max(n, min))));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class StagingPool {
|
|
48
|
+
private readonly pool = new Map<number, GPUBuffer[]>();
|
|
49
|
+
private readonly device: GPUDevice;
|
|
50
|
+
|
|
51
|
+
constructor(device: GPUDevice) {
|
|
52
|
+
this.device = device;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
acquire(size: number): GPUBuffer {
|
|
56
|
+
const bucketSize = nextPowerOf2(size);
|
|
57
|
+
const bucket = this.pool.get(bucketSize);
|
|
58
|
+
|
|
59
|
+
if (bucket && bucket.length > 0) {
|
|
60
|
+
return bucket.pop()!;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.device.createBuffer({
|
|
64
|
+
label: `staging-pool-${bucketSize}`,
|
|
65
|
+
size: bucketSize,
|
|
66
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
release(buffer: GPUBuffer): void {
|
|
71
|
+
const bucketSize = buffer.size;
|
|
72
|
+
let bucket = this.pool.get(bucketSize);
|
|
73
|
+
if (!bucket) {
|
|
74
|
+
bucket = [];
|
|
75
|
+
this.pool.set(bucketSize, bucket);
|
|
76
|
+
}
|
|
77
|
+
bucket.push(buffer);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dispose(): void {
|
|
81
|
+
for (const bucket of this.pool.values()) {
|
|
82
|
+
for (const buffer of bucket) {
|
|
83
|
+
buffer.destroy();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.pool.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { NodeId } from "./graph";
|
|
2
|
+
|
|
3
|
+
export interface NodeTiming {
|
|
4
|
+
readonly nodeId: NodeId;
|
|
5
|
+
readonly cpuMs: number;
|
|
6
|
+
readonly gpuNs?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FrameTiming {
|
|
10
|
+
readonly frameIndex: number;
|
|
11
|
+
readonly nodes: readonly NodeTiming[];
|
|
12
|
+
readonly totalCpuMs: number;
|
|
13
|
+
readonly totalGpuNs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TimingConfig {
|
|
17
|
+
readonly enabled: boolean;
|
|
18
|
+
readonly gpuTimestamps: boolean;
|
|
19
|
+
readonly historySize: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TimingState {
|
|
23
|
+
readonly config: TimingConfig;
|
|
24
|
+
readonly history: FrameTiming[];
|
|
25
|
+
readonly querySet?: GPUQuerySet;
|
|
26
|
+
readonly resolveBuffer?: GPUBuffer;
|
|
27
|
+
readonly readBuffer?: GPUBuffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function supportsTimestampQuery(device: GPUDevice): boolean {
|
|
31
|
+
return device.features.has("timestamp-query");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createTimingState(
|
|
35
|
+
device: GPUDevice,
|
|
36
|
+
maxNodes: number,
|
|
37
|
+
historySize = 60
|
|
38
|
+
): TimingState {
|
|
39
|
+
const gpuTimestamps = supportsTimestampQuery(device);
|
|
40
|
+
|
|
41
|
+
let querySet: GPUQuerySet | undefined;
|
|
42
|
+
let resolveBuffer: GPUBuffer | undefined;
|
|
43
|
+
let readBuffer: GPUBuffer | undefined;
|
|
44
|
+
|
|
45
|
+
if (gpuTimestamps) {
|
|
46
|
+
const queryCount = maxNodes * 2;
|
|
47
|
+
querySet = device.createQuerySet({
|
|
48
|
+
label: "timing-queries",
|
|
49
|
+
type: "timestamp",
|
|
50
|
+
count: queryCount,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
resolveBuffer = device.createBuffer({
|
|
54
|
+
label: "timing-resolve",
|
|
55
|
+
size: queryCount * 8,
|
|
56
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
readBuffer = device.createBuffer({
|
|
60
|
+
label: "timing-read",
|
|
61
|
+
size: queryCount * 8,
|
|
62
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
config: { enabled: true, gpuTimestamps, historySize },
|
|
68
|
+
history: [],
|
|
69
|
+
querySet,
|
|
70
|
+
resolveBuffer,
|
|
71
|
+
readBuffer,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readTimestamps(
|
|
76
|
+
device: GPUDevice,
|
|
77
|
+
timing: TimingState,
|
|
78
|
+
nodeCount: number
|
|
79
|
+
): Promise<BigUint64Array | null> {
|
|
80
|
+
if (!timing.querySet || !timing.resolveBuffer || !timing.readBuffer) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const encoder = device.createCommandEncoder();
|
|
85
|
+
encoder.resolveQuerySet(timing.querySet, 0, nodeCount * 2, timing.resolveBuffer, 0);
|
|
86
|
+
encoder.copyBufferToBuffer(timing.resolveBuffer, 0, timing.readBuffer, 0, nodeCount * 2 * 8);
|
|
87
|
+
device.queue.submit([encoder.finish()]);
|
|
88
|
+
|
|
89
|
+
await timing.readBuffer.mapAsync(GPUMapMode.READ);
|
|
90
|
+
const data = new BigUint64Array(timing.readBuffer.getMappedRange().slice(0));
|
|
91
|
+
timing.readBuffer.unmap();
|
|
92
|
+
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function disposeTimingState(timing: TimingState): void {
|
|
97
|
+
timing.querySet?.destroy();
|
|
98
|
+
timing.resolveBuffer?.destroy();
|
|
99
|
+
timing.readBuffer?.destroy();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TimingCollector {
|
|
103
|
+
readonly nodeTimings: Map<NodeId, { start: number; end: number }>;
|
|
104
|
+
beginNode(nodeId: NodeId): void;
|
|
105
|
+
endNode(nodeId: NodeId): void;
|
|
106
|
+
finish(frameIndex: number): FrameTiming;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createTimingCollector(): TimingCollector {
|
|
110
|
+
const nodeTimings = new Map<NodeId, { start: number; end: number }>();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
nodeTimings,
|
|
114
|
+
|
|
115
|
+
beginNode(nodeId: NodeId): void {
|
|
116
|
+
nodeTimings.set(nodeId, { start: performance.now(), end: 0 });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
endNode(nodeId: NodeId): void {
|
|
120
|
+
const timing = nodeTimings.get(nodeId);
|
|
121
|
+
if (timing) {
|
|
122
|
+
timing.end = performance.now();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
finish(frameIndex: number): FrameTiming {
|
|
127
|
+
const nodes: NodeTiming[] = [];
|
|
128
|
+
let totalCpuMs = 0;
|
|
129
|
+
|
|
130
|
+
for (const [nodeId, timing] of nodeTimings) {
|
|
131
|
+
const cpuMs = timing.end - timing.start;
|
|
132
|
+
totalCpuMs += cpuMs;
|
|
133
|
+
nodes.push({ nodeId, cpuMs });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { frameIndex, nodes, totalCpuMs };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -30,6 +30,10 @@ const inputState: InputState = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
let canvas: HTMLCanvasElement | null = null;
|
|
33
|
+
let lastPointerX = 0;
|
|
34
|
+
let lastPointerY = 0;
|
|
35
|
+
let activePointerId: number | null = null;
|
|
36
|
+
let activeButton: number | null = null;
|
|
33
37
|
|
|
34
38
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
35
39
|
if (!keys.has(e.code)) {
|
|
@@ -43,22 +47,55 @@ function handleKeyUp(e: KeyboardEvent): void {
|
|
|
43
47
|
keysReleased.add(e.code);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
function
|
|
50
|
+
function setButtonState(button: number, pressed: boolean): void {
|
|
51
|
+
if (button === 0) mouse.left = pressed;
|
|
52
|
+
if (button === 1) mouse.middle = pressed;
|
|
53
|
+
if (button === 2) mouse.right = pressed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearPointerState(): void {
|
|
57
|
+
if (activeButton !== null) {
|
|
58
|
+
setButtonState(activeButton, false);
|
|
59
|
+
}
|
|
60
|
+
activePointerId = null;
|
|
61
|
+
activeButton = null;
|
|
62
|
+
lastPointerX = 0;
|
|
63
|
+
lastPointerY = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handlePointerDown(e: PointerEvent): void {
|
|
47
67
|
if (e.target !== canvas) return;
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
if (activePointerId === null) {
|
|
69
|
+
activePointerId = e.pointerId;
|
|
70
|
+
activeButton = e.button;
|
|
71
|
+
lastPointerX = e.clientX;
|
|
72
|
+
lastPointerY = e.clientY;
|
|
73
|
+
setButtonState(e.button, true);
|
|
74
|
+
canvas!.setPointerCapture(e.pointerId);
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handlePointerUp(e: PointerEvent): void {
|
|
80
|
+
if (e.pointerId === activePointerId) {
|
|
81
|
+
canvas?.releasePointerCapture(e.pointerId);
|
|
82
|
+
clearPointerState();
|
|
83
|
+
}
|
|
51
84
|
}
|
|
52
85
|
|
|
53
|
-
function
|
|
54
|
-
if (e.
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
function handlePointerCancel(e: PointerEvent): void {
|
|
87
|
+
if (e.pointerId === activePointerId) {
|
|
88
|
+
clearPointerState();
|
|
89
|
+
}
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
function handlePointerMove(e: PointerEvent): void {
|
|
93
|
+
if (e.pointerId !== activePointerId) return;
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
mouse.deltaX += e.clientX - lastPointerX;
|
|
96
|
+
mouse.deltaY += e.clientY - lastPointerY;
|
|
97
|
+
lastPointerX = e.clientX;
|
|
98
|
+
lastPointerY = e.clientY;
|
|
62
99
|
}
|
|
63
100
|
|
|
64
101
|
function handleWheel(e: WheelEvent): void {
|
|
@@ -91,6 +128,10 @@ function clearAllState(): void {
|
|
|
91
128
|
mouse.left = false;
|
|
92
129
|
mouse.right = false;
|
|
93
130
|
mouse.middle = false;
|
|
131
|
+
activePointerId = null;
|
|
132
|
+
activeButton = null;
|
|
133
|
+
lastPointerX = 0;
|
|
134
|
+
lastPointerY = 0;
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
export const InputSystem: System = {
|
|
@@ -99,12 +140,14 @@ export const InputSystem: System = {
|
|
|
99
140
|
setup(state: State) {
|
|
100
141
|
if (!state.canvas) return;
|
|
101
142
|
canvas = state.canvas;
|
|
143
|
+
canvas.style.touchAction = "none";
|
|
102
144
|
|
|
103
145
|
window.addEventListener("keydown", handleKeyDown);
|
|
104
146
|
window.addEventListener("keyup", handleKeyUp);
|
|
105
|
-
canvas.addEventListener("
|
|
106
|
-
window.addEventListener("
|
|
107
|
-
|
|
147
|
+
canvas.addEventListener("pointerdown", handlePointerDown);
|
|
148
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
149
|
+
window.addEventListener("pointercancel", handlePointerCancel);
|
|
150
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
108
151
|
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
|
109
152
|
canvas.addEventListener("contextmenu", handleContextMenu);
|
|
110
153
|
|
|
@@ -115,9 +158,10 @@ export const InputSystem: System = {
|
|
|
115
158
|
if (canvas) {
|
|
116
159
|
window.removeEventListener("keydown", handleKeyDown);
|
|
117
160
|
window.removeEventListener("keyup", handleKeyUp);
|
|
118
|
-
canvas.removeEventListener("
|
|
119
|
-
window.removeEventListener("
|
|
120
|
-
|
|
161
|
+
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
162
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
163
|
+
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
164
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
121
165
|
canvas.removeEventListener("wheel", handleWheel);
|
|
122
166
|
canvas.removeEventListener("contextmenu", handleContextMenu);
|
|
123
167
|
canvas = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setTraits } from "../../core/component";
|
|
2
2
|
import { WorldTransform } from "../transforms";
|
|
3
3
|
import { clearColor } from "./forward";
|
|
4
|
-
import { perspective, multiply, invert } from "./scene";
|
|
4
|
+
import { perspective, orthographic, multiply, invert } from "./scene";
|
|
5
5
|
|
|
6
6
|
export const RenderMode = {
|
|
7
7
|
Raster: 0,
|
|
@@ -16,6 +16,11 @@ export const DebugMode = {
|
|
|
16
16
|
Hit: 4,
|
|
17
17
|
} as const;
|
|
18
18
|
|
|
19
|
+
export const CameraMode = {
|
|
20
|
+
Perspective: 0,
|
|
21
|
+
Orthographic: 1,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
19
24
|
export const Camera = {
|
|
20
25
|
fov: [] as number[],
|
|
21
26
|
near: [] as number[],
|
|
@@ -24,6 +29,8 @@ export const Camera = {
|
|
|
24
29
|
clearColor: [] as number[],
|
|
25
30
|
renderMode: [] as number[],
|
|
26
31
|
debugMode: [] as number[],
|
|
32
|
+
mode: [] as number[],
|
|
33
|
+
size: [] as number[],
|
|
27
34
|
};
|
|
28
35
|
|
|
29
36
|
setTraits(Camera, {
|
|
@@ -35,6 +42,8 @@ setTraits(Camera, {
|
|
|
35
42
|
clearColor: 0x1a1a1a,
|
|
36
43
|
renderMode: RenderMode.Raster,
|
|
37
44
|
debugMode: DebugMode.Color,
|
|
45
|
+
mode: CameraMode.Perspective,
|
|
46
|
+
size: 5,
|
|
38
47
|
}),
|
|
39
48
|
});
|
|
40
49
|
|
|
@@ -77,7 +86,10 @@ export function uploadCamera(
|
|
|
77
86
|
clearColor.g = color.g;
|
|
78
87
|
clearColor.b = color.b;
|
|
79
88
|
|
|
80
|
-
const proj =
|
|
89
|
+
const proj =
|
|
90
|
+
Camera.mode[eid] === CameraMode.Orthographic
|
|
91
|
+
? orthographic(Camera.size[eid], aspect, Camera.near[eid], Camera.far[eid])
|
|
92
|
+
: perspective(Camera.fov[eid], aspect, Camera.near[eid], Camera.far[eid]);
|
|
81
93
|
const world = WorldTransform.data.subarray(eid * 16, eid * 16 + 16);
|
|
82
94
|
const view = invert(world);
|
|
83
95
|
const viewProj = multiply(proj, view);
|
|
@@ -6,26 +6,6 @@ struct VertexOutput {
|
|
|
6
6
|
@location(0) uv: vec2f,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
@vertex
|
|
10
|
-
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
11
|
-
var positions = array<vec2f, 3>(
|
|
12
|
-
vec2f(-1.0, -1.0),
|
|
13
|
-
vec2f(3.0, -1.0),
|
|
14
|
-
vec2f(-1.0, 3.0)
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
let pos = positions[vertexIndex];
|
|
18
|
-
|
|
19
|
-
var output: VertexOutput;
|
|
20
|
-
output.position = vec4f(pos, 0.0, 1.0);
|
|
21
|
-
output.uv = (pos + 1.0) * 0.5;
|
|
22
|
-
output.uv.y = 1.0 - output.uv.y;
|
|
23
|
-
return output;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
27
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
28
|
-
|
|
29
9
|
struct Uniforms {
|
|
30
10
|
exposure: f32,
|
|
31
11
|
vignetteStrength: f32,
|
|
@@ -37,12 +17,31 @@ struct Uniforms {
|
|
|
37
17
|
_pad: u32,
|
|
38
18
|
}
|
|
39
19
|
|
|
20
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
21
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
40
22
|
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
41
23
|
|
|
42
24
|
const FLAG_TONEMAP: u32 = 1u;
|
|
43
25
|
const FLAG_FXAA: u32 = 2u;
|
|
44
26
|
const FLAG_VIGNETTE: u32 = 4u;
|
|
45
27
|
|
|
28
|
+
@vertex
|
|
29
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
30
|
+
var positions = array<vec2f, 3>(
|
|
31
|
+
vec2f(-1.0, -1.0),
|
|
32
|
+
vec2f(3.0, -1.0),
|
|
33
|
+
vec2f(-1.0, 3.0)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
let pos = positions[vertexIndex];
|
|
37
|
+
|
|
38
|
+
var output: VertexOutput;
|
|
39
|
+
output.position = vec4f(pos, 0.0, 1.0);
|
|
40
|
+
output.uv = (pos + 1.0) * 0.5;
|
|
41
|
+
output.uv.y = 1.0 - output.uv.y;
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
|
|
46
45
|
fn aces(x: vec3f) -> vec3f {
|
|
47
46
|
let a = 2.51;
|
|
48
47
|
let b = 0.03;
|
|
@@ -111,11 +110,11 @@ fn applyVignette(color: vec3f, uv: vec2f) -> vec3f {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
@fragment
|
|
114
|
-
fn fragmentMain(
|
|
115
|
-
var color = textureSample(inputTexture, inputSampler, uv).rgb;
|
|
113
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
114
|
+
var color = textureSample(inputTexture, inputSampler, input.uv).rgb;
|
|
116
115
|
|
|
117
116
|
if (uniforms.flags & FLAG_FXAA) != 0u {
|
|
118
|
-
color = applyFXAA(uv, color);
|
|
117
|
+
color = applyFXAA(input.uv, color);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
if (uniforms.flags & FLAG_TONEMAP) != 0u {
|
|
@@ -123,7 +122,7 @@ fn fragmentMain(@location(0) uv: vec2f) -> @location(0) vec4f {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
if (uniforms.flags & FLAG_VIGNETTE) != 0u {
|
|
126
|
-
color = applyVignette(color, uv);
|
|
125
|
+
color = applyVignette(color, input.uv);
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
return vec4f(color, 1.0);
|
|
@@ -66,6 +66,18 @@ export function perspective(fov: number, aspect: number, near: number, far: numb
|
|
|
66
66
|
]);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function orthographic(
|
|
70
|
+
size: number,
|
|
71
|
+
aspect: number,
|
|
72
|
+
near: number,
|
|
73
|
+
far: number
|
|
74
|
+
): Float32Array {
|
|
75
|
+
const lr = 1 / (size * aspect);
|
|
76
|
+
const bt = 1 / size;
|
|
77
|
+
const nf = 1 / (near - far);
|
|
78
|
+
return new Float32Array([lr, 0, 0, 0, 0, bt, 0, 0, 0, 0, nf, 0, 0, 0, near * nf, 1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
export function multiply(a: Float32Array, b: Float32Array): Float32Array {
|
|
70
82
|
const out = new Float32Array(16);
|
|
71
83
|
for (let i = 0; i < 4; i++) {
|
|
@@ -8,6 +8,6 @@ export {
|
|
|
8
8
|
type TweenOptions,
|
|
9
9
|
} from "./tween";
|
|
10
10
|
|
|
11
|
-
export { Sequence, SequenceState, Pause,
|
|
11
|
+
export { Sequence, SequenceState, Pause, finalizeSequences } from "./sequence";
|
|
12
12
|
|
|
13
13
|
export { EASING_FUNCTIONS, getEasing, getEasingIndex, type EasingFn } from "./easing";
|
|
@@ -64,6 +64,10 @@ function updateSequencePlayheads(state: State, dt: number): void {
|
|
|
64
64
|
if (Sequence.state[seqEid] !== SequenceState.PLAYING) continue;
|
|
65
65
|
|
|
66
66
|
const prevElapsed = Sequence.elapsed[seqEid] ?? 0;
|
|
67
|
+
|
|
68
|
+
if (prevElapsed === 0) {
|
|
69
|
+
computeTweenDelays(state, seqEid);
|
|
70
|
+
}
|
|
67
71
|
const elapsed = prevElapsed + dt;
|
|
68
72
|
Sequence.elapsed[seqEid] = elapsed;
|
|
69
73
|
|
|
@@ -135,7 +135,6 @@ export function finalizePendingTweens(state: State, context: ParseContext): void
|
|
|
135
135
|
if (!binding) continue;
|
|
136
136
|
|
|
137
137
|
state.addRelation(pending.tweenEid, TweenTarget, targetEid);
|
|
138
|
-
|
|
139
138
|
Tween.to[pending.tweenEid] = parseFloat(pending.to);
|
|
140
139
|
}
|
|
141
140
|
pendingXmlTweens = [];
|