mirror of
https://github.com/project-dorado/dorado-hd.git
synced 2026-10-11 21:10:01 +00:00
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:
@@ -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())
|
||||
Reference in New Issue
Block a user