From 60b05bcd90d2a476a1f737981e663b96fe42e1db Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Tue, 1 Sep 2026 17:01:11 +0000 Subject: [PATCH] re: the game blends in the ENCODED space -- no gamma render target, anywhere The Port asks whether the blend is evaluated on sRGB-encoded values or linearised and re-encoded. It is answered by one register field I already log, and the answer is the same on every draw of two full captures. RB_COLOR_INFO.color_format is k_8_8_8_8 (0) on 2402/2402 splash draws and 33779/33791 draws of the boot-through-title capture; the other 12 are k_32_FLOAT and k_16_16_FLOAT and are not colour passes. k_8_8_8_8_GAMMA (1) appears ZERO times, and color_exp_bias is 0 everywhere so nothing stands in for a gamma either. The mechanism is Canary's own source, not our inference: k_8_8_8_8_GAMMA is the ONLY colour format around which a piecewise-linear gamma<->linear conversion is applied (spirv_shader_translator.h:510 PWLGammaToLinear / LinearToPWLGamma, render_target_cache.h:720, dxbc_shader_translator_om.cc). With k_8_8_8_8 there is none, so the blender operates on the stored values as they are. So a renderer that linearises before blending and re-encodes after is doing a different operation -- and the difference is gamma-shaped and exactly zero on unblended pixels, which is the divergence signature the port reports. Also renamed my units-per-second pre-registration off the 'h4' prefix: the Port's BLOCKED.md numbers this blend-space question H4 and two different H4s in one corpus is how a citation goes wrong. --- docs/re/data/blend-space-rt-format.txt | 35 ++++++++ docs/re/data/input-decoder-masks.txt | 43 +++++++++ docs/re/input-pad-read-path.md | 90 +++++++++++++++++-- ...md => units-per-second-preregistration.md} | 2 +- 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 docs/re/data/blend-space-rt-format.txt create mode 100644 docs/re/data/input-decoder-masks.txt rename docs/re/{h4-units-per-second-preregistration.md => units-per-second-preregistration.md} (98%) diff --git a/docs/re/data/blend-space-rt-format.txt b/docs/re/data/blend-space-rt-format.txt new file mode 100644 index 00000000..b110fb00 --- /dev/null +++ b/docs/re/data/blend-space-rt-format.txt @@ -0,0 +1,35 @@ +# In what SPACE does the game blend? -- answered from RB_COLOR_INFO.color_format, +# over every draw of two full captures. +# +# The mechanism, from Canary's own source (a non-ours instrument): +# ColorRenderTargetFormat (xenos.h:297) +# k_8_8_8_8 = 0 plain UNORM. No conversion anywhere. +# k_8_8_8_8_GAMMA = 1 the ONLY colour format for which a piecewise-linear +# gamma<->linear conversion is applied around the +# render target (spirv_shader_translator.h:510, +# PWLGammaToLinear / LinearToPWLGamma; +# render_target_cache.h:720; dxbc_shader_translator_om.cc) +# So: fmt=1 would mean "decode to linear, blend, re-encode". +# fmt=0 means the blender operates on the stored values as they are. +# +# ── census ──────────────────────────────────────────────────────────────────── +# +# Capture A -- both boot splashes (2402 draws, frames 1..600) +# 2402 rt0=[tile=0 fmt=0 exp=0] +# +# Capture B -- boot through the attract movie to the settled title with the +# PRESS (A) plate (33 791 draws over 6565 frame labels) +# 33779 rt0=[tile=0 fmt=0 exp=0] +# 10 rt0=[tile=0 fmt=14 exp=0] k_32_FLOAT -- 10 draws, not a colour pass +# 2 rt0=[tile=0 fmt=6 exp=0] k_16_16_FLOAT -- 2 draws +# +# k_8_8_8_8_GAMMA (fmt=1) appears ZERO times in either capture. +# color_exp_bias is 0 on every draw in both, so there is no exponent scaling +# standing in for a gamma either. +# +# ── conclusion ──────────────────────────────────────────────────────────────── +# The game blends in the ENCODED space, on the stored 8-bit values, with no +# linearisation and no re-encode. A renderer that linearises before blending and +# re-encodes after is performing a different operation, and the difference is +# gamma-shaped and exactly zero on unblended pixels -- which is the signature the +# port reports. diff --git a/docs/re/data/input-decoder-masks.txt b/docs/re/data/input-decoder-masks.txt new file mode 100644 index 00000000..eb2b31d5 --- /dev/null +++ b/docs/re/data/input-decoder-masks.txt @@ -0,0 +1,43 @@ +# Every button mask the C_PAD_DECODER's update function applies, sub_8220B8C0 +# (0x8220B8C0..0x8220CFA0, 1400 instructions). Two sources, both listed: +# * pure masks -- rlwinm rA,rS,0,MB,ME, the PPC idiom for 'test these bits' +# * immediates -- andi. / cmpli operands +# Bit values are XINPUT_GAMEPAD's, in their NATIVE positions: the decoder reads +# a 32-bit word at +12 of the C_PAD_RINGBUF (this+76) and masks its low 16 bits, +# verified 7/7 against /image/sylpheed.pe. There is no shift and no remap. + +## SINGLE-BIT masks -- one XINPUT button each + 0x0001 DPAD_UP rlwinm x21 0x8220BB9C 0x8220BC14 0x8220BE48 0x8220BEA8 0x8220BEEC 0x8220C28C andi./cmpli x2 + 0x0002 DPAD_DOWN rlwinm x2 0x8220BC64 0x8220C864 + 0x0004 DPAD_LEFT rlwinm x3 0x8220B8E4 0x8220C858 0x8220C8D8 + 0x0008 DPAD_RIGHT rlwinm x1 0x8220BC50 + 0x0010 START rlwinm x5 0x8220BE64 0x8220C47C 0x8220C594 0x8220C6B0 0x8220C920 + 0x0020 BACK rlwinm x4 0x8220C498 0x8220C5A0 0x8220C6C8 0x8220C93C + 0x0040 LEFT_THUMB rlwinm x2 0x8220BF98 0x8220C444 + 0x0080 RIGHT_THUMB rlwinm x2 0x8220C08C 0x8220C460 + 0x0100 LEFT_SHOULDER rlwinm x0 + 0x0200 RIGHT_SHOULDER rlwinm x0 + 0x0400 (0x0400) rlwinm x1 0x8220CBAC + 0x0800 (0x0800) rlwinm x0 + 0x1000 A rlwinm x2 0x8220C584 0x8220C608 + 0x2000 B rlwinm x1 0x8220C694 + 0x4000 X rlwinm x1 0x8220C66C + 0x8000 Y rlwinm x1 0x8220C680 + +## GROUP masks -- the XINPUT groupings, which is what identifies these as buttons + 0x0003 x1 DPAD_UP,DPAD_DOWN + 0x000F x1 DPAD_UP,DPAD_DOWN,DPAD_LEFT,DPAD_RIGHT + 0x0030 x1 START,BACK + 0x0060 x1 BACK,LEFT_THUMB + 0x00FF x5 DPAD_UP,DPAD_DOWN,DPAD_LEFT,DPAD_RIGHT,START,BACK,LEFT_THUMB,RIGHT_THUMB + 0xE000 x18 B,X,Y + 0xF000 x1 A,B,X,Y + +## Verified against the image (database used only as an index) + 0x8220C558 image=0x554A0426 expect=0x554A0426 OK rlwinm r10,r10,0,16,19 mask A|B|X|Y ("any face button") + 0x8220C680 image=0x556A0420 expect=0x556A0420 OK rlwinm r10,r11,0,16,16 mask Y + 0x8220C66C image=0x556A0462 expect=0x556A0462 OK rlwinm r10,r11,0,17,17 mask X + 0x8220C694 image=0x556B04A4 expect=0x556B04A4 OK rlwinm r11,r11,0,18,18 mask B + 0x8220BB80 image=0x556B0424 expect=0x556B0424 OK rlwinm r11,r11,0,16,18 mask B|X|Y + 0x8220C550 image=0x815F004C expect=0x815F004C OK lwz r10,76(r31) this+76 = the C_PAD_RINGBUF + 0x8220C554 image=0x814A000C expect=0x814A000C OK lwz r10,12(r10) +12 inside it = the button word diff --git a/docs/re/input-pad-read-path.md b/docs/re/input-pad-read-path.md index 7e73dd88..613c2ec5 100644 --- a/docs/re/input-pad-read-path.md +++ b/docs/re/input-pad-read-path.md @@ -1,7 +1,9 @@ # The pad read path — what the game asks the console for -**Status: ✅ decoded from the image** for the *driver* half; ❔ **not yet decoded** -for which button bits the menus test. Instrument: ⟨image⟩ — the executable's own +**Status: ✅ decoded from the image** for the driver, for the field set, and for +the complete button set `C_PAD_DECODER` tests; ❔ **not decoded** for which of +those each individual screen acts on, or for the decoder's own output bit +numbering. Instrument: ⟨image⟩ — the executable's own bytes, with the database used only as an index and every load verified against the file. 2026-09-01. @@ -87,10 +89,88 @@ title. A menu that responds to a **press** is likely reading the queue; anything that responds to a **hold** must be reading the polled state. Which of the two each menu action uses is not decoded. -## ❔ Not decoded — which bits each screen tests +## ✅ Which buttons the decoder actually tests — the complete set -The consumer side is open. Two named footholds exist for it, from the image's own -strings: +**`C_PAD_DECODER` is the game's own name for the layer between `wButtons` and +the menus**, from the executable's own Shift-JIS trace strings. Its constructor is +`sub_8220B610` (684-byte object, `memset` then a block of configuration +constants) and its update is **`sub_8220B8C0`**, 1 400 instructions immediately +after it. The construction trace `addi r3,r11,21840 → 0x820A5550` is +`C_PAD_DECODER 初期化`, verified against the image. + +**It reads a 32-bit word at `+12` of the `C_PAD_RINGBUF` (`this+76`) and masks +its low 16 bits directly. There is no shift and no remap on the way in** — the +bit positions are XINPUT's own: + +``` +0x8220C550 lwz r10, 76(r31) ; this+76 = the C_PAD_RINGBUF +0x8220C554 lwz r10, 12(r10) ; +12 = the button word +0x8220C558 rlwinm r10, r10, 0,16,19 ; mask 0xF000 = A|B|X|Y -> "any face button" +``` + +Every mask the update applies, from +[`data/input-decoder-masks.txt`](data/input-decoder-masks.txt): + +| bit | button | tested individually? | +|---|---|---| +| `0x0001` | D-pad **Up** | ✅ ×21 | +| `0x0002` | D-pad **Down** | ✅ ×2 | +| `0x0004` | D-pad **Left** | ✅ ×3 | +| `0x0008` | D-pad **Right** | ✅ ×1 | +| `0x0010` | **START** | ✅ ×5 | +| `0x0020` | **BACK** | ✅ ×4 | +| `0x0040` | **Left thumb click** | ✅ ×2 | +| `0x0080` | **Right thumb click** | ✅ ×2 | +| `0x0100` | **Left shoulder** | 🔴 **never** | +| `0x0200` | **Right shoulder** | 🔴 **never** | +| `0x0400` | (unnamed) | ✅ ×1 | +| `0x0800` | (unnamed) | 🔴 never | +| `0x1000` | **Ⓐ** | ✅ ×2 | +| `0x2000` | **Ⓑ** | ✅ ×1 | +| `0x4000` | **Ⓧ** | ✅ ×1 | +| `0x8000` | **Ⓨ** | ✅ ×1 | + +and the **group** masks, which are what identify these as buttons rather than as +coincidental bit constants — no other quantity in a program produces exactly +these groupings: + +| mask | meaning | ×| +|---|---|---| +| `0x000F` | the whole D-pad | 1 | +| `0x0003` | Up \| Down | 1 | +| `0x0030` | START \| BACK | 1 | +| `0x0060` | BACK \| left thumb | 1 | +| `0x00FF` | D-pad + START + BACK + both thumbs | 5 | +| `0xE000` | Ⓑ \| Ⓧ \| Ⓨ | **18** | +| `0xF000` | Ⓐ \| Ⓑ \| Ⓧ \| Ⓨ — *"any face button"* | 1 | + +**Verified 7/7 against `/image/sylpheed.pe`**, database used only as an index. + +### 🔴 The one negative, and it is the useful part + +**The shoulder buttons are the only pad inputs the decoder never tests.** +`0x0100` and `0x0200` appear in this function **only** as `ori` — the decoder +*setting* bits in its own output word (`0x8220C334`, `0x8220C48C`), never +`andi.`/`rlwinm` reading them. Same for `0x0800`. + +So on the evidence of this layer: **LB and RB are not menu inputs.** A binding +table that maps them to anything is mapping them to nothing. + +⚠️ **Reach.** This is `C_PAD_DECODER`, one layer, and it is the layer the *menus* +sit above — but gameplay code may read `wButtons` by another route, and this says +nothing about that. It is also not per-screen: it is the set the decoder +distinguishes, and a given screen may act on a subset. + +📌 **And the decoder emits its own bit assignment.** The `ori 0x100 / 0x200 / +0x400 / 0x800` sites build an output word whose bit positions are **not** +XINPUT's. Anything downstream reads *that* word, so a table found later in menu +code will be in the decoder's numbering and not the console's. Mapping the two is +not done and is the next read. + +## ❔ Still not decoded — which bits each SCREEN acts on + +The decoder's set is above. What remains is the layer past it. The footholds, from +the image's own strings: ``` 0x820A5550 'C_PAD_DECODER 初期化' referenced from sub_8220B610 diff --git a/docs/re/h4-units-per-second-preregistration.md b/docs/re/units-per-second-preregistration.md similarity index 98% rename from docs/re/h4-units-per-second-preregistration.md rename to docs/re/units-per-second-preregistration.md index c3e1c75a..2923e948 100644 --- a/docs/re/h4-units-per-second-preregistration.md +++ b/docs/re/units-per-second-preregistration.md @@ -1,4 +1,4 @@ -# H4 — units per SECOND: is the animation clock frame-counted or time-integrated? +# Units per SECOND: is the animation clock frame-counted or time-integrated? **Pre-registration. Written 2026-09-01 BEFORE the capture was taken**, per [`../agents/TEMPORAL-VERIFICATION.md`](../agents/TEMPORAL-VERIFICATION.md).