WAI energy truth: sink measurement profiles, one measured figure per capability, stream energy per hop, display capability #459

Closed
dcharlot wants to merge 12 commits from wai/energy-truth into wai/gpu-int-tier
47 changed files with 9678 additions and 33 deletions

View file

@ -95,6 +95,13 @@ on:
# reports, energy_steering_verify.py reads that table, and the registry's wire
# identifiers test reads its sections.
- 'wai/extensions/energy-steering.md'
# tests/stream_energy.rs requires this extension's §7 to list the codes the reference
# reports, stream_energy_verify.py reads that table, and the registry's wire
# identifiers test reads its sections.
- 'wai/extensions/stream-energy.md'
# codecs::energy_budget's tests recompute each reference budget from the raw spans
# recorded here.
- 'docs/research/wai-sink-energy.jsonl'
# tests/live_moderation.rs requires this extension's §10 to list the codes the reference
# reports, live_moderation_verify.py reads its §9.5, §9.6 and §10 tables, and the
# registry's wire identifiers test reads its sections.
@ -569,7 +576,10 @@ jobs:
# OER's second implementation is portable C, not Rust. Rebuild it from source and
# require its output to match the pinned corpus byte for byte: the kernel is
# deterministic, and wai_oer_vectors above has already verified the pinned copy,
# so equality carries that verification to what the source produces today.
# so equality carries that verification to what the source produces today. The
# modelled corpus is the kernel graded from a meter that is not a measurement
# (model_estimated, projected); the harness also requires the kernel to refuse
# signing such a figure as measured, so a regression there fails the run.
# `cc` with the Makefile's flags, so the step needs no `make` on the runner.
- name: OER C kernel reproduces its pinned receipts (unix)
if: matrix.os != 'windows'
@ -581,6 +591,8 @@ jobs:
-o "$RUNNER_TEMP/settle_host" host_main.c settle.c sha256.c tweetnacl.c -lm
"$RUNNER_TEMP/settle_host" "$RUNNER_TEMP/firmware_vectors.json" 400
cmp "$RUNNER_TEMP/firmware_vectors.json" ../firmware_vectors.json
"$RUNNER_TEMP/settle_host" "$RUNNER_TEMP/firmware_vectors_modelled.json" 400 modelled
cmp "$RUNNER_TEMP/firmware_vectors_modelled.json" ../firmware_vectors_modelled.json
- name: Every cargo-class corpus reproduces its frozen hashes (Windows)
if: matrix.os == 'windows'
shell: pwsh
@ -1105,6 +1117,45 @@ jobs:
(cd wai-rs && cargo test --no-default-features --features energy_steering --lib energy_steering::)
(cd wai-rs && cargo test --no-default-features --features energy_steering --test energy_steering)
python3 -I live-conformance/energy_steering_verify.py live-conformance/steering
# wai.stream.energy (extensions/stream-energy.md): the reference module's unit tests
# (a record round-trips and its signature covers every byte, the legacy class refused
# before anything else, a hardware decode on the media engine a lower bound, the
# display hop always an estimate with its basis, a sum read at its weakest class), the
# sink profiles' tests (energy-measurement §9: the span figure and its declaration
# follow the formula, each refusal has its condition, each profile admits its classes
# and bounds its fabrics) and the reference budgets' tests (§9.5: every budget
# recomputes from its published spans and parameter set), then tests/stream_energy.rs
# (the committed corpus regenerates byte for byte from its fixed seeds, wai-rs gives
# every vector its stated outcome, every reason of §7 refuses a vector, and §7 lists
# exactly the reference's codes); then the corpus's second implementation,
# stream_energy_verify.py, written from the extension's text and sharing no code with
# wai-rs, which checks every signature, recomputes shares, totals and sums at their
# weakest class, and requires every mutated copy of the corpus to fail; standard
# library only, run isolated (-I).
- name: stream energy · wai-rs and stream_energy_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
(cd wai-rs && cargo test --no-default-features --features stream_energy --lib -- stream_energy:: sink_energy:: codecs::energy_budget::)
(cd wai-rs && cargo test --no-default-features --features stream_energy --test stream_energy)
python3 -I energy-conformance/stream_energy_verify.py energy-conformance/stream
# Display capability (extensions/capability-constraints.md §6): the reference module's
# unit tests (declarations checked by §6.2, the acuity bound at the arcminute
# distance, each dimension its own bit, the first rendition within selected by §6.3)
# and tests/display_caps.rs (the committed corpus regenerates byte
# for byte, every vector and refusal gets its stated outcome, and the cases §6.5 names
# are in it); then display_verify.py, written from the extension's text and sharing no
# code with wai-rs, which recomputes every outcome and requires every mutated copy of
# the corpus to fail; standard library only, run isolated (-I).
- name: display capability · wai-rs and display_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
(cd wai-rs && cargo test --no-default-features --lib display_caps::)
(cd wai-rs && cargo test --no-default-features --test display_caps)
python3 -I constraints-conformance/display_verify.py constraints-conformance/display
# wai.live.moderation (extensions/live-moderation.md): the reference module's unit tests
# (policies, statements and notices round-trip and their rules refuse, the export's
# strings and dates, the content types, the states that show an item) and

View file

@ -0,0 +1,39 @@
{"case":"int_motion/video_real","capability":"wai.video.int_motion","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"efce5ada256cb3443a673ee0c10aa4e0a8ce4a372102cd918627e5eec746880f","input_bytes":29428,"output_sha256":"cbf8a7bdba15fb39811a5fcfa6bbd19ff6767a2e715fc1914b3619f2c9f43af3","work":{"units":73728,"unit":"pixels"},"completions_per_span":408,"spans":{"energies_nj":[3941555156,3954730401,3930370213,3933549585,3935447503],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1619279,1594481,1597435,1564551,1590902],"settle_nj":[7127,5048,7518,7245,2312],"liveness":[[29,29,29],[28,28,28],[28,28,28],[28,28,28],[29,29,29]],"cpu_ns":[1566385542,1538280833,1553175542,1531647750,1532395541],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":9655,"class":"OnChipCounter","relative_ppm":1094,"window_us":1564551,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[59.77294921875,99.21826171875,115.98095703125],"loadavg_after":[54.8876953125,96.87255859375,114.95263671875]}
{"case":"int_hyper/tiny","capability":"wai.neural.int_hyper","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"5dd962ad8d60675f458fab5b36d7bcfc62d409755190a77d8a98b8addf118a3b","input_bytes":2764,"output_sha256":"dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf","work":{"units":256,"unit":"pixels"},"completions_per_span":57019,"spans":{"energies_nj":[6937697884,6868113900,6879045507,6882789139,6857964575],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1548132,1576618,1535088,1575180,1552010],"settle_nj":[2877,2989,2436,2798,2710],"liveness":[[28,28,28],[30,30,30],[26,26,26],[29,29,29],[27,27,27]],"cpu_ns":[1495456666,1543469709,1504631583,1527556041,1505366375],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":121,"class":"OnChipCounter","relative_ppm":3118,"window_us":1535088,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[54.8876953125,96.87255859375,114.95263671875],"loadavg_after":[49.33447265625,94.29345703125,113.8251953125]}
{"case":"video_int_hyper/video_nk","capability":"wai.video.int_hyper","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"853a8bb31ece7f04536e9881809c0cedf991e9911549375c6250f6a2e411d806","input_bytes":4824,"output_sha256":"8594d78eba936c12db0b58c4ba5f50379d03e1d0f6f44e78ff6c0103ca520382","work":{"units":1280,"unit":"pixels"},"completions_per_span":16511,"spans":{"energies_nj":[4487231567,4478343632,4490347874,4453344374,4415225080],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1516652,1570581,1617006,1543647,1610334],"settle_nj":[2507,7819,7553,7251,8069],"liveness":[[27,27,27],[28,28,28],[30,30,30],[28,28,28],[29,29,29]],"cpu_ns":[1494510208,1537754041,1572013666,1524001375,1552551583],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":270,"class":"OnChipCounter","relative_ppm":3321,"window_us":1516652,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[49.33447265625,94.29345703125,113.8251953125],"loadavg_after":[48.20361328125,92.55810546875,112.97998046875]}
{"case":"int_codec/audio","capability":"wai.audio.int_codec","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"a428b93c017cdc7e91fd600835e749a89f52014ec36686fcd0060353a570a0fa","input_bytes":14863,"output_sha256":"4b0344c87c67fc30b7d977ae2077fc310822199598e5d2f9f8fc06314b3a2875","work":{"units":8192,"unit":"samples"},"completions_per_span":917,"spans":{"energies_nj":[6607793834,6541667053,6564402340,6565224344,6555325371],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1580015,1539743,1628337,1559906,1568349],"settle_nj":[7134,8528,8176,7400,31273],"liveness":[[27,27,27],[27,27,27],[29,29,29],[28,28,28],[27,27,27]],"cpu_ns":[1542785750,1530169250,1588191042,1534059667,1539334250],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":7161,"class":"OnChipCounter","relative_ppm":1687,"window_us":1539743,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[48.20361328125,92.55810546875,112.97998046875],"loadavg_after":[42.47705078125,89.8720703125,111.7880859375]}
{"case":"int_codec/splat","capability":"wai.splat.int_codec","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"d426c00992eabbb8d673fb516c9f3cdd1135671347cb7a48724f37b41bd2ab93","input_bytes":81727,"output_sha256":"f07679de87ff7c3d4fc4890b80919ef857fb85bd4d9e4cfa16105211d580427f","work":{"units":1024,"unit":"gaussians"},"completions_per_span":2695,"spans":{"energies_nj":[6686397889,6603477907,6574463676,6586784513,6574148918],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1541455,1586069,1604703,1585531,1576673],"settle_nj":[3477,8292,7342,3403,8307],"liveness":[[27,27,27],[28,28,28],[29,29,29],[28,28,28],[27,27,27]],"cpu_ns":[1515077291,1538163458,1557676833,1536804375,1553693875],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":2451,"class":"OnChipCounter","relative_ppm":3186,"window_us":1541455,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[42.47705078125,89.8720703125,111.7880859375],"loadavg_after":[39.248046875,87.6201171875,110.732421875]}
{"case":"int_synth/photo","capability":"wai.neural.int_synth","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"2404a4260bc05ebbdfa4fb3f49f27a20995796e3474754b3ced67e01bac26e9c","input_bytes":798896,"output_sha256":"2a3db89614cd632829d873cf59d6300b0e49c5647a9ede1115719a6604d42115","work":{"units":16384,"unit":"pixels"},"completions_per_span":301,"spans":{"energies_nj":[6291548959,6375851754,6378805470,6367938380,6343074413],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1634880,1488961,1516055,1517420,1502678],"settle_nj":[2965,2678,3161,7337,6706],"liveness":[[29,29,29],[26,26,26],[26,26,26],[27,27,27],[27,27,27]],"cpu_ns":[1562461750,1462136834,1487185916,1489691458,1480779459],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":21101,"class":"OnChipCounter","relative_ppm":2557,"window_us":1488961,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[39.248046875,87.6201171875,110.732421875],"loadavg_after":[35.46533203125,85.212890625,109.60791015625]}
{"case":"int_mlicpp/stream","capability":"wai.neural.int_mlicpp","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"b1d34c812c213a8de002c72894557662cb481576457a9c63812ad6eb4c51ebe0","input_bytes":42289,"output_sha256":"45040d17b4178b91a0547b435e57a50fa1021101e290a94d1c57e6fbb4583a10","work":{"units":4096,"unit":"pixels"},"completions_per_span":2009,"spans":{"energies_nj":[6830396655,7053556688,7015876893,6776800435,7160143501],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1784054,1636352,1706675,1795078,1633800],"settle_nj":[8443,7060,8698,6989,3010],"liveness":[[31,31,31],[29,29,29],[30,30,30],[32,32,32],[29,29,29]],"cpu_ns":[1717501667,1590977917,1632206875,1731686917,1573983792],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":3468,"class":"OnChipCounter","relative_ppm":10252,"window_us":1633800,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[35.46533203125,85.212890625,109.60791015625],"loadavg_after":[34.0830078125,83.27880859375,108.63623046875]}
{"case":"world_model/int_world","capability":"wai.world.model","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"ae1491cd01b2d4583a8e5b26bffd8102bc417e0361d239d33bdf5924abb7a420","input_bytes":77108,"output_sha256":"fa0d33b375167d9f5eeb13bc170efa986f24edeb24d64db2e3f00027403869fb","work":{"units":9,"unit":"frames"},"completions_per_span":175,"spans":{"energies_nj":[5323779999,5490370902,5476573007,5500882124,5496916016],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1612890,1339668,1419044,1262568,1256739],"settle_nj":[8480,2708,6925,7612,2780],"liveness":[[28,28,28],[24,24,24],[25,25,25],[22,22,22],[22,22,22]],"cpu_ns":[1389297542,1294971375,1320472375,1236618375,1222840291],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":31187,"class":"OnChipCounter","relative_ppm":6182,"window_us":1256739,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[34.0830078125,83.27880859375,108.63623046875],"loadavg_after":[31.40478515625,81.09375,107.56494140625]}
{"case":"png/1024x768","capability":"wai.image.png","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"42892d6716dfb9fe2246bab4fa6f7a4c780df326a1eb0043161f79933ce9b2e4","input_bytes":1782056,"output_sha256":"a99c8dd16f7ff58593abbb55316b82fbe84efaae8bb566d4d07327c2abf5d9ce","work":{"units":786432,"unit":"pixels"},"completions_per_span":355,"spans":{"energies_nj":[5458490554,5366121674,5404918887,5312757693,4721777656],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1561733,1533632,1536515,1629357,1965927],"settle_nj":[3611,7472,8751,9649,10711],"liveness":[[27,27,27],[27,27,27],[27,27,27],[29,29,29],[34,34,34]],"cpu_ns":[1503086709,1525169625,1525737625,1547501917,1714757250],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":14797,"class":"OnChipCounter","relative_ppm":25678,"window_us":1533632,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[31.40478515625,81.09375,107.56494140625],"loadavg_after":[35.12841796875,80.2119140625,106.93994140625]}
{"case":"jpeg/1024x768","capability":"wai.image.jpeg","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"143f0cee657a70379281667177bfe182bf751c5b68dbab94c94b1c508aaadc41","input_bytes":238765,"output_sha256":"cf1c0be470d3850c44c53328a2715c8c951603c8c6a26b4c33d477965c3754d4","work":{"units":786432,"unit":"pixels"},"completions_per_span":470,"spans":{"energies_nj":[5634959867,5659303813,5648139752,5771810715,5811444827],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1968616,1936801,1845424,1774926,1729158],"settle_nj":[9728,9978,11207,9657,8923],"liveness":[[33,33,33],[33,33,33],[31,31,31],[32,32,32],[31,31,31]],"cpu_ns":[1720136541,1714746334,1710393542,1680447042,1656277166],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":12139,"class":"OnChipCounter","relative_ppm":6323,"window_us":1729158,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[35.12841796875,80.2119140625,106.93994140625],"loadavg_after":[41.31201171875,80.05517578125,106.57177734375]}
{"case":"avif/1024x768","capability":"wai.image.avif","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"8407f25a62a4b55945a9ed52f2405f73e5d48c89098d18e115578b41651fef25","input_bytes":154488,"output_sha256":"d450d14349a8fcf635c222bfb1b662b74abe4219cebf350c7078582d1396c09d","work":{"units":786432,"unit":"pixels"},"completions_per_span":72,"spans":{"energies_nj":[5708085073,5526089646,5865463383,5840421448,5878811093],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1577669,1621732,1555384,1511096,1484988],"settle_nj":[7594,9582,4846,7565,8295],"liveness":[[29,29,29],[30,30,30],[27,27,27],[26,26,26],[27,27,27]],"cpu_ns":[1529491666,1573725500,1494611583,1479097833,1464621208],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":80052,"class":"OnChipCounter","relative_ppm":11577,"window_us":1484988,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[41.31201171875,80.05517578125,106.57177734375],"loadavg_after":[39.90869140625,78.48681640625,105.70556640625]}
{"case":"jxl/1024x768","capability":"wai.image.jxl","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"d231f13bee3540cfc948735b29aaf22684b642463610d33ad1cabfedadd0b4c3","input_bytes":11632,"output_sha256":"b7b5f41aa41d7435e02d9c409b0eeccdeb3d6abd0de7d1e1257a5d6db85c9a3a","work":{"units":786432,"unit":"pixels"},"completions_per_span":122,"spans":{"energies_nj":[7493898767,7659259523,7556088876,7729505463,7732611610],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1720295,1442485,1630845,1543563,1460258],"settle_nj":[8286,7830,8946,2782,8674],"liveness":[[32,32,32],[27,27,27],[30,30,30],[28,28,28],[27,27,27]],"cpu_ns":[1603166875,1432874208,1565481542,1482746709,1446699292],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":62576,"class":"OnChipCounter","relative_ppm":6226,"window_us":1442485,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[39.90869140625,78.48681640625,105.70556640625],"loadavg_after":[38.294921875,76.8857421875,104.81982421875]}
{"case":"opus/10s-stereo","capability":"wai.audio.opus","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"67c07b61a751fbdf92cb659b138008877a43a1fe525233b9e802add52531e628","input_bytes":323406,"output_sha256":"89f391cb96b6865297f6f864b078e31eab93d35d7b214d234ef5cead892da271","work":{"units":960000,"unit":"samples"},"completions_per_span":67,"spans":{"energies_nj":[5466692050,5565531843,5457566274,5613370774,5648448157],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1806138,1807526,1770942,1751822,1688065],"settle_nj":[8945,9992,8252,7699,7390],"liveness":[[33,33,33],[32,32,32],[33,33,33],[32,32,32],[31,31,31]],"cpu_ns":[1654724459,1626549083,1659824667,1632952167,1617460917],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":82841,"class":"OnChipCounter","relative_ppm":6912,"window_us":1688065,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[38.294921875,76.8857421875,104.81982421875],"loadavg_after":[43.04052734375,76.65283203125,104.412109375]}
{"case":"flac/10s-stereo","capability":"wai.audio.flac","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"b76f782ae2571503ef43abb5dd85a3d5e1437c57f2e09d48bf76b32b13d5186d","input_bytes":1353452,"output_sha256":"60790d0b64d0eb0ff2f23e1b733afc5a067bb4003699402872d7553ebda05033","work":{"units":960000,"unit":"samples"},"completions_per_span":134,"spans":{"energies_nj":[5117693599,5167201472,5159609360,5010862708,4947352439],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1597676,1560647,1504677,1620752,1736054],"settle_nj":[40033,7634,2513,8818,8345],"liveness":[[28,28,28],[27,27,27],[26,26,26],[29,29,29],[31,31,31]],"cpu_ns":[1534612500,1501885917,1498535875,1561133125,1590742875],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":37915,"class":"OnChipCounter","relative_ppm":8550,"window_us":1504677,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.04052734375,76.65283203125,104.412109375],"loadavg_after":[43.3544921875,75.583984375,103.70703125]}
{"case":"zstd/SPEC.md","capability":"wai.text.zstd","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"9125054aebee1c1ee0cb483cb8bba39c1fccb91027fdbbc5761d63703c1baa29","input_bytes":66728,"output_sha256":"696d19ecdee0960a6f02758385e8e267c4d6e59e20cf1f2064628adbd50c25ea","work":{"units":221853,"unit":"bytes"},"completions_per_span":8372,"spans":{"energies_nj":[6298097936,6379399080,6436745302,6309252264,6081999789],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1547572,1439305,1440648,1490711,1654838],"settle_nj":[8539,3112,2644,7273,7843],"liveness":[[28,28,28],[25,25,25],[27,27,27],[26,26,26],[29,29,29]],"cpu_ns":[1465826959,1427246250,1403116375,1445770250,1496091792],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":753,"class":"OnChipCounter","relative_ppm":9566,"window_us":1439305,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.3544921875,75.583984375,103.70703125],"loadavg_after":[43.61328125,74.55078125,103.009765625]}
{"case":"xz/SPEC.md","capability":"wai.text.xz","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"691cdca1a6bfb2a877f31257578aa5a6db2a10665413cd4ea5f76b2c8d91143c","input_bytes":65476,"output_sha256":"696d19ecdee0960a6f02758385e8e267c4d6e59e20cf1f2064628adbd50c25ea","work":{"units":221853,"unit":"bytes"},"completions_per_span":695,"spans":{"energies_nj":[4830244595,4892331354,4937320545,4780419984,4788886453],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1640260,1612425,1598427,1801930,1798496],"settle_nj":[7855,7298,9128,10355,8159],"liveness":[[30,30,30],[29,29,29],[29,29,29],[32,32,32],[32,32,32]],"cpu_ns":[1576398417,1572607334,1563928750,1641636125,1632170125],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":6972,"class":"OnChipCounter","relative_ppm":6243,"window_us":1598427,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.61328125,74.55078125,103.009765625],"loadavg_after":[43.6337890625,73.5166015625,102.30859375]}
{"case":"av1/640x360x30","capability":"wai.video.av1","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"5070597235c5d601b6cf33f661393baa9f6d9e18a2787789c83c602d3a566356","input_bytes":73064,"output_sha256":"644f0d02dfad11a1d0f9d186feabfeb80d2aa11e221175d51356cf4481dd10c5","work":{"units":6912000,"unit":"pixels"},"completions_per_span":26,"spans":{"energies_nj":[3295829878,3643794862,3142667812,4073011061,5396618076],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[2589061,1783018,1642590,1325672,1114854],"settle_nj":[7798,8866,7171,2399,7516],"liveness":[[43,43,43],[31,31,31],[29,29,29],[23,23,23],[19,19,19]],"cpu_ns":[1621506792,1850466875,1846541625,2437807333,3305228917],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":150399,"class":"OnChipCounter","relative_ppm":103460,"window_us":1114854,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.6337890625,73.5166015625,102.30859375],"loadavg_after":[42.888671875,72.404296875,101.5791015625]}
{"case":"av1.lossless/640x360x30","capability":"wai.video.av1.lossless","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"2ad3daf044fc9cfbddda61403e7a1a6f444292380c1a249e1f5f85bdab60c435","input_bytes":227898,"output_sha256":"9b46e198b310cd8099de82f5a3996f1a2b1c2462a247e94a6936205640afa063","work":{"units":6912000,"unit":"pixels"},"completions_per_span":32,"spans":{"energies_nj":[7956753651,8619583887,7652663740,8166032848,8530623315],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1596419,1660593,1696276,1619073,1564101],"settle_nj":[6940,2665,2773,2387,5654],"liveness":[[27,27,27],[31,31,31],[30,30,30],[28,28,28],[28,28,28]],"cpu_ns":[3792755584,4163230375,3707972917,3901309042,3987975542],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":255785,"class":"OnChipCounter","relative_ppm":21926,"window_us":1564101,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[42.888671875,72.404296875,101.5791015625],"loadavg_after":[38.6484375,70.52880859375,100.57275390625]}
{"case":"haptic/thud","capability":"wai.haptic.signal","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"f316c6df2fe04a2c5c9afbe9cf1b48c6399b7f6bdefb3559b88608a130ff743f","input_bytes":160,"output_sha256":"aa496bf1d42be4e34d361f8f5ef8cca515a9eeac93a42a93de6525cd4526b281","work":{"units":120,"unit":"samples"},"completions_per_span":1048576,"spans":{"energies_nj":[5467195874,5182613465,5239221700,5310510243,5165679644],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1456335,1659419,1667889,1585544,1669422],"settle_nj":[8196,8262,7618,7851,2660],"liveness":[[27,27,27],[30,30,30],[30,30,30],[29,29,29],[30,30,30]],"cpu_ns":[1429010667,1578721583,1561732750,1499225709,1590597125],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":5,"class":"OnChipCounter","relative_ppm":58672,"window_us":1456335,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[38.6484375,70.52880859375,100.57275390625],"loadavg_after":[43.07080078125,70.44140625,100.19140625]}
{"case":"score/chord","capability":"wai.audio.score","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"e39e4123c8b404f6d316a94f47639ab7c8a3a9144635a373f0fa11627c5584ba","input_bytes":304,"output_sha256":"c4e9c4e9f3fe59b76875cd22e16a8251092a921d2ddffefc2c05aa61ef8f79b7","work":{"units":10000,"unit":"samples"},"completions_per_span":8073,"spans":{"energies_nj":[4825609897,4664326202,4751293336,4773567299,4615665319],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1742149,1949267,1956641,1861239,1986934],"settle_nj":[5242,10461,35936,45027,10045],"liveness":[[30,30,30],[34,34,34],[36,36,36],[33,33,33],[35,35,35]],"cpu_ns":[1633623459,1748934167,1705787625,1679212459,1781763167],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":585,"class":"OnChipCounter","relative_ppm":8051,"window_us":1742149,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.07080078125,70.44140625,100.19140625],"loadavg_after":[47.787109375,70.52978515625,99.87353515625]}
{"case":"film/dolly","capability":"wai.film.linear","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"f02eae0262627de5a24ef94ec4b01a24d3241d393a65e7c349dc99eec3840754","input_bytes":1257,"output_sha256":"c75dca469f3b708a628cf025882dc0624cba773d63538f6f02a521a52270ca40","work":{"units":9,"unit":"frames"},"completions_per_span":12584,"spans":{"energies_nj":[4142518603,4155666313,4088800958,4206070059,4131187991],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1951976,1849127,2042283,1742308,1948261],"settle_nj":[9564,42940,7996,8061,47948],"liveness":[[34,34,34],[33,33,33],[35,35,35],[31,31,31],[34,34,34]],"cpu_ns":[1663598792,1642939750,1711729000,1591322791,1649427708],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":329,"class":"OnChipCounter","relative_ppm":4666,"window_us":1742308,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[47.787109375,70.52978515625,99.87353515625],"loadavg_after":[55.1767578125,71.353515625,99.822265625]}
{"case":"reel/coverage","capability":"wai.film.reel","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"83013c3485437b054770ab9cf574a0dd806cc62afab9f4a208938c75480f5742","input_bytes":6618,"output_sha256":"b525fed7e5041b2a9c69c53364ab4da84734650292d7847910e2cf25e6b5c517","work":{"units":12,"unit":"frames"},"completions_per_span":1528,"spans":{"energies_nj":[4078194247,4095275758,3950314078,4017159276,4027218908],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1839817,1765251,2032223,1829153,1797778],"settle_nj":[8425,8873,54523,7888,7607],"liveness":[[32,32,32],[31,31,31],[35,35,35],[33,33,33],[32,32,32]],"cpu_ns":[1636112125,1589322083,1714594208,1672336750,1658620500],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":2640,"class":"OnChipCounter","relative_ppm":6331,"window_us":1765251,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[59.16650390625,71.91162109375,99.8525390625],"loadavg_after":[66.00439453125,72.95263671875,99.89453125]}
{"case":"splat4d/orbit","capability":"wai.splat.4d","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"0a6da0771b55aee9d66b1b15aa0d55c731749e7504d0c31090e07e34ef552dc8","input_bytes":592,"output_sha256":"f7de606c76d5e0161d337adfb860020cc15375f1176329f9bf00538dc68584e3","work":{"units":26,"unit":"frame-gaussians"},"completions_per_span":290833,"spans":{"energies_nj":[4584568058,4495919118,4372569408,4411707581,4566096031],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1659086,1695351,1861703,1745877,1588901],"settle_nj":[9070,9550,8752,10457,10039],"liveness":[[29,29,29],[29,29,29],[32,32,32],[31,31,31],[29,29,29]],"cpu_ns":[1460073375,1494050958,1553117166,1523893666,1461961375],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":15,"class":"OnChipCounter","relative_ppm":21479,"window_us":1588901,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[66.00439453125,72.95263671875,99.89453125],"loadavg_after":[73.20654296875,74.28271484375,100.052734375]}
{"case":"protocol/sleep-1s","capability":"none","decoder":"none","policy":"fast kernels, one worker","input_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","input_bytes":0,"output_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","work":{"units":1,"unit":"spans"},"completions_per_span":2,"spans":{"energies_nj":[671892,453995,869648,900240,393672],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[2045012,2051338,2054279,2033261,2068323],"settle_nj":[9235,9148,8862,45931,11408],"liveness":[[38,38,38],[38,38,38],[38,38,38],[38,38,38],[37,37,37]],"cpu_ns":[2287459,2234417,2318083,2156542,2163291],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":329,"class":"OnChipCounter","relative_ppm":162702,"window_us":2033261,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[73.20654296875,74.28271484375,100.052734375],"loadavg_after":[67.9931640625,73.076171875,99.17138671875]}
{"case":"av1_10bit/dav1d","capability":"wai.video.av1","decoder":"dav1d 1.5.4, 4 threads","command":["dav1d","-q","-i","in/av1_10bit.ivf","-o","/dev/null","--muxer","null","--threads","4"],"input_sha256":"37c45a33f5be0f702e9346eead49e5d0a0025749551bd11a1bd572cd4e0ef1fe","input_bytes":13810150,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[9651871964,10695529844,9335532704,12178866288,13811942203],"billed_nj":[6956,11315,8782,5873,6574],"service_nj":[5598163893,1411085485,0,0,0],"services":[16,3,0,0,0],"windows_us":[9918905,4761574,8162966,4143730,3594433],"settle_nj":[0,0,0,0,0],"liveness":[[151,150,150],[80,79,79],[129,128,128],[68,67,67],[60,59,59]],"cpu_ns":[7313357125,7205569958,7262531083,7049588916,6677534416],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":11134749,"class":"OnChipCounter","relative_ppm":74835,"window_us":3594433,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[67.9931640625,73.076171875,99.17138671875],"loadavg_after":[62.74365234375,71.48095703125,97.6982421875]}
{"case":"av1_10bit/videotoolbox","capability":"wai.video.av1","decoder":"VideoToolbox through FFmpeg 9.0.2","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-hwaccel","videotoolbox","-hwaccel_output_format","videotoolbox_vld","-i","in/av1_10bit.mp4","-map","0:v","-f","null","-"],"input_sha256":"545599cd04e9978130675c5da4165f167c6c6f790268611e8f2cc2358fdc8a22","input_bytes":13796900,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[914602472,839165487,792155819,723534979,780917268],"billed_nj":[463265330,442582603,393052474,360731797,405969123],"service_nj":[670475772,630034187,582087058,541696005,588826608],"services":[1,1,1,1,1],"windows_us":[2621912,2461291,2381344,2417116,2308343],"settle_nj":[0,0,0,0,0],"liveness":[[44,43,43],[42,41,41],[40,39,39],[41,40,40],[39,38,38]],"cpu_ns":[668078458,655284416,604123291,596413541,574777416],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu","media"],"figure":{"joules_micro":810075,"class":"OnChipCounter","relative_ppm":39457,"window_us":2308343,"filtering":"Unknown","coverage":"Partial","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[62.74365234375,71.48095703125,97.6982421875],"loadavg_after":[66.98291015625,71.955078125,97.40673828125]}
{"case":"av1_8bit/dav1d","capability":"wai.video.av1","decoder":"dav1d 1.5.4, 4 threads","command":["dav1d","-q","-i","in/av1_8bit.ivf","-o","/dev/null","--muxer","null","--threads","4"],"input_sha256":"b7f59e1c04fb544170e81982d8281971898d1ec83c1ba31d93f8b132549fe36c","input_bytes":11986821,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[8333663467,8213689868,6770106266,7373794105,7865218647],"billed_nj":[7332,7038,7914,7420,20666],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[8150883,9085374,4837294,11309988,12552573],"settle_nj":[0,0,0,0,0],"liveness":[[124,123,123],[136,135,135],[77,76,76],[167,166,166],[183,182,182]],"cpu_ns":[4826437000,4802418833,5581635250,4985488125,4782767083],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":7711294,"class":"OnChipCounter","relative_ppm":37397,"window_us":4837294,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[66.98291015625,71.955078125,97.40673828125],"loadavg_after":[87.927734375,76.78515625,97.87158203125]}
{"case":"av1_8bit/videotoolbox","capability":"wai.video.av1","decoder":"VideoToolbox through FFmpeg 9.0.2","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-hwaccel","videotoolbox","-hwaccel_output_format","videotoolbox_vld","-i","in/av1_8bit.mp4","-map","0:v","-f","null","-"],"input_sha256":"a8a7448793a855d19a07ce33cc55559328549e123c5d73abae1012c6327de438","input_bytes":11974893,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[889608809,827210394,861313186,928983804,785513678],"billed_nj":[521029468,509705924,523850709,526093098,477791725],"service_nj":[7586228658,676358651,669731041,5488342543,630060102],"services":[17,1,1,12,1],"windows_us":[4371784,4531783,4447877,4515958,4475457],"settle_nj":[0,0,0,0,0],"liveness":[[72,71,71],[75,74,74],[75,74,74],[74,73,73],[74,73,73]],"cpu_ns":[735364583,822050583,792923375,810231875,780794041],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu","media"],"figure":{"joules_micro":858526,"class":"OnChipCounter","relative_ppm":28827,"window_us":4371784,"filtering":"Unknown","coverage":"Partial","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[87.927734375,76.78515625,97.87158203125],"loadavg_after":[82.5048828125,76.58447265625,97.0703125]}
{"case":"hevc/ffmpeg","capability":"wai.video.hevc","decoder":"FFmpeg 9.0.2 hevc, 4 threads","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-threads","4","-i","in/hevc.mp4","-map","0:v","-f","null","-"],"input_sha256":"ca061d92265642de4db05b292001d778d35bc84961b43d451522b2a74474870d","input_bytes":13879385,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[10255171837,11693556332,10141508887,11810832350,11545874792],"billed_nj":[6833,5415,6786,9421,7291],"service_nj":[6987969221,0,3477668235,0,0],"services":[16,0,8,0,0],"windows_us":[7915215,6805438,6616973,9900601,10217469],"settle_nj":[0,0,0,0,0],"liveness":[[121,120,120],[106,105,105],[103,102,102],[148,147,147],[152,149,149]],"cpu_ns":[7453248916,6988401500,7432819541,6881976958,6903496916],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":11089389,"class":"OnChipCounter","relative_ppm":33061,"window_us":6616973,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[82.5048828125,76.58447265625,97.0703125],"loadavg_after":[85.50830078125,77.7578125,96.537109375]}
{"case":"hevc/videotoolbox","capability":"wai.video.hevc","decoder":"VideoToolbox through FFmpeg 9.0.2","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-hwaccel","videotoolbox","-hwaccel_output_format","videotoolbox_vld","-i","in/hevc.mp4","-map","0:v","-f","null","-"],"input_sha256":"ca061d92265642de4db05b292001d778d35bc84961b43d451522b2a74474870d","input_bytes":13879385,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[888570353,820557820,844953918,831143758,849513973],"billed_nj":[458470020,455859781,454356933,450916641,459251547],"service_nj":[613773654,609558910,584000634,602294372,610532203],"services":[1,1,1,1,1],"windows_us":[4307213,4329493,4301051,4331327,4308526],"settle_nj":[0,0,0,0,0],"liveness":[[71,70,70],[72,71,71],[71,70,70],[72,70,70],[72,71,71]],"cpu_ns":[912642625,962709250,944142416,935362541,938999708],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu","media"],"figure":{"joules_micro":846948,"class":"OnChipCounter","relative_ppm":13692,"window_us":4301051,"filtering":"Unknown","coverage":"Partial","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[85.50830078125,77.7578125,96.537109375],"loadavg_after":[86.048828125,78.48681640625,96.25439453125]}
{"case":"h264/ffmpeg","capability":"wai.video.avc","decoder":"FFmpeg 9.0.2 h264, 4 threads","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-threads","4","-i","in/h264.mp4","-map","0:v","-f","null","-"],"input_sha256":"718ceec17117b3de280efa413055b48635e1f1f17b9aa1a073d2fe3ba25f9bcf","input_bytes":22992199,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[12948885911,12717486712,12943163990,18631630496,16641862077],"billed_nj":[27918,7527,6302,5326,4348],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[5424298,4021953,3061957,1903541,2327641],"settle_nj":[0,0,0,0,0],"liveness":[[85,84,84],[63,62,62],[50,49,49],[32,31,31],[39,38,38]],"cpu_ns":[6251787875,6373217291,6532757541,4802011125,5235079416],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":14776606,"class":"OnChipCounter","relative_ppm":81887,"window_us":1903541,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[86.048828125,78.48681640625,96.25439453125],"loadavg_after":[76.1044921875,76.66455078125,95.29345703125]}
{"case":"h264/videotoolbox","capability":"wai.video.avc","decoder":"VideoToolbox through FFmpeg 9.0.2","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-hwaccel","videotoolbox","-hwaccel_output_format","videotoolbox_vld","-i","in/h264.mp4","-map","0:v","-f","null","-"],"input_sha256":"718ceec17117b3de280efa413055b48635e1f1f17b9aa1a073d2fe3ba25f9bcf","input_bytes":22992199,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[1053333890,850490829,853093769,947261538,897165664],"billed_nj":[485028442,389174963,382768553,441868244,415827031],"service_nj":[651913424,549945049,545922736,12170072952,585528255],"services":[1,1,1,30,1],"windows_us":[4587943,4519071,4455891,4444665,4418829],"settle_nj":[0,0,0,0,0],"liveness":[[76,75,75],[72,71,71],[73,72,72],[72,71,71],[72,71,71]],"cpu_ns":[1040427958,1072122458,1042478541,962991375,1023413166],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu","media"],"figure":{"joules_micro":920269,"class":"OnChipCounter","relative_ppm":40912,"window_us":4418829,"filtering":"Unknown","coverage":"Partial","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[76.1044921875,76.66455078125,95.29345703125],"loadavg_after":[60.48583984375,72.947265625,93.4130859375]}
{"case":"av1_10bit/ffmpeg-demux-only","capability":"none","decoder":"FFmpeg 9.0.2, stream copy (no decode)","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-i","in/av1_10bit.mp4","-map","0:v","-c","copy","-f","null","-"],"input_sha256":"545599cd04e9978130675c5da4165f167c6c6f790268611e8f2cc2358fdc8a22","input_bytes":13796900,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[121260261,122938157,78846819,70906615,104917211],"billed_nj":[4040,11331,3393,5939,5667],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[65330,65513,66611,57387,65802],"settle_nj":[0,0,0,0,0],"liveness":[[2,1,1],[2,1,1],[2,1,1],[2,1,1],[2,1,1]],"cpu_ns":[35999416,36342583,49567958,50384208,40167666],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"refused":"span_not_live"},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[60.48583984375,72.947265625,93.4130859375],"loadavg_after":[60.48583984375,72.947265625,93.4130859375]}
{"case":"av1_8bit/ffmpeg-demux-only","capability":"none","decoder":"FFmpeg 9.0.2, stream copy (no decode)","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-i","in/av1_8bit.mp4","-map","0:v","-c","copy","-f","null","-"],"input_sha256":"a8a7448793a855d19a07ce33cc55559328549e123c5d73abae1012c6327de438","input_bytes":11974893,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[120216992,121892878,90931044,92498799,98900226],"billed_nj":[13453,5591,6488,3083,5083],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[65700,65537,58479,56439,66463],"settle_nj":[0,0,0,0,0],"liveness":[[2,1,1],[2,1,1],[2,1,1],[2,1,1],[2,1,1]],"cpu_ns":[35596916,39085291,46166708,45715958,44142916],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"refused":"span_not_live"},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[60.48583984375,72.947265625,93.4130859375],"loadavg_after":[60.48583984375,72.947265625,93.4130859375]}
{"case":"hevc/ffmpeg-demux-only","capability":"none","decoder":"FFmpeg 9.0.2, stream copy (no decode)","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-i","in/hevc.mp4","-map","0:v","-c","copy","-f","null","-"],"input_sha256":"ca061d92265642de4db05b292001d778d35bc84961b43d451522b2a74474870d","input_bytes":13879385,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[112347297,113218116,72950860,82071290,112360953],"billed_nj":[19013,11992,2512,2507,12356],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[65011,64712,62340,66527,65536],"settle_nj":[0,0,0,0,0],"liveness":[[2,1,1],[2,1,1],[2,1,1],[2,1,1],[2,1,1]],"cpu_ns":[32156208,33844625,50618750,42744541,32340041],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"refused":"span_not_live"},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[60.48583984375,72.947265625,93.4130859375],"loadavg_after":[60.48583984375,72.947265625,93.4130859375]}
{"case":"h264/ffmpeg-demux-only","capability":"none","decoder":"FFmpeg 9.0.2, stream copy (no decode)","command":["ffmpeg","-nostdin","-hide_banner","-loglevel","error","-i","in/h264.mp4","-map","0:v","-c","copy","-f","null","-"],"input_sha256":"718ceec17117b3de280efa413055b48635e1f1f17b9aa1a073d2fe3ba25f9bcf","input_bytes":22992199,"work":{"units":1440,"unit":"frames"},"completions_per_span":1,"spans":{"energies_nj":[186282708,186429425,187602955,189126032,183602735],"billed_nj":[5777,5329,2163,29295,5972],"service_nj":[0,0,0,0,5791425964],"services":[0,0,0,0,16],"windows_us":[64941,65230,64955,64743,57821],"settle_nj":[0,0,0,0,0],"liveness":[[2,1,1],[2,1,1],[2,1,1],[2,1,1],[2,1,1]],"cpu_ns":[53018708,52426791,52557125,56403875,50737958],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"refused":"span_not_live"},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[60.48583984375,72.947265625,93.4130859375],"loadavg_after":[56.60302734375,71.93505859375,92.93603515625]}
{"case":"world_replay/basic_kinematics","capability":"wai.world.replay","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"c90b8a03ea11ab87428b2255f402c17eb26a89c625b59d8d753afad143707491","input_bytes":1394,"output_sha256":"917ccd419bf0e1360441ce11c3cde83a9ee90badca2b7a0a0651799b47891953","work":{"units":200,"unit":"ticks"},"completions_per_span":6998,"spans":{"energies_nj":[4165206495,4221683404,4163956788,4258611767,4246600207],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1735662,1579109,1613841,1532674,1568832],"settle_nj":[8453,8268,6784,7621,6334],"liveness":[[30,30,30],[28,28,28],[29,29,29],[27,27,27],[27,27,27]],"cpu_ns":[1569792542,1535462708,1577928750,1487730625,1512120833],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":602,"class":"OnChipCounter","relative_ppm":4760,"window_us":1532674,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[43.96240234375,47.66015625,74.95654296875],"loadavg_after":[41.1748046875,46.9453125,74.3837890625]}
{"case":"feed_state/market_ticks","capability":"wai.feed.state","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"b8b9aee96579bf8a3c75512e3d835290c140a4830b8f0e17bc875ab92c18366b","input_bytes":1154,"output_sha256":"c3e22385ec0c45384ea12e8cec396d280d4b40350e90c7188385f4a2a0c43a4d","work":{"units":3,"unit":"ticks"},"completions_per_span":8786,"spans":{"energies_nj":[4329738901,4251518871,4277318667,4291100386,4305501354],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1529618,1537512,1537924,1542556,1535822],"settle_nj":[2739,3377,2794,2582,2976],"liveness":[[26,26,26],[26,26,26],[26,26,26],[26,26,26],[27,27,27]],"cpu_ns":[1494978458,1508027333,1526298042,1504904292,1525680167],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":488,"class":"OnChipCounter","relative_ppm":3123,"window_us":1529618,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[36.50732421875,45.638671875,73.4384765625],"loadavg_after":[32.65234375,44.513671875,72.7138671875]}
{"case":"avatar_pose/arm_wave","capability":"wai.avatar.pose","decoder":"builtin:[email protected]","policy":"fast kernels, one worker","input_sha256":"c53a0c65c91dd8bb481e48e815e4146efdea5cb6c7cf05ba4e1aefd415638a80","input_bytes":2442,"output_sha256":"c35086d4e9be522ed39223196651e188c3316efa691f75a6315949a1deb12921","work":{"units":8,"unit":"ticks"},"completions_per_span":3757,"spans":{"energies_nj":[4142125715,4043965954,3990721853,4084091280,4128101863],"billed_nj":[0,0,0,0,0],"service_nj":[0,0,0,0,0],"services":[0,0,0,0,0],"windows_us":[1464198,1506323,1657400,1492994,1518008],"settle_nj":[2905,7873,8855,7738,2550],"liveness":[[26,26,26],[27,27,27],[29,29,29],[26,26,26],[27,27,27]],"cpu_ns":[1437717250,1486289458,1588206167,1461699583,1455524625],"gpu_ns":[0,0,0,0,0]},"fabrics":["cpu"],"figure":{"joules_micro":1085,"class":"OnChipCounter","relative_ppm":6821,"window_us":1464198,"filtering":"Unknown","coverage":"Whole","fits_profile":true},"profile":"macos-task","build":{"profile":"release","toolchain":"rustc 1.98.0 (88d9e12ae 2026-08-18)","target":"aarch64-apple-darwin"},"host":{"chip":"Apple M3 Ultra","cpus":"32","os":"macOS 27.0.1"},"loadavg_before":[31.36572265625,43.65380859375,71.9140625],"loadavg_after":[30.626953125,43.0888671875,71.38330078125]}

View file

@ -0,0 +1,219 @@
# What a sink spends decoding: one figure per capability, without privileges
Measurements of 2026-10-22, for [energy-measurement](../../wai/extensions/energy-measurement.md)
§9 (the sink measurement profiles, the span protocol for a task counter, and the energy
budgets of learned capabilities) and for the sink decode figure of a
[stream-energy](../../wai/extensions/stream-energy.md) hop record. Every figure is a decode
on one sink, the build host, read through the `macos-task` profile with no privilege:
no `sudo`, no root counter, no entitlement.
**Method.** Exact counts and digests are exact. Times and energies are indicative: the host
is shared, and each figure's row gives the one-minute load average before and after it. The
raw spans of every row are in [`wai-sink-energy.jsonl`](wai-sink-energy.jsonl), one JSON line
per case, from which §9.4's arithmetic (`wai::sink_energy::SpanSet::figure`) gives each
figure and its declaration again; a test (`codecs::energy_budget`) does so for the budgets.
## 1. The sink and the reader
| | |
|---|---|
| Sink | the build host: Apple M3 Ultra, 32 CPU cores, macOS 27.0.1 |
| Load | one-minute load average 30 to 88 from other work over the runs (per row below) |
| Counter | `proc_pid_rusage(RUSAGE_INFO_V6)`: `ri_energy_nj + ri_billed_energy` of the decoding process, read by that process for itself, or for a child before it is reaped |
| GPU | the IORegistry's per-client `accumulatedGPUTime`: no case accrued any, so no GPU rail was needed |
| Media engine, neural engine | no counter (energy-measurement §9.2); a decode on the media engine is a lower bound |
| Protocol | §9.4: 5 spans per case; in process, each span about 1.5 s of whole decodes on one worker thread under the fast kernels (`Policy::fast(1)`), started after the opening read; out of process, one run of the decoder per span; the counter read every 50 ms for liveness; a 200 ms settle wait, then a 100 ms settle check |
| Tool | `wai_sink_meter` (`wai/wai-rs/src/bin/wai_sink_meter.rs`), release build, rustc 1.98.0, `aarch64-apple-darwin`; SHA-256 `4e5cdab6…4a43` for every row but the three sealed-world rows, `72fea002…a2a` (the same code with those cases added) for them |
| External decoders | dav1d 1.5.4 and FFmpeg 9.0.2 (its `hevc` and `h264` decoders, and VideoToolbox through `-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld`, which cannot fall back to software), from Homebrew |
**The counter, probed first** (one thread spinning, the process reading itself). It advanced
in each of 100 successive reads 2 ms, 10 ms and 50 ms apart. After the spinning thread was
joined, the process's counter rose by a further 75 to 227 µJ over the next second, most of it
within the first 200 ms, while its only remaining thread slept between reads; in two of three
earlier probes whose reader polled every 2 to 10 ms, 9.4 mJ and 12.1 mJ arrived within 100 ms
of the join. That is why a span closes after a 200 ms settle wait and declares what its last
100 ms check still shows as a systematic term.
**The protocol's own cost.** A span whose work is a thread sleeping for one second costs
329 µJ (±16 %): the reader's wakes and the sleeping thread. It is inside every in-process
span, so every in-process figure includes about 0.3 mJ per second of span, at most 0.03 % of
any figure below except `protocol/sleep-1s` itself.
**Why the kernel's billing, not the decoder service's total.** VideoToolbox decodes in a
service process of its own (`VTDecoderXPCService`), and on this shared host other clients
start such services at any moment: in one 58 ms run of a decoder that uses no VideoToolbox at
all, 16 services started and spent 5.8 J. So a service's whole energy is never attributed to
the decode. The kernel bills a service's work for a client to that client
(`ri_billed_energy`, its `energy_billed_to_me` ledger, in nanojoules); the figure counts that,
and a VideoToolbox figure is a lower bound for that reason too: what the kernel bills to no
one is not in it.
## 2. WAI's capabilities, decoded in the reference sink
Each row decodes the capability's committed vector (the integer codecs' and the extension
classes' conformance vectors) or a source this tool makes from a fixed seed and encodes once
before metering (1024×768 pictures, ten seconds of 48 kHz stereo, the repository's SPEC.md,
and 30 frames of 640×360 video). Each figure is per decode of that input, `OnChipCounter`,
filtering `Unknown`, coverage `Whole`: every one ran on the CPU alone.
| case | capability | figure | `relative_ppm` | `window_us` | per unit | load |
|---|---|---:|---:|---:|---:|---|
| int_motion/video_real | `wai.video.int_motion` | 9.655 mJ | 1,094 | 1,564,551 | 130 nJ per pixel | 60–55 |
| int_hyper/tiny | `wai.neural.int_hyper` | 121 µJ | 3,118 | 1,535,088 | 472 nJ per pixel | 55–49 |
| video_int_hyper/video_nk | `wai.video.int_hyper` | 270 µJ | 3,321 | 1,516,652 | 210 nJ per pixel | 49–48 |
| int_codec/audio | `wai.audio.int_codec` | 7.161 mJ | 1,687 | 1,539,743 | 874 nJ per sample | 48–42 |
| int_codec/splat | `wai.splat.int_codec` | 2.451 mJ | 3,186 | 1,541,455 | 2,393 nJ per Gaussian | 42–39 |
| int_synth/photo | `wai.neural.int_synth` | 21.101 mJ | 2,557 | 1,488,961 | 1,287 nJ per pixel | 39–35 |
| int_mlicpp/stream | `wai.neural.int_mlicpp` | 3.468 mJ | 10,252 | 1,633,800 | 846 nJ per pixel | 35–34 |
| world_model/int_world | `wai.world.model` | 31.187 mJ | 6,182 | 1,256,739 | 3.465 mJ per frame | 34–31 |
| world_replay/basic_kinematics | `wai.world.replay` | 602 µJ | 4,760 | 1,532,674 | 3.0 µJ per tick | 44–41 |
| feed_state/market_ticks | `wai.feed.state` | 488 µJ | 3,123 | 1,529,618 | 163 µJ per tick | 37–33 |
| avatar_pose/arm_wave | `wai.avatar.pose` | 1.085 mJ | 6,821 | 1,464,198 | 136 µJ per tick | 31–31 |
| png/1024x768 | `wai.image.png` | 14.797 mJ | 25,678 | 1,533,632 | 18 nJ per pixel | 31–35 |
| jpeg/1024x768 | `wai.image.jpeg` | 12.139 mJ | 6,323 | 1,729,158 | 15 nJ per pixel | 35–41 |
| avif/1024x768 | `wai.image.avif` | 80.052 mJ | 11,577 | 1,484,988 | 101 nJ per pixel | 41–40 |
| jxl/1024x768 | `wai.image.jxl` | 62.576 mJ | 6,226 | 1,442,485 | 79 nJ per pixel | 40–38 |
| opus/10s-stereo | `wai.audio.opus` | 82.841 mJ | 6,912 | 1,688,065 | 86 nJ per sample | 38–43 |
| flac/10s-stereo | `wai.audio.flac` | 37.915 mJ | 8,550 | 1,504,677 | 39 nJ per sample | 43–43 |
| zstd/SPEC.md | `wai.text.zstd` | 753 µJ | 9,566 | 1,439,305 | 3 nJ per byte | 43–44 |
| xz/SPEC.md | `wai.text.xz` | 6.972 mJ | 6,243 | 1,598,427 | 31 nJ per byte | 44–44 |
| av1/640x360x30 | `wai.video.av1` | 150.399 mJ | 103,460 | 1,114,854 | 21 nJ per pixel | 44–43 |
| av1.lossless/640x360x30 | `wai.video.av1.lossless` | 255.785 mJ | 21,926 | 1,564,101 | 37 nJ per pixel | 43–39 |
| haptic/thud | `wai.haptic.signal` | 5 µJ | 58,672 | 1,456,335 | 41 nJ per sample | 39–43 |
| score/chord | `wai.audio.score` | 585 µJ | 8,051 | 1,742,149 | 58 nJ per sample | 43–48 |
| film/dolly | `wai.film.linear` | 329 µJ | 4,666 | 1,742,308 | 37 µJ per frame | 48–55 |
| reel/coverage | `wai.film.reel` | 2.640 mJ | 6,331 | 1,765,251 | 220 µJ per frame | 59–66 |
| splat4d/orbit | `wai.splat.4d` | 15 µJ | 21,479 | 1,588,901 | 576 nJ per frame-Gaussian | 66–73 |
The decoded output of the integer vectors is the conformance output: its SHA-256 equals the
committed sidecar's for `int_hyper/tiny` (`dce7282d…82cf`), `int_codec/audio`
(`4b0344c8…2875`) and `int_codec/splat` (`f07679de…427f`), and every case's input and output
SHA-256 are in the JSON lines. The per-unit column divides the figure by the decode's output
and is derived, for reading across rows; it carries no declaration of its own. The
`wai.video.av1` row runs the dav1d library inside the process with its own threads, which is
why its spans scatter most (10.3 %).
## 3. The classical decoders: software against the media engine
The same 60 seconds of big-buck-bunny (from 120 s, 1440 frames of 1920×1080 at 24 fps), as
the ladder's own 10-bit AV1 rung (copied, 1.84 Mbit/s) and as 8-bit encodes made for this
run: AV1 by SVT-AV1 4.2.0 (preset 8, CRF 32, 1.60 Mbit/s), HEVC by x265 (medium, CRF 22,
1.85 Mbit/s), H.264 by x264 (medium, CRF 20, 3.06 Mbit/s). Each figure is one decode of the
whole clip.
| input | decoder | figure | `relative_ppm` | `window_us` | coverage | per frame | load |
|---|---|---:|---:|---:|---|---:|---|
| AV1, 10-bit | dav1d, 4 threads | 11.134 J | 74,835 | 3,594,433 | `Whole` | 7.73 mJ | 68–63 |
| AV1, 10-bit | VideoToolbox | ≥ 810.075 mJ | 39,457 | 2,308,343 | `Partial` | ≥ 0.56 mJ | 63–67 |
| AV1, 8-bit | dav1d, 4 threads | 7.711 J | 37,397 | 4,837,294 | `Whole` | 5.36 mJ | 67–88 |
| AV1, 8-bit | VideoToolbox | ≥ 858.526 mJ | 28,827 | 4,371,784 | `Partial` | ≥ 0.60 mJ | 88–83 |
| HEVC, 8-bit | FFmpeg `hevc`, 4 threads | 11.089 J | 33,061 | 6,616,973 | `Whole` | 7.70 mJ | 83–86 |
| HEVC, 8-bit | VideoToolbox | ≥ 846.948 mJ | 13,692 | 4,301,051 | `Partial` | ≥ 0.59 mJ | 86–86 |
| H.264, 8-bit | FFmpeg `h264`, 4 threads | 14.776 J | 81,887 | 1,903,541 | `Whole` | 10.26 mJ | 86–76 |
| H.264, 8-bit | VideoToolbox | ≥ 920.269 mJ | 40,912 | 4,418,829 | `Partial` | ≥ 0.64 mJ | 76–60 |
Every class is `OnChipCounter`, filtering `Unknown`. Of each VideoToolbox figure, 0.36 to
0.53 J per run is what the kernel billed to FFmpeg for the decoder service's work; every run
started one service, and no run accrued GPU time. A stream-copy run of FFmpeg over each input
(no decode) gives no figure: its spans last 56 to 66 ms, under §9.4's 300 ms, and its counter
rose by 71 to 189 mJ, which is FFmpeg's start, demux and exit rather than any decode.
Decoded-output digests, from separate runs that were not metered: dav1d's MD5 of the 10-bit
AV1 clip `59493f9c8a34d276e85b8636c31a26b9`, of the 8-bit `87f829462e087649d053309bbe233629`;
FFmpeg's MD5 of the HEVC clip `5cbeaebc59aeacb8efa2d99728eeb191`, of the H.264
`b821318fbc668637e2512823c5325641`. Input SHA-256: 10-bit AV1 `545599cd…8a22` (MP4) and
`37c45a33…f1fe` (IVF), 8-bit AV1 `a8a74487…e438` and `b7f59e1c…036f`, HEVC
`ca061d92…870d`, H.264 `718ceec1…9bcf`.
## 4. What is a lower bound, and why
- **Every VideoToolbox row.** The media engine has no counter an unprivileged process can
read on macOS 27 (energy-measurement §9.2): IOReport has no decoder channel. The figure
covers FFmpeg's CPU energy and what the kernel billed to it of the decoder service's, and
neither the media engine nor the part of the service's energy billed to no one. It is
sealed `Partial` and reads "at least". The rows say that the CPU side of a hardware decode
is between a ninth and a sixteenth of a software decode's whole energy on this sink; they do not say what
the hardware decode costs in all.
- **No WAI capability row.** Each ran on the CPU alone, which the task counter covers, so each
is `Whole`, as far as the class goes: an `OnChipCounter` declaration covers only what the
implementation could evaluate (the spans' scatter, the settle bound, the readings' and the
figure's resolution), never the counter's own systematic error (§2.3).
## 5. The budgets of the learned capabilities
The learned capabilities measured above have energy budgets
([energy-measurement](../../wai/extensions/energy-measurement.md) §9.5) in the form
[platform-decode](../../wai/extensions/platform-decode.md) §5 reads, each bound to the
parameter set its vector decodes with:
| capability | per | budget | `relative_ppm` | parameter set (`wai.prior.set` listing SHA-256) |
|---|---|---:|---:|---|
| `wai.audio.int_codec` | second | 13,986 µJ | 1,688 | `69c4d553…85d6` |
| `wai.neural.int_hyper` | picture | 121 µJ | 3,118 | `13e8b0d1…b818` |
| `wai.neural.int_mlicpp` | picture | 3,468 µJ | 10,252 | `85a05b0d…9d1b` |
| `wai.neural.int_synth` | picture | 21,101 µJ | 2,557 | `04073f4c…df1b` |
| `wai.splat.int_codec` | frame | 2,451 µJ | 3,186 | `75cc5f10…8657` |
| `wai.video.int_hyper` | frame | 54 µJ | 3,321 | `13e8b0d1…b818` |
The audio budget is the 0.512 s vector's figure per second, its rounding added to its
declaration. These budgets are of the conformance vectors' models: seeded random weights for
`int_hyper` and synthetic ones for `int_mlicpp`, trained ones for `int_synth`, `int_codec`
and the splat codec, all small. A production model of the same capability has its own weights
and no budget until it is measured: a budget applies only to an entry pinned to its set.
## 6. Registered capabilities not measured here
- **Learned, sink-supplied.** Every `wai.neural.*` capability of the `NeuralFloat` class
(bmshj2018, cmic, dac, dcvc_fm, dcvc_rt, elic, encodec32, glc, mbt2018_mean, mimi, mlicpp,
mlicv2, snac, video_bmshj2018, wavtokenizer), `wai.image.jpegai` (its exact decoder's
integer weight bundle is sink-supplied and not in the repository), and the behavioural
`wai.semantic.clip` and `wai.semantic.perco2`. With no budget, each ranks after classical
decode under platform-decode §4.
- **Standard codecs the reference sink does not decode itself**: AAC, AC-4, AVS3 (audio and
video), IAMF, IVAS, MPEG-H, Opus packets, JPEG 2000 (`wai.image.jp2`, `wai.video.j2k`),
Draco, glTF, G-PCC, V-PCC, SPZ, MIHS, AV2, EVC, VP8, VP9, VVC, and the companions
(the SMPTE ST 2094 dynamic metadata `wai.meta.dovi` and `wai.meta.hdr10plus`, LCEVC). AVC
and HEVC are measured above through FFmpeg and VideoToolbox.
- **WAI capabilities with a decoder in the reference sink and no case in this revision**:
`wai.audio.scene`, `wai.generate.spectral`, `wai.prior.nnc`, `wai.prior.set`,
`wai.quantum.circuit`, `wai.quantum.circuit2`, `wai.ternary.tt`, `wai.world.live`,
`wai.world.scene`, and the four refinements (`wai.audio.int_refine`,
`wai.image.int_refine`, `wai.splat.int_refine`, `wai.video.int_refine`).
- **Other sinks.** Only the build host was measured. The fleet's other Macs serve
production work and were not used. No Linux sink was measured (RAPL needs root there,
§9.2), and no Android or iOS device is in the fleet.
## 7. What these figures do not support
- **A property of the capability.** Each figure is one decode of one input with one build on
one machine at one load (energy-measurement §5). The software decoders' spans scatter by
up to 8 % at these loads, and the scatter is what the declaration states, not the counter's
systematic error, which nobody has stated.
- **A figure for the media engine, or for a hardware decode in all.** §4.
- **A ranking of codecs by energy.** The encodes differ in bitrate and quality, and the
in-process sources are synthetic; the tables compare decoders on one input, not codecs.
- **A display, a network or a phone figure.** None of these was measured; stream-energy
carries the display as an `Estimator` from a declared power, and the network as no figure
at all.
- **A rate a viewer would see.** The decodes ran as fast as the sink could go, not at
presentation rate, which changes how long the cores stay at a high clock.
## 8. Reproducing
On a Mac with the repository checked out, unprivileged:
```sh
cd wai/wai-rs
cargo build --release --no-default-features \
--features "sink_meter codecs video_int audio_int splat_int world_int haptic score splat4d film" \
--bin wai_sink_meter
target/release/wai_sink_meter run --spans 5 --seconds 1.5 --out inproc.jsonl
target/release/wai_sink_meter command --case h264/videotoolbox --capability wai.video.avc \
--decoder "VideoToolbox through FFmpeg" --fabrics cpu,media --units 1440 --unit frames \
--input h264.mp4 --spans 5 --out external.jsonl -- \
ffmpeg -nostdin -loglevel error -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld \
-i h264.mp4 -map 0:v -f null -
```
The build host's run kept its inputs, logs and the script that drove it in its own scratch
directory; the JSON lines here are its complete output.

View file

@ -6,6 +6,60 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### Energy a stream spends: measured where it can be, bounded where it cannot
- **Sink measurement profiles** ([energy-measurement](extensions/energy-measurement.md)
§9). Per operating system, which counters an unprivileged sink can read, the class each
figure is sealed under, and which fabrics none covers: `macos-task` (the kernel's per-task
CPU energy, `ri_energy_nj` with what it bills the task for work done on its behalf, and
the GPU rail under the sole-user rule; the media engine and the neural engine unmetered),
`linux-rapl-nvml` (RAPL readable only by root since Linux 5.10, NVML not), `android-app`
and `ios-app` (an `Estimator` from a declared basis, or `ModelBased`; Android 15's power
monitors refresh every 30 s with up to 10 J of deliberate noise, and iOS's task counter
is unmeasured). A figure over work on a fabric its profile does not meter is `Partial`,
a lower bound, and a reader shows it as one. The span protocol for a task counter
(§9.4) computes the figure and the least declaration from the spans' integers alone.
- **One figure per capability, measured** (`docs/research/wai-sink-energy.md`): WAI's
integer codecs, its world, film, score, haptic and splat classes and the standard codecs
the reference sink decodes itself, each decoded in the reference sink, and AV1, HEVC and
H.264 decoded in software and on the media engine; each figure `OnChipCounter` with its
`relative_ppm` and `window_us`, the media-engine ones as lower bounds, and the spans
published. `wai_sink_meter` runs
the protocol unprivileged.
- **Energy budgets** (§9.5): the learned capabilities measured have a budget in the form
platform-decode §5 reads, per picture, frame or second, bound to the parameter set it was
measured with (`codecs::energy_budget::budget_for`), so a figure of one model never
qualifies another.
- **`wai.stream.energy`** ([stream-energy](extensions/stream-energy.md)): each hop's signed
figure for a segment (`WSH1`) with its class, coverage and, for an estimate, the declared
power and time it is computed from; the display always an `Estimator` from the panel's
declared on-mode power; the sink's stream-segment receipt (`WSE1`) with each hop's share,
a total per class, a delivery sum and a display sum at their weakest class, and the panel
verdict; and a termination record (`WST1`) for a stream stopped because its page was
hidden, its player occluded, its display off, its autoplay not continued, or its viewer
idle, with the joules spent while the condition held. `HwShunt` is refused before any
other rule. Corpus `energy-conformance/stream/`, second implementation
`stream_energy_verify.py`.
- **Display capability** ([capability-constraints](extensions/capability-constraints.md)
§6): WAI-authored `wai-display:*` and `wai-video:*` parameters (sides, refresh, bit depth,
transfers, diagonal, viewing distance), the exact test of whether a rendition is above a
panel, the one-arc-minute acuity rule where a sink declares its distance, and selection
that sends no rendition above the panel when one within it is offered. Corpus
`constraints-conformance/display/`, second implementation `display_verify.py`.
- **Unmetered work is never zero joules.** `energy_class::Presented` is the one rendering
of a figure, and it writes the unmetered marker as "unmetered" and a `Partial` figure as
"at least". The world demo of `wai-web` labels its wall-time estimate `Estimator` and
renders it through `energy_text`.
- **The C kernel's grade is its meter's** (`oer-conformance/settle/`). The portable kernel
wrote `measured` for every receipt; it now writes the grade and device its meter states
and refuses `measured` unless the meter names an instrument. Its harness grades its
modelled meter `projected` (`firmware_vectors_modelled.json`, which `wai_oer_vectors`
verifies); the pinned `firmware_vectors.json` keeps its bytes as the receipts the kernel
issued, and [oer-receipts](extensions/oer-receipts.md) is unchanged.
- **The packer's GPU kernels in integers** (`wai-pack-gpu/tests/integer_only.rs`):
packaging §11.6's integer steps, held to conditions 1 and 2 of determinism-tiers §5 on
their source.
### GPUs: when `decode-equivalence` may be claimed, and WGSL's integer semantics on every adapter
- **Claiming the tier on a GPU** ([determinism-tiers](extensions/determinism-tiers.md)

View file

@ -250,6 +250,7 @@ and a role are fixed once registered.
| `wai.splat.int_refine` | refinement | registered | volumetric | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) |
| `wai.splat.sog` | payload | candidate | volumetric | standalone | patent-pool | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR |
| `wai.splat.spz` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Interactive, temporal & computed content |
| `wai.stream.energy` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.tensor.nnc` | payload | candidate | binary | standalone | patent-pool | standard-unclassified | — | §5 Prior artifacts, general tensors and derivation operations |
| `wai.ternary.tt` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content |
| `wai.text.timed` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
@ -341,9 +342,12 @@ and a role are fixed once registered.
| `WRM1` | magic | defined | live-recording §4.1 |
| `WRP1` | magic | defined | live-rooms §10.5.1 |
| `WSD1` | magic | defined | energy-steering §7.1 |
| `WSE1` | magic | defined | stream-energy §4.1 |
| `WSH1` | magic | defined | stream-energy §3.1 |
| `WSIX` | magic | defined | packaging §6 |
| `WSP1` | magic | defined | energy-steering §3.1 |
| `WSS1` | magic | defined | settlement §7.1 |
| `WST1` | magic | defined | stream-energy §5.1 |
| `WTM1` | magic | defined | capture-binding §7.1 |
| `WTX1` | magic | defined | timed-text §4.2 |
| `WUR1` | magic | defined | capture-binding §6.2 |

View file

@ -1005,6 +1005,18 @@ event timeline, and only a cell of at least fifty sessions reaches the receipt.
onto C2PA's `c2pa.environmental-sustainability` assertion where one class covers the session
whole: [`extensions/live-metering.md`](extensions/live-metering.md).
And for **what one segment of a stream cost at each hop** — `wai.stream.energy`: the
origin, each relay, the sink's decode and its display each sign a hop figure record,
`WSH1`, for the segment, with the figure's acquisition class, its coverage (`Partial`, a
lower bound, wherever a fabric no counter covers did part of the work) and, for an
estimate, the declared power and time it is computed from; a display figure is always an
`Estimator` from the panel's declared on-mode power. The sink signs a stream-segment
receipt, `WSE1`, with each hop's share, a total per class, a delivery sum and a display sum
each at its weakest class, and whether the rendition was above the panel; and a termination
record, `WST1`, when it stops a stream nobody is watching, with the reason and the joules
spent while the condition held. A verifier recomputes every number from the records:
[`extensions/stream-energy.md`](extensions/stream-energy.md).
And for the protocols standard encoders and players already speak, a binding onto a room's
tracks rather than a record: WHIP (RFC 9725) into them through a gateway that admits a
publisher by its room grant, carries each frame as one envelope on a MoQ track, places it on

View file

@ -0,0 +1,17 @@
# constraints-conformance
The conformance corpus of [display capability](../extensions/capability-constraints.md) §6:
a panel's `wai-display:*` parameters against renditions' `wai-video:*` parameters, the
respects in which each rendition is above the panel (size, rate, depth, transfer, acuity),
and the rendition §6.3 selects; and each declaration §6.2 refuses, with its code.
| file | what it is |
|---|---|
| `display/vectors.json` | written by `wai_display_vectors gen`: panels of common devices against a ladder from 2160p60 HDR to 360p, a panel offered only renditions above it, a frame rate above a panel's refresh rate, a ladder with nothing pickable, and the refusals. The panels and ladders are examples, not measurements |
| `display_verify.py` | the second implementation, written from §6 in Python's integers; it shares no code with `wai-rs` and requires every mutated copy of the corpus to fail |
```sh
cargo run --no-default-features --bin wai_display_vectors -- verify ../constraints-conformance/display
cargo test --no-default-features --test display_caps
python3 -I constraints-conformance/display_verify.py constraints-conformance/display
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Independent verifier of the display-capability corpus (extensions/capability-constraints.md §6).
Written from the extension's text. It shares no code with wai-rs, which generates the
corpus, and never runs it. For every vector it recomputes, in Python's integers, each
rendition's bits above the panel (§6.1: size, rate, depth, transfer, acuity) and the
selection of §6.3, and requires the ones the corpus states; for every refusal, the code
§6.2 gives the declaration. Then it mutates copies of the corpus, one defect each, and
requires every copy to fail.
python3 display_verify.py <constraints-conformance/display>
Exit status 0 only if every check holds and every mutated copy fails. Python 3.8 or later;
standard library only.
"""
import copy
import json
import os
import sys
# tan(1') x 10^13, rounded down (§6.1).
TAN_ARCMIN = 2908882086
BITS = ((1, "size"), (2, "rate"), (4, "depth"), (8, "transfer"), (16, "acuity"))
def above(d, r):
W, H = d["width"], d["height"]
w, h = r["width"], r["height"]
bits = 0
if max(w, h) > max(W, H) or min(w, h) > min(W, H):
bits |= 1
if r["frame_rate_mhz"] > d["refresh_mhz"]:
bits |= 2
if r["bit_depth"] > d["bit_depth"]:
bits |= 4
if r["transfer"] not in (1, 2, 3) or not (d["transfers"] >> (r["transfer"] - 1)) & 1:
bits |= 8
dist, diag = d["distance_mm"], d["diagonal_mm"]
if dist and diag:
s, lines = min(W, H), min(w, h)
# lines > S_mm / (D tan 1'), with S_mm = diag * s / sqrt(W^2 + H^2), squared.
if lines * lines * dist * dist * TAN_ARCMIN * TAN_ARCMIN * (W * W + H * H) > diag * diag * s * s * 10 ** 26:
bits |= 16
return [t for b, t in BITS if bits & b]
def select(d, renditions):
pickable = [(i, r) for i, r in enumerate(renditions) if r["pickable"]]
for i, r in pickable:
if not above(d, r["rendition"]):
return {"index": i, "within": True, "above": []}
if pickable:
i, r = pickable[0]
return {"index": i, "within": False, "above": above(d, r["rendition"])}
return None
def refusal(f):
if "display" in f:
d = f["display"]
if d["width"] == 0 or d["height"] == 0 or d["refresh_mhz"] == 0 or not 1 <= d["bit_depth"] <= 16:
return "display_malformed"
if d["transfers"] == 0 or d["transfers"] & ~0b111:
return "display_transfer"
if d["distance_mm"] and not d["diagonal_mm"]:
return "display_distance_alone"
return None
r = f["rendition"]
if r["width"] == 0 or r["height"] == 0 or r["frame_rate_mhz"] == 0 or not 1 <= r["bit_depth"] <= 16:
return "display_malformed"
if r["transfer"] not in (1, 2, 3):
return "display_transfer"
return None
def check(doc, quiet=False):
fails = []
if doc.get("tan_arcmin_e13") != TAN_ARCMIN:
fails.append("the corpus's tan(1') constant")
for v in doc["vectors"]:
for i, r in enumerate(v["renditions"]):
if above(v["display"], r["rendition"]) != r["above"]:
fails.append("%s: rendition %d's bits" % (v["name"], i))
if select(v["display"], v["renditions"]) != v["selected"]:
fails.append("%s: the selection" % v["name"])
for f in doc["refusals"]:
if refusal(f) != f["code"]:
fails.append("%s: the refusal" % f["name"])
if not quiet:
for f in fails:
print("FAIL " + f)
return fails
def mutations(doc):
out = []
def mutate(name, f):
d = copy.deepcopy(doc)
f(d)
out.append((name, d))
def vec(d, n):
return next(v for v in d["vectors"] if v["name"] == n)
mutate("a selection's index", lambda d: vec(d, "monitor_1440p_sdr")["selected"].update(index=0))
mutate("an acuity bit dropped", lambda d: vec(d, "tv_4k_hdr_far")["renditions"][0].update(above=["size"]))
mutate("a rendition made pickable", lambda d: vec(d, "nothing_pickable")["renditions"][0].update(pickable=True))
mutate("a panel's refresh", lambda d: vec(d, "rate_120_on_60")["display"].update(refresh_mhz=120000))
mutate("a refusal's code", lambda d: d["refusals"][0].update(code="display_transfer"))
mutate("the arc-minute constant", lambda d: d.update(tan_arcmin_e13=TAN_ARCMIN + 1))
return out
def main(corpus_dir):
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
fail = 0
fails = check(doc)
print("%s %d vectors and %d refusals verify from the text" % ("PASS" if not fails else "FAIL", len(doc["vectors"]), len(doc["refusals"])))
fail += bool(fails)
caught = 0
muts = mutations(doc)
for name, d in muts:
if check(d, quiet=True):
caught += 1
else:
print("FAIL the mutation '%s' goes unnoticed" % name)
fail += 1
print("%s %d of %d mutated copies of the corpus fail" % ("PASS" if caught == len(muts) else "FAIL", caught, len(muts)))
return 1 if fail else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: display_verify.py <constraints-conformance/display>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))

View file

@ -0,0 +1,23 @@
# energy-conformance
The conformance corpus of [stream energy](../extensions/stream-energy.md) (`wai.stream.energy`):
hop figure records (`WSH1`), stream-segment receipts (`WSE1`) and termination records
(`WST1`), each vector with the outcome extensions/stream-energy.md §6 gives it.
| file | what it is |
|---|---|
| `stream/vectors.json` | every vector: a receipt or a termination record with the receipts and hop records it names, as lowercase hex, and the outcome (accept with its facts, or refuse with the reason and the place). Written from fixed seeds by `wai_stream_energy_vectors gen`; the figures are synthetic and claim nothing about any device |
| `stream_energy_verify.py` | the corpus's second implementation, written from the extension's text; it shares no code with `wai-rs`, verifies Ed25519 with `../claim-conformance/_ed25519.py`, recomputes every share, total, sum and panel verdict, and requires every mutated copy of the corpus to fail |
```sh
# wai-rs: the file regenerates byte for byte, and every vector gets its outcome
cargo run --no-default-features --features stream_energy --bin wai_stream_energy_vectors -- verify ../energy-conformance/stream
cargo test --no-default-features --features stream_energy --test stream_energy
# the second implementation
python3 -I energy-conformance/stream_energy_verify.py energy-conformance/stream
```
The figures a real sink seals come from energy-measurement §9: the sink measurement
profiles, and the span protocol whose reference figures are in the repository's research
notes (`docs/research/wai-sink-energy.md`).

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,603 @@
#!/usr/bin/env python3
"""Independent verifier of the stream-energy corpus (extensions/stream-energy.md).
Written from the extension's text. It shares no code with wai-rs, which generates the
corpus, and never runs it. It checks Ed25519 signatures with
../claim-conformance/_ed25519.py, WAI's strict rule (SPEC §7 *Signatures*) written from
RFC 8032 with the standard library. Everything else is here, from the texts:
- the hop figure record (§3): its layout, the class of energy-measurement §2.7, the rules of
§3.6 in their order, the declared-power basis (§3.4), and the sink measurement profiles of
energy-measurement §9.2 that a decode figure's class and coverage are held to;
- the stream-segment receipt (§4) and its verification (§6.1): shares, totals, the
delivery and display sums at their weakest class, and the panel verdict, read with the
display-capability comparison of capability-constraints §6.1 in Python's integers;
- the termination record (§5) and its verification (§6.2).
For every vector it requires the outcome, the reason and the place the corpus states, and
for an accepted vector every fact; of the corpus, that every reason of §7 refuses a vector
and that §7 lists exactly those reasons. Then it mutates copies of the corpus, one defect
each, and requires every copy to fail.
python3 stream_energy_verify.py <energy-conformance/stream>
Exit status 0 only if every check holds and every mutated copy fails. Python 3.8 or later;
standard library only.
"""
import copy
import functools
import hashlib
import json
import os
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
import _ed25519 as ED # noqa: E402
REASONS = [
"legacy_class", "energy_invalid", "record_invalid", "basis_invalid", "basis_mismatch",
"profile_mismatch", "bound_overstated", "receipt_invalid", "classes_summed",
"record_missing", "wrong_segment", "unit_mismatch", "rendition_mismatch", "sum_mismatch",
"panel_mismatch", "termination_invalid", "wrong_stream", "index_mismatch",
]
# energy-measurement §2: strongest first, by tag.
RANK = {5: 4, 4: 3, 2: 2, 3: 1}
LABEL = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
CPU, GPU, MEDIA, NEURAL = 1, 2, 4, 8
# energy-measurement §9.2: each profile's counters, as (fabrics, class tag).
PROFILES = {
1: [(CPU, 4), (GPU, 4)], # macos-task
2: [(CPU | GPU | MEDIA, 4), (GPU | MEDIA, 4)], # linux-rapl-nvml
3: [], # android-app
4: [], # ios-app
}
TAN_ARCMIN = 2908882086 # tan(1') x 10^13, rounded down (capability-constraints §6.1)
class Refused(Exception):
def __init__(self, reason):
Exception.__init__(self, reason)
self.reason = reason
def sha256(b):
return hashlib.sha256(b).digest()
@functools.lru_cache(maxsize=None)
def sig_check(pk, msg, sig):
return ED.verify(pk, msg, sig)
class Rd:
def __init__(self, b, p=0):
self.b, self.p = b, p
def take(self, n):
if self.p + n > len(self.b):
raise Refused("truncated")
s = self.b[self.p:self.p + n]
self.p += n
return s
def u8(self):
return self.take(1)[0]
def u32(self):
return struct.unpack("<I", self.take(4))[0]
def u64(self):
return struct.unpack("<Q", self.take(8))[0]
def be32(self):
return struct.unpack(">I", self.take(4))[0]
def be64(self):
return struct.unpack(">Q", self.take(8))[0]
def head(b, magic):
if len(b) < 4:
raise Refused("truncated")
if b[:4] != magic:
raise Refused("malformed")
if len(b) < 5:
raise Refused("truncated")
if b[4] != 1:
raise Refused("unknown_version")
def read_class(r):
"""A class as energy-measurement §2.7 writes it: (tag, evidence) or a deferred
refusal, read past its bytes."""
tag = r.u8()
if tag == 0:
return (0, None), None
if tag == 1:
return (1, None), "legacy_class"
if tag in (2, 3):
return (tag, None), None
if tag == 4:
ppm, window, filt = r.be32(), r.be64(), r.u8()
bad = ppm == 0 or window == 0 or filt > 2
return (4, (ppm, window, filt)), "energy_invalid" if bad else None
if tag == 5:
ppm, window, n = r.be32(), r.be64(), r.be32()
if n > 255:
raise Refused("malformed")
ref = r.take(n)
try:
s = ref.decode("utf-8")
blank = s.strip() == ""
except UnicodeDecodeError:
blank = True
return (5, (ppm, window, ref)), "energy_invalid" if (ppm == 0 or window == 0 or blank) else None
return (tag, None), "energy_invalid"
def energy_rule(joules, cls, cov):
tag, ev = cls
if tag == 0:
return joules == 0 and cov == 0
if joules == 0 or cov not in (1, 2):
return False
if tag in (4, 5):
return joules * ev[0] >= 288676
return True
def decl_ok_display(d):
w, h, hz, depth, tfs, diag, dist = d
if w == 0 or h == 0 or hz == 0 or depth == 0 or depth > 16:
return False
if tfs == 0 or tfs & ~0b111:
return False
if dist != 0 and diag == 0:
return False
return True
def decl_ok_rendition(x):
w, h, rate, depth, tf = x
return w and h and rate and 1 <= depth <= 16 and tf in (1, 2, 3)
def above_bits(d, x):
"""capability-constraints §6.1: size 1, rate 2, depth 4, transfer 8, acuity 16."""
W, H, hz, depth, tfs, diag, dist = d
w, h, rate, rdepth, tf = x
bits = 0
if max(w, h) > max(W, H) or min(w, h) > min(W, H):
bits |= 1
if rate > hz:
bits |= 2
if rdepth > depth:
bits |= 4
if not (tfs >> (tf - 1)) & 1:
bits |= 8
if dist and diag:
s, lines = min(W, H), min(w, h)
if lines * lines * dist * dist * TAN_ARCMIN * TAN_ARCMIN * (W * W + H * H) > diag * diag * s * s * 10 ** 26:
bits |= 16
return bits
def above_terms(bits):
return [t for b, t in ((1, "size"), (2, "rate"), (4, "depth"), (8, "transfer"), (16, "acuity")) if bits & b]
def basis_joules(power_mw, time_us):
return (power_mw * time_us + 500) // 1000
def parse_record(b):
"""§3.1 read and §3.6's rules in their order, then the signature (§3.2)."""
head(b, b"WSH1")
r = Rd(b, 5)
rec = {"role": r.u8(), "hop": r.take(32), "segment": r.take(32), "index": r.u64(), "stream": r.take(32),
"deliveries": r.u32(), "joules": r.u64()}
rec["class"], fault = read_class(r)
rec["coverage"], rec["profile"], rec["fabrics"] = r.u8(), r.u8(), r.u8()
rec["rendition"] = (r.u32(), r.u32(), r.u32(), r.u8(), r.u8())
rec["duration_us"] = r.u64()
kind = r.u8()
if kind == 0:
rec["basis"] = None
elif kind == 1:
rec["basis"] = (r.u32(), r.u64(), r.u8())
else:
raise Refused("malformed")
rec["panel"] = None
if rec["role"] == 4:
pc = r.u8()
w, h, hz = r.u32(), r.u32(), r.u32()
depth, tfs = r.u8(), r.u8()
diag, dist, bright = r.u32(), r.u32(), r.u32()
rec["panel"] = (pc, (w, h, hz, depth, tfs, diag, dist), bright)
sig = r.take(64)
if r.p != len(b):
raise Refused("malformed")
if fault == "legacy_class":
raise Refused("legacy_class")
if fault or not energy_rule(rec["joules"], rec["class"], rec["coverage"]):
raise Refused("energy_invalid")
role, tag = rec["role"], rec["class"][0]
sink = role in (3, 4)
zero = rec["stream"] == bytes(32)
invalid = (
role not in (1, 2, 3, 4)
or zero == sink
or rec["deliveries"] == 0
or (sink and rec["deliveries"] != 1)
or rec["profile"] > 4
or (role != 3 and (rec["profile"] or rec["fabrics"]))
or (role == 3 and (rec["fabrics"] == 0 or rec["fabrics"] & ~0b1111))
or not decl_ok_rendition(rec["rendition"])
or (rec["panel"] is not None and (rec["panel"][0] not in range(1, 8) or not decl_ok_display(rec["panel"][1])))
or rec["duration_us"] == 0
or (rec["basis"] is not None and rec["basis"][2] not in (1, 2, 3))
)
if invalid:
raise Refused("record_invalid")
hardware = tag in (4, 5)
if ((tag == 3 and rec["basis"] is None) or (hardware and rec["basis"] is not None)
or (tag == 0 and rec["basis"] is not None) or (role == 4 and tag not in (0, 3))):
raise Refused("basis_invalid")
if rec["basis"] is not None and basis_joules(rec["basis"][0], rec["basis"][1]) != rec["joules"]:
raise Refused("basis_mismatch")
if role == 3 and hardware:
if tag == 5 and rec["profile"] != 0:
raise Refused("profile_mismatch")
if tag == 4:
counters = PROFILES.get(rec["profile"])
if not counters or not any(t == 4 for _, t in counters):
raise Refused("profile_mismatch")
metered = 0
for f, _ in counters:
metered |= f
if rec["coverage"] == 1 and rec["fabrics"] & ~metered:
raise Refused("bound_overstated")
v = sig_check(rec["hop"], b"wai:stream-hop\x01" + b[:-64], sig)
if v:
raise Refused(v)
rec["share"] = rec["joules"] // rec["deliveries"]
return rec
def parse_receipt(b):
"""§4.1 read and §4.2's rules, then the signature."""
head(b, b"WSE1")
r = Rd(b, 5)
rc = {"signer": r.take(32), "stream": r.take(32), "segment": r.take(32), "index": r.u64()}
n = r.u8()
hops, fault = [], None
for _ in range(n):
e = {"record": r.take(32), "role": r.u8(), "hop": r.take(32), "joules": r.u64(), "deliveries": r.u32(), "share": r.u64()}
e["class"], f = read_class(r)
e["coverage"] = r.u8()
if f and fault is None:
fault = f
hops.append(e)
rc["hops"] = hops
rc["unmetered"] = r.u8()
rc["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
rc["delivery"] = (r.u64(), r.u8(), r.u8())
rc["display"] = (r.u64(), r.u8(), r.u8())
rc["panel"] = (r.u8(), r.u8())
sig = r.take(64)
if r.p != len(b):
raise Refused("malformed")
if fault:
raise Refused(fault)
for e in hops:
if not energy_rule(e["joules"], e["class"], e["coverage"]):
raise Refused("energy_invalid")
if any(t[0] <= 1 for t in rc["totals"]):
raise Refused("classes_summed")
roles = [e["role"] for e in hops]
def sum_ok(s):
j, c, cov = s
return c in (0, 2, 3, 4, 5) and cov <= 2 and (c == 0) == (cov == 0) and (c != 0 or j == 0)
keys = [(e["role"], e["record"]) for e in hops]
invalid = (
not 1 <= n <= 16
or any(keys[i] >= keys[i + 1] for i in range(len(keys) - 1))
or any(e["role"] not in (1, 2, 3, 4) or e["deliveries"] == 0 or (e["role"] >= 3 and e["deliveries"] != 1) for e in hops)
or roles.count(1) > 1 or roles.count(3) > 1 or roles.count(4) > 1
or any(rc["totals"][i][0] >= rc["totals"][i + 1][0] for i in range(len(rc["totals"]) - 1))
or any(t[1] == 0 for t in rc["totals"])
or not sum_ok(rc["delivery"]) or not sum_ok(rc["display"])
or rc["panel"][0] > 2 or (rc["panel"][0] != 2 and rc["panel"][1] != 0) or rc["panel"][1] & ~0b11111
)
if invalid:
raise Refused("receipt_invalid")
v = sig_check(rc["signer"], b"wai:stream-segment\x01" + b[:-64], sig)
if v:
raise Refused(v)
rc["digest"] = sha256(b[:-64])
return rc
def totals(entries):
unmetered, by = 0, {}
for e in entries:
t = e["class"][0]
if t == 0:
unmetered += 1
else:
f, j = by.get(t, (0, 0))
by[t] = (f + 1, j + e["share"])
return unmetered, by
def sums(entries):
part = [e for e in entries if e["role"] <= 3]
metered = [e for e in part if e["class"][0] != 0]
if metered:
j = sum(e["share"] for e in metered)
c = min((e["class"][0] for e in metered), key=lambda t: RANK[t])
whole = (any(e["role"] == 1 for e in part) and any(e["role"] == 3 for e in part)
and all(e["class"][0] != 0 and e["coverage"] == 1 for e in part))
delivery = (j, c, 1 if whole else 2)
else:
delivery = (0, 0, 0)
disp = [e for e in entries if e["role"] == 4 and e["class"][0] != 0]
display = (disp[0]["share"], disp[0]["class"][0], disp[0]["coverage"]) if disp else (0, 0, 0)
return delivery, display
def verify_receipt(b, records):
"""§6.1: (facts) or raises (place, reason)."""
try:
rc = parse_receipt(b)
except Refused as e:
raise Refused(("receipt", e.reason))
read = []
for i, e in enumerate(rc["hops"]):
at = "hop/%d" % i
rb = records.get(e["record"])
if rb is None or sha256(rb[:-64]) != e["record"]:
raise Refused((at, "record_missing"))
try:
rec = parse_record(rb)
except Refused as x:
raise Refused((at, x.reason))
if rec["segment"] != rc["segment"] or rec["index"] != rc["index"] or (rec["role"] >= 3 and rec["stream"] != rc["stream"]):
raise Refused((at, "wrong_segment"))
same = all(rec[k] == e[k] for k in ("role", "hop", "joules", "deliveries", "class", "coverage", "share"))
if not same:
raise Refused((at, "unit_mismatch"))
read.append(rec)
if any(read[i]["rendition"] != read[i + 1]["rendition"] for i in range(len(read) - 1)):
raise Refused(("rendition", "rendition_mismatch"))
unmetered, by = totals(rc["hops"])
if unmetered != rc["unmetered"]:
raise Refused(("energy", "unit_mismatch"))
for tag, figures, joules in rc["totals"]:
if tag not in by:
raise Refused(("energy", "unit_mismatch"))
own = by[tag][1]
if joules != own:
others = [v[1] for k, v in by.items() if k != tag]
summed = any(own + sum(o for i, o in enumerate(others) if m >> i & 1) == joules for m in range(1, 1 << len(others)))
raise Refused(("energy", "classes_summed" if summed else "unit_mismatch"))
if figures != by[tag][0]:
raise Refused(("energy", "unit_mismatch"))
if len(rc["totals"]) != len(by):
raise Refused(("energy", "unit_mismatch"))
delivery, display = sums(rc["hops"])
if delivery != rc["delivery"] or display != rc["display"]:
raise Refused(("sums", "sum_mismatch"))
dec = [x for x in read if x["role"] == 3]
dsp = [x for x in read if x["role"] == 4]
if dec and dsp:
bits = above_bits(dsp[0]["panel"][1], dec[0]["rendition"])
panel = (1, 0) if bits == 0 else (2, bits)
else:
panel = (0, 0)
if panel != rc["panel"]:
raise Refused(("panel", "panel_mismatch"))
cov = ["none", "Whole", "Partial"]
verdict = ["none", "within", "above"]
def sumj(s):
return {"joules_micro": s[0], "class": LABEL.get(s[1], "unmetered"), "coverage": cov[s[2]]}
facts = {
"shares": [{"role": e["role"], "share": e["share"], "class": LABEL.get(e["class"][0], "unmetered")} for e in rc["hops"]],
"unmetered": unmetered,
"totals": [{"class": LABEL[t], "figures": by[t][0], "joules_micro": by[t][1]} for t in sorted(by)],
"delivery": sumj(delivery),
"display": sumj(display),
"panel": {"verdict": verdict[panel[0]], "above": above_terms(panel[1])},
}
return rc, facts
def read_spent(r):
out = []
for _ in range(4):
u = r.u32()
out.append((u, [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]))
return out
def spent_of(receipts):
unmetered, by = [0] * 4, [dict() for _ in range(4)]
for rc in receipts:
for e in rc["hops"]:
i, t = e["role"] - 1, e["class"][0]
if t == 0:
unmetered[i] += 1
else:
f, j = by[i].get(t, (0, 0))
by[i][t] = (f + 1, j + e["share"])
return [(unmetered[i], [(t, by[i][t][0], by[i][t][1]) for t in sorted(by[i])]) for i in range(4)]
WHY = {1: "hidden", 2: "occluded", 3: "display-off", 4: "autoplay-off", 5: "idle"}
def verify_termination(b, receipts, records):
try:
head(b, b"WST1")
r = Rd(b, 5)
t = {"sink": r.take(32), "stream": r.take(32), "reason": r.u8(), "at_us": r.u64(), "from": r.u64(), "last": r.u64(),
"n": r.u32(), "list": r.take(32)}
t["spent"], t["held"] = read_spent(r), read_spent(r)
sig = r.take(64)
if r.p != len(b):
raise Refused("malformed")
def classes_ok(s):
return all(all(2 <= c[0] <= 5 and c[1] > 0 for c in cl) and all(cl[i][0] < cl[i + 1][0] for i in range(len(cl) - 1)) for _, cl in s)
if t["reason"] not in WHY or t["from"] > t["last"] or t["n"] == 0 or not classes_ok(t["spent"]) or not classes_ok(t["held"]):
raise Refused("termination_invalid")
v = sig_check(t["sink"], b"wai:stream-termination\x01" + b[:-64], sig)
if v:
raise Refused(v)
except Refused as e:
raise Refused(("termination", e.reason))
if len(receipts) != t["n"] or sha256(b"".join(sha256(x[:-64]) for x in receipts)) != t["list"]:
raise Refused(("termination", "record_missing"))
acc = []
for i, rb in enumerate(receipts):
try:
rc, _ = verify_receipt(rb, records)
except Refused as e:
raise Refused(("segment/%d" % i, e.reason[1]))
if rc["stream"] != t["stream"] or rc["signer"] != t["sink"]:
raise Refused(("segment/%d" % i, "wrong_stream"))
if acc and rc["index"] <= acc[-1]["index"]:
raise Refused(("segment/%d" % i, "termination_invalid"))
acc.append(rc)
if acc[-1]["index"] != t["last"] or t["from"] < acc[0]["index"]:
raise Refused(("termination", "index_mismatch"))
if spent_of(acc) != t["spent"] or spent_of([x for x in acc if x["index"] >= t["from"]]) != t["held"]:
raise Refused(("termination", "unit_mismatch"))
def sj(s):
return [{"role": i + 1, "unmetered": u, "totals": [{"class": LABEL[c], "figures": f, "joules_micro": j} for c, f, j in cl]}
for i, (u, cl) in enumerate(s)]
return {"reason": WHY[t["reason"]], "from_index": t["from"], "last_index": t["last"], "spent": sj(t["spent"]), "held": sj(t["held"])}
def outcome(v):
records = {}
for h in v.get("records", []):
rb = bytes.fromhex(h)
records[sha256(rb[:-64])] = rb
try:
if v["kind"] == "receipt":
_, facts = verify_receipt(bytes.fromhex(v["receipt"]), records)
else:
facts = verify_termination(bytes.fromhex(v["termination"]), [bytes.fromhex(x) for x in v["receipts"]], records)
return ("accept", facts)
except Refused as e:
return ("refuse", e.reason)
def check(doc, quiet=False):
fails = []
seen = set()
for v in doc["vectors"]:
try:
got = outcome(v)
except Exception as e: # a corpus that cannot be read is a failure, not a crash
got = ("error", repr(e))
if v["outcome"] == "accept":
ok = got == ("accept", v["facts"])
else:
ok = got == ("refuse", (v["place"], v["reason"]))
seen.add(v["reason"])
if not ok:
fails.append("%s: got %r" % (v["name"], got))
for r in REASONS:
if r not in seen:
fails.append("nothing in the corpus is refused %s" % r)
if not quiet:
for f in fails:
print("FAIL " + f)
return fails
def reasons_listed():
with open(os.path.join(HERE, "..", "extensions", "stream-energy.md"), encoding="utf-8") as f:
text = f.read()
at_ = text.index("\n## 7. Reasons")
end = text.find("\n## ", at_ + 1)
return [line.split("`")[1] for line in text[at_:end].splitlines() if line.startswith("| `")]
def flip(h, at_, bit=0x01):
b = bytearray(bytes.fromhex(h))
b[at_] ^= bit
return b.hex()
def mutations(doc):
out = []
def mutate(name, f):
d = copy.deepcopy(doc)
f(d)
out.append((name, d))
def vec(d, n):
return next(v for v in d["vectors"] if v["name"] == n)
mutate("an accepted receipt's signature", lambda d: vec(d, "four_hops_metered").update(receipt=flip(vec(d, "four_hops_metered")["receipt"], -1)))
mutate("an accepted delivery class", lambda d: vec(d, "four_hops_metered")["facts"]["delivery"].update({"class": "OnChipCounter"}))
mutate("a lower bound read as whole", lambda d: vec(d, "media_engine_lower_bound")["facts"]["delivery"].update(coverage="Whole"))
mutate("an unmetered relay read as zero", lambda d: vec(d, "relay_unmetered")["facts"].update(unmetered=0))
mutate("a share", lambda d: vec(d, "origin_shared")["facts"]["shares"][0].update(share=5000000))
mutate("a panel verdict", lambda d: vec(d, "above_panel")["facts"]["panel"].update(verdict="within"))
mutate("an acuity bit", lambda d: vec(d, "above_acuity")["facts"]["panel"].update(above=[]))
mutate("a held figure", lambda d: vec(d, "termination_hidden")["facts"]["held"][3]["totals"][0].update(joules_micro=1))
mutate("a termination reason", lambda d: vec(d, "termination_idle")["facts"].update(reason="hidden"))
mutate("a missing record", lambda d: vec(d, "four_hops_metered")["records"].pop())
mutate("a refusal's reason", lambda d: vec(d, "record_media_engine_whole").update(reason="profile_mismatch"))
mutate("a refusal's place", lambda d: vec(d, "record_legacy_class").update(place="receipt"))
mutate("a record's figure byte", lambda d: vec(d, "phone_estimator")["records"].__setitem__(2, flip(vec(d, "phone_estimator")["records"][2], 140)))
mutate("a dropped vector of its reason", lambda d: d["vectors"].remove(vec(d, "receipt_panel_stated_within")))
return out
def main(corpus_dir):
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
fail = 0
st = ED._self_test()
print("%s Ed25519 strict rule self-test" % ("PASS" if not st else "FAIL"))
fail += bool(st)
fails = check(doc)
print("%s %d vectors verify from the text" % ("PASS" if not fails else "FAIL", len(doc["vectors"])))
fail += bool(fails)
ok = reasons_listed() == REASONS
print("%s §7 lists exactly the reasons this verifier raises" % ("PASS" if ok else "FAIL"))
fail += not ok
caught = 0
muts = mutations(doc)
for name, d in muts:
if check(d, quiet=True):
caught += 1
else:
print("FAIL the mutation '%s' goes unnoticed" % name)
fail += 1
print("%s %d of %d mutated copies of the corpus fail" % ("PASS" if caught == len(muts) else "FAIL", caught, len(muts)))
return 1 if fail else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: stream_energy_verify.py <energy-conformance/stream>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))

View file

@ -4,7 +4,11 @@
> check** rather than a binary claim, by ingesting the constraint-set grammar and
> register pattern the broadcast-IP world already ratified. No transport, no
> registry service, no negotiation protocol — it evaluates values WAI already
> carries.
> carries. §6 adds the first WAI-authored register entries, for the panel a sink
> presents on, so that a sink selects no rendition above its panel when one within it
> is offered. Reference implementation: `wai::display_caps`; corpus
> [`constraints-conformance/display/`](../constraints-conformance/display/), second
> implementation `constraints-conformance/display_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
## 1. The problem: support is not binary
@ -73,8 +77,9 @@ author itself**, and until it does, those capabilities remain unconstrained unde
mechanisms in the tree for one job.
- It is **not** a transport concern. Constraint evaluation reads values the
object already declares.
- It is **not** an energy mechanism. A constraint says what a sink can decode,
never what it should spend.
- It is **not** an energy mechanism. A constraint says what a sink can decode and
show, never what it should spend. §6 keeps a sink from receiving bits its panel cannot
show; the energy those bits cost is [stream-energy](stream-energy.md)'s to state.
## 5. Framing
@ -82,3 +87,106 @@ The grammar is thin — close to a JSON-Schema subset — and that is fine. The
is not expressiveness; it is that a ratified, register-backed way of saying
"supported, to here" already exists, and WAI's dispatch, which has only
"supported", gains a range by adopting it.
## 6. Display capability
A sink decodes for a panel, and a rendition the panel cannot show costs every hop its
bytes and the sink its decode for nothing it can present. A sink states its panel in the
register entries below, and a rendition states what it carries in the same terms; a sink
then selects no rendition above its panel when one within it is offered (§6.3).
### 6.1 The register entries
The parameters are WAI-authored, namespaced `wai-display:` for a panel and `wai-video:`
for a rendition (§3):
| parameter | meaning |
|---|---|
| `wai-display:width`, `wai-display:height` | the panel's pixels, in the orientation the sink presents in |
| `wai-display:refresh-mhz` | its highest refresh rate, in millihertz |
| `wai-display:bit-depth` | the bits per component it shows |
| `wai-display:transfers` | the transfer functions it shows: a set of SDR, PQ and HLG |
| `wai-display:diagonal-mm` | its diagonal in millimetres, where the sink declares it |
| `wai-display:distance-mm` | the viewer's distance from it in millimetres, where the sink declares it |
| `wai-video:width`, `wai-video:height` | a rendition's pixels |
| `wai-video:frame-rate-mhz` | its frame rate, in millihertz |
| `wai-video:bit-depth` | its bits per component |
| `wai-video:transfer` | its transfer function: SDR, PQ or HLG |
A rendition is **above** a panel in each of these respects, independently, and **within**
it when it is above it in none:
| respect | the rendition is above the panel when |
|---|---|
| `size` | its longer side is longer than the panel's longer side, or its shorter side longer than the panel's shorter side: the sides are compared in the orientation that fits, so a landscape rendition is within a portrait panel turned |
| `rate` | its frame rate is above the panel's refresh rate |
| `depth` | its bit depth is above the panel's |
| `transfer` | the panel does not show its transfer function |
| `acuity` | the sink declares both a diagonal and a distance, and the rendition's shorter side holds more lines than a viewer at that distance resolves across the panel's shorter side, at one arc-minute a line |
The acuity respect is the criterion behind ITU-R BT.1845-1's optimal viewing distance,
which that Recommendation defines as the distance at which two adjacent pixels of the
picture subtend one arc-minute at the viewer's eye; it puts a 1080-line picture's at 3.2
picture heights and a 2160-line picture's at 1.6. With the panel's sides `W`
and `H` in pixels, `S` the shorter, `G` its diagonal and `D` the distance in millimetres,
the panel's shorter side is `G·S/√(W² + H²)` millimetres for square pixels, and a viewer
at `D` resolves `S_mm / (D · tan 1′)` lines across it. A rendition whose shorter side has
`n` lines is above in acuity exactly when
n² · D² · T² · (W² + H²) > G² · S² · 10²⁶
with `T = 2 908 882 086`, tan 1′ × 10¹³ rounded down, so that the rule never calls a
rendition above that the exact value would not. Every term is an integer and the
comparison is exact. One arc-minute is the acuity the criterion assumes, not a limit of
every viewer's eye, which is why the respect applies only where the sink declares the
distance: a sink that does not declare one constrains nothing by acuity.
### 6.2 Declarations
A panel declaration is refused `display_malformed` when a side, the refresh rate or the
bit depth is 0 or the bit depth is above 16; `display_transfer` when it shows no transfer
function or names one other than SDR, PQ and HLG; and `display_distance_alone` when it
declares a distance without a diagonal. A rendition declaration is refused
`display_malformed` when a side, the frame rate or the bit depth is 0 or the bit depth is
above 16, and `display_transfer` when its transfer function is none of the three. A sink
**MUST NOT** evaluate §6.3 over a refused declaration.
### 6.3 Selection
Among the renditions a sink can otherwise pick (SPEC §7.1 step 2), in the order its
deployer's policy gives them, a sink that has declared its panel **MUST** select the first
that is within the panel. Where none it can pick is within, it selects the first it can
pick and records that it is above the panel and in which respects; a sink never refuses to
present for this reason alone. A rendition that declares none of the `wai-video:`
parameters is within every panel, since nothing it declares exceeds one. The same rule
applies to the variants of an HTTP streaming ladder, whose playlists declare the same
properties (HLS `RESOLUTION`, `FRAME-RATE` and `VIDEO-RANGE`; DASH `@width`, `@height` and
`@frameRate`).
Selection by the panel is a restriction of the entries a sink considers, which SPEC §7.1
permits a deployer policy; this section makes it the sink's own rule wherever the sink
knows its panel. A stream-segment receipt states whether the rendition was within the panel
([stream-energy](stream-energy.md) §4.5).
### 6.4 What this does not do
- It does not read a panel. A sink declares its panel from what its platform reports (the
screen's pixels, refresh rate and colour capabilities, and the size where it is known);
nothing here verifies a declaration.
- It does not choose between renditions within the panel; the deployer's order does.
- It does not weigh energy. A rendition within the panel can still cost more than another
within it, and §6.3 still selects the first of them.
### 6.5 Conformance
[`constraints-conformance/display/`](../constraints-conformance/display/) holds the corpus,
`vectors.json`, written by `wai_display_vectors gen`: panels of common devices (televisions
near and far, monitors, a laptop, a phone with and without a declared distance, a tablet
that shows HLG and not PQ) against a ladder from 2160p60 HDR to 360p, with each rung's
respects above the panel and the selection; a panel offered only renditions above it; a
120 fps rendition on a 60 Hz panel; and a ladder with nothing pickable; and each refusal
of §6.2. `wai_display_vectors verify` and `tests/display_caps.rs` check that the file
regenerates byte for byte and that `wai-rs` gives every vector its outcome;
`constraints-conformance/display_verify.py`, written from this text and sharing no code
with `wai-rs`, recomputes every respect and selection and requires every mutated copy of
the corpus to fail. The panels and ladders are examples, not measurements.

View file

@ -6,8 +6,10 @@
> encoding (§2, §2.7–§2.8), an acquisition protocol with a liveness rule for the
> power reader (§3, §3.1), a quality rule (§4), and a reporting contract (§6)
> whose values are published in a signed report that names the receipt it
> describes and leaves that receipt's bytes unchanged. A receipt that carries
> none of these additions keeps exactly the bytes it had.
> describes and leaves that receipt's bytes unchanged. It fixes, per operating
> system, which counters a sink can read and when its figure is only a lower
> bound (§9). A receipt that carries none of these additions keeps exactly the
> bytes it had.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
## 1. Why an acquisition protocol
@ -520,6 +522,11 @@ Receipts sealed before this revision carry `HwShunt`. Its meaning is fixed as
- `HwShunt` carries no evidence fields. A receipt pairing `HwShunt` with a
declared uncertainty, a filtering state or a calibration reference is
ambiguous and **MUST** be rejected.
- No sink measurement profile (§9) names `HwShunt`, and no record defined after
this revision accepts it: a [stream-energy](stream-energy.md) record or receipt
that carries it is refused `legacy_class`, before any other rule. The declared
uncertainty of §2.3 and its resolution floor are what a hardware figure must
now carry, and `HwShunt` carries neither.
### 2.7 Signed encoding
@ -1058,7 +1065,9 @@ and its rail cannot be attributed, the span has no fabric record: the producer
meters it otherwise (§3's bracket, over the whole figure), or seals it
unmetered. So does a span that ran a step on a fabric with no live counter: on
the reference hosts (macOS 27 on arm64) the media engine and the neural engine
have none an unprivileged process can read.
have none an unprivileged process can read. Such a span's figure can still be
sealed without a fabric record, with coverage `Partial`, as a lower bound
(§9.3).
The record says where the work ran and what each fabric drew; it changes
nothing a figure's verifier checks of the figure itself (§6.2). Which fabric
@ -1127,3 +1136,210 @@ prevent. A measurement report (§6) publishes, signed, the build profile, the
toolchain and target, the power reader and the bracket with its liveness facts,
in cleartext: the measuring machine's power levels and build environment are
visible to whoever holds the report.
## 9. Sink measurement profiles
The figure a sink spends decoding is the one a stream's receipt can least do without
([stream-energy](stream-energy.md) §3) and the hardest to obtain: a browser gives a page
no energy reading, and each operating system gives the process that decodes a different
counter, or none. A **sink measurement profile** fixes, for one operating system, which
counters an unprivileged sink process can read, the class a figure from each is sealed
under, the fabrics (§6.3) each covers, what reading it takes, and which fabrics none
covers. A sink decode figure names its profile by code (stream-energy §3.1).
### 9.1 What a profile fixes
- A figure under a hardware class is read from a counter of its profile, under that
counter's rule: the bracket of §3 for a rail other work shares, the sole-user rule of
§6.3 for a GPU rail, and the span protocol of §9.4 for a counter scoped to the work's
own process.
- `ModelBased` and `Estimator` figures fit every profile. An estimate states the declared
basis it is computed from (stream-energy §3.4), never a bare constant.
- A `CalibratedInstrument` figure is read from an instrument outside the device, which no
operating system profiles; it names no profile.
- No profile names `HwShunt` (§2.6).
### 9.2 The profiles
| code | profile | counters a sink reads, and their class | not metered |
|---|---|---|---|
| 1 | `macos-task` | CPU: the task counter, `OnChipCounter`, attribution `task`, no privilege. GPU: IOReport's `GPU Energy` rail, `OnChipCounter`, under §6.3's sole-user rule, no privilege | the media engine, the neural engine |
| 2 | `linux-rapl-nvml` | CPU, with an integrated GPU and media engine in the package: RAPL, `OnChipCounter`, a rail, **readable only by root since Linux 5.10**. GPU board, with its video engines: NVML, `OnChipCounter`, a rail, no privilege documented | a neural engine; a fabric no counter's device holds |
| 3 | `android-app` | none per segment; `Estimator` with a declared basis, or `ModelBased` | every fabric, per segment |
| 4 | `ios-app` | none measured; `Estimator` with a declared basis, or `ModelBased` | every fabric |
**`macos-task`** (macOS on Apple silicon; the reference hosts run macOS 27.0.1):
- The **task counter** is `proc_pid_rusage(RUSAGE_INFO_V6).ri_energy_nj`, the kernel's
account of a task's CPU energy (equal to `task_info(TASK_POWER_INFO_V2).task_energy`).
xnu accumulates it per thread from the CPU's own energy counters as the power manager
samples them: "CPU energy in nanojoules, when available", "sampled out-of-band by
ApplePMGR through CLPC" (`osfmk/kern/recount.h`), compiled where the CPU has the
digital power estimator counters it reads (`HAS_CPU_DPE_COUNTER`,
`osfmk/kern/recount.c`). It is the measured silicon's report about itself, as IOReport's
is (§2.9), attributed by the kernel to the task, so other work on the host does not
enter it: `OnChipCounter`, filtering `Unknown` (no unprivileged reader can see whether
the counter filters its output, §2.4). Beside it the kernel keeps `ri_billed_energy`,
the energy it bills to the task for work other processes did on its behalf (the task's
`energy_billed_to_me` ledger, in nanojoules, `osfmk/kern/task.c`): VideoToolbox decodes
in a service of its own (`VTDecoderXPCService`), and the kernel bills that service's work
for a client to the client. A process reads both for itself, and for a child before it
reaps it.
- The **GPU** is read as §6.3 reads it: IOReport's `GPU Energy` rail, attributed only
where the work held at least 99 % of every client's GPU time, which the IORegistry's
per-client `accumulatedGPUTime` shows.
- **Not metered.** IOReport's "Energy Model" channels for the CPU and the neural engine
read zero or stop updating on macOS 27 without a private entitlement, and there is no
channel for the video decoder at all; `powermetrics` refuses to run without root. So the
media engine and the neural engine are unmetered, and the reference meters of §3, which
read the CPU through IOReport, seal unmetered on these hosts.
**`linux-rapl-nvml`**:
- **RAPL** (`/sys/class/powercap/intel-rapl*/energy_uj`) has been **readable only by
root since Linux 5.10** (December 2020, commit `949dd0104c49`, the response to
CVE-2020-8694, INTEL-SA-00389), and perf's RAPL events are system-wide and need
`CAP_PERFMON` or a `perf_event_paranoid` the distribution does not ship. An unprivileged
sink on Linux therefore has no CPU counter. Only the host's owner opens one (a
group-readable `energy_uj`, a capability granted to the sink), and what it opens is a
package rail that other work shares: a per-segment figure from it is §3's bracket
marginal, never `task`, and its filtering state is `Unknown` unless the reader can read
`IA32_MISC_PACKAGE_CTLS` (§2.4), which takes root.
- **NVML** (`nvmlDeviceGetTotalEnergyConsumption`, millijoules since the driver loaded,
Volta and later) reads the whole board, its video decode engines included, and is not
documented to need root; a figure from it follows §6.3's sole-user rule, the driver's
per-process accounting naming the board's users. amdgpu's `power1_average` (a power
reader, §3.1) and an Intel discrete GPU's `energy1_input` are world-readable.
**`android-app`**:
- The Power Stats HAL (`android.hardware.power.stats`) serves the system, not apps. Since
Android 15 (API level 35) an app can read its readings through
`SystemHealthManager.getPowerMonitorReadings`: on-device power-rail monitors (ODPM,
`POWER_MONITOR_TYPE_MEASUREMENT`) and energy consumers that "may be a direct
pass-through power rail measurement, or modeled" (`POWER_MONITOR_TYPE_CONSUMER`), in
microwatt-seconds since boot (`PowerMonitorReadings.getConsumedEnergy`). Three things
keep a per-segment figure from them: the service refreshes the readings at most every
30 s (`MAX_POWER_MONITOR_AGE_MILLIS = 30_000`), it returns each with deliberate noise of
up to 10 J (`MAX_RANDOM_NOISE_UWS = 10_000_000`, `PowerStatsService`), and rail names
are device-specific ("No assumptions should be made about the names and exact purpose
of ODPM rails across different device models", `PowerMonitor`).
- So in this revision an Android sink seals a decode figure as `Estimator` from a declared
basis, its decode's CPU time at a declared power per busy core (stream-energy §3.4), or
as `ModelBased` where it calibrated that power on the device. A rail figure over a span
of 30 s or more is the route to a hardware class, `OnChipCounter` with filtering `On`
(the noise is deliberate, §2.4) and a declared uncertainty that includes the noise; this
profile admits none until a reference sink maps its rails to fabrics.
**`ios-app`**:
- No public energy interface is documented for apps. The iOS SDK declares `task_energy`
in `task_info(TASK_POWER_INFO_V2)` on arm64 (`mach/task_info.h`, iPhoneOS 27.1 SDK), the
account macOS reports as `ri_energy_nj`; whether it advances on iOS has not been
measured, since no iOS device is a reference sink. Until it is, an iOS sink seals
`Estimator` from a declared basis, or `ModelBased`.
### 9.3 A lower bound
- A figure over work that ran in part on a fabric its profile's counters do not meter
covers only the metered fabrics. It **MUST** be sealed with coverage `Partial`
([energy-binding](energy-binding.md) §4), and it is a **lower bound** on the work's
energy.
- A reader **MUST** present a lower bound as one ("at least"), **MUST NOT** present it as
the work's energy, and **MUST NOT** compare it with a `Whole` figure as though the two
covered the same work.
- On macOS 27 a hardware decode through VideoToolbox is such a figure: the task counter
covers the client's CPU energy and what the kernel bills to it of the decoder service's,
and no counter covers the media engine. So is a decode that runs a network on the neural
engine.
- A figure sealed `Whole` over work on an unmetered fabric overstates its bound and is
refused (stream-energy §3.6, `bound_overstated`). Work no counter covered at all has no
hardware figure: it is unmetered, or an estimate from a declared basis.
### 9.4 The span protocol for a task counter
A task counter is scoped to the work's process, so no idle bracket is needed: other work
on the host does not enter it, and changes it only through contention, as the cores the
scheduler places the work on and their clocks. A figure from it is read over spans of
whole completions:
1. A span opens with a read of the counter and runs `k` completions on threads started
after that read. It closes after those threads have finished and a **settle wait** of
at least 200 ms has passed. A span over a child process closes after the child has
exited and before it is reaped. The counter read is the task's own energy and the
energy the kernel billed to it (`ri_energy_nj + ri_billed_energy`). A service's energy
is counted only as the kernel bills it: a service that started during the span may
serve another client on a shared host, so its whole energy is never attributed to the
work, and the part of it the kernel bills to no one is not counted at all.
2. The counter is read during the span, at most 50 ms apart, and each span is live under
§3.1: at least five reads advanced, four of every five, two in a row, over at least
300 ms.
3. 100 ms after the closing read the counter is read again. The largest increase any span
showed over that wait, `d`, bounds what a closing read can have missed (a credit that
arrives late) or added (the reader's own wakes), and is declared as a systematic term,
which more spans do not reduce.
4. There are `R` spans, at least 5 and at most 64, of the same `k`; each span's increase
`eᵢ` is in nanojoules, non-zero, and at most 2⁵³ − 1.
With `S1 = Σ eᵢ` and `S2 = Σ eᵢ²`, the figure is the mean per completion rounded to the
nearest microjoule, a half rounding up, and the least declaration it supports is the least
integer `p` for which `p²·K ≥ P`:
```
F = ⌊(2·S1 + 1000·R·k) / (2000·R·k)⌋ µJ
K = 6·R²·(R−1)·k²·F²
P = 10⁶·( 6·(R·S2 − S1²) + 2·d²·R²·(R−1) + R·(R−1) + 500 000·R²·(R−1)·k² )
```
The four terms of `P` are, carried in nanojoules over the per-completion figure, the
standard uncertainty of the spans' mean (`s² / R`, with `s` the spans' sample standard
deviation), the settle bound taken as rectangular over ±`d` (`d²/3`), each span's two
nanojoule readings (`1/√6` nJ, §2.3), and the rounding of the figure to a microjoule
(`0.5/√3` µJ). The least `p` always meets the resolution floor of §2.3. The figure is
sealed `OnChipCounter` with filtering `Unknown`, `relative_ppm = p` and `window_us` the
shortest span's duration, which is the window the spans' scatter describes. A set of spans
that breaks rule 2 or 4, or whose figure rounds to zero, or whose `p` exceeds 2³² − 1,
gives no figure under a hardware class. A `measurement` claim about such a figure carries
a `null` bracket and names the counter as its `power_reader`
([jwp-receipts](jwp-receipts.md) §2.4.8); this revision has no member for the spans, so a
producer publishes them beside the figure, as the reference figures of §9.5 are.
Measured on the build host (macOS 27.0.1, M3 Ultra, 2026-10-22, one-minute load average
50 to 110 from other work): the task counter advanced in each of 100 successive reads
2 ms, 10 ms and 50 ms apart while one thread ran; after the thread was joined, the process
counter rose by a further 75 to 227 µJ over the next second, most of it within the first
200 ms, while its only thread slept between reads. The reference implementation is
`wai::sink_energy` (the arithmetic, every reader's) and its macOS reader
`wai::sink_energy::macos` (feature `sink_meter`); `wai_sink_meter` meters a decode under
this protocol.
### 9.5 Energy budgets of learned capabilities
[platform-decode](platform-decode.md) §5 reads a learned entry's **energy budget** to let it
rank with power-efficient classical decode, and leaves how a budget is carried and checked
to this extension. A budget is:
- an `OnChipCounter` figure, coverage `Whole`, of the decode of a learned capability
(platform-decode §4.1) with **one parameter set**, on a reference sink, obtained under
§9.4, expressed per unit of output: `picture`, `frame` or `second`. The per-unit figure is
the per-decode figure divided by the units one decode outputs, rounded to the nearest
microjoule, a half rounding up; where the division is not whole, its declared
uncertainty adds that rounding's `0.5/√3` µJ in quadrature, so that
`12·F²·p'² ≥ 12·F²·p² + 10¹²` holds for the per-unit `F` and `p'`, with `p` the
per-decode declaration. Its `relative_ppm` and `window_us` are those, and its
`reference_sink` names the sink, the profile and the date;
- **bound to its parameter set.** A network's draw depends on its weights' shapes and on
the stream as much as on its capability: a figure measured with one parameter set says
nothing about another. A budget names its set as a pin does (SPEC §3.1): the SHA-256 of
the set's `wai.prior.set` listing and, for a set of one file, that file's SHA-256. A
sink applies a budget only to an entry whose pin names that set; an entry with no pin
names no set and has no budget;
- **carried in the registry and checked against its spans.** The budgets of this revision
are `wai::codecs::energy_budget` (`budget_for` gives one in the form platform-decode §5
reads), and the spans each is computed from are published in the repository's research
notes ([wai-sink-energy](../../docs/research/wai-sink-energy.md)); a reader recomputes the
figure from the spans by §9.4, and the set's digests from the set.
A budget measured on a vector's parameter set states what that set costs on that sink at
the load the notes record. It is a figure under a class, not a ceiling: platform-decode §5
lets a deployer set a ceiling, and the order reads the budget only to rank.

View file

@ -0,0 +1,478 @@
# WAI Extension: Stream Energy
> Status: Draft. Registers the record `wai.stream.energy`: what one segment of a stream
> cost at each hop that carried it, from the origin and the relays to the sink's decode
> and its display. Each hop signs a **hop figure record**, `WSH1`, for the segment: its
> figure in microjoules with the acquisition class of
> [energy-measurement](energy-measurement.md) §2, its coverage, and, for an estimate, the
> declared basis the figure is computed from. A display figure is always an `Estimator`,
> computed from the panel's declared on-mode power and the time the segment was presented.
> The sink signs a **stream-segment receipt**, `WSE1`, naming the records: each hop's
> figure and the share of it this stream carries, a total per class and never one across
> classes, and two sums a reader may show, **delivery** (origin, relays, decode) and
> **display**, each labelled with the weakest class among its parts and marked as a lower
> bound wherever a part is one or is unmetered. The receipt also states whether the
> rendition was above the panel ([capability-constraints](capability-constraints.md) §6).
> The sink signs a **termination record**, `WST1`, when it stops a stream nobody is
> watching (the page hidden, the player occluded, the display off, an autoplayed stream
> the viewer's setting does not continue, or no activity), with the reason and the joules
> spent, in all and while the condition held. A verifier recomputes every number from the
> records. No new transport. The reference implementation is the `stream_energy` module
> of `wai-rs` (feature `stream_energy`); the corpus is
> [`energy-conformance/stream/`](../energy-conformance/stream/), and its second
> implementation is `energy-conformance/stream_energy_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
## 1. Scope
In scope: the hop figure record and its rules (§3), the declared-power basis that every
estimate in these records rests on (§3.4), the stream-segment receipt (§4), the
termination record (§5), verification (§6), reasons (§7) and conformance (§8).
Out of scope:
- **How a hop measures.** [energy-measurement](energy-measurement.md) governs that; its
§9 fixes which counters a sink can read on each operating system and when a sink's
figure is a lower bound.
- **The network between the hops.** The access network, the core and a radio link carry
the segment between the hops, and no party on the path can meter them per stream. A
receipt states no figure for them, and its delivery sum is over the hops that state
one. A reader **MUST NOT** present a delivery sum as the energy of delivering the
segment end to end.
- **Who may sign for a hop.** A verifier here checks each record's signature and the
arithmetic, and reports the keys. Where a hop's key is in an origin's key log, that
log's `energy/` authorities say which classes it states (jwp-receipts §7.5); this
revision does not check them for these records.
Relationship to the core spec: `wai.stream.energy` is a **record** ([SPEC](../SPEC.md)
§5): a signed record a WAI tool emits and verifies. It names no payload format and is
not dispatchable.
## 2. Terms
- **Segment**: one object of a stream's track, named by its content hash
([jwp-receipts](jwp-receipts.md) §2.1) and its **index**, the segment's place in its
track as the hop counts it.
- **Stream**: one presentation of a title at one sink, named by a **stream id**: 32 bytes
the sink draws at random for the stream, derived from nothing that identifies a person,
a device or an account.
- **Hop** and **role**: a party that spent energy on the segment and signs a record of
it, named by its Ed25519 key (RFC 8032). Its role is 1 **origin** (it made or stored
the segment and served it), 2 **relay** (it forwarded the segment), 3 **sink decode**
(it decoded the segment for this stream) or 4 **display** (it presented the decoded
pictures for this stream). A sink signs its own decode and display records.
- **Coverage**: 1 `Whole` or 2 `Partial` (energy-binding §4). A `Partial` figure covers
only part of the hop's work, so it is a **lower bound** on it (energy-measurement §9.3).
- **Unmetered marker**: a zero figure with class tag 0 (energy-measurement §2), never a
figure of zero joules.
- **Class rank**: the order of energy-measurement §2, strongest first:
`CalibratedInstrument` (tag 5), `OnChipCounter` (4), `ModelBased` (2), `Estimator`
(3). The **weakest class** of a set of figures is the last of theirs in that order.
- *u64*, *u32*, *u8*: little-endian unsigned integers of that many bits; a class's
evidence is written as energy-measurement §2.7 writes it (big-endian). A **digest** is
SHA-256 (FIPS 180-4), 32 bytes.
## 3. The hop figure record
### 3.1 Layout
A hop signs one record for one segment, `WSH1`:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WSH1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `role` | 1 | 1 origin, 2 relay, 3 sink decode, 4 display |
| `hop` | 32 | the hop's key, which signs the record |
| `segment` | 32 | the segment's content hash |
| `index` | 8 | u64: the segment's index |
| `stream` | 32 | for roles 3 and 4, the stream id; for roles 1 and 2, 32 zero bytes |
| `deliveries` | 4 | u32: the deliveries of the segment the figure covers, at least 1 (§3.3); 1 for roles 3 and 4 |
| `joules` | 8 | u64: the hop's figure for the segment, in microjoules; 0 is the unmetered marker |
| `class` | 1 + … | its acquisition class: energy-measurement §2.7's tag byte, then that tag's evidence |
| `coverage` | 1 | 0 for the unmetered marker; otherwise 1 `Whole` or 2 `Partial` |
| `profile` | 1 | for role 3, the sink measurement profile the figure was read under (energy-measurement §9.2), or 0 for none; otherwise 0 |
| `fabrics` | 1 | for role 3, the fabrics the decode ran on: bit 1 CPU, 2 GPU, 4 media engine, 8 neural engine; otherwise 0 |
| `rendition` | 14 | the segment's rendition as the hop states it: `width` u32, `height` u32, `frame_rate_mhz` u32 (millihertz), `bit_depth` u8, `transfer` u8 (1 SDR, 2 PQ, 3 HLG); capability-constraints §6.1 |
| `duration_us` | 8 | u64: the segment's media duration |
| `basis` | 1 + … | 0 for none; or 1, then the declared-power basis (§3.4): `power_mw` u32, `time_us` u64, `source` u8 |
| `panel` | 27 | role 4 only: the panel as the sink declares it (§3.5); absent for every other role |
| `sig` | 64 | §3.2 |
### 3.2 Digest and signature
The **digest** of a record is SHA-256 over its bytes before `sig`. Its **signature** is
Ed25519 by `hop` over the 15 bytes `wai:stream-hop` (ASCII) and 0x01, followed by the
bytes the digest covers, verified strictly as [SPEC](../SPEC.md) §7 *Signatures*
requires: a key that fails step 1 or 2 is `bad_key`, and a signature that fails a later
step is `bad_signature`.
### 3.3 Deliveries and the share
An origin or a relay spends energy on a segment once for many streams: an origin that
packages it once and serves it to every viewer, a relay that forwards it to each of its
sessions. Its record states how many deliveries its figure covers, as the hop counted
them over the span it reports, and a stream's receipt carries the **share** ⌊`joules` /
`deliveries`⌋ (§4.3). A share is an allocation of a hop's figure, not a measurement of
this stream's use, and keeps the figure's class and coverage. A sink's own records cover
one stream, and their share is their figure.
### 3.4 The declared-power basis
An estimate in these records is never a bare number. A figure classed `Estimator` **MUST**
carry a basis, and a figure classed `ModelBased` **MAY**; a figure under a hardware class
**MUST NOT**. The one basis of this revision is a **declared power**: a power in
milliwatts the hop declares for the work, the time it applied over in microseconds, and
the declaration's `source`:
| `source` | the power is |
|---|---|
| 1 | the on-mode power of the panel's EU energy label (Commission Delegated Regulation (EU) 2019/2013), for the dynamic range presented |
| 2 | a manufacturer's declaration for the device at the setting it ran at |
| 3 | the hop's own declaration |
The figure is the energy the declaration gives, rounded to the nearest microjoule with a
half rounding up:
joules = ⌊(power_mw × time_us + 500) / 1000⌋
A milliwatt over a microsecond is a nanojoule, so the arithmetic is exact. A declared
power is a constant nobody calibrated on that device for that content, so its figure is
`Estimator` (energy-measurement §2), whatever the source; a hop that calibrated the power
on itself may seal `ModelBased` instead. The source says where the constant came from,
not how good it is.
On a sink whose profile meters no counter per segment (energy-measurement §9.2, the
`android-app` and `ios-app` profiles), the sink decode figure is such an estimate: the
decode's CPU time as `time_us` and a declared power per busy core as `power_mw`.
### 3.5 The display figure and the panel
A display record (role 4) states the panel the stream was presented on:
| field | size | meaning |
|---|---|---|
| `panel_class` | 1 | 1 television, 2 monitor, 3 laptop, 4 tablet, 5 phone, 6 headset, 7 other |
| `width`, `height` | 4 each | u32: the panel's pixels |
| `refresh_mhz` | 4 | u32: its highest refresh rate, in millihertz |
| `bit_depth` | 1 | the bits per component it shows |
| `transfers` | 1 | the transfer functions it shows: bit 1 SDR, 2 PQ, 4 HLG |
| `diagonal_mm` | 4 | u32: its diagonal in millimetres, 0 when not declared |
| `distance_mm` | 4 | u32: the viewer's distance from it in millimetres, 0 when not declared |
| `brightness_ppm` | 4 | u32: the brightness setting the declared power describes, as a fraction of the panel's maximum in parts per million; 0 when not known |
Its figure is an `Estimator` from the declared-power basis, with `power_mw` the panel's
declared on-mode power at that setting and for the dynamic range presented, and
`time_us` the time the segment's pictures were presented. Its coverage is `Whole`: it
estimates the panel's whole draw while it showed the segment, which the stream is the
reason for. A sink that holds no declared power for its panel seals the display record
unmetered, never with a typical value it has no declaration for.
The estimate does not scale a power by the brightness setting: a panel's power is not
proportional to its brightness, so a power is declared for the setting it describes, and
`brightness_ppm` says which setting that is. A label's on-mode power is the one measured
in the panel's normal configuration.
### 3.6 Reading and rules
A reader reads exactly the bytes it is given as one record. It reads `magic` (`malformed`
when it is not `WSH1`), `version` (`unknown_version` when it is not 1) and every field in
§3.1's order, a short record being `truncated` and bytes after `sig` `malformed`; the
class's evidence as energy-measurement §2.7 writes it, a tag 5 reference longer than 255
bytes being `malformed`; and a `basis` kind other than 0 and 1, whose length this revision
does not define, is `malformed` too. It then refuses, in this order:
1. as `legacy_class`, a class tag of 1 (`HwShunt`): no record of this extension carries
the legacy class (energy-measurement §2.6);
2. as `energy_invalid`, a record where the tag is 0 and `joules` is not, or `joules` is 0
and the tag is not; where `coverage` is 0 and the tag is not, or the tag is 0 and
`coverage` is not; a `coverage` above 2; a tag other than 0, 2, 3, 4 and 5; and, for
tags 4 and 5, a `relative_ppm` or `window_us` of 0, a figure below the resolution floor
(`joules × relative_ppm ≥ 288 676`, energy-measurement §2.3), tag 4's filtering byte
above 2, or tag 5's reference not UTF-8 or only white space;
3. as `record_invalid`, a record whose `role` is not 1 to 4; whose `stream` is zero for
role 3 or 4, or not zero for role 1 or 2; whose `deliveries` is 0, or not 1 for role 3
or 4; whose `profile` is above 4, or not 0 outside role 3; whose `fabrics` is 0 or has
a bit above 8 for role 3, or is not 0 outside it; whose `rendition` or `panel` breaks
capability-constraints §6.2, or whose `panel_class` is not 1 to 7; whose
`duration_us` is 0; or whose basis `source` is not 1 to 3;
4. as `basis_invalid`, a record whose class is `Estimator` and that carries no basis;
whose class is a hardware class and that carries one; that is unmetered and carries
one; or whose role is 4 and whose class is neither `Estimator` nor the unmetered
marker;
5. as `basis_mismatch`, a record that carries a basis and whose `joules` is not the
figure §3.4 gives;
6. for role 3 with a hardware class: as `profile_mismatch`, an `OnChipCounter` figure
whose profile is 0, or whose profile has no counter of its class, and a
`CalibratedInstrument` figure whose profile is not 0 (an external instrument is no
counter of the operating system); and as `bound_overstated`, a `Whole` figure whose
`fabrics` include one its profile does not meter (energy-measurement §9.3).
A record that passes is **read**.
## 4. The stream-segment receipt
### 4.1 Layout
The sink signs one receipt for each segment of its stream it presented, `WSE1`:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WSE1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `signer` | 32 | the sink's key, which signs the receipt |
| `stream` | 32 | the stream id |
| `segment` | 32 | the segment's content hash |
| `index` | 8 | u64: the segment's index |
| `hops` | 1 + … | a u8 count *n*, 1 to 16, then each hop entry: `record` (32, the record's digest), `role` (u8), `hop` (32), `joules` (u64), `deliveries` (u32), `share` (u64), `class` (1 + …) and `coverage` (u8); in strictly ascending order of (`role`, `record`) |
| `unmetered` | 1 | u8: the entries whose figure is the unmetered marker |
| `totals` | 1 + 13 × *m* | a u8 count *m*, then for each class among the metered entries: `tag` (u8), `figures` (u32) and `joules` (u64, the sum of their shares), in strictly ascending order of `tag` |
| `delivery` | 10 | the delivery sum (§4.4): `joules` (u64), `class` (u8, a tag as a class name) and `coverage` (u8) |
| `display` | 10 | the display sum (§4.4), the same three fields |
| `panel` | 2 | the panel verdict (§4.5): `verdict` (u8: 0 none, 1 within, 2 above) and `above` (u8, capability-constraints §6.1's bits) |
| `sig` | 64 | Ed25519 by `signer` over the 19 bytes `wai:stream-segment` (ASCII) and 0x01, followed by the bytes before `sig` |
The **digest** of a receipt is SHA-256 over its bytes before `sig`, verified strictly as
§3.2 verifies a record's.
### 4.2 Reading and rules
A reader reads the receipt as §3.6 reads a record, with `WSE1` for `WSH1`, and refuses as
§3.6's rules 1 and 2 refuse an entry's class and coverage (`legacy_class`,
`energy_invalid`), and as `classes_summed` a receipt whose `totals` names tag 0 or 1. It
refuses as `receipt_invalid` a receipt that breaks any of these:
- `hops` holds 1 to 16 entries, in strictly ascending order of (`role`, `record`), each
`role` 1 to 4, at most one of role 1, one of role 3 and one of role 4, any number of
relays;
- each entry's `deliveries` is at least 1, and 1 for roles 3 and 4;
- `totals` is in strictly ascending order of `tag`, each `figures` above 0;
- the `class` of `delivery` and of `display` is 0, 2, 3, 4 or 5, and 0 exactly when its
`coverage` is 0 and its `joules` is 0; each `coverage` is at most 2;
- the panel's `verdict` is at most 2, and `above` is 0 unless the verdict is 2, and has
no bit above 16.
### 4.3 Entries and shares
Each entry restates its record's `role`, `hop`, `joules`, `deliveries`, `class` and
`coverage`, and states the share the stream carries, ⌊`joules` / `deliveries`⌋ (§3.3).
The remainder of the division is attributed to no stream. A reader **MUST NOT** present
an unmetered entry as zero joules; its share is 0 only as the unmetered marker is.
### 4.4 Totals and sums
The **totals** are one per class: for each tag among the metered entries, the number of
entries of that tag and the sum of their shares. Nothing in `totals` sums two classes, and
a receipt whose total for a class is the sum of that class's shares and of at least one
other class's, while not the sum of its own, is refused `classes_summed`, as
live-metering §4.5 refuses one.
The two **sums** are what a reader may show as one number, each read as
energy-measurement §2.8 reads a total over parts of several classes:
- **delivery**: over the entries of roles 1 to 3. `joules` is the sum of the metered
entries' shares; `class` is the tag of the weakest class among them, as a class name
only, with no declared uncertainty; `coverage` is 1 `Whole` when there is an origin
entry and a sink decode entry and every entry of roles 1 to 3 is metered and `Whole`,
2 `Partial` when at least one is metered and the sum is not `Whole`, and 0 when none is
metered (then `joules` and `class` are 0 too);
- **display**: the display entry's share, class and coverage, or three zeros when there
is no display entry or it is unmetered.
A `Partial` sum is a lower bound and **MUST** be presented as one. A sum's class says how
strong its weakest part is, not that every part is of that class: a delivery sum of an
`OnChipCounter` decode and an `Estimator` origin is labelled `Estimator`, and reads no
better. The display sum is kept apart from the delivery sum, so that the panel's estimate,
the largest figure of a streamed hour on most screens, never lowers the class of the
delivery figures, and the delivery figures never lend the estimate a class it does not
have. A reader that shows the two together shows them as two figures, each with its
class.
### 4.5 The panel verdict
Where the receipt has both a sink decode entry and a display entry, the **panel verdict**
is capability-constraints §6.1's comparison of the decode record's `rendition` with the
display record's `panel` (its sides, refresh, bit depth, transfers, diagonal and
distance): 1 **within** when the rendition is within the panel in every respect, and
otherwise 2 **above**, with `above` holding the bits of each respect it is above in. With
either entry missing, the verdict is 0 and `above` is 0. A verdict of above says that the
segment carried bits the panel could not show, which is waste capability-constraints §6.3
exists to avoid; it does not say why the sink selected that rendition.
## 5. The termination record
### 5.1 Layout
A sink that stops a stream because nobody is watching it signs a termination record,
`WST1`:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WST1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `sink` | 32 | the sink's key, which signs the record and every receipt it names |
| `stream` | 32 | the stream id |
| `reason` | 1 | §5.2: 1 `hidden`, 2 `occluded`, 3 `display-off`, 4 `autoplay-off`, 5 `idle` |
| `at_us` | 8 | u64: microseconds from the presentation of the stream's first segment to the termination |
| `from_index` | 8 | u64: the first segment the sink presented while the condition held |
| `last_index` | 8 | u64: the last segment it presented |
| `receipts` | 4 + 32 | u32: the stream-segment receipts the record accounts for, at least 1; then their **list digest**, SHA-256 over their digests in the order of their `index`, concatenated |
| `spent` | 4 × (4 + 1 + 13 × *m*) | for each role 1 to 4, in that order: `unmetered` (u32, the receipts' entries of that role whose figure is the unmetered marker), then a u8 count *m* and, for each class among the role's metered entries, `tag` (u8), `figures` (u32) and `joules` (u64, the sum of their shares), in strictly ascending order of `tag` |
| `held` | as `spent` | the same over the receipts whose `index` is at least `from_index` |
| `sig` | 64 | Ed25519 by `sink` over the 23 bytes `wai:stream-termination` (ASCII) and 0x01, followed by the bytes before `sig` |
### 5.2 Reasons for terminating
| `reason` | the condition, as the sink observed it |
|---|---|
| 1 `hidden` | the page or the application presenting the stream was hidden: in a browser, the document's `visibilityState` was `"hidden"` (W3C Page Visibility); natively, the application was in the background |
| 2 `occluded` | the player was not visible although its page or application was: its element outside the viewport, or its window covered |
| 3 `display-off` | the display the stream was presented on was off or asleep |
| 4 `autoplay-off` | the stream began without the viewer's activation (an autoplayed preview or next item), and the viewer's setting does not continue autoplay |
| 5 `idle` | no activation by the viewer for the period the sink declares as idle, and no answer to the sink's prompt |
`from_index` is the first segment presented while the condition held, so the `held`
figures are the joules spent on segments nobody was watching: the waste the termination
ended. A sink **SHOULD** stop fetching segments when it terminates a stream, and a stream
it resumes is a new stream with a new id. A termination record says what the sink
observed and did; it is not evidence about a person.
### 5.3 Reading and rules
A reader reads the record as §3.6 reads a record, with `WST1` for `WSH1`, and refuses as
`termination_invalid` a record whose `reason` is not 1 to 5, whose `from_index` is above
its `last_index`, whose `receipts` is 0, or whose `spent` or `held` names a tag other than
2 to 5, or names tags out of strictly ascending order within a role, or a class with no
figures.
## 6. Verification
### 6.1 A stream-segment receipt
A verifier is given the receipt and the hop records it names. It runs these steps and
stops at the first refusal, which it reports with the reason and where:
1. **The receipt** (§4.1, §4.2), at `receipt`: `truncated`, `malformed`,
`unknown_version`, `legacy_class`, `energy_invalid`, `classes_summed`,
`receipt_invalid`; `bad_key`, `bad_signature`.
2. **Each hop entry**, in order, at `hop` and the entry's index:
1. a record whose digest is the entry's `record` is given: otherwise
`record_missing`. (The digest of given bytes is SHA-256 over all but their last 64.)
2. It reads (§3.6, each of that section's reasons) and its signature verifies
(`bad_key`, `bad_signature`).
3. Its `segment` and `index` are the receipt's, and for roles 3 and 4 its `stream` is
the receipt's: otherwise `wrong_segment`.
4. The entry's `role`, `hop`, `joules`, `deliveries`, `class` and `coverage` are the
record's, and its `share` is the record's figure over its deliveries (§4.3):
otherwise `unit_mismatch`.
3. **The rendition**, at `rendition`: every record states the same `rendition`, and the
display record's is the decode record's: otherwise `rendition_mismatch`.
4. **The totals** (§4.4), at `energy`: `unmetered` is the number of unmetered entries, and
each stated total names a class among the entries' and states that class's number of
figures and sum of shares, or else is `classes_summed` when the sum it states is one
§4.4 describes and `unit_mismatch` otherwise; and the totals name every class of the
metered entries. Otherwise `unit_mismatch`.
5. **The sums** (§4.4), at `sums`: `delivery` and `display` are the ones the entries
give: otherwise `sum_mismatch`.
6. **The panel** (§4.5), at `panel`: the verdict and its bits are the ones the decode and
display records give: otherwise `panel_mismatch`.
A receipt that passes is **accepted**: every entry, share, total and sum, and the panel
verdict, is what the records it names give.
### 6.2 A termination record
A verifier is given the record, the receipts it accounts for and the hop records they
name. In order:
1. **The record** (§5.1, §5.3), at `termination`: `truncated`, `malformed`,
`unknown_version`, `termination_invalid`; `bad_key`, `bad_signature`.
2. **The receipts**, at `termination`: as many are given as `receipts` states, and the
digest over their digests in the order of their `index` is the list digest: otherwise
`record_missing`.
3. **Each receipt**, in that order, at `segment` and its index: it verifies under §6.1,
with that section's reasons; its `stream` is the record's and its `signer` is the
record's `sink` (`wrong_stream`); and its `index` is above the previous receipt's
(`termination_invalid`).
4. **The span**, at `termination`: the last receipt's `index` is `last_index`, and
`from_index` is at least the first receipt's `index`: otherwise `index_mismatch`.
5. **The joules**, at `termination`: `spent` and `held` are the ones the receipts'
entries give, role by role and class by class: otherwise `unit_mismatch`.
A record that passes is **accepted**: the receipts it names are the stream's, by its sink,
up to the segment it ended on, and the joules it states, by role and class, are theirs.
## 7. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `legacy_class` | §3.6, §4.2 |
| `energy_invalid` | §3.6, §4.2 |
| `record_invalid` | §3.6 |
| `basis_invalid` | §3.6 |
| `basis_mismatch` | §3.6 |
| `profile_mismatch` | §3.6 |
| `bound_overstated` | §3.6 |
| `receipt_invalid` | §4.2 |
| `classes_summed` | §4.2, §6.1 step 4 |
| `record_missing` | §6.1 step 2, §6.2 step 2 |
| `wrong_segment` | §6.1 step 2 |
| `unit_mismatch` | §6.1 steps 2 and 4, §6.2 step 5 |
| `rendition_mismatch` | §6.1 step 3 |
| `sum_mismatch` | §6.1 step 5 |
| `panel_mismatch` | §6.1 step 6 |
| `termination_invalid` | §5.3, §6.2 step 3 |
| `wrong_stream` | §6.2 step 3 |
| `index_mismatch` | §6.2 step 4 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version`
for a record or a receipt that does not read, and `bad_key` and `bad_signature` for their
signatures.
## 8. Conformance
[`energy-conformance/stream/`](../energy-conformance/stream/) holds the corpus, in one
file, `vectors.json`, written from fixed seeds by `wai_stream_energy_vectors gen`. Its
figures are synthetic, chosen to exercise the rules, and claim nothing about any device.
It has **vectors**, each a receipt or a termination record with what it names and the
outcome §6 gives it:
- **Accepted**: a segment whose four hops are metered, the decode an `OnChipCounter`
figure under the `macos-task` profile, and whose delivery sum is `Whole` and labelled
with its weakest class; a hardware decode on the media engine, sealed `Partial` (a lower
bound), which makes the delivery sum `Partial`; a relay that signs with the unmetered
marker, counted in `unmetered` and in no total; an origin shared by many deliveries,
whose share is its figure over them; a phone sink whose decode figure is an `Estimator`
from its CPU time and a declared power; a display with no declared power, unmetered; a
rendition above the panel; and a termination record for each reason of §5.2, with its
joules held.
- **Refused**: each reason of §7 at least once, each vector a control of one defect, and
the codes a record or a receipt raises when it does not read or verify.
A conforming verifier gives every vector the outcome, the reason and the place the corpus
states. `wai_stream_energy_vectors verify` and the test `tests/stream_energy.rs` check
that the file regenerates byte for byte, that `wai-rs` gives every vector its outcome, that
every reason of §7 refuses a vector, and that §7 lists exactly the reference's codes.
`energy-conformance/stream_energy_verify.py` is the second implementation, written from
this text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule
written from RFC 8032 (`claim-conformance/_ed25519.py`), recomputes every share, total,
sum and verdict, gives every vector the same outcome, and requires every mutated copy of
the corpus to fail.
## 9. Security and privacy considerations
- Every figure rests on its hop's signature, and its class says how it was obtained
(energy-measurement §5): the receipt makes the figures attributable and their sums
recomputable, not true. A share is an allocation by deliveries and says so.
- An estimate states its basis, so a reader can see the constant it rests on and who
declared it; a reader **MUST NOT** present a display figure as a measurement.
- A display record names a panel class, a size and a viewing distance, which describe a
device. A sink holds its records and receipts and shares them only with whom it chooses;
a stream id is fresh per stream, so two streams of one sink do not link through it. A
sink **MAY** declare a diagonal and a distance rounded to the nearest 10 mm, which the
acuity rule reads as declared.
- A termination record states when the sink observed the condition, not why. A
deployment that collects termination records **MUST NOT** use them to infer anything
about a viewer beyond the stream they describe.

View file

@ -14,7 +14,8 @@ the reference verifier here, the portable C kernel in [`settle/`](settle/) that
| file | vectors | what it is |
|---|---|---|
| `vectors.json` | 9 | OER/2 (draft 0.1, as the extension profiles it): the last 150 decision receipts of one simulated day of Settlement World's gatewayless corridor, signed with the Ed25519 ledger key published in the file, an hourly bundle with its provenance root, an Iridium anchor, and seven tampered twins |
| `firmware_vectors.json` | 1 | 400 receipts written by the C kernel with its own SHA-256 and Ed25519 — pinned, and regenerated byte-for-byte in CI |
| `firmware_vectors.json` | 1 | 400 receipts written by the C kernel with its own SHA-256 and Ed25519 — pinned, and regenerated byte-for-byte in CI. Their meter is a model, yet each names the method `hardware_meter` and the grade `measured`, which [oer-receipts](../extensions/oer-receipts.md) §2 does not support for a modelled figure; they are kept byte for byte as the receipts the kernel issued (below) |
| `firmware_vectors_modelled.json` | 1 | the same 400 decisions with the meter graded as what it is: method `model_estimated`, grade `projected` (its weakest input's) |
| `edge_vectors.json` | 99 | one case for each behaviour the upstream nine never exercise — see below |
| `unreadable/` | 11 documents | one for each way the extension refuses a document whole — a number whose nearest binary64 is infinite (the midpoint above the largest, as a float and as an integer, and far past it), `NaN`, `Infinity`, `-Infinity`, an unpaired surrogate, a repeated member name (plain, and a second `joules_total` inside a sealed receipt), nesting 65 deep, and nesting 5000 deep |
| `sw_export.json` | — | the Settlement World export `generate_vectors.py` builds `vectors.json` from |
@ -104,8 +105,23 @@ python3 oer2_verify.py --unreadable unreadable # every document there must be
# the C kernel: rebuild and regenerate its vectors; the output is deterministic
make -C settle && ./settle/settle_host /tmp/fw.json 400 && cmp /tmp/fw.json firmware_vectors.json
./settle/settle_host /tmp/fwm.json 400 modelled && cmp /tmp/fwm.json firmware_vectors_modelled.json
python3 oer2_verify.py firmware_vectors_modelled.json
```
**The kernel's grade is its meter's.** The kernel used to write `"how":"measured"` and
`"device":"hardware"` into every receipt, whatever its meter was, and the host harness's
meter is a model: a cycle count times the kernel's projected cycle energy, and constant
idle and shared powers nobody measured on the host. Those receipts graded a modelled
figure `measured`. The kernel now writes the grade and the device its meter states, and
refuses to grade a figure `measured` unless the meter names an instrument
(`hardware_meter`), so a meter that models its figures cannot be signed as one that read
them. The harness's `modelled` mode grades its meter as a model; its default mode,
`issued`, recreates the pinned corpus byte for byte, which CI compares, and is the only
path that still writes those receipts. They stay because the corpus pins every receipt the
kernel issued; the grade they state is not one oer-receipts §2 supports. The `modelled`
run also checks that the kernel refuses a modelled meter graded `measured`.
Every run prints the fixture check first. The vectors cannot check the canonical
form themselves: `generate_vectors.py` imports this verifier's
canonicaliser, so a defect in the field order is baked into every receipt id and

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,15 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Host harness: replays a day of demand through the kernel with a modelled meter and writes an OER/2 vector file. */
/* Host harness: replays a day of demand through the kernel with a modelled meter and writes an OER/2 vector file.
settle_host <out.json> <n> [issued|modelled]
The meter is a model: its compute energy is a cycle count times the kernel's projected cycle energy, and its idle
and shared allocations are constant powers nobody measured on this host. `modelled` states that: method
"model_estimated", grade "projected", the grade of its weakest input. `issued`, the default, recreates byte for
byte the receipts this harness issued before the kernel took its grade from its meter, which name the method
"hardware_meter" and the grade "measured" for the same modelled figures: oer-receipts §2 does not support that
grade, and those receipts are kept only because the pinned corpus ../firmware_vectors.json holds them
(oer-conformance/README.md). */
#include "settle.h"
#include <stdio.h>
#include <stdlib.h>
@ -7,22 +17,31 @@
static uint32_t rng_s = 42; static double urand(void){ rng_s = rng_s * 1664525u + 1013904223u; return (rng_s >> 8) / 16777216.0; }
int main(int argc, char **argv){
const char *path = argc > 1 ? argv[1] : "firmware_vectors.json"; int n_events = argc > 2 ? atoi(argv[2]) : 400;
int modelled = argc > 3 && !strcmp(argv[3], "modelled");
if (argc > 3 && !modelled && strcmp(argv[3], "issued")) { fprintf(stderr, "usage: settle_host <out.json> <n> [issued|modelled]\n"); return 2; }
static settle_ctx_t ctx; uint8_t seed[32]; int i; for (i = 0; i < 32; i++) seed[i] = (uint8_t)(i * 7 + 3);
settle_init(&ctx, seed, 300.0, 86400.0);
FILE *f = fopen(path, "w"); if (!f) return 1;
char pk[65]; settle_pk_hex(&ctx, pk);
fprintf(f, "{\"oer\":\"2.0\",\"draft\":\"0.1\",\"source\":\"settle kernel host harness, %d events\",\"keys\":{\"ed25519:bench-node-01\":\"%s\"},\"vectors\":[{\"name\":\"v10 firmware chain from the C kernel\",\"receipts\":[", n_events, pk);
fprintf(f, "{\"oer\":\"2.0\",\"draft\":\"0.1\",\"source\":\"settle kernel host harness, %d events%s\",\"keys\":{\"ed25519:bench-node-01\":\"%s\"},\"vectors\":[{\"name\":\"v10 firmware chain from the C kernel%s\",\"receipts\":[",
n_events, modelled ? ", modelled meter graded as a model" : "", pk, modelled ? ", modelled meter" : "");
static char buf[4096]; double now = 0, last = 0, meter = 0; const double standby_w = 1.8e-3, shared_w = 0.06;
for (i = 0; i < n_events; i++) {
now += 20 + 80 * urand(); char cls = urand() < 0.34 ? 'A' : 'B';
double z = urand(); uint32_t id = (uint32_t)(cls == 'A' ? 1 + 2999 * z * z * z : 1 + 399 * z * z * z);
settle_meter_t m; m.compute_j = 120000.0 * ctx.cycle_j; m.movement_j = cls == 'A' ? 2.3e-6 : 0; m.idle_alloc_j = standby_w * (now - last); m.shared_alloc_j = shared_w * (now - last); m.upstream_j = 0;
m.latency_s = cls == 'A' ? 0.019 : 0.0017; m.method = "hardware_meter"; m.weakest_how = "projected"; m.weakest_input = "wire_w";
m.latency_s = cls == 'A' ? 0.019 : 0.0017; m.weakest_how = "projected"; m.weakest_input = "wire_w"; m.device = "hardware";
if (modelled) { m.method = "model_estimated"; m.how = "projected"; } else { m.method = "hardware_meter"; m.how = "measured"; }
settle_result_t res; if (settle_decide(&ctx, cls, id, now, &m, buf, sizeof buf, &res)) { fprintf(stderr, "decide failed\n"); return 2; }
meter += res.joules_total; last = now;
fprintf(f, "%s%s", i ? "," : "", buf);
}
static char recbuf[1 << 20]; fprintf(f, "],\"records\":"); if (settle_records_json(&ctx, recbuf, sizeof recbuf)) { fprintf(stderr, "records overflow\n"); return 3; } fputs(recbuf, f);
fprintf(f, ",\"ledger\":{\"meter_joules\":\"%.12e\",\"unallocated_joules\":\"0.000000e+00\",\"carried_forward_joules\":\"0.000000e+00\"},\"expected\":[]}]}\n", meter);
fclose(f); fprintf(stderr, "wrote %s: %d receipts, meter %.6f J\n", path, n_events, meter); return 0;
fclose(f); fprintf(stderr, "wrote %s: %d receipts, meter %.6f J\n", path, n_events, meter);
if (modelled) { /* the kernel refuses to grade a modelled figure "measured" */
settle_meter_t bad = { 1e-3, 0, 0, 0, 0, 0.001, "model_estimated", "projected", "wire_w", "measured", "hardware" };
settle_result_t res; if (settle_decide(&ctx, 'A', 1, now + 1, &bad, buf, sizeof buf, &res) != -3) { fprintf(stderr, "a modelled figure graded measured was not refused\n"); return 4; }
}
return 0;
}

View file

@ -35,7 +35,17 @@ void settle_pk_hex(const settle_ctx_t *c, char out[65]){ hex32(c->pk, out); }
static const char *dec(char *buf, double v){ snprintf(buf, 24, "%.6e", v); return buf; }
#define PUT(...) do { int _n = snprintf(p, (size_t)(end - p), __VA_ARGS__); if (_n < 0 || p + _n >= end) return -1; p += _n; } while (0)
/* oer-receipts §2: the four grades; `measured` only for a meter that read an instrument. */
static int grade_supported(const settle_meter_t *m){
static const char *grades[] = { "projected", "simulated", "derived", "measured" }; int i, known = 0;
if (!m->how || !m->device || !m->method || !m->weakest_how) return 0;
for (i = 0; i < 4; i++) if (!strcmp(m->how, grades[i])) known = 1;
if (!known) return 0;
return strcmp(m->how, "measured") || !strcmp(m->method, "hardware_meter");
}
int settle_decide(settle_ctx_t *c, char cls, uint32_t id, double now, const settle_meter_t *m, char *out, size_t outlen, settle_result_t *res){
if (!grade_supported(m)) return -3;
uint8_t key[32]; char keyhex[65], rechash[72] = "", d1[24], d2[24], d3[24], d4[24], d5[24], d6[24], d7[24], d8[24], d9[24];
question_key(cls, id, key); hex32(key, keyhex);
settle_record_t *r = lookup(c, key, now); int served = r != NULL; double deriv_j;
@ -53,7 +63,7 @@ int settle_decide(settle_ctx_t *c, char cls, uint32_t id, double now, const sett
PUT("\"work\":{\"status\":1,\"outcome\":\"%s\",\"latency_s\":\"%s\"},", served ? "served_from_record" : "derive", dec(d1, m->latency_s));
PUT("\"energy\":{\"joules_total\":\"%.12e\",\"boundary\":\"%s\",\"breakdown\":{\"compute_j\":\"%s\",\"movement_j\":\"%s\",\"idle_alloc_j\":\"%s\",\"shared_alloc_j\":\"%s\",\"upstream_j\":\"%s\"},\"method\":\"%s\",\"temperature_k\":\"%s\"},",
total, c->boundary, dec(d2, m->compute_j), dec(d3, m->movement_j), dec(d4, m->idle_alloc_j), dec(d5, m->shared_alloc_j), dec(d6, m->upstream_j), m->method, dec(d7, c->temperature_k));
PUT("\"provenance\":{\"how\":\"measured\",\"device\":\"hardware\",\"weakest_input\":{\"how\":\"%s\",\"input\":\"%s\"}},", m->weakest_how, m->weakest_input);
PUT("\"provenance\":{\"how\":\"%s\",\"device\":\"%s\",\"weakest_input\":{\"how\":\"%s\",\"input\":\"%s\"}},", m->how, m->device, m->weakest_how, m->weakest_input);
PUT("\"settlement\":{\"question_key\":\"sha256:%s\",\"served_from_record\":%s,\"record_id\":\"r%u\",\"record_hash\":\"%s\",\"reuse_count\":%u,\"derivation_j\":\"%s\",\"valid_until\":%lld},",
keyhex, served ? "true" : "false", r->id, rechash, r->reuse, dec(d8, deriv_j), (long long)r->valid_until);
PUT("\"floor\":{\"bits_committed\":%u,\"bits_delivered\":\"1.585000e+00\",\"floor_j\":\"%s\",\"decades_above_floor\":\"%.6e\"},", c->bits_committed, dec(d9, floor_j), decades);

View file

@ -8,7 +8,12 @@
#include <stddef.h>
#define SETTLE_RECORDS 4096
#define SETTLE_ANSWER 24
typedef struct { double compute_j, movement_j, idle_alloc_j, shared_alloc_j, upstream_j; double latency_s; const char *method; const char *weakest_how, *weakest_input; } settle_meter_t;
/* What the meter states about its figures. `how` is the receipt's grade (oer-receipts §2: projected,
simulated, derived or measured) and `device` its provenance.device; the kernel writes both as given and
writes neither on its own account. A figure is graded `measured` only when its meter read an instrument:
the kernel refuses `measured` unless `method` is "hardware_meter" (settle_decide returns -3). A meter
that models its figures names a model as its method and grades them by its weakest input. */
typedef struct { double compute_j, movement_j, idle_alloc_j, shared_alloc_j, upstream_j; double latency_s; const char *method; const char *weakest_how, *weakest_input; const char *how, *device; } settle_meter_t;
typedef struct { uint8_t key[32]; char answer[SETTLE_ANSWER]; double created, valid_until, deriv_j; uint32_t reuse, id; uint8_t used; } settle_record_t;
typedef struct { int status; const char *outcome; char answer[SETTLE_ANSWER]; uint32_t record_id; uint32_t reuse; char receipt_hash[72]; double joules_total; } settle_result_t;
typedef struct {
@ -22,7 +27,8 @@ typedef struct {
} settle_ctx_t;
int settle_init(settle_ctx_t *c, const uint8_t seed[32], double temperature_k, double validity_s);
/* Runs the loop for one question. cls is 'A' (charging authorization) or 'B' (sensor check), id the canonical question id.
Writes the full receipt JSON (with chain) into out. Returns 0 on success. */
Writes the full receipt JSON (with chain) into out. Returns 0 on success, -3 for a meter whose grade its method
does not support (above). */
int settle_decide(settle_ctx_t *c, char cls, uint32_t id, double now, const settle_meter_t *m, char *out, size_t outlen, settle_result_t *res);
/* Records as a JSON object {id: {key, answer, created, valid_until}}, for vector files. */
int settle_records_json(const settle_ctx_t *c, char *out, size_t outlen);

View file

@ -34,6 +34,7 @@ int main(void){
m.idle_alloc_j = standby_w * (now_ms - last_ms) / 1000.0;
m.shared_alloc_j = shared_w * (now_ms - last_ms) / 1000.0;
m.upstream_j = 0; m.method = "calibrated_model"; m.weakest_how = "projected"; m.weakest_input = "standby_w";
m.how = "projected"; m.device = "hardware"; /* a model over a projected constant: graded as one, never "measured" */
m.compute_j = 0; m.latency_s = 0;
settle_result_t res;
int rc = settle_decide(&ctx, cls, id, now_ms / 1000.0, &m, out, sizeof out, &res);

View file

@ -1443,6 +1443,20 @@
"edition": null,
"registered": "2026-06-13"
},
{
"capability": "wai.stream.energy",
"kind": "record",
"status": "registered",
"media": null,
"role": null,
"tier": null,
"licence": null,
"replicate": null,
"sets": [],
"spec_section": "§5 Interactive, temporal & computed content",
"edition": null,
"registered": "2026-10-24"
},
{
"capability": "wai.tensor.nnc",
"kind": "payload",
@ -2249,6 +2263,20 @@
"section": "§7.1",
"status": "defined"
},
{
"name": "WSE1",
"kind": "magic",
"document": "extensions/stream-energy.md",
"section": "§4.1",
"status": "defined"
},
{
"name": "WSH1",
"kind": "magic",
"document": "extensions/stream-energy.md",
"section": "§3.1",
"status": "defined"
},
{
"name": "WSIX",
"kind": "magic",
@ -2270,6 +2298,13 @@
"section": "§7.1",
"status": "defined"
},
{
"name": "WST1",
"kind": "magic",
"document": "extensions/stream-energy.md",
"section": "§5.1",
"status": "defined"
},
{
"name": "WTM1",
"kind": "magic",

View file

@ -1102,6 +1102,13 @@ wai:wai.splat.spz a wai:Capability ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-06-13"^^xsd:date .
wai:wai.stream.energy a wai:Capability ;
rdfs:label "wai.stream.energy" ;
wai:kind wai:kind-record ;
wai:status wai:status-registered ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-10-24"^^xsd:date .
wai:wai.tensor.nnc a wai:Capability ;
rdfs:label "wai.tensor.nnc" ;
wai:kind wai:kind-payload ;

View file

@ -0,0 +1,149 @@
//! packaging §11.6 item 1: no step runs in floating point where the step is defined in
//! integers. The packer's WGSL kernels (the motion search, SSE and BLAKE3) are held here to
//! conditions 1 and 2 of determinism-tiers §5, *Claiming the tier on a GPU*, which
//! `wai-ops-gpu/tests/wgsl_rules.rs` checks for the decoders' kernels on naga's IR: this
//! test reads each kernel's source, with its comments removed, and refuses a floating-point
//! type (`f32`, `f16` and the vector and matrix shorthands built on them), a floating-point
//! literal, an `enable` directive and a subgroup or quad built-in.
use std::path::PathBuf;
/// Why a token is refused, or `None`.
fn refused(token: &str) -> Option<&'static str> {
let b = token.as_bytes();
// A leading '.' is a member access or a swizzle (`v.x`, `a[i].xy`) unless a digit
// follows it (`.5`).
if b[0] == b'.' {
return (b.len() > 1 && b[1].is_ascii_digit()).then_some("a floating-point literal");
}
if b[0].is_ascii_digit() {
let int = if let Some(h) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
let h = h.trim_end_matches(['i', 'u']);
!h.is_empty() && h.bytes().all(|c| c.is_ascii_hexdigit())
} else {
let d = token.trim_end_matches(['i', 'u']);
!d.is_empty() && d.bytes().all(|c| c.is_ascii_digit())
};
return (!int).then_some("a floating-point literal");
}
let float_shorthand = |t: &str| {
(t.starts_with("vec") && t.len() == 5 && (t.ends_with('f') || t.ends_with('h')))
|| (t.starts_with("mat") && t.len() == 7 && t.as_bytes()[4] == b'x' && (t.ends_with('f') || t.ends_with('h')))
};
match token {
"f32" | "f16" => Some("a floating-point type"),
"enable" => Some("an enable directive"),
t if float_shorthand(t) => Some("a floating-point type"),
t if t.starts_with("subgroup") || t.starts_with("quad") => Some("a subgroup built-in"),
_ => None,
}
}
/// The source without its comments (line comments, and block comments, which WGSL nests).
fn strip_comments(src: &str) -> String {
let (b, mut out, mut i, mut depth) = (src.as_bytes(), String::new(), 0usize, 0usize);
while i < b.len() {
if b[i..].starts_with(b"/*") {
depth += 1;
i += 2;
} else if depth > 0 && b[i..].starts_with(b"*/") {
depth -= 1;
i += 2;
} else if depth > 0 {
i += 1;
} else if b[i..].starts_with(b"//") {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
} else {
out.push(b[i] as char);
i += 1;
}
}
out
}
/// Each refused token of `src`, with the reason.
fn findings(src: &str) -> Vec<(String, &'static str)> {
let s = strip_comments(src);
let mut out = Vec::new();
let mut tok = String::new();
let flush = |tok: &mut String, out: &mut Vec<(String, &'static str)>| {
if !tok.is_empty() {
if let Some(why) = refused(tok) {
out.push((tok.clone(), why));
}
tok.clear();
}
};
for c in s.chars() {
// A number's exponent sign ("1e-3") belongs to the number.
let exponent_sign = (c == '-' || c == '+') && tok.as_bytes().first().is_some_and(u8::is_ascii_digit) && tok.ends_with(['e', 'E', 'p', 'P']);
if c.is_ascii_alphanumeric() || c == '_' || (c == '.' && tok.as_bytes().first().is_none_or(u8::is_ascii_digit)) || exponent_sign {
if c == '.' && tok.is_empty() {
tok.push(c);
continue;
}
tok.push(c);
} else {
flush(&mut tok, &mut out);
}
}
flush(&mut tok, &mut out);
// A lone '.' (member access) is not a literal.
out.retain(|(t, _)| t != ".");
out
}
fn kernels() -> Vec<(PathBuf, String)> {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/wgsl");
let mut v: Vec<(PathBuf, String)> = std::fs::read_dir(&dir)
.expect("src/wgsl")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|x| x == "wgsl"))
.map(|p| {
let s = std::fs::read_to_string(&p).unwrap();
(p, s)
})
.collect();
v.sort();
v
}
#[test]
fn every_kernel_computes_in_integers_alone() {
let ks = kernels();
assert!(!ks.is_empty(), "no kernel found");
let mut bad = Vec::new();
for (p, src) in &ks {
for (t, why) in findings(src) {
bad.push(format!("{}: {why}: `{t}`", p.display()));
}
}
assert!(bad.is_empty(), "{}", bad.join("\n"));
}
/// The scanner itself: each thing the rule refuses is found in a snippet, and the integer
/// forms the kernels use are not.
#[test]
fn the_scanner_finds_what_the_rule_refuses() {
for (src, want) in [
("let x: f32 = 1;", "a floating-point type"),
("let x = 1.0;", "a floating-point literal"),
("let x = .5;", "a floating-point literal"),
("let x = 2e3;", "a floating-point literal"),
("let x = 1e-3;", "a floating-point literal"),
("let x = 3f;", "a floating-point literal"),
("let x = 0x1p4;", "a floating-point literal"),
("var v: vec4f;", "a floating-point type"),
("var m: mat4x4h;", "a floating-point type"),
("enable f16;", "an enable directive"),
("let s = subgroupAdd(x);", "a subgroup built-in"),
("let q = quadBroadcast(x, 0u);", "a subgroup built-in"),
] {
let f = findings(src);
assert!(f.iter().any(|(_, w)| *w == want), "{src}: {f:?}");
}
let ok = "const ONES: u32 = 0xFFFFFFFFu; let a = v.x + 1u; let b = 7i; let c = vec2<u32>(0u, 12u); let d = w[i].xy; let e = f(a).x; // f32 in a comment\n/* 1.0 /* nested */ f16 */";
assert_eq!(findings(ok), Vec::<(String, &str)>::new());
}

View file

@ -239,6 +239,13 @@ live_metering = ["live_compose", "capture_take", "telemetry"]
# steering manifest, the HLS tag and the DASH element, written exactly. SHA-256, Ed25519 and the key
# log only; no new dependency.
energy_steering = ["live_metering"]
# The macOS reader of the `macos-task` sink profile (energy-measurement §9.2, §9.4): the
# kernel's per-task CPU energy and the IORegistry's GPU time, read unprivileged. Off by
# default, so no other build links IOKit for it.
sink_meter = []
# wai.stream.energy (extensions/stream-energy.md): the hop figure record, the stream-segment
# receipt and the termination record, and their corpus (energy-conformance/stream/).
stream_energy = ["ed25519"]
# Live moderation (extensions/live-moderation): `wai.live.moderation` — the room authority's
# signed review policy (WMP1: what is held for review or reviewed after delivery, by whom, each
# deadline and fallback, and the reason codes with their grounds), the statement (WMS1) that
@ -791,6 +798,20 @@ name = "wai_live_metering_vectors"
path = "src/bin/wai_live_metering_vectors.rs"
required-features = ["live_metering"]
[[bin]]
name = "wai_stream_energy_vectors"
path = "src/bin/wai_stream_energy_vectors.rs"
required-features = ["stream_energy"]
[[bin]]
name = "wai_display_vectors"
path = "src/bin/wai_display_vectors.rs"
[[bin]]
name = "wai_sink_meter"
path = "src/bin/wai_sink_meter.rs"
required-features = ["sink_meter", "codecs", "video_int", "audio_int", "splat_int", "world_int", "haptic", "score", "splat4d", "film"]
[[bin]]
name = "wai_energy_steering_vectors"
path = "src/bin/wai_energy_steering_vectors.rs"
@ -1544,6 +1565,15 @@ name = "live_metering"
path = "tests/live_metering.rs"
required-features = ["live_metering"]
[[test]]
name = "stream_energy"
path = "tests/stream_energy.rs"
required-features = ["stream_energy"]
[[test]]
name = "display_caps"
path = "tests/display_caps.rs"
[[test]]
name = "energy_steering"
path = "tests/energy_steering.rs"

View file

@ -0,0 +1,42 @@
//! Conformance corpus for display capability (`constraints-conformance/display/`,
//! extensions/capability-constraints.md §6.5).
//!
//! wai_display_vectors gen <corpus-dir> write vectors.json
//! wai_display_vectors verify <corpus-dir> vectors.json regenerates byte for byte, and
//! every vector gets the outcome it states
//!
//! The corpus's second implementation is `constraints-conformance/display_verify.py`,
//! written from the extension's text and sharing no code with this binary.
use std::path::Path;
use wai::display_caps::corpus;
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2)) {
(Some("gen"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, corpus::generate())) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
println!("wrote {}", p.display());
}
(Some("verify"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
let doc = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
let same = doc == corpus::generate();
println!("{} vectors.json regenerates byte for byte", if same { "PASS" } else { "FAIL" });
let fails = corpus::verify(&doc);
for f in &fails {
println!("FAIL {f}");
}
println!("{} every vector gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
std::process::exit((!same || !fails.is_empty()) as i32);
}
_ => {
eprintln!("usage: wai_display_vectors gen|verify <corpus-dir>");
std::process::exit(2);
}
}
}

View file

@ -6,11 +6,13 @@
//! reason does not conform.
//!
//! The corpus holds vector files from two independent producers — `vectors.json`
//! from the reference generator (Python) and `firmware_vectors.json` from the
//! portable C kernel in `settle/` — plus `edge_vectors.json`, one hand-declared case
//! for each reason and number shape the upstream nine never exercise. This binary is the third
//! implementation and checks all three files, plus the two fixtures — canonical
//! form and stage list — the vectors cannot check.
//! from the reference generator (Python) and `firmware_vectors.json` and
//! `firmware_vectors_modelled.json` from the portable C kernel in `settle/`, as it
//! issued its receipts and with its modelled meter graded as a model — plus
//! `edge_vectors.json`, one hand-declared case for each reason and number shape the
//! upstream nine never exercise. This binary is the third implementation and checks
//! every file, plus the two fixtures — canonical form and stage list — the vectors
//! cannot check.
//!
//! wai_oer_vectors verify <corpus-dir>
//!
@ -25,7 +27,8 @@ use wai::oer::{check_canonical_form, load, run};
/// path or a truncated file fails instead of passing on nothing.
const FILES: &[(&str, usize)] = &[
("vectors.json", 9), // the upstream draft's vectors (reference generator)
("firmware_vectors.json", 1), // the portable C kernel's output
("firmware_vectors.json", 1), // the portable C kernel's output, as it issued it
("firmware_vectors_modelled.json", 1), // the same, its modelled meter graded as a model
("edge_vectors.json", 99), // the reasons and number shapes the upstream nine never exercise
];

View file

@ -0,0 +1,553 @@
//! `wai_sink_meter` — one decode figure per capability, metered on the `macos-task` sink
//! profile without privileges (extensions/energy-measurement.md §9.2, §9.4).
//!
//! ```text
//! wai_sink_meter list
//! wai_sink_meter run [--spans R] [--seconds S] [--filter SUBSTRING] [--out FILE]
//! wai_sink_meter command --case NAME --capability CAP --decoder DECODER --fabrics cpu[,gpu,media]
//! --units N --unit UNIT --input FILE [--spans R] [--out FILE] -- ARGV...
//! ```
//!
//! `run` decodes, in this process, each capability the reference sink decodes, from a
//! committed vector or from a source this binary makes from a fixed seed and encodes once
//! before metering. Each span runs whole decodes on a thread started after the span's
//! opening read of the task counter (`ri_energy_nj`), one worker under the fast kernels,
//! and closes after the thread has finished and a settle wait has passed. `command` meters
//! an external decoder (dav1d, FFmpeg's software decoders or VideoToolbox) as a child
//! process, read before it is reaped: its own energy and what the kernel billed to it for
//! work other processes did on its behalf, the VideoToolbox decoder service's among them.
//!
//! Every case prints one JSON line: the capability, the decoder, the input's and the
//! output's SHA-256, the work, every span's integers, the fabrics the decode used, the
//! figure with its class and declaration or the reason it has none, its coverage (`Partial`,
//! a lower bound, where the decode ran on a fabric this profile does not meter), the build
//! and the load averages before and after. The figures are the host's at that load:
//! indicative, never a property of the capability alone.
use std::sync::Arc;
use std::time::{Duration, Instant};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use wai::energy_report::{BuildRecord, Liveness};
use wai::int_kernels::{with_policy, Policy};
use wai::sink_energy::macos::{self, Span};
use wai::sink_energy::{fabric, profile, SpanSet};
const VECTORS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors");
const WAI: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
const EVERY: Duration = Duration::from_millis(50);
fn sha256(b: &[u8]) -> String {
Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect()
}
fn read(p: &str) -> Vec<u8> {
std::fs::read(p).unwrap_or_else(|e| panic!("read {p}: {e}"))
}
fn json_of(p: &str) -> Value {
serde_json::from_slice(&read(p)).unwrap_or_else(|e| panic!("{p}: {e}"))
}
/// A seeded generator (SplitMix64).
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
/// One case: what is decoded and how much work one decode is.
struct Case {
name: String,
capability: &'static str,
decoder: String,
input: Vec<u8>,
units: u64,
unit: &'static str,
decode: Arc<dyn Fn() -> Vec<u8> + Send + Sync>,
}
fn builtin() -> String {
format!("builtin:wai@{}", env!("CARGO_PKG_VERSION"))
}
/// A picture with structure at every scale: gradients, edges and seeded noise.
fn picture(w: usize, h: usize) -> Vec<u8> {
let mut g = Rng(0x5157_4149_5f70_6963);
let mut rgb = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let n = (g.next() % 24) as i32 - 12;
let r = ((x * 255) / w) as i32 + if (x / 64 + y / 64) % 2 == 0 { 30 } else { -30 };
let gg = ((y * 255) / h) as i32;
let b = (((x + y) * 3) % 256) as i32;
for (c, v) in [r, gg, b].into_iter().enumerate() {
rgb[(y * w + x) * 3 + c] = (v + n).clamp(0, 255) as u8;
}
}
}
rgb
}
/// Ten seconds of stereo at 48 kHz: three partials and seeded noise.
fn sound() -> (Vec<f32>, u32) {
let (sr, secs) = (48_000u32, 10usize);
let mut g = Rng(0x5157_4149_5f61_7564);
let mut s = Vec::with_capacity(sr as usize * secs * 2);
for i in 0..sr as usize * secs {
let t = i as f32 / sr as f32;
let base = 0.3 * (2.0 * std::f32::consts::PI * 220.0 * t).sin() + 0.2 * (2.0 * std::f32::consts::PI * 330.0 * t).sin() + 0.1 * (2.0 * std::f32::consts::PI * 1760.0 * t).sin();
for _ in 0..2 {
let n = ((g.next() % 2001) as f32 - 1000.0) / 1000.0 * 0.02;
s.push(base + n);
}
}
(s, sr)
}
/// Thirty frames at 640×360 of a moving pattern, for the in-crate AV1 decode.
fn clip() -> (Vec<Vec<u8>>, u32, u32) {
let (w, h) = (640usize, 360usize);
let base = picture(w + 64, h);
let frames = (0..30)
.map(|f| {
let mut fr = vec![0u8; w * h * 3];
for y in 0..h {
let src = (y * (w + 64) + f * 2) * 3;
fr[y * w * 3..(y + 1) * w * 3].copy_from_slice(&base[src..src + w * 3]);
}
fr
})
.collect();
(frames, h as u32, w as u32)
}
fn int_case(name: &str, cap: &'static str, payload: &str, set: &[(&str, &str)], units: u64, unit: &'static str) -> Case {
let p = read(payload);
let set: Vec<(String, Vec<u8>)> = set.iter().map(|(k, v)| (k.to_string(), read(v))).collect();
let input = {
let mut all = p.clone();
for (_, b) in &set {
all.extend_from_slice(b);
}
all
};
let decode = Arc::new(move || {
let view: std::collections::BTreeMap<String, &[u8]> = set.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect();
let held = [wai::pin::HeldBytes::Set { listing: b"", files: &view }];
let params = wai::int_payload::resolve(cap, None, &held, None).unwrap_or_else(|_| wai::int_payload::IntParams::none());
let out = with_policy(Policy::fast(1), || wai::int_payload::decode_int(cap, &p, &params)).expect("the committed vector decodes");
match out {
wai::int_payload::IntDecoded::Image { rgb, .. } => rgb,
wai::int_payload::IntDecoded::Video { frames, .. } => frames.concat(),
wai::int_payload::IntDecoded::Pcm { samples, .. } => samples.iter().flat_map(|s| s.to_le_bytes()).collect(),
wai::int_payload::IntDecoded::Attr { bytes, .. } => bytes,
_ => Vec::new(),
}
});
Case { name: name.into(), capability: cap, decoder: builtin(), input, units, unit, decode }
}
fn cases() -> Vec<Case> {
use wai::codecs::{audio, image, text, video};
let v = |p: &str| format!("{VECTORS}/{p}");
let mut out = Vec::new();
// The integer codecs, from their committed vectors (int-payload-conformance).
let real = json_of(&v("int_video/video_real.json"));
let (rw, rh, rn) = (real["w"].as_u64().unwrap(), real["h"].as_u64().unwrap(), real["n_frames"].as_u64().unwrap());
out.push(int_case("int_motion/video_real", "wai.video.int_motion", &v("int_video/video_real_bitstream.wiv1"), &[], rw * rh * rn, "pixels"));
let tiny = [("entropy.json", v("int_hyper/tiny_entropy.json")), ("g_s.whm", v("int_hyper/tiny_g_s.bin")), ("h_s.whm", v("int_hyper/tiny_h_s.bin"))];
let tiny: Vec<(&str, &str)> = tiny.iter().map(|(a, b)| (*a, b.as_str())).collect();
out.push(int_case("int_hyper/tiny", "wai.neural.int_hyper", &v("int_hyper/tiny_bitstream.wih1"), &tiny, 16 * 16, "pixels"));
out.push(int_case("video_int_hyper/video_nk", "wai.video.int_hyper", &v("int_video/video_nk_bitstream.wiv1"), &tiny, 16 * 16 * 5, "pixels"));
let audio_n = json_of(&v("int_audio/audio.json"))["n_samples"].as_u64().unwrap();
out.push(int_case("int_codec/audio", "wai.audio.int_codec", &v("int_audio/audio_bitstream.wia1"), &[("model.wam", &v("int_audio/audio_model.bin"))], audio_n, "samples"));
let splat_n = json_of(&v("int_splat/splat.json"))["n_gaussians"].as_u64().unwrap();
out.push(int_case("int_codec/splat", "wai.splat.int_codec", &v("int_splat/splat.wis1"), &[("g_s.whm", &v("int_splat/splat_gs.bin"))], splat_n, "gaussians"));
let photo = format!("{WAI}/neural-conformance/vectors/photo");
out.push(int_case("int_synth/photo", "wai.neural.int_synth", &format!("{photo}/latent.bin"), &[("int_synth.json", &format!("{photo}/int_synth.json"))], 128 * 128, "pixels"));
{
let env = wai::container::Wai::from_bytes(&read(&v("int_mlicpp/stream.wai"))).expect("the committed envelope");
let path = format!("{}/waienergy-int_mlicpp.wim1", std::env::temp_dir().display());
std::fs::write(&path, &env.payload).expect("write the int_mlicpp payload");
out.push(int_case("int_mlicpp/stream", "wai.neural.int_mlicpp", &path, &[("model.wmb", &v("int_mlicpp/model.wmb"))], 64 * 64, "pixels"));
}
{
let d = v("int_world");
let t = wai::int_hyper_synth::IntModel::from_bytes(&read(&format!("{d}/world_T.bin"))).unwrap();
let gs = wai::int_hyper_synth::IntModel::from_bytes(&read(&format!("{d}/world_gs.bin"))).unwrap();
let j = json_of(&format!("{d}/world.json"));
let (c, lat) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize);
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect();
let actions: Vec<usize> = j["actions"].as_array().unwrap().iter().map(|x| x.as_u64().unwrap() as usize).collect();
let frames = actions.len() as u64 + 1;
let mut input = read(&format!("{d}/world_T.bin"));
input.extend(read(&format!("{d}/world_gs.bin")));
input.extend(read(&format!("{d}/world.json")));
out.push(Case {
name: "world_model/int_world".into(),
capability: "wai.world.model",
decoder: builtin(),
input,
units: frames,
unit: "frames",
decode: Arc::new(move || with_policy(Policy::fast(1), || wai::int_world::decode(&t, &gs, &z0, &actions, c, lat).unwrap().clip_hash().to_le_bytes().to_vec())),
});
}
// The standard codecs the reference sink decodes, each from one source encoded once.
let (pw, ph) = (1024u32, 768u32);
let pic = picture(pw as usize, ph as usize);
let px = pw as u64 * ph as u64;
type Dec = fn(&[u8]) -> Vec<u8>;
for (cap, enc, dec) in [
("wai.image.png", image::png_encode(&pic, ph, pw).unwrap(), (|b: &[u8]| image::png_decode(b).unwrap().0) as Dec),
("wai.image.jpeg", image::jpeg_encode(&pic, ph, pw, 90).unwrap(), (|b: &[u8]| image::jpeg_decode(b).unwrap().0) as Dec),
("wai.image.avif", image::avif_encode(&pic, ph, pw, 80).unwrap(), (|b: &[u8]| image::avif_decode(b).unwrap().0) as Dec),
("wai.image.jxl", image::jxl_encode(&pic, ph, pw, Some(1.0)).unwrap(), (|b: &[u8]| image::jxl_decode(b).unwrap().0) as Dec),
] {
let input = enc.clone();
out.push(Case { name: format!("{}/1024x768", &cap[10..]), capability: cap, decoder: builtin(), input, units: px, unit: "pixels", decode: Arc::new(move || dec(&enc)) });
}
let (snd, sr) = sound();
let ns = snd.len() as u64;
for (cap, enc, dec) in [
("wai.audio.opus", audio::opus_encode(&snd, sr, 128_000).unwrap(), (|b: &[u8]| audio::opus_decode(b).unwrap().0.iter().flat_map(|f| f.to_le_bytes()).collect()) as Dec),
("wai.audio.flac", audio::flac_encode(&snd, sr).unwrap(), (|b: &[u8]| audio::flac_decode(b).unwrap().0.iter().flat_map(|f| f.to_le_bytes()).collect()) as Dec),
] {
let input = enc.clone();
out.push(Case {
name: format!("{}/10s-stereo", &cap[10..]),
capability: cap,
decoder: builtin(),
input,
units: ns,
unit: "samples",
decode: Arc::new(move || dec(&enc)),
});
}
let spec = read(&format!("{WAI}/SPEC.md"));
let nb = spec.len() as u64;
for (cap, enc, dec) in [
("wai.text.zstd", text::zstd_encode(&spec, 19).unwrap(), (|b: &[u8]| text::zstd_decode(b).unwrap()) as Dec),
("wai.text.xz", text::xz_encode(&spec, 9).unwrap(), (|b: &[u8]| text::xz_decode(b).unwrap()) as Dec),
] {
let input = enc.clone();
out.push(Case { name: format!("{}/SPEC.md", &cap[9..]), capability: cap, decoder: builtin(), input, units: nb, unit: "bytes", decode: Arc::new(move || dec(&enc)) });
}
let (frames, h, w) = clip();
let vpx = frames.len() as u64 * w as u64 * h as u64;
for (cap, lossless) in [("wai.video.av1", false), ("wai.video.av1.lossless", true)] {
let enc = video::av1_encode(&frames, h, w, 30, 1, lossless, 70).unwrap();
let input = enc.clone();
out.push(Case {
name: format!("{}/640x360x30", &cap[10..]),
capability: cap,
decoder: builtin(),
input,
units: vpx,
unit: "pixels",
decode: Arc::new(move || video::av1_decode(&enc).unwrap().0.concat()),
});
}
// The extension classes, from their conformance corpora.
{
let b = read(&format!("{WAI}/haptic-conformance/vectors/thud.whap"));
let sig = wai::haptic::HapticSignal::from_bytes(&b).unwrap();
let units = sig.contract.duration * sig.contract.channels as u64;
out.push(Case {
name: "haptic/thud".into(),
capability: "wai.haptic.signal",
decoder: builtin(),
input: b,
units,
unit: "samples",
decode: Arc::new(move || (0..sig.channels.len()).flat_map(|c| sig.reconstruct_channel(c)).collect()),
});
}
{
let b = read(&format!("{WAI}/score-conformance/vectors/chord.wscr"));
let score = wai::score::Score::from_bytes(&b).unwrap();
let units = score.contract.duration;
out.push(Case {
name: "score/chord".into(),
capability: "wai.audio.score",
decoder: builtin(),
input: b,
units,
unit: "samples",
decode: Arc::new(move || score.mixdown().iter().flat_map(|s| s.to_le_bytes()).collect()),
});
}
{
let b = read(&format!("{WAI}/film-conformance/vectors/dolly.wflm"));
let film = wai::film::Film::from_bytes(&b).unwrap();
let units = film.contract.frames;
out.push(Case {
name: "film/dolly".into(),
capability: "wai.film.linear",
decoder: builtin(),
input: b,
units,
unit: "frames",
decode: Arc::new(move || film.frame_hashes().unwrap().iter().flat_map(|h| h.hash).collect()),
});
}
{
let b = read(&format!("{WAI}/film-conformance/vectors/coverage.wrel"));
let reel = wai::film::Reel::from_bytes(&b).unwrap();
let units = reel.total_frames();
out.push(Case {
name: "reel/coverage".into(),
capability: "wai.film.reel",
decoder: builtin(),
input: b,
units,
unit: "frames",
decode: Arc::new(move || reel.frame_hashes().unwrap().iter().flat_map(|h| h.hash).collect()),
});
}
{
let b = read(&format!("{WAI}/volumetric-conformance/vectors/orbit.w4ds"));
let clip = wai::splat4d::Splat4dClipData::from_bytes(&b).unwrap();
let units = clip.contract.n_frames * clip.contract.n_gaussians as u64;
out.push(Case {
name: "splat4d/orbit".into(),
capability: "wai.splat.4d",
decoder: builtin(),
input: b,
units,
unit: "frame-gaussians",
decode: Arc::new(move || (0..clip.contract.n_frames).flat_map(|f| clip.frame_hash(f)).collect()),
});
}
// Sealed worlds, replayed to their seal through the simulation they name.
for (name, cap, path) in [
("world_replay/basic_kinematics", "wai.world.replay", "world-conformance/vectors/basic_kinematics.wwld"),
("feed_state/market_ticks", "wai.feed.state", "feed-conformance/vectors/market_ticks.wwld"),
("avatar_pose/arm_wave", "wai.avatar.pose", "avatar-conformance/vectors/arm_wave.wwld"),
] {
let b = read(&format!("{WAI}/{path}"));
let container = wai::world::WorldContainer::from_bytes(&b).unwrap();
let ticks = container.seal.as_ref().expect("a sealed replay").final_tick;
wai::world::run_sealed_dispatch(&container).expect("the committed replay verifies");
out.push(Case {
name: name.into(),
capability: cap,
decoder: builtin(),
input: b,
units: ticks,
unit: "ticks",
decode: Arc::new(move || {
wai::world::run_sealed_dispatch(&container).expect("replays");
container.seal.as_ref().map(|s| s.final_hash.to_vec()).unwrap_or_default()
}),
});
}
// The protocol's own cost: a span whose work is a thread that sleeps.
out.push(Case {
name: "protocol/sleep-1s".into(),
capability: "none",
decoder: "none".into(),
input: Vec::new(),
units: 1,
unit: "spans",
decode: Arc::new(|| {
std::thread::sleep(Duration::from_secs(1));
Vec::new()
}),
});
out
}
fn arg(a: &[String], name: &str) -> Option<String> {
a.iter().position(|x| x == name).and_then(|i| a.get(i + 1)).cloned()
}
fn build() -> Value {
let b = BuildRecord::of_this_build();
json!({ "profile": b.profile.label(), "toolchain": b.toolchain, "target": b.target })
}
fn host() -> Value {
let sh = |c: &str, a: &[&str]| std::process::Command::new(c).args(a).output().ok().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_default();
json!({
"chip": sh("sysctl", &["-n", "machdep.cpu.brand_string"]),
"cpus": sh("sysctl", &["-n", "hw.ncpu"]),
"os": format!("{} {}", sh("sw_vers", &["-productName"]), sh("sw_vers", &["-productVersion"])),
})
}
/// The figure, or the refusal, from the spans; and the coverage the fabrics give.
fn figure(spans: &[Span], k: u64, used: u8) -> Value {
let set = SpanSet {
energies_nj: spans.iter().map(|s| s.energy_nj).collect(),
completions: k,
settle_nj: spans.iter().map(|s| s.settle_nj).max().unwrap_or(0),
windows_us: spans.iter().map(|s| s.window_us).collect(),
liveness: spans.iter().map(|s| s.liveness).collect(),
};
let p = profile(1).expect("macos-task");
match set.figure() {
Ok(f) => {
let whole = used & !p.metered() == 0;
let fits = p.admits(wai::energy_class::AcquisitionKind::OnChipCounter, used, whole).is_ok();
json!({
"joules_micro": f.joules_micro,
"class": "OnChipCounter",
"relative_ppm": f.relative_ppm,
"window_us": f.window_us,
"filtering": "Unknown",
"coverage": if whole { "Whole" } else { "Partial" },
"fits_profile": fits,
})
}
Err(r) => json!({ "refused": r.code() }),
}
}
fn span_json(spans: &[Span]) -> Value {
let live = |l: &Liveness| json!([l.intervals, l.live_intervals, l.longest_live_run]);
json!({
"energies_nj": spans.iter().map(|s| s.energy_nj).collect::<Vec<_>>(),
"billed_nj": spans.iter().map(|s| s.billed_nj).collect::<Vec<_>>(),
"service_nj": spans.iter().map(|s| s.service_nj).collect::<Vec<_>>(),
"services": spans.iter().map(|s| s.services).collect::<Vec<_>>(),
"windows_us": spans.iter().map(|s| s.window_us).collect::<Vec<_>>(),
"settle_nj": spans.iter().map(|s| s.settle_nj).collect::<Vec<_>>(),
"liveness": spans.iter().map(|s| live(&s.liveness)).collect::<Vec<_>>(),
"cpu_ns": spans.iter().map(|s| s.cpu_ns).collect::<Vec<_>>(),
"gpu_ns": spans.iter().map(|s| s.gpu_ns).collect::<Vec<_>>(),
})
}
fn emit(out: &Option<String>, line: &Value) {
let s = line.to_string();
println!("{s}");
if let Some(p) = out {
use std::io::Write;
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(p).expect("open --out");
writeln!(f, "{s}").expect("write --out");
}
}
fn run(a: &[String]) {
let spans_n: usize = arg(a, "--spans").map_or(5, |s| s.parse().expect("--spans"));
let seconds: f64 = arg(a, "--seconds").map_or(1.5, |s| s.parse().expect("--seconds"));
let filter = arg(a, "--filter");
let out = arg(a, "--out");
for c in cases() {
if filter.as_ref().is_some_and(|f| !c.name.contains(f.as_str()) && !c.capability.contains(f.as_str())) {
continue;
}
let output = (c.decode)();
// Completions per span: enough for the span to last about `seconds`.
let t = Instant::now();
let mut n = 0u64;
while t.elapsed() < Duration::from_millis(300) || n == 0 {
std::hint::black_box((c.decode)());
n += 1;
}
let each = t.elapsed().as_secs_f64() / n as f64;
let k = ((seconds / each).ceil() as u64).clamp(1, 1 << 20);
let load0 = macos::loadavg();
let work = Arc::clone(&c.decode);
let w: Arc<dyn Fn() + Send + Sync> = Arc::new(move || {
std::hint::black_box(work());
});
let spans: Vec<Span> = (0..spans_n).filter_map(|_| macos::span_in_process(k, EVERY, Arc::clone(&w))).collect();
let used = fabric::CPU | if spans.iter().any(|s| s.gpu_ns > 0) { fabric::GPU } else { 0 };
let line = json!({
"case": c.name,
"capability": c.capability,
"decoder": c.decoder,
"policy": "fast kernels, one worker",
"input_sha256": sha256(&c.input),
"input_bytes": c.input.len(),
"output_sha256": sha256(&output),
"work": { "units": c.units, "unit": c.unit },
"completions_per_span": k,
"spans": span_json(&spans),
"fabrics": fabric::terms(used),
"figure": figure(&spans, k, used),
"profile": "macos-task",
"build": build(),
"host": host(),
"loadavg_before": load0,
"loadavg_after": macos::loadavg(),
});
emit(&out, &line);
}
}
fn command(a: &[String]) {
let split = a.iter().position(|x| x == "--").expect("-- before the command");
let (opts, argv) = (&a[..split], &a[split + 1..]);
let need = |n: &str| arg(opts, n).unwrap_or_else(|| panic!("{n} is required"));
let spans_n: usize = arg(opts, "--spans").map_or(5, |s| s.parse().expect("--spans"));
let mut used = 0u8;
for f in need("--fabrics").split(',') {
used |= match f {
"cpu" => fabric::CPU,
"gpu" => fabric::GPU,
"media" => fabric::MEDIA,
"neural" => fabric::NEURAL,
other => panic!("unknown fabric {other}"),
};
}
let input = need("--input");
let load0 = macos::loadavg();
let mut spans = Vec::new();
for _ in 0..spans_n {
match macos::span_command(argv, EVERY) {
Ok(s) if s.status == Some(0) => spans.push(s),
Ok(s) => panic!("the command exited {:?}", s.status),
Err(e) => panic!("{e}"),
}
}
if spans.iter().any(|s| s.gpu_ns > 0) {
used |= fabric::GPU;
}
let units: u64 = need("--units").parse().expect("--units");
let line = json!({
"case": need("--case"),
"capability": need("--capability"),
"decoder": need("--decoder"),
"command": argv,
"input_sha256": sha256(&read(&input)),
"input_bytes": std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0),
"work": { "units": units, "unit": need("--unit") },
"completions_per_span": 1,
"spans": span_json(&spans),
"fabrics": fabric::terms(used),
"figure": figure(&spans, 1, used),
"profile": "macos-task",
"build": build(),
"host": host(),
"loadavg_before": load0,
"loadavg_after": macos::loadavg(),
});
emit(&arg(opts, "--out"), &line);
}
fn main() {
let a: Vec<String> = std::env::args().collect();
match a.get(1).map(String::as_str) {
Some("list") => {
for c in cases() {
println!("{}\t{}\t{} {} per decode", c.name, c.capability, c.units, c.unit);
}
}
Some("run") => run(&a[2..]),
Some("command") => command(&a[2..]),
_ => {
eprintln!("usage: wai_sink_meter list | run [--spans R] [--seconds S] [--filter SUBSTRING] [--out FILE] | command ... -- ARGV");
std::process::exit(2);
}
}
}

View file

@ -0,0 +1,42 @@
//! Conformance corpus for stream energy (`energy-conformance/stream/`,
//! extensions/stream-energy.md §8).
//!
//! wai_stream_energy_vectors gen <corpus-dir> write vectors.json
//! wai_stream_energy_vectors verify <corpus-dir> vectors.json regenerates byte for byte, and
//! every vector gets the outcome it states
//!
//! The corpus's second implementation is `energy-conformance/stream_energy_verify.py`,
//! written from the extension's text and sharing no code with this binary.
use std::path::Path;
use wai::stream_energy::corpus;
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2)) {
(Some("gen"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, corpus::generate())) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
println!("wrote {}", p.display());
}
(Some("verify"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
let doc = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
let same = doc == corpus::generate();
println!("{} vectors.json regenerates byte for byte", if same { "PASS" } else { "FAIL" });
let fails = corpus::verify(&doc);
for f in &fails {
println!("FAIL {f}");
}
println!("{} every vector gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
std::process::exit((!same || !fails.is_empty()) as i32);
}
_ => {
eprintln!("usage: wai_stream_energy_vectors gen|verify <corpus-dir>");
std::process::exit(2);
}
}
}

View file

@ -0,0 +1,235 @@
//! The energy budgets of learned capabilities (`extensions/energy-measurement.md` §9.5):
//! the figures [platform-decode](../../../extensions/platform-decode.md) §5 reads to let a
//! learned entry rank with power-efficient classical decode.
//!
//! A **budget** is an `OnChipCounter` figure, coverage `Whole`, of a learned capability's
//! decode with one parameter set on a reference sink, measured under the span protocol of
//! energy-measurement §9.4, for one unit of output (a picture, a frame or a second), with
//! its declared uncertainty and window. The spans it is computed from are published in
//! `docs/research/wai-sink-energy.jsonl`; a test recomputes every figure here from them and
//! every parameter-set digest from the committed set. A budget belongs to its parameter
//! set: [`budget_for`] gives one only for an entry whose pin names that set, so a figure
//! measured on one model never qualifies another.
use crate::platform_decode::{self, EnergyBudget};
/// A reference-sink budget, as this crate holds it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReferenceBudget {
pub capability: &'static str,
/// The case of `docs/research/wai-sink-energy.jsonl` it is computed from.
pub case: &'static str,
/// The parameter set measured, as a pin names it: the SHA-256 of its `wai.prior.set`
/// listing, and, for a set of one file, that file's SHA-256 (SPEC §3.1).
pub set_listing_sha256: &'static str,
pub set_file_sha256: Option<&'static str>,
/// Microjoules per unit of output.
pub joules_micro: u64,
/// `picture`, `frame` or `second` (platform-decode §5).
pub per: &'static str,
pub relative_ppm: u64,
pub window_us: u64,
}
/// The reference sink every budget here was measured on, and its profile.
pub const REFERENCE_SINK: &str = "Apple M3 Ultra, macOS 27.0.1, sink profile macos-task, measured 2026-10-22";
/// The budgets of this revision, in capability order.
pub const REFERENCE_BUDGETS: &[ReferenceBudget] = &[
ReferenceBudget {
capability: "wai.audio.int_codec",
case: "int_codec/audio",
set_listing_sha256: "69c4d55346241097187c709a97fe1057c403830b54f05de4cc016d1cd82485d6",
set_file_sha256: Some("132958122b251869ad6725d24a2bb96e2733daaf3e09ef65ebc7af6f46030855"),
joules_micro: 13_986,
per: "second",
relative_ppm: 1_688,
window_us: 1_539_743,
},
ReferenceBudget {
capability: "wai.neural.int_hyper",
case: "int_hyper/tiny",
set_listing_sha256: "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818",
set_file_sha256: None,
joules_micro: 121,
per: "picture",
relative_ppm: 3_118,
window_us: 1_535_088,
},
ReferenceBudget {
capability: "wai.neural.int_mlicpp",
case: "int_mlicpp/stream",
set_listing_sha256: "85a05b0dac15566c5a657ddeee253eaa9f54ce852ae04924f5b04151901d9d1b",
set_file_sha256: Some("7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4"),
joules_micro: 3_468,
per: "picture",
relative_ppm: 10_252,
window_us: 1_633_800,
},
ReferenceBudget {
capability: "wai.neural.int_synth",
case: "int_synth/photo",
set_listing_sha256: "04073f4c1d00f2c37e68dab115eff059848494fc68ca2334c3cd4283442cdf1b",
set_file_sha256: Some("50868e72f5f1599569c413ed1f67ab8610fa0b3c17919162cbf370f49275125c"),
joules_micro: 21_101,
per: "picture",
relative_ppm: 2_557,
window_us: 1_488_961,
},
ReferenceBudget {
capability: "wai.splat.int_codec",
case: "int_codec/splat",
set_listing_sha256: "75cc5f10af4c510115d036f6e2ecd5b90bf33d754e19f9ca38cfbe62e7ce8657",
set_file_sha256: Some("033a37c9d29d7ea9675a0b4a3e269d74f2ab874b5a3b1425eec69bf3bad8b26f"),
joules_micro: 2_451,
per: "frame",
relative_ppm: 3_186,
window_us: 1_541_455,
},
ReferenceBudget {
capability: "wai.video.int_hyper",
case: "video_int_hyper/video_nk",
set_listing_sha256: "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818",
set_file_sha256: None,
joules_micro: 54,
per: "frame",
relative_ppm: 3_321,
window_us: 1_516_652,
},
];
impl ReferenceBudget {
/// The budget as platform-decode §5 reads it.
pub fn budget(&self) -> EnergyBudget {
EnergyBudget {
class: "OnChipCounter".into(),
joules_micro: self.joules_micro,
per: self.per.into(),
relative_ppm: self.relative_ppm,
window_us: self.window_us,
reference_sink: REFERENCE_SINK.into(),
}
}
/// Whether a pin naming `sha256` names this budget's parameter set.
pub fn names_set(&self, sha256: &str) -> bool {
self.set_listing_sha256 == sha256 || self.set_file_sha256 == Some(sha256)
}
}
/// The budget for an entry of `capability` whose pin names `pin_sha256` (SPEC §3.1), as the
/// `budgets` input of `platform_decode::select` takes it: `Some` only for a learned
/// capability measured here with that very parameter set. An entry with no pin names no
/// set, and has no budget.
pub fn budget_for(capability: &str, pin_sha256: Option<&str>) -> Option<EnergyBudget> {
let pin = pin_sha256?;
if !platform_decode::is_learned(capability) {
return None;
}
REFERENCE_BUDGETS.iter().find(|b| b.capability == capability && b.names_set(pin)).map(ReferenceBudget::budget)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::energy_report::Liveness;
use crate::sink_energy::SpanSet;
use std::collections::BTreeMap;
fn wai(p: &str) -> Vec<u8> {
std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/..").to_string() + "/" + p).unwrap_or_else(|e| panic!("{p}: {e}"))
}
/// The committed parameter set each case decoded with (int-payload-conformance).
fn set_of(case: &str) -> BTreeMap<String, Vec<u8>> {
let v = |p: &str| wai(&format!("wai-rs/tests/vectors/{p}"));
let tiny = || BTreeMap::from([("entropy.json".into(), v("int_hyper/tiny_entropy.json")), ("g_s.whm".into(), v("int_hyper/tiny_g_s.bin")), ("h_s.whm".into(), v("int_hyper/tiny_h_s.bin"))]);
match case {
"int_hyper/tiny" | "video_int_hyper/video_nk" => tiny(),
"int_codec/audio" => BTreeMap::from([("model.wam".into(), v("int_audio/audio_model.bin"))]),
"int_codec/splat" => BTreeMap::from([("g_s.whm".into(), v("int_splat/splat_gs.bin"))]),
"int_synth/photo" => BTreeMap::from([("int_synth.json".into(), wai("neural-conformance/vectors/photo/int_synth.json"))]),
"int_mlicpp/stream" => BTreeMap::from([("model.wmb".into(), v("int_mlicpp/model.wmb"))]),
other => panic!("no set for {other}"),
}
}
/// Units of output per decode of each case's vector, as a fraction.
fn units_per_decode(case: &str) -> (u128, u128) {
match case {
"video_int_hyper/video_nk" => (5, 1),
"int_codec/audio" => (8_192, 16_000), // 8 192 samples at 16 kHz: 0.512 s
_ => (1, 1),
}
}
/// Every budget is §9.4's figure from the spans the research notes publish for its case,
/// divided by the units of output one decode gives, rounded to the nearest microjoule,
/// with the rounding's `0.5/√3` µJ added to its declaration wherever the division does not
/// come out whole; and its parameter set's digests are the committed set's.
#[test]
fn every_budget_recomputes_from_its_published_spans_and_set() {
let text = String::from_utf8(wai("../docs/research/wai-sink-energy.jsonl")).unwrap();
let lines: Vec<serde_json::Value> = text.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
for b in REFERENCE_BUDGETS {
let d = lines.iter().find(|d| d["case"] == b.case).unwrap_or_else(|| panic!("no case {}", b.case));
assert_eq!(d["capability"], b.capability);
assert_eq!(d["figure"]["coverage"], "Whole", "{}", b.case);
let s = &d["spans"];
let nums = |k: &str| s[k].as_array().unwrap().iter().map(|x| x.as_u64().unwrap()).collect::<Vec<u64>>();
let set = SpanSet {
energies_nj: nums("energies_nj"),
completions: d["completions_per_span"].as_u64().unwrap(),
settle_nj: nums("settle_nj").into_iter().max().unwrap(),
windows_us: nums("windows_us"),
liveness: s["liveness"].as_array().unwrap().iter().map(|l| Liveness { intervals: l[0].as_u64().unwrap(), live_intervals: l[1].as_u64().unwrap(), longest_live_run: l[2].as_u64().unwrap() }).collect(),
};
let f = set.figure().unwrap_or_else(|r| panic!("{}: {}", b.case, r.code()));
let (num, den) = units_per_decode(b.case);
// F × den / num, to the nearest microjoule, a half rounding up.
let fu = (2 * f.joules_micro as u128 * den + num) / (2 * num);
let p = f.relative_ppm as u128;
let pu = if (f.joules_micro as u128 * den) % num == 0 {
p
} else {
// The least p' with 12·Fu²·p'² ≥ 12·Fu²·p² + 10¹²: the rounding added in quadrature.
let need = 12 * fu * fu * p * p + 1_000_000_000_000;
let mut x = p;
while 12 * fu * fu * x * x < need {
x += 1;
}
x
};
assert_eq!((b.joules_micro as u128, b.relative_ppm as u128, b.window_us), (fu, pu, f.window_us), "{}", b.case);
let members = set_of(b.case);
let view: BTreeMap<String, &[u8]> = members.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect();
let listing = crate::pin::listing_from(&view).unwrap();
assert_eq!(crate::pin::sha256_hex(&listing), b.set_listing_sha256, "{}: the listing digest", b.case);
let one = (members.len() == 1).then(|| crate::pin::sha256_hex(members.values().next().unwrap()));
assert_eq!(one.as_deref(), b.set_file_sha256, "{}: the file digest", b.case);
b.budget().qualifies(None).unwrap_or_else(|r| panic!("{}: {r:?}", b.case));
}
}
/// A budget belongs to its parameter set, and the default order of platform-decode §4
/// reads it: an `int_hyper` entry pinned to the measured set ranks with power-efficient
/// classical decode, the same capability pinned to another set ranks last, and so does a
/// learned capability nobody measured.
#[test]
fn a_budget_qualifies_only_its_own_parameter_set() {
use crate::platform_decode::{rank, Entry, Rank, Rules};
let tiny = "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818";
let measured = budget_for("wai.neural.int_hyper", Some(tiny)).expect("the measured set has a budget");
assert!(budget_for("wai.neural.int_hyper", Some(&"ab".repeat(32))).is_none(), "another model has no budget");
assert!(budget_for("wai.neural.int_hyper", None).is_none(), "no pin, no set, no budget");
assert!(budget_for("wai.neural.mlicpp", Some(tiny)).is_none());
assert!(budget_for("wai.video.int_motion", Some(tiny)).is_none(), "not learned");
let ranked = |c: &str, b: Option<&EnergyBudget>| rank(&Entry { capability: c, answers: None, budget: b }, &Rules::DEFAULT).ok();
assert_eq!(ranked("wai.neural.int_hyper", Some(&measured)), Some(Rank::First));
assert_eq!(ranked("wai.neural.int_hyper", None), Some(Rank::LearnedWithoutBudget));
assert_eq!(ranked("wai.neural.mlicpp", None), Some(Rank::LearnedWithoutBudget));
for b in REFERENCE_BUDGETS {
assert!(platform_decode::is_learned(b.capability), "{}", b.capability);
}
}
}

View file

@ -24,6 +24,10 @@ pub mod video;
// the wire forms a `Prior` may declare (extensions/prior-carriage).
pub mod derivation;
pub mod prior_form;
// The energy budgets of learned capabilities, measured on a reference sink, in the form
// platform-decode §5 reads (energy-measurement §9.5).
/// cbindgen:ignore
pub mod energy_budget;
// The registry as one table, and its export (`wai registry export`).
pub mod registry;
// The codec glossary (wai/glossary) held to its schema and the registry. Test-only.

View file

@ -230,6 +230,7 @@ pub const CAP_MEDIA_CONTRIBUTION: &str = "wai.media.contribution";
pub const CAP_DELIVERY_SETTLEMENT: &str = "wai.delivery.settlement";
pub const CAP_ACCESS_SERVICES: &str = "wai.access.services";
pub const CAP_MEDIA_EXPLORATION: &str = "wai.media.exploration";
pub const CAP_STREAM_ENERGY: &str = "wai.stream.energy";
/// One row per string SPEC §5 registers that is a payload, companion or record:
/// `(string, SPEC section, edition, registered)`.
@ -328,6 +329,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[
(CAP_DELIVERY_SETTLEMENT, S_INTERACTIVE, None, "2026-10-21"),
(CAP_ACCESS_SERVICES, S_INTERACTIVE, None, "2026-10-21"),
(CAP_MEDIA_EXPLORATION, S_INTERACTIVE, None, "2026-10-21"),
(CAP_STREAM_ENERGY, S_INTERACTIVE, None, "2026-10-24"),
(CAP_AUDIO_IAMF, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_AC4, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_IVAS, S_IMMERSIVE, None, "2026-06-17"),
@ -389,6 +391,7 @@ const RECORDS: &[&str] = &[
CAP_DELIVERY_SETTLEMENT,
CAP_ACCESS_SERVICES,
CAP_MEDIA_EXPLORATION,
CAP_STREAM_ENERGY,
];
/// A named identifier SPEC uses that has the shape of a capability string but is
@ -541,6 +544,7 @@ const LIVE_RECORDING: &str = "extensions/live-recording.md";
const TIMED_TEXT: &str = "extensions/timed-text.md";
const LIVE_METERING: &str = "extensions/live-metering.md";
const ENERGY_STEERING: &str = "extensions/energy-steering.md";
const STREAM_ENERGY: &str = "extensions/stream-energy.md";
const LIVE_MODERATION: &str = "extensions/live-moderation.md";
const CONFIDENTIALITY: &str = "extensions/confidentiality.md";
const FORENSIC_WATERMARK: &str = "extensions/forensic-watermark.md";
@ -639,6 +643,9 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("WMR1", WireKind::Magic, LIVE_METERING, "§4.1", WireStatus::Defined),
wire("WSP1", WireKind::Magic, ENERGY_STEERING, "§3.1", WireStatus::Defined),
wire("WSD1", WireKind::Magic, ENERGY_STEERING, "§7.1", WireStatus::Defined),
wire("WSH1", WireKind::Magic, STREAM_ENERGY, "§3.1", WireStatus::Defined),
wire("WSE1", WireKind::Magic, STREAM_ENERGY, "§4.1", WireStatus::Defined),
wire("WST1", WireKind::Magic, STREAM_ENERGY, "§5.1", WireStatus::Defined),
wire("WMP1", WireKind::Magic, LIVE_MODERATION, "§3.1", WireStatus::Defined),
wire("WMS1", WireKind::Magic, LIVE_MODERATION, "§4.2", WireStatus::Defined),
wire("WMN1", WireKind::Magic, LIVE_MODERATION, "§5.1", WireStatus::Defined),

View file

@ -0,0 +1,268 @@
//! Display capability (`extensions/capability-constraints.md` §6): what a sink's panel can
//! show, the WAI-authored register entries that name it, and the rule that a sink selects
//! no rendition above its panel when one within it is offered, so bits the panel cannot
//! show are never sent.
//!
//! A rendition is **above** a panel in each of these, independently:
//!
//! - **size**: its longer side is longer than the panel's longer side, or its shorter side
//! longer than the panel's shorter side (the sides compared in the orientation that fits);
//! - **rate**: its frame rate is above the panel's refresh rate;
//! - **depth**: its bit depth is above the panel's;
//! - **transfer**: its transfer function (SDR, PQ or HLG) is one the panel does not show;
//! - **acuity**: where the sink declares its viewing distance and the panel's diagonal, its
//! shorter side holds more lines than a viewer at that distance resolves across the
//! panel's shorter side, at one arc-minute a line (the criterion of ITU-R BT.1845-1's
//! optimal viewing distance).
//!
//! Every comparison is exact in integers; [`exceeds`] gives the bits, and [`select`] the
//! rendition a sink picks. Pure std, compiled unconditionally.
use crate::energy_report::Nat;
pub mod corpus;
/// What [`exceeds`] reports, as bits of one byte.
pub mod above {
pub const SIZE: u8 = 1;
pub const RATE: u8 = 2;
pub const DEPTH: u8 = 4;
pub const TRANSFER: u8 = 8;
pub const ACUITY: u8 = 16;
/// The terms of the bits set, in the order of the bits.
pub fn terms(bits: u8) -> Vec<&'static str> {
[(SIZE, "size"), (RATE, "rate"), (DEPTH, "depth"), (TRANSFER, "transfer"), (ACUITY, "acuity")]
.into_iter()
.filter(|(b, _)| bits & b != 0)
.map(|(_, t)| t)
.collect()
}
}
/// A transfer function, by its code in a display or rendition declaration.
pub mod transfer {
pub const SDR: u8 = 1;
pub const PQ: u8 = 2;
pub const HLG: u8 = 3;
/// The bit of a transfer in a panel's set: `1 << (code − 1)`.
pub fn bit(code: u8) -> u8 {
if (SDR..=HLG).contains(&code) { 1 << (code - 1) } else { 0 }
}
}
/// A panel as a sink declares it (`wai-display:*`, capability-constraints §6.1).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Display {
pub width: u32,
pub height: u32,
/// The highest refresh rate, in millihertz.
pub refresh_mhz: u32,
/// Bits per component the panel shows.
pub bit_depth: u8,
/// The transfer functions it shows: bit `1 << (code − 1)` for each.
pub transfers: u8,
/// Its diagonal, in millimetres; 0 when not declared.
pub diagonal_mm: u32,
/// The viewer's distance from it, in millimetres; 0 when not declared.
pub distance_mm: u32,
}
/// A rendition as an envelope or a playlist declares it (`wai-video:*`, §6.1).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rendition {
pub width: u32,
pub height: u32,
/// Its frame rate, in millihertz.
pub frame_rate_mhz: u32,
pub bit_depth: u8,
/// Its transfer function's code.
pub transfer: u8,
}
/// tan(1′) = tan(π/10 800) as `TAN_ARCMIN / 10¹³`, rounded down, so the acuity rule never
/// flags a rendition the exact value would not.
pub const TAN_ARCMIN: u64 = 2_908_882_086;
/// Why a declaration is refused (§6.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeclarationError {
/// A zero width, height, rate or depth, or a depth above 16.
Malformed,
/// A transfer code other than 1–3, or a panel that shows none.
Transfer,
/// A distance without a diagonal.
DistanceAlone,
}
impl DeclarationError {
pub fn code(self) -> &'static str {
match self {
DeclarationError::Malformed => "display_malformed",
DeclarationError::Transfer => "display_transfer",
DeclarationError::DistanceAlone => "display_distance_alone",
}
}
}
impl Display {
/// §6.2's rules for a panel declaration.
pub fn check(&self) -> Result<(), DeclarationError> {
if self.width == 0 || self.height == 0 || self.refresh_mhz == 0 || self.bit_depth == 0 || self.bit_depth > 16 {
return Err(DeclarationError::Malformed);
}
if self.transfers == 0 || self.transfers & !0b111 != 0 {
return Err(DeclarationError::Transfer);
}
if self.distance_mm != 0 && self.diagonal_mm == 0 {
return Err(DeclarationError::DistanceAlone);
}
Ok(())
}
}
impl Rendition {
/// §6.2's rules for a rendition declaration.
pub fn check(&self) -> Result<(), DeclarationError> {
if self.width == 0 || self.height == 0 || self.frame_rate_mhz == 0 || self.bit_depth == 0 || self.bit_depth > 16 {
return Err(DeclarationError::Malformed);
}
if transfer::bit(self.transfer) == 0 {
return Err(DeclarationError::Transfer);
}
Ok(())
}
}
fn long_short(w: u32, h: u32) -> (u64, u64) {
(w.max(h) as u64, w.min(h) as u64)
}
/// Whether `lines` across the panel's shorter side are more than a viewer at the declared
/// distance resolves at one arc-minute a line:
///
/// ```text
/// lines² · D² · T² · (W² + H²) > G² · S² · 10²⁶
/// ```
///
/// with `D` the distance and `G` the diagonal in millimetres, `W`, `H` the panel's sides and
/// `S` its shorter side in pixels, and `T` = [`TAN_ARCMIN`]: `lines > S_mm / (D · tan 1′)`,
/// squared, where `S_mm = G · S / √(W² + H²)` for square pixels.
pub fn above_acuity(d: &Display, lines: u64) -> bool {
if d.distance_mm == 0 || d.diagonal_mm == 0 {
return false;
}
let n = |v: u128| Nat::from(v);
let (w, h) = (d.width as u128, d.height as u128);
let s = w.min(h);
let lhs = n(lines as u128 * lines as u128)
.mul(&n(d.distance_mm as u128 * d.distance_mm as u128))
.mul(&n(TAN_ARCMIN as u128 * TAN_ARCMIN as u128))
.mul(&n(w * w + h * h));
let rhs = n(d.diagonal_mm as u128 * d.diagonal_mm as u128).mul(&n(s * s)).mul(&n(10u128.pow(26)));
lhs > rhs
}
/// The ways `r` is above `d` (the bits of [`above`]); 0 when it is within.
pub fn exceeds(d: &Display, r: &Rendition) -> u8 {
let mut bits = 0;
let ((pl, ps), (rl, rs)) = (long_short(d.width, d.height), long_short(r.width, r.height));
if rl > pl || rs > ps {
bits |= above::SIZE;
}
if r.frame_rate_mhz > d.refresh_mhz {
bits |= above::RATE;
}
if r.bit_depth > d.bit_depth {
bits |= above::DEPTH;
}
if d.transfers & transfer::bit(r.transfer) == 0 {
bits |= above::TRANSFER;
}
if above_acuity(d, rs) {
bits |= above::ACUITY;
}
bits
}
/// What [`select`] picks: an index, whether it is within the panel, and the ways it is
/// above when it is not.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Selection {
pub index: usize,
pub within: bool,
pub above: u8,
}
/// §6.3's selection over renditions in the deployer's order, each with whether the sink can
/// otherwise pick it (SPEC §7.1 step 2): the first one it can pick that is within the panel,
/// or, when none is, the first one it can pick, reported above. `None` when it can pick none.
pub fn select(d: &Display, renditions: &[(Rendition, bool)]) -> Option<Selection> {
let pickable = || renditions.iter().enumerate().filter(|(_, (_, ok))| *ok);
if let Some((i, _)) = pickable().find(|(_, (r, _))| exceeds(d, r) == 0) {
return Some(Selection { index: i, within: true, above: 0 });
}
pickable().next().map(|(i, (r, _))| Selection { index: i, within: false, above: exceeds(d, r) })
}
#[cfg(test)]
mod tests {
use super::*;
fn r(w: u32, h: u32, fps: u32, depth: u8, tf: u8) -> Rendition {
Rendition { width: w, height: h, frame_rate_mhz: fps * 1000, bit_depth: depth, transfer: tf }
}
/// A 4:3 panel 1 600 mm by 1 200 mm has a 2 000 mm diagonal. ITU-R BT.1845-1 puts the
/// optimal viewing distance of a 1080-line picture at 3.2 H (exactly 1/(1080·tan 1′) =
/// 3.18 H): from 3.1 H a viewer resolves 1 108 lines, so 1 080 are within; from 3.3 H,
/// 1 041, so they are above. At 4 000 mm the boundary is 1 031.32 lines.
#[test]
fn acuity_meets_the_arcminute_distance() {
let at = |mm: u32| Display { width: 1600, height: 1200, refresh_mhz: 60_000, bit_depth: 10, transfers: 0b111, diagonal_mm: 2000, distance_mm: mm };
assert!(!above_acuity(&at(3720), 1080), "3.1 H");
assert!(above_acuity(&at(3960), 1080), "3.3 H");
assert!(!above_acuity(&at(4000), 1031));
assert!(above_acuity(&at(4000), 1032));
let undeclared = Display { distance_mm: 0, ..at(4000) };
assert!(!above_acuity(&undeclared, 100_000));
}
#[test]
fn each_dimension_is_its_own_bit() {
let phone = Display { width: 1179, height: 2556, refresh_mhz: 60_000, bit_depth: 10, transfers: 0b011, diagonal_mm: 0, distance_mm: 0 };
assert_eq!(exceeds(&phone, &r(1920, 1080, 30, 8, transfer::SDR)), 0, "a landscape 1080p fits the portrait panel turned");
assert_eq!(exceeds(&phone, &r(3840, 2160, 30, 10, transfer::PQ)), above::SIZE);
assert_eq!(exceeds(&phone, &r(1920, 1080, 120, 8, transfer::SDR)), above::RATE);
assert_eq!(exceeds(&phone, &r(1920, 1080, 30, 12, transfer::SDR)), above::DEPTH);
assert_eq!(exceeds(&phone, &r(1920, 1080, 30, 10, transfer::HLG)), above::TRANSFER);
}
#[test]
fn the_first_rendition_within_is_selected() {
let monitor = Display { width: 2560, height: 1440, refresh_mhz: 60_000, bit_depth: 8, transfers: 0b001, diagonal_mm: 0, distance_mm: 0 };
let ladder = [
(r(3840, 2160, 60, 10, transfer::PQ), true),
(r(2560, 1440, 60, 10, transfer::PQ), true),
(r(2560, 1440, 60, 8, transfer::SDR), false),
(r(1920, 1080, 60, 8, transfer::SDR), true),
];
assert_eq!(select(&monitor, &ladder), Some(Selection { index: 3, within: true, above: 0 }), "the third is within but not pickable");
let none_within = [(r(3840, 2160, 60, 10, transfer::PQ), true)];
assert_eq!(select(&monitor, &none_within), Some(Selection { index: 0, within: false, above: above::SIZE | above::DEPTH | above::TRANSFER }));
assert_eq!(select(&monitor, &[(r(640, 360, 30, 8, transfer::SDR), false)]), None);
}
#[test]
fn declarations_are_checked() {
let ok = Display { width: 1920, height: 1080, refresh_mhz: 60_000, bit_depth: 8, transfers: 1, diagonal_mm: 600, distance_mm: 700 };
assert_eq!(ok.check(), Ok(()));
assert_eq!(Display { bit_depth: 0, ..ok }.check(), Err(DeclarationError::Malformed));
assert_eq!(Display { transfers: 0, ..ok }.check(), Err(DeclarationError::Transfer));
assert_eq!(Display { transfers: 8, ..ok }.check(), Err(DeclarationError::Transfer));
assert_eq!(Display { diagonal_mm: 0, ..ok }.check(), Err(DeclarationError::DistanceAlone));
assert_eq!(r(1920, 1080, 30, 8, 4).check(), Err(DeclarationError::Transfer));
assert_eq!(r(0, 1080, 30, 8, 1).check(), Err(DeclarationError::Malformed));
}
}

View file

@ -0,0 +1,171 @@
//! The conformance corpus of extensions/capability-constraints.md §6.5:
//! `constraints-conformance/display/vectors.json`.
//!
//! [`generate`] writes it: each vector is a panel and a ladder of renditions in a
//! deployer's order, each with whether the sink can otherwise pick it, and states every
//! rendition's bits above the panel and the selection §6.3 gives; each refusal is a
//! declaration §6.2 refuses, with its code. The panels and ladders describe common devices
//! and services; they are examples, not measurements. [`verify`] checks a corpus file
//! against this module. `constraints-conformance/display_verify.py` checks the same file
//! from the extension's text, sharing no code with this module.
use serde_json::{json, Value};
use super::*;
fn disp(w: u32, h: u32, hz: u32, depth: u8, transfers: u8, diag: u32, dist: u32) -> Display {
Display { width: w, height: h, refresh_mhz: hz * 1000, bit_depth: depth, transfers, diagonal_mm: diag, distance_mm: dist }
}
fn rend(w: u32, h: u32, fps_milli: u32, depth: u8, tf: u8) -> Rendition {
Rendition { width: w, height: h, frame_rate_mhz: fps_milli, bit_depth: depth, transfer: tf }
}
const SDR: u8 = transfer::SDR;
const PQ: u8 = transfer::PQ;
const HLG: u8 = transfer::HLG;
/// A ladder from 2160p60 HDR down to 360p, highest first, every rung pickable.
fn ladder() -> Vec<(Rendition, bool)> {
vec![
(rend(3840, 2160, 60_000, 10, PQ), true),
(rend(3840, 2160, 30_000, 10, PQ), true),
(rend(2560, 1440, 60_000, 10, PQ), true),
(rend(1920, 1080, 60_000, 8, SDR), true),
(rend(1920, 1080, 30_000, 8, SDR), true),
(rend(1280, 720, 30_000, 8, SDR), true),
(rend(640, 360, 30_000, 8, SDR), true),
]
}
fn cases() -> Vec<(&'static str, Display, Vec<(Rendition, bool)>)> {
let mut c = vec![
("tv_4k_hdr_near", disp(3840, 2160, 120, 10, 0b111, 1651, 1200), ladder()),
("tv_4k_hdr_far", disp(3840, 2160, 60, 10, 0b111, 1397, 3000), ladder()),
("monitor_1440p_sdr", disp(2560, 1440, 60, 8, 0b001, 686, 0), ladder()),
("monitor_1080p_sdr_144hz", disp(1920, 1080, 144, 8, 0b001, 610, 650), ladder()),
("laptop_1600p", disp(2560, 1600, 120, 10, 0b011, 406, 0), ladder()),
("phone_portrait_hdr", disp(1179, 2556, 60, 10, 0b011, 155, 350), ladder()),
("phone_portrait_hdr_no_distance", disp(1179, 2556, 60, 10, 0b011, 155, 0), ladder()),
("tablet_hlg_only", disp(2048, 2732, 60, 10, 0b101, 330, 0), ladder()),
];
let mut hdr_only = ladder();
for r in &mut hdr_only[3..] {
r.1 = false;
}
c.push(("monitor_sdr_offered_hdr_only", disp(1920, 1080, 60, 8, 0b001, 610, 0), hdr_only));
let mut hlg = ladder();
hlg.insert(0, (rend(3840, 2160, 50_000, 10, HLG), true));
c.push(("tv_hlg_broadcast_first", disp(3840, 2160, 50, 10, 0b111, 1397, 2000), hlg));
c.push(("rate_120_on_60", disp(1920, 1080, 60, 8, 0b001, 0, 0), vec![(rend(1920, 1080, 120_000, 8, SDR), true), (rend(1920, 1080, 59_940, 8, SDR), true)]));
c.push(("nothing_pickable", disp(1920, 1080, 60, 8, 0b001, 0, 0), vec![(rend(1280, 720, 30_000, 8, SDR), false)]));
c
}
fn refusals() -> Vec<(&'static str, Value, &'static str)> {
let d = |m: Display| json!({ "display": dj(&m) });
let r = |x: Rendition| json!({ "rendition": rj(&x) });
let ok = disp(1920, 1080, 60, 8, 1, 600, 700);
vec![
("display_zero_width", d(Display { width: 0, ..ok }), DeclarationError::Malformed.code()),
("display_depth_17", d(Display { bit_depth: 17, ..ok }), DeclarationError::Malformed.code()),
("display_no_transfer", d(Display { transfers: 0, ..ok }), DeclarationError::Transfer.code()),
("display_unknown_transfer_bit", d(Display { transfers: 0b1001, ..ok }), DeclarationError::Transfer.code()),
("display_distance_without_diagonal", d(Display { diagonal_mm: 0, ..ok }), DeclarationError::DistanceAlone.code()),
("rendition_zero_rate", r(rend(1920, 1080, 0, 8, SDR)), DeclarationError::Malformed.code()),
("rendition_transfer_4", r(rend(1920, 1080, 30_000, 8, 4)), DeclarationError::Transfer.code()),
]
}
fn dj(d: &Display) -> Value {
json!({ "width": d.width, "height": d.height, "refresh_mhz": d.refresh_mhz, "bit_depth": d.bit_depth, "transfers": d.transfers, "diagonal_mm": d.diagonal_mm, "distance_mm": d.distance_mm })
}
fn rj(r: &Rendition) -> Value {
json!({ "width": r.width, "height": r.height, "frame_rate_mhz": r.frame_rate_mhz, "bit_depth": r.bit_depth, "transfer": r.transfer })
}
/// The corpus, as `constraints-conformance/display/vectors.json` holds it.
pub fn generate() -> String {
let vectors: Vec<Value> = cases()
.into_iter()
.map(|(name, d, l)| {
d.check().expect("a corpus panel is well formed");
let sel = select(&d, &l);
json!({
"name": name,
"display": dj(&d),
"renditions": l.iter().map(|(r, ok)| json!({ "rendition": rj(r), "pickable": ok, "above": above::terms(exceeds(&d, r)) })).collect::<Vec<_>>(),
"selected": sel.map(|s| json!({ "index": s.index, "within": s.within, "above": above::terms(s.above) })),
})
})
.collect();
let refused: Vec<Value> = refusals()
.into_iter()
.map(|(name, decl, code)| {
let got = match (decl.get("display"), decl.get("rendition")) {
(Some(d), _) => display_of(d).check(),
(_, Some(r)) => rendition_of(r).check(),
_ => unreachable!(),
};
assert_eq!(got.err().map(DeclarationError::code), Some(code), "{name}");
let mut v = decl;
v["name"] = json!(name);
v["code"] = json!(code);
v
})
.collect();
let doc = json!({
"format": "wai capability-constraints display corpus v1",
"about": "extensions/capability-constraints.md §6.5. Each vector is a panel (wai-display:*) and renditions (wai-video:*) in a deployer's order, each pickable or not; it states each rendition's bits above the panel and the selection §6.3 gives. Each refusal is a declaration §6.2 refuses, with its code.",
"tan_arcmin_e13": TAN_ARCMIN,
"vectors": vectors,
"refusals": refused,
});
let mut s = serde_json::to_string_pretty(&doc).expect("serialise");
s.push('\n');
s
}
fn n(v: &Value, k: &str) -> u64 {
v[k].as_u64().unwrap_or(0)
}
fn display_of(v: &Value) -> Display {
Display { width: n(v, "width") as u32, height: n(v, "height") as u32, refresh_mhz: n(v, "refresh_mhz") as u32, bit_depth: n(v, "bit_depth") as u8, transfers: n(v, "transfers") as u8, diagonal_mm: n(v, "diagonal_mm") as u32, distance_mm: n(v, "distance_mm") as u32 }
}
fn rendition_of(v: &Value) -> Rendition {
Rendition { width: n(v, "width") as u32, height: n(v, "height") as u32, frame_rate_mhz: n(v, "frame_rate_mhz") as u32, bit_depth: n(v, "bit_depth") as u8, transfer: n(v, "transfer") as u8 }
}
/// Check a corpus file against this module.
pub fn verify(doc: &str) -> Vec<String> {
let mut fails = Vec::new();
let Ok(d) = serde_json::from_str::<Value>(doc) else { return vec!["the corpus does not parse".into()] };
for v in d["vectors"].as_array().into_iter().flatten() {
let name = v["name"].as_str().unwrap_or("?");
let disp = display_of(&v["display"]);
let ladder: Vec<(Rendition, bool)> = v["renditions"].as_array().into_iter().flatten().map(|r| (rendition_of(&r["rendition"]), r["pickable"].as_bool().unwrap_or(false))).collect();
for (i, (r, _)) in ladder.iter().enumerate() {
if json!(above::terms(exceeds(&disp, r))) != v["renditions"][i]["above"] {
fails.push(format!("{name}: rendition {i}'s bits above the panel"));
}
}
let sel = select(&disp, &ladder).map(|s| json!({ "index": s.index, "within": s.within, "above": above::terms(s.above) }));
if json!(sel) != v["selected"] {
fails.push(format!("{name}: the selection"));
}
}
for f in d["refusals"].as_array().into_iter().flatten() {
let got = match (f.get("display"), f.get("rendition")) {
(Some(x), _) => display_of(x).check(),
(_, Some(x)) => rendition_of(x).check(),
_ => Ok(()),
};
if got.err().map(DeclarationError::code) != f["code"].as_str() {
fails.push(format!("{}: the refusal", f["name"]));
}
}
fails
}

View file

@ -668,6 +668,67 @@ pub fn check_label(joules_micro: u64, label: Option<&EnergyProvenance>) -> Resul
check_resolution(joules_micro, p)
}
/// A figure as a reader shows it (energy-measurement §2, §9.3): the unmetered marker is
/// never a figure of zero joules, a figure with no class says that nothing states how it
/// was obtained, and a classed figure carries its class, its declared uncertainty where it
/// has one, and whether it is only a lower bound. Every page and tool that shows a figure
/// renders it through [`Presented::of`], so none can show unmetered work as costing nothing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Presented {
/// The unmetered marker: nobody measured this work.
Unmetered,
/// A figure sealed with no class (a legacy form, §2.8).
Unlabelled { joules_micro: u64 },
/// A figure with its class; `lower_bound` where its coverage is `Partial`.
Classed { joules_micro: u64, class: EnergyProvenance, lower_bound: bool },
}
impl Presented {
/// What a reader shows for `joules_micro` sealed with `class`, `partial` where the
/// figure's coverage is `Partial`. A class beside a zero figure is refused, as every
/// verifier refuses it ([`AcquisitionError::NoFigure`]); so is a lower bound with no
/// figure.
pub fn of(joules_micro: u64, class: Option<&EnergyProvenance>, partial: bool) -> Result<Presented, AcquisitionError> {
match (joules_micro, class) {
(0, None) if !partial => Ok(Presented::Unmetered),
(0, _) => Err(AcquisitionError::NoFigure),
(n, None) => Ok(Presented::Unlabelled { joules_micro: n }),
(n, Some(c)) => {
c.validate()?;
check_resolution(n, c)?;
Ok(Presented::Classed { joules_micro: n, class: c.clone(), lower_bound: partial })
}
}
}
}
/// A whole number of microjoules in the largest unit that keeps it at or above one, with
/// three decimals: integer arithmetic, so every reader writes the same text.
pub fn joules_text(uj: u64) -> String {
match uj {
0..=999 => format!("{uj} µJ"),
1_000..=999_999 => format!("{}.{:03} mJ", uj / 1_000, uj % 1_000),
_ => format!("{}.{:03} J", uj / 1_000_000, (uj / 1_000) % 1_000),
}
}
impl core::fmt::Display for Presented {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Presented::Unmetered => f.write_str("unmetered"),
Presented::Unlabelled { joules_micro } => write!(f, "{} (unlabelled: how it was obtained is not stated)", joules_text(*joules_micro)),
Presented::Classed { joules_micro, class, lower_bound } => {
write!(f, "{}{}, {}", if *lower_bound { "at least " } else { "" }, joules_text(*joules_micro), class.label())?;
if let Some(u) = class.uncertainty() {
// ±ppm as a percentage with three decimals, over the window in ms.
write!(f, " ±{}.{:03} % over {} ms", u.relative_ppm / 10_000, (u.relative_ppm % 10_000) / 10, u.window_us / 1_000)?;
}
Ok(())
}
}
}
}
/// [`check_label`] as a predicate, for `verify()`.
pub fn label_coheres(joules_micro: u64, label: Option<&EnergyProvenance>) -> bool {
check_label(joules_micro, label).is_ok()
@ -1110,6 +1171,19 @@ pub(crate) fn test_on_chip() -> EnergyProvenance {
mod tests {
use super::*;
#[test]
fn a_reader_never_shows_unmetered_work_as_zero_joules() {
assert_eq!(Presented::of(0, None, false).unwrap().to_string(), "unmetered");
assert_eq!(Presented::of(0, Some(&EnergyProvenance::Estimator), false), Err(AcquisitionError::NoFigure));
assert_eq!(Presented::of(0, None, true), Err(AcquisitionError::NoFigure), "a lower bound of nothing is not a figure");
assert_eq!(Presented::of(1_234_567, None, false).unwrap().to_string(), "1.234 J (unlabelled: how it was obtained is not stated)");
let occ = EnergyProvenance::on_chip_counter(u(19_000, 2_000_000), CounterFiltering::Unknown).unwrap();
assert_eq!(Presented::of(58_000, Some(&occ), true).unwrap().to_string(), "at least 58.000 mJ, OnChipCounter ±1.900 % over 2000 ms");
assert_eq!(Presented::of(196_000_000, Some(&EnergyProvenance::Estimator), false).unwrap().to_string(), "196.000 J, Estimator");
assert_eq!(joules_text(999), "999 µJ");
assert!(!Presented::of(0, None, false).unwrap().to_string().contains('0'), "no digit in an unmetered figure");
}
fn u(ppm: u32, win: u64) -> Uncertainty {
Uncertainty { relative_ppm: ppm, window_us: win }
}

View file

@ -504,10 +504,10 @@ impl BuildRecord {
/// An unsigned integer of any width, little-endian in 64-bit limbs with no
/// high zero limb — only what §6.2's products need: multiply, add, compare.
#[derive(Clone, Debug, PartialEq, Eq)]
struct Nat(Vec<u64>);
pub(crate) struct Nat(Vec<u64>);
impl Nat {
fn from(v: u128) -> Nat {
pub(crate) fn from(v: u128) -> Nat {
let mut n = Nat(vec![v as u64, (v >> 64) as u64]);
n.trim();
n
@ -519,7 +519,7 @@ impl Nat {
}
}
fn mul(&self, o: &Nat) -> Nat {
pub(crate) fn mul(&self, o: &Nat) -> Nat {
if self.0.is_empty() || o.0.is_empty() {
return Nat(Vec::new());
}
@ -544,7 +544,7 @@ impl Nat {
n
}
fn add(&self, o: &Nat) -> Nat {
pub(crate) fn add(&self, o: &Nat) -> Nat {
let len = self.0.len().max(o.0.len());
let mut out = Vec::with_capacity(len + 1);
let mut carry = 0u128;

View file

@ -303,6 +303,19 @@ pub mod energy_class;
// (energy-measurement §3.1, §6.2). Integer only, pure std, compiled unconditionally.
pub mod energy_report;
// The sink measurement profiles, one per operating system, and the span protocol for a
// task counter with its exact arithmetic (energy-measurement §9). Integer only, pure std,
// compiled unconditionally; its macOS reader is behind `sink_meter`.
/// cbindgen:ignore
pub mod sink_energy;
// Display capability (extensions/capability-constraints.md §6): a panel's sides, refresh, bit
// depth, transfers, diagonal and viewing distance, the exact test of whether a rendition is
// above it, and the selection that sends no rendition above the panel when one within it is
// offered. Integer only, pure std, compiled unconditionally.
/// cbindgen:ignore
pub mod display_caps;
// What build.rs records about the build, shared with build.rs through `#[path]`;
// compiled into the crate for its tests.
#[cfg(test)]
@ -455,6 +468,13 @@ pub mod live_metering;
/// cbindgen:ignore
#[cfg(feature = "energy_steering")]
pub mod energy_steering;
// Stream energy (extensions/stream-energy.md): `wai.stream.energy` — each hop's signed figure
// for a segment (WSH1) with its class, coverage and declared basis, the sink's stream-segment
// receipt (WSE1) with a total per class, a delivery sum and a display sum at their weakest
// class and the panel verdict, and the termination record (WST1) of a stream nobody watches.
/// cbindgen:ignore
#[cfg(feature = "stream_energy")]
pub mod stream_energy;
// Live moderation (extensions/live-moderation.md): `wai.live.moderation` — the room authority's
// review policy, the statement that follows a `decision` event, notices and complaints, lines
// of decisions by digest, the review state machine, gaps explained by the decisions that

View file

@ -0,0 +1,446 @@
//! Sink energy (`extensions/energy-measurement.md` §9): the sink measurement profiles, one
//! per operating system, and the span protocol for a task counter (§9.4) with its exact
//! arithmetic.
//!
//! A **profile** names, for one operating system, the energy counters an unprivileged sink
//! process can read, the class each figure from them may be sealed under, the fabrics each
//! covers, and the fabrics none covers. A decode figure whose work ran on a fabric its
//! profile does not meter covers only the metered fabrics: its coverage is `Partial`, and it
//! is a **lower bound** (§9.3). `HwShunt` is in no profile (§2.6).
//!
//! The **span protocol** reads a counter scoped to the work itself (on macOS, the kernel's
//! account of a task's CPU energy) at the two ends of a span of whole completions, over
//! several spans, and computes the figure and the least declared uncertainty from the
//! integers alone ([`SpanSet::figure`]), so every reader reproduces both. The reader of the
//! counter on macOS is `macos` (feature `sink_meter`).
//!
//! Pure std and integer arithmetic, compiled unconditionally; the reader is macOS only.
use crate::energy_class::{AcquisitionKind, CounterFiltering, EnergyProvenance, Uncertainty};
use crate::energy_report::{Liveness, Nat};
/// cbindgen:ignore
#[cfg(all(target_os = "macos", feature = "sink_meter"))]
pub mod macos;
/// The fabrics a step of work can run on (energy-measurement §6.3), as bits of one byte.
pub mod fabric {
/// The CPU.
pub const CPU: u8 = 1;
/// A GPU.
pub const GPU: u8 = 2;
/// A hardware video engine.
pub const MEDIA: u8 = 4;
/// A neural-network engine.
pub const NEURAL: u8 = 8;
/// Every fabric this revision names.
pub const ALL: u8 = CPU | GPU | MEDIA | NEURAL;
/// The fabrics' terms, in the order of their bits.
pub fn terms(mask: u8) -> Vec<&'static str> {
[(CPU, "cpu"), (GPU, "gpu"), (MEDIA, "media"), (NEURAL, "neural")]
.into_iter()
.filter(|(b, _)| mask & b != 0)
.map(|(_, t)| t)
.collect()
}
}
/// What a counter's reading is attributed to (§9.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scope {
/// The work's own process: the operating system's account of its energy, which other
/// work on the host does not enter.
Task,
/// A rail or package other work shares: a figure needs the bracket of §3, or the
/// sole-user rule of §6.3.
Rail,
}
/// What reading a counter takes (§9.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Privilege {
/// Any process of the user may read it.
None,
/// Only an act of the host's owner opens it to the sink (a group-readable file, a
/// capability granted to the binary): root by default.
OwnerGrant,
}
/// One counter of a profile.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Counter {
/// The fabrics its reading covers.
pub fabrics: u8,
/// The interface read.
pub reader: &'static str,
/// The class a figure from it is sealed under.
pub kind: AcquisitionKind,
/// What the reading is attributed to.
pub scope: Scope,
/// What reading it takes.
pub privilege: Privilege,
}
/// A sink measurement profile (§9.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Profile {
/// Its code in a stream-energy hop record (stream-energy §3), 1 to 4; 0 is none.
pub code: u8,
/// Its term.
pub term: &'static str,
/// The operating system it profiles.
pub os: &'static str,
/// The counters a sink can read under it, unprivileged or by its owner's grant.
pub counters: &'static [Counter],
}
const MACOS_COUNTERS: [Counter; 2] = [
Counter {
fabrics: fabric::CPU,
reader: "proc_pid_rusage(RUSAGE_INFO_V6).ri_energy_nj",
kind: AcquisitionKind::OnChipCounter,
scope: Scope::Task,
privilege: Privilege::None,
},
Counter {
fabrics: fabric::GPU,
reader: "IOReport Energy Model \"GPU Energy\", with IORegistry accumulatedGPUTime for the sole-user rule",
kind: AcquisitionKind::OnChipCounter,
scope: Scope::Rail,
privilege: Privilege::None,
},
];
const LINUX_COUNTERS: [Counter; 2] = [
Counter {
fabrics: fabric::CPU | fabric::GPU | fabric::MEDIA,
reader: "powercap intel-rapl energy_uj (package, with an integrated GPU and its media engine)",
kind: AcquisitionKind::OnChipCounter,
scope: Scope::Rail,
privilege: Privilege::OwnerGrant,
},
Counter {
fabrics: fabric::GPU | fabric::MEDIA,
reader: "NVML nvmlDeviceGetTotalEnergyConsumption (the board, with its video engines)",
kind: AcquisitionKind::OnChipCounter,
scope: Scope::Rail,
privilege: Privilege::None,
},
];
/// The profiles of this revision, by code.
pub const PROFILES: [Profile; 4] = [
Profile { code: 1, term: "macos-task", os: "macOS on Apple silicon", counters: &MACOS_COUNTERS },
Profile { code: 2, term: "linux-rapl-nvml", os: "Linux", counters: &LINUX_COUNTERS },
Profile { code: 3, term: "android-app", os: "Android", counters: &[] },
Profile { code: 4, term: "ios-app", os: "iOS and iPadOS", counters: &[] },
];
/// The profile of a code; `None` for 0 and for a code this revision does not define.
pub fn profile(code: u8) -> Option<&'static Profile> {
PROFILES.iter().find(|p| p.code == code)
}
/// Why a decode figure's class or coverage does not fit its profile (§9.3).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProfileRefusal {
/// The legacy `HwShunt`, which no profile admits (§2.6).
Legacy,
/// A hardware class under a profile with no counter of that class.
ClassNotInProfile,
/// `Whole` coverage of work that ran on a fabric no counter of the profile meters: the
/// figure is a lower bound and says it is not.
BoundOverstated,
/// No fabric named.
NoFabric,
}
impl Profile {
/// The fabrics some counter of the profile meters.
pub fn metered(&self) -> u8 {
self.counters.iter().fold(0, |m, c| m | c.fabrics)
}
/// Whether a decode figure of class `kind`, over work that ran on the fabrics `used`
/// and sealed with coverage `whole`, fits this profile (§9.3). `ModelBased` and
/// `Estimator` fit every profile; a hardware class fits one with a counter of that
/// class, and `CalibratedInstrument` fits none, since an external instrument is no
/// counter of the operating system (its figure names no profile).
pub fn admits(&self, kind: AcquisitionKind, used: u8, whole: bool) -> Result<(), ProfileRefusal> {
if used & fabric::ALL == 0 {
return Err(ProfileRefusal::NoFabric);
}
match kind {
AcquisitionKind::HwShunt => return Err(ProfileRefusal::Legacy),
AcquisitionKind::ModelBased | AcquisitionKind::Estimator => {}
hw => {
if !self.counters.iter().any(|c| c.kind == hw) {
return Err(ProfileRefusal::ClassNotInProfile);
}
if whole && used & !self.metered() != 0 {
return Err(ProfileRefusal::BoundOverstated);
}
}
}
Ok(())
}
}
// ---------------------------------------------------------------------------------------
// §9.4 The span protocol for a task counter
// ---------------------------------------------------------------------------------------
/// The fewest spans a figure is computed from.
pub const MIN_SPANS: usize = 5;
/// The most spans one figure is computed from: it keeps every sum in 128 bits.
pub const MAX_SPANS: usize = 64;
const MAX_INTEGER: u64 = (1 << 53) - 1;
/// The integers of one figure under the span protocol (§9.4): the counter's increase over
/// each span, in nanojoules; the completions each span ran; the largest increase any span's
/// counter showed over its last settle wait; each span's duration; and each span's
/// liveness facts (§3.1), from the counter read during the span.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpanSet {
pub energies_nj: Vec<u64>,
pub completions: u64,
pub settle_nj: u64,
pub windows_us: Vec<u64>,
pub liveness: Vec<Liveness>,
}
/// The figure a span set gives, and the least declaration it supports.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpanFigure {
/// The per-completion figure, in whole microjoules.
pub joules_micro: u64,
/// The least `relative_ppm` the spans support (§9.4).
pub relative_ppm: u32,
/// The shortest span: the window the declaration holds over.
pub window_us: u64,
}
impl SpanFigure {
/// The class the figure is sealed under: `OnChipCounter`, with the counter's filtering
/// state unknown (an unprivileged reader cannot see one, §2.4).
pub fn class(&self) -> EnergyProvenance {
let u = Uncertainty::new(self.relative_ppm, self.window_us).expect("a span figure declares a non-zero uncertainty");
EnergyProvenance::on_chip_counter(u, CounterFiltering::Unknown).expect("a span figure meets the resolution floor")
}
}
/// Why a span set gives no figure under a hardware class (§9.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpanRefusal {
/// Fewer than [`MIN_SPANS`] spans, or more than [`MAX_SPANS`].
SpanCount,
/// The lists are not one per span.
Lengths,
/// A span whose counter did not increase, or no completion.
EmptySpan,
/// A value above 2⁵³ − 1.
OutOfRange,
/// A span whose counter was not live (§3.1), or that lasted under 300 ms.
NotLive,
/// The figure rounds to zero microjoules.
RoundsToZero,
/// The least declaration exceeds 2³² − 1 ppm (§2.7).
Unsealable,
}
impl SpanRefusal {
/// The reason's code.
pub fn code(self) -> &'static str {
match self {
SpanRefusal::SpanCount => "span_count",
SpanRefusal::Lengths => "span_lengths",
SpanRefusal::EmptySpan => "span_empty",
SpanRefusal::OutOfRange => "span_out_of_range",
SpanRefusal::NotLive => "span_not_live",
SpanRefusal::RoundsToZero => "span_rounds_to_zero",
SpanRefusal::Unsealable => "span_unsealable",
}
}
}
impl SpanSet {
/// The figure and the least declaration, from the integers alone (§9.4). With `R`
/// spans of `k` completions each, `S1 = Σ eᵢ`, `S2 = Σ eᵢ²` and `d` the settle bound:
///
/// ```text
/// F = ⌊(2·S1 + 1000·R·k) / (2000·R·k)⌋ µJ
/// p = the least integer with p²·K ≥ P, where
/// K = 6·R²·(R−1)·k²·F²
/// P = 10⁶·( 6·(R·S2 − S1²) + 2·d²·R²·(R−1) + R·(R−1) + 500 000·R²·(R−1)·k² )
/// ```
///
/// The four terms of `P` are the scatter of the spans' mean, the settle bound (a
/// systematic term, not reduced by averaging), the nanojoule resolution of each span's
/// two readings, and the `0.5/√3` µJ of rounding the figure, all carried in
/// nanojoules over the per-completion figure.
pub fn figure(&self) -> Result<SpanFigure, SpanRefusal> {
let r = self.energies_nj.len();
if !(MIN_SPANS..=MAX_SPANS).contains(&r) {
return Err(SpanRefusal::SpanCount);
}
if self.windows_us.len() != r || self.liveness.len() != r {
return Err(SpanRefusal::Lengths);
}
if self.completions == 0 || self.energies_nj.contains(&0) {
return Err(SpanRefusal::EmptySpan);
}
let all = self.energies_nj.iter().chain(self.windows_us.iter()).chain([&self.completions, &self.settle_nj]);
if all.into_iter().any(|v| *v > MAX_INTEGER) || self.completions > u32::MAX as u64 {
return Err(SpanRefusal::OutOfRange);
}
if self.liveness.iter().zip(&self.windows_us).any(|(l, w)| !l.is_live_over(*w)) {
return Err(SpanRefusal::NotLive);
}
let (rr, k) = (r as u128, self.completions as u128);
let s1: u128 = self.energies_nj.iter().map(|e| *e as u128).sum();
let s2: u128 = self.energies_nj.iter().map(|e| (*e as u128) * (*e as u128)).sum();
let f = (2 * s1 + 1000 * rr * k) / (2000 * rr * k);
if f == 0 {
return Err(SpanRefusal::RoundsToZero);
}
let f = u64::try_from(f).map_err(|_| SpanRefusal::OutOfRange)?;
let (kk, pp) = k_and_p(r, self.completions, s1, s2, self.settle_nj, f);
let holds = |x: u32| Nat::from(x as u128).mul(&Nat::from(x as u128)).mul(&kk) >= pp;
if !holds(u32::MAX) {
return Err(SpanRefusal::Unsealable);
}
let (mut lo, mut hi) = (1u32, u32::MAX);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if holds(mid) {
hi = mid;
} else {
lo = mid + 1;
}
}
let window_us = *self.windows_us.iter().min().expect("at least MIN_SPANS spans");
Ok(SpanFigure { joules_micro: f, relative_ppm: lo, window_us })
}
}
/// `K` and `P` of §9.4 for figure `f`.
fn k_and_p(r: usize, k: u64, s1: u128, s2: u128, d: u64, f: u64) -> (Nat, Nat) {
let rr = Nat::from(r as u128);
let r1 = Nat::from(r as u128 - 1);
let r2r1 = rr.mul(&rr).mul(&r1);
let k2 = Nat::from(k as u128).mul(&Nat::from(k as u128));
let fsq = Nat::from(f as u128).mul(&Nat::from(f as u128));
let kk = Nat::from(6).mul(&r2r1).mul(&k2).mul(&fsq);
// R·S2 ≥ S1² (Cauchy–Schwarz); both fit 128 bits for R ≤ 64 and eᵢ < 2⁵³.
let scatter = Nat::from(6 * (r as u128 * s2 - s1 * s1));
let settle = Nat::from(2).mul(&Nat::from(d as u128)).mul(&Nat::from(d as u128)).mul(&r2r1);
let resolution = Nat::from(r as u128 * (r as u128 - 1));
let rounding = Nat::from(500_000).mul(&r2r1).mul(&k2);
let pp = Nat::from(1_000_000).mul(&scatter.add(&settle).add(&resolution).add(&rounding));
(kk, pp)
}
#[cfg(test)]
mod tests {
use super::*;
fn live() -> Liveness {
Liveness { intervals: 20, live_intervals: 20, longest_live_run: 20 }
}
fn set(e: &[u64], k: u64, d: u64) -> SpanSet {
SpanSet {
energies_nj: e.to_vec(),
completions: k,
settle_nj: d,
windows_us: vec![1_000_000; e.len()],
liveness: vec![live(); e.len()],
}
}
/// The figure and the declaration against the same arithmetic in floating point, on
/// values where the two cannot disagree by a whole ppm.
#[test]
fn the_figure_and_declaration_follow_the_formula() {
let e = [2_000_000_000u64, 2_010_000_000, 1_990_000_000, 2_005_000_000, 1_995_000_000];
let s = set(&e, 1000, 1_000_000);
let f = s.figure().unwrap();
assert_eq!(f.joules_micro, 2000, "2 J over 1000 completions is 2000 µJ");
let mean = e.iter().sum::<u64>() as f64 / 5.0 / 1000.0;
let sd = (e.iter().map(|x| (*x as f64 / 1000.0 - mean).powi(2)).sum::<f64>() / 4.0).sqrt();
let u2 = sd * sd / 5.0 + (1_000_000.0f64 / 1000.0).powi(2) / 3.0 + 1.0 / (6.0 * 1e6 * 5.0) + 250_000.0 / 3.0;
let want = (1000.0 * u2.sqrt() / 2000.0).ceil() as u32;
assert_eq!(f.relative_ppm, want);
assert_eq!(f.window_us, 1_000_000);
assert!(f.joules_micro * f.relative_ppm as u64 >= crate::energy_class::RESOLUTION_FLOOR_UJ_PPM);
f.class().validate().unwrap();
}
/// Equal spans with no settle increase leave only the counter's and the figure's
/// resolution, which meets the floor exactly as §2.3 states it.
#[test]
fn equal_spans_declare_the_resolution_floor() {
let s = set(&[5_000_000; 5], 1, 0);
let f = s.figure().unwrap();
assert_eq!(f.joules_micro, 5000);
assert!(f.joules_micro * f.relative_ppm as u64 >= 288_676);
assert!(f.joules_micro * (f.relative_ppm as u64 - 1) < 288_676 + 5000, "the least p, not a larger one");
}
#[test]
fn each_refusal_has_its_condition() {
assert_eq!(set(&[1_000_000; 4], 1, 0).figure(), Err(SpanRefusal::SpanCount));
assert_eq!(set(&[1_000_000; 65], 1, 0).figure(), Err(SpanRefusal::SpanCount));
assert_eq!(set(&[1_000_000, 0, 1_000_000, 1_000_000, 1_000_000], 1, 0).figure(), Err(SpanRefusal::EmptySpan));
assert_eq!(set(&[1_000_000; 5], 0, 0).figure(), Err(SpanRefusal::EmptySpan));
assert_eq!(set(&[400; 5], 1, 0).figure(), Err(SpanRefusal::RoundsToZero));
let mut frozen = set(&[1_000_000; 5], 1, 0);
frozen.liveness[2] = Liveness { intervals: 20, live_intervals: 1, longest_live_run: 1 };
assert_eq!(frozen.figure(), Err(SpanRefusal::NotLive));
let mut short = set(&[1_000_000; 5], 1, 0);
short.windows_us[0] = 299_999;
assert_eq!(short.figure(), Err(SpanRefusal::NotLive));
let mut lengths = set(&[1_000_000; 5], 1, 0);
lengths.windows_us.pop();
assert_eq!(lengths.figure(), Err(SpanRefusal::Lengths));
assert_eq!(set(&[1u64 << 53; 5], 1, 0).figure(), Err(SpanRefusal::OutOfRange));
// A one-microjoule figure under a settle bound of 2⁵² nJ cannot be declared in 32 bits.
assert_eq!(set(&[1_000; 5], 1, 1u64 << 52).figure(), Err(SpanRefusal::Unsealable));
}
/// The settle bound is systematic: more spans do not shrink it.
#[test]
fn the_settle_bound_is_not_averaged_away() {
let few = set(&[1_000_000_000; 5], 1, 10_000_000).figure().unwrap();
let many = set(&[1_000_000_000; 60], 1, 10_000_000).figure().unwrap();
// d/√3 over the figure: 10 mJ / √3 / 1 J = 5774 ppm, whatever the span count.
assert_eq!((few.relative_ppm, many.relative_ppm), (5774, 5774));
}
#[test]
fn profiles_admit_their_classes_and_bound_their_fabrics() {
let mac = profile(1).unwrap();
assert_eq!(mac.term, "macos-task");
assert_eq!(mac.metered(), fabric::CPU | fabric::GPU);
let occ = AcquisitionKind::OnChipCounter;
assert_eq!(mac.admits(occ, fabric::CPU, true), Ok(()));
assert_eq!(mac.admits(occ, fabric::CPU | fabric::MEDIA, true), Err(ProfileRefusal::BoundOverstated));
assert_eq!(mac.admits(occ, fabric::CPU | fabric::MEDIA, false), Ok(()), "a lower bound says so");
assert_eq!(mac.admits(occ, fabric::NEURAL, false), Ok(()));
assert_eq!(mac.admits(AcquisitionKind::HwShunt, fabric::CPU, true), Err(ProfileRefusal::Legacy));
assert_eq!(mac.admits(AcquisitionKind::CalibratedInstrument, fabric::CPU, true), Err(ProfileRefusal::ClassNotInProfile));
assert_eq!(mac.admits(occ, 0, true), Err(ProfileRefusal::NoFabric));
for code in [3, 4] {
let p = profile(code).unwrap();
assert_eq!(p.admits(occ, fabric::CPU, false), Err(ProfileRefusal::ClassNotInProfile), "{}", p.term);
assert_eq!(p.admits(AcquisitionKind::Estimator, fabric::CPU | fabric::MEDIA, true), Ok(()), "{}", p.term);
assert_eq!(p.admits(AcquisitionKind::HwShunt, fabric::CPU, false), Err(ProfileRefusal::Legacy), "{}", p.term);
}
let linux = profile(2).unwrap();
assert_eq!(linux.admits(occ, fabric::CPU | fabric::MEDIA, true), Ok(()));
assert_eq!(linux.admits(occ, fabric::NEURAL, true), Err(ProfileRefusal::BoundOverstated));
assert!(linux.counters.iter().any(|c| c.privilege == Privilege::OwnerGrant), "RAPL is root-only since Linux 5.10");
assert!(profile(0).is_none() && profile(5).is_none());
}
}

View file

@ -0,0 +1,416 @@
//! The `macos-task` profile's reader (energy-measurement §9.2, §9.4), unprivileged:
//!
//! - **the task counter**: `proc_pid_rusage(RUSAGE_INFO_V6).ri_energy_nj`, the kernel's
//! account of a task's CPU energy, which xnu accumulates per thread from the CPU's
//! energy counters as the power manager samples them (`osfmk/kern/recount.h`: "CPU energy
//! in nanojoules, when available", sampled "out-of-band by ApplePMGR through CLPC"),
//! together with `ri_billed_energy`, the energy the kernel bills to the task for work
//! other processes did on its behalf (its `energy_billed_to_me` ledger, in nanojoules,
//! `osfmk/kern/task.c`), such as the VideoToolbox decoder service that decodes for a
//! client out of process (`VTDecoderXPCService`). Both are readable for this process and
//! for a child before it is reaped;
//! - **GPU time**: each IORegistry `IOAccelerator` user client's creator pid and its
//! `accumulatedGPUTime`, readable by any process, which says whether the work used the
//! GPU at all.
//!
//! A span ([`span_in_process`], [`span_command`]) reads the counter before the work starts
//! and after the work's threads have finished and a settle wait has passed, and reads it
//! during the span for its liveness facts (§3.1). The media engine and the neural engine
//! have no counter this reader can see on macOS 27, so work on them is unmetered: a figure
//! over it is a lower bound (§9.3).
use std::ffi::{c_char, c_void, CStr, CString};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::energy_report::Liveness;
type CFTypeRef = *const c_void;
type CFStringRef = *const c_void;
type CFMutableDictionaryRef = *mut c_void;
type CFArrayRef = *const c_void;
type CFIndex = isize;
type IoObject = u32;
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFStringCreateWithCString(alloc: CFTypeRef, s: *const c_char, encoding: u32) -> CFStringRef;
fn CFStringGetCString(s: CFStringRef, buf: *mut c_char, size: CFIndex, encoding: u32) -> u8;
fn CFDictionaryGetValue(d: CFTypeRef, key: CFTypeRef) -> CFTypeRef;
fn CFArrayGetCount(a: CFArrayRef) -> CFIndex;
fn CFArrayGetValueAtIndex(a: CFArrayRef, i: CFIndex) -> CFTypeRef;
fn CFNumberGetValue(n: CFTypeRef, ty: CFIndex, out: *mut c_void) -> u8;
fn CFGetTypeID(x: CFTypeRef) -> usize;
fn CFStringGetTypeID() -> usize;
fn CFArrayGetTypeID() -> usize;
fn CFDictionaryGetTypeID() -> usize;
fn CFRelease(x: CFTypeRef);
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;
fn IOServiceGetMatchingServices(port: u32, matching: CFMutableDictionaryRef, it: *mut IoObject) -> i32;
fn IOIteratorNext(it: IoObject) -> IoObject;
fn IOObjectRelease(o: IoObject) -> i32;
fn IORegistryEntryGetChildIterator(e: IoObject, plane: *const c_char, it: *mut IoObject) -> i32;
fn IORegistryEntryCreateCFProperty(e: IoObject, key: CFStringRef, alloc: CFTypeRef, opts: u32) -> CFTypeRef;
}
unsafe extern "C" {
fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut c_void) -> i32;
fn proc_listallpids(buffer: *mut c_void, size: i32) -> i32;
fn proc_pidpath(pid: i32, buffer: *mut c_void, size: u32) -> i32;
fn getpid() -> i32;
fn waitid(idtype: u32, id: u32, info: *mut c_void, options: i32) -> i32;
fn mach_timebase_info(info: *mut [u32; 2]) -> i32;
}
const UTF8: u32 = 0x0800_0100;
const NUMBER_SINT64: CFIndex = 4;
const RUSAGE_INFO_V6: i32 = 6;
/// `rusage_info_v6`'s 64-bit fields after its 16-byte UUID, by place.
const RI_USER_TIME: usize = 0;
const RI_SYSTEM_TIME: usize = 1;
const RI_INSTRUCTIONS: usize = 29;
const RI_BILLED_ENERGY: usize = 31;
const RI_ENERGY_NJ: usize = 40;
/// `<sys/wait.h>`: `P_PID`, and `WEXITED | WNOWAIT | WNOHANG`.
const P_PID: u32 = 1;
const WAIT_EXITED_PEEK: i32 = 0x4 | 0x20 | 0x1;
/// `<sys/signal.h>`: `si_pid` follows `si_signo`, `si_errno` and `si_code`.
const SI_PID_OFFSET: usize = 12;
/// The settle wait after the work's threads finish, before the closing read.
pub const SETTLE: Duration = Duration::from_millis(200);
/// The wait after the closing read whose increase bounds what the read missed (§9.4).
pub const SETTLE_CHECK: Duration = Duration::from_millis(100);
/// What `proc_pid_rusage` reports of a task.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TaskUsage {
/// `ri_energy_nj + ri_billed_energy`: the task's own CPU energy and the energy billed
/// to it for work done on its behalf.
pub energy_nj: u64,
/// Of `energy_nj`, `ri_billed_energy`.
pub billed_nj: u64,
/// `ri_user_time + ri_system_time`, in nanoseconds.
pub cpu_ns: u64,
/// `ri_instructions`.
pub instructions: u64,
}
fn timebase() -> (u64, u64) {
let mut tb = [0u32; 2];
// SAFETY: mach_timebase_info writes two u32s.
unsafe { mach_timebase_info(&mut tb) };
(tb[0].max(1) as u64, tb[1].max(1) as u64)
}
/// The task usage of `pid`, where this process may read it.
pub fn task_usage(pid: i32) -> Option<TaskUsage> {
let mut buf = [0u64; 2 + 64];
// SAFETY: a buffer larger than rusage_info_v6.
let rc = unsafe { proc_pid_rusage(pid, RUSAGE_INFO_V6, buf.as_mut_ptr().cast()) };
if rc != 0 {
return None;
}
let (n, d) = timebase();
let ticks = buf[2 + RI_USER_TIME] as u128 + buf[2 + RI_SYSTEM_TIME] as u128;
Some(TaskUsage {
energy_nj: buf[2 + RI_ENERGY_NJ].saturating_add(buf[2 + RI_BILLED_ENERGY]),
billed_nj: buf[2 + RI_BILLED_ENERGY],
cpu_ns: (ticks * n as u128 / d as u128) as u64,
instructions: buf[2 + RI_INSTRUCTIONS],
})
}
/// This process's id.
pub fn self_pid() -> i32 {
// SAFETY: getpid has no preconditions.
unsafe { getpid() }
}
fn cfstr(s: &str) -> CFStringRef {
let c = CString::new(s).expect("no NUL");
// SAFETY: a C string and the UTF-8 encoding; the caller releases the result.
unsafe { CFStringCreateWithCString(std::ptr::null(), c.as_ptr(), UTF8) }
}
fn string_of(s: CFTypeRef) -> Option<String> {
if s.is_null() {
return None;
}
let mut buf = [0 as c_char; 256];
// SAFETY: a CF object and a buffer of its stated size.
let ok = unsafe { CFGetTypeID(s) == CFStringGetTypeID() && CFStringGetCString(s, buf.as_mut_ptr(), buf.len() as CFIndex, UTF8) != 0 };
// SAFETY: CFStringGetCString wrote a NUL-terminated string.
ok.then(|| unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy().into_owned())
}
/// The GPU time accrued so far, in nanoseconds, by the user clients whose creator is one of
/// `pids`.
pub fn gpu_time_ns(pids: &[i32]) -> u64 {
let plane = CString::new("IOService").expect("no NUL");
let class = CString::new("IOAccelerator").expect("no NUL");
let (k_creator, k_usage, k_time) = (cfstr("IOUserClientCreator"), cfstr("AppUsage"), cfstr("accumulatedGPUTime"));
let mut total = 0u64;
let mut it: IoObject = 0;
// SAFETY: IOKit's documented calls; every object obtained is released.
unsafe {
if IOServiceGetMatchingServices(0, IOServiceMatching(class.as_ptr()), &mut it) == 0 {
loop {
let acc = IOIteratorNext(it);
if acc == 0 {
break;
}
let mut cit: IoObject = 0;
if IORegistryEntryGetChildIterator(acc, plane.as_ptr(), &mut cit) == 0 {
loop {
let c = IOIteratorNext(cit);
if c == 0 {
break;
}
let creator = IORegistryEntryCreateCFProperty(c, k_creator, std::ptr::null(), 0);
let pid = string_of(creator).and_then(|s| s.strip_prefix("pid ").and_then(|r| r.split(',').next()).and_then(|p| p.trim().parse::<i32>().ok()));
if !creator.is_null() {
CFRelease(creator);
}
if pid.is_some_and(|p| pids.contains(&p)) {
let usage = IORegistryEntryCreateCFProperty(c, k_usage, std::ptr::null(), 0);
if !usage.is_null() && CFGetTypeID(usage) == CFArrayGetTypeID() {
for i in 0..CFArrayGetCount(usage) {
let d = CFArrayGetValueAtIndex(usage, i);
if CFGetTypeID(d) != CFDictionaryGetTypeID() {
continue;
}
let n = CFDictionaryGetValue(d, k_time);
let mut v: i64 = 0;
if !n.is_null() && CFNumberGetValue(n, NUMBER_SINT64, (&mut v as *mut i64).cast()) != 0 && v > 0 {
total = total.saturating_add(v as u64);
}
}
}
if !usage.is_null() {
CFRelease(usage);
}
}
IOObjectRelease(c);
}
IOObjectRelease(cit);
}
IOObjectRelease(acc);
}
IOObjectRelease(it);
}
CFRelease(k_creator);
CFRelease(k_usage);
CFRelease(k_time);
}
total
}
fn pids_named(names: &[&str]) -> Vec<i32> {
let mut pids = vec![0i32; 16384];
// SAFETY: a buffer of the stated byte size.
let n = unsafe { proc_listallpids(pids.as_mut_ptr().cast(), (pids.len() * 4) as i32) };
if n <= 0 {
return Vec::new();
}
pids.truncate(n as usize);
pids.into_iter()
.filter(|&p| p > 0)
.filter(|&p| {
let mut buf = [0u8; 4096];
// SAFETY: a buffer of the stated size.
let len = unsafe { proc_pidpath(p, buf.as_mut_ptr().cast(), buf.len() as u32) };
len > 0 && {
let path = String::from_utf8_lossy(&buf[..len as usize]);
let base = path.rsplit('/').next().unwrap_or("");
names.contains(&base)
}
})
.collect()
}
/// The VideoToolbox services that decode and encode for a client, out of its process.
pub const VT_SERVICES: [&str; 2] = ["VTDecoderXPCService", "VTEncoderXPCService"];
/// One span as read.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Span {
/// The counter's increase over the span, in nanojoules: the work's process's own and
/// what the kernel billed to it.
pub energy_nj: u64,
/// Of `energy_nj`, what the kernel billed to the process.
pub billed_nj: u64,
/// The energy of the VideoToolbox services that started during the span, whichever
/// client they served: a diagnostic, never part of the figure.
pub service_nj: u64,
/// The services that started during the span.
pub services: u32,
/// The span's duration, opening read to closing read before the settle wait.
pub window_us: u64,
/// The counter's liveness facts, from reads during the span.
pub liveness: Liveness,
/// The counter's increase over [`SETTLE_CHECK`] after the closing read.
pub settle_nj: u64,
/// The CPU time of the process over the span, in nanoseconds.
pub cpu_ns: u64,
/// The GPU time of the process and the services over the span, in nanoseconds.
pub gpu_ns: u64,
/// The process's exit status, for a command.
pub status: Option<i32>,
}
/// Run `work` `k` times on a thread started after the opening read, reading this process's
/// counter every `every` while it runs, and close the span after the thread has finished
/// and [`SETTLE`] has passed (§9.4).
pub fn span_in_process(k: u64, every: Duration, work: Arc<dyn Fn() + Send + Sync>) -> Option<Span> {
let me = self_pid();
let g0 = gpu_time_ns(&[me]);
let u0 = task_usage(me)?;
let t0 = Instant::now();
let done = Arc::new(AtomicBool::new(false));
let worker = {
let (done, work) = (Arc::clone(&done), Arc::clone(&work));
std::thread::spawn(move || {
for _ in 0..k {
work();
}
done.store(true, Ordering::Release);
})
};
let mut prev = u0.energy_nj;
let mut advances = Vec::new();
while !done.load(Ordering::Acquire) {
std::thread::sleep(every);
let e = task_usage(me)?.energy_nj;
advances.push(e > prev);
prev = e;
}
worker.join().ok()?;
let window_us = t0.elapsed().as_micros() as u64;
std::thread::sleep(SETTLE);
let u1 = task_usage(me)?;
std::thread::sleep(SETTLE_CHECK);
let u2 = task_usage(me)?;
Some(Span {
energy_nj: u1.energy_nj.checked_sub(u0.energy_nj)?,
billed_nj: u1.billed_nj.saturating_sub(u0.billed_nj),
window_us,
liveness: Liveness::from_advances(advances),
settle_nj: u2.energy_nj.saturating_sub(u1.energy_nj),
cpu_ns: u1.cpu_ns.saturating_sub(u0.cpu_ns),
gpu_ns: gpu_time_ns(&[me]).saturating_sub(g0),
..Span::default()
})
}
/// Whether `pid` has exited, without reaping it.
fn exited(pid: i32) -> bool {
let mut info = [0u8; 128];
// SAFETY: a buffer larger than siginfo_t; WNOWAIT leaves the child waitable.
let rc = unsafe { waitid(P_PID, pid as u32, info.as_mut_ptr().cast(), WAIT_EXITED_PEEK) };
rc == 0 && i32::from_ne_bytes(info[SI_PID_OFFSET..SI_PID_OFFSET + 4].try_into().expect("4 bytes")) == pid
}
/// Run `argv` as a child, reading its counter every `every` while it runs; read it again
/// once it has exited, before it is reaped, and after [`SETTLE_CHECK`] more (§9.4). The
/// figure is the child's own energy and what the kernel billed to it for work done on its
/// behalf. The VideoToolbox services that started during the span are read too, for the
/// record only: on a shared host another client can start one at the same time, so their
/// energy is not attributed to the child.
pub fn span_command(argv: &[String], every: Duration) -> Result<Span, String> {
let (prog, args) = argv.split_first().ok_or("an empty command")?;
let before = pids_named(&VT_SERVICES);
let t0 = Instant::now();
let mut child = std::process::Command::new(prog)
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("{prog}: {e}"))?;
let pid = child.id() as i32;
let mut prev = 0u64;
let mut advances = Vec::new();
let mut fresh: Vec<(i32, u64)> = Vec::new();
let mut gpu_seen: Vec<i32> = vec![pid];
let g0 = gpu_time_ns(&gpu_seen);
let scan = |fresh: &mut Vec<(i32, u64)>, gpu_seen: &mut Vec<i32>| {
for p in pids_named(&VT_SERVICES) {
if before.contains(&p) {
continue;
}
let e = task_usage(p).map(|u| u.energy_nj);
match fresh.iter_mut().find(|(q, _)| *q == p) {
Some(slot) => {
if let Some(e) = e {
slot.1 = slot.1.max(e);
}
}
None => {
fresh.push((p, e.unwrap_or(0)));
gpu_seen.push(p);
}
}
}
};
loop {
let gone = exited(pid);
if !gone {
std::thread::sleep(every);
}
scan(&mut fresh, &mut gpu_seen);
let u = task_usage(pid);
if let Some(u) = u {
advances.push(u.energy_nj > prev);
prev = u.energy_nj;
}
if gone {
break;
}
}
let window_us = t0.elapsed().as_micros() as u64;
let gpu_ns = gpu_time_ns(&gpu_seen).saturating_sub(g0);
let u1 = task_usage(pid).ok_or("the child's usage could not be read before it was reaped")?;
std::thread::sleep(SETTLE_CHECK);
let u2 = task_usage(pid).unwrap_or(u1);
let status = child.wait().map_err(|e| e.to_string())?.code();
// The services the child started, until they exit or two seconds pass.
let until = Instant::now() + Duration::from_secs(2);
while Instant::now() < until {
scan(&mut fresh, &mut gpu_seen);
if fresh.iter().all(|(p, _)| task_usage(*p).is_none()) {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
let service_nj: u64 = fresh.iter().map(|(_, e)| *e).sum();
Ok(Span {
energy_nj: u1.energy_nj,
billed_nj: u1.billed_nj,
service_nj,
services: fresh.len() as u32,
window_us,
// The first read is of a counter that started at zero with the child.
liveness: Liveness::from_advances(advances),
settle_nj: u2.energy_nj.saturating_sub(u1.energy_nj),
cpu_ns: u1.cpu_ns,
gpu_ns,
status,
})
}
/// The load averages, as `getloadavg` gives them.
pub fn loadavg() -> [f64; 3] {
unsafe extern "C" {
fn getloadavg(l: *mut f64, n: i32) -> i32;
}
let mut l = [0f64; 3];
// SAFETY: three doubles.
unsafe { getloadavg(l.as_mut_ptr(), 3) };
l
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,553 @@
//! The conformance corpus of extensions/stream-energy.md §8:
//! `energy-conformance/stream/vectors.json`.
//!
//! [`generate`] writes it from fixed seeds: every key is SHA-256 of this corpus's label and
//! an index, every stream id and segment hash SHA-256 of another label and an index, and
//! Ed25519 signing is deterministic (RFC 8032). The figures are synthetic, chosen to
//! exercise the rules, and claim nothing about any device. Each vector states the outcome it
//! was written to have, and the generator asserts that this module reaches it.
//!
//! [`verify`] checks a corpus file against this module.
//! `energy-conformance/stream_energy_verify.py` checks the same file from the extension's
//! text, sharing no code with this module.
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use super::*;
use crate::display_caps::transfer;
const ORIGIN: u8 = 0;
const RELAY_A: u8 = 1;
const RELAY_B: u8 = 2;
const SINK: u8 = 3;
const PHONE: u8 = 4;
const STRANGER: u8 = 5;
const KEYS: [(&str, u8); 6] = [("origin", ORIGIN), ("relay_a", RELAY_A), ("relay_b", RELAY_B), ("sink", SINK), ("phone", PHONE), ("stranger", STRANGER)];
fn h(label: &[u8], i: u8) -> [u8; 32] {
sha256(&[b"wai:stream-energy/corpus/", label, b"\x00", &[i]])
}
fn key(i: u8) -> SigningKey {
SigningKey::from_bytes(&h(b"key", i))
}
fn occ(ppm: u32, window_us: u64) -> EnergyProvenance {
EnergyProvenance::on_chip_counter(Uncertainty::new(ppm, window_us).unwrap(), CounterFiltering::Unknown).unwrap()
}
/// A 1080p, 24 fps, 8-bit SDR rendition of 2 s segments.
const HD: Rendition = Rendition { width: 1920, height: 1080, frame_rate_mhz: 24_000, bit_depth: 8, transfer: transfer::SDR };
const UHD_HDR: Rendition = Rendition { width: 3840, height: 2160, frame_rate_mhz: 24_000, bit_depth: 10, transfer: transfer::PQ };
const SEG_US: u64 = 2_000_000;
/// A 65-inch television, 3840×2160, 120 Hz, 10-bit, SDR/PQ/HLG, viewed from 2.5 m.
fn tv() -> Panel {
Panel { class: 1, display: Display { width: 3840, height: 2160, refresh_mhz: 120_000, bit_depth: 10, transfers: 0b111, diagonal_mm: 1651, distance_mm: 2500 }, brightness_ppm: 600_000 }
}
/// A 1080p 8-bit SDR monitor of 24 inches, no distance declared.
fn monitor() -> Panel {
Panel { class: 2, display: Display { width: 1920, height: 1080, refresh_mhz: 60_000, bit_depth: 8, transfers: 0b001, diagonal_mm: 610, distance_mm: 0 }, brightness_ppm: 500_000 }
}
/// A 55-inch 4K television viewed from 3 m: its 1080 lines are finer than a viewer resolves.
fn far_tv() -> Panel {
Panel { class: 1, display: Display { width: 3840, height: 2160, refresh_mhz: 60_000, bit_depth: 10, transfers: 0b111, diagonal_mm: 1397, distance_mm: 3000 }, brightness_ppm: 1_000_000 }
}
/// A phone panel, 1179×2556, 60 Hz, 10-bit, SDR and PQ.
fn phone_panel() -> Panel {
Panel { class: 5, display: Display { width: 1179, height: 2556, refresh_mhz: 60_000, bit_depth: 10, transfers: 0b011, diagonal_mm: 155, distance_mm: 0 }, brightness_ppm: 500_000 }
}
/// A segment: its content hash and index.
#[derive(Clone, Copy)]
struct Seg {
hash: [u8; 32],
index: u64,
}
fn seg(i: u8) -> Seg {
Seg { hash: h(b"segment", i), index: 100 + i as u64 }
}
fn stream(i: u8) -> [u8; 32] {
h(b"stream", i)
}
struct Hop {
role: u8,
deliveries: u32,
joules: u64,
class: Option<EnergyProvenance>,
coverage: u8,
profile: u8,
fabrics: u8,
rendition: Rendition,
basis: Option<Basis>,
panel: Option<Panel>,
}
impl Hop {
fn record(&self, k: u8, s: Seg, st: [u8; 32]) -> HopRecord {
HopRecord {
role: self.role,
hop: [0; 32],
segment: s.hash,
index: s.index,
stream: if self.role >= role::DECODE { st } else { [0; 32] },
deliveries: self.deliveries,
joules: self.joules,
class: self.class.clone(),
coverage: self.coverage,
profile: self.profile,
fabrics: self.fabrics,
rendition: self.rendition,
duration_us: SEG_US,
basis: self.basis,
panel: self.panel,
sig: [0; 64],
}
.sign(&key(k))
}
}
fn origin(joules: u64, deliveries: u32) -> Hop {
Hop { role: role::ORIGIN, deliveries, joules, class: Some(EnergyProvenance::ModelBased), coverage: coverage::WHOLE, profile: 0, fabrics: 0, rendition: HD, basis: None, panel: None }
}
fn relay(joules: u64, deliveries: u32) -> Hop {
Hop { role: role::RELAY, deliveries, joules, class: Some(occ(40_000, 2_000_000)), coverage: coverage::WHOLE, profile: 0, fabrics: 0, rendition: HD, basis: None, panel: None }
}
fn relay_unmetered() -> Hop {
Hop { class: None, joules: 0, coverage: coverage::NONE, ..relay(0, 12) }
}
/// A CPU decode under `macos-task`: 660 000 µJ at 1.9 % over a 2 s window.
fn decode_cpu() -> Hop {
Hop { role: role::DECODE, deliveries: 1, joules: 660_000, class: Some(occ(19_000, 2_000_000)), coverage: coverage::WHOLE, profile: 1, fabrics: fabric::CPU, rendition: HD, basis: None, panel: None }
}
/// A VideoToolbox decode: the CPU side only, the media engine unmetered.
fn decode_media() -> Hop {
Hop { joules: 58_000, class: Some(occ(52_000, 2_000_000)), coverage: coverage::PARTIAL, fabrics: fabric::CPU | fabric::MEDIA, ..decode_cpu() }
}
/// A phone decode under `android-app`: 180 ms of CPU at a declared 1 200 mW per busy core.
fn decode_phone() -> Hop {
let basis = Basis { power_mw: 1_200, time_us: 180_000, source: 3 };
Hop { role: role::DECODE, deliveries: 1, joules: basis.joules(), class: Some(EnergyProvenance::Estimator), coverage: coverage::WHOLE, profile: 3, fabrics: fabric::CPU, rendition: HD, basis: Some(basis), panel: None }
}
/// A display from the panel's label power over the 2 s it was presented.
fn display(panel: Panel, power_mw: u32, rendition: Rendition) -> Hop {
let basis = Basis { power_mw, time_us: SEG_US, source: if panel.class == 1 || panel.class == 2 { 1 } else { 2 } };
Hop { role: role::DISPLAY, deliveries: 1, joules: basis.joules(), class: Some(EnergyProvenance::Estimator), coverage: coverage::WHOLE, profile: 0, fabrics: 0, rendition, basis: Some(basis), panel: Some(panel) }
}
fn display_unmetered(panel: Panel) -> Hop {
Hop { joules: 0, class: None, coverage: coverage::NONE, basis: None, ..display(panel, 1, HD) }
}
/// The records of a segment's hops, each signed by its hop's key.
fn records(hops: &[(u8, Hop)], s: Seg, st: [u8; 32]) -> Vec<HopRecord> {
hops.iter().map(|(k, x)| x.record(*k, s, st)).collect()
}
fn receipt(signer: u8, s: Seg, st: [u8; 32], recs: &[HopRecord]) -> SegmentReceipt {
SegmentReceipt::over(st, s.hash, s.index, recs).sign(&key(signer))
}
fn standard(s: Seg, st: [u8; 32]) -> Vec<HopRecord> {
records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (SINK, decode_cpu()), (SINK, display(tv(), 98_000, HD))], s, st)
}
// ---------------------------------------------------------------------------------------
// Vectors
// ---------------------------------------------------------------------------------------
struct Vector {
name: &'static str,
kind: &'static str,
/// The receipt, or the termination record.
subject: Vec<u8>,
receipts: Vec<Vec<u8>>,
records: Vec<Vec<u8>>,
outcome: Result<Value, (String, Reason)>,
}
fn entry_facts(r: &SegmentReceipt) -> Value {
const COVERAGE: [&str; 3] = ["none", "Whole", "Partial"];
const VERDICT: [&str; 3] = ["none", "within", "above"];
let sum = |s: &Sum| json!({ "joules_micro": s.joules, "class": tag_label(s.class), "coverage": COVERAGE[s.coverage as usize] });
json!({
"shares": r.hops.iter().map(|e| json!({ "role": e.role, "share": e.share, "class": tag_label(tag_of(e.class.as_ref())) })).collect::<Vec<_>>(),
"unmetered": r.unmetered,
"totals": r.totals.iter().map(|t| json!({ "class": tag_label(t.tag), "figures": t.figures, "joules_micro": t.joules })).collect::<Vec<_>>(),
"delivery": sum(&r.delivery),
"display": sum(&r.display),
"panel": { "verdict": VERDICT[r.panel.verdict as usize], "above": display_caps::above::terms(r.panel.above) },
})
}
fn by_digest(recs: &[Vec<u8>]) -> BTreeMap<[u8; 32], Vec<u8>> {
recs.iter().map(|b| (given_digest(b), b.clone())).collect()
}
/// What this module gives a vector.
fn outcome_of(v: &Vector) -> Result<Value, (String, Reason)> {
let recs = by_digest(&v.records);
match v.kind {
"receipt" => verify_receipt(&v.subject, &recs).map(|a| entry_facts(&a.receipt)).map_err(|(p, r)| (p.label(), r)),
_ => verify_termination(&v.subject, &v.receipts, &recs)
.map(|t| {
let spent = |s: &[RoleSpent; 4]| {
s.iter()
.enumerate()
.map(|(i, r)| json!({ "role": i + 1, "unmetered": r.unmetered, "totals": r.totals.iter().map(|(t, f, j)| json!({ "class": tag_label(*t), "figures": f, "joules_micro": j })).collect::<Vec<_>>() }))
.collect::<Vec<_>>()
};
json!({ "reason": why::label(t.reason), "from_index": t.from_index, "last_index": t.last_index, "spent": spent(&t.spent), "held": spent(&t.held) })
})
.map_err(|(p, r)| (p.label(), r)),
}
}
fn accept_receipt(name: &'static str, recs: Vec<HopRecord>, r: SegmentReceipt) -> Vector {
let mut v = Vector { name, kind: "receipt", subject: r.to_bytes(), receipts: Vec::new(), records: recs.iter().map(HopRecord::to_bytes).collect(), outcome: Ok(Value::Null) };
v.outcome = Ok(outcome_of(&v).unwrap_or_else(|e| panic!("{name}: {e:?}")));
v
}
fn refuse(name: &'static str, kind: &'static str, subject: Vec<u8>, receipts: Vec<Vec<u8>>, records: Vec<Vec<u8>>, place: &str, reason: Reason) -> Vector {
Vector { name, kind, subject, receipts, records, outcome: Err((place.into(), reason)) }
}
/// A receipt over `recs` refused at a record of it. An entry restating a class or a figure
/// §4.2 refuses in a receipt is written as the unmetered marker instead, so that the
/// receipt reads and the refusal is the record's, at its entry.
#[allow(deprecated)]
fn refuse_at_record(name: &'static str, recs: Vec<HopRecord>, place: &str, reason: Reason) -> Vector {
let s = Seg { hash: recs[0].segment, index: recs[0].index };
let st = recs.iter().find(|r| r.role >= role::DECODE).map_or(stream(0), |r| r.stream);
let mut r = SegmentReceipt::over(st, s.hash, s.index, &recs);
for e in &mut r.hops {
if e.class == Some(EnergyProvenance::HwShunt) || energy_rule(e.joules, &e.class, e.coverage).is_err() {
(e.joules, e.share, e.class, e.coverage) = (0, 0, None, coverage::NONE);
}
}
(r.unmetered, r.totals) = totals_of(&r.hops).expect("no overflow");
(r.delivery, r.display) = sums_of(&r.hops).expect("no overflow");
let r = r.sign(&key(SINK));
refuse(name, "receipt", r.to_bytes(), Vec::new(), recs.iter().map(HopRecord::to_bytes).collect(), place, reason)
}
/// The index of the entry of role `r` in a receipt over `recs`.
fn hop_place(recs: &[HopRecord], r: u8) -> String {
let mut es: Vec<(u8, [u8; 32])> = recs.iter().map(|x| (x.role, x.digest())).collect();
es.sort();
format!("hop/{}", es.iter().position(|e| e.0 == r).unwrap())
}
fn resign(mut r: SegmentReceipt) -> SegmentReceipt {
r.sig = [0; 64];
r.sign(&key(SINK))
}
#[allow(deprecated)]
fn vectors() -> Vec<Vector> {
let mut out = Vec::new();
let s0 = stream(0);
// ---- Accepted receipts.
let recs = standard(seg(0), s0);
out.push(accept_receipt("four_hops_metered", recs.clone(), receipt(SINK, seg(0), s0, &recs)));
let media = records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (SINK, decode_media()), (SINK, display(tv(), 98_000, HD))], seg(1), s0);
out.push(accept_receipt("media_engine_lower_bound", media.clone(), receipt(SINK, seg(1), s0, &media)));
let unm = records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay_unmetered()), (SINK, decode_cpu()), (SINK, display(tv(), 98_000, HD))], seg(2), s0);
out.push(accept_receipt("relay_unmetered", unm.clone(), receipt(SINK, seg(2), s0, &unm)));
let shared = records(&[(ORIGIN, origin(5_000_000, 1_000)), (RELAY_A, relay(3_600_000, 400)), (SINK, decode_cpu()), (SINK, display(tv(), 98_000, HD))], seg(3), s0);
out.push(accept_receipt("origin_shared", shared.clone(), receipt(SINK, seg(3), s0, &shared)));
let two = records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (RELAY_B, relay(700, 1)), (SINK, decode_cpu()), (SINK, display(tv(), 98_000, HD))], seg(4), s0);
out.push(accept_receipt("two_relays", two.clone(), receipt(SINK, seg(4), s0, &two)));
let s1 = stream(1);
let phone = records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (PHONE, decode_phone()), (PHONE, display(phone_panel(), 1_100, HD))], seg(0), s1);
out.push(accept_receipt("phone_estimator", phone.clone(), receipt(PHONE, seg(0), s1, &phone)));
let dunm = records(&[(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (SINK, decode_cpu()), (SINK, display_unmetered(monitor()))], seg(5), s0);
out.push(accept_receipt("display_unmetered", dunm.clone(), receipt(SINK, seg(5), s0, &dunm)));
let uhd = Hop { rendition: UHD_HDR, ..decode_cpu() };
let above_recs = records(&[(ORIGIN, Hop { rendition: UHD_HDR, ..origin(9_000, 1) }), (SINK, uhd), (SINK, display(monitor(), 21_000, UHD_HDR))], seg(6), s0);
out.push(accept_receipt("above_panel", above_recs.clone(), receipt(SINK, seg(6), s0, &above_recs)));
let acuity = records(&[(ORIGIN, origin(3_000, 1)), (SINK, decode_cpu()), (SINK, display(far_tv(), 85_000, HD))], seg(7), s0);
out.push(accept_receipt("above_acuity", acuity.clone(), receipt(SINK, seg(7), s0, &acuity)));
let decode_only = records(&[(SINK, decode_cpu())], seg(8), s0);
out.push(accept_receipt("decode_only", decode_only.clone(), receipt(SINK, seg(8), s0, &decode_only)));
// ---- Accepted terminations, one per reason.
for (name, reason, st, from) in [
("termination_hidden", why::HIDDEN, 10u8, 102u64),
("termination_occluded", why::OCCLUDED, 11, 101),
("termination_display_off", why::DISPLAY_OFF, 12, 103),
("termination_autoplay_off", why::AUTOPLAY_OFF, 13, 100),
("termination_idle", why::IDLE, 14, 102),
] {
let stid = stream(st);
let mut rs = Vec::new();
let mut all_recs = Vec::new();
for i in 0..4u8 {
let recs = standard(seg(i), stid);
rs.push(receipt(SINK, seg(i), stid, &recs));
all_recs.extend(recs);
}
let t = Termination::over(stid, reason, 8_000_000, from, &rs).sign(&key(SINK));
let mut v = Vector { name, kind: "termination", subject: t.to_bytes(), receipts: rs.iter().map(SegmentReceipt::to_bytes).collect(), records: all_recs.iter().map(HopRecord::to_bytes).collect(), outcome: Ok(Value::Null) };
v.outcome = Ok(outcome_of(&v).unwrap_or_else(|e| panic!("{name}: {e:?}")));
out.push(v);
}
// ---- Refused at a record (§3.6).
let with = |role_: u8, hop: Hop, k: u8, s: u8| -> Vec<HopRecord> {
let mut base: Vec<(u8, Hop)> = vec![(ORIGIN, origin(3_000, 1)), (RELAY_A, relay(900, 1)), (SINK, decode_cpu()), (SINK, display(tv(), 98_000, HD))];
let i = base.iter().position(|(_, x)| x.role == role_).unwrap();
base[i] = (k, hop);
records(&base, seg(s), s0)
};
let legacy = with(role::RELAY, Hop { class: Some(EnergyProvenance::HwShunt), ..relay(900, 1) }, RELAY_A, 20);
out.push(refuse_at_record("record_legacy_class", legacy.clone(), &hop_place(&legacy, role::RELAY), Reason::LegacyClass));
let zero = with(role::ORIGIN, Hop { joules: 0, ..origin(3_000, 1) }, ORIGIN, 21);
out.push(refuse_at_record("record_class_beside_zero", zero.clone(), &hop_place(&zero, role::ORIGIN), Reason::EnergyInvalid));
let streamed_relay = {
let mut r = with(role::RELAY, relay(900, 1), RELAY_A, 22);
let i = r.iter().position(|x| x.role == role::RELAY).unwrap();
let mut bad = r[i].clone();
bad.stream = s0;
r[i] = bad.sign(&key(RELAY_A));
r
};
out.push(refuse_at_record("record_relay_names_a_stream", streamed_relay.clone(), &hop_place(&streamed_relay, role::RELAY), Reason::RecordInvalid));
let bare_estimate = with(role::ORIGIN, Hop { class: Some(EnergyProvenance::Estimator), ..origin(3_000, 1) }, ORIGIN, 23);
out.push(refuse_at_record("record_estimate_without_basis", bare_estimate.clone(), &hop_place(&bare_estimate, role::ORIGIN), Reason::BasisInvalid));
let display_counter = with(role::DISPLAY, Hop { class: Some(occ(40_000, 2_000_000)), basis: None, ..display(tv(), 98_000, HD) }, SINK, 24);
out.push(refuse_at_record("record_display_not_an_estimate", display_counter.clone(), &hop_place(&display_counter, role::DISPLAY), Reason::BasisInvalid));
let mismatch = with(role::DISPLAY, Hop { joules: 196_000_001, ..display(tv(), 98_000, HD) }, SINK, 25);
out.push(refuse_at_record("record_basis_mismatch", mismatch.clone(), &hop_place(&mismatch, role::DISPLAY), Reason::BasisMismatch));
let phone_counter = with(role::DECODE, Hop { profile: 3, ..decode_cpu() }, SINK, 26);
out.push(refuse_at_record("record_counter_outside_profile", phone_counter.clone(), &hop_place(&phone_counter, role::DECODE), Reason::ProfileMismatch));
let no_profile = with(role::DECODE, Hop { profile: 0, ..decode_cpu() }, SINK, 27);
out.push(refuse_at_record("record_counter_without_profile", no_profile.clone(), &hop_place(&no_profile, role::DECODE), Reason::ProfileMismatch));
let overstated = with(role::DECODE, Hop { coverage: coverage::WHOLE, ..decode_media() }, SINK, 28);
out.push(refuse_at_record("record_media_engine_whole", overstated.clone(), &hop_place(&overstated, role::DECODE), Reason::BoundOverstated));
let neural = with(role::DECODE, Hop { fabrics: fabric::CPU | fabric::NEURAL, ..decode_cpu() }, SINK, 29);
out.push(refuse_at_record("record_neural_engine_whole", neural.clone(), &hop_place(&neural, role::DECODE), Reason::BoundOverstated));
// ---- Refused at the receipt and its steps (§4.2, §6.1).
let good = standard(seg(30), s0);
let good_bytes: Vec<Vec<u8>> = good.iter().map(HopRecord::to_bytes).collect();
let base = receipt(SINK, seg(30), s0, &good);
let mut two_decodes = base.clone();
let extra = Hop { joules: 640_000, ..decode_cpu() }.record(SINK, seg(30), s0);
let mut both = good.clone();
both.push(extra);
let over_both = SegmentReceipt::over(s0, seg(30).hash, seg(30).index, &both);
two_decodes.hops = over_both.hops;
two_decodes.totals = over_both.totals;
two_decodes.unmetered = over_both.unmetered;
two_decodes.delivery = over_both.delivery;
out.push(refuse("receipt_two_decodes", "receipt", resign(two_decodes).to_bytes(), Vec::new(), both.iter().map(HopRecord::to_bytes).collect(), "receipt", Reason::ReceiptInvalid));
let mut legacy_entry = base.clone();
legacy_entry.hops[1].class = Some(EnergyProvenance::HwShunt);
out.push(refuse("receipt_legacy_entry", "receipt", resign(legacy_entry).to_bytes(), Vec::new(), good_bytes.clone(), "receipt", Reason::LegacyClass));
let mut summed = base.clone();
let model = summed.totals.iter().find(|t| t.tag == 2).unwrap().joules;
let t4 = summed.totals.iter_mut().find(|t| t.tag == 4).unwrap();
t4.joules += model;
out.push(refuse("receipt_classes_summed", "receipt", resign(summed).to_bytes(), Vec::new(), good_bytes.clone(), "energy", Reason::ClassesSummed));
let mut zero_total = base.clone();
zero_total.totals.insert(0, Total { tag: 0, figures: 1, joules: 0 });
out.push(refuse("receipt_total_of_no_class", "receipt", resign(zero_total).to_bytes(), Vec::new(), good_bytes.clone(), "receipt", Reason::ClassesSummed));
out.push(refuse("receipt_record_missing", "receipt", base.to_bytes(), Vec::new(), good_bytes[1..].to_vec(), &hop_place(&good, role::ORIGIN), Reason::RecordMissing));
let other = standard(seg(31), s0);
let mut wrong = good.clone();
let i = wrong.iter().position(|r| r.role == role::RELAY).unwrap();
wrong[i] = other.iter().find(|r| r.role == role::RELAY).unwrap().clone();
let wr = receipt(SINK, seg(30), s0, &wrong);
out.push(refuse("receipt_wrong_segment", "receipt", wr.to_bytes(), Vec::new(), wrong.iter().map(HopRecord::to_bytes).collect(), &hop_place(&wrong, role::RELAY), Reason::WrongSegment));
let other_stream = standard(seg(30), stream(2));
let mut ws = good.clone();
let i = ws.iter().position(|r| r.role == role::DECODE).unwrap();
ws[i] = other_stream.iter().find(|r| r.role == role::DECODE).unwrap().clone();
let wsr = receipt(SINK, seg(30), s0, &ws);
out.push(refuse("receipt_decode_of_another_stream", "receipt", wsr.to_bytes(), Vec::new(), ws.iter().map(HopRecord::to_bytes).collect(), &hop_place(&ws, role::DECODE), Reason::WrongSegment));
let mut share = base.clone();
share.hops[0].share += 1;
out.push(refuse("receipt_share_restated", "receipt", resign(share).to_bytes(), Vec::new(), good_bytes.clone(), "hop/0", Reason::UnitMismatch));
let renditions = with(role::RELAY, Hop { rendition: Rendition { width: 1280, height: 720, ..HD }, ..relay(900, 1) }, RELAY_A, 32);
let rr = receipt(SINK, seg(32), s0, &renditions);
out.push(refuse("receipt_rendition_mismatch", "receipt", rr.to_bytes(), Vec::new(), renditions.iter().map(HopRecord::to_bytes).collect(), "rendition", Reason::RenditionMismatch));
let mut count = base.clone();
count.unmetered = 1;
out.push(refuse("receipt_unmetered_miscounted", "receipt", resign(count).to_bytes(), Vec::new(), good_bytes.clone(), "energy", Reason::UnitMismatch));
let unm_bytes: Vec<Vec<u8>> = unm.iter().map(HopRecord::to_bytes).collect();
let mut whole = receipt(SINK, seg(2), s0, &unm);
whole.delivery.coverage = coverage::WHOLE;
out.push(refuse("receipt_partial_sum_stated_whole", "receipt", resign(whole).to_bytes(), Vec::new(), unm_bytes, "sums", Reason::SumMismatch));
let mut strongest = base.clone();
strongest.delivery.class = 4;
out.push(refuse("receipt_sum_at_its_strongest_class", "receipt", resign(strongest).to_bytes(), Vec::new(), good_bytes.clone(), "sums", Reason::SumMismatch));
let above_bytes: Vec<Vec<u8>> = above_recs.iter().map(HopRecord::to_bytes).collect();
let mut within = receipt(SINK, seg(6), s0, &above_recs);
within.panel = PanelVerdict { verdict: 1, above: 0 };
out.push(refuse("receipt_panel_stated_within", "receipt", resign(within).to_bytes(), Vec::new(), above_bytes, "panel", Reason::PanelMismatch));
let mut sig = base.to_bytes();
let n = sig.len();
sig[n - 1] ^= 1;
out.push(refuse("receipt_bad_signature", "receipt", sig, Vec::new(), good_bytes.clone(), "receipt", Reason::BadSignature));
let mut stranger = base.clone();
stranger.signer = key(STRANGER).verifying_key().to_bytes();
out.push(refuse("receipt_signed_by_another_key", "receipt", stranger.to_bytes(), Vec::new(), good_bytes.clone(), "receipt", Reason::BadSignature));
let b = base.to_bytes();
out.push(refuse("receipt_truncated", "receipt", b[..b.len() - 65].to_vec(), Vec::new(), good_bytes.clone(), "receipt", Reason::Truncated));
let mut magic = b.clone();
magic[3] = b'2';
out.push(refuse("receipt_wrong_magic", "receipt", magic, Vec::new(), good_bytes.clone(), "receipt", Reason::Malformed));
let mut version = b.clone();
version[4] = 2;
out.push(refuse("receipt_version_2", "receipt", version, Vec::new(), good_bytes.clone(), "receipt", Reason::UnknownVersion));
let mut trailing = b.clone();
trailing.push(0);
out.push(refuse("receipt_trailing_byte", "receipt", trailing, Vec::new(), good_bytes.clone(), "receipt", Reason::Malformed));
// ---- Refused terminations (§5.3, §6.2).
let stid = stream(20);
let mut rs = Vec::new();
let mut tr = Vec::new();
for i in 0..3u8 {
let recs = standard(seg(40 + i), stid);
rs.push(receipt(SINK, seg(40 + i), stid, &recs));
tr.extend(recs.iter().map(HopRecord::to_bytes));
}
let rb: Vec<Vec<u8>> = rs.iter().map(SegmentReceipt::to_bytes).collect();
let t = Termination::over(stid, why::HIDDEN, 6_000_000, seg(41).index, &rs);
let signed = |t: Termination| t.sign(&key(SINK)).to_bytes();
out.push(refuse("termination_unknown_reason", "termination", signed(Termination { reason: 9, ..t.clone() }), rb.clone(), tr.clone(), "termination", Reason::TerminationInvalid));
out.push(refuse("termination_from_after_last", "termination", signed(Termination { from_index: t.last_index + 1, ..t.clone() }), rb.clone(), tr.clone(), "termination", Reason::TerminationInvalid));
out.push(refuse("termination_receipt_missing", "termination", signed(t.clone()), rb[..2].to_vec(), tr.clone(), "termination", Reason::RecordMissing));
out.push(refuse("termination_last_index", "termination", signed(Termination { last_index: t.last_index + 7, ..t.clone() }), rb.clone(), tr.clone(), "termination", Reason::IndexMismatch));
let mut held = t.clone();
held.held[3].totals[0].2 += 1;
out.push(refuse("termination_held_restated", "termination", signed(held), rb.clone(), tr.clone(), "termination", Reason::UnitMismatch));
let mut spent_all = t.clone();
spent_all.held = spent_all.spent.clone();
out.push(refuse("termination_held_is_all_spent", "termination", signed(spent_all), rb.clone(), tr.clone(), "termination", Reason::UnitMismatch));
{
let mut mixed = rs.clone();
let recs = standard(seg(42), stream(21));
mixed[2] = receipt(SINK, seg(42), stream(21), &recs);
let mut mr = tr.clone();
mr.extend(recs.iter().map(HopRecord::to_bytes));
let mt = Termination::over(stid, why::HIDDEN, 6_000_000, seg(41).index, &mixed);
out.push(refuse("termination_receipt_of_another_stream", "termination", signed(mt), mixed.iter().map(SegmentReceipt::to_bytes).collect(), mr, "segment/2", Reason::WrongStream));
}
{
let mut order = rs.clone();
order.swap(1, 2);
let mt = Termination::over(stid, why::HIDDEN, 6_000_000, seg(40).index, &order);
out.push(refuse("termination_receipts_out_of_order", "termination", signed(mt), order.iter().map(SegmentReceipt::to_bytes).collect(), tr.clone(), "segment/2", Reason::TerminationInvalid));
}
out.push(refuse("termination_bad_signature", "termination", {
let mut b = signed(t.clone());
let n = b.len();
b[n - 1] ^= 1;
b
}, rb.clone(), tr.clone(), "termination", Reason::BadSignature));
out
}
fn hexs(v: &[Vec<u8>]) -> Vec<String> {
v.iter().map(|b| hex(b)).collect()
}
fn vector_json(v: &Vector) -> Value {
let mut m = Map::new();
m.insert("name".into(), json!(v.name));
m.insert("kind".into(), json!(v.kind));
m.insert(if v.kind == "receipt" { "receipt" } else { "termination" }.into(), json!(hex(&v.subject)));
if v.kind == "termination" {
m.insert("receipts".into(), json!(hexs(&v.receipts)));
}
m.insert("records".into(), json!(hexs(&v.records)));
match &v.outcome {
Ok(facts) => {
m.insert("outcome".into(), json!("accept"));
m.insert("facts".into(), facts.clone());
}
Err((place, reason)) => {
m.insert("outcome".into(), json!("refuse"));
m.insert("reason".into(), json!(reason.code()));
m.insert("place".into(), json!(place));
}
}
Value::Object(m)
}
/// The corpus, as `energy-conformance/stream/vectors.json` holds it.
pub fn generate() -> String {
let vs = vectors();
for v in &vs {
let got = outcome_of(v);
let want = match &v.outcome {
Ok(f) => Ok(f.clone()),
Err((p, r)) => Err((p.clone(), *r)),
};
assert_eq!(got, want, "{}: the module does not give the outcome the vector was written for", v.name);
}
let keys: Map<String, Value> = KEYS.iter().map(|(n, i)| (n.to_string(), json!(hex(&key(*i).verifying_key().to_bytes())))).collect();
let doc = json!({
"format": "wai.stream.energy corpus v1",
"about": "extensions/stream-energy.md §8. Each vector is a stream-segment receipt (WSE1) or a termination record (WST1), with the receipts and hop figure records (WSH1) it names, as lowercase hex, and the outcome §6 gives it: accept with the facts every verifier recomputes, or refuse with the reason and the place. The figures are synthetic and claim nothing about any device.",
"keys": keys,
"vectors": vs.iter().map(vector_json).collect::<Vec<_>>(),
});
let mut s = serde_json::to_string_pretty(&doc).expect("serialise");
s.push('\n');
s
}
fn unhex(s: &str) -> Option<Vec<u8>> {
(s.len() % 2 == 0).then(|| (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect::<Option<Vec<u8>>>()).flatten()
}
/// Check a corpus file against this module: every vector gets the outcome it states.
pub fn verify(doc: &str) -> Vec<String> {
let mut fails = Vec::new();
let Ok(d) = serde_json::from_str::<Value>(doc) else { return vec!["the corpus does not parse".into()] };
for v in d["vectors"].as_array().into_iter().flatten() {
let name = v["name"].as_str().unwrap_or("?").to_string();
let kind = v["kind"].as_str().unwrap_or("");
let field = |k: &str| v[k].as_str().and_then(unhex).unwrap_or_default();
let list = |k: &str| v[k].as_array().into_iter().flatten().filter_map(|x| x.as_str().and_then(unhex)).collect::<Vec<_>>();
let vec = Vector {
name: "",
kind: if kind == "receipt" { "receipt" } else { "termination" },
subject: field(if kind == "receipt" { "receipt" } else { "termination" }),
receipts: list("receipts"),
records: list("records"),
outcome: Ok(Value::Null),
};
let got = outcome_of(&vec);
let ok = match (v["outcome"].as_str(), &got) {
(Some("accept"), Ok(f)) => *f == v["facts"],
(Some("refuse"), Err((p, r))) => v["reason"] == r.code() && v["place"] == p.as_str(),
_ => false,
};
if !ok {
fails.push(format!("{name}: got {got:?}"));
}
}
fails
}

View file

@ -0,0 +1,113 @@
use super::*;
use crate::display_caps::transfer;
fn key(i: u8) -> SigningKey {
SigningKey::from_bytes(&[i; 32])
}
const HD: Rendition = Rendition { width: 1920, height: 1080, frame_rate_mhz: 25_000, bit_depth: 8, transfer: transfer::SDR };
fn occ(ppm: u32) -> EnergyProvenance {
EnergyProvenance::on_chip_counter(Uncertainty::new(ppm, 2_000_000).unwrap(), CounterFiltering::Unknown).unwrap()
}
fn rec(role: u8, joules: u64, class: Option<EnergyProvenance>, cov: u8) -> HopRecord {
HopRecord {
role,
hop: [0; 32],
segment: [7; 32],
index: 3,
stream: if role >= role::DECODE { [9; 32] } else { [0; 32] },
deliveries: 1,
joules,
class,
coverage: cov,
profile: if role == role::DECODE { 1 } else { 0 },
fabrics: if role == role::DECODE { fabric::CPU } else { 0 },
rendition: HD,
duration_us: 2_000_000,
basis: None,
panel: None,
sig: [0; 64],
}
}
#[test]
fn a_record_round_trips_and_its_signature_covers_every_byte() {
let r = rec(role::DECODE, 500_000, Some(occ(20_000)), coverage::WHOLE).sign(&key(1));
let b = r.to_bytes();
assert_eq!(HopRecord::parse(&b), Ok(r.clone()));
for i in [5, 40, 120, b.len() - 70] {
let mut x = b.clone();
x[i] ^= 0x10;
assert!(HopRecord::parse(&x).is_err(), "byte {i}");
}
assert_eq!(given_digest(&b), r.digest());
}
#[test]
fn the_declared_power_rounds_half_up() {
assert_eq!(Basis { power_mw: 1, time_us: 500, source: 3 }.joules(), 1, "0.5 µJ rounds up");
assert_eq!(Basis { power_mw: 1, time_us: 499, source: 3 }.joules(), 0);
assert_eq!(Basis { power_mw: 98_000, time_us: 2_000_000, source: 1 }.joules(), 196_000_000, "98 W for 2 s is 196 J");
}
#[test]
#[allow(deprecated)]
fn the_legacy_class_is_refused_before_anything_else() {
let r = rec(role::RELAY, 900, Some(EnergyProvenance::HwShunt), coverage::WHOLE).sign(&key(2));
assert_eq!(HopRecord::parse(&r.to_bytes()), Err(Reason::LegacyClass));
}
#[test]
fn a_hardware_decode_on_the_media_engine_is_a_lower_bound() {
let mut r = rec(role::DECODE, 58_000, Some(occ(52_000)), coverage::WHOLE);
r.fabrics = fabric::CPU | fabric::MEDIA;
assert_eq!(r.clone().sign(&key(3)).rules(), Err(Reason::BoundOverstated));
r.coverage = coverage::PARTIAL;
let r = r.sign(&key(3));
assert_eq!(HopRecord::parse(&r.to_bytes()).map(|x| x.coverage), Ok(coverage::PARTIAL));
}
#[test]
fn an_estimate_needs_its_basis_and_a_display_is_always_one() {
let bare = rec(role::ORIGIN, 3_000, Some(EnergyProvenance::Estimator), coverage::WHOLE);
assert_eq!(bare.rules(), Err(Reason::BasisInvalid));
let mut with = bare.clone();
with.basis = Some(Basis { power_mw: 3, time_us: 1_000_000, source: 3 });
assert_eq!(with.rules(), Ok(()));
let mut counter_display = rec(role::DISPLAY, 196_000_000, Some(occ(30_000)), coverage::WHOLE);
counter_display.panel = Some(Panel { class: 1, display: Display { width: 3840, height: 2160, refresh_mhz: 60_000, bit_depth: 10, transfers: 7, diagonal_mm: 0, distance_mm: 0 }, brightness_ppm: 0 });
assert_eq!(counter_display.rules(), Err(Reason::BasisInvalid));
}
/// The delivery sum is labelled with its weakest class, never its strongest, and is
/// `Partial` whenever a part is unmetered or a lower bound.
#[test]
fn a_sum_reads_at_its_weakest_class() {
let origin = rec(role::ORIGIN, 3_000, Some(EnergyProvenance::ModelBased), coverage::WHOLE);
let relay = rec(role::RELAY, 900, Some(occ(40_000)), coverage::WHOLE);
let decode = rec(role::DECODE, 660_000, Some(occ(19_000)), coverage::WHOLE);
let all = [origin.clone(), relay.clone(), decode.clone()].map(|r| r.sign(&key(4)));
let r = SegmentReceipt::over([9; 32], [7; 32], 3, &all);
assert_eq!(r.delivery, Sum { joules: 663_900, class: 2, coverage: coverage::WHOLE });
let unmetered_relay = rec(role::RELAY, 0, None, coverage::NONE);
let part = [origin.clone(), unmetered_relay, decode.clone()].map(|r| r.sign(&key(4)));
let r = SegmentReceipt::over([9; 32], [7; 32], 3, &part);
assert_eq!((r.delivery.joules, r.delivery.coverage, r.unmetered), (663_000, coverage::PARTIAL, 1));
let no_origin = [relay, decode].map(|r| r.sign(&key(4)));
assert_eq!(SegmentReceipt::over([9; 32], [7; 32], 3, &no_origin).delivery.coverage, coverage::PARTIAL, "no origin figure: not the whole delivery");
}
#[test]
fn a_share_divides_by_deliveries_and_drops_the_remainder() {
let mut origin = rec(role::ORIGIN, 5_000_003, Some(EnergyProvenance::ModelBased), coverage::WHOLE);
origin.deliveries = 1_000;
assert_eq!(origin.share(), 5_000);
}
#[test]
fn every_reason_has_a_distinct_code() {
let codes: std::collections::BTreeSet<&str> = Reason::ALL.iter().map(|r| r.code()).collect();
assert_eq!(codes.len(), Reason::ALL.len());
}

View file

@ -0,0 +1,42 @@
//! Display capability (extensions/capability-constraints.md §6): the committed corpus
//! `constraints-conformance/display/vectors.json` regenerates byte for byte, every vector
//! and refusal gets the outcome it states, and the cases §6.5 names are in it.
use std::path::PathBuf;
use wai::display_caps::corpus;
fn committed() -> String {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../constraints-conformance/display/vectors.json");
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))
}
#[test]
fn the_committed_corpus_regenerates() {
assert!(committed() == corpus::generate(), "constraints-conformance/display/vectors.json differs from `wai_display_vectors gen`");
}
#[test]
fn every_vector_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
#[test]
fn the_corpus_shows_each_bit_and_the_fallback() {
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let vs = doc["vectors"].as_array().unwrap();
let get = |n: &str| vs.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"));
let mut bits = std::collections::BTreeSet::new();
for v in vs {
for r in v["renditions"].as_array().unwrap() {
for b in r["above"].as_array().unwrap() {
bits.insert(b.as_str().unwrap().to_string());
}
}
}
assert_eq!(bits.into_iter().collect::<Vec<_>>(), ["acuity", "depth", "rate", "size", "transfer"]);
assert_eq!(get("tv_4k_hdr_near")["selected"]["index"], 0, "a near 4K HDR panel takes the top rung");
assert_eq!(get("monitor_sdr_offered_hdr_only")["selected"]["within"], false, "nothing within: the first pickable, reported above");
assert!(get("nothing_pickable")["selected"].is_null());
assert_eq!(get("phone_portrait_hdr_no_distance")["selected"]["index"], 3, "the sides compared in the orientation that fits");
}

View file

@ -0,0 +1,78 @@
//! `wai.stream.energy` (extensions/stream-energy.md): the committed corpus
//! `energy-conformance/stream/vectors.json` regenerates byte for byte from its fixed seeds,
//! every vector in it gets the outcome it states, every reason the extension adds refuses a
//! vector, the accepted vectors are the cases §8 names, and §7 lists exactly the reasons the
//! reference reports.
#![cfg(feature = "stream_energy")]
use std::collections::BTreeSet;
use std::path::PathBuf;
use wai::stream_energy::{corpus, Reason};
fn read(p: &str) -> String {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join(p);
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{p}: {e}"))
}
fn committed() -> String {
read("energy-conformance/stream/vectors.json")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
assert!(committed() == corpus::generate(), "energy-conformance/stream/vectors.json differs from `wai_stream_energy_vectors gen`");
}
#[test]
fn every_vector_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
#[test]
fn the_corpus_covers_every_reason_and_case() {
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let vs = doc["vectors"].as_array().unwrap();
let seen: BTreeSet<&str> = vs.iter().filter_map(|v| v["reason"].as_str()).collect();
for r in Reason::ALL {
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
}
for r in ["truncated", "malformed", "unknown_version", "bad_signature"] {
assert!(seen.contains(r), "nothing in the corpus is refused {r}");
}
let get = |n: &str| vs.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"));
let facts = |n: &str| get(n)["facts"].clone();
assert_eq!(facts("four_hops_metered")["delivery"]["coverage"], "Whole");
assert_eq!(facts("four_hops_metered")["delivery"]["class"], "ModelBased", "the weakest class among the parts, not the strongest");
assert_eq!(facts("four_hops_metered")["display"]["class"], "Estimator");
assert_eq!(facts("media_engine_lower_bound")["delivery"]["coverage"], "Partial", "a media-engine decode is a lower bound");
assert_eq!(facts("relay_unmetered")["unmetered"], 1);
assert_eq!(facts("relay_unmetered")["delivery"]["coverage"], "Partial");
assert_eq!(facts("origin_shared")["shares"][0]["share"], 5_000);
assert_eq!(facts("phone_estimator")["delivery"]["class"], "Estimator");
assert_eq!(facts("display_unmetered")["display"]["class"], "unmetered");
assert_eq!(facts("above_panel")["panel"]["verdict"], "above");
assert_eq!(facts("above_panel")["panel"]["above"], serde_json::json!(["size", "depth", "transfer"]));
assert_eq!(facts("above_acuity")["panel"]["above"], serde_json::json!(["acuity"]));
assert_eq!(facts("four_hops_metered")["panel"]["verdict"], "within");
for r in ["hidden", "occluded", "display-off", "autoplay-off", "idle"] {
let n = format!("termination_{}", r.replace('-', "_"));
assert_eq!(get(&n)["outcome"], "accept", "{n}");
assert_eq!(get(&n)["facts"]["reason"], r, "{n}");
}
let t = facts("termination_hidden");
let display_held = &t["held"][3]["totals"][0]["joules_micro"];
let display_spent = &t["spent"][3]["totals"][0]["joules_micro"];
assert!(display_held.as_u64().unwrap() < display_spent.as_u64().unwrap(), "held covers the segments from from_index only");
}
/// §7 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = read("extensions/stream-energy.md");
let at = text.find("\n## 7. Reasons").expect("a §7");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, want, "§7's table and Reason::ALL differ");
}

View file

@ -11,7 +11,7 @@
// identical state hash because the math does, not because a server
// says so.
import init, { WorldSink, Actor, op_lamport } from './pkg/wai_web.js';
import init, { WorldSink, Actor, op_lamport, energy_text } from './pkg/wai_web.js';
const WORLD = { xMin: -9, xMax: 9, yMin: -1, yMax: 11 }; // ortho camera box
const TICK_HZ = 60;
@ -739,9 +739,10 @@ function chainHead(worldHash) {
// Honest energy figure: work_steps is the EXACT signed quantity; joules
// is an ESTIMATE from measured wall-clock compute at a declared power
// coefficient (a browser can't read RAPL — native sinks supply real
// joules via the sink-metering path). Labeled "est" so it's never
// mistaken for a measurement.
// coefficient (a browser reads no energy counter — native sinks supply
// measured joules through the sink-metering path). The receipt seals it
// `Estimator`, and the page shows it through `energy_text`, which writes
// the unmetered marker as "unmetered", never as zero joules.
const WATTS = 4.0; // declared coefficient for the estimate
let computeMs = 0; // accumulated wall-clock in the deterministic step loop
@ -765,10 +766,10 @@ ui.seal.addEventListener('click', () => {
// Advance the world's chain head.
localStorage.setItem(CHAIN_KEY(worldHash),
JSON.stringify({ receipt_hash: sealed.receipt_hash(), group_id: Number(groupId) }));
const mJ = (Number(joulesMicro) / 1000).toFixed(1);
const energy = energy_text(joulesMicro, joulesMicro > 0n ? 'Estimator' : '', false);
ui.receipt.textContent =
`chain #${groupId}${head ? ' ↳linked' : ''} · root ${sealed.root_hash().slice(0, 10)}… · `
+ `${sealed.work_steps()} steps · ≈${mJ} mJ est@${WATTS}W · ${reverified ? '✓ re-verified' : '✗ FAILED'}`;
+ `${sealed.work_steps()} steps · ${energy}${joulesMicro > 0n ? ` (compute time × a declared ${WATTS} W)` : ''} · ${reverified ? '✓ re-verified' : '✗ FAILED'}`;
} catch (e) {
ui.receipt.textContent = '✗ ' + (e.message || e);
}

View file

@ -215,6 +215,20 @@ pub fn known_capabilities() -> Box<[JsValue]> {
].iter().map(|s| JsValue::from_str(s)).collect()
}
/// The text a page shows for an energy figure (`wai::energy_class::Presented`): a figure
/// of `joules_micro` with the class `class_label` (`""` for none), `partial` when it is a
/// lower bound. The unmetered marker reads `unmetered`, never zero joules; a class beside a
/// zero figure, or a label this sink does not know, is an error, never a number.
#[wasm_bindgen]
pub fn energy_text(joules_micro: u64, class_label: &str, partial: bool) -> Result<String, JsError> {
use wai::energy_class::{EnergyProvenance, Presented};
let class = match class_label {
"" => None,
l => Some(EnergyProvenance::from_label(l).map_err(|e| JsError::new(&format!("{l}: {e}")))?),
};
Presented::of(joules_micro, class.as_ref(), partial).map(|p| p.to_string()).map_err(|e| JsError::new(&e.to_string()))
}
/// Whether `cap` is a registered **candidate** capability: registered, but with no
/// payload format pinned, so an encoder MUST NOT emit it and a sink treats it as
/// unsupported (SPEC §5, *Candidate capabilities*). The registry's own predicate,

View file

@ -391,6 +391,14 @@ fn seal_with<M: Sim>(
let receipt = SessionReceipt::build(
publisher, signer_id, wh, group_id, &c.ops, &checkpoints, joules_micro, work, parent,
);
// The page's figure is wall-clock time at a declared power, a constant nobody
// calibrated on this device: an `Estimator` (energy-measurement §2), labelled so that
// no reader takes it for a measurement. Zero stays the unmetered marker, unlabelled.
let receipt = if joules_micro > 0 {
receipt.with_energy_class(publisher, wai::energy_class::EnergyProvenance::Estimator).map_err(js_err)?
} else {
receipt
};
Ok(SealedSession {
replay: c.to_bytes(),
@ -416,8 +424,8 @@ impl WorldSink {
/// `wai.world.replay` plus its §8 receipt, signed by `publisher`.
///
/// `joules_micro` is **carried, never invented** — the page passes a
/// measured wall-clock estimate (with a declared coefficient) or `0`
/// when unmetered. `group_id` + `parent_hex` chain this seal onto the
/// wall-clock estimate at a declared power, sealed `Estimator`, or `0`
/// when unmetered, sealed as the unmetered marker. `group_id` + `parent_hex` chain this seal onto the
/// world's prior receipt (empty `parent_hex` starts a fresh chain).
/// The exact deterministic work figure is computed here, not passed.
pub fn seal_session(
@ -567,3 +575,32 @@ fn kin_scalar(_n: &KinNode) -> f32 {
// Silence the unused import when only some sims produce these types.
#[allow(unused)]
fn _types(_b: &BalNode) {}
#[cfg(test)]
mod tests {
use super::*;
const VECTOR: &[u8] = include_bytes!("../../world-conformance/vectors/basic_kinematics.wwld");
fn sealed(joules_micro: u64) -> SealedSession {
let mut sink = WorldSink::new(VECTOR).ok().expect("the committed vector opens");
let t = sink.final_tick();
sink.advance_to(t).ok().expect("advances");
sink.seal_session(&[7u8; 32], "did:example:wai-world", joules_micro, 0, "").ok().expect("seals")
}
/// The page's estimate is sealed `Estimator`, and a zero is the unmetered marker,
/// unlabelled (energy-measurement §2, §2.8): no seal of this demo states a figure with
/// no class, and none states zero joules as a figure.
#[test]
fn the_demo_seals_its_estimate_as_an_estimate_and_zero_as_unmetered() {
let est: serde_json::Value = serde_json::from_str(&sealed(4_000).receipt_json).unwrap();
assert_eq!(est["energy_provenance"], "Estimator");
assert_eq!(est["joules_micro"], 4_000);
let unmetered: serde_json::Value = serde_json::from_str(&sealed(0).receipt_json).unwrap();
assert!(unmetered.get("energy_provenance").is_none());
assert_eq!(unmetered["joules_micro"], 0);
assert_eq!(crate::energy_text(0, "", false).ok().unwrap(), "unmetered");
assert_eq!(crate::energy_text(4_000, "Estimator", false).ok().unwrap(), "4.000 mJ, Estimator");
}
}