@aethervtt/shard-render 0.1.0 → 0.2.1
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/atmosphere-nodes.js +100 -12
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/exposure.d.ts +1 -1
- package/dist/baseline/exposure.js +18 -5
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- 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.js +4 -1
- package/dist/display-nodes.js +24 -11
- 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.js +26 -2
- package/dist/graph.d.ts +34 -0
- package/dist/graph.js +139 -14
- package/dist/index.d.ts +11 -1
- package/dist/index.js +57 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +5 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/plugin.d.ts +11 -1
- package/dist/plugin.js +33 -2
- package/dist/post-common.d.ts +20 -2
- package/dist/post-common.js +79 -2
- package/dist/post-nodes.d.ts +5 -0
- package/dist/post-nodes.js +136 -104
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +41 -63
- package/dist/post.js +11 -1
- 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/shaders.js +58 -5
- package/dist/shadows.d.ts +2 -1
- package/dist/shadows.js +3 -1
- package/dist/standard.js +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +6 -0
- package/dist/timer.d.ts +16 -1
- package/dist/timer.js +66 -19
- 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/package.json +19 -11
package/dist/overlays.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DirectionalLight, Lights } from "./lights.js";
|
|
16
16
|
import { defineOverlay, OVERLAYS, registeredOverlays } from "./overlay-registry.js";
|
|
17
17
|
const extraOverlays = registeredOverlays();
|
|
18
|
+
import { drawPerfOverlay } from "./perf-overlay.js";
|
|
18
19
|
import { Views } from "./plugin.js";
|
|
19
20
|
import { Cameras, cameraOf } from "./view.js";
|
|
20
21
|
import {
|
|
@@ -35,7 +36,7 @@ function scenePath(world, entity) {
|
|
|
35
36
|
}
|
|
36
37
|
const DebugOverlays = defineResource("render/DebugOverlays", {
|
|
37
38
|
hostWritable: true,
|
|
38
|
-
description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, and
|
|
39
|
+
description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, entity labels, and perf (frame time, top spans, GPU frame).",
|
|
39
40
|
init: () => ({
|
|
40
41
|
bounds: false,
|
|
41
42
|
lights: false,
|
|
@@ -44,6 +45,7 @@ const DebugOverlays = defineResource("render/DebugOverlays", {
|
|
|
44
45
|
normals: false,
|
|
45
46
|
axes: false,
|
|
46
47
|
labels: false,
|
|
48
|
+
perf: false,
|
|
47
49
|
extra: {},
|
|
48
50
|
filter: { components: [], path: "" },
|
|
49
51
|
name: scenePath
|
|
@@ -227,7 +229,7 @@ const drawOverlays = defineSystem({
|
|
|
227
229
|
g.overlay.clear();
|
|
228
230
|
let extra = false;
|
|
229
231
|
for (const name in o.extra) if (o.extra[name] && extraOverlays.has(name)) extra = true;
|
|
230
|
-
if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || extra))
|
|
232
|
+
if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || o.perf || extra))
|
|
231
233
|
return;
|
|
232
234
|
g.beginOverlays();
|
|
233
235
|
try {
|
|
@@ -250,6 +252,7 @@ function draw(world, o, g) {
|
|
|
250
252
|
}
|
|
251
253
|
if (o.lights) drawLights(world, o, g, defs);
|
|
252
254
|
const primary = primaryCamera(world);
|
|
255
|
+
if (o.perf) drawPerfOverlay(world, g, primary);
|
|
253
256
|
if (o.cameras) {
|
|
254
257
|
for (const [entity, cam] of world.resource(Cameras)) {
|
|
255
258
|
if (entity === primary || !passes(world, o, entity, defs)) continue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { GizmoStore } from './gizmos.js';
|
|
3
|
+
/** The overlay's text and colours: refreshed four times a second, so it's readable and cheap. */
|
|
4
|
+
export declare function perfOverlayLines(world: World): {
|
|
5
|
+
lines: string[];
|
|
6
|
+
colors: number[][];
|
|
7
|
+
};
|
|
8
|
+
/** Draws the perf overlay for the primary camera. Gizmo labels center on their anchor, above it. */
|
|
9
|
+
export declare function drawPerfOverlay(world: World, g: GizmoStore, camera: Entity | undefined): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ProfilerResource, spanStats } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
gpuPassOverlap,
|
|
4
|
+
isGpuPass,
|
|
5
|
+
measureSlices,
|
|
6
|
+
PerfBudgets,
|
|
7
|
+
PerfScenario,
|
|
8
|
+
perfBudgetState,
|
|
9
|
+
Time
|
|
10
|
+
} from "@aethervtt/shard-runtime";
|
|
11
|
+
import { PassCosts } from "./ablation.js";
|
|
12
|
+
import { LABEL_FONT, labelWidth } from "./gizmo-font.js";
|
|
13
|
+
import { Cameras } from "./view.js";
|
|
14
|
+
const LINES = 8;
|
|
15
|
+
const PASS_LINES = 4;
|
|
16
|
+
const REFRESH_S = 0.25;
|
|
17
|
+
const MARGIN = 8;
|
|
18
|
+
const COLOR = [1, 1, 1, 1];
|
|
19
|
+
const HEADER = [0.55, 0.9, 1, 1];
|
|
20
|
+
const GREY = [0.6, 0.6, 0.6, 1];
|
|
21
|
+
const RED = [1, 0.35, 0.3, 1];
|
|
22
|
+
const LABEL_OPTIONS = { depthTest: false };
|
|
23
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
const pad = (ms) => ms.toFixed(2).padStart(6);
|
|
25
|
+
const percent = (share) => `${Math.round(share * 100)}%`.padStart(4);
|
|
26
|
+
function perfOverlayLines(world) {
|
|
27
|
+
const now = world.tryResource(Time)?.elapsed ?? 0;
|
|
28
|
+
let state = states.get(world);
|
|
29
|
+
if (state && now - state.at < REFRESH_S && now >= state.at) return state;
|
|
30
|
+
const profiler = world.tryResource(ProfilerResource);
|
|
31
|
+
const lines = [];
|
|
32
|
+
const colors = [];
|
|
33
|
+
const line = (text, color) => {
|
|
34
|
+
lines.push(text);
|
|
35
|
+
colors.push(color);
|
|
36
|
+
};
|
|
37
|
+
if (profiler) {
|
|
38
|
+
const frame = spanStats(profiler, "frame");
|
|
39
|
+
const gpu = spanStats(profiler, "gpu:frame");
|
|
40
|
+
line(
|
|
41
|
+
`frame ${frame ? pad(frame.avg) : " -"} ms p95 ${frame ? pad(frame.p95) : " -"}` + (profiler.gpu.status === "unavailable" ? " gpu n/a" : ` gpu ${gpu ? pad(gpu.avg) : " -"} ms`),
|
|
42
|
+
HEADER
|
|
43
|
+
);
|
|
44
|
+
const overlapping = gpuPassOverlap(profiler)?.overlapping ?? false;
|
|
45
|
+
const spans = [];
|
|
46
|
+
const passes = [];
|
|
47
|
+
for (const name2 of profiler.names()) {
|
|
48
|
+
if (name2 === "frame" || name2 === "gpu:frame" || name2.startsWith("schedule/") || name2.startsWith("commands/"))
|
|
49
|
+
continue;
|
|
50
|
+
const entry = { name: name2, avg: profiler.timing(name2).avg };
|
|
51
|
+
if (overlapping && name2.startsWith("gpu:")) {
|
|
52
|
+
if (isGpuPass(name2)) passes.push(entry);
|
|
53
|
+
} else spans.push(entry);
|
|
54
|
+
}
|
|
55
|
+
spans.sort((a, b) => b.avg - a.avg || (a.name < b.name ? -1 : 1));
|
|
56
|
+
for (let i = 0; i < Math.min(LINES, spans.length); i++) {
|
|
57
|
+
line(`${pad(spans[i].avg)} ms ${spans[i].name}`, COLOR);
|
|
58
|
+
}
|
|
59
|
+
if (overlapping) {
|
|
60
|
+
const ablation = world.tryResource(PassCosts)?.latest;
|
|
61
|
+
if (ablation && ablation.passes.length > 0) {
|
|
62
|
+
line("gpu passes by ablation (timestamps overlap)", GREY);
|
|
63
|
+
const ranked = [...ablation.passes].sort((a, b) => b.ms - a.ms);
|
|
64
|
+
for (let i = 0; i < Math.min(PASS_LINES, ranked.length); i++) {
|
|
65
|
+
line(`${pad(ranked[i].ms)} ms gpu:${ranked[i].pass} ablated`, COLOR);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
line("gpu passes overlapping: perf.ablate ranks them", GREY);
|
|
69
|
+
passes.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
70
|
+
for (let i = 0; i < Math.min(PASS_LINES, passes.length); i++) {
|
|
71
|
+
line(`${pad(passes[i].avg)} ms ${passes[i].name} overlapping`, GREY);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const name = world.tryResource(PerfScenario)?.name;
|
|
76
|
+
const scenario = name ? world.tryResource(PerfBudgets)?.budgets.scenarios[name] : void 0;
|
|
77
|
+
if (name && scenario) {
|
|
78
|
+
const machine = perfBudgetState(world)?.detection.machine ?? null;
|
|
79
|
+
line(`scenario ${name}${machine ? ` on ${machine}` : ""}: share of frame / budget`, HEADER);
|
|
80
|
+
for (const s of measureSlices(profiler, scenario, machine).slices) {
|
|
81
|
+
const measured = s.measuredShare === null ? " -" : percent(s.measuredShare);
|
|
82
|
+
line(
|
|
83
|
+
`${s.track} ${measured} / ${percent(s.share)} ${s.slice}`,
|
|
84
|
+
s.verdict === "over" ? RED : s.verdict === "unmeasured" ? GREY : COLOR
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
state = { at: now, lines, colors };
|
|
90
|
+
states.set(world, state);
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
const point = new Float32Array(3);
|
|
94
|
+
function unproject(inv, width, height, px, py) {
|
|
95
|
+
const x = px / width * 2 - 1;
|
|
96
|
+
const y = 1 - py / height * 2;
|
|
97
|
+
const z = 0.5;
|
|
98
|
+
const w = inv[3] * x + inv[7] * y + inv[11] * z + inv[15];
|
|
99
|
+
point[0] = (inv[0] * x + inv[4] * y + inv[8] * z + inv[12]) / w;
|
|
100
|
+
point[1] = (inv[1] * x + inv[5] * y + inv[9] * z + inv[13]) / w;
|
|
101
|
+
point[2] = (inv[2] * x + inv[6] * y + inv[10] * z + inv[14]) / w;
|
|
102
|
+
return point;
|
|
103
|
+
}
|
|
104
|
+
function drawPerfOverlay(world, g, camera) {
|
|
105
|
+
if (camera === void 0) return;
|
|
106
|
+
const cam = world.resource(Cameras).get(camera);
|
|
107
|
+
if (!cam || cam.width <= 0 || cam.height <= 0) return;
|
|
108
|
+
const { lines, colors } = perfOverlayLines(world);
|
|
109
|
+
const step = LABEL_FONT.cellHeight + 4;
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const text = lines[i];
|
|
112
|
+
const px = MARGIN + labelWidth(text) / 2;
|
|
113
|
+
const py = MARGIN + (i + 1) * step;
|
|
114
|
+
g.label(
|
|
115
|
+
unproject(cam.invViewProj, cam.width, cam.height, px, py),
|
|
116
|
+
text,
|
|
117
|
+
colors[i],
|
|
118
|
+
LABEL_OPTIONS
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
drawPerfOverlay,
|
|
124
|
+
perfOverlayLines
|
|
125
|
+
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -101,6 +101,11 @@ export declare function captureView(world: World, view?: string): Promise<Captur
|
|
|
101
101
|
* perspective, fog, bloom…), `depth`, G-buffer channels, effect buffers.
|
|
102
102
|
*/
|
|
103
103
|
export declare function captureBuffer(world: World, view: string, buffer: string): Promise<CapturedBuffer>;
|
|
104
|
+
/** Every buffer and texture on the device by category: the ledger's total (0055, 0074). */
|
|
105
|
+
export declare function gpuMemory(world: World): {
|
|
106
|
+
bytes: number;
|
|
107
|
+
byCategory: Record<string, number>;
|
|
108
|
+
};
|
|
104
109
|
/** What the renderer is doing: graph order, culled nodes, views, pending pipelines. For agents. */
|
|
105
110
|
export declare function describeRender(world: World): {
|
|
106
111
|
order: {
|
|
@@ -134,7 +139,12 @@ export declare function describeRender(world: World): {
|
|
|
134
139
|
taaResets: number;
|
|
135
140
|
originShifts: number;
|
|
136
141
|
};
|
|
137
|
-
memory:
|
|
142
|
+
memory: {
|
|
143
|
+
textures: number;
|
|
144
|
+
textureBytes: number;
|
|
145
|
+
bytes: number;
|
|
146
|
+
byCategory: Record<string, number>;
|
|
147
|
+
};
|
|
138
148
|
recentErrors: {
|
|
139
149
|
code: string;
|
|
140
150
|
message: string;
|
package/dist/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
defineSystem,
|
|
6
6
|
defineSystemSet,
|
|
7
7
|
Last,
|
|
8
|
+
ProfilerResource,
|
|
8
9
|
ShardError
|
|
9
10
|
} from "@aethervtt/shard-core";
|
|
10
11
|
import {
|
|
@@ -14,9 +15,11 @@ import {
|
|
|
14
15
|
definePlugin,
|
|
15
16
|
FrameDemand,
|
|
16
17
|
LOADING_DEMAND,
|
|
17
|
-
LogResource
|
|
18
|
+
LogResource,
|
|
19
|
+
PerfProviders
|
|
18
20
|
} from "@aethervtt/shard-runtime";
|
|
19
21
|
import { ShaderLibrary } from "@aethervtt/shard-shader";
|
|
22
|
+
import { PassCosts } from "./ablation.js";
|
|
20
23
|
import { loadDataTextures, releaseDataStores } from "./data-store.js";
|
|
21
24
|
import { checkUnsupportedFeatures, describeFeatures, RenderFeatures } from "./features.js";
|
|
22
25
|
import { RenderGraph } from "./graph.js";
|
|
@@ -142,6 +145,7 @@ function renderPlugin(options = {}) {
|
|
|
142
145
|
// graph and device
|
|
143
146
|
Gpu,
|
|
144
147
|
GpuDeviceLost,
|
|
148
|
+
PassCosts,
|
|
145
149
|
Graph,
|
|
146
150
|
RenderDescribers,
|
|
147
151
|
RenderFeatures,
|
|
@@ -217,6 +221,13 @@ function renderPlugin(options = {}) {
|
|
|
217
221
|
app.insertResource(Gpu, gpu);
|
|
218
222
|
app.insertResource(Graph, new RenderGraph(gpu));
|
|
219
223
|
});
|
|
224
|
+
const providers = app.world.initResource(PerfProviders);
|
|
225
|
+
providers.gpuMemory = gpuMemory;
|
|
226
|
+
providers.adapter = adapterIdentity;
|
|
227
|
+
const profiler = app.world.tryResource(ProfilerResource);
|
|
228
|
+
if (profiler) {
|
|
229
|
+
profiler.gpu.status = gpu.features.has("timestamp-query") ? "available" : "unavailable";
|
|
230
|
+
}
|
|
220
231
|
app.setPresenter(presentOn(state));
|
|
221
232
|
const shaders = options.shaders ?? new ShaderLibrary();
|
|
222
233
|
if (!options.shaders) registerEngineShaders(shaders);
|
|
@@ -286,6 +297,20 @@ async function captureBuffer(world, view, buffer) {
|
|
|
286
297
|
}
|
|
287
298
|
return result;
|
|
288
299
|
}
|
|
300
|
+
function adapterIdentity(world) {
|
|
301
|
+
const info = world.tryResource(Gpu)?.adapter?.info;
|
|
302
|
+
if (!info) return void 0;
|
|
303
|
+
return {
|
|
304
|
+
vendor: info.vendor,
|
|
305
|
+
architecture: info.architecture,
|
|
306
|
+
device: info.device,
|
|
307
|
+
description: info.description
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function gpuMemory(world) {
|
|
311
|
+
const memory = world.tryResource(Gpu)?.memory();
|
|
312
|
+
return memory ? { bytes: memory.bytes, byCategory: { ...memory.byCategory } } : { bytes: 0, byCategory: {} };
|
|
313
|
+
}
|
|
289
314
|
function describeRender(world) {
|
|
290
315
|
const graph = world.tryResource(Graph);
|
|
291
316
|
const gpu = world.tryResource(Gpu);
|
|
@@ -308,7 +333,12 @@ function describeRender(world) {
|
|
|
308
333
|
gpuTimings: gpu.features.has("timestamp-query"),
|
|
309
334
|
stats: Object.fromEntries(world.tryResource(RenderStats) ?? []),
|
|
310
335
|
counters: { ...world.tryResource(RenderCounters) ?? { taaResets: 0, originShifts: 0 } },
|
|
311
|
-
|
|
336
|
+
// The ledger's total, so it agrees with perf.describe and perf records; and the textures the
|
|
337
|
+
// forward renderer uploaded, as before.
|
|
338
|
+
memory: {
|
|
339
|
+
...gpuMemory(world),
|
|
340
|
+
...world.tryResource(GpuMemory) ?? { textures: 0, textureBytes: 0 }
|
|
341
|
+
},
|
|
312
342
|
recentErrors: gpu.errors.slice(-5).map((e) => e.toJSON()),
|
|
313
343
|
features: describeFeatures(world),
|
|
314
344
|
health: world.tryResource(RenderHealth) ?? { state: "ok", issues: [] },
|
|
@@ -344,6 +374,7 @@ export {
|
|
|
344
374
|
captureBuffer,
|
|
345
375
|
captureView,
|
|
346
376
|
describeRender,
|
|
377
|
+
gpuMemory,
|
|
347
378
|
renderOwner,
|
|
348
379
|
renderPlugin
|
|
349
380
|
};
|
package/dist/post-common.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
|
|
2
2
|
import { type ViewGpu } from './forward.js';
|
|
3
|
-
import type { NodeContext } from './graph.js';
|
|
3
|
+
import type { NodeContext, RenderView } from './graph.js';
|
|
4
4
|
import { type CameraData } from './view.js';
|
|
5
5
|
export declare function idOf(o: object): number;
|
|
6
6
|
export declare const F: () => number;
|
|
@@ -37,7 +37,7 @@ export declare class PostCache {
|
|
|
37
37
|
check(gpu: GpuContext): void;
|
|
38
38
|
/** A fullscreen render pipeline: `shard::fullscreen` vertex stage and `root`'s `entry`. */
|
|
39
39
|
render(ctx: NodeContext, key: string, root: string, entry: string, layouts: GPUBindGroupLayout[], targets: GPUColorTargetState[], defines?: Record<string, boolean>): GPURenderPipeline | undefined;
|
|
40
|
-
compute(ctx: NodeContext, key: string, root: string, entry: string, layouts: GPUBindGroupLayout[]): GPUComputePipeline | undefined;
|
|
40
|
+
compute(ctx: NodeContext, key: string, root: string, entry: string, layouts: GPUBindGroupLayout[], defines?: Record<string, boolean>): GPUComputePipeline | undefined;
|
|
41
41
|
/** A bind group, rebuilt when `key` (the ids of what it binds) changes. */
|
|
42
42
|
group(gpu: GpuContext, slot: string, key: string, layout: GPUBindGroupLayout, entries: () => GPUBindGroupEntry[]): GPUBindGroup;
|
|
43
43
|
buffer(gpu: GpuContext, name: string, size: number): GpuBuffer;
|
|
@@ -50,4 +50,22 @@ export declare function forwardView(ctx: NodeContext): {
|
|
|
50
50
|
/** A fullscreen pass the node begins itself (raw nodes with several passes). */
|
|
51
51
|
export declare function beginPass(ctx: NodeContext, name: string, view: GPUTextureView, load?: boolean): GPURenderPassEncoder;
|
|
52
52
|
export declare const HDR: GPUColorTargetState[];
|
|
53
|
+
/** Color targets for a post pass that writes `texture`: the chain's format, per view. */
|
|
54
|
+
export declare function targetsFor(texture: GPUTexture): GPUColorTargetState[];
|
|
55
|
+
/**
|
|
56
|
+
* The format of the post chain's full-resolution ping-pong (post-a, post-b) for a view:
|
|
57
|
+
* rg11b10ufloat, half the bytes of rgba16float, where the device renders it, the view has no alpha
|
|
58
|
+
* to carry (0052's transparent views), and the device's rounding is known (`rg11Truncates`);
|
|
59
|
+
* rgba16float otherwise, and until then.
|
|
60
|
+
*/
|
|
61
|
+
export declare function postFormat(gpu: GpuContext, view: RenderView): GPUTextureFormat;
|
|
62
|
+
/** Whether a pass writing `texture` rounds its output in the shader (the RG11_ROUND define). */
|
|
63
|
+
export declare function roundsRg11(gpu: GpuContext, texture: GPUTexture): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the device truncates when it stores rg11b10ufloat (Metal does; the format leaves the
|
|
66
|
+
* rounding to the device). Post passes writing it then round in the shader (`rg11_round`, the
|
|
67
|
+
* RG11_ROUND define). Probed once per device, rendering 1 + 0.75 of a step and reading it back;
|
|
68
|
+
* undefined until the answer lands.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rg11Truncates(gpu: GpuContext): boolean | undefined;
|
|
53
71
|
export declare const scratch: Float32Array<ArrayBuffer>;
|
package/dist/post-common.js
CHANGED
|
@@ -66,12 +66,12 @@ class PostCache {
|
|
|
66
66
|
if (pipeline) this.pipelines.set(key, pipeline);
|
|
67
67
|
return pipeline;
|
|
68
68
|
}
|
|
69
|
-
compute(ctx, key, root, entry, layouts) {
|
|
69
|
+
compute(ctx, key, root, entry, layouts, defines) {
|
|
70
70
|
const gpu = ctx.gpu;
|
|
71
71
|
this.check(gpu);
|
|
72
72
|
const cached = this.pipelines.get(key);
|
|
73
73
|
if (cached) return cached;
|
|
74
|
-
const module = ctx.world.resource(Shaders).module(gpu, { root });
|
|
74
|
+
const module = ctx.world.resource(Shaders).module(gpu, defines ? { root, defines } : { root });
|
|
75
75
|
if (!module) {
|
|
76
76
|
gpu.pipelines.skipped++;
|
|
77
77
|
return void 0;
|
|
@@ -135,6 +135,79 @@ function beginPass(ctx, name, view, load = false) {
|
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
const HDR = [{ format: "rgba16float" }];
|
|
138
|
+
const RG11 = [{ format: "rg11b10ufloat" }];
|
|
139
|
+
function targetsFor(texture) {
|
|
140
|
+
return texture.format === "rg11b10ufloat" ? RG11 : HDR;
|
|
141
|
+
}
|
|
142
|
+
function postFormat(gpu, view) {
|
|
143
|
+
const cam = cameraOf(view);
|
|
144
|
+
if (!gpu.features.has("rg11b10ufloat-renderable") || cam === void 0 || cam.alphaOutput)
|
|
145
|
+
return "rgba16float";
|
|
146
|
+
return rg11Truncates(gpu) === void 0 ? "rgba16float" : "rg11b10ufloat";
|
|
147
|
+
}
|
|
148
|
+
function roundsRg11(gpu, texture) {
|
|
149
|
+
return texture.format === "rg11b10ufloat" && rg11Truncates(gpu) === true;
|
|
150
|
+
}
|
|
151
|
+
const truncates = /* @__PURE__ */ new WeakMap();
|
|
152
|
+
function rg11Truncates(gpu) {
|
|
153
|
+
const device = gpu.device;
|
|
154
|
+
const known = truncates.get(device);
|
|
155
|
+
if (known === "probing") return void 0;
|
|
156
|
+
if (known !== void 0) return known;
|
|
157
|
+
truncates.set(device, "probing");
|
|
158
|
+
probeRg11(device).then(
|
|
159
|
+
(v) => truncates.set(device, v),
|
|
160
|
+
// A lost device or a failed map: assume truncation, the safe side (rounding then is exact).
|
|
161
|
+
() => truncates.set(device, true)
|
|
162
|
+
);
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
async function probeRg11(device) {
|
|
166
|
+
const texture = device.createTexture({
|
|
167
|
+
label: "post/rg11-probe",
|
|
168
|
+
size: [1, 1],
|
|
169
|
+
format: "rg11b10ufloat",
|
|
170
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
171
|
+
});
|
|
172
|
+
const buffer = device.createBuffer({
|
|
173
|
+
label: "post/rg11-probe",
|
|
174
|
+
size: 4,
|
|
175
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
176
|
+
});
|
|
177
|
+
const module = device.createShaderModule({
|
|
178
|
+
label: "post/rg11-probe",
|
|
179
|
+
code: `
|
|
180
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
181
|
+
let xy = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
|
182
|
+
return vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
|
|
183
|
+
}
|
|
184
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1.0 + 0.75 / 64.0, 0.0, 0.0, 1.0); }`
|
|
185
|
+
});
|
|
186
|
+
const pipeline = await device.createRenderPipelineAsync({
|
|
187
|
+
label: "post/rg11-probe",
|
|
188
|
+
layout: "auto",
|
|
189
|
+
vertex: { module, entryPoint: "vs" },
|
|
190
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rg11b10ufloat" }] }
|
|
191
|
+
});
|
|
192
|
+
const encoder = device.createCommandEncoder({ label: "post/rg11-probe" });
|
|
193
|
+
const pass = encoder.beginRenderPass({
|
|
194
|
+
colorAttachments: [{ view: texture.createView(), loadOp: "clear", storeOp: "store" }]
|
|
195
|
+
});
|
|
196
|
+
pass.setPipeline(pipeline);
|
|
197
|
+
pass.draw(3);
|
|
198
|
+
pass.end();
|
|
199
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow: 256 }, [1, 1]);
|
|
200
|
+
device.queue.submit([encoder.finish()]);
|
|
201
|
+
try {
|
|
202
|
+
await buffer.mapAsync(GPUMapMode.READ);
|
|
203
|
+
const red = new Uint32Array(buffer.getMappedRange())[0] & 2047;
|
|
204
|
+
buffer.unmap();
|
|
205
|
+
return red === 15 << 6;
|
|
206
|
+
} finally {
|
|
207
|
+
texture.destroy();
|
|
208
|
+
buffer.destroy();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
138
211
|
const scratch = new Float32Array(64);
|
|
139
212
|
export {
|
|
140
213
|
F,
|
|
@@ -144,8 +217,12 @@ export {
|
|
|
144
217
|
depthTex,
|
|
145
218
|
forwardView,
|
|
146
219
|
idOf,
|
|
220
|
+
postFormat,
|
|
221
|
+
rg11Truncates,
|
|
222
|
+
roundsRg11,
|
|
147
223
|
sampler,
|
|
148
224
|
scratch,
|
|
225
|
+
targetsFor,
|
|
149
226
|
tex,
|
|
150
227
|
uniform
|
|
151
228
|
};
|
package/dist/post-nodes.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { type World } from '@aethervtt/shard-core';
|
|
|
2
2
|
import { type CameraData } from './view.js';
|
|
3
3
|
/** Mip levels of a view's bloom chain: `radius` spreads it from 3 levels to the whole view. */
|
|
4
4
|
export declare function bloomLevels(cam: CameraData): number;
|
|
5
|
+
/**
|
|
6
|
+
* Bloom's glow parameters for what composites it (the tonemap) and what meters it: intensity, the
|
|
7
|
+
* chain's level count (one mip per level), mix (1, no threshold) or add (0). Zeros without bloom.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bloomGlow(cam: CameraData, chain: GPUTexture | undefined, out: Float32Array, at: number): void;
|
|
5
10
|
/** Metered EV100 per camera (from the GPU histogram, a frame or two late) and the adapted EV. */
|
|
6
11
|
export interface ExposureState {
|
|
7
12
|
metered: number | undefined;
|