I had parked the tone-curve finding behind "this may be the emulator, not the game: canary applies kernel_display_gamma_type = 2 (BT.709) on output", with a planned run setting it to 0 and re-fitting. Reading the source kills both the confound and the experiment. VdGetCurrentDisplayGamma_entry is a kStub GETTER the guest calls (xboxkrnl_video.cc). Its own comment: "Used in D3D SetGammaRamp/ SetPWLGamma to adjust the ramp for the display." The cvar is a value REPORTED TO THE GAME, which then builds its own ramp. Canary's role is downstream: the guest writes DC_LUT, command_processor.cc reads it into gamma_ramp_256_entry_table_, and the swap path applies it via swap_apply_gamma_pipeline_layout with apply_gamma_table.ps / apply_gamma_pwl.ps compiled in. So there is no emulator-side BT.709 post-process to subtract, and any gamma in a captured frame is a ramp the game installed. What is NOT established, and the reach is stated: that this game installs a ramp at all, or that the measured 1.34-1.49 is it. The run logs cannot say -- kernel exports log at Debug and this harness masks Kernel logging (log_mask = 13, per boot_menu.sh's own comment), so their silence is guaranteed regardless of what the game did. The planned experiment was wrong in design: changing the cvar changes what the GUEST is told and therefore which ramp the GAME builds, so it could never isolate a stage that does not exist -- and it perturbs the capture harness, since skip_intro classifies movie-vs-static on an absolute rmse threshold that a brighter frame biases. The right run changes nothing about the output: LOG_MASK=12 LOG_LEVEL=3 and look for the call and the DC_LUT writes. Also for the port: the ramp depends on the display type the game is told, and canary hard-codes TV/BT.709 where hardware uses a console setting. So this is a display profile, not a fixed property of the game. METHOD: read what a cvar does before building an experiment around it; and an absence in a log is only evidence if the log would have shown it.
6.0 KiB
🟡 Our composite is brighter than the emulator's frame — measured, not decoded
Status: 🟡 measured, with a narrow reach and a live confound. Closes an observation left dangling by ui-8ax-fullres-background ("the capture is ~4× darker than the render"), and puts a number on the ❔ that INDEX's texture row already carried: "exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is untested, since a hue comparison cannot see it."
⚠️ This is not a decode. A port applying it is authoring a value.
First: the geometry is right
Cross-correlating live-main-menu.png against our render over ±6 px finds the
best alignment at exactly dy = 0, dx = 0, correlation 0.9466. So the
composite is in the right place at the right size and only the tone differs.
(The capture is 1279×675 and top-aligned; that is the screenshot tool's crop.)
🔴 The first two methods were wrong, and both failed visibly
- Three dark patches gave "capture ≈ 4× darker". Over the whole frame the best linear scale is 0.914. Three patches from one region are not a transfer curve.
- A pixel-wise fit over 854 685 pixels produced a non-monotonic transfer (render 96–127 → capture 143, brighter than render 128–159 → 132). That is the signature of edge misalignment, not of a tone curve: at a 0.947 correlation a bright render pixel routinely lands on a dark capture pixel. Mean abs error was 10–14 for every model, which is the tell that none of them fit.
Both are recorded because the second is the interesting failure — a fit whose residual is large everywhere is not a model to choose between, it is a method to throw away.
The method that works: flat patches only
16×16 patches where both images have std < 8, so local edges cannot
contribute. The threshold is not arbitrary — at std < 3 there are zero
patches, and the count runs 0 / 83 / 404 / 1055 / 1788 for std <
3 / 5 / 8 / 12 / 20.
| screen | flat patches | gamma exponent | mean abs err | best linear | its err |
|---|---|---|---|---|---|
| main menu | 404 | 1.491 | 0.28 | 0.276 | 0.34 |
EXTRAS |
382 | 1.493 | 0.22 | 0.273 | 0.28 |
| title | 506 | 1.338 | 1.08 | 0.842 | 9.02 |
So capture ≈ 255·(render/255)^γ with γ ≈ 1.34 – 1.49.
⚠️ The reach — and it is narrow
- The flat patches are almost all dark: render values ~0–60. Over that range a gamma and a linear scale are nearly indistinguishable — on the two menus the errors are 0.28 vs 0.34 and 0.22 vs 0.28, which decides nothing. Only the title separates them (1.08 vs 9.02), because its flat regions reach ~60.
- The held-out control could not test it. Running the same fit on the developer splash gives 2 918 flat patches whose render range is 0–4 — pure black. Every model scores ≈ 0.00 there. That is a control that failed to discriminate, not a control that passed.
- Nothing here constrains midtones or highlights, which is exactly where a γ = 1.4 curve does its visible work.
🔴 The confound as I stated it is REFUTED — canary applies no gamma of its own
I wrote that "canary applies its own output transform: kernel_display_gamma_type = 2 — BT.709". That is not what the cvar does. Reading the source:
void VdGetCurrentDisplayGamma_entry(lpdword_t type_ptr, lpfloat_t power_ptr) {
// 1 - sRGB. 2 - TV (BT.709). 3 - use the power written to *power_ptr.
// Anything else - linear.
// Used in D3D SetGammaRamp/SetPWLGamma to adjust the ramp for the display.
*type_ptr = cvars::kernel_display_gamma_type;
...
It is a getter the guest calls (xboxkrnl_video.cc, exported kStub). The
cvar is a value reported to the game, which then builds its own ramp. The
emulator's role is downstream and faithful:
- the guest writes its ramp to the
DC_LUTregisters; command_processor.ccreads them intogamma_ramp_256_entry_table_;- the swap (present) path applies them —
swap_apply_gamma_pipeline_layout, withapply_gamma_table.ps/apply_gamma_pwl.pscompiled in.
So there is no emulator-side BT.709 post-process to subtract. Any gamma in the captured frame is a ramp the game installed.
🟡 What that does and does not settle
✅ The stated confound is gone: the measured exponent is not an emulator artefact bolted onto the game's output.
🟡 Not established: that this game installs a ramp at all, or that
γ ≈ 1.34–1.49 is that ramp. The run logs cannot say — kernel exports log at
Debug and this harness masks Kernel logging (log_mask = 13, per
boot_menu.sh's own comment), so the absence of gamma lines in them is expected
and means nothing. That is the reach of this negative.
⚠️ And the ramp a game builds depends on the display type it is told. Canary
hard-codes 2 (TV/BT.709); on hardware that is the console's display setting. So
the exponent is display-profile dependent by design, not a fixed property of
the game.
✅ The corrected next experiment
My planned run — set kernel_display_gamma_type = 0 and re-fit — was the wrong
design: it changes what the guest is told and therefore which ramp the game
builds, so it could never isolate an emulator stage that does not exist. It also
perturbs the capture harness, because skip_intro.sh classifies movie-vs-static
on an absolute rmse threshold and a brighter frame biases it (see
capture-harness-status).
The right run changes nothing about the output: boot with LOG_MASK=12 LOG_LEVEL=3 (both are needed — kernel calls log at Debug) and look for
VdGetCurrentDisplayGamma and the DC_LUT writes. Same frames, no perturbation.
What a port should do with this
Treat it as authored, not transcribed. If the goal is to match the emulator — which is what every capture in this corpus is — a γ ≈ 1.4 darkening of the composite gets closer, and is best applied where it was measured (the dark background), not extrapolated to the whole range on this evidence.