From e889cd8b55138e343b48048fb10f962e415ee6a7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Fri, 11 Sep 2026 14:48:55 -0400 Subject: [PATCH] tools: classify official apps by render stack (framework vs ZDKGL) Scans the external decompiled corpus and reports per-app framework/GL classification plus the Zune framework families each app uses. Writes a prose-only summary for dorado-emu docs. --- tools/app_render_stack.py | 171 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tools/app_render_stack.py diff --git a/tools/app_render_stack.py b/tools/app_render_stack.py new file mode 100644 index 0000000..587f7d5 --- /dev/null +++ b/tools/app_render_stack.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Classify official Zune HD apps by render stack. + +Reads the external decompiled corpus produced by ``app_mine.py`` and answers a +single question per app: does its managed code render through the framework +``SpriteBatch`` surface (which Dorado's clean-room XNA shim can satisfy), or +does it call the Zune-specific ``Microsoft.Xna.Zune`` GL layer, which +P/Invokes native ``ZDK`` (``ZDKGL_*``) on the device? + +The classification is a *reference* scan of decompiled IL: no Microsoft code or +asset bytes are copied, and only app slugs / counts / type names are written. + +Outputs: + /_render-stack.json machine-readable per-app result + dorado-emu/docs/official-app-stacks.md prose summary (committed) + +Usage: + python3 tools/app_render_stack.py + python3 tools/app_render_stack.py --doc ../dorado-emu/docs/official-app-stacks.md +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +WORKSPACE = REPO.parent +DEFAULT_MINE = WORKSPACE / "Zune HD Apps (Decompiled)" / "_mine" +DEFAULT_DOC = WORKSPACE / "dorado-emu" / "docs" / "official-app-stacks.md" + +GL_TYPE = re.compile( + r"\b(GlSpriteBatch|GlSpriteFont|GlTexture|GlFont|GlModel|GlEffect|" + r"GlGraphicsDeviceManager|Gl.*Reader)\b" +) +ZDK_DLLIMPORT = re.compile(r'DllImport\(\s*"ZDK"') +ZUNE_NS = re.compile(r"\bMicrosoft\.Xna\.Zune(\.Content)?\b") +FRAMEWORK_FAMILIES = ("ZuneAppLib", "ZuneCoreLib", "ZuneGamesLib", "Noodles") + + +def source_files(root: Path) -> list[Path]: + return sorted(p for p in root.rglob("*.cs") if p.is_file()) + + +def classify_app(mine: Path, slug: str) -> dict: + app_dir = mine / "_decompiled" / slug + files = source_files(app_dir) if app_dir.is_dir() else [] + gl_hits = 0 + zune_hits = 0 + zdk_hits = 0 + gl_types: set[str] = set() + for path in files: + text = path.read_text(errors="ignore") + for m in GL_TYPE.finditer(text): + gl_hits += 1 + gl_types.add(m.group(1)) + zune_hits += len(ZUNE_NS.findall(text)) + zdk_hits += len(ZDK_DLLIMPORT.findall(text)) + + families: set[str] = set() + index_path = mine / "_index" / f"{slug}.json" + exe = "" + if index_path.exists(): + try: + index = json.loads(index_path.read_text()) + for asm in index.get("assemblies", []): + name = str(asm.get("name", "")).removesuffix(".dll") + if name in FRAMEWORK_FAMILIES: + families.add(name) + if asm.get("role") == "app": + exe = str(asm.get("name", "")) + except json.JSONDecodeError: + pass + + stack = "gl" if (gl_hits or zdk_hits) else "framework" + return { + "slug": slug, + "exe": exe, + "stack": stack, + "gl_references": gl_hits, + "gl_types": sorted(gl_types), + "zune_namespace_references": zune_hits, + "zdk_dllimports": zdk_hits, + "framework_families": sorted(families), + } + + +def write_doc(path: Path, apps: list[dict], mine: Path) -> None: + framework = [a for a in apps if a["stack"] == "framework"] + gl = [a for a in apps if a["stack"] == "gl"] + lines = [ + "# Official app render stacks (external corpus)", + "", + "Generated by `dorado-hd/tools/app_render_stack.py` from the external", + "decompiled corpus (`Zune HD Apps (Decompiled)/_mine`). It records which", + "apps render through the framework `SpriteBatch` surface versus the", + "Zune-specific `Microsoft.Xna.Zune` GL layer that P/Invokes native `ZDK`", + "(`ZDKGL_*`, implemented on device by `zdksystem.dll`).", + "", + "No Microsoft code or assets are reproduced here; only app slugs, counts", + "and public type names.", + "", + f"- **Framework-path apps: {len(framework)}** — runnable against the", + " clean-room XNA shim once the gap list in `official-app-corpus.md` is", + " satisfied.", + f"- **GL-path apps: {len(gl)}** — additionally require a `ZDKGL_*` bridge", + " (Dorado ships a no-op loader first; real rendering is a follow-up).", + "", + "## Framework path", + "", + "| App | Exe | Zune frameworks | Notes |", + "|---|---|---|---|", + ] + for app in framework: + fams = ", ".join(app["framework_families"]) or "—" + note = ( + f"{app['zune_namespace_references']} Zune-namespace references" + if app["zune_namespace_references"] + else "no Zune GL references" + ) + lines.append(f"| `{app['slug']}` | {app['exe'] or '—'} | {fams} | {note} |") + lines += [ + "", + "## GL path (ZDKGL)", + "", + "| App | Exe | GL type references | ZDK DllImports |", + "|---|---|---|---|", + ] + for app in gl: + lines.append( + f"| `{app['slug']}` | {app['exe'] or '—'} | {app['gl_references']} " + f"| {app['zdk_dllimports']} |" + ) + lines += [ + "", + "The ZDKGL bridge entry-point inventory is generated from the app-local", + "`Microsoft.Xna.Zune.dll` IL by `dorado-emu/tools/gen_zdk_bridge.py`.", + "", + ] + path.write_text("\n".join(lines)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mine", type=Path, default=DEFAULT_MINE) + parser.add_argument("--doc", type=Path, default=DEFAULT_DOC) + args = parser.parse_args() + + decompiled = args.mine / "_decompiled" + if not decompiled.is_dir(): + raise SystemExit(f"corpus not found: {decompiled}") + + slugs = sorted( + p.name for p in decompiled.iterdir() if p.is_dir() and p.name != "_framework" + ) + apps = [classify_app(args.mine, slug) for slug in slugs] + out = args.mine / "_render-stack.json" + out.write_text(json.dumps({"apps": apps}, indent=2) + "\n") + write_doc(args.doc, apps, args.mine) + + fw = sum(1 for a in apps if a["stack"] == "framework") + print(f"apps={len(apps)} framework={fw} gl={len(apps) - fw}") + print(f"json: {out}") + print(f"doc: {args.doc}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())