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)