@aethervtt/shard-render 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ablation.d.ts +74 -0
- package/dist/ablation.js +117 -0
- package/dist/assets.d.ts +3 -0
- package/dist/assets.js +43 -2
- package/dist/atmosphere-nodes.d.ts +9 -0
- package/dist/atmosphere-nodes.js +157 -30
- package/dist/atmosphere-plugin.js +8 -2
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/atmosphere.d.ts +16 -0
- package/dist/baseline/atmosphere.js +301 -0
- package/dist/baseline/data-texture.d.ts +36 -0
- package/dist/baseline/data-texture.js +181 -0
- package/dist/baseline/environment.d.ts +14 -0
- package/dist/baseline/environment.js +368 -0
- package/dist/baseline/exposure.d.ts +25 -0
- package/dist/baseline/exposure.js +133 -0
- package/dist/baseline/lights.d.ts +33 -0
- package/dist/baseline/lights.js +133 -0
- package/dist/baseline/textures.d.ts +97 -0
- package/dist/baseline/textures.js +203 -0
- package/dist/camera.d.ts +2 -0
- package/dist/camera.js +4 -0
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- package/dist/culling-shaders.js +21 -2
- package/dist/culling.d.ts +11 -2
- package/dist/culling.js +44 -18
- package/dist/cutaway-plugin.d.ts +48 -0
- package/dist/cutaway-plugin.js +364 -0
- package/dist/cutaway.d.ts +27 -0
- package/dist/cutaway.js +49 -0
- package/dist/data-store.d.ts +69 -0
- package/dist/data-store.js +105 -0
- package/dist/debug-shaders.js +17 -3
- package/dist/debug-views.js +4 -2
- package/dist/decals-plugin.d.ts +5 -0
- package/dist/decals-plugin.js +54 -0
- package/dist/decals.d.ts +69 -0
- package/dist/decals.js +326 -0
- package/dist/deferred-shaders.js +2 -0
- package/dist/deferred.js +28 -4
- package/dist/deform.d.ts +5 -4
- package/dist/deform.js +4 -9
- package/dist/describe-plugin.d.ts +4 -2
- package/dist/describe-plugin.js +75 -3
- package/dist/display-nodes.js +37 -13
- package/dist/dynamic-resolution.d.ts +2 -0
- package/dist/dynamic-resolution.js +6 -1
- package/dist/environment-plugin.js +17 -2
- package/dist/environment-state.d.ts +7 -3
- package/dist/environment-state.js +20 -9
- package/dist/environment.js +7 -1
- package/dist/features.d.ts +49 -0
- package/dist/features.js +106 -0
- package/dist/foliage-budget.d.ts +64 -0
- package/dist/foliage-budget.js +108 -0
- package/dist/foliage-path.d.ts +19 -0
- package/dist/foliage-path.js +7 -0
- package/dist/foliage-shaders.d.ts +11 -0
- package/dist/foliage-shaders.js +504 -0
- package/dist/foliage.d.ts +194 -0
- package/dist/foliage.js +1027 -0
- package/dist/forward.d.ts +38 -2
- package/dist/forward.js +608 -186
- package/dist/fxaa.js +12 -2
- package/dist/gizmos-plugin.js +7 -0
- package/dist/gizmos.d.ts +3 -3
- package/dist/gizmos.js +11 -13
- package/dist/gpu-assets.d.ts +58 -4
- package/dist/gpu-assets.js +191 -8
- package/dist/graph.d.ts +45 -0
- package/dist/graph.js +169 -19
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +40 -11
- package/dist/index.js +343 -9
- package/dist/instances.d.ts +119 -8
- package/dist/instances.js +387 -78
- package/dist/interior-plugin.d.ts +121 -0
- package/dist/interior-plugin.js +465 -0
- package/dist/interior.d.ts +24 -0
- package/dist/interior.js +13 -0
- package/dist/layers.d.ts +35 -0
- package/dist/layers.js +49 -0
- package/dist/lens-plugin.d.ts +43 -0
- package/dist/lens-plugin.js +304 -0
- package/dist/lens.d.ts +41 -0
- package/dist/lens.js +99 -0
- package/dist/lights.d.ts +50 -5
- package/dist/lights.js +101 -22
- package/dist/material-pipelines.d.ts +37 -4
- package/dist/material-pipelines.js +102 -5
- package/dist/materials.d.ts +23 -0
- package/dist/materials.js +44 -1
- package/dist/outline-plugin.d.ts +48 -0
- package/dist/outline-plugin.js +529 -0
- package/dist/outline.d.ts +15 -0
- package/dist/outline.js +42 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +6 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/picking.d.ts +17 -0
- package/dist/picking.js +40 -11
- package/dist/pixel-perfect-plugin.js +10 -2
- package/dist/plugin.d.ts +69 -7
- package/dist/plugin.js +223 -25
- package/dist/post-common.d.ts +22 -2
- package/dist/post-common.js +82 -2
- package/dist/post-nodes.d.ts +7 -1
- package/dist/post-nodes.js +235 -111
- package/dist/post-plugin.js +5 -3
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +85 -62
- package/dist/post.js +11 -1
- package/dist/projection.d.ts +24 -0
- package/dist/projection.js +87 -0
- package/dist/readback.d.ts +1 -0
- package/dist/readback.js +1 -0
- package/dist/render-scale.js +1 -0
- package/dist/ribbons-plugin.d.ts +5 -0
- package/dist/ribbons-plugin.js +59 -0
- package/dist/ribbons.d.ts +44 -0
- package/dist/ribbons.js +288 -0
- package/dist/scenario.d.ts +70 -0
- package/dist/scenario.js +125 -0
- package/dist/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +189 -34
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +27 -2
- package/dist/shadows.js +100 -20
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +18 -0
- package/dist/stats.d.ts +45 -1
- package/dist/stats.js +91 -3
- package/dist/surface-model.d.ts +57 -0
- package/dist/surface-model.js +289 -0
- package/dist/surface-shaders.d.ts +3 -0
- package/dist/surface-shaders.js +160 -0
- package/dist/surface.d.ts +40 -0
- package/dist/surface.js +86 -0
- package/dist/target.d.ts +1 -13
- package/dist/target.js +1 -26
- package/dist/testing.d.ts +11 -0
- package/dist/testing.js +39 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +18 -1
- package/dist/timer.js +73 -20
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -0
- package/dist/vfx-draw.d.ts +37 -0
- package/dist/vfx-draw.js +291 -0
- package/dist/vfx-state.d.ts +42 -0
- package/dist/vfx-state.js +81 -0
- package/dist/vfx.d.ts +69 -0
- package/dist/vfx.js +103 -0
- package/dist/view-visibility-plugin.d.ts +61 -0
- package/dist/view-visibility-plugin.js +267 -0
- package/dist/view-visibility.d.ts +28 -0
- package/dist/view-visibility.js +46 -0
- package/dist/view.d.ts +29 -1
- package/dist/view.js +57 -34
- package/package.json +23 -11
package/dist/testing.js
CHANGED
|
@@ -3,6 +3,10 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { deflateSync } from "node:zlib";
|
|
4
4
|
import { Culler } from "./culling.js";
|
|
5
5
|
import { captureView, Gpu, Shaders } from "./plugin.js";
|
|
6
|
+
import {
|
|
7
|
+
expectScenario,
|
|
8
|
+
measureScenario
|
|
9
|
+
} from "./scenario.js";
|
|
6
10
|
async function settle(app, frames = 40) {
|
|
7
11
|
const gpu = app.world.resource(Gpu);
|
|
8
12
|
for (let i = 0; i < frames; i++) {
|
|
@@ -116,11 +120,45 @@ function compareGolden(dir, name, image) {
|
|
|
116
120
|
}
|
|
117
121
|
return { mean: sum / expected.length, max, written: false };
|
|
118
122
|
}
|
|
123
|
+
function watchBaseline(gpu) {
|
|
124
|
+
const found = { computePasses: 0, renderStorage: [] };
|
|
125
|
+
const device = gpu.device;
|
|
126
|
+
const createEncoder = device.createCommandEncoder.bind(device);
|
|
127
|
+
device.createCommandEncoder = (descriptor) => {
|
|
128
|
+
const encoder = createEncoder(descriptor);
|
|
129
|
+
const begin = encoder.beginComputePass.bind(encoder);
|
|
130
|
+
encoder.beginComputePass = (d) => {
|
|
131
|
+
found.computePasses++;
|
|
132
|
+
return begin(d);
|
|
133
|
+
};
|
|
134
|
+
return encoder;
|
|
135
|
+
};
|
|
136
|
+
const createLayout = device.createBindGroupLayout.bind(device);
|
|
137
|
+
device.createBindGroupLayout = (descriptor) => {
|
|
138
|
+
for (const e of descriptor.entries) {
|
|
139
|
+
const render = e.visibility & (GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
|
|
140
|
+
const t = e.buffer?.type;
|
|
141
|
+
if (render && (t === "storage" || t === "read-only-storage"))
|
|
142
|
+
found.renderStorage.push(`${descriptor.label ?? "?"}#${e.binding}`);
|
|
143
|
+
}
|
|
144
|
+
return createLayout(descriptor);
|
|
145
|
+
};
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
function meanDifference(a, b) {
|
|
149
|
+
let sum = 0;
|
|
150
|
+
for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
|
|
151
|
+
return sum / a.length;
|
|
152
|
+
}
|
|
119
153
|
export {
|
|
120
154
|
compareGolden,
|
|
121
155
|
comparePng,
|
|
156
|
+
expectScenario,
|
|
157
|
+
meanDifference,
|
|
158
|
+
measureScenario,
|
|
122
159
|
pixel,
|
|
123
160
|
pngBytes,
|
|
124
161
|
renderView,
|
|
125
|
-
settle
|
|
162
|
+
settle,
|
|
163
|
+
watchBaseline
|
|
126
164
|
};
|
package/dist/tier.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
/**
|
|
3
|
+
* A texture's binding dimension, fixed at creation on baseline (compatibility mode and WebGL2):
|
|
4
|
+
* spread into the texture descriptor. Nothing on the full tier.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bindingDimension(gpu: GpuContext, dimension: GPUTextureViewDimension): {
|
|
7
|
+
textureBindingViewDimension?: GPUTextureViewDimension;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The layout entry for a depth texture read without comparison. On baseline the shader rewrite
|
|
11
|
+
* reads it as a float texture (naga can't load from depth), so it binds as unfilterable float.
|
|
12
|
+
*/
|
|
13
|
+
export declare function depthReadEntry(gpu: GpuContext, binding: number, visibility: GPUShaderStageFlags, viewDimension?: GPUTextureViewDimension): GPUBindGroupLayoutEntry;
|
package/dist/tier.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function bindingDimension(gpu, dimension) {
|
|
2
|
+
return gpu.tier === "baseline" ? { textureBindingViewDimension: dimension } : {};
|
|
3
|
+
}
|
|
4
|
+
function depthReadEntry(gpu, binding, visibility, viewDimension = "2d") {
|
|
5
|
+
return {
|
|
6
|
+
binding,
|
|
7
|
+
visibility,
|
|
8
|
+
texture: gpu.tier === "baseline" ? { sampleType: "unfilterable-float", viewDimension } : viewDimension === "2d" ? { sampleType: "depth" } : { sampleType: "depth", viewDimension }
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
bindingDimension,
|
|
13
|
+
depthReadEntry
|
|
14
|
+
};
|
package/dist/timer.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ export declare class GpuTimer {
|
|
|
11
11
|
frameMs: number;
|
|
12
12
|
/** Frames measured so far: changes when `frameMs` does. */
|
|
13
13
|
frameSamples: number;
|
|
14
|
+
/**
|
|
15
|
+
* Every pass time so far was a whole number of 100 µs: the browser quantizes timestamps
|
|
16
|
+
* (0074's `gpuQuantized`). Decided after a few dozen passes.
|
|
17
|
+
*/
|
|
18
|
+
quantized: boolean;
|
|
19
|
+
private passTimes;
|
|
20
|
+
private unquantized;
|
|
21
|
+
/** Each query slot's stamps at the last readback: a slot that still holds them is left over. */
|
|
22
|
+
private readonly lastStamps;
|
|
14
23
|
private querySet;
|
|
15
24
|
private resolveBuffer;
|
|
16
25
|
private readonly readbacks;
|
|
@@ -18,11 +27,19 @@ export declare class GpuTimer {
|
|
|
18
27
|
private generation;
|
|
19
28
|
private readonly gpu;
|
|
20
29
|
constructor(gpu: GpuContext);
|
|
30
|
+
/** Destroys the query set; buffers are released with the rest of the app's (0052). */
|
|
31
|
+
destroy(): void;
|
|
21
32
|
beginFrame(): void;
|
|
22
33
|
/** Timestamp writes for the next pass, or undefined when timing is off or full. */
|
|
23
34
|
allocate(name: string): GPURenderPassTimestampWrites | undefined;
|
|
24
35
|
private pendingCopy;
|
|
36
|
+
/** Pass `i`'s slot still holds the stamps it had at the last readback: the GPU skipped it. */
|
|
37
|
+
private stale;
|
|
25
38
|
resolve(encoder: GPUCommandEncoder): void;
|
|
26
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Call after submit. Records timings when the copy lands: aggregates per name, and (during a
|
|
41
|
+
* capture) GPU-track spans placed from this frame's submit time, since WebGPU's clock isn't the
|
|
42
|
+
* CPU's (0074).
|
|
43
|
+
*/
|
|
27
44
|
readback(world: World): void;
|
|
28
45
|
}
|
package/dist/timer.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
|
-
import { ProfilerResource } from "@aethervtt/shard-core";
|
|
1
|
+
import { ProfilerResource, TRACK } from "@aethervtt/shard-core";
|
|
2
2
|
const MAX_PASSES = 96;
|
|
3
|
-
const STALE_NS = 1000000000n;
|
|
4
3
|
const SPANS = [
|
|
5
4
|
["gpu:span/post", (n) => n.startsWith("post/") || n === "tonemap"],
|
|
6
5
|
["gpu:span/ssao", (n) => n === "ssao" || n.startsWith("ssao/")]
|
|
7
6
|
];
|
|
8
7
|
const READBACK_BUFFERS = 3;
|
|
8
|
+
const QUANTUM_NS = 100000n;
|
|
9
|
+
const QUANTIZED_AFTER = 32;
|
|
10
|
+
const gpuNames = /* @__PURE__ */ new Map();
|
|
11
|
+
function gpuName(node) {
|
|
12
|
+
let name = gpuNames.get(node);
|
|
13
|
+
if (name === void 0) {
|
|
14
|
+
name = `gpu:${node}`;
|
|
15
|
+
gpuNames.set(node, name);
|
|
16
|
+
}
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
9
19
|
class GpuTimer {
|
|
10
20
|
enabled;
|
|
11
21
|
/** The latest frame's GPU span in ms (first pass start to last pass end), once one landed. */
|
|
12
22
|
frameMs = 0;
|
|
13
23
|
/** Frames measured so far: changes when `frameMs` does. */
|
|
14
24
|
frameSamples = 0;
|
|
25
|
+
/**
|
|
26
|
+
* Every pass time so far was a whole number of 100 µs: the browser quantizes timestamps
|
|
27
|
+
* (0074's `gpuQuantized`). Decided after a few dozen passes.
|
|
28
|
+
*/
|
|
29
|
+
quantized = false;
|
|
30
|
+
passTimes = 0;
|
|
31
|
+
unquantized = false;
|
|
32
|
+
/** Each query slot's stamps at the last readback: a slot that still holds them is left over. */
|
|
33
|
+
lastStamps = new BigUint64Array(MAX_PASSES * 2);
|
|
15
34
|
querySet;
|
|
16
35
|
resolveBuffer;
|
|
17
36
|
readbacks = [];
|
|
@@ -22,10 +41,17 @@ class GpuTimer {
|
|
|
22
41
|
this.gpu = gpu;
|
|
23
42
|
this.enabled = gpu.features.has("timestamp-query");
|
|
24
43
|
}
|
|
44
|
+
/** Destroys the query set; buffers are released with the rest of the app's (0052). */
|
|
45
|
+
destroy() {
|
|
46
|
+
this.querySet?.destroy();
|
|
47
|
+
this.querySet = void 0;
|
|
48
|
+
this.generation = this.gpu.generation;
|
|
49
|
+
}
|
|
25
50
|
beginFrame() {
|
|
26
51
|
this.names = [];
|
|
27
52
|
if (!this.enabled || this.generation === this.gpu.generation) return;
|
|
28
53
|
this.generation = this.gpu.generation;
|
|
54
|
+
this.lastStamps.fill(0n);
|
|
29
55
|
const device = this.gpu.device;
|
|
30
56
|
this.querySet = device.createQuerySet({
|
|
31
57
|
label: "gpu-timer",
|
|
@@ -46,7 +72,9 @@ class GpuTimer {
|
|
|
46
72
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
47
73
|
}),
|
|
48
74
|
busy: false,
|
|
49
|
-
names: []
|
|
75
|
+
names: [],
|
|
76
|
+
frame: 0,
|
|
77
|
+
submitAt: 0
|
|
50
78
|
});
|
|
51
79
|
}
|
|
52
80
|
}
|
|
@@ -62,6 +90,11 @@ class GpuTimer {
|
|
|
62
90
|
};
|
|
63
91
|
}
|
|
64
92
|
pendingCopy;
|
|
93
|
+
/** Pass `i`'s slot still holds the stamps it had at the last readback: the GPU skipped it. */
|
|
94
|
+
stale(times, i) {
|
|
95
|
+
const last = this.lastStamps;
|
|
96
|
+
return times[i * 2] === last[i * 2] && times[i * 2 + 1] === last[i * 2 + 1];
|
|
97
|
+
}
|
|
65
98
|
resolve(encoder) {
|
|
66
99
|
this.pendingCopy = void 0;
|
|
67
100
|
if (!this.enabled || this.names.length === 0) return;
|
|
@@ -74,36 +107,54 @@ class GpuTimer {
|
|
|
74
107
|
target.names = this.names;
|
|
75
108
|
this.pendingCopy = target;
|
|
76
109
|
}
|
|
77
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Call after submit. Records timings when the copy lands: aggregates per name, and (during a
|
|
112
|
+
* capture) GPU-track spans placed from this frame's submit time, since WebGPU's clock isn't the
|
|
113
|
+
* CPU's (0074).
|
|
114
|
+
*/
|
|
78
115
|
readback(world) {
|
|
116
|
+
const profiler = world.tryResource(ProfilerResource);
|
|
117
|
+
if (profiler) profiler.gpu.status = this.enabled ? "available" : "unavailable";
|
|
79
118
|
const target = this.pendingCopy;
|
|
80
119
|
if (!target) return;
|
|
81
120
|
this.pendingCopy = void 0;
|
|
121
|
+
target.frame = profiler?.frame ?? 0;
|
|
122
|
+
target.submitAt = profiler ? profiler.now() : 0;
|
|
82
123
|
const generation = this.generation;
|
|
83
124
|
target.buffer.mapAsync(GPUMapMode.READ).then(
|
|
84
125
|
() => {
|
|
85
126
|
if (generation !== this.gpu.generation) return;
|
|
86
127
|
const times = new BigUint64Array(target.buffer.getMappedRange());
|
|
87
|
-
const profiler = world.tryResource(ProfilerResource);
|
|
88
128
|
const totals = /* @__PURE__ */ new Map();
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < target.names.length; i++) {
|
|
91
|
-
const end = times[i * 2 + 1];
|
|
92
|
-
if (end > latest) latest = end;
|
|
93
|
-
}
|
|
94
|
-
const stale = latest - STALE_NS;
|
|
129
|
+
const count = target.names.length * 2;
|
|
95
130
|
let first = 0n;
|
|
96
|
-
let
|
|
131
|
+
let lastEnd = 0n;
|
|
97
132
|
for (let i = 0; i < target.names.length; i++) {
|
|
98
133
|
const begin = times[i * 2];
|
|
99
134
|
const end = times[i * 2 + 1];
|
|
100
|
-
if (begin === 0n || end < begin ||
|
|
135
|
+
if (begin === 0n || end < begin || this.stale(times, i)) continue;
|
|
101
136
|
const name = target.names[i];
|
|
102
137
|
totals.set(name, (totals.get(name) ?? 0) + Number(end - begin) / 1e6);
|
|
103
138
|
if (first === 0n || begin < first) first = begin;
|
|
104
|
-
if (end >
|
|
139
|
+
if (end > lastEnd) lastEnd = end;
|
|
140
|
+
if (end > begin) {
|
|
141
|
+
if ((end - begin) % QUANTUM_NS !== 0n) this.unquantized = true;
|
|
142
|
+
this.passTimes++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (this.passTimes >= QUANTIZED_AFTER) this.quantized = !this.unquantized;
|
|
146
|
+
if (profiler) profiler.gpu.quantized = this.quantized;
|
|
147
|
+
for (const [name, ms] of totals) profiler?.sample(gpuName(name), ms);
|
|
148
|
+
const at = (stamp) => target.submitAt + Number(stamp - first) / 1e6;
|
|
149
|
+
if (profiler?.capturing && first !== 0n) {
|
|
150
|
+
for (let i = 0; i < target.names.length; i++) {
|
|
151
|
+
const begin = times[i * 2];
|
|
152
|
+
const end = times[i * 2 + 1];
|
|
153
|
+
if (begin === 0n || end < begin || this.stale(times, i)) continue;
|
|
154
|
+
const ms = Number(end - begin) / 1e6;
|
|
155
|
+
profiler.event(gpuName(target.names[i]), TRACK.gpu, at(begin), ms, target.frame);
|
|
156
|
+
}
|
|
105
157
|
}
|
|
106
|
-
for (const [name, ms] of totals) profiler?.record(`gpu:${name}`, ms);
|
|
107
158
|
for (let g = 0; g < SPANS.length; g++) {
|
|
108
159
|
const [label, match] = SPANS[g];
|
|
109
160
|
let a = 0n;
|
|
@@ -111,17 +162,19 @@ class GpuTimer {
|
|
|
111
162
|
for (let i = 0; i < target.names.length; i++) {
|
|
112
163
|
const begin = times[i * 2];
|
|
113
164
|
const end = times[i * 2 + 1];
|
|
114
|
-
if (begin === 0n || end < begin ||
|
|
165
|
+
if (begin === 0n || end < begin || this.stale(times, i) || !match(target.names[i]))
|
|
166
|
+
continue;
|
|
115
167
|
if (a === 0n || begin < a) a = begin;
|
|
116
168
|
if (end > b) b = end;
|
|
117
169
|
}
|
|
118
|
-
if (b > a) profiler?.record(label, Number(b - a) / 1e6);
|
|
170
|
+
if (b > a) profiler?.record(label, Number(b - a) / 1e6, TRACK.gpu, at(a), target.frame);
|
|
119
171
|
}
|
|
120
|
-
if (
|
|
121
|
-
this.frameMs = Number(
|
|
172
|
+
if (first !== 0n) {
|
|
173
|
+
this.frameMs = Number(lastEnd - first) / 1e6;
|
|
122
174
|
this.frameSamples++;
|
|
123
|
-
profiler?.record("gpu:frame", this.frameMs);
|
|
175
|
+
profiler?.record("gpu:frame", this.frameMs, TRACK.gpu, target.submitAt, target.frame);
|
|
124
176
|
}
|
|
177
|
+
for (let k = 0; k < count; k++) this.lastStamps[k] = times[k];
|
|
125
178
|
target.buffer.unmap();
|
|
126
179
|
target.busy = false;
|
|
127
180
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { App } from '@aethervtt/shard-runtime';
|
|
3
|
+
/** A kind of manifest entry, and how to show the variants it names. */
|
|
4
|
+
export interface ShaderVariantSource {
|
|
5
|
+
/** The entry's key: `{ "dice": … }` is run by the source named `dice`. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
/**
|
|
9
|
+
* Shows every variant `entry` names for at least a frame, stepping the app until their pipelines
|
|
10
|
+
* are made (`app.update`, then `Shaders.whenIdle` and `gpu.pipelines.whenIdle`). Returns how many
|
|
11
|
+
* variants it showed.
|
|
12
|
+
*/
|
|
13
|
+
show(app: App, entry: unknown): Promise<number>;
|
|
14
|
+
}
|
|
15
|
+
export declare const ShaderVariantSources: import("@aethervtt/shard-core").ResourceDef<Map<string, ShaderVariantSource>>;
|
|
16
|
+
/** Registers a manifest entry kind. Plugins call it in `build`. */
|
|
17
|
+
export declare function addShaderVariantSource(world: World, source: ShaderVariantSource): void;
|
|
18
|
+
/** The manifest's shape. */
|
|
19
|
+
export interface ShaderVariantManifest {
|
|
20
|
+
variants: Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
/** Runs one manifest entry through its source. Throws `render/unknown-variant-source` for a key no plugin registered. */
|
|
23
|
+
export declare function showVariants(app: App, entry: Record<string, unknown>): Promise<number>;
|
|
24
|
+
/** A material asset's path (or guid) and parameter overrides. */
|
|
25
|
+
export interface MaterialVariantEntry {
|
|
26
|
+
path?: string;
|
|
27
|
+
guid?: string;
|
|
28
|
+
params?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Shows what `spawn` makes to a camera of its own (an offscreen target, a shadowed sun), stepping
|
|
32
|
+
* the app until every pipeline it asked for is made, then takes it all away again. For variant
|
|
33
|
+
* sources: what's drawn is what a bake records.
|
|
34
|
+
*/
|
|
35
|
+
export declare function showToCamera(app: App, spawn: (at: [number, number, number]) => Entity[]): Promise<void>;
|
|
36
|
+
/** `{ "material": { "path": …, "params": … } }`: a material asset, with its parameters set, on a cube. */
|
|
37
|
+
export declare const materialVariants: ShaderVariantSource;
|
package/dist/variants.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
defineResource,
|
|
4
|
+
ShardError
|
|
5
|
+
} from "@aethervtt/shard-core";
|
|
6
|
+
import { cube } from "@aethervtt/shard-mesh";
|
|
7
|
+
import { lookAt, Transform } from "@aethervtt/shard-transform";
|
|
8
|
+
import { Materials, Meshes, RenderTargets } from "./assets.js";
|
|
9
|
+
import { Camera3d } from "./camera.js";
|
|
10
|
+
import { Mesh3d, MeshMaterial } from "./instances.js";
|
|
11
|
+
import { DirectionalLight } from "./lights.js";
|
|
12
|
+
import { Gpu, Shaders } from "./plugin.js";
|
|
13
|
+
import { OffscreenTarget } from "./target.js";
|
|
14
|
+
const ShaderVariantSources = defineResource(
|
|
15
|
+
"render/ShaderVariantSources",
|
|
16
|
+
{
|
|
17
|
+
description: "Kinds of shaders.variants.json entries (0064) and how to show their variants, for `shard shaders bake`.",
|
|
18
|
+
init: () => /* @__PURE__ */ new Map()
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
function addShaderVariantSource(world, source) {
|
|
22
|
+
world.initResource(ShaderVariantSources).set(source.name, source);
|
|
23
|
+
}
|
|
24
|
+
function showVariants(app, entry) {
|
|
25
|
+
const keys = Object.keys(entry);
|
|
26
|
+
if (keys.length !== 1) {
|
|
27
|
+
throw new ShardError(
|
|
28
|
+
"render/variant-entry",
|
|
29
|
+
`A shaders.variants.json entry has one key, its source: got ${keys.length ? keys.join(", ") : "none"}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const name = keys[0];
|
|
33
|
+
const source = app.world.tryResource(ShaderVariantSources)?.get(name);
|
|
34
|
+
if (!source) {
|
|
35
|
+
const known = [...app.world.tryResource(ShaderVariantSources)?.keys() ?? []];
|
|
36
|
+
throw new ShardError("render/unknown-variant-source", `No shader variant source "${name}"`, {
|
|
37
|
+
hint: `Known: ${known.join(", ") || "none"}. Plugins register theirs with addShaderVariantSource.`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return source.show(app, entry[name]);
|
|
41
|
+
}
|
|
42
|
+
const STAGE = [5e4, 0, 5e4];
|
|
43
|
+
async function showToCamera(app, spawn) {
|
|
44
|
+
const world = app.world;
|
|
45
|
+
const gpu = world.resource(Gpu);
|
|
46
|
+
const target = new OffscreenTarget(gpu, { label: "shader-variants", width: 64, height: 64 });
|
|
47
|
+
const ref = world.resource(RenderTargets).add(target, `render:variants/${STAGE.join(",")}`);
|
|
48
|
+
const eye = [STAGE[0], STAGE[1] + 2, STAGE[2] + 4];
|
|
49
|
+
const made = [
|
|
50
|
+
world.spawn(
|
|
51
|
+
[Camera3d, { target: ref, order: 90 }],
|
|
52
|
+
[Transform, { translation: eye, rotation: lookAt(eye, STAGE) }]
|
|
53
|
+
),
|
|
54
|
+
world.spawn(
|
|
55
|
+
[DirectionalLight, { shadows: true }],
|
|
56
|
+
[Transform, { translation: STAGE, rotation: lookAt([0, 0, 0], [-0.4, -1, -0.3]) }]
|
|
57
|
+
),
|
|
58
|
+
...spawn(STAGE)
|
|
59
|
+
];
|
|
60
|
+
try {
|
|
61
|
+
for (let i = 0; i < 8; i++) {
|
|
62
|
+
app.update(1 / 60);
|
|
63
|
+
await world.resource(Shaders).whenIdle();
|
|
64
|
+
await gpu.pipelines.whenIdle();
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
for (const e of made) if (world.isAlive(e)) world.despawn(e);
|
|
68
|
+
app.update(1 / 60);
|
|
69
|
+
target.destroy?.();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const materialVariants = {
|
|
73
|
+
name: "material",
|
|
74
|
+
description: "A material asset by path or guid, with parameter overrides: { path, params? }.",
|
|
75
|
+
async show(app, entry) {
|
|
76
|
+
const list = Array.isArray(entry) ? entry : [entry];
|
|
77
|
+
const world = app.world;
|
|
78
|
+
for (const m of list) {
|
|
79
|
+
const ref = m.guid ? { guid: m.guid } : { path: m.path };
|
|
80
|
+
await assetServer(world).load(ref);
|
|
81
|
+
const material = world.resource(Materials).get(ref);
|
|
82
|
+
if (!material) {
|
|
83
|
+
throw new ShardError("render/variant-material", `No material ${m.guid ?? m.path}`);
|
|
84
|
+
}
|
|
85
|
+
if (m.params) material.set(m.params);
|
|
86
|
+
const mesh = world.resource(Meshes).add(cube({ size: 1 }));
|
|
87
|
+
await showToCamera(app, (at) => [
|
|
88
|
+
world.spawn(
|
|
89
|
+
[Mesh3d, { mesh }],
|
|
90
|
+
[MeshMaterial, { material: ref }],
|
|
91
|
+
[Transform, { translation: at }]
|
|
92
|
+
)
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
return list.length;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
export {
|
|
99
|
+
ShaderVariantSources,
|
|
100
|
+
addShaderVariantSource,
|
|
101
|
+
materialVariants,
|
|
102
|
+
showToCamera,
|
|
103
|
+
showVariants
|
|
104
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { World } from '@aethervtt/shard-core';
|
|
2
|
+
import { GpuBuffer } from '@aethervtt/shard-gpu';
|
|
3
|
+
import { type NodeDescriptor } from './graph.js';
|
|
4
|
+
import type { VfxRecord } from './vfx-state.js';
|
|
5
|
+
export declare const VFX_SHADERS: {
|
|
6
|
+
'shard::vfx::ribbon': string;
|
|
7
|
+
};
|
|
8
|
+
interface DrawRecord {
|
|
9
|
+
source: VfxRecord;
|
|
10
|
+
view: string;
|
|
11
|
+
vertices: GpuBuffer;
|
|
12
|
+
uniform: GpuBuffer;
|
|
13
|
+
data: Float32Array;
|
|
14
|
+
group: GPUBindGroup | undefined;
|
|
15
|
+
texture: GPUTextureView | undefined;
|
|
16
|
+
version: number;
|
|
17
|
+
generation: number;
|
|
18
|
+
}
|
|
19
|
+
/** Per-world, per-view uniforms: two cameras never overwrite one another before submit. */
|
|
20
|
+
export declare class VfxRenderer {
|
|
21
|
+
readonly draws: DrawRecord[];
|
|
22
|
+
private generation;
|
|
23
|
+
private layout;
|
|
24
|
+
private pipelines;
|
|
25
|
+
private white;
|
|
26
|
+
private whiteView;
|
|
27
|
+
private sampler;
|
|
28
|
+
private readonly decal;
|
|
29
|
+
private readonly records;
|
|
30
|
+
constructor(records: VfxRecord[], decal: boolean);
|
|
31
|
+
dispose(): void;
|
|
32
|
+
prune(world: World): void;
|
|
33
|
+
private init;
|
|
34
|
+
node(): NodeDescriptor;
|
|
35
|
+
private draw;
|
|
36
|
+
}
|
|
37
|
+
export {};
|