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]>
39 KiB
WAI Extension: Exact Tone Mapping
Status: Draft. Registers two companions and a record.
wai.meta.gainmapcarries a gain map of ISO 21496-1:2025 (its metadata and its gain map image) as a companion of an image payload, andwai.meta.st2094_50carries 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 alog2value. 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:gainmapandwai-tonemap:st2094-50map the base'sRGB8samples, the companion's bytes and a headroom, an integer in units of 1/10000 of a stop, to oneRGB16rendition, with a stated fixed-pointlog2, a stated fixed-pointexp2, 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 aconvertstep of synthesis: it rebuilds what the base and the companion hold and generates nothing. No new transport. The reference implementation is theexact_tonemapmodule ofwai-rs(featureexact_tonemap); the corpus islive-conformance/tonemap/, and its second implementation islive-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:
- n is the bit length of v less 1, so 2^n ≤ v < 2^(n+1).
- m = v · 2^(30 − n) where n ≤ 30, and ⌊v / 2^(n − 30)⌋ otherwise: the mantissa in Q30, 2^30 ≤ m < 2^31.
- 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).
- 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):
- e = ⌊y / 2^16⌋ and f = y − 2^16 · e, 0 ≤ f < 2^16.
- 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
log2of (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:
- refuses metadata whose
minimum_versionis not 0 withmetadata_unsupported: a reader does not apply metadata whose minimum version it does not know; - refuses metadata shorter than its fields, or whose
writer_versionis below itsminimum_version, or, with awriter_versionof 0, longer than its fields, withmetadata_malformed; bytes after the fields of metadata of a laterwriter_versionare read past, and the reserved bits are not read; - refuses with
metadata_out_of_rangemetadata 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; again_map_minorgain_map_maxpast 16 in magnitude, or again_map_mingreater than itsgain_map_max; agammaof 0 or outside 1/64 to 64; abase_offsetoralternate_offsetpast 1 in magnitude; - refuses metadata whose
use_base_colour_spaceis 0 withgain_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):
- 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:
- Base. B = S[base(x, y)[c]].
- 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.
- 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/γⱼ. - Log gain. L (Q16) = rnd(minⱼ · (2^30 − v) + maxⱼ · v, 2^30).
- Weighted gain. G (Q16) = rnd(L · W, 2^16).
- 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:
- refuses a set whose
minimum_application_versionis not 0 withmetadata_unsupported: Annex C.2.1 has a parser ignore the whole bitstream; - refuses a set that ends inside a structure, or holds a
reserved_zeroelement that is not 0 (Annex C.1), withmetadata_malformed. Bytes after the last element are padding and are not read (Annex C.2.1); - 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);
NumAlternateImagesmin(·, 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); - refuses with
metadata_out_of_rangea 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; - refuses with
gain_space_unsupporteda set whoseuse_reference_white_tone_mapping_flagis 1, whose gain application colour space C.3.8 sets to the primaries of ITU-R BT.2020, or whosegain_application_space_chromaticities_modeis 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 −
- · 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]).
-
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). -
Class. The record's
classis §7.2's:class_mismatch. -
The envelope, where the verifier holds the one
base.objectnames:- 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): otherwiseenvelope_nonconforming; - entry
base.renditionis a selectable rendition, entrycompanion.indexa companion bound to it (SPEC §7.1, Companion components) whose capability is the derivation's (wai.meta.gainmapforwai-tonemap:gainmap,wai.meta.st2094_50forwai-tonemap:st2094-50), and the SHA-256 of the companion's payload iscompanion.sha256: otherwisecompanion_mismatch; - the companion reads: §4.1 to §4.3, or §5.1 and §5.2, in their order, each with its reason.
- 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
-
Each picture, in order, where the verifier holds its frame:
- for
wai-tonemap:st2094-50, a set of the companion covers the frame (frame_uncovered); - the frame's samples are
width·height· 3 bytes and their digest isinput(input_samples_mismatch); forwai-tonemap:gainmap, the map's size fits the base's (§4.3,map_dimensions_mismatch); - 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.
- for
-
Nothing else is read: a picture's output is never compared with anything but its recomputation.
-
Under
tonemaprequire-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
gammaof 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 aWAI1'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.