@aethervtt/shard-render 0.1.0 → 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.
Files changed (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. 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 entity labels.",
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: import("./stats.js").GpuMemoryData;
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
- memory: world.tryResource(GpuMemory) ?? { textures: 0, textureBytes: 0 },
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
  };
@@ -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>;
@@ -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
  };
@@ -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;