Skip to content

Commit 20ac21e

Browse files
committed
Record VSM debug view evidence
1 parent 840eb49 commit 20ac21e

4 files changed

Lines changed: 336 additions & 5 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
{
2+
"schema": "bloom-vsm-debug-views-evidence-v1",
3+
"issue": 132,
4+
"revision": "840eb49fa3190183c01cae29d81945045fe69ea5",
5+
"environment": {
6+
"adapter": "Apple M1 Max",
7+
"backend": "Metal",
8+
"quality_preset": 3,
9+
"render_scale": 1.0,
10+
"width": 800,
11+
"height": 450,
12+
"fixed_timestep_seconds": 0.016666667
13+
},
14+
"palette": {
15+
"order": [
16+
"free",
17+
"miss-unrendered",
18+
"invalidated",
19+
"clip-level-0",
20+
"clip-level-1",
21+
"clip-level-2"
22+
],
23+
"colors": [
24+
"#080808",
25+
"#ffb423",
26+
"#ff37be",
27+
"#46d26e",
28+
"#4696ff",
29+
"#be64ff"
30+
],
31+
"legend_sha256": "6d9b0f90af459fff1bc8c48dcd367c699aee9fc0771b4c39cf8b233002868eb0"
32+
},
33+
"captures": {
34+
"miss": {
35+
"fixture": "quality-motion --vsm-dynamic",
36+
"warmup_frames": 1,
37+
"measured_frames": 1,
38+
"requirement": "miss-unrendered",
39+
"passed": true,
40+
"failures": [],
41+
"virtual_sha256": "089c3d0da0ce515757566b5a44aa9a659a27158ef906857d4ce172cc8243587d",
42+
"physical_sha256": "de2660b4e7241015975f38c8dd39c1c6729ab032bf0fc9d8cb849c774ed0df07",
43+
"report_sha256": "0b63d9a40ae51f43dd8201fad3a1fa157e02893d17b298ef54dae4f87c6a431b",
44+
"page_cells": {
45+
"free": 2848,
46+
"miss_unrendered": 212,
47+
"invalidated": 0,
48+
"clip_level_0": 5,
49+
"clip_level_1": 7,
50+
"clip_level_2": 0,
51+
"unknown": 0
52+
},
53+
"state": {
54+
"resident": 224,
55+
"dirty": 212,
56+
"cache_hits": 224,
57+
"cache_misses": 0,
58+
"invalidated_events": 4,
59+
"rendered": 8
60+
}
61+
},
62+
"invalidation": {
63+
"fixture": "quality-motion --vsm-dynamic --vsm-light-motion",
64+
"warmup_frames": 1,
65+
"measured_frames": 28,
66+
"captured_fixture_frame": 30,
67+
"requirement": "invalidated",
68+
"passed": true,
69+
"failures": [],
70+
"virtual_sha256": "3c5a57bc3e88c81ab2c30dcf36b046a6688383bd46e22a05b83771ea979da7b0",
71+
"physical_sha256": "16f1779a9ad96bb314abf2918fe6cf68e8968c8a96211e077311d15e15f22410",
72+
"report_sha256": "fa3098a68ca4cbb3b334fb2abab16efa928ca5c5577ef2a979210e64006022a3",
73+
"page_cells": {
74+
"free": 2836,
75+
"miss_unrendered": 126,
76+
"invalidated": 102,
77+
"clip_level_0": 4,
78+
"clip_level_1": 4,
79+
"clip_level_2": 0,
80+
"unknown": 0
81+
},
82+
"state": {
83+
"resident": 236,
84+
"dirty": 228,
85+
"requested_pages": 224,
86+
"cache_hits": 212,
87+
"cache_misses": 12,
88+
"invalidated_events": 105,
89+
"rendered": 8
90+
},
91+
"per_light_cost": {
92+
"light": 0,
93+
"kind": "directional",
94+
"requested_pages": 224,
95+
"cache_hits": 212,
96+
"cache_misses": 12,
97+
"invalidated_pages": 105,
98+
"rendered_pages": 8,
99+
"resident_pages": 236,
100+
"dirty_pages": 228,
101+
"clipmap_level_rebases": 0,
102+
"dynamic_overlay_draws": 8,
103+
"physical_depth_bytes_owned": 16448256,
104+
"shared_pool_bytes": 17842176,
105+
"shared_metadata_staging_bytes": 2109648,
106+
"render_budget_pages": 8
107+
}
108+
},
109+
"static": {
110+
"fixture": "quality-motion --vsm-contact-detail",
111+
"warmup_frames": 120,
112+
"measured_frames": 1,
113+
"requirement": "three settled clip levels",
114+
"passed": true,
115+
"failures": [],
116+
"virtual_sha256": "27252efced8e7ba900e95d5a204427f81a0958b179913dc0e43c4d99cf798aec",
117+
"physical_sha256": "5d8e1a785f5154f0123a13d79ecadd599460ce029547b04d42a7a7e649cb7331",
118+
"report_sha256": "7e9d6dedeff5df6d2e61040d07ff9efc052d5b0da880c813f59befa25f8e0ac0",
119+
"page_cells": {
120+
"free": 2848,
121+
"miss_unrendered": 0,
122+
"invalidated": 0,
123+
"clip_level_0": 144,
124+
"clip_level_1": 64,
125+
"clip_level_2": 16,
126+
"unknown": 0
127+
},
128+
"state": {
129+
"resident": 224,
130+
"dirty": 0,
131+
"cache_hits": 224,
132+
"cache_misses": 0,
133+
"invalidated_events": 0,
134+
"rendered": 0
135+
}
136+
}
137+
},
138+
"regression": {
139+
"quality_contract": "pass",
140+
"strict_clippy_correctness_suspicious_performance": "pass",
141+
"debug_oracle_tests_passed": 7,
142+
"rust_debug_semantic_tests_passed": 2,
143+
"headless_device_negotiation": "pass",
144+
"shared_unit_tests_passed": 349,
145+
"shared_unit_tests_ignored": 1,
146+
"gpu_goldens_passed": 59,
147+
"gpu_goldens_policy_ignored": 2,
148+
"render_target_tests_passed": 4,
149+
"quality_tests_passed": 39,
150+
"wasm_check": "pass",
151+
"canonical_examples_passed": 20,
152+
"quick_lane": "pass"
153+
},
154+
"production_render_path_changes": {
155+
"shaders": 0,
156+
"passes": 0,
157+
"resources": 0,
158+
"bindings": 0,
159+
"draws": 0,
160+
"page_policy": 0
161+
}
162+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Issue #132 complete VSM debug-view qualification v1
2+
3+
This evidence qualifies the debug-view acceptance criterion implemented at
4+
`840eb49fa3190183c01cae29d81945045fe69ea5` on an Apple M1 Max using
5+
Metal and Bloom's native high-end profile. It covers virtual pages, physical
6+
occupancy, misses, invalidations, clip levels, and per-light cost.
7+
8+
These are page-grid diagnostics, not geometry rendered into the game view.
9+
Their rectangular shapes deliberately represent virtual or physical cache
10+
cells.
11+
12+
## Capture contract
13+
14+
An opt-in intermediate capture now writes four mutually consistent artifacts:
15+
16+
- `virtual-shadow-pages.png` is a 32 by 96-cell virtual address-space map.
17+
The three 32 by 32 clip levels are stacked vertically from near to far.
18+
- `virtual-shadow-physical.png` is the compact physical pool, in physical-slot
19+
order. It exposes occupancy, fragmentation, and eviction placement.
20+
- `virtual-shadow-legend.png` is a machine-readable six-cell palette.
21+
- `virtual-shadow-report.json` is a same-frame VSM state and cost snapshot.
22+
Using a capture-time sidecar avoids comparing post-measurement images with
23+
telemetry serialized one frame earlier.
24+
25+
The stable palette is:
26+
27+
| State | RGB | Meaning |
28+
| --- | --- | --- |
29+
| free | `#080808` | no virtual owner / unallocated slot |
30+
| miss-unrendered | `#ffb423` | resident miss that has never produced valid depth |
31+
| invalidated | `#ff37be` | previously rendered depth made dirty |
32+
| clip-level-0 | `#46d26e` | valid near-level page |
33+
| clip-level-1 | `#4696ff` | valid middle-level page |
34+
| clip-level-2 | `#be64ff` | valid far-level page |
35+
36+
The report also publishes a directional-light cost row containing requests,
37+
hits, misses, invalidations, renders, residency, dirty pages, rebases, dynamic
38+
overlay draws, owned physical depth bytes, shared pool bytes, shared metadata
39+
and staging bytes, and the page render budget.
40+
41+
## Automated fail-closed gate
42+
43+
`tools/quality/vsm_debug_views.py` decodes the PNGs without third-party
44+
dependencies and rejects:
45+
46+
- an unknown or reordered color;
47+
- partial cells or incorrect virtual/physical dimensions;
48+
- disagreement between the virtual and physical non-free page counts;
49+
- occupancy, dirty-state, or per-level clean counts that disagree with the
50+
same-frame report;
51+
- missing explicitly required miss or invalidation states;
52+
- changed artifact filenames or palette metadata;
53+
- any per-light counter or byte cost inconsistent with the top-level state.
54+
55+
Seven unit tests include negative controls for missing required event colors,
56+
unknown colors, a reordered legend, per-light cost disagreement, and clip
57+
level disagreement. Rust tests independently prove that never-rendered and
58+
previously-rendered dirty pages receive different colors, all three clean
59+
levels retain stable colors, free slots remain distinct, and the legend is
60+
exact.
61+
62+
## Live GPU captures
63+
64+
Three captures exercise complementary states. Pixel counts below are
65+
normalized to page cells; the output scale is four pixels per cell.
66+
67+
| Capture | Resident | Free virtual | Miss | Invalidated | Level 0 | Level 1 | Level 2 |
68+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
69+
| early dynamic fill | 224 | 2,848 | 212 | 0 | 5 | 7 | 0 |
70+
| frame-30 light transition | 236 | 2,836 | 126 | 102 | 4 | 4 | 0 |
71+
| settled static contact scene | 224 | 2,848 | 0 | 0 | 144 | 64 | 16 |
72+
73+
Every capture had zero unknown-color pixels. Virtual and physical page counts
74+
were identical for every non-free state. The static control exactly exposed
75+
the expected 144/64/16 near/middle/far demand distribution with zero dirty
76+
pages. The transition captured 102 still-dirty previously rendered pages in
77+
magenta and 126 never-rendered pages in amber. Its event counters recorded
78+
105 invalidations and 12 new cache misses; pages rendered during the frame
79+
correctly left the dirty-color population.
80+
81+
The transition per-light row reconciled to 236 resident pages, 228 dirty
82+
pages, eight rendered pages, 224 requests, 212 hits, 12 misses, 105
83+
invalidations, eight dynamic overlay draws, the hard eight-page render budget,
84+
16,448,256 bytes of depth owned by resident pages, a fixed 17,842,176-byte
85+
physical pool, and 2,109,648 shared metadata/staging bytes.
86+
87+
Reproduction from `examples/quality-motion`:
88+
89+
```sh
90+
export BLOOM_VSM=1
91+
92+
export BLOOM_QUALITY_INTERMEDIATES=/tmp/vsm-debug-miss
93+
./main --vsm-dynamic \
94+
--quality-run 1 1 0.016666667 \
95+
/tmp/vsm-debug-miss.png /tmp/vsm-debug-miss-telemetry.json \
96+
/tmp/vsm-debug-miss
97+
98+
export BLOOM_QUALITY_INTERMEDIATES=/tmp/vsm-debug-invalidation
99+
./main --vsm-dynamic --vsm-light-motion \
100+
--quality-run 1 28 0.016666667 \
101+
/tmp/vsm-debug-invalidation.png \
102+
/tmp/vsm-debug-invalidation-telemetry.json \
103+
/tmp/vsm-debug-invalidation
104+
105+
export BLOOM_QUALITY_INTERMEDIATES=/tmp/vsm-debug-static
106+
./main --vsm-contact-detail \
107+
--quality-run 120 1 0.016666667 \
108+
/tmp/vsm-debug-static.png /tmp/vsm-debug-static-telemetry.json \
109+
/tmp/vsm-debug-static
110+
111+
cd ../..
112+
python3 tools/quality/vsm_debug_views.py \
113+
--virtual /tmp/vsm-debug-invalidation/virtual-shadow-pages.png \
114+
--physical /tmp/vsm-debug-invalidation/virtual-shadow-physical.png \
115+
--legend /tmp/vsm-debug-invalidation/virtual-shadow-legend.png \
116+
--telemetry /tmp/vsm-debug-invalidation/virtual-shadow-report.json \
117+
--require-invalidation \
118+
--output /tmp/vsm-debug-invalidation-result.json
119+
```
120+
121+
Run the validator with `--require-miss` for the early-fill capture and without
122+
an event requirement for the static control.
123+
124+
## Rendering and performance isolation
125+
126+
This checkpoint changes no shader, render pass, texture, buffer, bind group,
127+
draw submission, page request, cache policy, VSM sampling, or CSM fallback.
128+
The page-state images were already generated only when an intermediate
129+
capture was requested; the change separates their dirty-state colors and adds
130+
a legend plus a same-frame JSON sidecar in that same post-measurement path.
131+
Normal frames execute none of the image generation, PNG encoding, or sidecar
132+
I/O. The expanded capability JSON is serialized only when explicitly queried.
133+
134+
Consequently the production frame graph and final game image are unchanged,
135+
and the diagnostic work remains excluded from measured performance windows.
136+
Strict Clippy correctness/suspicious/performance policy and the quality
137+
contract passed. The complete quick lane passed with FFI parity on every
138+
backend, 349 shared unit tests plus one ignored, negotiated headless device
139+
construction, 59 GPU goldens plus two hardware-policy ignores, four render
140+
target tests, Web/WASM checking, 39 quality tests, and all 20 canonical
141+
examples.
142+
143+
Machine-readable evidence is in
144+
`docs/evidence/issue-132-debug-views-v1.json`.

tools/quality/test_vsm_debug_views.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22

3-
from tools.quality.vsm_debug_views import ORDER, evaluate
3+
from tools.quality.khronos_materials import QualificationError
4+
from tools.quality.vsm_debug_views import COLORS, ORDER, evaluate, validate_legend
45

56

67
def summary(counts: dict[str, int], width: int, height: int) -> dict[str, object]:
@@ -124,6 +125,21 @@ def test_clip_level_disagreement_fails_closed(self) -> None:
124125
)
125126
self.assertTrue(any("clip-level-1" in failure for failure in failures))
126127

128+
def test_exact_machine_readable_legend_passes(self) -> None:
129+
pixels = [
130+
color
131+
for _y in range(2)
132+
for color in COLORS
133+
for _x in range(2)
134+
]
135+
validate_legend(12, 2, pixels, "synthetic")
136+
137+
def test_reordered_legend_fails_closed(self) -> None:
138+
pixels = list(COLORS)
139+
pixels[1], pixels[2] = pixels[2], pixels[1]
140+
with self.assertRaisesRegex(QualificationError, "legend cell"):
141+
validate_legend(6, 1, pixels, "synthetic")
142+
127143

128144
if __name__ == "__main__":
129145
unittest.main()

tools/quality/vsm_debug_views.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,26 @@ def image_summary(
8282
}
8383

8484

85-
def legend_summary(path: Path) -> dict[str, object]:
86-
width, height, pixels = png_rgb(path)
85+
def validate_legend(
86+
width: int,
87+
height: int,
88+
pixels: list[tuple[int, int, int]],
89+
source: str,
90+
) -> None:
8791
if height < 1 or width != len(ORDER) * height:
88-
raise QualificationError(f"{path}: expected a six-cell horizontal legend")
92+
raise QualificationError(f"{source}: expected a six-cell horizontal legend")
8993
for index, expected in enumerate(COLORS):
9094
for y in range(height):
9195
start = y * width + index * height
9296
if any(pixel != expected for pixel in pixels[start : start + height]):
9397
raise QualificationError(
94-
f"{path}: legend cell {index} is not {ORDER[index]!r}"
98+
f"{source}: legend cell {index} is not {ORDER[index]!r}"
9599
)
100+
101+
102+
def legend_summary(path: Path) -> dict[str, object]:
103+
width, height, pixels = png_rgb(path)
104+
validate_legend(width, height, pixels, str(path))
96105
return {
97106
"path": str(path),
98107
"sha256": sha256_file(path),

0 commit comments

Comments
 (0)