Files
Sylpheed/docs/re/structures/ui-render-tone-curve.md
Sylpheed RE agent 969947e0e6 re(ui): put a number on the render-vs-capture tone difference
Closes an observation I left dangling last iteration ("the capture is ~4x
darker than the render") and puts a figure on the  INDEX's texture row
already carried: exact gamma/sRGB fidelity untested because a hue
comparison cannot see it.

Geometry first: cross-correlating the main-menu capture against our
render over +/-6 px puts the best alignment at exactly dy=0 dx=0,
correlation 0.9466. Only the tone differs.

Two methods failed before one worked, and both failures are recorded.
Three dark patches gave "4x darker" -- the whole-frame best linear scale
is 0.914, so three patches from one region are not a transfer curve. A
pixel-wise fit over 854,685 pixels then produced a NON-MONOTONIC transfer
(render 96-127 mapping brighter than render 128-159) with mean abs error
10-14 for every candidate model. That is edge misalignment, not a tone
curve: at correlation 0.947 a bright pixel routinely lands on a dark one.

Flat patches fix it -- 16x16 blocks with std < 8 in BOTH images, a
threshold chosen from the counts (0/83/404/1055/1788 at std<3/5/8/12/20):

  main menu  404 patches  gamma 1.491  err 0.28   (best linear 0.276, 0.34)
  EXTRAS     382 patches  gamma 1.493  err 0.22   (best linear 0.273, 0.28)
  title      506 patches  gamma 1.338  err 1.08   (best linear 0.842, 9.02)

Reach, stated because it is narrow: those patches span only render values
~0-60, where gamma and a plain scale are nearly indistinguishable -- the
two menus decide nothing (0.28 vs 0.34, 0.22 vs 0.28) and only the title
separates them. Nothing constrains midtones or highlights.

The held-out control FAILED TO DISCRIMINATE and is reported as such: the
splash's 2918 flat patches are pure black (render 0-4), so every model
scores ~0.00. That is a test with no power, not corroboration.

Confound left open: this compares our composite to what canary DISPLAYS,
and canary applies kernel_display_gamma_type = 2 (BT.709). The exponent
may be its output stage. The discriminating run -- set it to 0, recapture,
refit -- needs one emulator session reaching the main menu and was not
done.

Classified measured, not decoded; HANDOFF says plainly that a port
applying it is authoring.
2026-08-29 01:05:25 +00:00

4.1 KiB
Raw Blame History

🟡 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 96127 → capture 143, brighter than render 128159 → 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 1014 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 ~060. 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 04 — 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: this may be the emulator, not the game

The comparison is our composite against what Xenia Canary displays, and canary applies its own output transform: kernel_display_gamma_type = 2 — BT.709 (HDTV) — in this container's config (0 linear, 1 sRGB, 3 a power set by kernel_display_gamma_power).

So the measured exponent may belong to canary's display stage rather than to the game or the console. The discriminating experiment is cheap and was not run: set kernel_display_gamma_type = 0, re-capture the same screen, and re-fit. If the exponent collapses toward 1.0 it is canary's; if it survives it is upstream. That needs one emulator run reaching the main menu.

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.