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 5b783e276b
commit 81625e8f29
3 changed files with 67 additions and 7 deletions

View File

@@ -631,3 +631,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
any title screen. A blocker that stops one experiment does not stop every
experiment in the same area, and it is worth re-reading the parked list against
what each item really requires rather than against the area it belongs to.
* **A default value is evidence.** Whether the game writes a gamma ramp looked
like it needed a GPU trace. It mostly did not: canary initialises the ramp
table to **identity** and applies it through a shader that is a pure LUT lookup
with no other transfer. An unwritten ramp is therefore a no-op, and any
non-identity transfer in the output implies a write. Reading what a field holds
when nobody has touched it turns "I cannot observe the write" into "the write
must have happened" — cheaper than instrumenting, though it stays an inference
and should be labelled one.
* **Name the weak joint of an inference in the same breath as the conclusion.**
The chain above assumes our composite reproduces the pre-ramp framebuffer,
which is the one step that could be wrong. Writing that down beside the
conclusion — rather than only the supporting facts — is what lets a later
reader attack it instead of inheriting it.