@woosh/meep-engine 3.10.0 → 3.11.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.
- package/README.md +168 -152
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06.md +610 -610
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +28 -4
- package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +26 -0
- package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
- package/src/engine/graphics/render/frame_graph/FrameGraph.js +47 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +0 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +0 -3
- package/src/shade/device/ShadeGPUCommandContext.d.ts +21 -0
- package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
- package/src/shade/device/ShadeGPUCommandContext.js +940 -749
- package/src/shade/device/timing/GPUTimerArray.d.ts +22 -4
- package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
- package/src/shade/device/timing/GPUTimerArray.js +88 -14
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +349 -98
- package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts +93 -0
- package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUFrameRecorder.js +297 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.d.ts +62 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.js +77 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.d.ts +73 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.js +88 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.d.ts +20 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.js +50 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.d.ts +63 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.js +73 -0
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts +173 -0
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSession.js +470 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.d.ts +60 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.js +65 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts +19 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.js +25 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.d.ts +159 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.js +202 -0
- package/src/shade/device/timing/profile/GPUProfileWork.d.ts +90 -0
- package/src/shade/device/timing/profile/GPUProfileWork.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileWork.js +115 -0
- package/src/shade/device/timing/profile/SGPTDefect.d.ts +43 -0
- package/src/shade/device/timing/profile/SGPTDefect.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTDefect.js +65 -0
- package/src/shade/device/timing/profile/SGPTHeader.d.ts +38 -0
- package/src/shade/device/timing/profile/SGPTHeader.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTHeader.js +46 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.d.ts +37 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.js +45 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.d.ts +103 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.js +287 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts +19 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.js +26 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.d.ts +8 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.js +15 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +345 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts +10 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.js +9 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts +17 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.js +16 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts +16 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.js +15 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts +15 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.js +29 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.js +6 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.js +6 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts +16 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.js +15 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.js +6 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts +13 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.js +23 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts +12 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.js +11 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts +18 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.js +46 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts +20 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.js +174 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +21 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +1 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +114 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +20 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +1 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.js +43 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.d.ts +18 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.js +34 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts +42 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.js +172 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts +20 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.js +65 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.d.ts +19 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.js +134 -0
- package/src/shade/device/timing/profile/sgpt_read_header.d.ts +22 -0
- package/src/shade/device/timing/profile/sgpt_read_header.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_header.js +94 -0
- package/src/shade/device/timing/profile/sgpt_read_records.d.ts +27 -0
- package/src/shade/device/timing/profile/sgpt_read_records.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_records.js +153 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts +54 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.js +329 -0
- package/src/shade/device/timing/profile/sgpt_write_header.d.ts +22 -0
- package/src/shade/device/timing/profile/sgpt_write_header.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_write_header.js +47 -0
- package/src/shade/device/timing/profile/sgpt_write_record.d.ts +23 -0
- package/src/shade/device/timing/profile/sgpt_write_record.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_write_record.js +62 -0
- package/src/shade/renderer/Renderer.d.ts +21 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +2513 -2455
- package/src/engine/save/GameStateLoader.d.ts +0 -49
- package/src/engine/save/GameStateLoader.d.ts.map +0 -1
- package/src/engine/save/GameStateLoader.js +0 -168
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# A graphical GPU profiler for Shade — recorder, format, inspector
|
|
2
2
|
|
|
3
|
-
Proposal, 2026-08-28. Alex Goldring / Company Named Limited.
|
|
3
|
+
Proposal, 2026-08-28. Decisions folded in 2026-08-29. Alex Goldring / Company Named Limited.
|
|
4
4
|
|
|
5
5
|
Scope: a **recording** side that ships with the engine, a **binary container** that carries a
|
|
6
6
|
capture, and an **inspector** web application under `packages/gpu-inspector-tool/` that reads the
|
|
@@ -9,6 +9,11 @@ container and nothing else.
|
|
|
9
9
|
> The request spelled the directory `gpu-inespector-tool`. Reading that as a typo and using
|
|
10
10
|
> `packages/gpu-inspector-tool/` throughout. Say the word if the misspelling was deliberate.
|
|
11
11
|
|
|
12
|
+
**Four decisions are settled and written into the design below** — npm workspaces with the existing
|
|
13
|
+
tree moved to conform (§4.2), a user-instantiated recorder passed in rather than a renderer-owned one
|
|
14
|
+
(§6.1), the minimal `FrameGraph` bracket via `Signal` (§1.4), and an open-ended uncapped stream of
|
|
15
|
+
self-contained frame records rather than a ring buffer (§5.8). §11 records them and what each costs.
|
|
16
|
+
|
|
12
17
|
---
|
|
13
18
|
|
|
14
19
|
## 0. Verdict
|
|
@@ -141,21 +146,25 @@ feature, which additionally needs the device to expose `timestamp-query`.
|
|
|
141
146
|
passes. **The median pass is below the quantum.** Unmitigated, the tool reports a histogram of zeros
|
|
142
147
|
with occasional 100 µs spikes, and every conclusion drawn from it is noise.
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
**The decision is to leave it entirely alone.** Not measure it, not calibrate against it, not
|
|
150
|
+
correct for it, not store a period in the container. Rejected 2026-08-29, and the reasoning is
|
|
151
|
+
worth keeping because it is not obvious:
|
|
152
|
+
|
|
153
|
+
- Recovering the quantum — GCD over observed timestamps, or any equivalent — **is the timing attack
|
|
154
|
+
the mitigation exists to prevent.** Doing it *well* means defeating a browser security control on
|
|
155
|
+
purpose, inside code we ship to other people.
|
|
156
|
+
- Doing it *badly* is worse than not doing it. A period that is a multiple of the real one, or a
|
|
157
|
+
stale one from a browser update, becomes a correction applied to every number in the capture. A
|
|
158
|
+
profiler that silently skews its own data is worse than one that reports coarse data honestly.
|
|
159
|
+
- Either way it is standing complexity and a source of fragility, bought for something the reader
|
|
160
|
+
can be told in one sentence.
|
|
161
|
+
|
|
162
|
+
So: **note it, document it, do not treat it.** The capture records what the device reported —
|
|
163
|
+
zeros included. `GPUProfileMeta` documents that browsers quantize and names the flag. The inspector
|
|
164
|
+
says so where a reader will see it. What to make of a coarse capture is the reader's judgement, and
|
|
165
|
+
the honest advice is the same either way: **aggregate across frames rather than trusting any single
|
|
166
|
+
one.** That shapes the format in exactly one way, which it already did — keep every frame's raw
|
|
167
|
+
values and never pre-aggregate on the recorder side.
|
|
159
168
|
|
|
160
169
|
### 2.2 Pass granularity is the floor
|
|
161
170
|
|
|
@@ -274,7 +283,7 @@ Without this a shared recording is unreadable:
|
|
|
274
283
|
- Adapter info (`vendor`, `architecture`, `device`, `description`), features, the limits that matter.
|
|
275
284
|
- Engine version, git revision, build flags, `ENV_PRODUCTION`.
|
|
276
285
|
- Renderer settings: internal vs output resolution, upscaler, which features are on.
|
|
277
|
-
-
|
|
286
|
+
- Nothing about timestamp quantization: see §2.1 for why that is a deliberate absence.
|
|
278
287
|
- Wall-clock start, session id, user-supplied note.
|
|
279
288
|
- Scene scale: mesh/instance/light counts, resident material and geometry bytes.
|
|
280
289
|
|
|
@@ -287,7 +296,7 @@ Without this a shared recording is unreadable:
|
|
|
287
296
|
───────────────────── ───────── ───────────────────────────────
|
|
288
297
|
GPUTimerArray ────┐
|
|
289
298
|
FrameGraph ───────┤ ┌── timeline / flame graph
|
|
290
|
-
proxy encoders ───┼──>
|
|
299
|
+
proxy encoders ───┼──> GPUProfileSession ───> .sgpt ──drop──> ├── dependency graph
|
|
291
300
|
GraphicsContext ──┘ │ bytes ├── resource table
|
|
292
301
|
│ ├── pass detail
|
|
293
302
|
sgpt_write_* └── frame comparison
|
|
@@ -322,13 +331,75 @@ A capture is a trade between fidelity and cost. Four levels, recorded in the hea
|
|
|
322
331
|
Levels 0–2 are the product. Level 3 is for us, on a repro, and the inspector should say so when it
|
|
323
332
|
opens one.
|
|
324
333
|
|
|
334
|
+
Because a session is now **uncapped by default** (§5.8), those per-frame figures are also a rate.
|
|
335
|
+
At 60 Hz:
|
|
336
|
+
|
|
337
|
+
| Level | Per second | Per minute |
|
|
338
|
+
|---|---|---|
|
|
339
|
+
| 0 `TIMING` | ~120 KB | ~7 MB |
|
|
340
|
+
| 1 `STRUCTURE` | ~120 KB | ~7 MB |
|
|
341
|
+
| 2 `WORKLOAD` | ~360 KB | ~21 MB |
|
|
342
|
+
| 3 `VERBOSE` | ~2.4 MB | ~144 MB |
|
|
343
|
+
|
|
344
|
+
Levels 0–2 will run for many minutes without anyone noticing. **Level 3 will not**, and the session
|
|
345
|
+
should say so — see the byte-budget warning in §5.8. This is the honest cost of "full and uncapped
|
|
346
|
+
explicit history", and it is the right trade at levels 0–2.
|
|
347
|
+
|
|
348
|
+
### 4.2 Repository layout
|
|
349
|
+
|
|
350
|
+
The repo becomes an npm workspace root, and the engine moves down a level so the two packages are
|
|
351
|
+
siblings rather than one being nested inside the other.
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
meep/
|
|
355
|
+
package.json workspace root: { "private": true, "workspaces": ["packages/*"] }
|
|
356
|
+
.gitlab-ci.yml
|
|
357
|
+
LICENSE, README.md, CONTRIBUTING.md, CHANGELOG.md, ...
|
|
358
|
+
packages/
|
|
359
|
+
meep-engine/ @woosh/meep-engine — everything that is published today
|
|
360
|
+
package.json unchanged `exports`, `files`, version
|
|
361
|
+
src/ editor/ samples/ tools/
|
|
362
|
+
rollup.config.js vite.config.mjs vitest.config.mjs tsconfig.types.json babel.config.cjs
|
|
363
|
+
gpu-inspector-tool/ the inspector, published separately
|
|
364
|
+
package.json index.html src/ fixtures/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`@woosh/meep-engine`'s own `exports` map (`./src/*`, `./editor/*`) is relative to its own
|
|
368
|
+
`package.json`, so **consumers installing from npm see no change at all**. The paths that move are
|
|
369
|
+
in-repo and in anything vendoring this tree by path.
|
|
370
|
+
|
|
371
|
+
**The cost is in `moh`, not here.** This tree is vendored into the game at
|
|
372
|
+
`app/src/mir-engine/meep/`, and that is where the restructure is felt: every engine path becomes
|
|
373
|
+
`app/src/mir-engine/meep/packages/meep-engine/src/...`. Two ways to absorb it, and this is the one
|
|
374
|
+
call the restructure actually needs:
|
|
375
|
+
|
|
376
|
+
- **Re-point the vendoring one level deeper** — mount `packages/meep-engine` as
|
|
377
|
+
`app/src/mir-engine/meep`. Every path in `moh` stays byte-identical, and the whole cost collapses
|
|
378
|
+
to one submodule/copy path. **This is the one to take** unless something in `moh` needs the
|
|
379
|
+
workspace root itself.
|
|
380
|
+
- **Update paths in `moh`** — mechanical, wide, and it churns the ~20 design documents in this tree
|
|
381
|
+
that quote `app/src/mir-engine/meep/src/...` as a source root.
|
|
382
|
+
|
|
383
|
+
In-repo edits either way: `vitest.config.mjs` `include`/`coverage.include` globs, `tsconfig.types.json`
|
|
384
|
+
`include`/`rootDir`, `rollup.config.js` `input` paths, `.gitlab-ci.yml` script and artifact paths, and
|
|
385
|
+
`vite.config.mjs`. All of them are prefix changes, and all of them get shorter, not longer, because
|
|
386
|
+
they become relative to the package rather than the root.
|
|
387
|
+
|
|
388
|
+
> Worth knowing before committing: npm lets a workspace root *also* be a package, so
|
|
389
|
+
> `packages/gpu-inspector-tool` could have been added with the engine left exactly where it is and
|
|
390
|
+
> zero paths touched. That option is on the table if the `moh` re-pointing turns out to be more
|
|
391
|
+
> awkward than it looks. Proceeding with the move as decided — it is the cleaner end state and the
|
|
392
|
+
> re-pointing above makes it cheap.
|
|
393
|
+
|
|
325
394
|
---
|
|
326
395
|
|
|
327
396
|
## 5. The `.sgpt` container
|
|
328
397
|
|
|
329
|
-
**S**hade **G**PU **P**rofile **T**race. Magic `
|
|
398
|
+
**S**hade **G**PU **P**rofile **T**race. Magic `0x54504753` — the ASCII bytes `S G P T` read as a
|
|
330
399
|
little-endian u32 at offset 0, matching the `VGEO_MAGIC` convention in
|
|
331
|
-
[VGEO_MAGIC.js](src/shade/renderer/geometry/virtual/format/header/VGEO_MAGIC.js).
|
|
400
|
+
[VGEO_MAGIC.js](packages/meep-engine/src/shade/renderer/geometry/virtual/format/header/VGEO_MAGIC.js).
|
|
401
|
+
`sgpt_fourcc.js` computes these and a spec asserts every literal against it, because hand-deriving
|
|
402
|
+
a little-endian FourCC is exactly the kind of arithmetic that is wrong once and then forever.
|
|
332
403
|
|
|
333
404
|
### 5.1 Why binary, concretely
|
|
334
405
|
|
|
@@ -347,31 +418,29 @@ can pull the header and the frame index without reading the payload.
|
|
|
347
418
|
```
|
|
348
419
|
offset size field
|
|
349
420
|
──────────────────────────────────────────────────────────────────
|
|
350
|
-
0 4 magic u32
|
|
421
|
+
0 4 magic u32 0x54504753
|
|
351
422
|
4 2 format_version u16
|
|
352
423
|
6 2 min_reader_version u16
|
|
353
|
-
8 4 flags u32 bit0 =
|
|
354
|
-
12 4 header_checksum u32 over bytes [0,
|
|
355
|
-
16 8 directory_offset u64
|
|
424
|
+
8 4 flags u32 bit0 = CLOSED (writer reached stop)
|
|
425
|
+
12 4 header_checksum u32 over bytes [0, 8) only — see below
|
|
426
|
+
16 8 directory_offset u64 0 when the session never stopped
|
|
356
427
|
24 8 directory_byte_length u64
|
|
357
|
-
32 ...
|
|
358
|
-
... ... directory
|
|
428
|
+
32 ... records, in write order
|
|
429
|
+
... ... directory record
|
|
359
430
|
```
|
|
360
431
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
12 ... payload
|
|
367
|
-
```
|
|
432
|
+
**The checksum stops at byte 8, short of `flags`.** Everything from `flags` onward is patched at
|
|
433
|
+
stop — the CLOSED bit and the directory pointer — and a checksum covering them would have to be
|
|
434
|
+
recomputed then. Fine for a capture that stops; exactly wrong for one that does not, which would
|
|
435
|
+
carry a checksum over bytes never written and read as corrupt. What stays under it is what a reader
|
|
436
|
+
must trust before it can do anything at all: is this an `.sgpt`, and can this build read it.
|
|
368
437
|
|
|
369
438
|
### 5.3 Chunks
|
|
370
439
|
|
|
371
440
|
| FourCC | Purpose | Cardinality |
|
|
372
441
|
|---|---|---|
|
|
373
|
-
| `META` | §3.5 context. Adapter, engine version, settings,
|
|
374
|
-
| `
|
|
442
|
+
| `META` | §3.5 context. Adapter, engine version, settings, verbosity level, session note. | 1 |
|
|
443
|
+
| `SYMS` | Symbol block. Names, labels, formats, scope names — emitted **inside the record that first needs them** and referenced by u32 index thereafter, so a truncated capture still resolves every name it references (§5.8). | 0..n |
|
|
375
444
|
| `PIPE` | Pipeline table: id, label, kind, shader module name, `@workgroup_size`, entry point, vertex layout digest. | 1 |
|
|
376
445
|
| `TOPO` | Frame-graph **topologies**: pass list, resource node list, edges, scopes, descriptors. Content-hashed and deduplicated across frames (§5.4). | 0..n |
|
|
377
446
|
| `FRAM` | One per frame: topology id, CPU timestamps, span array, dispatch/draw array, counters. | 0..n |
|
|
@@ -430,41 +499,155 @@ is precisely why it is an export and not the storage format.
|
|
|
430
499
|
|
|
431
500
|
### 5.7 String encoding
|
|
432
501
|
|
|
433
|
-
|
|
434
|
-
deduplicates by writing back-references to **absolute buffer
|
|
435
|
-
([EncodingBinaryBuffer.js:21](src/core/binary/EncodingBinaryBuffer.js:21)), which is
|
|
436
|
-
single flat buffer and wrong the moment
|
|
437
|
-
|
|
438
|
-
|
|
502
|
+
Symbols live in `SYMS` blocks with u32 indices, emitted inside the record that first needs them
|
|
503
|
+
(§5.8). Not `EncodingBinaryBuffer`: it deduplicates by writing back-references to **absolute buffer
|
|
504
|
+
positions** ([EncodingBinaryBuffer.js:21](src/core/binary/EncodingBinaryBuffer.js:21)), which is
|
|
505
|
+
correct for a single flat buffer and wrong the moment records are skipped by a reader that does not
|
|
506
|
+
understand them, resynchronised after corruption, or read from a truncated file — all three of which
|
|
507
|
+
this format is explicitly built to survive. An index table costs one indirection and holds up under
|
|
508
|
+
all of them.
|
|
509
|
+
|
|
510
|
+
### 5.8 Streaming: self-contained frame records
|
|
511
|
+
|
|
512
|
+
The session is an **open-ended forward stream with an explicit stop**. No ring, no cap: `frame_limit`
|
|
513
|
+
defaults to `Infinity`, `stop()` ends it early, and everything recorded is kept.
|
|
514
|
+
|
|
515
|
+
**A frame is written as one complete, self-describing record and pushed when it closes.** That is
|
|
516
|
+
the organising decision, and three properties follow from it:
|
|
517
|
+
|
|
518
|
+
- **It is resynchronisable.** Every record opens with a sync marker, its type, its payload length and
|
|
519
|
+
a checksum. A reader that lands on garbage scans forward for the next marker and carries on. A
|
|
520
|
+
capture cut off mid-session — tab killed, GPU reset, `device.lost` — stays readable up to the last
|
|
521
|
+
intact record, and those are exactly the captures worth having.
|
|
522
|
+
- **Symbols travel with the record that introduces them.** A record carrying a new topology carries
|
|
523
|
+
the names that topology needs; a frame record reusing a known topology carries none. There is no
|
|
524
|
+
global string table to finalise, which is what would otherwise force a tail chunk and make a
|
|
525
|
+
truncated file unreadable — the failure mode where the one capture of the crash resolves no names.
|
|
526
|
+
- **Nothing is back-patched except the header's directory pointer**, and even that is optional: a
|
|
527
|
+
reader can recover the whole stream by scanning records when the directory is missing or the file
|
|
528
|
+
was truncated before `stop()` ran.
|
|
529
|
+
|
|
530
|
+
Order on the wire:
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
header magic, versions, flags; directory_offset patched at stop, 0 if never stopped
|
|
534
|
+
META at start — adapter, engine version, renderer settings, level
|
|
535
|
+
TOPO #0 + its symbols first frame's topology
|
|
536
|
+
FRAM #0 topology id + spans + counts
|
|
537
|
+
FRAM #1 same topology → id only
|
|
538
|
+
...
|
|
539
|
+
TOPO #1 + its symbols shadow refresh changed the graph
|
|
540
|
+
FRAM #57
|
|
541
|
+
...
|
|
542
|
+
CNTR, DIRE at stop — the directory is an index, not a dependency
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Record framing, every record identical:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
0 4 sync u32 0x43455253 ('SREC')
|
|
549
|
+
4 4 record_type u32 FourCC
|
|
550
|
+
8 4 payload_byte_length u32
|
|
551
|
+
12 4 payload_checksum u32 crc32 over the payload
|
|
552
|
+
16 ... payload
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
u32 for the length rather than u64: a record describes one frame's control flow, and resource
|
|
556
|
+
*contents* are a non-goal, so nothing in one scales with the size of what it describes. Four
|
|
557
|
+
gigabytes is not a ceiling anything can approach.
|
|
558
|
+
|
|
559
|
+
**The sync marker alone is not enough to resynchronise on**, and the reader does not treat it as
|
|
560
|
+
though it were. Four bytes of payload can spell `SREC` — a pass label could — so a candidate counts
|
|
561
|
+
only when its length also fits and its payload also checksums. There is a spec for exactly that
|
|
562
|
+
case.
|
|
563
|
+
|
|
564
|
+
**Reallocation is a non-issue, and the slack reservation settles what is left of it.** We are
|
|
565
|
+
measuring GPU time, not CPU time; a `setCapacity` copy is a heap allocation and a memcpy that may
|
|
566
|
+
evict some cache, which at worst makes the next GPU upload marginally slower. That is a rounding
|
|
567
|
+
error against what is being measured. To keep even that out of the frame interior, the writer
|
|
568
|
+
reserves at the frame boundary:
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
buffer.ensureCapacity(buffer.position + FRAME_SLACK); // FRAME_SLACK = 1 MiB
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Any growth then happens between frames, never inside a record. 1 MiB against level 2's ~6 KB per
|
|
575
|
+
frame is three orders of magnitude of headroom, so the reservation only actually forces a grow once
|
|
576
|
+
every ~170 frames. One `BinaryBuffer`, no segmentation, no assembly pass.
|
|
577
|
+
|
|
578
|
+
**Scope note, and it is what makes the above generous rather than marginal: GPU buffer and texture
|
|
579
|
+
contents are a non-goal.** The stream carries control flow and dataflow metadata — passes, edges,
|
|
580
|
+
descriptors, counts, timings. Nothing in it scales with the size of the resources it describes. That
|
|
581
|
+
is why a frame record is kilobytes and why 1 MiB of slack is never the binding constraint.
|
|
582
|
+
|
|
583
|
+
**Uncapped still means uncapped, so the session stays legible about it.** `bytes_written` is readable
|
|
584
|
+
at any time and `onBytesWritten` fires per frame, so an application can show a counter. A configurable
|
|
585
|
+
`byte_budget` — default 512 MB — does not stop the recording; it warns once, naming the level and the
|
|
586
|
+
observed rate. Silent unbounded growth in a debug tool is how you lose a browser tab and the capture
|
|
587
|
+
with it.
|
|
439
588
|
|
|
440
589
|
---
|
|
441
590
|
|
|
442
591
|
## 6. The recorder
|
|
443
592
|
|
|
444
|
-
### 6.1 Surface
|
|
593
|
+
### 6.1 Surface — the caller owns the session
|
|
594
|
+
|
|
595
|
+
**The engine never constructs a profile session.** The caller builds one and hands it in; the
|
|
596
|
+
renderer holds a nullable reference and nothing more. Nothing in `Renderer` imports the profiler.
|
|
445
597
|
|
|
446
598
|
```js
|
|
599
|
+
import { GPUProfileSession } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileSession.js";
|
|
600
|
+
import { GPUProfileLevel } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileLevel.js";
|
|
601
|
+
|
|
447
602
|
const session = new GPUProfileSession({
|
|
448
|
-
graphics,
|
|
449
603
|
level: GPUProfileLevel.WORKLOAD,
|
|
450
|
-
|
|
604
|
+
frame_limit: Infinity, // default; a number caps it
|
|
451
605
|
note: "hitch on shadow refresh, RTX 3070"
|
|
452
606
|
});
|
|
453
607
|
|
|
454
|
-
session
|
|
455
|
-
|
|
456
|
-
|
|
608
|
+
renderer.profile_session = session; // nullable field, that is the whole API
|
|
609
|
+
|
|
610
|
+
session.start(); // META is written here; populate meta first
|
|
611
|
+
// ... frames run, for as long as you like ...
|
|
612
|
+
const bytes = await session.stop(); // ArrayBuffer, ready to save
|
|
613
|
+
|
|
614
|
+
renderer.profile_session = null;
|
|
457
615
|
```
|
|
458
616
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
617
|
+
Three things fall out of the caller owning it, and all three are why this is the right shape:
|
|
618
|
+
|
|
619
|
+
- **It is genuinely optional.** The profiler is a leaf module nothing in the engine imports. An
|
|
620
|
+
application that never imports `GPUProfileSession` does not have it in its bundle — the answer to
|
|
621
|
+
"does this ship in production builds" is *it ships, and it costs nothing to anyone who does not ask
|
|
622
|
+
for it*. No build flag, no strip-plugin interaction, no dead-code branch to keep honest.
|
|
623
|
+
- **Lifetime is explicit.** A session that outlives a device, or two sessions at once, are the
|
|
624
|
+
caller's problem to not create, and `start()` asserts against both rather than papering over them.
|
|
625
|
+
- **The engine surface is one nullable field.** `Renderer.profile_session`, forwarded to the
|
|
626
|
+
`GraphicsContext` so command contexts can find it. That is the entire integration.
|
|
627
|
+
|
|
628
|
+
`frame_limit` reaching zero stops the session exactly as `stop()` does. Both resolve the same
|
|
629
|
+
promise, so a caller that wants "500 frames or until I say" writes one `await`.
|
|
630
|
+
|
|
631
|
+
`add_debug_frame` and `onFrameDebug` stay exactly as they are — cheap, synchronous, console-shaped,
|
|
632
|
+
and a different tool for a different question.
|
|
462
633
|
|
|
463
634
|
### 6.2 The five hooks
|
|
464
635
|
|
|
465
|
-
1. **`FrameGraph.execute`** — bracket `node.execute(...)`. Notify
|
|
466
|
-
|
|
467
|
-
|
|
636
|
+
1. **`FrameGraph.execute`** — bracket `node.execute(...)`. Notify listeners which graph pass is open;
|
|
637
|
+
take CPU timestamps either side; push/pop a debug group. The only edit outside `src/shade/`.
|
|
638
|
+
|
|
639
|
+
Two `Signal`s on `FrameGraph`, `onPassBegin` / `onPassEnd`, alongside the `onExecuted` that is
|
|
640
|
+
already there. **Signal, not a nullable callback field** — it is the project's standardised
|
|
641
|
+
observer interface, it supports more than one listener, and `remove` is symmetric with `add`.
|
|
642
|
+
A bare nullable callback lets the second consumer silently clobber the first, which is exactly
|
|
643
|
+
the bug that does not announce itself.
|
|
644
|
+
|
|
645
|
+
Guard the dispatch with `hasHandlers()`: `send1` on an empty signal still bumps `generation` and
|
|
646
|
+
walks a Map iterator, and this fires twice per pass — a couple of hundred passes a frame. One
|
|
647
|
+
`Map.size` comparison ahead of it keeps the disabled path honest.
|
|
648
|
+
|
|
649
|
+
`FrameGraph` still learns nothing about what a profiler is; it announces its own pass boundaries
|
|
650
|
+
and the profiler is one possible listener.
|
|
468
651
|
2. **`ShadeGPUCommandContext`** — a `#profile_sink` field. When set, `beginComputePass` /
|
|
469
652
|
`beginRenderPass` report `{label, kind, graph_pass_id, query_slot, query_set_id}` and wrap the
|
|
470
653
|
returned encoder in the recording proxy.
|
|
@@ -478,10 +661,14 @@ console-shaped, and a different tool for a different question.
|
|
|
478
661
|
|
|
479
662
|
### 6.3 Cost when off
|
|
480
663
|
|
|
481
|
-
Every hook is a null check against a field that is `
|
|
482
|
-
|
|
664
|
+
Every hook is a null check against a field that is `null` in normal operation. No allocation and no
|
|
665
|
+
proxy construction; the frame-graph bracket is two comparisons per pass. This must stay true — a
|
|
483
666
|
profiler that costs something when disabled will be disabled at the build level and then rot.
|
|
484
667
|
|
|
668
|
+
Because the session is caller-constructed (§6.1), an application that never imports it pays not even
|
|
669
|
+
that: the profiler modules are unreachable from any engine entry point and drop out of the bundle
|
|
670
|
+
entirely. The null checks are the only residue, and they are the price of the feature existing.
|
|
671
|
+
|
|
485
672
|
### 6.4 Cost when on
|
|
486
673
|
|
|
487
674
|
Level 0–2 add: one `performance.now()` pair per graph pass, one small record per GPU pass, one per
|
|
@@ -514,8 +701,8 @@ other engine import.
|
|
|
514
701
|
| View | Answers |
|
|
515
702
|
|---|---|
|
|
516
703
|
| **Frame strip** | Which frame is interesting. Frame time over the session, GPU and CPU overlaid, hitches marked, brush to select a range. Drawn from the directory index alone, so it appears before the payload finishes parsing. |
|
|
517
|
-
| **Timeline / flame graph** | Where the time went in *this* frame. Spans on a GPU track, nested by `FrameGraphScope`; a CPU track above with encode time; hover for exact ns; click to select.
|
|
518
|
-
| **Statistics** | Where the time goes *in general*. Per-pass min/median/p95/max/total across the selected frame range, sorted by total contribution. **On a
|
|
704
|
+
| **Timeline / flame graph** | Where the time went in *this* frame. Spans on a GPU track, nested by `FrameGraphScope`; a CPU track above with encode time; hover for exact ns; click to select. |
|
|
705
|
+
| **Statistics** | Where the time goes *in general*. Per-pass min/median/p95/max/total across the selected frame range, sorted by total contribution. **On a quantized capture this is the only honest view** — and since the tool does not measure quantization (§2.1), it cannot detect that case and switch by itself. It says so plainly instead, always. |
|
|
519
706
|
| **Dependency graph** | Why this pass runs, and what it waits on. Passes and resource nodes, culled ones greyed, edges directed. Select a pass → highlight its transitive inputs. Select a resource → its version chain and every reader. |
|
|
520
707
|
| **Resource table** | What memory costs. Every resource node with declared bytes, format, usage, transient/imported, lifetime span, peak concurrent footprint. Sorted by size. Declared total vs `gpu_memory_usage` side by side. |
|
|
521
708
|
| **Pass detail** | Everything about one pass. Timings across frames as a sparkline, dispatch/draw counts, derived total invocations, pipeline and workgroup size, attachments, bindings at level 3. |
|
|
@@ -523,8 +710,9 @@ other engine import.
|
|
|
523
710
|
|
|
524
711
|
### 7.3 What it must refuse to do
|
|
525
712
|
|
|
526
|
-
- **Never
|
|
527
|
-
|
|
713
|
+
- **Never invent precision the data does not have.** Durations are drawn as reported. Where many
|
|
714
|
+
spans read as exactly zero, that is shown as what it is — a quantized capture — with a note
|
|
715
|
+
naming the browser flag, not smoothed into plausible-looking small numbers.
|
|
528
716
|
- **Never present the CPU and GPU tracks as one clock** (§2.3). Two tracks, anchored per frame,
|
|
529
717
|
labelled as nominal.
|
|
530
718
|
- **Never hide dropped spans.** If the recorder dropped passes on slot exhaustion (§1.2.1), say how
|
|
@@ -542,41 +730,101 @@ Before any of the below. Instrument one Sponza capture and answer three question
|
|
|
542
730
|
size estimates and one whole design decision rest on them:
|
|
543
731
|
|
|
544
732
|
1. How many distinct frame-graph topologies over 600 frames? (§5.4)
|
|
545
|
-
2. What is the observed
|
|
733
|
+
2. ~~What is the observed timestamp period~~ — dropped; the tool does not measure this (§2.1).
|
|
546
734
|
3. How many GPU passes per frame, actually? (`GPUTimerArray` default is 1024 slots; §1.2.1)
|
|
547
735
|
|
|
548
736
|
Write the answers into this document.
|
|
549
737
|
|
|
550
|
-
###
|
|
738
|
+
### M0.5 — Workspace restructure — **DONE**
|
|
551
739
|
|
|
552
|
-
`
|
|
553
|
-
`
|
|
554
|
-
|
|
740
|
+
Landed 2026-08-29. `packages/meep-engine/` holds the engine, the workspace root holds the two
|
|
741
|
+
`.gitignore` halves, the CI paths and a README. Suite green at 2156 files / 14580 tests. One spec
|
|
742
|
+
had to move with it: `meep_three_free.spec.js` asserts the `*.d.ts` ignore rule at the engine root,
|
|
743
|
+
so that rule lives in the package's `.gitignore` rather than the workspace's.
|
|
555
744
|
|
|
556
|
-
|
|
745
|
+
**`moh` still needs its vendoring re-pointed one level deeper** — mount `packages/meep-engine` where
|
|
746
|
+
it currently mounts the repository root, and every path inside it stays byte-identical.
|
|
557
747
|
|
|
558
|
-
|
|
559
|
-
capture you can look at.
|
|
748
|
+
<details><summary>Original plan</summary>
|
|
560
749
|
|
|
561
|
-
|
|
750
|
+
§4.2. Independent of everything else and worth landing first so no profiler work has to be moved
|
|
751
|
+
afterwards. Order: create `packages/meep-engine/`, `git mv` the tree, fix the five config files,
|
|
752
|
+
green the suite, re-point `moh`'s vendoring one level deeper, green `moh`. Land as its own commit —
|
|
753
|
+
a pure move with no content changes, so the diff stays reviewable and a bisect through it is honest.
|
|
562
754
|
|
|
563
|
-
|
|
564
|
-
resource table in the inspector. Deliverable: the "why is this pass here" question becomes
|
|
565
|
-
answerable.
|
|
755
|
+
</details>
|
|
566
756
|
|
|
567
|
-
###
|
|
757
|
+
### M1 — Timing spine — **DONE**
|
|
568
758
|
|
|
569
|
-
|
|
570
|
-
this dispatch the right size" question becomes answerable.
|
|
759
|
+
Landed:
|
|
571
760
|
|
|
572
|
-
|
|
761
|
+
- `FrameGraph.onPassBegin` / `onPassEnd`, guarded by `hasHandlers()`, closed in a `finally` so a
|
|
762
|
+
throwing pass still ends its bracket. Five specs including the exception path.
|
|
763
|
+
- `GPUTimerArray`: bound check (a full array now refuses a slot instead of addressing past its query
|
|
764
|
+
set), `dropped_count`, `traverse_results` for raw `BigInt` timestamps. Seven specs.
|
|
765
|
+
- `ShadeGPUCommandContext`: `#with_timestamp_writes` — copies the descriptor instead of stamping the
|
|
766
|
+
caller's, tolerates a missing descriptor, and leaves `timestampWrites` off when no slot is free.
|
|
767
|
+
- The whole `.sgpt` container: header, framing, resync reader, defect model, symbol interning,
|
|
768
|
+
`META`/`SYMS`/`FRAM`/`DIRE` codecs, `GPUProfileSession`, `snapshot()`. 26 specs.
|
|
769
|
+
- `GPUFrameRecorder` — holds the graph-pass ↔ timer-slot join, accumulates across every command
|
|
770
|
+
context in a frame, and rebases spans onto the frame epoch at close.
|
|
573
771
|
|
|
574
|
-
|
|
772
|
+
Removed again on 2026-08-29: a `timestamp_period_ns` field and the GCD estimator that filled it.
|
|
773
|
+
See §2.1 — quantization is documented and left alone.
|
|
575
774
|
|
|
576
|
-
|
|
775
|
+
Wired end to end: `Renderer.profile_session`, `GPUFrameRecorder` holding the graph-pass ↔ timer-slot
|
|
776
|
+
join, and `ShadeGPUCommandContext.profiling_absorbed` — which exists because `done` resolves at
|
|
777
|
+
submit, before any timestamp has been read back.
|
|
577
778
|
|
|
578
|
-
|
|
579
|
-
|
|
779
|
+
The calibration step is gone rather than outstanding: see §2.1.
|
|
780
|
+
|
|
781
|
+
<details><summary>Original plan</summary>
|
|
782
|
+
|
|
783
|
+
`GPUTimerArray` pooling and bound checks; `FrameGraph.onPassBegin`/`onPassEnd` and debug groups;
|
|
784
|
+
`GPUProfileSession` at level 0, caller-constructed, streaming framed records with per-frame slack;
|
|
785
|
+
`META`/`SYMS`/`FRAM`/`DIRE` records; round-trip and truncation-recovery tests on `SoftwareGPUDevice`.
|
|
786
|
+
Deliverable: a `.sgpt` with real spans and no viewer.
|
|
787
|
+
|
|
788
|
+
</details>
|
|
789
|
+
|
|
790
|
+
### M2 — Inspector skeleton — **DONE**
|
|
791
|
+
|
|
792
|
+
`packages/gpu-inspector-tool`. Plain ES modules, Canvas 2D, no runtime dependencies. Session strip,
|
|
793
|
+
timeline, statistics, pass detail. `make_demo_capture` builds a synthetic capture through the
|
|
794
|
+
engine's own writer, so the tool works without a GPU and the format's two halves are exercised
|
|
795
|
+
against each other.
|
|
796
|
+
|
|
797
|
+
### M3 — Structure — **DONE**
|
|
798
|
+
|
|
799
|
+
`TOPO` with content-hash deduplication — 50 frames of an unchanged graph come to under 4 KB. Scopes,
|
|
800
|
+
cull state, resource descriptors with footprints computed at record time. The inspector's Structure
|
|
801
|
+
view is two cross-linked tables rather than a node-link diagram: two hundred passes over as many
|
|
802
|
+
resource versions do not lay out into anything readable, and the questions actually asked are local.
|
|
803
|
+
|
|
804
|
+
### M4 — Workload — **DONE**
|
|
805
|
+
|
|
806
|
+
`make_profiling_pass_encoder` is a `Proxy` rather than a hand-written forwarder: a pass encoder has
|
|
807
|
+
around twenty methods and WebGPU keeps adding them, and one silently dropped would break the
|
|
808
|
+
renderer only while profiling. Dispatch and draw counts, pipeline identity, and `@workgroup_size`
|
|
809
|
+
parsed from WGSL where it is a literal — an override expression records as *unknown*, which is not
|
|
810
|
+
the same as zero.
|
|
811
|
+
|
|
812
|
+
No `PIPE` table in the end: a pass's pipeline label and workgroup size ride in its own workload
|
|
813
|
+
block, which costs a symbol reference and avoids a second id space.
|
|
814
|
+
|
|
815
|
+
### M5 — Analysis — **partly done**
|
|
816
|
+
|
|
817
|
+
Statistics view and Chrome Trace Event export are in. Frame comparison and counter tracks are not;
|
|
818
|
+
`CNTR` is reserved in the format for the latter.
|
|
819
|
+
|
|
820
|
+
### M6 — Publish — **partly done**
|
|
821
|
+
|
|
822
|
+
[`SGPT_FORMAT.md`](profile/SGPT_FORMAT.md) is the normative format specification, written for
|
|
823
|
+
somebody outside the company, with a conformance summary for readers and writers. The inspector's
|
|
824
|
+
README covers recording, levels, and the reading caveats.
|
|
825
|
+
|
|
826
|
+
Outstanding: a hosting target for the built site, and deciding the licence the inspector ships
|
|
827
|
+
under.
|
|
580
828
|
|
|
581
829
|
**Not in scope, listed so it stays that way:** buffer/texture content capture, shader source in the
|
|
582
830
|
container, replay, live attach to a running session, anything requiring a browser extension.
|
|
@@ -596,30 +844,33 @@ Per the established tiers:
|
|
|
596
844
|
|
|
597
845
|
---
|
|
598
846
|
|
|
599
|
-
## 10. Risks
|
|
847
|
+
## 10. Risks
|
|
600
848
|
|
|
601
849
|
| Risk | Severity | Handling |
|
|
602
850
|
|---|---|---|
|
|
603
|
-
| **Timestamp quantization makes
|
|
851
|
+
| **Timestamp quantization makes per-frame timings unreadable on default browsers** (§2.1) | **High, and deliberately untreated** | Documented, not measured and not corrected — measuring it is the timing attack the mitigation prevents, and measuring it badly skews everything. The inspector names the flag and steers toward cross-frame aggregation, which is the honest reading of a coarse capture. |
|
|
604
852
|
| Topology dedup does not pay off | Medium | M0 measures it. Design degrades gracefully; the size claims do not. |
|
|
605
853
|
| Proxy encoder overhead distorts what it measures | Medium | Level-gate it; record measured overhead in `META`; forward-only methods, no allocation per call. |
|
|
606
854
|
| One `GPUTimerArray` per context means several query sets per frame (§2.4) | Low–Medium | Record `query_set_id`. Consider a per-frame shared array as a follow-up, which would also simplify pooling. |
|
|
607
855
|
| Format churn during development invalidates captures | Low | `min_reader_version` from day one; the inspector reads every version it ever supported; fixtures in the test suite. |
|
|
608
856
|
| Publishing a proprietary-engine tool separately | Low, but real | The §4 rule-1 boundary is what makes this tractable. Enforce it in the build. Confirm the licensing intent for the published inspector before M6. |
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
2
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
857
|
+
| **Uncapped session exhausts memory on a long level-3 capture** (§4.1, §5.8) | Medium | `bytes_written` readable and signalled; a `byte_budget` that warns rather than truncates. Accepted deliberately — full history is the point, and buffer/texture contents are a non-goal so nothing scales with resource size. |
|
|
858
|
+
| Workspace restructure churns `moh` (§4.2) | Medium | Re-point the vendoring one level deeper so every `moh` path stays identical. Land as a pure move, separately. |
|
|
859
|
+
|
|
860
|
+
## 11. Decisions taken — 2026-08-29
|
|
861
|
+
|
|
862
|
+
| # | Question | Decision | Where it lives |
|
|
863
|
+
|---|---|---|---|
|
|
864
|
+
| 1 | Monorepo or not | **npm workspaces, and the existing tree moves to conform.** `packages/meep-engine/` + `packages/gpu-inspector-tool/`, symmetric siblings. | §4.2, M0.5 |
|
|
865
|
+
| 2 | Does the recorder ship in production? | **Yes, but optional — the caller instantiates a `GPUProfileSession` and passes it in.** The engine never constructs one, so an application that does not import it does not carry it. | §6.1, §6.3 |
|
|
866
|
+
| 3 | The `FrameGraph.execute` bracket | **Agreed, kept minimal — via `Signal`.** `onPassBegin` / `onPassEnd` beside the existing `onExecuted`, dispatch guarded by `hasHandlers()`. `Signal` is the project's standardised observer interface and a nullable callback field would let a second consumer silently clobber the first. | §1.4, §6.2 hook 1 |
|
|
867
|
+
| 4 | Recording length policy | **Open-ended forward stream of self-contained frame records. `frame_limit` of N (1..∞) with an explicit `stop()`.** No ring buffer — full, uncapped, explicit history is the desired behaviour. Records are self-describing and checksummed, so a truncated capture stays readable; 1 MiB of slack reserved per frame keeps reallocation out of the record interior. | §5.8 |
|
|
868
|
+
|
|
869
|
+
Two consequences of (4) worth carrying forward:
|
|
870
|
+
|
|
871
|
+
- **There is no global string table.** Symbols are emitted inside the record that first needs them
|
|
872
|
+
(`SYMS`, §5.3), not gathered into a `STRS` chunk. A tail table would have made a truncated capture
|
|
873
|
+
resolve no names at all — the failure mode that hits precisely the capture of the crash you were
|
|
874
|
+
trying to record.
|
|
875
|
+
- **Records are framed and checksummed** so a reader can resynchronise. This is what buys corruption
|
|
876
|
+
and truncation resilience, and it costs 20 bytes per frame.
|