metrale_bench/benchmarks/vision/provision.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The vision fixtures, written to `~/.metrale/artifacts/vision` on
4//! `load()`.
5//!
6//! The images are compiled into the binary from `crates/bench/assets/vision/`
7//! and written out only when a file's bytes differ (`write_asset_bytes`),
8//! behind a content-derived [`Stamp`], so a run that already provisioned
9//! writes nothing. The written directory lets an operator send the exact bytes
10//! a run used with any HTTP client.
11//!
12//! The same files are in `tests/fixtures/images/`, and
13//! `assets_match_the_repo_fixtures` fails when the two copies differ. They are
14//! copied into the crate because `cargo package` does not include a path
15//! outside it. `scripts/gen_test_images.py` generates `01` to `09`; it does
16//! not generate `10` to `16`.
17//!
18//! Owner: bench, vision.
19//! Invariants:
20//! - `provision` commits the stamp only after every fixture write succeeded.
21
22use anyhow::Result;
23
24use crate::artifacts::{ArtifactStore, Stamp, write_asset_bytes};
25
26/// 2026-09-26: The geometry ladder as `(name, bytes, width, height)`: `01` to
27/// `08` square, landscape and portrait sizes up to 1280x720, `09` over the
28/// 1280 px fallback clamp, `10` and `11` geometry extremes, and `12` to `14`
29/// decode variants at 224x224.
30pub const FIXTURES: &[(&str, &[u8], u32, u32)] = &[
31 (
32 "01_square_224.png",
33 include_bytes!("../../../assets/vision/01_square_224.png"),
34 224,
35 224,
36 ),
37 (
38 "02_square_336.png",
39 include_bytes!("../../../assets/vision/02_square_336.png"),
40 336,
41 336,
42 ),
43 (
44 "03_landscape_512x384.png",
45 include_bytes!("../../../assets/vision/03_landscape_512x384.png"),
46 512,
47 384,
48 ),
49 (
50 "04_wide_640x360.png",
51 include_bytes!("../../../assets/vision/04_wide_640x360.png"),
52 640,
53 360,
54 ),
55 (
56 "05_square_768.png",
57 include_bytes!("../../../assets/vision/05_square_768.png"),
58 768,
59 768,
60 ),
61 (
62 "06_wide_1024x576.png",
63 include_bytes!("../../../assets/vision/06_wide_1024x576.png"),
64 1024,
65 576,
66 ),
67 (
68 "07_hd_1280x720.png",
69 include_bytes!("../../../assets/vision/07_hd_1280x720.png"),
70 1280,
71 720,
72 ),
73 (
74 "08_portrait_480x854.png",
75 include_bytes!("../../../assets/vision/08_portrait_480x854.png"),
76 480,
77 854,
78 ),
79 // 2026-09-26: Every fixture above has a long side of at most 1280 px, so
80 // its count is the same whether the engine honours the checkpoint's area
81 // bound or falls back to the 1280 px clamp. 1600x900 is 1.44M px, under the
82 // 16777216 px of the Qwen3.6 preprocessor configs, so at native size it is
83 // 1400 tokens; under the clamp it is served at 1280x736, 920 tokens.
84 (
85 "09_over_clamp_1600x900.png",
86 include_bytes!("../../../assets/vision/09_over_clamp_1600x900.png"),
87 1600,
88 900,
89 ),
90 // 2026-09-26: 8x8 snaps up to one 32 px grid unit, one merged token: the
91 // smallest target the preprocessor produces (`.max(1)` in
92 // `target_size_for`).
93 (
94 "10_tiny_8x8.png",
95 include_bytes!("../../../assets/vision/10_tiny_8x8.png"),
96 8,
97 8,
98 ),
99 // 2026-09-26: 64x2048 has a small area and a long side over the 1280 px
100 // fallback clamp: 128 tokens at native size, 40 under the clamp (served at
101 // 32x1280).
102 (
103 "11_strip_64x2048.png",
104 include_bytes!("../../../assets/vision/11_strip_64x2048.png"),
105 64,
106 2048,
107 ),
108 // 2026-09-26: Decode variants at 224x224, so each asserts the same 49
109 // tokens as `01`; only the pixel format differs. `12` is RGBA with a
110 // varying alpha channel, which the engine's `to_rgb8()` drops.
111 (
112 "12_rgba_224.png",
113 include_bytes!("../../../assets/vision/12_rgba_224.png"),
114 224,
115 224,
116 ),
117 // 2026-09-26: Single-channel (grayscale) JPEG.
118 (
119 "13_gray_224.jpg",
120 include_bytes!("../../../assets/vision/13_gray_224.jpg"),
121 224,
122 224,
123 ),
124 // 2026-09-26: 16-bit grayscale PNG.
125 (
126 "14_png16_224.png",
127 include_bytes!("../../../assets/vision/14_png16_224.png"),
128 224,
129 224,
130 ),
131];
132
133/// 2026-09-26: The EXIF-orientation pair, kept out of the geometry ladder
134/// because the integrity leg checks what the model reports, not a token count.
135///
136/// Both store the same pixels, red on the top half and blue on the bottom, at
137/// 224x224. `15` carries `Orientation = 6` ("rotate 90 CW to display"); `16`
138/// carries no EXIF. The engine applies the tag
139/// (`model-layers/src/vision_preprocess.rs`), which carries the stored top
140/// edge to the right, so the tagged image must read "right" and the untagged
141/// one "top".
142pub const EXIF_PAIR: &[(&str, &[u8])] = &[
143 (
144 "15_exif_rot90_224.jpg",
145 include_bytes!("../../../assets/vision/15_exif_rot90_224.jpg"),
146 ),
147 (
148 "16_exif_none_224.jpg",
149 include_bytes!("../../../assets/vision/16_exif_none_224.jpg"),
150 ),
151];
152
153/// 2026-09-26: Content-derived over every fixture's name and bytes, so a
154/// changed image re-provisions without a version bump.
155fn stamp_value() -> String {
156 crate::benchmarks::content_stamp(
157 "vision-fixtures-v1",
158 FIXTURES
159 .iter()
160 .map(|(name, bytes, _, _)| (*name, *bytes))
161 .chain(EXIF_PAIR.iter().copied()),
162 )
163}
164
165/// 2026-09-26: The artifact directory name: `~/.metrale/artifacts/vision`.
166pub const PLUGIN_ID: &str = "vision";
167
168/// 2026-09-26: Write the fixtures. With a current stamp it writes nothing.
169pub fn provision(store: &ArtifactStore) -> Result<std::path::PathBuf> {
170 let dir = store.plugin_dir(PLUGIN_ID)?;
171 let stamp = Stamp::new(&dir, ".provisioned", stamp_value());
172 if stamp.is_current() {
173 return Ok(dir);
174 }
175 for (name, bytes, _, _) in FIXTURES {
176 write_asset_bytes(&dir, name, bytes)?;
177 }
178 for (name, bytes) in EXIF_PAIR {
179 write_asset_bytes(&dir, name, bytes)?;
180 }
181 // 2026-09-26: Last: a stamp written before the writes complete would mark
182 // a partial directory as current.
183 stamp.commit()?;
184 Ok(dir)
185}
186
187#[cfg(test)]
188#[path = "provision_tests.rs"]
189mod provision_tests;