A second falsification: L2's squarings rounded instead of truncated still regenerated the corpus (the variant differs on about 3 in 100000 mantissas). vectors.json now states the SHA-256 of L2 at 2^18 spread values, of E2's mantissa for every fraction and of tan for every angle; both implementations check them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
656 lines
39 KiB
Markdown
656 lines
39 KiB
Markdown
# 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](synthesis.md): 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/`](../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](../SPEC.md) §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](../SPEC.md) §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](derived-tracks.md)), and a tone map is none of
|
||
them.
|
||
|
||
Relationship to [synthesis](synthesis.md): 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](determinism-tiers.md): 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](synthesis.md) §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](jwp-receipts.md) §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](../SPEC.md) §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](jwp-receipts.md) §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 **Q*n*** 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 *g*w × *g*h, and the base's *w* × *h*: *s* = *w* / *g*w is
|
||
an integer of 1, 2, 4 or 8, and *h* = *s* · *g*h (`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 *H*b =
|
||
`base_hdr_headroom` = *b*n / *b*d, *H*a = `alternate_hdr_headroom` = *a*n / *a*d and *H*ₜ =
|
||
rnd(*h*ᵈ · 2^16, 10000):
|
||
|
||
1. **Weight.** *A* = *h*ᵈ · *b*d · *a*d − 10000 · *b*n · *a*d and *D* = 10000 · (*a*n · *b*d −
|
||
*b*n · *a*d), so *A* / *D* = (*h*ᵈ / 10000 − *H*b) / (*H*a − *H*b). 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:
|
||
|
||
2. **Base.** *B* = S[base(*x*, *y*)[*c*]].
|
||
3. **The map's sample at the pixel.** *X* = max(0, 2*x* + 1 − *s*), *x*₀ = ⌊*X* / 2*s*⌋,
|
||
*ax* = *X* − 2*s* · *x*₀ and *x*₁ = min(*x*₀ + 1, *g*w − 1); *Y*, *y*₀, *ay* and *y*₁ the same
|
||
from *y* and *g*h. Then *N* = *g*(*x*₀, *y*₀) · (2*s* − *ax*) · (2*s* − *ay*) + *g*(*x*₁, *y*₀) ·
|
||
*ax* · (2*s* − *ay*) + *g*(*x*₀, *y*₁) · (2*s* − *ax*) · *ay* + *g*(*x*₁, *y*₁) · *ax* · *ay*, and
|
||
*Q* = 255 · 4*s*²: 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.
|
||
4. **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/γⱼ.
|
||
5. **Log gain.** *L* (Q16) = rnd(minⱼ · (2^30 − *v*) + maxⱼ · *v*, 2^30).
|
||
6. **Weighted gain.** *G* (Q16) = rnd(*L* · *W*, 2^16).
|
||
7. **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*ₜ − *H*b) / (*H*a − *H*b), 0, 1), negated where *H*a < *H*b,
|
||
|
||
*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, *k*max = 150000; type 1, *k*component =
|
||
150000; type 2, *k*red = *k*green = *k*blue = 25000 and *k*max = 75000; type 3, each
|
||
coefficient 3 · min(·, 50000); every other weight 0. *k*sum 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 · 2*k* ·
|
||
(2*k* + 1)) and *σ* = *σ* + *t*;
|
||
- cosine: *t* = 2^62, *κ* = 2^62; for *k* = 1 to 15 in turn, *t* = rnd(−*t* · *q*, 2^62 · (2*k* −
|
||
1) · 2*k*) 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* = (*c*r, *c*g, *c*b) in Q30,
|
||
|
||
*M*ⱼ = rnd(*c*ⱼ · *k*component + *k*red · *c*r + *k*green · *c*g + *k*blue · *c*b + *k*max ·
|
||
max(*C*) + *k*min · min(*C*), *k*sum), 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*₃ = 2*Y*′ᵢ + *m̂*ᵢ − 2*Y*′ᵢ₊₁ + *m̂*ᵢ₊₁, *c*₂ = −3*Y*′ᵢ +
|
||
3*Y*′ᵢ₊₁ − 2*m̂*ᵢ − *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; *h*c = 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 *h*c = *H*ᵢ (Formula (2));
|
||
- otherwise, for the one *i* with *H*ᵢ < *h*c < *H*ᵢ₊₁, rnd(Gainᵢ(*C*) · (*H*ᵢ₊₁ − *h*c) +
|
||
Gainᵢ₊₁(*C*) · (*h*c − *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 *k*sum | §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](synthesis.md) §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](synthesis.md) §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](../SPEC.md) §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
|
||
|
||
[`live-conformance/tonemap/`](../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.
|