“Enable HDR” sounds like a display switch. In a compositor, it is an end-to-end data contract. A client buffer arrives with a format and a color meaning; composition needs a linear, high-precision working space; the final image needs the right gamut and transfer function; and the kernel must scan out a matching format with matching metadata.
When any one of those statements is merely implied, the monitor can enter HDR mode while the picture is still wrong. That was the failure I needed to make observable in FocalDesk. The higher-level rendering paths were productive for normal desktop composition, but they did not expose enough of the HDR chain to isolate whether a bad result came from import, sampling, blending, encoding, synchronization, or presentation.
The first approaches produced HDR signals—not trustworthy HDR images.
My first attempts kept more of the established rendering abstraction in place. They could negotiate an HDR-capable output and make the display report an HDR mode, but the visual result exposed a deeper problem: signaling HDR does not prove that every pixel was interpreted and transformed correctly before scanout.
Trying to compensate at the end of the pipeline was equally fragile. A final shader can apply a transfer function, but it cannot repair content that was decoded with the wrong assumptions or blended while still non-linear. It can also make one test image look plausible while breaking ordinary sRGB UI, wide-gamut P3 content, or video.
Switching the output
An HDR mode and metadata can wake the display’s HDR pipeline without proving that the framebuffer contains correctly encoded HDR.
Fixing color at the end
A single final transform cannot recover information already lost through incorrect decoding, gamut assumptions, or non-linear blending.
Debugging through layers
Opaque format selection and synchronization made it difficult to identify exactly where the displayed result diverged.
The color bugs were disagreements between stages.
The investigation reduced the problem to explicit questions at each boundary. Is the incoming value encoded or linear? Which primaries give those numbers meaning? Has content already been mapped? Which precision does composition preserve? What transfer function does the display expect? The pipeline needed one answer for every buffer, not a global guess.
Three distinctions mattered. Transfer functions are not gamuts: decoding sRGB and encoding PQ are different operations from converting color primaries. Blending belongs in linear light, not directly in encoded sRGB or PQ values. And DRM color metadata describes the signal sent to the display; it does not perform the pixel conversion on the compositor’s behalf.
The breakthrough was treating color as typed data all the way through the renderer—not as three floating-point channels whose meaning could be inferred later.
Raw Ash made every hidden decision an owned decision.
I implemented a dedicated Vulkan renderer directly with Ash. Client DMA-BUFs are imported as Vulkan images, sampled according to their source meaning, and composed into an FP16 intermediate target. The extra precision keeps the working image from being prematurely constrained to the final 10-bit signal and gives SDR and HDR content room to coexist during composition.
The output pass converts the composed image to the target output color space, applies the PQ encoding expected by the HDR10 signal, and writes a 10-bit scanout buffer. GBM provides buffers the display engine can use; atomic KMS commits the framebuffer and output state together, including the connector’s HDR metadata and color-space properties.
Explicit synchronization completes the ownership model. GPU rendering and KMS presentation cannot race over the same buffer, and reuse waits on the correct completion boundary. That matters for correctness even when a synchronization bug presents as an intermittent visual artifact rather than an obvious crash.