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.
This commit is contained in:
John Smith
2026-09-11 14:48:55 -04:00
parent 161f129f6a
commit e889cd8b55
+171
View File
@@ -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:
<mine>/_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())