re: what Canary does between the guest's draw and a captured pixel

Answers question 3 of the 2026-09-02 play-test, from Canary's own source.

Gamma: Canary applies NONE. VdGetCurrentDisplayGamma is declared kStub and
only reports kernel_display_gamma_type (default 2, TV/BT.709) to the guest
so D3D can build a ramp. And the guest does not apply one either, for the
splash: its dumped pixel shader is four ALU ops -- tfetch2D, three muls, a
max -- with no pow, no ramp, no lookup.

Geometry: yes, the presenter resamples. present_letterbox defaults true and
present_safe_area_x/y default to 100, so the guest's 1280x720 is scaled to
fit the window and letterboxed with nothing cropped. That accounts for a
number the corpus has carried without explaining: captures measure the game
surface at 1279x675, and a single-pixel oracle placed at 1280x720
coordinates read the copyright line instead.

The structural consequence is the useful part. There are two measurement
paths and only one has Canary in it:

  pixels        guest draw -> EDRAM -> resolve -> front buffer ->
                presenter (scale + letterbox) -> X11 -> screenshot -> PNG
                => carries a resample
  vertex stream guest CPU writes a vertex buffer -> the draw logger reads
                guest memory directly
                => carries nothing

The per-frame alpha series is the second path, which is why it can be
stated as the game's values rather than as pixels we measured. Everything
measured off a PNG carries the resample: every RMSE against a capture,
every glyph count, every surface mean.

Refutation attempt on this corpus's own founding principle, "the oracle is
the real game running in Canary, captured": it SURVIVES but needs a
qualifier it has never carried. A PNG capture is the oracle plus a
resample. Harmless for ordering, counts, durations and change; a filter in
the path for anything pixel-exact. The vertex stream is the stronger oracle
and should be preferred where the question can be asked of it.

Not measured: the resample's actual filter. I read the cvars saying scaling
happens, not the kernel doing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
This commit is contained in:
sylph-decoder
2026-09-02 16:11:09 +00:00
parent 934fbfbbbf
commit 515ad205e5

View File

@@ -0,0 +1,103 @@
# ✅ What Canary does between the guest's draw and a captured pixel — and which of our measurements it touches
**Status: ✅ decoded from Canary's own source.** Instrument: ⟨canary-source⟩,
with ⟨capture⟩ for the dumped guest shader. 2026-09-02.
Answers question 3 of [`../agents/PLAYTEST-2026-09-02.md`](../agents/PLAYTEST-2026-09-02.md)
— *"present cadence, and any resolve, scale or gamma between the guest's draw and
a capture's pixels."*
---
## 1 — gamma: **Canary applies none**
`VdGetCurrentDisplayGamma` is the export the play-test warns about. Reading it:
```cpp
void VdGetCurrentDisplayGamma_entry(lpdword_t type_ptr, lpfloat_t power_ptr) {
*type_ptr = cvars::kernel_display_gamma_type; // default 2 = TV (BT.709)
*power_ptr = float(cvars::kernel_display_gamma_power);
}
DECLARE_XBOXKRNL_EXPORT1(VdGetCurrentDisplayGamma, kVideo, kStub);
```
**It is declared `kStub` and it transforms nothing.** It *reports* a gamma type to
the guest, which D3D would use to build a ramp. So the question becomes whether
the **guest** applies one — and for the splash it demonstrably does not. The
splash pixel shader, dumped from the running game, is four ALU ops:
```
tfetch2D r2, r1.xy, tf0
mul r1.___w, r2.wwww, r0.wwww
mul r0.xyz_, r2.xyzz, r0.xyzz
mul r1.xyz_, r0.xyzz, r1.wwww
max oC0, r1, r1
```
**No `pow`, no ramp, no lookup.** Nothing in the splash path applies gamma —
neither side of the emulation boundary.
## 2 — geometry: **yes, the presenter resamples**
`presenter.cc` defaults:
| cvar | default | effect |
|---|---|---|
| `present_letterbox` | **true** | aspect preserved, bars added rather than stretching |
| `present_safe_area_x` / `_y` | **100** | nothing cropped |
So the guest's 1280×720 is **scaled to fit the host window and letterboxed**. That
is a resample, and it explains a number the corpus has carried without accounting
for it: captures measure the game surface at **1279×675**, not 1280×720. A
single-pixel oracle placed at 1280×720 coordinates read the copyright line instead
— which is exactly the failure `wait_title.sh` was found to have.
## 3 — 🔴 So there are TWO measurement paths, and only one has Canary in it
| path | route | Canary processing |
|---|---|---|
| **pixels** | guest draw → EDRAM → resolve → front buffer → **presenter (scale + letterbox)** → X11 → `screenshot` → PNG | **a resample**, plus whatever the window system does |
| **vertex stream** | guest CPU writes a vertex buffer → **the draw logger reads guest memory directly** | **none** |
📌 **The per-frame alpha series is the second path.** The `k_8_8_8_8` colour is
read out of the guest's own vertex buffer *before* it reaches a shader, a render
target, a resolve or the presenter. **There is no Canary processing between the
guest's intent and that number** — which is why
[`splash-interpolates-every-frame.md`](splash-interpolates-every-frame.md) can
state per-frame alphas as the game's values rather than as pixels we measured.
⚠️ **Everything in this corpus measured off a PNG carries the resample.** That
includes every RMSE against a capture, every glyph count, every surface mean, and
the `motion-census` numbers the play-test quotes for the port. It does not make
them wrong — a resample preserves *change*, which is what a motion census
measures — but it does mean a pixel-exact comparison is comparing through a
filter nobody has characterised.
## Refutation attempt, recorded per the adversarial duty
**Target:** this corpus's own founding principle, stated at the top of three
documents — *"the oracle is the real game running in Xenia Canary, captured."*
**Result: it SURVIVES, but it needs a qualifier it has never carried.** A capture
is the oracle **plus a resample** whenever it is a PNG. For ordering, counts,
durations and change — the quantities `TEMPORAL-VERIFICATION.md` already tells us
to prefer — the resample is harmless. For anything pixel-exact it is a filter in
the path, and the corpus has been treating PNG captures and vertex-stream captures
as the same kind of evidence when one has an uncharacterised transform in it and
the other has none.
**The vertex stream is the stronger oracle** and should be preferred wherever the
question can be asked of it.
## Reach
⟨canary-source⟩ for the gamma stub and the presenter defaults — these are facts
about *this build*, and a different `--present_*` or `kernel_display_gamma_type`
would change them. ⟨capture⟩ for the shader, which is the splash's; **other
screens' shaders have not been checked for gamma** and the negative in §1 covers
the splash only.
**Not measured here: the resample's actual filter.** I read the cvars that say
scaling happens, not the kernel that does it. What a 1280×720 → 1279×675 resample
does to a thin glyph is unquantified, and that is the number anyone doing
pixel-exact work against a PNG would need.