open-standards/wai/research/cross-fabric.md
Transaction Science 836d0255df wai packaging stack: #426, #438 and #452's GPU tier, squashed onto main
One squashed history of three stacked PRs, onto main at b16816671:

- #426 (wai/pack-efficiency): a title's whole AV1 ladder in one package over one
  integer base; several floor rungs (packaging §7.1), a re-pack from a parent
  (§11.5), a floor that is the ladder's own CMAF (§7.7), a floor-only profile
  (§7.8), a streaming pack (§11.4), and the independent verifier at scale (§9.4).
- #438 (wai/cross-fabric): each pack and verify step on the fabric with the least
  measured energy (§11.6); the `fabrics` member of measurement claims
  (jwp-receipts §2.4.8, energy-measurement §6.3); the wai-pack-gpu crate;
  `wai-pack fabric`; `wai-pack floor-decode`.
- #452 (wai/gpu-int-tier), its GPU-tier part only: claiming decode-equivalence on a
  GPU (determinism-tiers §5), the WGSL integer semantics suite, the rules check on
  naga's IR, and the kernels' shift masks and min(max()) clamps.

Left out, by the review's split (EN-C takes it over): #452's platform decode in
dispatch, its commits 6ad92e2e8, 8695bc75f and fed8694f4 and its CHANGELOG entry:
extensions/platform-decode.md, platform-decode-conformance/, wai::platform_decode,
wai-web's platform_decode bundle and demo, and its two SPEC.md sentences.

#438's research note is not carried as written: it is rewritten as
wai/research/cross-fabric.md, with hardware named by architecture, PCI id and OS
version, and every table marked as one run. floor_decode.rs and a wai_pack.rs
comment name the platform's media engine and a full-length film the same way.

wai/CHANGELOG.md is the union of both sides' entries, the stack's first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:31:28 -04:00

339 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI across fabrics: research for the packer and the verifiers
> **Status.** Research note, informative, not normative. It records what decided the change
> that lets each step of a WAI pack or verify run on the fabric that does it with the least
> measured energy (the CPU, a GPU, a hardware media engine, or a neural processor):
> [packaging](../extensions/packaging.md) §11.6, [jwp-receipts](../extensions/jwp-receipts.md)
> §2.4.8 and [energy-measurement](../extensions/energy-measurement.md) §6.3. Nothing here changes
> a byte of a package or a verdict.
**Method.** Every statement about an interface or a specification was checked against that
document on 2026-10-22. Items marked **[measured]** were run for this note (the setup is in §9);
items marked **[local]** were read from the platform's SDK headers on the machine that ran them.
Exact counts and digests are exact. Times and energies are indicative: each table in §9 is **one
run**, on a shared host whose load is given, and none is a sample of anything.
## 1. The hardware the measurements ran on
Hardware is named as the rest of the repository names it: by architecture, PCI vendor and device
id, and operating-system version.
- **arm64 desktops** (macOS 27.0.1), each with an integrated GPU and a media engine that decodes
AV1 in hardware and encodes no AV1. The measurements of §9 ran on one of them, shared with other
work. Every one reports the platform's current GPU interface.
- **A Windows x86_64 host** with an integrated GPU of PCI vendor 0x8086, device 0x9a49, and a
discrete GPU of PCI vendor 0x10de, device 0x1f15. Its vendor's published support matrix gives
that discrete GPU's architecture neither AV1 decode nor AV1 encode; the integrated GPU decodes
AV1, with a film-grain path its vendor does not state as bit-exact (§6.1).
- **Linux x86_64 hosts** with GPUs of PCI vendor 0x10de, device 0x28e1, of vendor 0x8086, device
0xa7a0, and of vendor 0x1002, device 0x1586, as determinism-tiers §5 records them. Their media
engines were not probed for this note.
## 2. The steps, and the fabrics that can run each
| Step | CPU | GPU | Media engine | Neural processor | Placed on |
|---|---|---|---|---|---|
| `source-decode` (a coded source to frames, before a pack) | dav1d | | AV1 decode where the engine has it | | either, on equal pictures (§6) |
| `source-read`, `segment` | yes | | | | CPU (sequential, cheap) |
| `base-search` (the integer motion search) | reference | exact integer kernel | none: no engine runs §5.5's search, and the platform's hardware motion estimation is not it (§3) | unsound (FP16, §5) | GPU where it qualifies |
| `base-code`, `refine-code` (residuals, rANS) | reference | | none: no engine codes `WIV1` or `WIR1` | | CPU (rANS is sequential) |
| `sse` | reference | exact (partial sums under 2^32) | | unsound | by measured cost |
| `object-hash` (BLAKE3) | reference | exact (chunks on the device, parents on the host) | | | by measured cost |
| `digest` (SHA-256) | yes | no: one message is one chain (FIPS 180-4 §6.2.2) | | | CPU |
| `floor-read` (AV1 and CMAF structure) | yes | | | | CPU |
| `floor-decode` | dav1d | | AV1 decode, as above | | informative only (§6.3) |
| `index`, `io` | yes | | | | CPU |
What this decides: the GPU takes the one step that dominates a pack's work, the motion search (at
least 96.8% of the CPU-only pack's CPU time in the run of §9.2), plus SSE and BLAKE3 where the
measured cost says so, which on the host of §9 it does not (§9.3). No hardware encoder computes any
WAI step. The media engine decodes AV1 sources and floors. The neural processor computes nothing
exact.
## 3. Media engines
- **AV1 decode, and no AV1 encode, on the arm64 desktops.** Every published specification of their
media engines lists AV1 decode and no AV1 encode, and the platform's compression headers name no
AV1 encoder **[local]**. ffmpeg's encoders for the platform's framework cover H.264, HEVC and
ProRes only.
- **What the decode needs.** A decode session takes the sequence header OBU from an `av1C` record;
a low-overhead OBU stream (`-f obu`) gives it none up front, so it does not decode there, while
the ladder's CMAF (with `av1C`) does **[measured]**. The reference tool therefore decodes floors
from the package's CMAF files.
- **What it gives back [measured]** (ffmpeg 9.0.2, macOS 27.0.1):
- The ladder of §9 is AV1 Main profile, **10-bit** (each rung's `av1C`). The engine's hardware
frames are P010; a download as 8-bit NV12 is refused.
- Its raw P010 frames equal, frame by frame, dav1d's planar 10-bit output repacked to P010. But
ffmpeg's P010-to-8-bit conversion of them does not equal dav1d's own 10-to-8-bit conversion:
that conversion rounds and dithers, so a comparison stays at the stream's bit depth or
converts exactly.
- Earlier comparisons hashed `-f framemd5` outputs with stderr piped into the hash; the
"difference" they showed was ffmpeg's error text. Every comparison in §9 hashes the output
file, not the console.
- **Hardware motion estimation.** The platform offers a motion-estimation session returning 4×4 or
16×16 vectors, and documents neither the engine, its cost metric, its search window nor its
determinism. It cannot compute §5.5's normative search.
- **Other platforms.** One vendor's published support matrix gives AV1 decode from one GPU
generation and AV1 encode from a later one, and states that its decoder runs independently of
the compute engine. Another's media driver decodes AV1 from its twelfth graphics generation, in
its full-feature build. A third's kernel driver decodes AV1 on its third video-core generation
and encodes from its fourth. Vulkan defines `VK_KHR_video_decode_av1` from Vulkan 1.3.277 and
`VK_KHR_video_encode_av1` from 1.3.302.
## 4. GPU compute
- **WGSL integer semantics** (WGSL, W3C Candidate Recommendation Draft): `i32` operations "use
two's complement arithmetic and have defined overflow behavior"; `>>` on a signed value inserts
copies of the sign bit (§8.10); an integer division by zero evaluates to `e1` (§8.8); an
out-of-bounds access is a dynamic error the implementation does not perform as written (§6.5.7).
So a WGSL kernel that keeps every value inside its type and every index inside its buffer has
one result on every conformant device. The WAI kernels do: a block's cost is at most
`block² · 765`, refused on the host where that passes 2^32 − 1. What a translator does with the
cases WGSL defines and its target languages do not is determinism-tiers §5's subject.
- **The backend languages.** MSL leaves signed integer overflow undefined and its atomics relaxed,
so only commutative integer reductions are order-free there. The WAI kernels use no atomics:
each block's minimum is a tree reduction over a total order, so it does not depend on which
thread visits which candidate.
- **BLAKE3 on GPUs.** The specification splits input into 1 KiB chunks hashed independently.
Published GPU ports measured slower than one CPU core on a discrete GPU, or than eight CPU
threads on an integrated one, and none reported energy. Measured here in §9.3.
## 5. Neural processors
The neural processor of the arm64 desktops computes in FP16, which the platform's machine-learning
framework can permit but not require, and its documentation states that its capabilities differ by
hardware and software generation. FP16 represents integers exactly only up to 2048, and one 8-bit
squared difference is 65025. **No WAI step can run on it.** The fabric term is `npu`, the term
determinism-tiers uses for `wai-ops/1-npu`.
## 6. Determinism and conformance
### 6.1 AV1 decoding is exact, film grain aside
AV1 Bitstream & Decoding Process Specification, Version 1.0.0 with Errata 1, §7.2: "When
film_grain_params_present is equal to 0, decoders shall produce output frames that are identical
in all respects and have the same output order as those produced by the decoding process
specified herein." With film grain, a conformant decoder may instead match only the pre-grain
frames of §7.18.2 and add grain with no "perceptually significant differences". dav1d states that
it passes the AV1 conformance streams with grain on. One GPU vendor's media driver states that its
film-grain path does not guarantee a bit match, and AV1 decode defects have been fixed in more than
one vendor's driver.
**Tested [measured]:** the media engine decoded each rung of the ladder of §9, and its pictures
were compared with dav1d's (§9.1). The ladder carries no film grain: dav1d gives the same frames
with grain applied and with it exported.
### 6.2 Hardware encoders are not reproducible
No vendor states that its hardware encoder is deterministic. One vendor's encoder guide documents
split-frame encoding that turns on with the number of engines and the frame height, and another's
low-power AV1 rate control runs in firmware. Software encoders are reproducible only under pinned
conditions.
### 6.3 What it means for WAI
- **Receipts.** A package's bytes do not depend on where its steps ran, because every step is
integer and the GPU kernels compute the same integers: the receipt, the index and every object
are the CPU's (§9.1). Where the steps ran is recorded only in a metered package's measurement
claim, which §11.2 already exempts, so an unmetered package records nothing machine-specific.
- **§11.2 on non-deterministic hardware.** No WAI step may be computed by a hardware encoder. A
floor a packager is given is an input; a floor encoded on a hardware encoder is one §7.5 covers,
and §11.2 does not hold its bytes.
- **Verifiers' independence.** A check whose expected value the package states (an object's
content hash) may run anywhere: a match shows the fabric computed it, and a mismatch is computed
again on the CPU before it is reported. A floor states no decoded-output digest, so a hardware
decode never decides a floor verdict, and a hardware decoder may conceal an error that dav1d
reports. The Python verifier stays on the CPU and shares no code with wai-rs; the GPU path is
wai-pack's.
## 7. Measuring energy per fabric without privileges
### 7.1 macOS 27
- **IOReport** is a private interface (the SDK ships a stub and no header **[local]**). On macOS 27
its Energy Model CPU and neural-processor channels stay visible and stop updating for a reader
without a private entitlement. **[measured]** on the host of §9: of 645 Energy Model channels,
only `GPU Energy` (nJ) moved over a 6 s span during a hardware decode, a dav1d decode and idle;
the CPU, per-core, neural-processor, video-encoder, memory and display channels read zero, and
there is no decoder energy channel. So **the media engine and the neural processor have no
unprivileged counter there**; a privileged sampler reads the neural processor, and nothing in
this change runs privileged.
- **CPU, per process:** `proc_pid_rusage(RUSAGE_INFO_V6)` gives the kernel's account of the task's
own CPU energy (`ri_energy_nj`) and the energy it billed to the task and serviced for others
(`ri_billed_energy`, `ri_serviced_energy`), readable for one's own process or a child. A task's
`task` share is `ri_energy_nj + ri_billed_energy − ri_serviced_energy`, the definition the
sink-metering profiles use, so energy a system service spent for the process is in it. Other
load on the host enters it only through contention.
- **GPU, per process:** the accelerator's user clients in the I/O registry carry each creator's pid
and its accumulated GPU time (ns), readable by any process **[measured]**. The repository's own
measurement found that dividing the shared rail by GPU time charged a decode 3 to 15 times its own
GPU energy, and that a client disappears with its process. So the rail is attributed to a process
only when it was the rail's sole user: the clients are sampled every 200 ms through the span, each
client's GPU time differenced between samples, and the span refused when a client vanishes, a
difference is negative, or the process held under 99% of the GPU time the samples saw. The rail
is then sealed whole, its idle and static draw over the span included, never divided by busy
time. The rail holds a reading for hundreds of milliseconds and then catches up in one step, so
it is read on its next republish at each end, inside the sampled window.
### 7.2 Linux and Windows (not run here)
- RAPL's `energy_uj` became root-only in Linux 5.10, and perf's RAPL events are system-wide only,
so reading CPU energy there takes an owner's act (`cap_perfmon`, or a group-readable
`energy_uj`).
- NVML's total-energy query (mJ) is not marked as needing privilege; the in-kernel hwmon power and
energy attributes of two other GPU drivers are world-readable.
- Windows' Energy Meter Interface is readable unelevated.
## 8. The design choices, and what decided them
| Choice | Decided by |
|---|---|
| The GPU computes the motion search, SSE and BLAKE3 chunks, in WGSL integer kernels through wgpu, not in one platform's shading language | WGSL's defined integer arithmetic (§4) gives one result on every conformant device; the repository's GPU precedent (`wai-ops-gpu`) is wgpu. Exactness is claimed only on the adapters a run showed (`wai-pack-gpu`'s crate documentation lists them), with qualification at open as the runtime gate |
| A backend runs a step only after it gives the reference's output on qualifying inputs; any call it cannot take runs on the CPU | §11.2 must hold on every fabric; a device or driver can be wrong (§6.1's driver defects) |
| Placement by a measured cost table (µJ per call + nJ per unit), the CPU on a tie or where unmeasured | **[measured]** the GPU's fixed cost per call (1.7 mJ for a hash, 3.1 mJ for an SSE) exceeds what a pack's calls save, while the search saves 14× per unit (§9.3); placing by fabric alone made the floor-only pack dearer (§9.2) |
| The fabric record lives in the measurement claim, not the receipt | the receipt and `LISTING` would otherwise differ by machine, voiding §11.2 for every package; the claim is already §11.2's exemption |
| `fabrics` is absent rather than `null` | every existing claim, and every package that carries one, keeps its bytes |
| Attributions `task` and `rail` only; a `rail` share is the whole rail, sealed only for its sole user | the repository's measurement of a 3–15× overcharge from dividing by busy time (§7.1) |
| The figure is an `OnChipCounter` reading with a declared uncertainty over its span | both readings are counters (energy-measurement §2, §2.9); a counter reading is not a model |
| Hardware AV1 decode for sources and for informative floor decodes; never a floor verdict | AV1 §7.2 (exact without grain); floors state no digest; hardware may conceal errors |
| Hardware frames read from P010 exactly, never through ffmpeg's 16-to-8-bit conversion | **[measured]**: that conversion differs from dav1d's (§3) |
| No hardware encoder computes a WAI step | §6.2 |
| No neural-processor backend | §5 |
## 9. Measurements
**Setup.** A 596 s, 24 fps open-licence film, 14,315 frames: a 256×144 integer proxy and the
ladder's four 10-bit AV1 rungs (1080, 720, 540 and 360 lines). One arm64 desktop (macOS 27.0.1),
shared with other work; each run's load average is in its report. wai-pack at this change,
release, features `gpu,fabric-meter`; ffmpeg 9.0.2 with dav1d 1.5.4. Each run was metered by an
external sampler that read the kernel's account of the child's CPU energy before reaping it (and of
every decode service the run started: hardware decodes run out of process, and a 2,000-frame
decode billed 1.20 J to ffmpeg and 1.09 J to its service), and the IOReport `GPU Energy` rail and
every client's GPU time every 200 ms, with idle brackets before and after.
**Class and scope of every figure below.** CPU figures are `OnChipCounter` readings of the
process's own task energy. GPU figures are `OnChipCounter` readings of the whole GPU rail over the
run, given only where the run held at least 99% of all GPU time; otherwise there is no GPU figure.
The media engine has no counter, so a media-engine run's figure is its CPU side alone, a lower
bound. **Each table is one run.** None was repeated, so none is a median, and none carries a
measured spread; they are indicative.
### 9.1 Exact results
- **Packages are the same bytes on the GPU.** A 360-line rung's policy packed with
`--fabric gpu`: the CPU pack's digest (`7454acc6…1118`), every file the same (`diff -r`), in
three runs, and with `--fabric auto:<costs>` too. A four-rung pack over the ladder's own CMAF,
the base coded once: pack digest `3912bd7c…b9b7f` and receipt `e0256cd9…`, the CPU's. A
floor-only pack: `f8871444…e032`, the CPU's.
- **The media engine decodes what dav1d decodes.** Each of the four rungs, all 14,315 frames: the
SHA-256 of every frame in P010 equal between the engine's hardware frames and dav1d's output.
And `wai-pack floor-decode` of the four-rung package: the same digest from both decoders for each
of its 600 pieces, in the stream's own 10-bit form, the P010 words read back exactly.
- **A source decoded by the media engine is the same source.** The proxy decoded from the 1080-line
rung on the engine, then by the same scaler: SHA-256 `8d236f5b…b56d`, the CPU decode's proxy byte
for byte.
- **Placement.** In a 12-piece pack, 24,915 of 24,930 motion-search calls ran on the GPU before the
map-wait fix. The other 15 were handed back to the CPU, with the same vectors.
- **Instructions.** The CPU-only 360-line pack retired 98.3 × 10¹² CPU instructions; the GPU pack
retired 2.05 × 10¹².
### 9.2 Pack and verify, joules per fabric (one run each)
The GPU's other clients held under 1% of its time in every pack of this run.
| Run | Wall | CPU (task) | GPU (rail) | Total | Peak memory | CPU-only baseline (task) |
|---|---:|---:|---:|---:|---:|---|
| 360-line pack, search on the GPU | 56 s | 573 J | 1,284 J | **1,857 J** | 0.95 GB | 20,299 J, 3,689 s, 3.04 GB |
| 360-line pack, `auto` (search on the GPU; SSE and hashes on the CPU) | 56 s | 571 J | 1,283 J | **1,854 J** | 0.76 GB | as above |
| four-rung CMAF pack, search on the GPU, `--jobs 12` | 71 s | 525 J | 1,239 J | **1,763 J** | 1.62 GB | 19,541 J, 980 s at `--jobs 6`, 0.17 GB |
| floor-only pack, hashes on the GPU | 2.2 s | 4.4 J | 2.3 J | **6.7 J** | 0.65 GB | 3.7 J, 1.5 s, 0.04 GB |
| `wai-pack verify`, 360-line, CPU | 32 s | 118 J | 0 | 118 J | 0.58 GB | 31–33 s |
| `wai-pack verify`, 360-line, hashes on the GPU | 31 s | 120 J | none (GPU shared) | ≥ 120 J | 2.04 GB | |
| `wai-pack verify`, four-rung, CPU | 34 s | 119 J | 0 | 119 J | 1.55 GB | 35 s, 121 J |
| `wai-pack verify`, four-rung, hashes on the GPU | 32 s | 122 J | 0.2 J | 122 J | 3.44 GB | |
What the table shows:
- Moving the motion search to the GPU cut the 360-line pack's energy 9 to 11 times over the two
runs made: 1,857 J here and 2,248 J in the sealed run of §9.5, against a 20,299 J baseline. The
baseline is CPU task energy alone, and the GPU runs add the whole rail. The CPU work left
(residuals, rANS, digests, I/O: 116 s of CPU against the baseline's 3,635 s) shows that the
search was at least 96.8% of the baseline's CPU time.
- Moving the object hashes or SSE gained nothing: the floor-only pack, whose only movable step is
hashing, cost more on the GPU.
- `wai-pack verify` is dominated by the integer base's decode, which is sequential rANS, so it
stays on the CPU.
### 9.3 The measured cost table (`wai-pack fabric calibrate`, one run)
Each backend, two call sizes, 6 s each, fitted to `µJ per call + nJ per unit`:
| Step | CPU | GPU | Placement |
|---|---|---|---|
| `base-search` (unit: candidate-sample) | 0 + 20.2 nJ | 11,276 µJ + 1.47 nJ | GPU from about 600,000 units, a 32×24 frame at the policy's range; every frame of the film is 32 million |
| `sse` (unit: byte) | 0 + 0.85 nJ | 3,136 µJ + 0.76 nJ | CPU below about 35 MB a call; a piece of the film is 5 MB |
| `object-hash` (unit: byte) | 7 µJ + 1.33 nJ | 1,699 µJ + 1.43 nJ | CPU always: the GPU's BLAKE3 is no cheaper per byte either |
The GPU's per-call cost includes its idle and static draw over the call, since the rail is the whole
GPU's.
### 9.4 Decoding: CPU against the media engine (one run)
The decode service was counted; the GPU was shared in this run, and no decode used it.
| Decode of the whole floor | dav1d: wall, CPU | media engine: wall, CPU (ffmpeg and its service), engine |
|---|---|---|
| 1080 lines | 23.1 s, 198.7 J | 22.0 s, 17.4 J, + unmetered |
| 720 lines | 12.4 s, 111.0 J | 17.3 s, 11.3 J, + unmetered |
| 540 lines | 7.7 s, 73.3 J | 14.1 s, 8.5 J, + unmetered |
| 360 lines | 5.1 s, 53.8 J | 11.3 s, 15.9 J, + unmetered |
| proxy source (1080 lines decoded and scaled to 256×144) | 22.7 s, 304.1 J | 25.6 s, 350.5 J, + unmetered |
What the table shows:
- A floor decode on the media engine cost the CPU 8 to 11 times less at 540 to 1080 lines, and 3.4
times less at 360, where the per-frame download dominates.
- The engine's own draw is not visible to an unprivileged reader on macOS 27 (§7.1), so these are
lower bounds.
- For the proxy source the engine saved nothing: the Lanczos scale and the P010 repack on the CPU
cost more than the decode saved. Placing `source-decode` on the media engine is worth it only
where the scaling also moves, and moving it would change the source's bytes unless the scaler is
exact.
### 9.5 A metered pack, sealed (one run)
- **Sealed.** The 360-line pack with `--fabric gpu --meter fabric`, run when the GPU's other
clients had held under 0.5% of its time over the preceding 10 s, sealed 2,248.4 J with the fabric
record in its measurement claim: the CPU's share 475.4 J (`task`) and the GPU's 1,773.0 J
(`rail`, the pack its sole user); the steps `base-search`, `sse` and `object-hash` on the GPU and
every other step on the CPU, with no search call handed back to the CPU.
- **Which meter.** That run used the meter's first revision, which sealed the figure as
`ModelBased`, took the GPU's time at the two ends only, and multiplied the rail by the pack's
share of GPU time (at least 99%). The meter now seals `OnChipCounter` with the whole rail and
samples GPU time through the span (§7.1). The run has not been repeated under it, so its figures
are the first revision's.
- **Verified.** `wai-pack verify` accepts the package, and `pack_verify.py` holds every check of it
(3622 of 3622), the claim among them.
- **Refused.** The same metered four-rung pack, a minute later, found the GPU shared (39.9 s of its
50.0 s) and was refused `pack-meter`, as was the calibration then.
### 9.6 Load, and what is indicative
- The host's one-minute load average was 14 to 50 over the runs of §9.2 and 25 to 90 over §9.4.
- The CPU figures are each process's own, by the kernel's attribution, so other load changes them
only through contention.
- The GPU figures of §9.2 are from a run that held the GPU alone. In the run of §9.4 another process
held about a third of the GPU's time; the GPU packs then had no GPU figure, and the metered packs
were refused `pack-meter` ("the GPU was shared: this process had 45.2 s of its 65.0 s, under
99%").
- Instruction counts and digests are exact.
## 10. Judged unsound
- **Any WAI step on the neural processor** (§5).
- **A hardware encoder for any WAI step:** floors are supplied, the packer refuses to encode one
(`pack-floor-encode-unsupported`), and no hardware encoder's bytes are reproducible (§6.2).
- **The platform's hardware motion estimation in place of §5.5's search** (§3).
- **Dividing a shared GPU rail by GPU time** (§7.1).
- **Media-engine and neural-processor energy on macOS 27 without privileges:** no live counter
(§7.1); a step run there makes a metered span refuse rather than guess.
- **A floor verdict from a hardware decoder** (§6.3).
- **ffmpeg's P010-to-8-bit conversion in a comparison** (§3).