From a4e7c69d7acfd26839b9e9520463e971fd8aa26a Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Fri, 4 Sep 2026 13:15:57 +0000 Subject: [PATCH] re: refute my own batching hypothesis -- blend state, not linkage Last iteration I proposed that the two sweeps share one indices=8 draw because ptloop01 links to ptloop02, and said testing it needed a loading-screen capture I lack. Wrong twice: a linked pair was already in every capture, ptbtn00 -> ptbtn00f. Measured: ptbtn00f is drawn ALONE in 899 (f6b) and 1441 (f6) draws and batched in ZERO, while the sweeps pair up in 1092 and 1744. Linkage does not batch. The constraint is blend state -- ptbtn00f is additive and its linked partner alpha-over, which cannot share a draw. The sweeps batch because both are additive on one page. Page+blend is necessary but not sufficient: 8154/alpha-over appears as two separate draws in a single frame, 2108 such draws in f6b. This removes a wrong cause rather than supplying a batching rule. Extends read_draws.py to preserve draw grouping (draw index and quad count per draw); check_labels.py still passes unchanged as a regression control. Refutation attempt on the port's 0x3002/0x3003 menu-item reading: survives. 958 of 970 stems contain "btn"; the 12 exceptions are psselect_slot and psselect_slot_blank, which are menu rows. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t --- .../examples/kind3002_names.rs | 38 ++++++++++++ docs/re/batching-is-blend-not-linkage.md | 61 +++++++++++++++++++ tools/re-capture/read_draws.py | 26 +++++--- 3 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 crates/sylpheed-formats/examples/kind3002_names.rs create mode 100644 docs/re/batching-is-blend-not-linkage.md diff --git a/crates/sylpheed-formats/examples/kind3002_names.rs b/crates/sylpheed-formats/examples/kind3002_names.rs new file mode 100644 index 00000000..1278c133 --- /dev/null +++ b/crates/sylpheed-formats/examples/kind3002_names.rs @@ -0,0 +1,38 @@ +//! What do 0x3002/0x3003 elements actually look like disc-wide? +//! The port's menu-item rule keys on this class; this asks whether the class is +//! uniformly menu-row-shaped or whether it contains other things too. +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::collections::BTreeMap; +use std::path::PathBuf; +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let mut names: BTreeMap = BTreeMap::new(); + let mut paks: Vec<_> = std::fs::read_dir(root.join("dat")).expect("dat") + .filter_map(|e| e.ok()).map(|e| e.path()) + .filter(|p| p.extension().map(|x| x=="pak").unwrap_or(false)).collect(); + paks.sort(); + for p in &paks { + let Ok(ar)=PakArchive::open(p) else { continue }; + for ent in ar.entries() { + let Ok(by)=ar.read(ent) else { continue }; + let Some(b)=ui_layout::parse_build(&by) else { continue }; + for el in &b.elements { + if el.kind==0x3002 || el.kind==0x3003 { + let stem = el.name.split('.').next().unwrap_or(""); + let cls: String = stem.trim_end_matches(|c:char| c.is_ascii_digit()).to_string(); + *names.entry(cls).or_default()+=1; + } + } + } + } + let total: usize = names.values().sum(); + println!("0x3002/0x3003 elements disc-wide: {total}, {} name-stems", names.len()); + let mut v: Vec<_> = names.into_iter().collect(); + v.sort_by_key(|(_,n)| std::cmp::Reverse(*n)); + let btn: usize = v.iter().filter(|(k,_)| k.contains("btn")).map(|(_,n)| n).sum(); + println!("stems containing \"btn\": {btn} of {total} ({:.1}%)", 100.0*btn as f64/total as f64); + println!("\ntop stems:"); + for (k,n) in v.iter().take(14) { println!(" {n:5} {k}"); } + println!("\nNON-btn stems (the ones a menu-item rule would also claim):"); + for (k,n) in v.iter().filter(|(k,_)| !k.contains("btn")).take(12) { println!(" {n:5} {k}"); } +} diff --git a/docs/re/batching-is-blend-not-linkage.md b/docs/re/batching-is-blend-not-linkage.md new file mode 100644 index 00000000..e264bd78 --- /dev/null +++ b/docs/re/batching-is-blend-not-linkage.md @@ -0,0 +1,61 @@ +# 🔴 Refuted — the sweeps are batched by **blend state**, not by being linked + +**Question:** are two linked records drawn in the same GPU draw call? + +**What the human looks at:** nothing — this kills a hypothesis of mine before it +reaches anything user-visible. + +**What this does NOT cover:** F5's code route, still open. + +**Instrument:** ⟨capture⟩ ×2, read with `read_draws.py` (now preserving draw +grouping). + +## The hypothesis, and it was mine + +Last iteration I noticed `ptloop01 --focus_link--> ptloop02` and suggested that +the two sweeps share one `indices=8` draw **because they are linked**, rather +than because they share a texture page. I recorded it 🟡 with the experiment +named, and said it needed a loading-screen capture I do not have. + +**That was wrong twice over.** A linked pair was already in every capture: +`ptbtn00 --> ptbtn00f`. + +## Measured — the linked pair is NEVER batched + +| | `ptbtn00f` drawn **alone** | batched with anything | +|---|---|---| +| `f6b` | **899 draws** | **0** | +| `f6` | **1441 draws** | **0** | + +Against the sweeps, which pair up in **1092** and **1744** draws respectively. + +So linkage does not cause batching. **The constraint is blend state:** +`ptbtn00f` is drawn additive (`0x01010101`) while its linked partner is +alpha-over (`0x07010701`), and two different blend states cannot share one draw. +The sweeps batch because they are **both additive on one page**. + +⚠️ Page + blend is **necessary, not sufficient**. A single settled frame carries +`8154`/alpha-over as *two separate* draws (draws 5 and 7), and the census counts +2108 such draws in `f6b`. Submission order and intervening state changes still +split them; this finding removes a wrong cause, it does not supply a complete +batching rule. + +## Refutation attempt — the port's `0x3002`/`0x3003` menu-item reading + +Their rule treats that kind as the class menus are built from. I asked whether the +class is uniformly menu-row-shaped, disc-wide: + +``` +0x3002/0x3003 elements: 970 across 91 name-stems +stems containing "btn": 958 of 970 (98.8%) +the 12 exceptions: psselect_slot (6), psselect_slot_blank (6) +``` + +**It survives.** The only non-`btn` members are save-slot rows, which are menu +items. Recorded as an attempt that did not land. + +## Reach + +The batching result is about two captures of one screen. It shows linkage is not +sufficient and blend state is a hard constraint; it does not establish what else +splits a draw. diff --git a/tools/re-capture/read_draws.py b/tools/re-capture/read_draws.py index 71c5c9bb..6bb6ad40 100755 --- a/tools/re-capture/read_draws.py +++ b/tools/re-capture/read_draws.py @@ -23,22 +23,25 @@ _V = re.compile(r'\[(-?\d+\.\d+),(-?\d+\.\d+),z=[-\d.]+,col=([0-9A-F]{8})\]') class Quad(tuple): __slots__ = () - def __new__(cls, page, verts, alpha, blend, cx): - return tuple.__new__(cls, (page, verts, alpha, blend, cx)) - page = property(lambda s: s[0]) - verts = property(lambda s: s[1]) - alpha = property(lambda s: s[2]) - blend = property(lambda s: s[3]) - cx = property(lambda s: s[4]) + def __new__(cls, page, verts, alpha, blend, cx, draw=-1, nquads=1): + return tuple.__new__(cls, (page, verts, alpha, blend, cx, draw, nquads)) + page = property(lambda s: s[0]) + verts = property(lambda s: s[1]) + alpha = property(lambda s: s[2]) + blend = property(lambda s: s[3]) + cx = property(lambda s: s[4]) + draw = property(lambda s: s[5]) # index of the draw line within the frame + nquads = property(lambda s: s[6]) # how many quads that draw carried def read(path): frames = collections.defaultdict(list) frame = page = blend = None idx = 0 + draw_no = -1 for line in open(path, errors='replace'): m = _F.match(line) if m: - frame = int(m.group(1)); continue + frame = int(m.group(1)); draw_no = -1; continue if frame is None: continue mt = _TEX.search(line) @@ -49,12 +52,15 @@ def read(path): continue if page is not None and ' v: ' in line: vs = _V.findall(line) + draw_no += 1 + nq = len(vs) // 4 # every group of 4 vertices is one quad; a partial tail is dropped - for q in range(len(vs) // 4): + for q in range(nq): quad = vs[q*4:(q+1)*4] cx = sum(float(v[0]) for v in quad) / 4 frames[frame].append(Quad(page, [(float(a), float(b)) for a, b, _ in quad], - int(quad[0][2][:2], 16), blend, round(cx, 3))) + int(quad[0][2][:2], 16), blend, round(cx, 3), + draw_no, nq)) page = None return dict(frames)