@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,633 @@
1
+ import { YaliError } from '@yali-engine/core';
2
+ import { OrbitCamera } from "./camera.js";
3
+ import { DebugLines, DebugLinesPass } from "./debug-lines.js";
4
+ import { acquireGpu } from "./device.js";
5
+ import { InstanceExtractor } from "./extract.js";
6
+ import { MaterialStore, MeshStore, registerMeshComponent } from "./meshes.js";
7
+ import { DEVICE_FILTER, collectDeviceProfile, selectTier } from "./device-profile.js";
8
+ import { FrameGraph } from "./frame-graph.js";
9
+ import { GpuHealth } from "./gpu-health.js";
10
+ import { GpuTimer } from "./gpu-timer.js";
11
+ import { PipelineCache } from "./pipeline-cache.js";
12
+ import { Environment } from "./environment.js";
13
+ import { ClusteredLights, registerLightComponent } from "./lights.js";
14
+ import { SunShadows } from "./shadows.js";
15
+ import { SceneRenderer } from "./scene.js";
16
+ import { pickFamily, TextureStore } from "./textures.js";
17
+ import { ToneMapper } from "./tonemap.js";
18
+ import { gradingDefaults } from "./post-options.js";
19
+ import { Viewport } from "./viewport.js";
20
+ /** Shadow cascades, their size and the shadow distance per tier (ADR-0068): T0's two 1024² cascades over 150 m are ~0.2 m texels at the far end. */
21
+ const SHADOW_DEFAULTS = {
22
+ t0: { cascades: 2, size: 1024, distance: 80, filter: 'hard' },
23
+ t1: { cascades: 3, size: 1024, distance: 120, filter: 'soft' },
24
+ t2: { cascades: 3, size: 2048, distance: 150, filter: 'soft' },
25
+ };
26
+ /** The texture size class per tier (ADR-0060). */
27
+ const TEXTURE_SIZE = { t0: 512, t1: 1024, t2: 2048 };
28
+ /** Render scales the governor may use, best first (DESIGN.md §4: 0.5×–1.0× in a few steps,
29
+ * so transient targets are rebuilt rarely). */
30
+ const RESOLUTION_LEVELS = [1, 0.9, 0.8, 0.7, 0.6, 0.5];
31
+ /** The scene's colour target: linear HDR, tone-mapped to the swapchain by `post.tonemap`. */
32
+ export const HDR_FORMAT = 'rgba16float';
33
+ /** Builds everything that lives on a device; used at init and again on recovery. */
34
+ async function buildPipeline(canvas, capacity, bundles, staging, options, store, materials, texturesFor, accept, debug) {
35
+ const gpu = await acquireGpu(canvas, { compat: options.compat, accept });
36
+ // Setup errors are not caught by GpuHealth (it attaches afterwards): scope them, and fail
37
+ // loudly instead of drawing from resources a rejected command left empty.
38
+ gpu.device.pushErrorScope('validation');
39
+ const { textures, tier: tierName, disable } = await texturesFor(gpu);
40
+ const t0 = performance.now();
41
+ const pipelines = new PipelineCache(gpu.device);
42
+ // The geometry and textures are re-uploaded from the stores, which outlive devices
43
+ // (recovery rebuilds this from them).
44
+ const po = options.post;
45
+ const hdr = options.hdr ?? (tierName !== 't0' || !!(po && (po.bloom || po.taa || po.fxaa || po.grading)));
46
+ const scene = new SceneRenderer(gpu.device, hdr ? HDR_FORMAT : gpu.format, store, materials, textures, capacity);
47
+ const environment = new Environment(gpu.device);
48
+ scene.environment = environment;
49
+ const lights = new ClusteredLights(gpu.device);
50
+ scene.lights = lights;
51
+ const so = options.shadows === false || (options.shadows === undefined && disable.has('shadows')) ? null : (options.shadows ?? {});
52
+ const shadows = new SunShadows(gpu.device, {
53
+ cascades: so ? (so.cascades ?? SHADOW_DEFAULTS[tierName].cascades) : 0,
54
+ size: so?.size ?? SHADOW_DEFAULTS[tierName].size,
55
+ distance: so?.distance ?? SHADOW_DEFAULTS[tierName].distance,
56
+ splitLambda: so?.splitLambda,
57
+ filter: so?.filter ?? SHADOW_DEFAULTS[tierName].filter,
58
+ });
59
+ scene.shadows = shadows;
60
+ Object.assign(scene.lighting, options.lighting ?? {});
61
+ // The post chain needs the HDR target (ADR-0092); grading rides in the tone-mapping pass.
62
+ // Its passes come from `@yali-engine/render/post`, so a page without them carries none of their bytes.
63
+ const passes = hdr && (po?.bloom || po?.taa || po?.fxaa) ? (po.passes ?? null) : null;
64
+ if (hdr && (po?.bloom || po?.taa || po?.fxaa) && !passes) {
65
+ throw Object.assign(new YaliError('POST_PASSES', 'Bloom, TAA and FXAA need the post chain\'s passes.', "import { postPasses } from '@yali-engine/render/post' and pass post: { passes: postPasses, ... }."), { path: 'post.passes' });
66
+ }
67
+ const bloom = passes && po?.bloom ? new passes.Bloom(gpu.device, po.bloom === true ? {} : po.bloom) : null;
68
+ // Compatibility mode cannot load from a depth texture: no TAA there.
69
+ const taa = passes && po?.taa && gpu.backend !== 'webgpu-compat' ? new passes.Taa(gpu.device, po.taa === true ? undefined : po.taa.blend, po.taa !== true && po.taa.upscale === true) : null;
70
+ if (taa && po?.taa && po.taa !== true) {
71
+ // Before init: the pass after TAA exists only when asked for (ADR-0119).
72
+ taa.sharpen = po.taa.sharpen ?? 0;
73
+ taa.motionBlur = po.taa.motionBlur ?? 0;
74
+ }
75
+ const fxaa = passes && po?.fxaa ? new passes.Fxaa(gpu.device, gpu.format) : null;
76
+ const tonemap = new ToneMapper(gpu.device, fxaa ? 'rgba8unorm' : gpu.format);
77
+ tonemap.curve = options.toneMapping ?? 'neutral';
78
+ tonemap.exposure = options.exposure ?? 1;
79
+ tonemap.grading = gradingDefaults(po?.grading);
80
+ if (!hdr)
81
+ scene.inlineTone = tonemap; // read each frame: the knobs stay live
82
+ scene.useBundles = options.bundles ?? (bundles && !disable.has('bundles'));
83
+ scene.shadingLevels = (options.dynamicShading ?? tierName === 't0') ? 3 : 1;
84
+ scene.shaderVariant = options.shaderVariant ?? null;
85
+ scene.useStaging = staging ?? (gpu.backend !== 'webgpu-compat' && !disable.has('staging'));
86
+ const vertexStorage = gpu.device.limits.maxStorageBuffersInVertexStage;
87
+ const mode = options.instances ?? 'auto';
88
+ // The storage path finds each draw's range through firstInstance, which needs
89
+ // `indirect-first-instance`; without it (or without vertex-stage storage) the vertex path.
90
+ const storageOk = (vertexStorage === undefined || vertexStorage >= 2) && gpu.features.has('indirect-first-instance');
91
+ scene.path = mode === 'auto' ? (storageOk && !disable.has('storage-instances') ? 'storage' : 'vertex') : mode;
92
+ const cull = options.cull ?? 'auto';
93
+ scene.cull = cull === 'auto' ? (gpu.backend === 'webgpu-compat' || disable.has('gpu-cull') ? 'cpu' : 'gpu') : cull;
94
+ scene.occlusion = options.occlusion ?? null;
95
+ const debugPass = new DebugLinesPass(gpu.device, scene.format, scene.cameraBuffer);
96
+ scene.overlays.push((pass) => debugPass.encode(pass, debug));
97
+ await Promise.all([scene.init(pipelines), tonemap.init(pipelines), environment.init(pipelines), lights.init(pipelines), debugPass.init(pipelines), bloom?.init(pipelines), taa?.init(pipelines), fxaa?.init(pipelines)]);
98
+ if (options.meshlets && scene.cull === 'gpu' && scene.path === 'storage') {
99
+ const clusters = await options.meshlets.create(gpu.device, scene, store);
100
+ await clusters.init(pipelines);
101
+ scene.clusters = clusters;
102
+ }
103
+ if (options.sky)
104
+ environment.setSky(options.sky);
105
+ const setupError = await gpu.device.popErrorScope();
106
+ if (setupError) {
107
+ throw new YaliError('RENDER_SETUP', `Building the renderer's GPU state failed validation: ${setupError.message}`, 'This is an engine bug; report it with the message.');
108
+ }
109
+ const shaderMs = performance.now() - t0;
110
+ const timer = new GpuTimer(gpu.device, gpu.features.has('timestamp-query'));
111
+ const graph = new FrameGraph(gpu.device);
112
+ graph.timer = timer;
113
+ graph.importTexture('backbuffer', () => gpu.context.getCurrentTexture().createView());
114
+ // TAA and occlusion's pyramid sample the depth after the draw.
115
+ graph.transientTexture('depth', { format: 'depth32float', usage: GPUTextureUsage.RENDER_ATTACHMENT | (taa || scene.occlusionActive ? GPUTextureUsage.TEXTURE_BINDING : 0) });
116
+ lights.addPass(graph);
117
+ if (hdr) {
118
+ const target = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
119
+ // Transmission samples a copy of the opaque scene (ADR-0099): the target is copied from.
120
+ scene.screenTransmission = true;
121
+ graph.transientTexture('scene.hdr', { format: HDR_FORMAT, usage: target | GPUTextureUsage.COPY_SRC });
122
+ scene.addPasses(graph, 'scene.hdr', 'depth', !!taa);
123
+ // scene.hdr -> [post.taa] -> [bloom chain] -> post.tonemap (+ grading) -> [post.fxaa] -> backbuffer
124
+ let color = 'scene.hdr';
125
+ if (taa) {
126
+ // Per-object motion where the scene can draw it (ADR-0098); elsewhere the camera's only.
127
+ if (po?.taa === true || (po?.taa && po.taa.motion !== false))
128
+ taa.motionFrom(scene);
129
+ color = taa.addPass(graph, color, 'depth');
130
+ }
131
+ bloom?.addPasses(graph, color);
132
+ if (fxaa) {
133
+ graph.transientTexture('post.ldr', { format: 'rgba8unorm', usage: target, output: true });
134
+ tonemap.addPass(graph, color, 'post.ldr', bloom?.output);
135
+ fxaa.addPass(graph, 'post.ldr', 'backbuffer');
136
+ }
137
+ else
138
+ tonemap.addPass(graph, color, 'backbuffer', bloom?.output);
139
+ }
140
+ else
141
+ scene.addPasses(graph, 'backbuffer', 'depth');
142
+ graph.setOutput('backbuffer');
143
+ return { gpu, scene, debugPass, tonemap, bloom, taa, fxaa, environment, lights, shadows, graph, timer, pipelines, shaderMs };
144
+ }
145
+ function destroyPipeline(p, destroyDevice) {
146
+ p.scene.clusters?.destroy?.();
147
+ p.scene.destroy();
148
+ p.debugPass.destroy();
149
+ p.tonemap.destroy();
150
+ p.bloom?.destroy();
151
+ p.taa?.destroy();
152
+ p.environment.destroy();
153
+ p.lights.destroy();
154
+ p.shadows.destroy();
155
+ p.timer.destroy();
156
+ p.graph.destroy();
157
+ if (destroyDevice) {
158
+ p.gpu.context.unconfigure();
159
+ p.gpu.device.destroy();
160
+ }
161
+ }
162
+ /**
163
+ * The render plugin (DESIGN.md §11–§14). It acquires a WebGPU device (core, then
164
+ * compatibility mode) or, where the browser has no WebGPU, falls back to WebGL2 (ADR-0070),
165
+ * selects the tier, draws extracted instances each frame (Submit stage), and rebuilds its
166
+ * whole device state after device loss without restarting the app (DESIGN.md §3).
167
+ *
168
+ * @example
169
+ * const app = App.create({ plugins: [renderPlugin({ canvas }), gamePlugin] });
170
+ * await app.init();
171
+ * app.start();
172
+ */
173
+ export function renderPlugin(options) {
174
+ let ctx = null;
175
+ return {
176
+ name: '@yali-engine/render',
177
+ version: '0.4.0',
178
+ async build(b) {
179
+ const lc = b.app.lifecycle;
180
+ const bundles = options.bundles ?? true;
181
+ const viewport = new Viewport(options.canvas, options.viewport);
182
+ const staging = options.staging;
183
+ const store = new MeshStore();
184
+ const materials = new MaterialStore();
185
+ const camera = new OrbitCamera();
186
+ const debug = new DebugLines(camera);
187
+ // `render.Mesh` exists only with a world (an App created with a core).
188
+ let world = null;
189
+ try {
190
+ world = b.app.world;
191
+ }
192
+ catch {
193
+ // no core, no world: meshes are not available
194
+ }
195
+ const Mesh = world ? registerMeshComponent(world) : null;
196
+ const Light = world ? registerLightComponent(world) : null;
197
+ const extractor = world && Mesh ? new InstanceExtractor(world, Mesh) : null;
198
+ const rules = options.deviceFilter ?? DEVICE_FILTER;
199
+ let textures = null;
200
+ let firstSelection = null;
201
+ // The profile is collected once (it opens a WebGL context to read the renderer string).
202
+ let firstProfile = null;
203
+ // The tier and the device filter, decided on the first adapter before a device or the
204
+ // canvas context exists: a row that sends this GPU to WebGL2 leaves the canvas free for it.
205
+ const accept = async (adapter, backend) => {
206
+ if (firstSelection)
207
+ return; // a recovery: decided on the first device
208
+ firstProfile = await collectDeviceProfile(adapter);
209
+ firstSelection = selectTier(backend, firstProfile, rules, options.tier);
210
+ if (firstSelection.useWebGL2 && options.backend !== 'webgpu') {
211
+ throw new YaliError('WEBGPU_FILTERED', `The device filter sends this GPU to WebGL2 (${firstSelection.matched.join(', ')}).`, 'Nothing to fix: pass backend: "webgpu" to keep WebGPU anyway.');
212
+ }
213
+ };
214
+ // Created on the first device (its family and tier decide the arrays), re-attached on recovery.
215
+ const texturesFor = async (g) => {
216
+ if (!textures) {
217
+ const t = firstSelection;
218
+ const family = options.textureFamily ?? (t.disable.includes('compressed-textures') ? 'rgba8' : pickFamily(g.features));
219
+ textures = new TextureStore({ family, size: options.textureSize ?? TEXTURE_SIZE[t.tier] });
220
+ }
221
+ textures.attach(g.device);
222
+ const s = firstSelection;
223
+ return { textures, tier: s.tier, disable: new Set(s.disable) };
224
+ };
225
+ let pipeline = null;
226
+ if (options.backend !== 'webgl2') {
227
+ try {
228
+ pipeline = await buildPipeline(options.canvas, options.capacity ?? 65_536, bundles, staging, options, store, materials, texturesFor, accept, debug);
229
+ }
230
+ catch (err) {
231
+ // No WebGPU here at all, or a device-filter row against it: the WebGL2 fallback
232
+ // (unless the app asked for WebGPU only).
233
+ const code = err instanceof YaliError ? err.code : '';
234
+ if (options.backend === 'webgpu' || (code !== 'WEBGPU_UNAVAILABLE' && code !== 'WEBGPU_NO_ADAPTER' && code !== 'WEBGPU_FILTERED'))
235
+ throw err;
236
+ }
237
+ }
238
+ if (pipeline)
239
+ ctx = startWebGpu(pipeline);
240
+ else {
241
+ // Loaded only here: a WebGPU page carries none of the WebGL2 backend's bytes.
242
+ const { startWebGL2 } = await import("./webgl2/webgl2.js");
243
+ ctx = await startWebGL2({ b, options, viewport, camera, debug, store, materials, world, Mesh, Light, extractor, rules, tierSizes: { texture: TEXTURE_SIZE, shadows: SHADOW_DEFAULTS } });
244
+ }
245
+ // Temporal upscaling (ADR-0135), only where TAA runs: the canvas at full size, renderScale for the scene.
246
+ viewport.upscale = !!pipeline?.taa?.upscale;
247
+ b.provide('render', ctx);
248
+ // What both backends share: the camera, the extract, the counters and the quality knobs.
249
+ b.counter('render.pixels', () => viewport.width * viewport.height, 'px');
250
+ b.counter('render.scale', () => viewport.renderScale);
251
+ b.counter('render.instances', () => extractor?.count ?? 0);
252
+ b.counter('render.visible', () => ctx?.scene.visible ?? 0);
253
+ b.counter('render.draws', () => ctx?.scene.stats.draws ?? 0);
254
+ b.counter('render.lights', () => ctx?.lights.count ?? 0);
255
+ b.counter('render.debugLines', () => debug.drawn / 2);
256
+ b.counter('render.gpuSceneBytes', () => ctx?.scene.gpuBytes ?? 0, 'bytes');
257
+ b.counter('render.gpuTextureBytes', () => ctx?.textures.gpuBytes ?? 0, 'bytes');
258
+ b.counter('render.transientBytes', () => ctx?.graph?.transientBytes ?? 0, 'bytes');
259
+ // Coarser LODs and no sub-pixel instances before a lower resolution: registered first,
260
+ // so the governor tries it first (ADR-0050). A step that saves nothing is undone.
261
+ if (options.dynamicLod ?? true)
262
+ b.quality({ name: 'render.lodBias', levels: [0, 1, 2, 3], cost: 'gpu', apply: (v) => (ctx.scene.lodBias = v) });
263
+ // Cheaper shading before a lower resolution (ADR-0072): only where it was compiled (T0 by default).
264
+ if (ctx.scene.shadingLevels > 1)
265
+ b.quality({ name: 'render.shading', levels: [0, 1, 2], cost: 'gpu', apply: (v) => (ctx.scene.shadingLevel = v) });
266
+ if (options.dynamicResolution ?? true) {
267
+ const initial = viewport.renderScale;
268
+ const levels = [initial, ...RESOLUTION_LEVELS.filter((v) => v < initial)];
269
+ b.quality({ name: 'render.scale', levels, cost: 'gpu', apply: (v) => (viewport.renderScale = v) });
270
+ }
271
+ // Camera before Extract, so extract systems read this frame's eye.
272
+ b.system({
273
+ name: 'render.camera',
274
+ stage: 'PostUpdate',
275
+ run: (f) => {
276
+ viewport.update();
277
+ camera.time = f.time.virtual;
278
+ camera.update(viewport.width / viewport.height);
279
+ },
280
+ });
281
+ // Every `render.Mesh` instance, extracted by the core in one call (camera-relative,
282
+ // grouped by mesh), uploaded in Submit.
283
+ b.system({
284
+ name: 'render.extract',
285
+ stage: 'Extract',
286
+ run: () => {
287
+ if (!extractor)
288
+ return;
289
+ const r = ctx;
290
+ if (r.scene.cull === 'cpu') {
291
+ const heap = extractor.world.core.heap;
292
+ heap.refresh();
293
+ r.scene.writeCullParams(heap.f32, extractor.cullAt >>> 2, camera, viewport.height, viewport.width);
294
+ extractor.runCulled(camera.eye, store);
295
+ }
296
+ else
297
+ extractor.run(camera.eye, store.entries.length);
298
+ },
299
+ });
300
+ b.onDispose(() => extractor?.dispose());
301
+ /** The WebGPU backend: its context, Submit system, recovery and teardown. */
302
+ function startWebGpu(first) {
303
+ let p = first;
304
+ const { gpu } = p;
305
+ lc.emit({ type: 'init:adapter', backend: gpu.backend, adapterInfo: gpu.adapterInfo });
306
+ const l = gpu.device.limits;
307
+ lc.emit({
308
+ type: 'init:capabilities',
309
+ features: [...gpu.features],
310
+ limits: { maxStorageBufferBindingSize: l.maxStorageBufferBindingSize, maxBufferSize: l.maxBufferSize },
311
+ });
312
+ const profile = firstProfile;
313
+ const tier = firstSelection;
314
+ lc.emit({ type: 'init:device-filter', matched: tier.matched });
315
+ lc.emit({ type: 'init:tier', tier: tier.tier, source: tier.source });
316
+ lc.emit({ type: 'init:shaders', pipelines: p.pipelines.stats.pipelines, ms: first.shaderMs });
317
+ const health = new GpuHealth();
318
+ let simulated = false;
319
+ // Frames in flight on the GPU and frames not drawn for it (ADR-0129); one callback, no per-frame closure.
320
+ let inFlight = 0;
321
+ let skipped = 0;
322
+ const finished = () => {
323
+ inFlight = Math.max(0, inFlight - 1); // a lost device's frames settle after the reset
324
+ };
325
+ let disposing = false;
326
+ let detach = () => { };
327
+ const watch = (q) => {
328
+ detach = health.attach(q.gpu.device, {
329
+ onLost: (info) => {
330
+ if (disposing)
331
+ return;
332
+ void b.app.deviceLost({
333
+ reason: simulated ? 'unknown' : info.reason === 'destroyed' ? 'destroyed' : 'unknown',
334
+ message: simulated ? 'simulated device loss (simulateDeviceLoss)' : info.message,
335
+ });
336
+ simulated = false;
337
+ },
338
+ onHeal: () => p.graph.invalidate(),
339
+ });
340
+ };
341
+ watch(p);
342
+ const captures = [];
343
+ const submits = [];
344
+ const c = {
345
+ backend: gpu.backend,
346
+ gpu: p.gpu,
347
+ scene: p.scene,
348
+ tonemap: p.tonemap,
349
+ post: {
350
+ get bloom() {
351
+ return !!p.bloom;
352
+ },
353
+ get fxaa() {
354
+ return !!p.fxaa;
355
+ },
356
+ get taa() {
357
+ return !!p.taa;
358
+ },
359
+ get bloomIntensity() {
360
+ return p.bloom?.intensity ?? 0;
361
+ },
362
+ set bloomIntensity(v) {
363
+ if (p.bloom)
364
+ p.bloom.intensity = v;
365
+ },
366
+ get bloomThreshold() {
367
+ return p.bloom?.threshold ?? 0;
368
+ },
369
+ set bloomThreshold(v) {
370
+ if (p.bloom)
371
+ p.bloom.threshold = v;
372
+ },
373
+ get bloomKnee() {
374
+ return p.bloom?.knee ?? 0;
375
+ },
376
+ set bloomKnee(v) {
377
+ if (p.bloom)
378
+ p.bloom.knee = v;
379
+ },
380
+ get bloomRadius() {
381
+ return p.bloom?.radius ?? 0;
382
+ },
383
+ set bloomRadius(v) {
384
+ if (p.bloom)
385
+ p.bloom.radius = v;
386
+ },
387
+ get taaBlend() {
388
+ return p.taa?.blend ?? 0;
389
+ },
390
+ set taaBlend(v) {
391
+ if (p.taa)
392
+ p.taa.blend = v;
393
+ },
394
+ get taaSharpen() {
395
+ return p.taa?.sharpen ?? 0;
396
+ },
397
+ set taaSharpen(v) {
398
+ if (p.taa)
399
+ p.taa.sharpen = v;
400
+ },
401
+ get motionBlur() {
402
+ return p.taa?.motionBlur ?? 0;
403
+ },
404
+ set motionBlur(v) {
405
+ if (p.taa)
406
+ p.taa.motionBlur = v;
407
+ },
408
+ get grading() {
409
+ return p.tonemap.grading;
410
+ },
411
+ resetHistory() {
412
+ p.taa?.reset();
413
+ },
414
+ readMotion() {
415
+ return p.taa ? p.taa.readMotion() : Promise.resolve(null);
416
+ },
417
+ },
418
+ environment: p.environment,
419
+ lights: p.lights,
420
+ shadows: p.shadows,
421
+ Light,
422
+ meshes: store,
423
+ materials,
424
+ textures: textures,
425
+ Mesh,
426
+ graph: p.graph,
427
+ timer: p.timer,
428
+ pipelines: p.pipelines,
429
+ viewport,
430
+ camera,
431
+ debug,
432
+ prepasses: [],
433
+ health,
434
+ profile,
435
+ tier: tier.tier,
436
+ recoveries: 0,
437
+ submitMs: 0,
438
+ uploadMs: 0,
439
+ maxFramesInFlight: 0,
440
+ get framesInFlight() {
441
+ return inFlight;
442
+ },
443
+ get skippedFrames() {
444
+ return skipped;
445
+ },
446
+ capture() {
447
+ return new Promise((resolve, reject) => captures.push({ resolve, reject }));
448
+ },
449
+ simulateDeviceLoss() {
450
+ simulated = true;
451
+ p.gpu.device.destroy();
452
+ },
453
+ };
454
+ // Profiling (ADR-0025): sub-scopes of the submit system, GPU passes by name, counters.
455
+ const profiler = b.app.profiler;
456
+ const upload = profiler.scope('render.upload', 'Submit');
457
+ const encode = profiler.scope('render.encode', 'Submit');
458
+ const queueSubmit = profiler.scope('render.queue-submit', 'Submit');
459
+ let passNames = [];
460
+ let passScopes = [];
461
+ let seenResults = 0;
462
+ b.counter('render.pipelines', () => p.pipelines.stats.pipelines);
463
+ /** The camera's offset from the GPU-cull records' anchor (ADR-0075); zero for camera-relative records. */
464
+ const origin = new Float32Array(3);
465
+ b.system({
466
+ name: 'render.submit',
467
+ stage: 'Submit',
468
+ run: () => {
469
+ if (health.state.status === 'lost')
470
+ return; // recovering: draw nothing
471
+ // The GPU is `maxFramesInFlight` frames behind: this frame is not drawn (ADR-0129).
472
+ if (c.maxFramesInFlight > 0 && inFlight >= c.maxFramesInFlight) {
473
+ skipped++;
474
+ return;
475
+ }
476
+ const t0 = performance.now();
477
+ p.graph.resize(viewport.width, viewport.height, viewport.outputWidth, viewport.outputHeight);
478
+ p.scene.setTargetSize(viewport.width, viewport.height);
479
+ profiler.begin(upload);
480
+ textures.pump(1);
481
+ for (let i = 0; i < 3; i++)
482
+ origin[i] = extractor && !extractor.culled ? camera.eye[i] - extractor.anchor[i] : 0;
483
+ if (p.taa) {
484
+ p.taa.setOrigin(origin);
485
+ p.taa.prepare(camera, viewport.width, viewport.height, viewport.outputWidth, viewport.outputHeight);
486
+ p.scene.jitter = p.taa.jitter;
487
+ }
488
+ // Each level of the chain holds about the scene's energy: the weight is shared out.
489
+ if (p.bloom)
490
+ p.tonemap.bloom = p.bloom.intensity / p.bloom.levels;
491
+ p.scene.setCamera(camera, viewport.height, origin);
492
+ p.lights.update(world, Light, camera, viewport.width, viewport.height);
493
+ p.shadows.update(camera, viewport.width / viewport.height, origin);
494
+ if (extractor) {
495
+ const core = extractor.world.core;
496
+ p.scene.ensureCapacity(extractor.count);
497
+ p.scene.upload(core.heap.u8, core.heap.u32, extractor);
498
+ }
499
+ profiler.end(upload);
500
+ c.uploadMs = performance.now() - t0;
501
+ profiler.begin(encode);
502
+ const encoder = p.gpu.device.createCommandEncoder();
503
+ p.scene.recordUpload(encoder);
504
+ const pool = p.scene.vertexPool;
505
+ const indices = p.scene.indexPool;
506
+ if (pool && indices)
507
+ for (const f of c.prepasses)
508
+ f(encoder, pool, indices);
509
+ p.graph.execute(encoder);
510
+ if (p.taa?.motion)
511
+ p.scene.recordMotionCopies(encoder);
512
+ const timing = p.timer.resolve(encoder);
513
+ const readVisible = p.scene.afterPasses(encoder);
514
+ const grab = captures.length ? recordCapture(p.gpu, encoder) : null;
515
+ // A capture frame runs in a validation scope: a failed submit must reject the
516
+ // capture, never hand back an empty frame (a blank golden image is a silent pass).
517
+ if (grab)
518
+ p.gpu.device.pushErrorScope('validation');
519
+ submits[0] = encoder.finish();
520
+ profiler.end(encode);
521
+ profiler.begin(queueSubmit);
522
+ p.gpu.device.queue.submit(submits);
523
+ if (c.maxFramesInFlight > 0) {
524
+ inFlight++;
525
+ void p.gpu.device.queue.onSubmittedWorkDone().then(finished, finished);
526
+ }
527
+ p.scene.afterSubmit();
528
+ profiler.end(queueSubmit);
529
+ p.timer.collect(timing);
530
+ if (readVisible)
531
+ p.scene.collect();
532
+ if (grab) {
533
+ const waiting = captures.splice(0);
534
+ const scope = p.gpu.device.popErrorScope();
535
+ scope
536
+ .then((err) => {
537
+ if (err)
538
+ throw new YaliError('CAPTURE_FAILED', `The captured frame's submit failed validation: ${err.message}`, 'See the message; the frame was not drawn.');
539
+ return grab();
540
+ })
541
+ .then((cap) => waiting.forEach((w) => w.resolve(cap)), (e) => waiting.forEach((w) => w.reject(e)));
542
+ }
543
+ c.submitMs = performance.now() - t0;
544
+ debug.clear();
545
+ // GPU pass durations, once per readback that completed since the last frame.
546
+ if (p.timer.results !== seenResults) {
547
+ seenResults = p.timer.results;
548
+ if (p.graph.passNames !== passNames) {
549
+ passNames = p.graph.passNames;
550
+ passScopes = passNames.map((n) => profiler.gpuScope(n));
551
+ }
552
+ for (let i = 0; i < passScopes.length; i++)
553
+ profiler.gpu(passScopes[i], p.timer.ms[i]);
554
+ }
555
+ },
556
+ });
557
+ b.recover(async () => {
558
+ detach();
559
+ inFlight = 0; // the lost device's frames never finish
560
+ destroyPipeline(p, false); // the device is already gone
561
+ const next = await buildPipeline(options.canvas, p.scene.capacity, bundles, staging, options, store, materials, texturesFor, accept, debug);
562
+ next.tonemap.exposure = p.tonemap.exposure;
563
+ next.tonemap.curve = p.tonemap.curve;
564
+ next.tonemap.grading = p.tonemap.grading;
565
+ if (next.bloom && p.bloom)
566
+ Object.assign(next.bloom, { intensity: p.bloom.intensity, threshold: p.bloom.threshold, knee: p.bloom.knee, radius: p.bloom.radius });
567
+ if (next.taa && p.taa)
568
+ next.taa.blend = p.taa.blend;
569
+ Object.assign(next.scene.lighting, p.scene.lighting);
570
+ next.environment.setSky(p.environment.sky);
571
+ next.shadows.distance = p.shadows.distance;
572
+ next.scene.shadingLevel = p.scene.shadingLevel;
573
+ p = next;
574
+ Object.assign(c, { gpu: next.gpu, scene: next.scene, tonemap: next.tonemap, environment: next.environment, lights: next.lights, shadows: next.shadows, graph: next.graph, timer: next.timer, pipelines: next.pipelines });
575
+ c.recoveries++;
576
+ seenResults = 0;
577
+ health.reset();
578
+ watch(next);
579
+ return { devices: 1, pipelines: next.pipelines.stats.pipelines, buffers: next.scene.liveBuffers + next.debugPass.liveBuffers, timers: next.timer.liveResources };
580
+ });
581
+ b.leaks(() => ({
582
+ gpuResources: p.scene.liveBuffers + p.debugPass.liveBuffers + p.timer.liveResources + p.graph.liveTextures + (p.taa?.liveTextures ?? 0),
583
+ listeners: viewport.listeners,
584
+ }));
585
+ b.onDispose(() => {
586
+ disposing = true;
587
+ viewport.dispose();
588
+ detach();
589
+ textures?.detach();
590
+ destroyPipeline(p, true);
591
+ ctx = null;
592
+ });
593
+ return c;
594
+ }
595
+ },
596
+ };
597
+ }
598
+ /**
599
+ * Records a copy of this frame's canvas texture into a readback buffer on `encoder` (so it
600
+ * sees exactly what the passes drew). Call the returned function after `queue.submit`: it
601
+ * maps the buffer (mapping before the submit would invalidate it) and resolves with
602
+ * tightly packed RGBA8.
603
+ */
604
+ function recordCapture(gpu, encoder) {
605
+ const texture = gpu.context.getCurrentTexture();
606
+ const w = texture.width;
607
+ const h = texture.height;
608
+ const bytesPerRow = Math.ceil((w * 4) / 256) * 256; // copies need 256-byte row alignment
609
+ const buffer = gpu.device.createBuffer({ label: 'yali:capture', size: bytesPerRow * h, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
610
+ encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [w, h]);
611
+ const bgra = gpu.format === 'bgra8unorm';
612
+ return () => buffer.mapAsync(GPUMapMode.READ).then(() => {
613
+ const src = new Uint8Array(buffer.getMappedRange());
614
+ const rgba = new Uint8Array(w * h * 4);
615
+ for (let y = 0; y < h; y++) {
616
+ const s = y * bytesPerRow;
617
+ const d = y * w * 4;
618
+ for (let x = 0; x < w * 4; x += 4) {
619
+ rgba[d + x] = src[s + x + (bgra ? 2 : 0)];
620
+ rgba[d + x + 1] = src[s + x + 1];
621
+ rgba[d + x + 2] = src[s + x + (bgra ? 0 : 2)];
622
+ rgba[d + x + 3] = src[s + x + 3];
623
+ }
624
+ }
625
+ buffer.unmap();
626
+ buffer.destroy();
627
+ return { width: w, height: h, rgba };
628
+ }, (err) => {
629
+ buffer.destroy();
630
+ throw err;
631
+ });
632
+ }
633
+ //# sourceMappingURL=render-plugin.js.map