Files
lightcraft/docs/display-profiles.md
T
insideTheFluxandClaude Opus 5.5 0b610c8307 LR-PREF-DISPLAY: an unreadable Windows advanced-colour state leaves previews sRGB
When the display configuration, the window's display path or its advanced-colour state can't be
read, `monitor_color` answered with the monitor's profile as if Windows weren't colour-managing the
desktop. If it was (HDR, Auto Color Management), previews were converted twice. It now answers
`Unknown`, as the crate's other failures do, so previews stay sRGB (Windows 10 before 1709 and
drivers that don't report the state included).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfjNkJiMUagz3DN3zoV2rV
2026-10-25 22:05:23 -07:00

75 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Display (monitor) profiles
LightCraft can show photos through the monitor's ICC profile, so colours are right on wide-gamut and calibrated
displays. Without one, previews are sRGB, which a wide-gamut panel shows oversaturated.
**macOS uses the screen's own profile** (the one System Settings ▸ Displays assigns) while none is chosen. macOS doesn't
colour-manage LightCraft's window (wgpu leaves its Metal layer without a colour space, which Core Animation shows
unmatched), so on a P3 screen previews used to look more saturated than the exported files in Preview or Photos (issue
#787). The profile is read again about once a second, so moving the window to another screen is followed.
**Linux uses the profile the desktop assigns in colord** (GNOME / KDE Settings ▸ Color, or a calibrator such as
DisplayCAL) while none is chosen. A background thread asks colord (`org.freedesktop.ColorManager` on the system bus)
every 3 s, so the UI never waits on D-Bus, and a profile change in the desktop's settings shows within that. eframe
doesn't say which monitor the window is on, so with several displays the profile is used only when all of them that
have one share it; otherwise, and without colord, previews stay sRGB (choose a profile by hand then). A compositor
that colour-manages windows itself with the same profile (KWin with an ICC profile set in Display Configuration)
would correct twice; KWin keeps that profile in its own settings, not colord, so this only happens when both are set.
**Windows uses the profile it assigns to the monitor showing the window** (Settings ▸ System ▸ Display ▸ Color
profile, Color Management, or a calibrator) while none is chosen. On an SDR desktop Windows passes what an app draws to
the monitor unchanged, so without it a wide-gamut monitor shows sRGB previews oversaturated and a calibration is
ignored. `crates/wincolor` finds the monitor the window is mostly on and asks Windows Color System for its default
profile, in the user's scope when they chose per-user profiles for that monitor, else the system's. It is asked about
once a second, so moving the window to another monitor and changing the profile in Windows are both followed without
a restart. (GDI's `GetICMProfileW` keeps answering the profile a process first saw, so it is only the fallback when the
monitor's device can't be found.) With no profile assigned, previews are sRGB. **While Windows colour-manages the
desktop itself** (HDR on, or Auto Color Management on Windows 11: its advanced-colour state, from
`DisplayConfigGetDeviceInfo`), it treats an ordinary app's output as sRGB and converts it for the monitor, so previews
stay sRGB rather than be converted twice. So do they when that state can't be read (Windows 10 before 1709, a driver
that doesn't report it). The log says which applies (`screen profile: …`) whenever it changes.
## Using it
Settings ▸ Display ▸ **Choose Profile…** picks an `.icc` / `.icm` file (the dialog opens where the system keeps
them: `~/.local/share/icc` on Linux, ColorSync's folders on macOS, `…\spool\drivers\color` on Windows). **Use Screen
Profile** (in hosts without one, **Use sRGB**) goes back to no profile. The choice is an app setting (`ui.json`, `displayProfile`)
and applies at once; a file that can't be used is reported there and previews stay sRGB.
Agents and scripts: `app.displayProfile {"path": "/path/to/monitor.icc"}` (`""` or `null`: none; no `path`: report
only) returns `{path, description, kind: "matrix" | "lut", primaries: {red, green, blue: [x, y]}}`; the screen's
profile (none chosen) reports `path: null, screen: true`. Hosts supply it as `Services::screen_profile`
(`apps/lightcraft/src/screen_profile.rs`, `apps/lightcraft/src/colord.rs`, `apps/lightcraft/src/win_screen_profile.rs`);
headless sessions have none and stay sRGB.
## How it works
- **The loupe renders into the display's gamut.** Views (Detail, Before, Compare / Reference, preset and profile
hover, the second window) ask the pipeline for a render in the display's own primaries
(`lightcraft_pipeline::DisplaySpace`, `RenderRequest::display`): the per-pixel stage converts from the working space
to the display's linear RGB, gamut maps into *its* gamut and encodes with the sRGB curve, on the CPU and the GPU
alike (only `FinishParams::to_out` / `out_luma` change). Colours an sRGB render would clip stay as saturated as the
display can show. For a display that is exactly a standard space the result equals an 8-bit render into that space.
- **Then to the display's device values** (`lightcraft_codecs::display::DisplayProfile::correct`): for a matrix/TRC
profile, three 256-entry tables from the sRGB curve to the profile's tone curves (nothing at all when they are the
sRGB curve, as for Display P3); for a LUT-based profile, a `moxcms` transform through its B2A tables. A LUT-only
profile (no colorant tags) gets its primaries measured through its A2B tables.
- **Everything made for sRGB is converted** (`lightcraft_engine::display::present`, on the render workers):
thumbnails, cached view previews, embedded camera JPEGs and merge previews.
- **What stays sRGB:** the histogram (of the same sampled pixels, converted back to sRGB), the rendered-thumbnail and
view-preview caches (a profile change doesn't invalidate them), exports, and the UI itself.
- **Soft proofing:** the blue "display" gamut warning shows what the monitor can't show, from its profile.
- Conversions are relative colorimetric: the display's white is the image's white.
## Cost
Measured on a 2-core cloud VM, 2560 × 1707 preview: the tone-curve tables take about 5 ms (single-threaded); converting the sampled
histogram pixels a few ms; the sRGB copy for the view-preview cache (full-quality renders only) about 30 ms; a
LUT-based profile's correction about 65 ms. Drafts during slider drags skip the full sRGB copy.
## Not yet
- X11's `_ICC_PROFILE`, and following the window across monitors on Linux (macOS and Windows do both, above).
- Windows' advanced-colour profiles (what a monitor uses while Auto Color Management is on): LightCraft leaves colour
to Windows then.