@woosh/meep-engine 3.11.1 → 3.11.2

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 (73) hide show
  1. package/package.json +1 -1
  2. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  4. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  6. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  7. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  10. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  13. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  14. package/src/shade/playground/volumetrics_froxel/README.md +104 -1
  15. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  17. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  18. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  21. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  22. package/src/shade/renderer/Renderer.js +22 -18
  23. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  24. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  25. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  27. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  29. package/src/shade/renderer/volumetrics/NOTES.md +649 -8
  30. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  31. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  33. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  34. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  36. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  41. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  42. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  44. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  45. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  46. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  47. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  48. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  49. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  50. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  54. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  55. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  56. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
  57. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
  59. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  60. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  62. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  63. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  64. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  65. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  66. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  67. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  68. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  69. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  70. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  71. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  72. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  73. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -0,0 +1,184 @@
1
+ import { assert } from "../../core/assert.js";
2
+ import { isArrayEqualStrict } from "../../core/collection/array/isArrayEqualStrict.js";
3
+ import { ParticipatingMediaVolume } from "../../shade/renderer/volumetrics/ParticipatingMediaVolume.js";
4
+ import { System } from "../ecs/System.js";
5
+ import { Transform } from "../ecs/transform/Transform.js";
6
+ import { ParticipatingMedia } from "./ParticipatingMedia.js";
7
+
8
+ /**
9
+ * Mirrors fog, smoke and cloud entities into Shade's volumetrics.
10
+ *
11
+ * The same shape as {@link LightSystem3}: the ECS says what exists and where, and this keeps the
12
+ * renderer's own objects in step. A {@link ParticipatingMediaVolume} is a unit cube posed by a
13
+ * transform, so an entity's `Transform` is the volume — position, size and orientation with no
14
+ * translation step in between.
15
+ *
16
+ * **The transform is taken as world-space, with no hierarchy applied**, which is what the volume
17
+ * model can express: a volume is an oriented box in the scene and Shade has no parent chain to walk
18
+ * for it. Parenting a fog volume to a moving entity therefore leaves the fog where the child's own
19
+ * transform puts it. The lights are placed the same way, for the same reason.
20
+ *
21
+ * **Changes are polled once a frame rather than subscribed to.** A `Transform` announces itself and
22
+ * nothing else here does — a density tuned by an editor slider, a particle spec swapped wholesale
23
+ * for a different Mie solution — so subscribing would cover one field in three and the other two
24
+ * would still have to be polled. Two paths that have to agree, where one does the whole job. It
25
+ * costs about two dozen float comparisons per volume per frame and a scene holds volumes in the
26
+ * tens, so the comparison is cheaper than the signal plumbing it replaces. The price is latency: a
27
+ * change is visible within a frame rather than instantly.
28
+ *
29
+ * **Nothing is uploaded unless something actually changed.** The GPU table is repacked whole
30
+ * whenever {@link SceneVolumetrics.version} moves, so an unconditional copy every frame would
31
+ * re-upload every volume in the scene forever. The comparison is what decides, and on almost every
32
+ * frame it decides nothing has to happen.
33
+ *
34
+ * @see ParticipatingMedia the component
35
+ * @see SceneVolumetrics what the volumes are handed to
36
+ */
37
+ export class ParticipatingMediaSystem3 extends System {
38
+
39
+ dependencies = [ParticipatingMedia, Transform];
40
+
41
+ /**
42
+ * @type {GraphicsEngine3}
43
+ */
44
+ #graphics;
45
+
46
+ /**
47
+ * @type {Scene}
48
+ */
49
+ #scene;
50
+
51
+ /**
52
+ * @type {Map<number, {component: ParticipatingMedia, transform: Transform, volume: ParticipatingMediaVolume}>}
53
+ */
54
+ #entries = new Map();
55
+
56
+ /**
57
+ * @param {GraphicsEngine3} graphics
58
+ * @param {Scene} scene
59
+ */
60
+ constructor(graphics, scene) {
61
+ super();
62
+
63
+ assert.defined(graphics, 'graphics');
64
+ assert.defined(scene, 'scene');
65
+ assert.equal(scene.isScene, true, 'scene.isScene !== true');
66
+
67
+ this.#graphics = graphics;
68
+ this.#scene = scene;
69
+ }
70
+
71
+ async startup(entityManager) {
72
+ this.entityManager = entityManager;
73
+
74
+ this.#graphics.set_scene(this.#scene);
75
+ }
76
+
77
+ async shutdown(entityManager) {
78
+ for (const entity of Array.from(this.#entries.keys())) {
79
+ this.#drop(entity);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {ParticipatingMedia} component
85
+ * @param {Transform} transform
86
+ * @param {number} entity
87
+ */
88
+ link(component, transform, entity) {
89
+ const volume = new ParticipatingMediaVolume();
90
+
91
+ participating_media_volume_write(volume, component, transform);
92
+
93
+ this.#scene.volumetrics.add(volume);
94
+
95
+ this.#entries.set(entity, { component, transform, volume });
96
+ }
97
+
98
+ /**
99
+ * @param {ParticipatingMedia} component
100
+ * @param {Transform} transform
101
+ * @param {number} entity
102
+ */
103
+ unlink(component, transform, entity) {
104
+ this.#drop(entity);
105
+ }
106
+
107
+ /**
108
+ * @param {number} time_delta_seconds
109
+ */
110
+ update(time_delta_seconds) {
111
+ const volumetrics = this.#scene.volumetrics;
112
+
113
+ for (const entry of this.#entries.values()) {
114
+ const volume = entry.volume;
115
+
116
+ if (participating_media_volume_is_current(volume, entry.component, entry.transform)) {
117
+ continue;
118
+ }
119
+
120
+ participating_media_volume_write(volume, entry.component, entry.transform);
121
+
122
+ volumetrics.invalidate();
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {number} entity
128
+ */
129
+ #drop(entity) {
130
+ const entry = this.#entries.get(entity);
131
+
132
+ if (entry === undefined) {
133
+ return;
134
+ }
135
+
136
+ this.#scene.volumetrics.remove(entry.volume);
137
+
138
+ this.#entries.delete(entity);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Copy what the component and its transform say onto the volume Shade will render.
144
+ *
145
+ * The particle spec is **copied, not referenced**: two entities are routinely built from the same
146
+ * blueprint and would otherwise share one spec, so tuning one volume's particle would silently
147
+ * retune the other's.
148
+ *
149
+ * @param {ParticipatingMediaVolume} volume Shade's
150
+ * @param {ParticipatingMedia} component meep's
151
+ * @param {Transform} transform where the box is
152
+ */
153
+ export function participating_media_volume_write(volume, component, transform) {
154
+ volume.density = component.density;
155
+ volume.fade_distance = component.fade_distance;
156
+
157
+ volume.particle_spec.copy(component.particle_spec);
158
+
159
+ volume.transform.copy(transform);
160
+ }
161
+
162
+ /**
163
+ * Whether the volume already says what the component and transform say.
164
+ *
165
+ * True on all but a handful of frames, which is the point of asking: a `false` here costs the whole
166
+ * scene's volumetrics table a trip to the GPU.
167
+ *
168
+ * **The transform is compared by its matrix rather than by `Transform.equals`**, because the matrix
169
+ * is the entire transform the GPU is given — the volume's position, rotation and scale are never
170
+ * read on their own. That makes this exactly the question worth asking, "would the upload differ",
171
+ * and it catches a transform posed by writing its matrix, which comparing position, rotation and
172
+ * scale would not.
173
+ *
174
+ * @param {ParticipatingMediaVolume} volume
175
+ * @param {ParticipatingMedia} component
176
+ * @param {Transform} transform
177
+ * @returns {boolean}
178
+ */
179
+ export function participating_media_volume_is_current(volume, component, transform) {
180
+ return volume.density === component.density
181
+ && volume.fade_distance === component.fade_distance
182
+ && isArrayEqualStrict(volume.transform.matrix, transform.matrix)
183
+ && volume.particle_spec.equals(component.particle_spec);
184
+ }
@@ -64,11 +64,11 @@ export class FluidObstacleSystem extends System<any> {
64
64
  /**
65
65
  * @param {FluidComponent} fluid
66
66
  */
67
- static "__#354@#refresh_masks"(fluid: FluidComponent): void;
67
+ static "__#356@#refresh_masks"(fluid: FluidComponent): void;
68
68
  /**
69
69
  * @param {FluidComponent} fluid
70
70
  */
71
- static "__#354@#clear_field"(fluid: FluidComponent): void;
71
+ static "__#356@#clear_field"(fluid: FluidComponent): void;
72
72
  /**
73
73
  * Mark every cell of `fluid` whose centre lies within `inflation` of the
74
74
  * posed shape as solid. Iteration is clipped to the shape's world AABB
@@ -80,7 +80,7 @@ export class FluidObstacleSystem extends System<any> {
80
80
  * @param {number} inflation world-units SDF threshold
81
81
  * @param {Float64Array} point length-3 scratch
82
82
  */
83
- static "__#354@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
83
+ static "__#356@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
84
84
  /**
85
85
  * Write the obstacle's translation velocity onto every face of every cell
86
86
  * it voxelized — the moving-wall boundary condition. Runs AFTER the mask
@@ -100,7 +100,7 @@ export class FluidObstacleSystem extends System<any> {
100
100
  * @param {number} wvy
101
101
  * @param {number} wvz
102
102
  */
103
- static "__#354@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
103
+ static "__#356@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
104
104
  constructor();
105
105
  dependencies: (typeof FluidObstacle)[];
106
106
  components_used: (ResourceAccessSpecification<typeof Transform> | ResourceAccessSpecification<typeof RigidBody> | ResourceAccessSpecification<typeof Collider> | ResourceAccessSpecification<typeof FluidComponent> | ResourceAccessSpecification<typeof FluidObstacle>)[];
@@ -30,6 +30,36 @@ Those fixes are in the permanent path now and there is nothing left to A/B. What
30
30
  the *next* change here — the performance findings especially, since every one of them trades image
31
31
  quality for time and none can be judged from a screenshot.
32
32
 
33
+ Q-01 was judged here and now ships unconditionally: every clustered point and spot light is occluded
34
+ by geometry, sampled with one nearest tap out of the shadow atlas. The two checkboxes that switched
35
+ that off, or swapped the tap for the deferred pass's filter kernels, are gone along with the metadata
36
+ word behind them — `volumetrics/NOTES.md` keeps the measurements that closed the question. The
37
+ torches are the local lights here: 130 point lights, every one of them a shadow caster, and **zero
38
+ spot lights**, so the spot half has no on-screen coverage on this page and is covered by
39
+ `shader_volumetrics_build_lighting_local_shadow.spec.js` instead. Adding a `SpotLight` from the
40
+ console is the way to look at it.
41
+
42
+ Q-04 went the same way and its switch is gone too. The sun's media self-shadowing is one fetch into
43
+ a ladder of three cascaded volumes built once a frame with their third axis on the sun, and that is
44
+ now the only path for it: the `__VOL_SUN_SHADOW` global and the checkbox over it are removed along
45
+ with the `sun_shadow_mode` word they drove, so nothing on this page reproduces the march/cascade
46
+ comparison any more. Restoring it means re-adding that word and the branch in
47
+ `shader_volumetrics_build_lighting` that reads it.
48
+
49
+ Worth knowing what it was, because it is the size of what the page can no longer show. Against a
50
+ march at `STEP_COUNT = 512` the cascades read within 1.2 mean luminance at every viewpoint here and
51
+ across 40 to 110 degrees of field of view, while sixteen steps read **+58** and **+70** at the two
52
+ poses beside the small fog boxes — the step there is eight units, the box is three thick, and the
53
+ march goes straight over it. They are also cheaper: 0.37 ms against 0.53 at 1080p, 0.81 against 1.52
54
+ at 2160p, per pass. `volumetrics/NOTES.md` has the whole measurement, including the one that says a
55
+ *single* world-uniform box could not have been sized correctly at any resolution — every shape
56
+ measured has a field of view at which it is 28 to 50 luminance from the truth, and which one that is
57
+ moves with the resolution.
58
+
59
+ The march itself is still in the tree and still runs: every point and spot light uses it, and so
60
+ would a second directional light. Only the first one is amortized, because there is one ladder of
61
+ boxes and it is aligned to one direction.
62
+
33
63
  ## Measuring, rather than eyeballing
34
64
 
35
65
  This pass is stochastic and temporally filtered, so most changes to it move the image by less than
@@ -50,6 +80,14 @@ confident wrong answer during the work above:
50
80
  *change → settle ~45 frames → average*, and take the floor by making an identical no-op change so
51
81
  both paths cost the same.
52
82
 
83
+ - **Frame time here is presentation-throttled, not GPU-bound.** At 1920×1080 every setting reads
84
+ ~16.5 ms, including settings that do obviously different amounts of work — `ms_octaves` at 1 and at
85
+ 8 come back within noise of each other, which is the tell. The swap chain is pacing the
86
+ measurement. Raise `renderer.pixel_ratio` until the GPU is the bottleneck (3 gives 3840×2160 and
87
+ ~10 ms of real work here), interleave the settings round-robin so drift hits all of them equally,
88
+ and take medians. Remember the froxel grid scales with resolution, so a per-froxel cost measured at
89
+ 4× the pixels is 4× the one you will pay.
90
+
53
91
  Prefer the **signed mean** when the change has a direction; it discriminates far better than the
54
92
  mean-absolute, which is dominated by noise. C-01b read −0.148 at 60° FOV against a floor of ±0.05,
55
93
  and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predicts.
@@ -57,6 +95,68 @@ and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predi
57
95
  Also: mutating `light.intensity` does not mark the light buffer dirty, so a sweep of it renders
58
96
  identically. Remove and re-add the light instead.
59
97
 
98
+ ### measure.js
99
+
100
+ `measure.js` is that protocol as code, loaded from the console rather than by the page:
101
+
102
+ ```js
103
+ const m = await (await import('./measure.js')).attach();
104
+ const r = await m.ab(v => globalThis.__VOL_TAA_ENABLED = v, false, true);
105
+ ```
106
+
107
+ `attach` pauses the page's loop — which matters for a reason beyond frame counting: that loop
108
+ assigns `renderer.pixel_ratio = window.devicePixelRatio` on every tick, ahead of its own pause check,
109
+ so a raised pixel ratio is undone before the next frame unless the loop is stopped. `set_pixel_ratio`
110
+ replaces the property for the same reason.
111
+
112
+ Everything renders into a texture the module owns, through `Renderer.render_to_target`, which shares
113
+ the view context and therefore all the temporal history with the canvas path. That is not a
114
+ refinement: a canvas texture is configured by the presentation path, cannot carry `COPY_SRC`, and is
115
+ rotated away the moment the frame is presented, so a `drawImage` off it a task later returns an empty
116
+ bitmap — and a comparison of two blanks reports perfect agreement. Driving the frames from here also
117
+ takes the swap chain out of the timing entirely, which is the presentation-throttling trap above.
118
+
119
+ `ab` runs the two settings interleaved and takes its own floor from a repeat of each. `fly`,
120
+ `converge_at` and `series_stats` are the camera-path half: a structure fitted to the camera can be
121
+ perfectly correct standing still and swim under motion, and neither swimming nor the ghosting the
122
+ 0.95-blend TAA turns it into is visible in a still. `time` and `profile` answer different questions and
123
+ `profile` is the one to believe — at 1080p, whole-frame timing of Q-04 came back with the wrong sign
124
+ three runs in a row while the per-pass capture read it cleanly.
125
+
126
+ `profile` records a `WORKLOAD` session and decodes it in the page, so a cost number does not need a
127
+ downloaded `.sgpt` and a second tool. It returns the median duration per pass label over the frames
128
+ it recorded. Interleave the settings and take medians across rounds even so — the run-to-run spread
129
+ on this machine moves a pass by more than most changes to it do:
130
+
131
+ ```js
132
+ const p = await m.profile({ frames: 24 });
133
+ p['Volumetrics / build sun shadow volume 0'];
134
+ ```
135
+
136
+ ## Recording a GPU profile
137
+
138
+ **T** starts a recording, **T** again stops it and downloads a `.sgpt` capture. The engine side is
139
+ already there — `Renderer.profile_session` is a nullable field and the null checks around it are the
140
+ whole integration — so this page is only the two imports, the key handler, and the metadata a capture
141
+ needs in order to mean anything later: adapter, engine version, device features, and a note carrying
142
+ the viewpoint, FOV, pixel ratio and volumetric TAA setting that were in force when you pressed the
143
+ key.
144
+
145
+ Level is `WORKLOAD`: pass spans, the frame graph, and dispatch/draw counts. Measured on this page it
146
+ runs about 11 KB a frame — roughly 660 KB/s at 60 Hz, and around 393 spans per frame — with the frame
147
+ graph topology stored once and shared by every frame. `VERBOSE` above it adds bind group contents at
148
+ about seven times the rate and wants a frame limit set; `PROFILE_LEVEL` in `main.js` is the one line
149
+ to change.
150
+
151
+ Two things to know. Stopping does not end the recording immediately: a frame is committed when its
152
+ timestamp readback lands, a `mapAsync` a frame or two behind submit, so the handler drains the queue
153
+ and waits for the frame count to settle before it writes the file — otherwise the last frames, the
154
+ ones you were probably looking at, are the ones missing. And the browser asks before letting a page
155
+ save more than one file, so a second capture may never appear; the filename goes to the console
156
+ either way.
157
+
158
+ Frames only accumulate while the page is rendering, so a recording taken while paused stays at zero.
159
+
60
160
  ## Scene
61
161
 
62
162
  `flying_world_-_battle_of_the_trash_god/v2`, six participating-media volumes, a low sun, and the
@@ -80,7 +180,10 @@ accepts version 2 only, deliberately and with no backward compatibility.
80
180
  ## Notes
81
181
 
82
182
  Two viewpoints from the sibling repo (`v_volumetrics_light_extinction_0` and `_1`) sit inside
83
- geometry here and were replaced with placements verified against this build. *Log camera* writes the
183
+ geometry here and were replaced with placements verified against this build. Two more
184
+ *Beside the fog box* and *Beside the fog box, deeper* — were added for Q-04: they are the only
185
+ placements in the scene where the sun's path through the medium is short enough for the 16-step
186
+ march to miss it, which makes them the ones any change to that path has to be judged at. *Log camera* writes the
84
187
  current placement to the console in the form the `VIEWS` table takes, which is how to add more.
85
188
 
86
189
  Frame time settles around 28 ms at 1920×1080 on a 4070-class part. That is the whole frame, not the
@@ -117,12 +117,14 @@
117
117
  #status.fail { color: var(--fail); }
118
118
  #status.warn { color: var(--warn); }
119
119
 
120
- #stats {
120
+ #stats, #profile_status {
121
121
  margin-top: 4px;
122
122
  font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
123
123
  color: var(--text);
124
124
  }
125
125
 
126
+ #profile_status.recording { color: var(--fail); }
127
+
126
128
  p.note { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
127
129
  </style>
128
130
  </head>
@@ -220,6 +222,23 @@
220
222
 
221
223
  <hr>
222
224
 
225
+ <h2>Profiling</h2>
226
+
227
+ <div id="profile_status">idle</div>
228
+ <p class="note">
229
+ <strong>T</strong> starts a GPU profile recording; <strong>T</strong> again stops it and
230
+ downloads the <code>.sgpt</code> capture. Level is <code>WORKLOAD</code> — pass spans, the
231
+ frame graph, and dispatch/draw counts, which measures around 11 KB a frame on this page.
232
+ Frames only accumulate while the page is rendering, so a recording taken while paused stays
233
+ at zero.
234
+ </p>
235
+ <p class="note">
236
+ The browser asks before letting a page save more than one file. If a second capture never
237
+ appears, that prompt is why — allow it, and the filename is on the console either way.
238
+ </p>
239
+
240
+ <hr>
241
+
223
242
  <div class="row">
224
243
  <button id="pause">Pause</button>
225
244
  <button id="log_camera">Log camera</button>
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA+TA,uCA2SC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA8ZA,uCAsfC"}