@yali-engine/render 0.0.0-stage → 0.4.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 (131) hide show
  1. package/dist/bvh.d.ts +66 -0
  2. package/dist/bvh.js +493 -0
  3. package/dist/bvh.js.map +1 -0
  4. package/dist/camera.d.ts +34 -0
  5. package/dist/camera.js +66 -0
  6. package/dist/camera.js.map +1 -0
  7. package/dist/cluster-build.d.ts +46 -0
  8. package/dist/cluster-build.js +179 -0
  9. package/dist/cluster-build.js.map +1 -0
  10. package/dist/cluster-pages.d.ts +70 -0
  11. package/dist/cluster-pages.js +203 -0
  12. package/dist/cluster-pages.js.map +1 -0
  13. package/dist/cluster-stream.d.ts +83 -0
  14. package/dist/cluster-stream.js +226 -0
  15. package/dist/cluster-stream.js.map +1 -0
  16. package/dist/culled-draws.d.ts +30 -0
  17. package/dist/culled-draws.js +103 -0
  18. package/dist/culled-draws.js.map +1 -0
  19. package/dist/debug-lines.d.ts +51 -0
  20. package/dist/debug-lines.js +158 -0
  21. package/dist/debug-lines.js.map +1 -0
  22. package/dist/device-profile.d.ts +100 -0
  23. package/dist/device-profile.js +131 -0
  24. package/dist/device-profile.js.map +1 -0
  25. package/dist/device.d.ts +39 -0
  26. package/dist/device.js +79 -0
  27. package/dist/device.js.map +1 -0
  28. package/dist/environment.d.ts +60 -0
  29. package/dist/environment.js +204 -0
  30. package/dist/environment.js.map +1 -0
  31. package/dist/extract.d.ts +74 -0
  32. package/dist/extract.js +194 -0
  33. package/dist/extract.js.map +1 -0
  34. package/dist/frame-graph.d.ts +112 -0
  35. package/dist/frame-graph.js +226 -0
  36. package/dist/frame-graph.js.map +1 -0
  37. package/dist/gpu-health.d.ts +74 -0
  38. package/dist/gpu-health.js +106 -0
  39. package/dist/gpu-health.js.map +1 -0
  40. package/dist/gpu-timer.d.ts +26 -0
  41. package/dist/gpu-timer.js +93 -0
  42. package/dist/gpu-timer.js.map +1 -0
  43. package/dist/headless.d.ts +28 -0
  44. package/dist/headless.js +91 -0
  45. package/dist/headless.js.map +1 -0
  46. package/dist/index.d.ts +34 -0
  47. package/dist/index.js +36 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/lights.d.ts +81 -0
  50. package/dist/lights.js +247 -0
  51. package/dist/lights.js.map +1 -0
  52. package/dist/matrix.d.ts +6 -0
  53. package/dist/matrix.js +59 -0
  54. package/dist/matrix.js.map +1 -0
  55. package/dist/meshes.d.ts +411 -0
  56. package/dist/meshes.js +545 -0
  57. package/dist/meshes.js.map +1 -0
  58. package/dist/meshlets-decoder.d.ts +20 -0
  59. package/dist/meshlets-decoder.js +47 -0
  60. package/dist/meshlets-decoder.js.map +1 -0
  61. package/dist/meshlets.d.ts +186 -0
  62. package/dist/meshlets.js +1040 -0
  63. package/dist/meshlets.js.map +1 -0
  64. package/dist/occlusion.d.ts +51 -0
  65. package/dist/occlusion.js +188 -0
  66. package/dist/occlusion.js.map +1 -0
  67. package/dist/pick-decoder.d.ts +19 -0
  68. package/dist/pick-decoder.js +45 -0
  69. package/dist/pick-decoder.js.map +1 -0
  70. package/dist/pick.d.ts +86 -0
  71. package/dist/pick.js +452 -0
  72. package/dist/pick.js.map +1 -0
  73. package/dist/pipeline-cache.d.ts +53 -0
  74. package/dist/pipeline-cache.js +96 -0
  75. package/dist/pipeline-cache.js.map +1 -0
  76. package/dist/post-options.d.ts +109 -0
  77. package/dist/post-options.js +23 -0
  78. package/dist/post-options.js.map +1 -0
  79. package/dist/post-shaders.gen.d.ts +40 -0
  80. package/dist/post-shaders.gen.js +43 -0
  81. package/dist/post-shaders.gen.js.map +1 -0
  82. package/dist/post.d.ts +101 -0
  83. package/dist/post.js +555 -0
  84. package/dist/post.js.map +1 -0
  85. package/dist/render-plugin.d.ts +239 -0
  86. package/dist/render-plugin.js +633 -0
  87. package/dist/render-plugin.js.map +1 -0
  88. package/dist/scene.d.ts +406 -0
  89. package/dist/scene.js +2005 -0
  90. package/dist/scene.js.map +1 -0
  91. package/dist/shaders.gen.d.ts +40 -0
  92. package/dist/shaders.gen.js +43 -0
  93. package/dist/shaders.gen.js.map +1 -0
  94. package/dist/shadows.d.ts +104 -0
  95. package/dist/shadows.js +227 -0
  96. package/dist/shadows.js.map +1 -0
  97. package/dist/staging-ring.d.ts +41 -0
  98. package/dist/staging-ring.js +125 -0
  99. package/dist/staging-ring.js.map +1 -0
  100. package/dist/textures.d.ts +165 -0
  101. package/dist/textures.js +361 -0
  102. package/dist/textures.js.map +1 -0
  103. package/dist/tonemap.d.ts +45 -0
  104. package/dist/tonemap.js +141 -0
  105. package/dist/tonemap.js.map +1 -0
  106. package/dist/viewport.d.ts +78 -0
  107. package/dist/viewport.js +130 -0
  108. package/dist/viewport.js.map +1 -0
  109. package/dist/webgl2/plugin.d.ts +40 -0
  110. package/dist/webgl2/plugin.js +190 -0
  111. package/dist/webgl2/plugin.js.map +1 -0
  112. package/dist/webgl2/program.d.ts +27 -0
  113. package/dist/webgl2/program.js +74 -0
  114. package/dist/webgl2/program.js.map +1 -0
  115. package/dist/webgl2/renderer.d.ts +128 -0
  116. package/dist/webgl2/renderer.js +976 -0
  117. package/dist/webgl2/renderer.js.map +1 -0
  118. package/dist/webgl2/shaders.glsl.gen.d.ts +27 -0
  119. package/dist/webgl2/shaders.glsl.gen.js +20 -0
  120. package/dist/webgl2/shaders.glsl.gen.js.map +1 -0
  121. package/dist/webgl2/textures.d.ts +11 -0
  122. package/dist/webgl2/textures.js +82 -0
  123. package/dist/webgl2/textures.js.map +1 -0
  124. package/dist/webgl2/webgl2.d.ts +1 -0
  125. package/dist/webgl2/webgl2.js +4 -0
  126. package/dist/webgl2/webgl2.js.map +1 -0
  127. package/dist/worker-mode.d.ts +85 -0
  128. package/dist/worker-mode.js +204 -0
  129. package/dist/worker-mode.js.map +1 -0
  130. package/package.json +47 -3
  131. package/README.md +0 -3
@@ -0,0 +1,226 @@
1
+ import { YaliError } from '@yali-engine/core';
2
+ /**
3
+ * The declarative frame graph (DESIGN.md §12, after O'Donnell, *FrameGraph*, GDC 2017).
4
+ * Plugins add passes with what they read and write; `compile` orders them, culls passes
5
+ * nothing downstream needs, and pools transient textures. `execute` records one frame.
6
+ *
7
+ * @example
8
+ * const graph = new FrameGraph(device);
9
+ * graph.importTexture('backbuffer', () => context.getCurrentTexture().createView());
10
+ * graph.transientTexture('depth', { format: 'depth32float', usage: GPUTextureUsage.RENDER_ATTACHMENT });
11
+ * graph.addPass(cullPass);
12
+ * graph.addPass(drawPass);
13
+ * graph.setOutput('backbuffer');
14
+ */
15
+ /** Bytes per texel of the formats the renderer's transients use (4 for anything else listed as RGBA8-sized). */
16
+ const TEXEL_BYTES = { rgba16float: 8, rgba32float: 16, rg16float: 4, r32float: 4, rgba8unorm: 4, 'rgba8unorm-srgb': 4, bgra8unorm: 4, depth32float: 4, depth24plus: 4, 'depth24plus-stencil8': 4, r32uint: 4, r16float: 2, r8unorm: 1 };
17
+ export class FrameGraph {
18
+ device;
19
+ #passes = [];
20
+ #imports = new Map();
21
+ #transients = new Map();
22
+ #allocated = new Map();
23
+ #output = '';
24
+ #compiled = [];
25
+ #culled = [];
26
+ #dirty = true;
27
+ /** Names of the compiled passes in execution order; pass `i` uses timestamp slot `i`.
28
+ * Replaced (not mutated) on each compile, so a holder can detect a recompile by identity. */
29
+ passNames = [];
30
+ #width = 0;
31
+ #height = 0;
32
+ #outWidth = 0;
33
+ #outHeight = 0;
34
+ #ctx;
35
+ timer = null;
36
+ constructor(device) {
37
+ this.device = device;
38
+ this.#ctx = { device, width: 0, height: 0, outputWidth: 0, outputHeight: 0, generation: 0, view: (name) => this.#view(name), texture: (name) => this.#texture(name) };
39
+ }
40
+ /** A texture owned outside the graph (the swapchain), fetched once per frame. */
41
+ importTexture(name, view) {
42
+ this.#imports.set(name, view);
43
+ this.#dirty = true;
44
+ }
45
+ /** A texture the graph allocates for the current size. */
46
+ transientTexture(name, desc) {
47
+ this.#transients.set(name, desc);
48
+ this.#dirty = true;
49
+ }
50
+ /** Adds a pass. Names are unique. */
51
+ addPass(def) {
52
+ if (this.#passes.some((p) => p.name === def.name)) {
53
+ throw new YaliError('DUPLICATE_PASS', `A pass named ${def.name} already exists.`, 'Give each pass a unique name.');
54
+ }
55
+ this.#passes.push(def);
56
+ this.#dirty = true;
57
+ }
58
+ /** The resource the frame exists to produce (usually `backbuffer`). */
59
+ setOutput(name) {
60
+ this.#output = name;
61
+ this.#dirty = true;
62
+ }
63
+ /** Resizes transients (on the next `execute`); the output size defaults to the render size. */
64
+ resize(width, height, outputWidth = width, outputHeight = height) {
65
+ if (width === this.#width && height === this.#height && outputWidth === this.#outWidth && outputHeight === this.#outHeight)
66
+ return;
67
+ this.#width = width;
68
+ this.#height = height;
69
+ this.#outWidth = outputWidth;
70
+ this.#outHeight = outputHeight;
71
+ this.#ctx.width = width;
72
+ this.#ctx.height = height;
73
+ this.#ctx.outputWidth = outputWidth;
74
+ this.#ctx.outputHeight = outputHeight;
75
+ for (const t of this.#allocated.values())
76
+ t.texture.destroy();
77
+ this.#allocated.clear();
78
+ this.#dirty = true;
79
+ }
80
+ static #targets(p) {
81
+ const out = [...(p.writes ?? [])];
82
+ if (p.kind === 'render') {
83
+ if (p.color)
84
+ out.push(p.color.target);
85
+ if (p.depth)
86
+ out.push(p.depth.target);
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * Orders passes (a pass runs after every earlier-declared pass that writes what it
92
+ * reads), culls passes that don't contribute to the output, and allocates transients.
93
+ */
94
+ compile() {
95
+ if (!this.#output) {
96
+ throw new YaliError('NO_OUTPUT', 'The frame graph has no output.', 'Call graph.setOutput("backbuffer").');
97
+ }
98
+ // Walk back from the output: a pass is needed if it writes something a needed pass
99
+ // (or the output) consumes.
100
+ const needed = new Set([this.#output]);
101
+ const keep = new Array(this.#passes.length).fill(false);
102
+ for (let i = this.#passes.length - 1; i >= 0; i--) {
103
+ const p = this.#passes[i];
104
+ if (FrameGraph.#targets(p).some((t) => needed.has(t))) {
105
+ keep[i] = true;
106
+ for (const r of p.reads ?? [])
107
+ needed.add(r);
108
+ if (p.kind === 'render' && p.depth)
109
+ needed.add(p.depth.target);
110
+ }
111
+ }
112
+ this.#culled = this.#passes.filter((_, i) => !keep[i]).map((p) => p.name);
113
+ const used = new Set();
114
+ this.#compiled = this.#passes
115
+ .filter((_, i) => keep[i])
116
+ .map((def, index) => {
117
+ for (const t of FrameGraph.#targets(def))
118
+ used.add(t);
119
+ for (const r of def.reads ?? [])
120
+ used.add(r);
121
+ if (def.kind === 'encoder')
122
+ return { def };
123
+ if (def.kind === 'compute') {
124
+ const tw = this.timer?.timestampWrites(index);
125
+ return { def, computeDesc: tw ? { label: def.name, timestampWrites: tw } : { label: def.name } };
126
+ }
127
+ const tw = this.timer?.timestampWrites(index);
128
+ const desc = {
129
+ label: def.name,
130
+ colorAttachments: def.color ? [{ view: undefined, clearValue: def.color.clear, loadOp: def.color.load ? 'load' : 'clear', storeOp: 'store' }] : [],
131
+ ...(def.depth
132
+ ? { depthStencilAttachment: { view: undefined, depthClearValue: def.depth.clear, depthLoadOp: def.depth.load ? 'load' : 'clear', depthStoreOp: def.depth.store ? 'store' : 'discard' } }
133
+ : {}),
134
+ ...(tw ? { timestampWrites: tw } : {}),
135
+ };
136
+ return { def, renderDesc: desc };
137
+ });
138
+ this.passNames = this.#compiled.map((c) => c.def.name);
139
+ for (const [name, t] of this.#transients) {
140
+ if (!used.has(name) || this.#allocated.has(name))
141
+ continue;
142
+ const scale = t.scale ?? 1;
143
+ const [w, h] = t.output ? [this.#outWidth || this.#width, this.#outHeight || this.#height] : [this.#width, this.#height];
144
+ const texture = this.device.createTexture({
145
+ label: `transient:${name}`,
146
+ size: [Math.max(1, Math.round(w * scale)), Math.max(1, Math.round(h * scale))],
147
+ format: t.format,
148
+ usage: t.usage,
149
+ });
150
+ this.#allocated.set(name, { texture, view: texture.createView() });
151
+ this.#ctx.generation++;
152
+ }
153
+ this.#dirty = false;
154
+ }
155
+ /** Bytes the allocated transient textures hold (M2's render-target budget, `budgets.json`). */
156
+ get transientBytes() {
157
+ let sum = 0;
158
+ for (const { texture } of this.#allocated.values())
159
+ sum += texture.width * texture.height * (TEXEL_BYTES[texture.format] ?? 4);
160
+ return sum;
161
+ }
162
+ #texture(name) {
163
+ const t = this.#allocated.get(name);
164
+ if (!t)
165
+ throw new YaliError('UNKNOWN_RESOURCE', `Frame graph texture ${name} is not a transient.`, 'Copy from a transient texture.');
166
+ return t.texture;
167
+ }
168
+ #view(name) {
169
+ const imported = this.#imports.get(name);
170
+ if (imported)
171
+ return imported();
172
+ const t = this.#allocated.get(name);
173
+ if (!t)
174
+ throw new YaliError('UNKNOWN_RESOURCE', `Frame graph texture ${name} does not exist.`, 'Import it or declare it as a transient.');
175
+ return t.view;
176
+ }
177
+ /** Records every compiled pass into `encoder`. */
178
+ execute(encoder) {
179
+ if (this.#dirty)
180
+ this.compile();
181
+ for (const c of this.#compiled) {
182
+ const def = c.def;
183
+ if (def.kind === 'encoder') {
184
+ def.execute(encoder, this.#ctx);
185
+ continue;
186
+ }
187
+ if (def.kind === 'compute') {
188
+ const pass = encoder.beginComputePass(c.computeDesc);
189
+ def.execute(pass, this.#ctx);
190
+ pass.end();
191
+ }
192
+ else {
193
+ const desc = c.renderDesc;
194
+ if (def.color)
195
+ desc.colorAttachments[0].view = this.#view(def.color.target);
196
+ if (def.depth && desc.depthStencilAttachment)
197
+ desc.depthStencilAttachment.view = this.#view(def.depth.target);
198
+ const pass = encoder.beginRenderPass(desc);
199
+ def.execute(pass, this.#ctx);
200
+ pass.end();
201
+ }
202
+ }
203
+ }
204
+ /** Transient textures currently allocated (for the dispose leak check). */
205
+ get liveTextures() {
206
+ return this.#allocated.size;
207
+ }
208
+ /** Forces transients to be rebuilt on the next frame (the attachment-mismatch heal). */
209
+ invalidate() {
210
+ for (const t of this.#allocated.values())
211
+ t.texture.destroy();
212
+ this.#allocated.clear();
213
+ this.#dirty = true;
214
+ }
215
+ /** Destroys every transient texture. */
216
+ destroy() {
217
+ this.invalidate();
218
+ }
219
+ /** The compiled graph, for inspection (CLI, MCP, overlay). */
220
+ describe() {
221
+ if (this.#dirty)
222
+ this.compile();
223
+ return { passes: this.#compiled.map((c) => c.def.name), culled: [...this.#culled], transients: [...this.#allocated.keys()] };
224
+ }
225
+ }
226
+ //# sourceMappingURL=frame-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-graph.js","sourceRoot":"","sources":["../src/frame-graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * GPU health: is the device alive, and if not, why (ADR-0010: ported from Character XYZ
3
+ * `mesh/viewport/gpu-health.ts`, per instance instead of module-global).
4
+ *
5
+ * A lost `GPUDevice` fails silently: every later command is ignored, with no exception,
6
+ * and the canvas just stops presenting. This class listens for `device.lost` and
7
+ * `uncapturederror`, so a loss arrives with its reason. It counts uncaptured errors even
8
+ * while the device is healthy, keeping the FIRST one (the error that started a cascade)
9
+ * as well as the last. It keeps a bounded diagnostic ring that survives until the page
10
+ * reloads, which is the one action that destroys every other trace.
11
+ */
12
+ /** Health of one device. */
13
+ export interface GpuHealthState {
14
+ status: 'ok' | 'lost';
15
+ /** The API's own reason (`destroyed` | `unknown`), null while healthy. */
16
+ reason: string | null;
17
+ /** The API's own message; often empty even when lost. Never invented. */
18
+ message: string | null;
19
+ /** `performance.now()` at the loss. */
20
+ at: number | null;
21
+ uncaptured: {
22
+ count: number;
23
+ first: string | null;
24
+ last: string | null;
25
+ };
26
+ }
27
+ /** One recorded moment in the device's life. */
28
+ export interface GpuDiagEvent {
29
+ t: number;
30
+ kind: 'uncaptured' | 'device-lost' | 'heal' | 'note';
31
+ text: string;
32
+ }
33
+ /**
34
+ * The recoverable attachment-size mismatch: a depth attachment allocated at one pixel ratio
35
+ * still bound after the ratio changed. Captured live in Character XYZ (2026-08-19); a
36
+ * resize rebuilds the attachments and fixes it.
37
+ */
38
+ export declare function isRecoverableAttachmentMismatch(text: string): boolean;
39
+ /**
40
+ * Tracks one device.
41
+ *
42
+ * @example
43
+ * const health = new GpuHealth();
44
+ * const detach = health.attach(device, { onLost: (info) => app.deviceLost(info), onHeal: () => graph.forceResize() });
45
+ */
46
+ export declare class GpuHealth {
47
+ #private;
48
+ readonly state: GpuHealthState;
49
+ readonly events: GpuDiagEvent[];
50
+ heals: number;
51
+ /** Records a note beside the GPU events (a heal, a mode change). */
52
+ note(text: string): void;
53
+ /**
54
+ * Listens to `device`. `onLost` gets the API's reason; `onHeal` runs when a known
55
+ * recoverable error arrives (rate-limited). Returns a detach function.
56
+ */
57
+ attach(device: GPUDevice, hooks?: {
58
+ onLost?: (info: {
59
+ reason: string;
60
+ message: string;
61
+ }) => void;
62
+ onHeal?: (why: string) => void;
63
+ }): () => void;
64
+ /** Marks a fresh device after recovery, keeping the history. */
65
+ reset(): void;
66
+ /** Everything needed to diagnose a black canvas, as one JSON-able object. */
67
+ diagnostics(): {
68
+ at: string;
69
+ userAgent: string;
70
+ health: GpuHealthState;
71
+ heals: number;
72
+ events: GpuDiagEvent[];
73
+ };
74
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * GPU health: is the device alive, and if not, why (ADR-0010: ported from Character XYZ
3
+ * `mesh/viewport/gpu-health.ts`, per instance instead of module-global).
4
+ *
5
+ * A lost `GPUDevice` fails silently: every later command is ignored, with no exception,
6
+ * and the canvas just stops presenting. This class listens for `device.lost` and
7
+ * `uncapturederror`, so a loss arrives with its reason. It counts uncaptured errors even
8
+ * while the device is healthy, keeping the FIRST one (the error that started a cascade)
9
+ * as well as the last. It keeps a bounded diagnostic ring that survives until the page
10
+ * reloads, which is the one action that destroys every other trace.
11
+ */
12
+ /** Bounded: a validation storm is thousands of errors per second. */
13
+ const MAX_EVENTS = 60;
14
+ /** Heals repeat every frame until fixed; at most one per second. */
15
+ const HEAL_COOLDOWN_MS = 1000;
16
+ /**
17
+ * The recoverable attachment-size mismatch: a depth attachment allocated at one pixel ratio
18
+ * still bound after the ratio changed. Captured live in Character XYZ (2026-08-19); a
19
+ * resize rebuilds the attachments and fixes it.
20
+ */
21
+ export function isRecoverableAttachmentMismatch(text) {
22
+ return /does not match the size/i.test(text) && /attachment/i.test(text);
23
+ }
24
+ /**
25
+ * Tracks one device.
26
+ *
27
+ * @example
28
+ * const health = new GpuHealth();
29
+ * const detach = health.attach(device, { onLost: (info) => app.deviceLost(info), onHeal: () => graph.forceResize() });
30
+ */
31
+ export class GpuHealth {
32
+ state = { status: 'ok', reason: null, message: null, at: null, uncaptured: { count: 0, first: null, last: null } };
33
+ events = [];
34
+ heals = 0;
35
+ #lastHeal = -Infinity;
36
+ #record(kind, text) {
37
+ this.events.push({ t: performance.now(), kind, text: text.length > 400 ? `${text.slice(0, 400)}…` : text });
38
+ // Keep the first few (the cascade's start) and the tail (what happened last).
39
+ if (this.events.length > MAX_EVENTS)
40
+ this.events.splice(MAX_EVENTS / 2, 1);
41
+ }
42
+ /** Records a note beside the GPU events (a heal, a mode change). */
43
+ note(text) {
44
+ this.#record('note', text);
45
+ }
46
+ /**
47
+ * Listens to `device`. `onLost` gets the API's reason; `onHeal` runs when a known
48
+ * recoverable error arrives (rate-limited). Returns a detach function.
49
+ */
50
+ attach(device, hooks = {}) {
51
+ let attached = true;
52
+ const onError = (e) => {
53
+ const text = e.error?.message ?? String(e);
54
+ const u = this.state.uncaptured;
55
+ u.count++;
56
+ if (u.first === null)
57
+ u.first = text;
58
+ u.last = text;
59
+ if (u.count <= 8)
60
+ this.#record('uncaptured', text);
61
+ if (hooks.onHeal && isRecoverableAttachmentMismatch(text)) {
62
+ const now = performance.now();
63
+ if (now - this.#lastHeal >= HEAL_COOLDOWN_MS) {
64
+ this.#lastHeal = now;
65
+ this.heals++;
66
+ this.#record('heal', `attachment-size mismatch → forcing a resize (attempt ${this.heals})`);
67
+ hooks.onHeal('attachment-size-mismatch');
68
+ }
69
+ }
70
+ };
71
+ device.addEventListener('uncapturederror', onError);
72
+ void device.lost.then((info) => {
73
+ if (!attached)
74
+ return;
75
+ this.state.status = 'lost';
76
+ this.state.reason = info.reason || 'unknown';
77
+ this.state.message = info.message || null;
78
+ this.state.at = performance.now();
79
+ this.#record('device-lost', `reason=${this.state.reason} message=${info.message || '(none)'}`);
80
+ hooks.onLost?.({ reason: this.state.reason, message: info.message ?? '' });
81
+ });
82
+ return () => {
83
+ attached = false;
84
+ device.removeEventListener('uncapturederror', onError);
85
+ };
86
+ }
87
+ /** Marks a fresh device after recovery, keeping the history. */
88
+ reset() {
89
+ this.state.status = 'ok';
90
+ this.state.reason = null;
91
+ this.state.message = null;
92
+ this.state.at = null;
93
+ this.note('device recovered');
94
+ }
95
+ /** Everything needed to diagnose a black canvas, as one JSON-able object. */
96
+ diagnostics() {
97
+ return {
98
+ at: new Date().toISOString(),
99
+ userAgent: typeof navigator === 'undefined' ? '' : navigator.userAgent,
100
+ health: structuredClone(this.state),
101
+ heals: this.heals,
102
+ events: this.events.slice(),
103
+ };
104
+ }
105
+ }
106
+ //# sourceMappingURL=gpu-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-health.js","sourceRoot":"","sources":["../src/gpu-health.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { PassTimer } from './frame-graph.ts';
2
+ /**
3
+ * Per-pass GPU timings from `timestamp-query` (DESIGN.md §12, §19). Results arrive a few
4
+ * frames late through a ring of readback buffers, so the frame never waits on the GPU.
5
+ *
6
+ * Browsers quantize timestamps (on the order of 100 µs, DESIGN.md §22), so read these as
7
+ * budgets averaged over frames, not precise single-frame measurements.
8
+ */
9
+ export declare class GpuTimer implements PassTimer {
10
+ #private;
11
+ readonly enabled: boolean;
12
+ /** Latest duration per pass slot, in milliseconds (NaN until the first result). */
13
+ readonly ms: Float64Array;
14
+ /** Readbacks completed so far; it changes when `ms` holds a new frame's results. */
15
+ results: number;
16
+ constructor(device: GPUDevice, enabled: boolean, maxPasses?: number);
17
+ /** GPU objects this timer owns (for the dispose leak check). */
18
+ get liveResources(): number;
19
+ /** Destroys the query set and buffers. */
20
+ destroy(): void;
21
+ timestampWrites(index: number): GPUComputePassTimestampWrites | undefined;
22
+ /** Call after recording the frame's passes, before `finish()`. */
23
+ resolve(encoder: GPUCommandEncoder): GPUBuffer | null;
24
+ /** Call after `queue.submit` with what `resolve` returned. */
25
+ collect(buffer: GPUBuffer | null): void;
26
+ }
@@ -0,0 +1,93 @@
1
+ const RING = 3;
2
+ /**
3
+ * Per-pass GPU timings from `timestamp-query` (DESIGN.md §12, §19). Results arrive a few
4
+ * frames late through a ring of readback buffers, so the frame never waits on the GPU.
5
+ *
6
+ * Browsers quantize timestamps (on the order of 100 µs, DESIGN.md §22), so read these as
7
+ * budgets averaged over frames, not precise single-frame measurements.
8
+ */
9
+ export class GpuTimer {
10
+ enabled;
11
+ /** Latest duration per pass slot, in milliseconds (NaN until the first result). */
12
+ ms;
13
+ /** Readbacks completed so far; it changes when `ms` holds a new frame's results. */
14
+ results = 0;
15
+ #device;
16
+ #set = null;
17
+ #resolve = null;
18
+ #ring = [];
19
+ #slots;
20
+ #writes = [];
21
+ #destroyed = false;
22
+ constructor(device, enabled, maxPasses = 32) {
23
+ this.#device = device;
24
+ this.enabled = enabled;
25
+ this.#slots = maxPasses;
26
+ this.ms = new Float64Array(maxPasses).fill(Number.NaN);
27
+ if (!enabled)
28
+ return;
29
+ const count = maxPasses * 2;
30
+ this.#set = device.createQuerySet({ label: 'yali:timestamps', type: 'timestamp', count });
31
+ this.#resolve = device.createBuffer({ label: 'yali:timestamps:resolve', size: count * 8, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC });
32
+ for (let i = 0; i < RING; i++) {
33
+ this.#ring.push({
34
+ buffer: device.createBuffer({ label: `yali:timestamps:read${i}`, size: count * 8, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }),
35
+ busy: false,
36
+ });
37
+ }
38
+ for (let i = 0; i < maxPasses; i++) {
39
+ this.#writes.push({ querySet: this.#set, beginningOfPassWriteIndex: i * 2, endOfPassWriteIndex: i * 2 + 1 });
40
+ }
41
+ }
42
+ /** GPU objects this timer owns (for the dispose leak check). */
43
+ get liveResources() {
44
+ return this.#destroyed || !this.enabled ? 0 : 2 + this.#ring.length;
45
+ }
46
+ /** Destroys the query set and buffers. */
47
+ destroy() {
48
+ if (this.#destroyed)
49
+ return;
50
+ this.#destroyed = true;
51
+ this.#set?.destroy();
52
+ this.#resolve?.destroy();
53
+ for (const r of this.#ring)
54
+ r.buffer.destroy();
55
+ }
56
+ timestampWrites(index) {
57
+ return this.enabled && index < this.#slots ? this.#writes[index] : undefined;
58
+ }
59
+ /** Call after recording the frame's passes, before `finish()`. */
60
+ resolve(encoder) {
61
+ if (!this.#set || !this.#resolve)
62
+ return null;
63
+ const free = this.#ring.find((r) => !r.busy);
64
+ if (!free)
65
+ return null; // every readback still in flight: skip this frame's sample
66
+ encoder.resolveQuerySet(this.#set, 0, this.#slots * 2, this.#resolve, 0);
67
+ encoder.copyBufferToBuffer(this.#resolve, 0, free.buffer, 0, this.#slots * 16);
68
+ free.busy = true;
69
+ return free.buffer;
70
+ }
71
+ /** Call after `queue.submit` with what `resolve` returned. */
72
+ collect(buffer) {
73
+ if (!buffer)
74
+ return;
75
+ const slot = this.#ring.find((r) => r.buffer === buffer);
76
+ void buffer.mapAsync(GPUMapMode.READ).then(() => {
77
+ const t = new BigInt64Array(buffer.getMappedRange());
78
+ for (let i = 0; i < this.#slots; i++) {
79
+ const d = Number(t[i * 2 + 1] - t[i * 2]);
80
+ if (d > 0)
81
+ this.ms[i] = d / 1e6;
82
+ }
83
+ buffer.unmap();
84
+ this.results++;
85
+ if (slot)
86
+ slot.busy = false;
87
+ }, () => {
88
+ if (slot)
89
+ slot.busy = false;
90
+ });
91
+ }
92
+ }
93
+ //# sourceMappingURL=gpu-timer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-timer.js","sourceRoot":"","sources":["../src/gpu-timer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type YaliPlugin } from '@yali-engine/core';
2
+ /** What `headlessRenderPlugin` records of the instances it extracts. */
3
+ export interface HeadlessFrameStats {
4
+ /** Frames that reached Submit. */
5
+ frames: number;
6
+ /** Instances extracted in the last frame. */
7
+ instances: number;
8
+ /** An order-sensitive checksum of the last frame's extracted instance records (FNV-1a). */
9
+ checksum: number;
10
+ }
11
+ /**
12
+ * The render plugin for a run without a GPU (DESIGN.md §19 headless mode; ADR-0041): it
13
+ * registers as `@yali-engine/render`, so game plugins that depend on it build unchanged, and
14
+ * provides a `render` resource with what game code uses: the camera (updated each frame,
15
+ * as the real plugin does), the mesh and material stores, `render.Mesh`, and the counters.
16
+ * It runs the same extract as the real plugin and checksums the records instead of
17
+ * drawing them. GPU members (`gpu`, `scene`, `graph`, `timer`, `pipelines`, `health`,
18
+ * `profile`, `viewport`) are absent; `capture()` rejects with `NO_GPU`.
19
+ *
20
+ * @example
21
+ * const app = App.create({ core, seed: 1, frames: manual, clock: fixed, plugins: [headlessRenderPlugin(), game] });
22
+ * await app.init();
23
+ * for (let i = 0; i < 120; i++) app.step(1 / 60);
24
+ * app.resource<RenderContext & { stats: HeadlessFrameStats }>('render').stats.checksum;
25
+ */
26
+ export declare function headlessRenderPlugin(options?: {
27
+ aspect?: number;
28
+ }): YaliPlugin;