re(ui): close the gamma chain from canary's defaults -- the game writes a ramp

Continues the previous iteration, where the game was measured calling
VdGetCurrentDisplayGamma at video init. The remaining link -- does it then
WRITE the ramp -- is a GPU register operation (XE_GPU_REG_DC_LUT_RW_INDEX
in CommandProcessor::WriteRegister), unlogged and invisible to kernel
logging. Two facts from the source close it without instrumenting.

1. The swap-path gamma stage is a PURE LUT. apply_gamma_table.xesli is
   the whole transform: index by input*255, fetch from a 256-entry ramp
   buffer, output. No sRGB encode, no second transfer function.

2. The table DEFAULTS TO IDENTITY. CommandProcessor::Initialize fills it
   with value = i * 0x3FF / 0xFF, and its own comment says the linear
   default is "what games set when starting with the sRGB (return value 1)
   VdGetCurrentDisplayGamma". An unwritten ramp is a no-op.

So the only transform is a LUT, the LUT is identity unless written, the
game queries the display gamma at init, and the capture differs from our
composite by gamma 1.34-1.49 -- which identity cannot produce. The guest
wrote a non-identity ramp.

Labelled an inference, with its weak joint named: it assumes our
composite reproduces the PRE-RAMP framebuffer, which it does not exactly.
What carries it is the shape -- a systematic ~1.4 fitted on flat patches
across three screens is not a compositor bug. The obvious alternative, a
fixed sRGB stage in the presenter, fits neither direction: an encode
(^0.45) brightens and we measured darkening; a decode (^2.2) darkens far
more than 1.4.

Direct observation remains available and cheap, and needs the emulator
only to boot: a GPU trace records gamma ramps as their own command type,
or one log line at the DC_LUT register write would settle it outright.
Not done.

METHOD: a default value is evidence; and name the weak joint of an
inference in the same breath as the conclusion.
This commit is contained in:
Sylpheed RE agent
2026-08-29 02:53:45 +00:00
parent c6ce000002
commit 421c61a517
3 changed files with 67 additions and 7 deletions

View File

@@ -243,8 +243,18 @@ authored version can be deleted.
`VdGetSystemCommandBuffer` and `VdSetDisplayMode` — the moment a ramp-builder
would ask. Control in the same log: 359 `VdRetrainEDRAM` lines, so an absent
call would have shown.
🟡 Still open: whether it then *writes* the ramp, and whether γ ≈ 1.4 is it.
That write is a GPU register operation (`DC_LUT`), invisible to kernel logs.
🟡 **That it then writes the ramp is inferred, not observed** — but the chain
is closed: canary's swap-path gamma stage is a **pure 256-entry LUT** with no
other transfer (`apply_gamma_table.xesli`), and that LUT **defaults to
identity** (`CommandProcessor::Initialize`, whose comment says the linear
default is "what games set when starting with the sRGB return value"). Identity
cannot produce the measured γ ≈ 1.4, so a non-identity ramp was written.
⚠️ The weak joint is that this assumes our composite reproduces the *pre-ramp*
framebuffer; what carries it is that a systematic ~1.4 across three screens is
not the shape of a compositor bug. A fixed sRGB stage does not fit either
direction (encode brightens; decode darkens far more).
**Practical upshot for you is unchanged:** the darkening is the game's own
display ramp, so it belongs in a port as a display profile, not baked in.
⚠️ Note the ramp depends on the display type the game is *told*; canary
hard-codes TV/BT.709, which on hardware is a console setting. **So this is a
display profile, not a fixed property of the game** — reasonable to expose as a