#!/usr/bin/env python3 """Render data/jarscan.json into ranked markdown tables (data/jarscan-report.md).""" import json, math, sys d = json.load(open(sys.argv[1])) out = open(sys.argv[2], 'w') P = lambda *a: print(*a, file=out) FRAME_EVENTS = ('RenderLevelStageEvent', 'RenderFrameEvent', 'RenderGuiEvent', 'RenderGuiLayerEvent', 'CustomizeGuiOverlayEvent', 'RenderLivingEvent', 'RenderPlayerEvent', 'RenderHandEvent', 'RenderArmEvent', 'RenderHighlightEvent', 'ViewportEvent', 'RenderNameTagEvent', 'RenderBlockScreenEffectEvent', 'RenderTooltipEvent', 'ComputeFovModifierEvent', 'WorldRenderEvents', 'HudRenderCallback', 'HudLayerRegistrationCallback', 'ClientGuiEvent', 'GuiDrawEvent') TICK_EVENTS = ('ClientTickEvent', 'PlayerTickEvent', 'EntityTickEvent', 'LevelTickEvent', 'ClientTickHandler') def base_events(r, names): return sorted({e.split('$')[0] for e in r['events'] if e.split('$')[0] in names}) rows = [] for slug, r in d.items(): if r['dir'] != 'mods': continue mx = r['mixins'] client_side = r['side'] != 'server' t = r['tex'] a = r['assets'] fi = r['frame_injectors'] rows.append(dict( slug=slug, side=r['side'], mix=sum(mx.values()), cmix=mx.get('client', 0), frame=r['frame_mixins'], risky=fi.get('Overwrite', 0) + fi.get('Redirect', 0), fev=base_events(r, FRAME_EVENTS), tev=base_events(r, TICK_EVENTS), atlas=t.get('atlas_px', 0), atlas_n=t.get('atlas_n', 0), anim=t.get('animated_n', 0), ent_px=t.get('entity_px', 0), tex_mb=t.get('bytes', 0) / 1e6, models=a.get('models', 0), bstates=a.get('blockstates', 0), shaders=a.get('shader_files', 0), post=a.get('post_effects', 0), core=len(r['core_shader_overrides']), geo=a.get('geckolib_files', 0), ogg_mb=a.get('ogg_bytes', 0) / 1e6, particles=a.get('particle_defs', 0), ents=r['lang'].get('entity', 0), blocks=r['lang'].get('block', 0), items=r['lang'].get('item', 0), jar_mb=r['jar_bytes'] / 1e6, client_side=client_side, top=', '.join(f'{k}×{v}' for k, v in sorted(r['frame_targets'].items(), key=lambda kv: -kv[1])[:6]), )) cl = [r for r in rows if r['client_side']] SP = 256 # one 16x16 sprite P('# Static client-perf surface scan') P() P('Generated by `scripts/jarscan.py` + `scripts/report.py` from the exact jars pinned in `mods/*.pw.toml`.') P('All numbers are static (what the jar *contains*), not runtime cost. They tell us where cost *can* come from.') P() P('- **frame mixins** = mixin classes whose `@Mixin` target is on a per-frame path (LevelRenderer, GameRenderer, entity/item/block-entity renderers, Gui, ParticleEngine, blaze3d, Sodium, Iris, Camera).') P('- **risky** = `@Overwrite`/`@Redirect` injectors inside those frame mixins (replace vanilla logic; most likely to conflict with Sodium/Iris/ModernFix).') P('- **frame events** = NeoForge/Architectury/Fabric per-frame event types referenced by the mod’s classes.') P('- **atlas sprites** = block+item textures in 16×16-sprite equivalents (what lands in `blocks.png-atlas`, mipmapped, in VRAM).') P() tot_atlas = sum(r['atlas'] for r in cl) tot = lambda k: sum(r[k] for r in cl) P('## Pack totals (client-loaded mods)') P() P('| metric | value |') P('| --- | --- |') P(f'| client-loaded mods | {len(cl)} (server-only: {len(rows) - len(cl)}) |') P(f'| mixin classes (client-loaded) | {tot("mix")} |') P(f'| frame-path mixin classes | {tot("frame")} |') P(f'| frame-path @Overwrite/@Redirect | {tot("risky")} |') P(f'| mods hooking ≥1 per-frame event | {sum(1 for r in cl if r["fev"])} |') P(f'| mods hooking client/player/entity tick events | {sum(1 for r in cl if r["tev"])} |') P(f'| block+item textures | {tot("atlas_n")} files, {tot_atlas / 1e6:.1f} Mpx ≈ {tot_atlas // SP} 16×16 sprites |') side = 2 ** math.ceil(math.log2(math.sqrt(tot_atlas))) if tot_atlas else 0 P(f'| implied blocks atlas (lower bound, + vanilla) | ≥ {side}×{side if side * side // 2 < tot_atlas else side // 2} ; ×4 B ×1.33 mip ≈ {side * side * 4 * 1.33 / 2**20:.0f} MiB VRAM at the square size |') P(f'| animated block/item textures | {tot("anim")} |') P(f'| entity textures | {tot("ent_px") / 1e6:.1f} Mpx |') P(f'| models / blockstates | {tot("models")} / {tot("bstates")} |') P(f'| entity types (lang proxy) | {tot("ents")} |') P(f'| blocks / items (lang proxy) | {tot("blocks")} / {tot("items")} |') P(f'| geckolib geo+animation files | {tot("geo")} |') P(f'| vanilla core-shader overrides | {", ".join(r["slug"] for r in cl if r["core"]) or "none"} |') P(f'| post-effect files | {", ".join(r["slug"] + f"({r['post']})" for r in cl if r["post"]) or "none"} |') P() def table(title, key, cols, n=25, filt=lambda r: True, note=''): rs = sorted((r for r in cl if filt(r) and r[key]), key=lambda r: -r[key])[:n] P(f'## {title}') P() if note: P(note) P() P('| mod | side | ' + ' | '.join(c[0] for c in cols) + ' |') P('| --- | --- | ' + ' | '.join('---:' if c[2] else '---' for c in cols) + ' |') for r in rs: P(f'| {r["slug"]} | {r["side"]} | ' + ' | '.join(c[1](r) for c in cols) + ' |') P() table('Per-frame mixin surface', 'frame', [ ('frame mixins', lambda r: str(r['frame']), 1), ('risky', lambda r: str(r['risky']), 1), ('all mixins', lambda r: str(r['mix']), 1), ('hottest targets', lambda r: r['top'], 0)], n=40) P('## Per-frame event subscribers') P() P('| mod | side | frame events | tick events |') P('| --- | --- | --- | --- |') for r in sorted((r for r in cl if r['fev']), key=lambda r: (-len(r['fev']), r['slug'])): P(f'| {r["slug"]} | {r["side"]} | {", ".join(r["fev"])} | {", ".join(r["tev"])} |') P() table('Block/item atlas pressure', 'atlas', [ ('sprites (16² eq.)', lambda r: f'{r["atlas"] // SP:,}', 1), ('files', lambda r: str(r['atlas_n']), 1), ('animated', lambda r: str(r['anim']), 1), ('% of pack', lambda r: f'{100 * r["atlas"] / tot_atlas:.1f}', 1), ('models', lambda r: str(r['models']), 1), ('blockstates', lambda r: str(r['bstates']), 1)], n=30) table('Animated atlas textures (uploaded every tick unless culled by Sodium)', 'anim', [ ('animated', lambda r: str(r['anim']), 1)], n=20) table('Entity content (renderer + animation load)', 'ents', [ ('entity types', lambda r: str(r['ents']), 1), ('entity tex Mpx', lambda r: f'{r["ent_px"] / 1e6:.2f}', 1), ('geckolib files', lambda r: str(r['geo']), 1)], n=30) table('Shader assets', 'shaders', [ ('shader files', lambda r: str(r['shaders']), 1), ('post effects', lambda r: str(r['post']), 1), ('core overrides', lambda r: str(r['core']), 1)], n=30) table('Sound payload', 'ogg_mb', [('ogg MB', lambda r: f'{r["ogg_mb"]:.1f}', 1)], n=15) table('Jar size', 'jar_mb', [('MB', lambda r: f'{r["jar_mb"]:.1f}', 1)], n=20) P('## Resource packs / shader packs') P() for slug, r in d.items(): if r['dir'] == 'mods': continue t, a = r['tex'], r['assets'] P(f'- **{slug}** ({r["side"]}): atlas sprites {t.get("atlas_px", 0) // SP:,}, entity tex {t.get("entity_px", 0) / 1e6:.2f} Mpx, ' f'models {a.get("models", 0)}, blockstates {a.get("blockstates", 0)}, CEM/EMF files {a.get("cem_files", 0)}, shader files {a.get("shader_files", 0)}') P()