# --force-color-profile=hdr10 washed out our Chrome pages

> Forcing Chrome's colour profile to hdr10 lifted a page's dark background and dimmed its white on macOS. Four checks missed it; a screen capture did not.

Source: https://mosaicbrowser.com/blog/hdr10-washed-out

## A canvas readback is not a rendering check

Forcing Chrome's colour profile to hdr10 lifted a page's dark background and dimmed its white on macOS. Four checks missed it; a screen capture did not.

![243: White dropped to 243 and only the screen showed it, beside a screenshot: mosaic general settings: theme and how profiles launch](https://mosaicbrowser.com/covers/hdr10-washed-out.webp)

On a Mac, `--force-color-profile=hdr10` makes Chrome put its window in an HDR colour space, and macOS then tone-maps every ordinary page inside it. Measured off the real screen, our test page's background went from `(54,54,58)` to `(70,71,74)` and its white fell from 255 to 243. We shipped it anyway, because every check we ran read the page before that happened. We now use `extended-srgb`, which reports the same media queries and renders correctly.

## Why we were forcing a colour profile at all

Mosaic draws a colour gamut for each profile instead of passing on the computer's own, because a value identical across every profile on one machine is a join key. For a Mac-like profile that gamut is `p3`. The first switch we used for it, `--force-color-profile=display-p3-d65`, moved `color-gamut` to `p3` and left `dynamic-range` at `standard`.

That pair is close to nonexistent. A Mac whose panel is wide-gamut also reports high dynamic range. We read both media queries together on branded Chrome 153, one launch per row:

```
nothing forced      p3   high        <- what a real Mac answers
display-p3-d65      p3   standard    <- what every profile answered
hdr10               p3   high
scrgb-linear        p3   high
extended-srgb       p3   high
srgb                srgb standard
generic-rgb         srgb standard

```

Fixing one field had produced a different contradiction. When a switch moves a pair of related values, read both.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## The checks we ran, and why they all agreed

`hdr10` was the first row that gave the right pair, so we checked what else it moved. A canvas drawn and hashed inside the page, the same readback [canvas fingerprinting](https://mosaicbrowser.com/glossary/canvas-fingerprinting) relies on, came back `d289dba9` under all five colour profiles. `screen.colorDepth`, `pixelDepth`, `(color-gamut: p3)` and the WebGL version string were unchanged. A screenshot of the page through the DevTools protocol was byte-identical to one taken before. So was our stored detector screenshot of IPHey's page, regenerated and compared against the old one from git: median luminance 54 and 99th percentile 255 in both.

Four independent checks, all clean, and we shipped it.

Within the hour the report came in from outside: "why is there a weird tint to the website when I visit IPHey". It was not IPHey. It was every page.

## What the screen showed

Forcing an HDR profile tells macOS the window holds HDR content. macOS then maps the page's SDR colours into that space, which puts reference white below peak and lifts black. We captured the actual screen, same page and same window geometry, one launch per profile:

```
display-p3-d65   background (54,54,58)   brightest 255
hdr10            background (70,71,74)   brightest 243
scrgb-linear     background (53,53,58)   brightest 255
extended-srgb    background (53,54,59)   brightest 255

```

Against the correctly rendering baseline, the mean absolute pixel difference across the whole page was 18.70 for `hdr10`, 0.46 for `scrgb-linear` and 0.62 for `extended-srgb`. The last two are text antialiasing between separate launches, so on pixels they tie. We chose `extended-srgb` on the transfer function: it keeps the sRGB curve and only allows values outside 0 to 1, where `scrgb-linear` composites in linear light, a bigger change for nothing gained.

## A canvas readback is not a rendering check

Every one of the four checks read the framebuffer before the display transform. The canvas hash, the CDP screenshot and the stored detector image all come from the page's own pixels, and the tone-map is applied after those, on the way to the panel. So all four agreed, and all four were answering a question nobody had asked. They told us the page was the same. They could not tell us it looked the same.

When a change sits downstream of the page, in a colour profile, the compositor or anything the window server does, the only measurement is a capture of the screen. On macOS that is `screencapture`, and nothing less counts.

The same family of mistake shows up elsewhere in this work. A check that reads a value too early or from the wrong place reports a pass, and the pass looks like evidence. The fix is always a control that can see the thing in question. For browser behaviour that control is [Google's own build of the same version](https://mosaicbrowser.com/blog/the-control-is-googles-build). For pixels it is the screen.

![A Mosaic profile fingerprint with the hardware, screen and locale drawn for the profile](https://mosaicbrowser.com/shots/fingerprint-detail.png)
*Gamut and dynamic range are drawn with the rest of the machine*

## What profiles launch with now

A profile drawn with a `p3` gamut launches with `--force-color-profile=extended-srgb`. One drawn as `srgb` launches with `srgb`. Each reads the same `color-gamut` and `dynamic-range` pair that a real machine of its kind reports, and pages render as they should. The switch is set by the engine at launch, not by a script in the page, which is the approach behind every value Mosaic changes ([why C++ and not JavaScript](https://mosaicbrowser.com/blog/cpp-not-javascript)). How the gamut and the rest of a profile are drawn together from real hardware records is on [real fingerprints](https://mosaicbrowser.com/features/real-fingerprints).

## Questions

### What does --force-color-profile=hdr10 do in Chrome on a Mac?

It puts the browser window in an HDR colour space. macOS then tone-maps ordinary SDR page content inside it, so blacks lift and white lands below full brightness. On our test page the background went from (54,54,58) to (70,71,74) and the brightest pixel from 255 to 243.

### How do I get dynamic-range: high without the washed-out look?

On our measurements --force-color-profile=extended-srgb reports color-gamut p3 and dynamic-range high, and renders within text antialiasing noise of the unforced page. It keeps the sRGB curve and allows values outside 0 to 1.

### Why didn't a canvas hash or a CDP screenshot catch it?

Both read the page's framebuffer before the display transform. The tone-map happens after it, on the way to the screen, so neither can see it. Only a capture of the actual screen can.

### Which colour profile does Mosaic use now?

A profile whose drawn gamut is p3 launches with extended-srgb, and one drawn as srgb launches with srgb. Both read the same pair of media queries a real machine of that kind reports.

## Related

[The control that settles "is it our patches?"](https://mosaicbrowser.com/blog/the-control-is-googles-build): Unpatched Chrome for Testing got the same IPHey verdict as our engine, so our patches were not the cause. Its voice list still differs from Chrome's.

[Why we change fingerprints in C++, not JavaScript](https://mosaicbrowser.com/blog/cpp-not-javascript): A JavaScript spoof is one realm deep. An iframe read the real source of 33 of our patched functions; the same values changed in C++ agree everywhere.

[Machines that exist: fingerprints drawn from real hardware](https://mosaicbrowser.com/features/real-fingerprints): Mosaic draws each profile's GPU, screen, CPU, memory and fonts from one real device, so the parts of a fingerprint agree with each other and with the host.

[Canvas fingerprinting: how a drawing identifies your computer](https://mosaicbrowser.com/glossary/canvas-fingerprinting): Canvas fingerprinting draws hidden text and shapes, reads the pixels back and hashes them. Small rendering differences make the hash a mark of your machine.

[Twenty APIs Chrome doesn't have: Chromium's field trials](https://mosaicbrowser.com/blog/twenty-apis-chrome-lacks): Unbranded Chromium runs its field-trial testing config at every launch. Against Chrome 153 that was 20 API differences, and one switch moved them all.







## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
