A video file stores integers. Matrix, range and transfer decide what those integers mean, and none of them are stored in every format. What each one does, what a wrong one looks like on screen, and how to settle it with numbers instead of opinion.
Codes, not colors
Open an uncompressed video file and what is on disk is a grid of integers, three per pixel at most and often fewer. There is no red in there, no brightness in nits, no white point. Those appear only when something declares how the integers should be read.
Three declarations carry nearly all of that meaning, and the same three names appear twice in a working day, in opposite roles. When a camera, an encoder or a signal generator writes a file, they are constructive: they decide which integers get written. When a player or an inspector opens that file, they are declarative: they decide what the integers are taken to mean.
The rule everything else follows from. Changing matrix, range or transfer in a viewer never rewrites a byte. It changes every derived quantity: the RGB readout, the nits figure, the waveform, the vectorscope, gamut statistics, quality metrics, legality checks. The codes sit still and the meaning moves.
The pipeline
The order is not the one most people guess. Light is converted to a signal first, by the transfer function, and only then split into luma and chroma by the matrix. That is why the components are written R'G'B' with a prime: they are already non-linear when the matrix touches them.
Codes and the 0 to 255 habit
A pixel probe in a 10-bit file typically shows something like Y 1023 Cb 512 Cr 512 → RGB 255 255 255. Those numbers live at different stages and are not two views of the same thing.
Y, Cb and Cr are the file, read straight from the plane bytes at the format's own depth, 0 to 1023 for 10-bit, with nothing applied. RGB is a derived signal on a legacy scale: the decode produces R'G'B' normalized 0 to 1, and the readout multiplies by 255 because that is the scale everyone reads at a glance. It is a display convention, not the file's precision.
The decode also clamps to 0 and 1, and that is where information disappears. At 10-bit video range the luma white point is 940, so every code from 940 to 1023 produces a signal at or above 1.0 and shows the same 255. Codes 1019 and 1023 are visibly different signals and an identical RGB triple. Measure in codes; use RGB for intuition, not for judgement.
Matrix
Video does not store R, G and B. It stores one brightness channel and two color differences, so the color channels can be sampled at lower resolution without an obvious penalty. Luma is a weighted sum of the three primaries, and those weights are the matrix. The three standards disagree about them, and that disagreement is the entire practical content of the setting.
Greys are immune. Neutral means Cb and Cr sit at the centre code, which decodes neutral under every matrix, so a grey ramp is byte-for-byte the same file whichever matrix authored it. The error shows only on saturated color, as a hue and brightness shift worst on red and blue. On a vectorscope the color targets sit rotated off their boxes rather than randomly scattered.
Why the mistake is common. Nothing about a 1920×1080 file makes it BT.709. Software guesses from resolution, SD to BT.601, HD to BT.709, UHD to BT.2020, and the guess is right often enough that the wrong cases are hard to spot. A file carrying no tag gets whatever its reader assumes.
Range
In video range, black sits above the bottom of the number space and white below the top. At 8-bit that is 16 and 235 for luma, 16 to 240 for chroma; at 10-bit multiply by four, so black 64, white 940, chroma 64 to 960. Full range uses everything.
The reserved space is not waste. Filtering and resampling overshoot, and a signal clipping at the boundary would fold those overshoots back into the picture as hard artefacts; footroom and headroom give them somewhere to live. SDI also reserves the outermost codes as sync words, 0 to 3 and 1020 to 1023 at 10-bit, so they cannot carry picture at all.
Legal is not the same as present. A video-range file can hold codes outside 64 to 940, legitimately. PLUGE bars sit deliberately below black so a monitor's brightness control has a reference to disappear against; a 10-bit PLUGE in video range writes 48, 56, 72 and 80 around the 64 floor, per BT.814. A range checker writes 0 and 1023 on purpose, because a chart covering only the legal window would check nothing. The range is the coordinate system a pattern maps its levels into, not a filter applied to the finished picture.
When the reading is wrong, the two directions are not symmetrical. One washes the picture out and is fully reversible. The other crushes and clips, and destroys distinctions.
code R' as video RGB R' as full RGB what it is
0 -0.0731 0 0.0000 0 full-range black
64 0.0000 0 0.0626 16 video-range black
255 0.2180 56 0.2493 64 quarter scale
511 0.5103 130 0.4995 127 mid scale
940 1.0000 255 0.9189 234 video-range white
1019 1.0902 255 0.9961 254 highest legal SDI code
1023 1.0947 255 1.0000 255 full-range white
Chroma is scaled as well as clipped, because its span is 896 codes in video range against 1023 in full. Read video-range chroma as full and saturation drops to about 88 percent of what was intended; read full-range chroma as video and it is pushed to about 114 percent, with the tips clipping. Greys are unaffected either way, since the centre code is the same in both.
Transfer
The transfer function is the only one of the three that talks about the physical world. It maps a signal value to a luminance, and the three families in use answer that question very differently. The SDR curves (sRGB, gamma 2.2, gamma 2.4 for BT.1886) are relative: full scale means diffuse white, conventionally 100 nits on a reference display. PQ (SMPTE ST 2084, the HDR10 curve) is absolute: a code means a specific number of nits, from 0 to 10 000, whether or not any display can produce it. HLG (BT.2100) is scene-referred: full scale means the display's peak, and the luminance comes from a system gamma applied at display time, quoted here against a 1000-nit nominal peak.
code signal gamma 2.4 HLG PQ
255 0.218 2.6 nits 6.9 nits 3.2 nits
511 0.510 19.9 nits 53 nits 102 nits
767 0.803 59 nits 280 nits 1591 nits
940 1.000 100 nits 1000 nits 10000 nits
Why HDR test patterns can all look like the same white. A typical panel gives an app roughly twice SDR white of extended range, about 200 nits. Under PQ, code 511 is already 102 nits and code 767 is 1591, so everything from the mid-scale step upward is beyond what the panel can show and clips to the same white. The picture is not wrong, the display is out of room. The codes and the nits readout still tell the truth, which is the argument for trusting the meter over the monitor.
Chroma subsampling and siting
Because luma carries most of the detail the eye resolves, the two color-difference channels are usually stored at lower resolution. 4:4:4 keeps everything, 4:2:2 halves chroma horizontally, 4:2:0 halves it in both directions, so a 4:2:0 chroma sample stands for a 2×2 block of luma. That raises a question with no self-evident answer: where in the block does the chroma sample sit? Different standards answered differently, and the answer has to travel with the file or edges pick up a half-sample color shift.
What the file carries
Given how much the three declarations change, you would expect every format to record them. Most raw and intermediate formats record almost none.
Check it yourself
None of this has to be taken on faith. A signal generator writes a file whose every code you chose, so you can predict the readout before you look at it. These use Videre's generator and pixel probe; the experiments work with any tool that authors exact codes and reports them back.
$ videre generate -o a.y4m --pattern solidField \ --param mode=signalRGB --param red=0 --param green=0 --param blue=1 \ --format yuv444p10 --size 640x480 --frames 1 --matrix bt709 --range video $ videre generate -o b.y4m ... --matrix bt601 $ cmp a.y4m b.y4m # differs $ videre generate -o c.y4m --pattern ramp --matrix bt709 ... $ videre generate -o d.y4m --pattern ramp --matrix bt601 ... $ cmp c.y4m d.y4m # identical, byte for byte
Open the blue field and flip the viewer's matrix between BT.601 and BT.709. The Y, Cb, Cr readout does not move. The RGB readout does.
$ videre generate -o range.y4m --pattern rangeChecker \
--format yuv420p10 --size 1920x1080 --frames 1 --range full
A full-range checker writes the cells 0, 4, 255, 511, 767, 1019 and 1023. Toggle the viewer between Full and Video and watch the readout: the codes never move, and under Video the top two cells both report RGB 255, then separate into 254 and 255 under Full. Codes 0 and 4 are the mirror case at the bottom.
$ videre generate -o pq.y4m --pattern pqStairs --transfer pq \
--format yuv420p10 --size 1920x1080 --frames 1
Hover a step with the viewer set to PQ and the nits figure lands at or just below the authored value, never above, because the nits-to-code quantization floors toward the legal side. Switch to HLG and the same code reports a different luminance. Switch to an SDR curve and the nits figure disappears, because a relative curve makes no absolute claim. The generator refuses to write a nits-defined pattern under an SDR transfer at all: a file claiming 1000 nits while carrying 100 is a lie the rest of the chain would believe.
The full pattern catalogue, with what each one exposes, is on the test signals page.
Symptom to cause
Most color faults in a delivery chain are one of these, and each has a signature you can confirm with a probe rather than an opinion. The tell is usually what stays correct.
The habit worth building. When a picture looks wrong, read the codes first and the picture second. Codes are the only part of the chain that is not an opinion. Everything else, the RGB triple, the nits figure, the scopes, the way it looks on your monitor, is downstream of three declarations that may well be the thing that is wrong.
Go deeper
Videre shows the codes, the interpretation and the derived numbers side by side, and generates the test signals to check them against.