open-standards/wai/extensions/exact-tonemap.md
Transaction Science 3ba5bccf78
Some checks failed
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 5s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 7s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 47s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 40s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m53s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 5m11s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 12m28s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 12m24s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 56s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 42s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 28s
WAI operation catalogue / does this change touch it (pull_request) Successful in 22s
WAI conformance / does this change touch it (pull_request) Successful in 28s
WAI energy receipts / does this change touch it (pull_request) Successful in 26s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 36s
wai-fleet service / does this change touch it (pull_request) Successful in 36s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 30s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been skipped
wai-key-proxy / does this change touch it (pull_request) Successful in 29s
wai-ledger service / does this change touch it (pull_request) Successful in 37s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 35s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 37s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 38s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 30s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 32s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m4s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 6m25s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 35s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 36s
WAI Studio / does this change touch it (pull_request) Successful in 24s
wai-web wasm32 / does this change touch it (pull_request) Successful in 26s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 2m47s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 6m11s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m54s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m25s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m49s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 20s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 51s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 13m11s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m48s
wai-fleet service / service · macos (pull_request) Successful in 37s
wai-ledger service / service · macos (pull_request) Successful in 47s
wai-pack / wai-pack · macos (pull_request) Successful in 2m24s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m37s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m16s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 8m2s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m28s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m26s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 5m16s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m41s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m51s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m20s
wai-fleet service / service · linux (pull_request) Successful in 1m45s
wai-key-proxy / proxy · linux (pull_request) Successful in 1m4s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 18m45s
wai-ledger service / service · linux (pull_request) Successful in 1m46s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 11m5s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 3m19s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Failing after 12m57s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m7s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 8s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 10m28s
wai-pack / wai-pack · linux (pull_request) Successful in 2m4s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 17m14s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Failing after 17s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m7s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m53s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 14m53s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m41s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m16s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 17m7s
WAI Studio / Studio in WebKit · linux (pull_request) Failing after 10m52s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been cancelled
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Has been cancelled
wai upscale and wai tonemap: an exact derivation from the command line, with its record and its check
The catalogue lists what works, and the toolchain could verify an upscale
record and a rendition record without any door that made one. The doing lived
in a hosted service's providers, which is the wrong side of the line: WAI holds
every open ability.

`wai upscale run` and `wai tonemap run` make the picture and, with `--record`,
sign the record. `wai upscale verify` and `wai tonemap verify` recompute that
record under require-proof from what it names, and hold the output to it.
`exact_upscale::make` and `exact_tonemap::make` are the functions under them,
and they return the record's body for a caller to sign, so a service that signs
with its own key can use them too.

The commands are `run` and `verify` because the catalogue refuses a command that
is the start of another: `wai upscale` beside `wai upscale verify` is a form
that can never be told apart from a file named verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 08:43:49 -05:00

39 KiB
Raw Permalink Blame History

WAI Extension: Exact Tone Mapping

Status: Draft. Registers two companions and a record. wai.meta.gainmap carries a gain map of ISO 21496-1:2025 (its metadata and its gain map image) as a companion of an image payload, and wai.meta.st2094_50 carries the dynamic metadata of SMPTE ST 2094-50:2026-08 (Application #5) as a companion of a video payload. A sink presents the picture it decoded at its display's headroom, the ratio of its peak to the luminance of HDR reference white, as a log2 value. Both standards state the computation from the base, the companion and the headroom in real numbers, so two sinks at one headroom compute it in their own floating point and present different pixels. This extension pins it in integer arithmetic: wai-tonemap:gainmap and wai-tonemap:st2094-50 map the base's RGB8 samples, the companion's bytes and a headroom, an integer in units of 1/10000 of a stop, to one RGB16 rendition, with a stated fixed-point log2, a stated fixed-point exp2, and a stated rounding at every step. Every sink at a headroom presents the same bytes, and a verifier holding the base and the companion recomputes them. A sink that presents a rendition signs a rendition record naming the base, the companion, each headroom and the digest of each rendition. The derivation is a convert step of synthesis: it rebuilds what the base and the companion hold and generates nothing. No new transport. The reference implementation is the exact_tonemap module of wai-rs (feature exact_tonemap); the corpus is live-conformance/tonemap/, and its second implementation is live-conformance/tonemap_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: SMPTE ST 2094-50:2026-08, Dynamic Metadata for Color Volume Transform — Application #5, approved 2026-08-24, in its published text, every clause and annex (its figures were not read); the WAI texts this one cites; RFC 8032 and RFC 8785. ISO 21496-1:2025 (first edition, Digital photography — Gain map metadata for image conversion — Part 1: Dynamic range conversion) was not read: its text is sold, and no part of it was obtained for this revision. What this extension states of it is what ST 2094-50:2026-08 says of it (its terms 3.1 to 3.11 cite ISO 21496-1:2025 clauses 3.1, 3.2, 3.4, 3.6, 5.2.6, 5.2.7 and 6.3) and what public, open-source readers of its binary metadata implement: the field list of §4.2 and the computation of §4.5. Each is marked where it is used. A revision that reads the standard confirms each against its text or corrects it.

1. Scope

In scope: the payloads of the two companions (§4.1, §5.1) and how their metadata reads (§4.2, §5.2); the integer arithmetic the derivations share (§3); the two derivations (§4.4, §5.3 to §5.6), each step mapped onto the standard's own computation (§4.5, §5.7); their error against a floating-point evaluation of the same formulas (§6); the rendition record and its place in synthesis's class lattice (§7); verification (§8).

Out of scope: a transport; how a sink measures its own headroom; a base in another transfer than sRGB, a gain application colour space whose primaries differ from the base's, and the HDR reference white's luminance in candelas, each of which this revision does not compute (§4.2, §5.2); how a gain map is made from an HDR original, which is not part of the derivation (the corpus's encoder is the reference's own, §10); the presentation of a rendition on a panel, which takes the rendition's code values as linear drive values relative to the display's peak (§3.5) and does with them what the panel does.

Relationship to the core spec: wai.meta.gainmap and wai.meta.st2094_50 are companion-only capabilities (SPEC §5, §7.1): each is applied with the rendition it binds to and never decoded on its own. wai.meta.gainmap applies to a payload of the image media class, wai.meta.st2094_50 to one of the video media class, as SPEC §7.1's rule 2 states. wai.media.tonemap is a record (SPEC §5): a signed record a WAI tool emits and verifies, which names no payload format and is not dispatchable. The two derivations are terms of this extension, not wai.derive.* strings: the registry's derivation kind is the operations of ISO/IEC 23001-16 (derived-tracks), and a tone map is none of them.

Relationship to synthesis: a rendition is a convert of its inputs (synthesis §3.2): it rebuilds, at the display's headroom, what the base and the companion together hold, names no model and generates nothing; its class is the greatest of reconstructed and its inputs' classes, so a rendition of a synthesized picture is synthesized (§7.2).

Relationship to determinism-tiers: display adaptation changes the decoded medium, so a tier covers a rendition only if its computation is pinned (determinism-tiers §2, What a tier covers). The derivations are integer throughout, so a rendition is a pure function of the base's samples, the companion's bytes and the headroom, and a sink that presents it holds the tier of the decode it ran on.

2. Records and buffers

The record of this extension (§7) is a JSON object under the encodings of synthesis §2: byte strings in lowercase hexadecimal, unsigned integers of at most 2^53 − 1 in plain decimal, no other number, no boolean, terms and date-times as jwp-receipts §2.4.3 defines them, no member named twice, at most 32 levels of nesting, exactly the members §7.1 lists at every level, and a kind naming the record; one that does not is record_malformed. It carries signer_pubkey (32 bytes) and sig (64 bytes):

sig       = Ed25519(signer_key, "wai:tonemap\x01" || JCS(record without "sig"))
link_hash = BLAKE3("wai:tonemap-link\x01" || JCS(record))

JCS is RFC 8785. The signature is verified strictly (SPEC §7 Signatures): a key that fails step 1 or 2 is bad_key, and a signature that fails a later step bad_signature.

A base picture is an RGB8 buffer (rows top to bottom, pixels left to right, three bytes a pixel, R, G, B, no padding), read as sRGB-coded (IEC 61966-2-1, as ST 2094-50:2026-08 Formula (A.2) restates it). A rendition is an RGB16 buffer: the same order, three unsigned 16-bit samples a pixel, each written little-endian. A buffer's digest is SHA-256 (FIPS 180-4) over it. An envelope's content hash is jwp-receipts §2.1's.

3. Integer arithmetic

3.1 Notation

Every value is an integer. ⌊a / b⌋, for b > 0, is the floor of the quotient, also for a negative a. rnd(a, b) = ⌊(a + ⌊b / 2⌋) / b⌋ for b > 0: the integer nearest a / b, halves upward, for either sign of a. clamp(v, lo, hi) = min(max(v, lo), hi). A value in Qn stands for itself divided by 2^n: a log2 value in Q16 is in units of 1/65536 of a stop, a linear light value in Q30 is in units of 2^−30 of HDR reference white. No intermediate value of this section or of §4 and §5 needs more than 127 bits and a sign; an implementation computes each exactly.

3.2 L2: a fixed-point log2

For an integer v ≥ 1, L2(v) is a Q16 approximation of log2(v) from below:

  1. n is the bit length of v less 1, so 2^n ≤ v < 2^(n+1).
  2. m = v · 2^(30 − n) where n ≤ 30, and ⌊v / 2^(n − 30)⌋ otherwise: the mantissa in Q30, 2^30 ≤ m < 2^31.
  3. f = 0; for i = 1 to 16 in turn: m = ⌊m² / 2^30⌋; where m ≥ 2^31, m = ⌊m / 2⌋ and f = f + 2^(16 − i).
  4. L2(v) = n · 2^16 + f.

Step 3 is the bitwise logarithm: each squaring doubles the fractional part, and a mantissa past 2 gives the next bit. Every squaring truncates.

3.3 E2: a fixed-point exp2, and scaling by it

For a Q16 integer y, E2(y) is a pair (p, e) with 2^(y / 2^16) ≈ p · 2^(e − 30):

  1. e = ⌊y / 2^16⌋ and f = y − 2^16 · e, 0 ≤ f < 2^16.
  2. p = 2^30; for i = 1 to 16 in turn, where bit 16 − i of f is 1, p = rnd(p · Kᵢ, 2^30).

Kᵢ is 2^(2^−i) in Q30, made by integer square roots: R₀ = 2^61, Rᵢ = ⌊√(Rᵢ₋₁ · 2^60)⌋ (each a Q60 value, the integer square root rounded down), and Kᵢ = rnd(Rᵢ, 2^30):

i Kᵢ i Kᵢ i Kᵢ i Kᵢ
1 1518500250 5 1097253708 9 1075196443 13 1073832680
2 1276901417 6 1085434106 10 1074468888 14 1073787251
3 1170923762 7 1079572136 11 1074105294 15 1073764537
4 1121280436 8 1076653033 12 1073923544 16 1073753181

scale(X, y, F), for an integer X and a Q16 y, is X · 2^(y / 2^16) / 2^F as an integer: with (p, e) = E2(y) and t = e − 30 − F, it is X · p · 2^t where t ≥ 0, and rnd(X · p, 2^−t) otherwise.

Since both functions are functions of one integer, an implementation MAY tabulate them, or compute them any other way that gives the same integers: a table of E2 over the 2^16 values of f is the same function.

3.4 S: the base's samples in linear light

S[k], for each sample value k from 0 to 255, is the sRGB decode of k / 255 in Q30, with HDR reference white at 2^30 (an sRGB-coded sample of 255 is reference white, ST 2094-50:2026-08 Note 4 to Formula (A.1)):

  • for k ≤ 10: S[k] = rnd(k · 100 · 2^30, 255 · 1292), the linear segment k / 255 / 12.92 (k / 255 ≤ 0.04045 exactly when k ≤ 10);
  • for k ≥ 11: with λ = L2(1000 · k + 14025) − L2(269025), the Q16 log2 of (k / 255 + 0.055) / 1.055, S[k] = scale(2^30, rnd(12 · λ, 5), 0): that value to the power 2.4.

S[255] = 2^30 exactly.

3.5 The rendition's code values

A sink presents a rendition to a display of headroom h, an integer from 0 to 60000 in units of 1/10000 of a stop (the unit ST 2094-50:2026-08 Annex C encodes headrooms in): the display's peak is 2^(h / 10000) times HDR reference white. Hₜ = rnd(h · 2^16, 10000) is h in Q16. A rendition's code value is linear light relative to the display's peak, at 16 bits: a linear value T (Q30, relative to reference white) is presented as

code(T) = clamp(scale(T · 65535, −Hₜ, 30), 0, 65535),

so the display's peak is 65535 and a value past it is clamped, the projection of ST 2094-50:2026-08 Clause A.4, Note. Each derivation forms its value and its exponent together and rounds once (§4.4 step 7, §5.6).

4. Gain maps: wai.meta.gainmap and wai-tonemap:gainmap

4.1 The payload: WGM1

A wai.meta.gainmap payload is a WGM1 frame. WAI's own fields are little-endian.

off size field rule
0 4 magic WGM1
4 1 version 1
5 1 reserved 0
6 2 md_len 1 to 4096: the metadata's length
8 md_len metadata ISO 21496-1:2025 metadata in its binary form (§4.2), as written
8 + md_len the rest gain map image one PNG file (W3C PNG, Third Edition) of 8-bit samples, colour type 0 (one channel) or 2 (three), no interlace

A frame that breaks a rule of this table, or whose image is not such a PNG, or does not decode, is companion_malformed. Nothing follows the PNG's IEND chunk.

4.2 The metadata

The metadata is ISO 21496-1:2025's binary form. This revision did not read ISO 21496-1; the fields below are the ones public, open-source readers of the form read, in their order, with the names they give. Its multi-byte fields are big-endian.

size field type
2 minimum_version unsigned
2 writer_version unsigned
1 is_multichannel (bit 7), use_base_colour_space (bit 6), six reserved bits
4 + 4 base_hdr_headroom numerator, denominator unsigned, unsigned
4 + 4 alternate_hdr_headroom numerator, denominator unsigned, unsigned
per channel, 1 or 3 times (3 where is_multichannel is 1)
4 + 4 gain_map_min numerator, denominator signed, unsigned
4 + 4 gain_map_max numerator, denominator signed, unsigned
4 + 4 gamma numerator, denominator unsigned, unsigned
4 + 4 base_offset numerator, denominator signed, unsigned
4 + 4 alternate_offset numerator, denominator signed, unsigned

Each pair is the rational numerator / denominator. Headrooms, gain_map_min and gain_map_max are log2 values. A reader, in this order:

  1. refuses metadata whose minimum_version is not 0 with metadata_unsupported: a reader does not apply metadata whose minimum version it does not know;
  2. refuses metadata shorter than its fields, or whose writer_version is below its minimum_version, or, with a writer_version of 0, longer than its fields, with metadata_malformed; bytes after the fields of metadata of a later writer_version are read past, and the reserved bits are not read;
  3. refuses with metadata_out_of_range metadata outside the range this revision computes: a denominator of 0; a headroom past 6 (the range of ST 2094-50:2026-08 §6.2.2); two equal headrooms, between which no weight is defined; a gain_map_min or gain_map_max past 16 in magnitude, or a gain_map_min greater than its gain_map_max; a gamma of 0 or outside 1/64 to 64; a base_offset or alternate_offset past 1 in magnitude;
  4. refuses metadata whose use_base_colour_space is 0 with gain_space_unsupported: its gain is applied in the alternate image's colour space, which a conversion of primaries reaches, and this revision computes none.

Then, for each channel j: minⱼ and maxⱼ in Q16 are rnd(n · 2^16, d) of gain_map_min and gain_map_max; boⱼ and aoⱼ in Q30 are rnd(n · 2^30, d) of base_offset and alternate_offset; γⱼ is the rational gamma itself. A single-channel map applies its one set to all three channels.

4.3 The gain map image

The PNG's colour type is 0 for metadata whose is_multichannel is 0, and 2 where it is 1 (map_channels_mismatch). Its size is gw × gh, and the base's w × h: s = w / gw is an integer of 1, 2, 4 or 8, and h = s · gh (map_dimensions_mismatch). g(x, y) is its sample at column x and row y, of channel j.

4.4 wai-tonemap:gainmap

wai-tonemap:gainmap maps a base of w × h, a wai.meta.gainmap payload that reads (§4.1 to §4.3) and a headroom hᵈ (§3.5) to a rendition of w × h. With Hb = base_hdr_headroom = bn / bd, Ha = alternate_hdr_headroom = an / ad and Hₜ = rnd(hᵈ · 2^16, 10000):

  1. Weight. A = hᵈ · bd · ad − 10000 · bn · ad and D = 10000 · (an · bd − bn · ad), so A / D = (hᵈ / 10000 − Hb) / (Ha − Hb). Where D > 0, W = clamp(rnd(A · 2^16, D), 0, 2^16); where D < 0, W = −clamp(rnd(−A · 2^16, −D), 0, 2^16). W is in Q16, and is negative where the alternate's headroom is below the base's.

For each pixel (x, y) and each of its channels c, with j = c for a three-channel map and j = 0 for a single-channel one:

  1. Base. B = S[base(x, y)[c]].
  2. The map's sample at the pixel. X = max(0, 2x + 1 − s), x₀ = ⌊X / 2s⌋, ax = X − 2s · x₀ and x₁ = min(x₀ + 1, gw − 1); Y, y₀, ay and y₁ the same from y and gh. Then N = g(x₀, y₀) · (2s − ax) · (2s − ay) + g(x₁, y₀) · ax · (2s − ay) + g(x₀, y₁) · (2s − ax) · ay + g(x₁, y₁) · ax · ay, and Q = 255 · 4s²: the map's normalised sample is N / Q, a centre-aligned bilinear interpolation of its codes, the edges repeated. With s = 1 it is the sample itself.
  3. Recovery. v (Q30) = rnd(N · 2^30, Q) where γⱼ = 1; 0 where N = 0; otherwise, with γⱼ = γn / γd, λ = L2(N) − L2(Q) and v = scale(2^30, rnd(λ · γd, γn), 0): the sample raised to 1/γⱼ.
  4. Log gain. L (Q16) = rnd(minⱼ · (2^30 − v) + maxⱼ · v, 2^30).
  5. Weighted gain. G (Q16) = rnd(L · W, 2^16).
  6. Output. P = scale((B + boⱼ) · 65535, G − Hₜ, 0) − scale(aoⱼ · 65535, −Hₜ, 0), and the rendition's sample is clamp(rnd(P, 2^30), 0, 65535): code((B + bo) · 2^(L · W) − ao) of §3.5, both terms in Q30 of a code value and rounded once.

The rendition at a headroom at or past the alternate's is the alternate image the map was made from, within the error of §6; at the base's headroom (W = 0) it is the base, offsets cancelling where they are equal.

4.5 Against ISO 21496-1's computation

The computation the integer steps reproduce is the one the public readers of §4.2 implement: with the base's linear value B, the map's sample g in [0, 1] at the pixel, and the weight W = clamp((Hₜ − Hb) / (Ha − Hb), 0, 1), negated where Ha < Hb,

L = min + (max − min) · g^(1/γ), T = (B + bo) · 2^(L · W) − ao.

ST 2094-50:2026-08 term 3.7 names ISO 21496-1:2025 clause 6.3 as the algorithm that produces an image at a targeted HDR headroom from per-pixel metadata; this revision did not read that clause (header note). Step by step:

step real-number form integer form (§4.4) what is pinned
base to linear sRGB decode of k / 255 S[k] (§3.4) L2, E2, one rounding each
the map at the pixel upsampled by a filter each implementation chooses step 3: bilinear, centre-aligned, edges repeated, exact the filter itself
recovery g^(1/γ) step 4 Q30, through Q16 log2
log gain min + (max − min) · g^(1/γ) step 5 Q16
weight clamp of a ratio of headrooms step 1, exact rational, rounded once Q16
gain and offsets (B + bo) · 2^(L · W) − ao step 7 Q30, through Q16 exp2
presentation T / 2^Hₜ at the display's peak §3.5 16-bit code values

The upsampling filter is where two floating-point sinks differ most, and the standard, as far as this revision knows it, leaves it to the implementation. The derivation fixes it.

5. ST 2094-50: wai.meta.st2094_50 and wai-tonemap:st2094-50

5.1 The payload: WD50

A wai.meta.st2094_50 payload is a WD50 frame: the metadata sets of a video, each with the frames it applies to. ST 2094-50:2026-08 carries one metadata set with the frames it applies to by ITU-T T.35 messages in a codec or a container (its §7.3), and its Annex C encoding holds no time; the frame gives each set its frames, the TimeInterval of its §7.1, in frames of the base. WAI's own fields are little-endian.

off size field rule
0 4 magic WD50
4 1 version 1
5 1 reserved 0
6 2 n_sets 1 to 256
8 … the sets n_sets sets, one after another; nothing follows the last

A set:

size field rule
4 first_frame the index of its first frame, frames counted from 0
4 frame_count 1 or more
2 len 1 to 4096
len the set one smpte_st_2094_50_application_info() of ST 2094-50:2026-08 Annex C, as written

Each set's first_frame is at least the end of the set before it (its first_frame plus its frame_count), and no end passes 2^32 − 1. A frame that breaks a rule of these tables is companion_malformed.

5.2 Reading a metadata set

A set reads as ST 2094-50:2026-08 Annex C has it: each syntax element of type u(N) an N-bit unsigned integer, most significant bit first, and the structures of its Tables C.1 to C.5 in order. The reader, in this order:

  1. refuses a set whose minimum_application_version is not 0 with metadata_unsupported: Annex C.2.1 has a parser ignore the whole bitstream;
  2. refuses a set that ends inside a structure, or holds a reserved_zero element that is not 0 (Annex C.1), with metadata_malformed. Bytes after the last element are padding and are not read (Annex C.2.1);
  3. assigns the metadata items as Annex C.3 does: HDR reference white, 203 or clamp(·, 1, 50000) / 5 (C.3.3); headrooms min(·, 60000) / 10000 (C.3.4); NumAlternateImages min(·, 4); component mixing (C.3.6); gain curves, with the sign s of C.3.7, x = min(·, 64000) / 1000 and y = s · min(·, 60000) / 10000, and each slope from its angle or by C.3.9 (§5.4);
  4. refuses with metadata_out_of_range a set that breaks a constraint of ST 2094-50:2026-08 §6.2.2, §6.4.2 or §6.5.2 (an alternate headroom equal to the baseline's, alternate headrooms not increasing, mixing weights that sum to 0, control points whose x decreases, or two of one x with different y), or whose gain curve's last control point has x = 0, beyond which Formula (12) is not finite;
  5. refuses with gain_space_unsupported a set whose use_reference_white_tone_mapping_flag is 1, whose gain application colour space C.3.8 sets to the primaries of ITU-R BT.2020, or whose gain_application_space_chromaticities_mode is not 0, the primaries and white point of ITU-R BT.709: this revision applies gains in the base's primaries, those of sRGB, and converts none.

The recommended constraints of §6.2.3 are not checked: §6.2.3 lets an implementation adjust metadata to meet them, and the derivation applies the metadata as it reads.

For a base read as sRGB-coded (§2), the conversion of ST 2094-50:2026-08 Formula (A.3) divides the luminance of Formula (A.2) by the HDR reference white it was formed with, so the value in the gain application colour space is S[k] of §3.4 and the reference white's value in candelas does not enter. A set with no headroom-adaptive tone map (has_adaptive_tone_map_flag 0) leaves the choice to the output system (its Clause A.3); the derivation then applies none, as a set with no alternate images does (its Clause B.4): the rendition is the base, projected (§3.5).

5.3 The parameters as integers

  • Headrooms H in units of 1/10000 of a stop: the Annex C values, min(·, 60000).
  • Mixing weights in units of 1/150000: type 0, kmax = 150000; type 1, kcomponent = 150000; type 2, kred = kgreen = kblue = 25000 and kmax = 75000; type 3, each coefficient 3 · min(·, 50000); every other weight 0. ksum is their sum.
  • Control points: Xᵢ = min(·, 64000), x in units of 1/1000; Yᵢ = s · min(·, 60000), y in units of 1/10000; slopes mᵢ in Q30 (§5.4).

5.4 Slopes

From an angle (C.3.7, gain_curve_use_pchip_slope_flag 0): m = tan((clamp(θ, 1, 35999) − 18000) · π / 36000). With d = clamp(θ, 1, 35999) − 18000, P = ⌊π · 2^62⌋ = 14488038916154245684 and the angle in Q62 a = rnd(d · P, 36000), and a² in Q62 q = rnd(a · a, 2^62):

  • sine: t = a, σ = a; for k = 1 to 15 in turn, t = rnd(−t · q, 2^62 · 2k · (2k + 1)) and σ = σ + t;
  • cosine: t = 2^62, κ = 2^62; for k = 1 to 15 in turn, t = rnd(−t · q, 2^62 · (2k −
    1. · 2k) and κ = κ + t;
  • m = rnd(σ · 2^30, κ).

The angle is within π/2 of 0, so κ > 0, and fifteen terms of each series leave a remainder below 2^−62.

By PCHIP (C.3.9, flag 1): each slope of C.3.9 is a rational function of the control points, computed exactly, and rounded once to Q30. With hᵢ = Xᵢ₊₁ − Xᵢ and Δᵢ = Yᵢ₊₁ − Yᵢ, the slope of interval i is sᵢ = Δᵢ / (10 · hᵢ) stops per unit of x, and 0 where hᵢ = 0 (Formula (C.7)). The interior rule (C.8), the endpoint rules (C.9, C.10) with their sign tests and their cap at 3 · |s|, and the degenerate case are C.3.9's, each comparison made on the exact rationals; the slope chosen, a rational, is m = rnd(numerator · 2^30, denominator) with a positive denominator.

5.5 Component mixing and the gain curve

Mixing (Formulas (9), (10)): for a colour C = (cr, cg, cb) in Q30,

Mⱼ = rnd(cⱼ · kcomponent + kred · cr + kgreen · cg + kblue · cb + kmax · max(C) + kmin · min(C), ksum), in Q30.

The gain curve (Formulas (11), (12)) at M in Q30, against 1000 · M and Xᵢ · 2^30, its value G in Q16:

  • 1000 · M < X₀ · 2^30: G = rnd(Y₀ · 2^16, 10000);
  • 1000 · M = Xᵢ · 2^30 for some i: G = rnd(Yᵢ · 2^16, 10000);
  • Xᵢ · 2^30 < 1000 · M < Xᵢ₊₁ · 2^30: the cubic of Formula (11). With t = rnd(1000 · M − Xᵢ · 2^30, hᵢ) (Q30), Y′ᵢ = rnd(Yᵢ · 2^30, 10000) (Q30) and m̂ᵢ = rnd(hᵢ · mᵢ, 1000) (Q30), the coefficients c₃ = 2Y′ᵢ + m̂ᵢ − 2Y′ᵢ₊₁ + m̂ᵢ₊₁, c₂ = −3Y′ᵢ + 3Y′ᵢ₊₁ − 2m̂ᵢ − m̂ᵢ₊₁, c₁ = m̂ᵢ and c₀ = Y′ᵢ are evaluated by Horner's rule, u = rnd(c₃ · t, 2^30) + c₂, then u = rnd(u · t, 2^30) + c₁, then u = rnd(u · t, 2^30) + c₀, and G = rnd(u, 2^14);
  • 1000 · M > Xₙ₋₁ · 2^30, past the last control point: G = rnd(Yₙ₋₁ · 2^16, 10000) + L2(Xₙ₋₁ · 2^30) − L2(1000 · M), the term log2(xₙ₋₁ / x) that holds the tone curve x · 2^G level.

The colour gain function of an alternate image is the curve at each of M's three components (Formula (7)); the zero function's is 0 (Formula (8)).

5.6 wai-tonemap:st2094-50

wai-tonemap:st2094-50 maps a frame of a base of w × h, a wai.meta.st2094_50 payload that reads (§5.1, §5.2), the set whose frames include the frame's index (frame_uncovered where none does), and a headroom hᵈ (§3.5) to a rendition of w × h. Following §6.2.5: the baseline headroom is placed among the alternates' in sorted position, H₀ < … < Hₙ₋₁, with the zero gain function at its place; hc = clamp(hᵈ, H₀, Hₙ₋₁). For each pixel, C = (S[r], S[g], S[b]) of its samples, and the gain G of each component (Q16) is:

  • Gainᵢ(C) where hc = Hᵢ (Formula (2));
  • otherwise, for the one i with Hᵢ < hc < Hᵢ₊₁, rnd(Gainᵢ(C) · (Hᵢ₊₁ − hc) + Gainᵢ₊₁(C) · (hc − Hᵢ), Hᵢ₊₁ − Hᵢ) (Formulas (3), (4), exact in the weights and rounded once).

Each component of the rendition is clamp(scale(c · 65535, G − Hₜ, 30), 0, 65535) with Hₜ = rnd(hᵈ · 2^16, 10000): code(C · 2^G) of §3.5 (Formula (5)), rounded once.

5.7 Against ST 2094-50's computation

ST 2094-50:2026-08 real-number form integer form what is pinned
Formula (A.2), (A.3) sRGB decode, over reference white S[k] (§3.4) L2, E2
C.3.7 m = tan of the angle §5.4, series in Q62, m in Q30 the series and its rounding
C.3.9 PCHIP slopes §5.4, exact rationals, rounded once Q30
(9), (10) weighted mix over ksum §5.5, rounded once Q30
(11) cubic Hermite in t §5.5, Horner in Q30 each product's rounding
(12) y + log2(xₙ₋₁ / x) L2 difference Q16
(2) to (4) weights of headrooms §5.6, exact, rounded once Q16
(5) and A.4 C · 2^G, projected at the display §5.6 and §3.5 16-bit code values

Every formula of §6 of ST 2094-50:2026-08 has an integer step above, and every value in it is the one Annex C assigns from the bits.

6. Error against floating point

The derivations round where the standards compute in real numbers, so a rendition differs from a floating-point evaluation of the same formulas by a little. The reference measures it over every rendition of the corpus: the same steps in IEEE 754 binary64 (sRGB decoding by pow, log2, exp2 and tan of the platform library, the same bilinear weights, the PCHIP slopes in binary64), each code value rounded to nearest and clamped as §3.5 has it, against the integer rendition's:

derivation renditions samples largest difference mean difference
wai-tonemap:gainmap 10 41472 2 0.1269
wai-tonemap:st2094-50 25 172800 2 0.1642

in code values of 65535. The corpus states the same figures (measurement), and its test fails when they change. A difference is the integer derivation's distance from that one floating-point evaluation; another floating-point implementation differs from it as well, by its own rounding and its own upsampling filter, and it is that spread which the derivation removes.

7. The rendition record

7.1 The record

A sink that presents renditions signs a rendition record, kind "wai-tonemap", for them, under a key of its session or of its own (the record names no viewer). A party that publishes a rendition signs one for what it published.

member type meaning
kind string "wai-tonemap"
derivation term wai-tonemap:gainmap or wai-tonemap:st2094-50
base object {"object", "rendition", "width", "height"}: the content hash of the envelope that carries the base and the companion; the index of the base's entry among its renditions (0 for a WAI1); and the base's size, each 1 to 4096
companion object {"index", "sha256"}: the index of the companion's entry, and the SHA-256 of its payload
pictures array 1 to 256 entries, each {"frame", "headroom", "input", "output"}: the frame's index (0 for a still), the headroom (§3.5, 0 to 60000), the digest of the base frame's RGB8 samples, and the digest of the rendition's RGB16 samples; no two entries with the same frame and headroom, and entries in increasing order of frame, then of headroom
class string the class of the renditions (§7.2)
inputs array 1 to 16 entries, each {"content_hash", "origin", "class"}: an object the derivation read, the link hash of its origin claim (synthesis §3.1) or null, and that claim's class, restated; one of them is base.object, and no content hash appears twice
issued_at date-time when it signed it
signer_pubkey, sig §2

A wai-tonemap:gainmap record's pictures are all of frame 0. class and an input's class are one of captured, reconstructed, assistive and synthesized. A record that breaks a rule of this table is record_malformed; one whose headroom passes 60000 is headroom_out_of_range.

7.2 Provenance

A rendition is a convert step of synthesis §3.2. It rebuilds what its inputs hold: the base's samples, and, at a headroom, the alternate rendering the companion states of them. It names no model, asserts no source of its own and makes no evidence claim: there is no edit to check, only a computation to repeat, and §8 repeats it. Its class is the greatest of reconstructed and its inputs' classes, so a rendition never stands lower than what it was made from: a rendition of a captured photograph is reconstructed, and one of a synthesized picture is synthesized. A record whose class is another is class_mismatch.

Where a rendition is published as an object, its origin claim is a convert claim whose inputs are the inputs of its record (synthesis §3.4, the row for a decoder's reconstruction), and its record is the claim's record.

8. Verification

8.1 Inputs

A verifier is given, each in a list: rendition records; the envelopes it holds, each named by its content hash; the decoded frames it holds, each the RGB8 samples of one frame of the base of an envelope, named by the envelope's content hash and the frame's index (a verifier that decodes a base itself holds its frames; the base of a wai.image.png entry is decoded by its PNG); and a policy:

member values default meaning
tonemap accept-unrecomputed, require-proof accept-unrecomputed require-proof: a picture the verifier does not recompute is refused (step 6)

8.2 Steps

The verifier runs these steps over each record in order and stops at the first refusal, which it reports with the reason and where: the record by its index (["record", i]), a picture of it by both (["record", i, "picture", j]).

  1. Reading. In this order: the record reads (§2, §7.1, record_malformed), its signature verifies (bad_key, bad_signature), and no picture's headroom passes 60000 (headroom_out_of_range).

  2. Class. The record's class is §7.2's: class_mismatch.

  3. The envelope, where the verifier holds the one base.object names:

    1. it parses (SPEC §2, §7.1) and every component conforms to SPEC §7.1's rules and to the role SPEC §5 gives its capability (the reference WaiMulti::check_component_roles): otherwise envelope_nonconforming;
    2. entry base.rendition is a selectable rendition, entry companion.index a companion bound to it (SPEC §7.1, Companion components) whose capability is the derivation's (wai.meta.gainmap for wai-tonemap:gainmap, wai.meta.st2094_50 for wai-tonemap:st2094-50), and the SHA-256 of the companion's payload is companion.sha256: otherwise companion_mismatch;
    3. the companion reads: §4.1 to §4.3, or §5.1 and §5.2, in their order, each with its reason.
  4. Each picture, in order, where the verifier holds its frame:

    1. for wai-tonemap:st2094-50, a set of the companion covers the frame (frame_uncovered);
    2. the frame's samples are width · height · 3 bytes and their digest is input (input_samples_mismatch); for wai-tonemap:gainmap, the map's size fits the base's (§4.3, map_dimensions_mismatch);
    3. the derivation of §4.4 or §5.6 at the picture's headroom gives a rendition whose digest is output (rendition_mismatch). The picture is then proved.

    A picture whose frame or companion the verifier does not hold is not recomputed.

  5. Nothing else is read: a picture's output is never compared with anything but its recomputation.

  6. Under tonemap require-proof, a picture that is not proved: rendition_not_proved.

8.3 What an accepted case states

For each record, in order: its link hash, its class, and for each picture, in order, proved or not-recomputed.

9. Reasons

The codes this extension adds, each with where it is raised:

reason raised at
record_malformed §2, §7.1, §8.2 step 1
bad_key §2
bad_signature §2
headroom_out_of_range §7.1, §8.2 step 1
class_mismatch §7.2, §8.2 step 2
envelope_nonconforming §8.2 step 3.1
companion_mismatch §8.2 step 3.2
companion_malformed §4.1, §5.1
metadata_unsupported §4.2 step 1, §5.2 step 1
metadata_malformed §4.2 step 2, §5.2 step 2
metadata_out_of_range §4.2 step 3, §5.2 step 4
gain_space_unsupported §4.2 step 4, §5.2 step 5
map_channels_mismatch §4.3
frame_uncovered §5.6, §8.2 step 4.1
input_samples_mismatch §8.2 step 4.2
map_dimensions_mismatch §4.3, §8.2 step 4.2
rendition_mismatch §8.2 step 4.3
rendition_not_proved §8.2 step 6

A sink that is given an envelope which breaks SPEC §7.1's component rules reports SPEC's own code for the break (companion-only-as-capability, companion-only-as-rendition, incompatible-base, …); a verifier reports the record over it as envelope_nonconforming.

10. Conformance

wai tonemap run renders a still's gain map and writes its record, and wai tonemap verify checks them under require-proof.

live-conformance/tonemap/ holds the corpus, written by wai_tonemap_vectors gen from fixed seeds and from the integer video corpus: vectors.json (with the arithmetic of §3 and §5.4 at stated points and over whole ranges: L2, E2, the table S, tan of every angle and the slopes of the video's curves, which a rendition of the corpus would not always expose), the envelopes it names in envelopes/, and frames.rgb8, the five frames of the int_video clip of wai-rs/tests/vectors/int_video/ as wai.video.int_motion decodes them (their digests are that clip's sidecar's). Its stills are generated: an HDR scene made from integer formulas, rendered to an sRGB base and a gain map by the reference's own encoder, which is integer throughout and pinned by its code, not by this text. It has:

  • Accepted: a single-channel map at a quarter of the base's size and a three-channel map at full size with a gamma of 2, each at several headrooms from the base's to past the alternate's; a map whose alternate's headroom is below the base's (a negative weight); and the int_video clip under two metadata sets, one with slopes from angles and two alternates, one with PCHIP slopes, component mixing of types 2 and 3, at headrooms between, on and past the alternates'; a record whose base is a synthesized picture, synthesized; and the envelopes that carry each, which conform.
  • Refused: each reason of §9 at least once, each a control of one defect: among them a rendition at another headroom than the one stated (rendition_mismatch), a headroom past 6 stops (headroom_out_of_range), a map whose size does not divide the base's (map_dimensions_mismatch), metadata out of range in each companion (metadata_out_of_range), a rendition one sample of which was changed (rendition_mismatch), and a gain map carried as a rendition rather than a companion (envelope_nonconforming); and envelopes that break SPEC §7.1, each with SPEC's code: a gain map as a WAI1's capability, a gain map as a selectable rendition, a gain map bound to a video, ST 2094-50 metadata bound to an image.

A conforming verifier gives every case the outcome, the reason, the place and the statements the corpus states. wai_tonemap_vectors verify and the test tests/exact_tonemap.rs check that the corpus regenerates byte for byte, that wai-rs gives every case its outcome, that §9 lists exactly the reference's codes, that §6's figures are the corpus's, and that frames.rgb8 is the decode of the int_video clip. live-conformance/tonemap_verify.py is the second implementation, written from this text: it shares no code with wai-rs; it computes both derivations in integer arithmetic of its own, reads PNG, the envelopes and both companions itself, recomputes §6's figures within one code value of the largest and 0.01 of the mean, reads §9's codes from this text, mutates copies of the corpus and requires every copy to be refused, and drops each of its own checks in turn and requires the corpus to catch the weakened verifier.

11. Security and privacy considerations

  • A rendition is a function of the base, the companion and the headroom. A proved record says that the sink presented that function at that headroom; it does not say the companion is the one the content's owner made. Who may supply a companion is the envelope's: a companion rides in the envelope of its base, bound by SPEC §7.1, so a third party cannot attach one without changing the envelope's content hash.
  • A record names the display's headroom. Headroom is a property of the display and its settings at the time, and a sequence of records from one key can show how a viewer's display was adjusted. A sink that signs records under a session key reveals no more than the session's.
  • A companion's metadata can drive the gain to 16 stops and the offsets to ±1. The derivation is defined over all of it, and §3.5 clamps every code value to the display's range, so no value it reads makes a sink present past its peak.
  • The readers of §4.2 and §5.2 bound every count and length before they read, and a set or a frame outside them is refused before anything sized from it is allocated.