@multiplekex/shallot 0.1.4 → 0.1.5
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/orbit/index.ts +16 -2
- package/src/standard/compute/graph.ts +5 -0
- package/src/standard/compute/index.ts +3 -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/render/camera.ts +14 -2
- package/src/standard/render/scene.ts +29 -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.5",
|
|
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) {
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
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;
|
|
@@ -162,4 +163,8 @@ export class ComputeGraph {
|
|
|
162
163
|
}
|
|
163
164
|
return this._plan;
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
inspect(): GraphInspection {
|
|
168
|
+
return inspectGraph(Array.from(this.nodes.values()));
|
|
169
|
+
}
|
|
165
170
|
}
|
|
@@ -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
|
|
|
@@ -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[] = ["render", "overlay", "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 ?? "render",
|
|
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 ?? "render";
|
|
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
|
+
}
|
|
@@ -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);
|
|
@@ -66,6 +66,35 @@ 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([
|
|
79
|
+
lr,
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
0,
|
|
83
|
+
0,
|
|
84
|
+
bt,
|
|
85
|
+
0,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
nf,
|
|
90
|
+
0,
|
|
91
|
+
0,
|
|
92
|
+
0,
|
|
93
|
+
near * nf,
|
|
94
|
+
1,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
export function multiply(a: Float32Array, b: Float32Array): Float32Array {
|
|
70
99
|
const out = new Float32Array(16);
|
|
71
100
|
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 = [];
|