yikerman
(Yi Cao)
November 6, 2026, 10:33am
1
Hi! It has been a really long-standing problem for darktable to not being able to output wide-gamut and HDR content. The following design & overhaul aims to fix it once and for all, and on every platform, by explicitly manage pipeline output encoding and offering abstraction for different backends:
master ← yikerman:display-color-management
opened 03:33AM - 04 Nov 26 UTC
## Summary
darktable's display path assumed that a darkroom pipe ends in 8-bi… t RGB in the selected display profile (as limited by GTK3), and that cairo paints those bytes into GTK's window.
This PR replaces that path completely and provides abstraction for cross-platform wide-gamut and HDR support:
A window declares an output description (profile + intent -> color management; headroom -> HDR), and its pipes render display-referred float to it. A per-window presenter shows the frames through a backend: native backends show the image on layers of their own, and the cairo fallback should look identical to master.
The first native backend is Wayland color-management-v1, which brings wide gamut and HDR negotiation.
Most importantly, preview should always match export now.
Since GTK3 sucks, there are some dirty workarounds. Complexity is contained and hopefully when GTK4 migration is done, it can be greatly simplified.
**Dependency change**: macOS builds now requires GTK >= 3.24.37 (the first one that tags its surface sRGB).
### Non-trivial Behavior Changes
- Backend selection scheme see https://github.com/darktable-org/darktable/pull/22953#issuecomment-6664238839. Additionally, a force cairo and profile option is provided as an escape hatch.
- "system display profile" is removed from histogram. Since color is managed by compositor we cannot say what ultimately turns out (e.g. some compositor don't have relative colorimetric, and perceptual is not standardized).
## Referenced issue
Supersedes #22491
Partially addresses #20477 #18078 (builds the abstraction for further work)
Related #17710 (a native macOS backend would plug into the presenter)
Fixes #19322; Fixes #19977 (explicit sRGB + cairo fallback); Fixes #20688
## Checklist
- [x] I have read [CONTRIBUTING.md](https://github.com/darktable-org/darktable/blob/master/CONTRIBUTING.md) and the [coding style](https://github.com/darktable-org/darktable/wiki/Developer's-guide#coding-style).
- [x] I have not merged master into the topic branch.
- [x] The pull request is one logical change, and every commit compiles on its own. You may use `git rebase --exec 'cmake --build build'` master to check this.
- [x] I ran the relevant tests: unit tests, `src/tests/integration/` where the pixelpipe is touched, or `darktable-cli` as a headless smoke test.
- [x] New user-visible strings use `_()`, new preferences are registered in `data/darktableconfig.xml.in`.
- [x] A `RELEASE_NOTES.md` entry was added (only needed if fixing an issue in a release). Do not reference GitHub issues.
## Test instructions
- Unit tests passes
- The integration suite on gives the same results as master on the same machine. This PR should not touch export workflow.
- On X11 (via xvfb), darkroom screenshots match master exactly.
- Visually checked on a wide gamut and HDR display with both synthetic test chart in #22491 and real photo..
- Additionally, visually compared with standalone mpv+gpu-next and hdr output on a test chart.
Compositors other than KWin and X11 Cinnamon are untested.
- [ ] Gnome Mutter... other linux desktop??
- [ ] macOS
- [ ] Windows
## AI assistance
Heavily assisted by AI.
## More Info
```mermaid
flowchart TB
out["window's output description<br/>profile and headroom<br/>the platform's display profile unless a backend declares one<br/>colorspaces.c"]
pipe["darkroom pipe renders to it<br/>float32 RGBA, 1.0 = reference white, up to the headroom<br/>develop.c, colorout.c, pixelpipe_hb.c"]
pres["presenter: the image, then overlays drawn after it<br/>headroom target settled here<br/>presenter.c, presenter_target.c"]
choice{"window's backend, chosen once<br/>presenter.c"}
wl["Wayland<br/>output: linear BT.2020, headroom target<br/>composed into FP16<br/>presenter_wayland.c"]
mac["macOS<br/>TODO"]:::todo
win["Windows<br/>TODO"]:::todo
cairo["fallback: cairo<br/>output: the platform's display profile, else sRGB<br/>headroom 1, presenter_cairo.c"]
subgraph screen["screen, top to bottom"]
subgraph layers["native layers"]
fg["foreground: overlays, GtkOverlay widgets redrawn<br/>8-bit, in GTK's profile"]
img["image layer<br/>in the window's output, wide gamut and HDR"]
end
gtk["GTK's surface: the rest of the UI<br/>8-bit, the display profile unless a backend declares one"]
fg ~~~ img ~~~ gtk
end
out --> pipe --> pres --> choice
choice --> wl
choice -.-> mac
choice -.-> win
choice --> cairo
wl --> layers
mac -.-> layers
win -.-> layers
cairo -->|image and overlays, 8-bit| gtk
choice -.->|the backend declares it| out
classDef todo stroke-dasharray: 5 5
```
The native layer drawing GUI controls again is the part I hate, and can be only addressed by GTK4 in which entire `presenter_layers.c` can be gone.
## Some Review Tips
This is a pretty big PR (~6K loc not counting vendored xml and tests), but compared to 22491 it is at least designed & comprehensible. A highlevel discussion is in `dev-doc/display_presentation.md`, read that first.
The commits come in parts:
1. **Output description and float backbuf.** What a presented pipe renders to.
2. **Presenter and Wayland backend.** `presenter_layers.c` is the GTK3 part every native backend shares, `presenter_wayland.c` the Wayland impl.
3. **Per-window outputs.** Every window described; GTK-side colors in the window's own encoding.
4. **Display profile from the platform.** Manual selection removed, soft proof gets its own intent.
5. **Frames and notification.** Producers pull by serial. Cairo is back to master's bytes and work.
6. **Cairo as a backend.** The presenter core has no backend branch left.
7. **Native compose.** The image composed straight into the buffer sent to the compositor.
8. **Backend criterion.** The mutter workaround, the two preferences, also WCS, GTK on macOS.
9. **HDR target.** Shared logic by all backends regarding headroom.
Launch `darktable -d control` and look for `[presenter wayland]`, then mess around with HDR/color profile in compositor setting, and use sigmoid or AgX. On X11 or any other platform (cairo), nothing should look different from master.
Also check out macOS WCG/HDR that builds on top of it (which I cannot verify)
master ← nicknezis:macos-edr
opened 08:00AM - 06 Nov 26 UTC
## Summary
**Depends on #22953. Only the last two commits are new here** (e4059… 081ad and c8762e4230). The rest are #22953's commits, and they will drop out of this diff once it merges. I'll keep it as a draft until then.
On macOS, every window was a cairo window. The image reached the screen as 8-bit sRGB through GTK, which clipped wide gamut and left an XDR display's HDR headroom unused. This adds a Quartz platform and transport for #22953's layered backend in `src/gui/presenter_quartz.m`.
- **Layers:** for each presenting widget, two views go above GDK's NSView. They take no input (`hitTest:` returns nil), so all input still reaches GTK.
- The image is in a `CAMetalLayer` of RGBA half floats, tagged extended linear BT.2020, with EDR on and 1.0 at SDR white.
- The foreground is in a BGRA8 `CAMetalLayer` tagged sRGB, matching GTK's own sRGB tagging since 3.24.37.
- **Upload and presentation:** two shared Metal buffers per layer, alternating. `reserve()` asks to try again next frame while the GPU still reads both, the same contract as Wayland's busy buffers. A commit blits into a fresh drawable inside a Core Animation transaction, after the frame clock's paint. A run loop observer just before Core Animation's commit catches anything left over.
- **Popovers:** GTK3 on macOS draws popovers into the toplevel's view, so the platform leaves them to the layered backend to replay.
- **Headroom:**
- The platform reports `maximumExtendedDynamicRangeColorComponentValue` once the image layer has shown a frame, and NAN before that. The potential headroom is the ceiling. While a reference preset is active, both are the reference headroom.
- Reports come on every screen-parameters notification, and the core's settling policy does the rest.
- Moving to another screen, or a display wake, starts a new generation.
- **Cairo stays in use** without a Quartz GDK window or a Metal device, and when "convert to the display profile in darktable" is set. SDR screens stay on the native path with target 1, so they get wide gamut only.
- **Build:** CMake enables Objective-C and links Metal and QuartzCore when GTK has the Quartz backend. Every other platform takes the existing `return NULL` in `dt_presenter_platform_new()`.
## Referenced issue
Related: #17710
## Checklist
- [x] I have read [CONTRIBUTING.md](https://github.com/darktable-org/darktable/blob/master/CONTRIBUTING.md) and the [coding style](https://github.com/darktable-org/darktable/wiki/Developer's-guide#coding-style).
- [x] I have not merged master into the topic branch.
- [x] The pull request is one logical change, and every commit compiles on its own. You may use `git rebase --exec 'cmake --build build'` master to check this.
- [ ] I ran the relevant tests: unit tests, `src/tests/integration/` where the pixelpipe is touched, or `darktable-cli` as a headless smoke test.
- [ ] New user-visible strings use `_()`, new preferences are registered in `data/darktableconfig.xml.in`.
- [x] A `RELEASE_NOTES.md` entry was added (only needed if fixing an issue in a release). Do not reference GitHub issues.
## Test instructions
Tested on a MacBook Pro M4 Pro (built-in XDR, potential headroom 16) with a Gigabyte M32U as a second, SDR screen, using GTK 3.24.52 from Homebrew and a Sony ARW with AgX:
- The log shows `[presenter quartz] main window: EDR layers`. At 100% brightness the target settles at 3.67 (current EDR headroom 4.0).
- Highlights are brighter than SDR white, and AgX's curve goes past 100%.
- Image placement, after resizing and toggling panels, plus clicking, dragging, zooming and scrolling, overlays (masks, guides, crop), and no flicker or holes.
- "Convert to the display profile in darktable" switches between cairo and the EDR layers live, both ways.
- Three moves to the Gigabyte screen and back: target 1 there, and back on the XDR, the ceiling (16) and then 3.67 a second later.
- Every commit builds on its own, with no new warnings. `presenter_quartz.m` compiles clean.
Not tested: the second window, live brightness changes with this backend, sleep and wake (the generation bump is there but unexercised), and a macOS build without an Objective-C compiler.
To try it: build on macOS with GTK >= 3.24.37 (Quartz), open an image in the darkroom on an EDR-capable screen, and look for `[presenter quartz]` lines with `-d verbose`.
The tests box is left unchecked because no pipeline code changed. The strings box is unchecked because the change adds no user-visible strings or preferences.
## AI assistance
This change was written with AI assistance (Claude), and I reviewed and tested it on my own hardware as described above.
For complete changes I recommend reading the top comment in the PR. This is still experimental and under review. And yes unfortunately you need to build it yourself. I’ve tested on KWin 6.8.2 with a wide-gamut/HDR display and in a VM of X11/Cinnamon, but more setups would be very helpful, especially GNOME/Mutter (see discussion, it’s a PIA), and whether any regressions are there on macOS and Windows.
The rule of thumb now is that preview (more specifically, proofing) should match export, assuming the preview application for your export is rendering WCG/HDR correctly. I find wkjarosz/hdrview looking quite promising as a ground truth, but personally I’ve only tried mpv on my setup.