From 7533ac281eba9ca40b5450a3d31b4aa2b9b75f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:21:56 +0000 Subject: [PATCH] linker: skip default palette + rendering enable for non-visual ROMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an `__oam_used` marker dropped by IrOp::DrawSprite codegen, and compute a `has_visual_output` flag in the linker from the marker plus the presence of any user palette / sprite / background. When that flag is false — i.e. a purely audio- or compute-only program — the linker skips both the reset-time default palette load and the `gen_enable_rendering` PPU_MASK write. `gen_init` already leaves rendering disabled, so the PPU stays silent and palette RAM stays in its power-on state. ~72 bytes reclaimed for non-visual programs. Caveat: audio-only ROMs now display an undefined backdrop colour instead of the default-palette black. jsnes renders that as a mid-grey; Mesen/real hardware may vary. Programs that want a specific backdrop should declare their own palette. The golden png for `examples/sfx_pitch_envelope` (the one audio-only example in the set) flips from all-black to all-grey to document this. `__oam_used` is also consumed by the next two commits (default smiley CHR gate, OAM DMA gate), so introducing it here keeps the marker table coherent in one place. Emitting it inline in the DrawSprite codegen path does shift a handful of peephole-block boundaries for programs that draw — pixel goldens flip for `examples/comparisons` by 56 out of 61440 pixels (a one-pixel sprite-position drift caused by accumulated branch-page-crossing cycle drift), a cousin of the audio-hash drift already documented in the prior two commits. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/audio_demo.nes | Bin 24592 -> 24592 bytes examples/bitwise_ops.nes | Bin 24592 -> 24592 bytes examples/bouncing_ball.nes | Bin 24592 -> 24592 bytes examples/coin_cavern.nes | Bin 24592 -> 24592 bytes examples/comparisons.nes | Bin 24592 -> 24592 bytes examples/friendly_assets.nes | Bin 24592 -> 24592 bytes examples/function_chain.nes | Bin 24592 -> 24592 bytes examples/hello_sprite.nes | Bin 24592 -> 24592 bytes examples/inline_asm_demo.nes | Bin 24592 -> 24592 bytes examples/logic_ops.nes | Bin 24592 -> 24592 bytes examples/match_demo.nes | Bin 24592 -> 24592 bytes examples/metasprite_demo.nes | Bin 24592 -> 24592 bytes examples/mmc1_banked.nes | Bin 57360 -> 57360 bytes examples/mmc3_per_state_split.nes | Bin 24592 -> 24592 bytes examples/nested_structs.nes | Bin 24592 -> 24592 bytes examples/noise_triangle_sfx.nes | Bin 24592 -> 24592 bytes examples/palette_and_background.nes | Bin 24592 -> 24592 bytes examples/platformer.nes | Bin 24592 -> 24592 bytes examples/pong.nes | Bin 24592 -> 24592 bytes examples/scanline_split.nes | Bin 24592 -> 24592 bytes examples/sfx_pitch_envelope.nes | Bin 24592 -> 24592 bytes examples/sprite_flicker_demo.nes | Bin 24592 -> 24592 bytes examples/sprites_and_palettes.nes | Bin 24592 -> 24592 bytes examples/state_machine.nes | Bin 24592 -> 24592 bytes examples/structs_enums_for.nes | Bin 24592 -> 24592 bytes examples/two_player.nes | Bin 24592 -> 24592 bytes examples/uxrom_banked.nes | Bin 90128 -> 90128 bytes examples/uxrom_banked_to_banked.nes | Bin 57360 -> 57360 bytes examples/uxrom_user_banked.nes | Bin 40976 -> 40976 bytes examples/war.nes | Bin 24592 -> 24592 bytes src/codegen/ir_codegen.rs | 29 ++++++++ src/linker/mod.rs | 64 ++++++++++++------ src/linker/tests.rs | 38 +++++++++-- tests/emulator/goldens/audio_demo.audio.hash | 2 +- tests/emulator/goldens/comparisons.png | Bin 1129 -> 1023 bytes .../goldens/friendly_assets.audio.hash | 2 +- .../goldens/noise_triangle_sfx.audio.hash | 2 +- tests/emulator/goldens/platformer.audio.hash | 2 +- tests/emulator/goldens/pong.audio.hash | 2 +- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- tests/emulator/goldens/sfx_pitch_envelope.png | Bin 711 -> 730 bytes tests/emulator/goldens/war.audio.hash | 2 +- 42 files changed, 111 insertions(+), 34 deletions(-) diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes index cecd4e05b2337c9ad4842cf263ed27c65bd052e3..1041513e0e30757acd572edfe1002799d3da07da 100644 GIT binary patch delta 236 zcmbPmfN=s4)vz+QGOav3v9ZkfW-HIiL#@0k|F?22Wz=-|AhuGWm&suzM=!I($|opt z0v|+Hu5M*t`FXMyqc!8J$!UycTAy3_T3Lbm`CHjmGPZKJ3NICCZCWbW+PH*sY2!== zrj-mc8JT7>F)eMHe1K8O;qk#<28U-n7nq*$E@jtb`cQt1`$F*ohK+m|I93|;vN-tY zA3FK}gR2kIp;bWn1FbAT(#Fe+m4y=|yE1XI5|frt!Xcn2;|K1gtgU=Y*;@I1-cF8V Zy2*HCvjy`R8&<``3B^S83CWNhVb6<#X1gmbCjOa`Wv3^N&- zW->7?6_|XCQHk;Hnm diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index 9f572767e56f88cd3bd708a4b518403898b4dc66..7921e14728e4bd74bd1a5f17a048072157114e43 100644 GIT binary patch delta 117 zcmbPmfN=s4)i7#rtnFbmP+D2h+SJ;(gmY=*Oa`Wv3^N&-W->7?ZJNo<^o$b%mkMY) zoMgDbx{|?1`_N=ICRI)aC^u?yGLs%-=j2IDR*Xj`pJkF}6qx)HD6)041oJx^R`0_M M2f8=&J1{B$0Kf1kng9R* delta 74 zcmbPmfN=s4)iA1WtnFc(Y{R59S)Yl6QGK!nlOAK(@&;xWR`C(WMW#{usM)%vmI;cL52fc KH}g9%DgXdP9~1ll delta 52 zcmbPmfN=s4)iC-`tUV+oxP)`5;7kUll?*c(nPxIEEfv@t&bZlb@&iUT*8GDE2i9-q IcVJWi0PJ58!~g&Q diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 6ae24af9e7d93ceb6db30ecf4b07cbf0cbf10e30..c80a9f3aa6cc7ae489971604d59c099b684eed61 100644 GIT binary patch delta 214 zcmbPmfN=s4)iCxOE{M{&SYR($uN_VX(kiX(uT=` zjAD$+lQkIaed7!JHrHq;aAGlUBwlXWo zPu5|yXN;bl!zf`=a&RT%2dWT|RjSqk`25kQ@h4x$sJsR-vT=C%1ql?}LOjmkORt0dwDhxKhtVj)^W6 dg&NAZS%~R}?PLLF7Sm$;dR5iD_xmOlGEMoDjHF z;AF!G)|CuC_YX|AXB325Y-^}m8r~m*7b}I@1 delta 118 zcmbPmfN=s4)iBQFTz!NO zty;>id7zaANZNRLv9fR)^s+dtEShZ0q$N~%2q?<7*uZ*}P8{|~M{yoXjTW!F5= z$^s;9yu4UhI1PGP99HH`wq(*0%02`XW&FUsl(m&_DO)SQ&yUTeOt)AV&utbHJ|vS114%7U2;Oe+~?GBV9%Vp_}2yt#yNnjOdg|Nqw>WH|74 JGrt3)0su%z6_Ee{ diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index 03e695c3209c29029e5228d2b123536d6923d7ff..d6f2d25aa624ca54dec8d5acea108f754f222a21 100644 GIT binary patch delta 54 zcmbPmfN=s4)iCl-tgVs|Xl+<3*xI;+b7|vD2Bwt^GZ~p?GBGV}*v!fJ+m2QDAj5&& J&HN6G3IHaT65Id) delta 52 zcmbPmfN=s4)iAP8tgR9fT*A3ja3%xON`{$?Of#97mI`d*IFtNKBP1L>Rj H9T*h=%a;&T diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index fd89a0e357ec4c110dfd10da103c2f401b807e4b..2b8a9cb50eb9b90759e0b08a57f4c67806dd06b8 100644 GIT binary patch delta 52 zcmbPmfN=s4)i82Stj!m1ZConY+OULkX~Rqgrj-mc8JT7>F)eM}_@CXLRr4Ujfy~YP H4vY!_4u=x7 delta 53 zcmbPmfN=s4)iAP7tj!l*D!7Dmso+corj-mc8JT7>F)bC?%*@DcKY0Qp3#;luh6Aaa I`5hP)0M^(K=l}o! diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index d2f40bd080f7d718b6128e43acb80a1e39a52952..9055961fba2b708533173ad62645f67584faf965 100644 GIT binary patch delta 108 zcmbPmfN=s4)i7S!Sli90ud-CAwRx#q-V6t;vQ=I*j2!QhahLlPF^gn6(JV+6ZE8KF?%o%lhRI!vT}c{0@u?09h9# Ap#T5? delta 106 zcmbPmfN=s4)i9phSli90DYsO33FlJbnG8%T8D=sv&17O)Dm0Us=@};kE)_W`w}Evf ygOAGOiA>^?{g^}<1Hr6nAgd0@(qUW;;%~mnWNJG(ftii<-64hpdYkzj7!?4W1S0tW diff --git a/examples/match_demo.nes b/examples/match_demo.nes index 7bdc15851befb810c20e891d4fe78238386b956c..741a551885fd9557d563547de9c1d89d9d3c3407 100644 GIT binary patch delta 236 zcmbPmfN=s4)i7?ISeql|zcQe;X=Ooc;}XuLjWZdTRx-?FWSYsuw6tmBUnyO|gDV+1 z8GNJ;u4HI!SlaM`VZfn(%!VR38S}Jgo5v<4nsOSQ-&#lQ`j7c1`AUYOL-omJ$b_l{b z4btAUvw@JVjKbznH;SxO9dx8 lGOB?%V3m{eAsn#G=DCcyc9R{LxL8jgWH`XRncsm?0RS2n9#H@Q diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index 4c90e5750715d38d0ae9be69b5b6bcfa3c441f46..5cfe88a45383b299ba68a684a55ac6242d2d354d 100644 GIT binary patch delta 80 zcmV-W0I&a$zypxL1F)We0iKhefJOoFljeXS0fUnWfxsT*!A!rvr4WUJr4faJqzR>g mnE(Q*0GR;-nF0c(f|KBZRRJ%vHiFJQ1-`-n!2YujzyTn!>mtek delta 58 zcmV-A0LA~1zypxL1F)We0hp7YfJOo9ljeXS0ezDQfxrRYlURZ!lk|Z{0V%UQg3djW QAO;1v!T`Ydvk$-lAO(gQT>t<8 diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes index 88a24252ca44d597eead7b782e4ba84157731317..a776605e86e3ac79fdf4e29e443daafbb5ed07bb 100644 GIT binary patch delta 235 zcmbPmfN=s4&0*X%ajq-t)Pp|z4@{oLs3qFk*xIy&b7|8|2Bwt^GZ~p?GBGV}oP3qh zM9=TwO2!XN$Ap$LY6^T1S}Jss;REYR2A@=*Feigg-ocd&tqn^XKJcw%Y-LvHo7}?K zD$jTbs}>2MFi?x)WDO=epXP%;<%fXiB;y6aB|Oh~4lrC`Q!oRG08z$@j8z#9K$Cfv zGM{9)z_-%Bm*GG!BM?3CSp(9-w{kv`$mC5-ri`_lUogqpvfetxaKLdhzXPKJ0JQpD AIsgCw delta 181 zcmbPmfN=s4&0*X&ajq-t#DhNj4@{oNn9t;Oa56uWsCeSRm5iJWK3NA>GPE`n`5hP)0GOReumAu6 diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 5fa3196d9e9b43d9ce646bda70ff15bae3feafeb..c63f0c4521d48790e21e2394526da312babc455f 100644 GIT binary patch delta 56 zcmbPmfN=s4)iCBwtlc2Rz}(ujmXW!&c?svz=9vskD;Z`oGR8{E&u=k delta 55 zcmbPmfN=s4)i7pGtlc2W$h?GeEhF&dE&rs`jMKeMkq)cb{f<^SHV>`Mh&8&+1dHZI{@ z+BlPeX(hu}#md5I z(97bmvUu`cMlB)fLqJi+58O*xTlto76=ts+34=t}ukv6aVK z#W|T;1(!;mWc76Ats+34=t|jEv6aVK z#W|T;1(!;mWc;M1& diff --git a/examples/platformer.nes b/examples/platformer.nes index ab755550666f455e16183b68a2d49a6b66d1d99e..d26e6a279dd9326747baf54cc7fbc6ddb032e6af 100644 GIT binary patch delta 848 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 Up=j)HH7?f&%F)cBU<*?J3lA$z>i_@% delta 2663 zcmZ8j4Qv$072dhI^X;AO`!VP9-mQPe7>o~WC&38<1yV<$Mv*t9L4uS9+N%P@F0~3x zXoEs(WP1yZ*vRRSD$u%Z(oUkWSjiC*jZkS~f-!3Yv9Sqfsy0=m&_s#o<)=*}p!Ch` z-a#wbcl*Bgz5h3}H~1DBgugxLFE2d%CfPE}l>BwBmf%>uJU2!0$tL3qGXy*uWv=-=4>p#Z zw3lMjZ?|DHVBdqy5;8Xmh}NwZz-1CV`kYV2cf`TP@Cg;0ef%ZU&tqnQ^?^zJWS04@ z2QZIWtFiq6c2GXR72^mU4RolVk8zRgZ)mjSRrD!*w$YDD8e0znZ2HN;u`E}t4s3T| z2M-FX#5kn67Uba*Wd%=7bX9OwdC7Q|Y0v<{`XSvHyszeU!PV?Fz*V3upV=>-S^toN zjamABa3T7fs_G)7(>qlieT^DwQ%ClU%%{Y$*WMoD$1ePzGj=C5S?2#7K=~?3(+ipW z8nBRJyhg97dZVDarB412)ir<5rG5z1dTrequ2~Obuoyn!69GaaqKrCvcc>W|^t+*I z)J=a9YOQ?9fuI@5Q?L`4ENEn?srRDL)G(A$^Fde!KRG-Og5MJ^jvLn;3AaPs;S=FP z6=f6BOqFJ$iHW^Ek+~D_n+F(Ouysu>%4t z<;pNeL%JC4NO(=?S+k5^Hp_Y6&r56X3{L{1&fai)hie6fudWxHw*ed1C+ziw99Ph_ zrERE(_LVmE{zA~!+nZfex0S;JTVX#|;2#%q1US}4@Mb-RjSnX6O$BXEXiJ6mTSB`@ z1l){`{gc9hJFja%fu%ff*4~Ik6=m3d6aq%ENi${HmR)DTLz$*!X{PtbSBCa^{d=DNG+v*k z!Z>7q4^jyX`LMkmJHEUaBAm#$)-ZNUjfP4v&lbnW<}cta29}B z{Q$ER^zU2`_6m7OXwr6%yDBW@ZORsaVN-hO-U>|}a0QG0wnD$_HCGt9;4Vsjp8b8O zk7>~1XQlzuID$UN+ft@`iGEUXmtH=@%(4Cvk$++aBEku_8nCqhlO$a}ueEo>4C@;k zkowI+4@6VTGTqyoodOGTbR}i%nK_+e)gJz{0-iPw&YVsI?w*gA1zyF{-YSg8*|WGtJLSZvD)4kaGNkB4;!!9r(=@IM9n}X#%d69d)jI4Q+O* zUjP-EI?(;((`jo*Vbq;M4V4I(Zch;OJ9;3|fPP6w5@~ddPQvFjy^`om)?5UlSmXw_ z0IGHGY&eINM(6Uk`3~6eEVO*j+kC(G2A$~VEitiG7o2D&|&4&Ax| zo=@t*7lm8j6}mH-=q_rke+Btv>&qa&X@H3h#|oS=jk0-BFN#Dy>gO!nlUB=;wcN9b?~=yn1l&|=a0$Awbt4Ka3r zUz5OptxR6HQLKg7YQ%il4DxA{T8R|>qt*bI>)*7>#oi`71*2^~RqXkV+-*G%gEvgJ z(7IGrRpT5R80(WMx6DD%g6^S|j-H|2sdjaI&bgWYx^bAko=OJ3GJSlGo=r7Y{CkcC pGjOz7z>KgGGisPe{plKnRvw+Nf4G27>1yD{W#z3`jz-%N`7ew{5F!8o diff --git a/examples/scanline_split.nes b/examples/scanline_split.nes index d39912db33197f1ec6704d547a58704d538181c4..03a34f261f4ed952c891dba3ff7698c1aa922093 100644 GIT binary patch delta 102 zcmbPmfN=s4&0)MXaqc1sjn;;h60MC(IF~lgWMEp!Fq4sKCKJ=rhRK19QL42EeO7`{ z#)^zp84fEMT6vZ-pJceex3avK;Xp4V5IyiYGx-9eDdYOhY)t=bS$`d5IMA_~-+@s9 E0E`wY2LJ#7 delta 76 zcmbPmfN=s4&0)MUaqc1^i6xvXC1x@(tz?+V$TX9QX{E;GaKz9KJ2bwnXJ1{B$08{N83;+NC diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 8989f4dd7037af30a79ccec84c91d0a02c686d59..8f646da10e66bdb49d27cbf9f0926a52bc11cc08 100644 GIT binary patch delta 190 zcmV;v073tdzyXlJ0g!HGsQ`r#g#)Dk@cgL&g#e`k$pG*JOk}|35Ty{wJn#jn08E6y zsR8gLsQ`r#sX&baK&c3g13;|v z45=`U06?h#g$hjUz>)bs2spvX|L{dj3zI+r76EFLR{<&$X~CrjDFN^cr3Hl!r3Qr$ sO!mN&lmVsT%i^$d!em26E#dxyKvBjI+)G(o`IfS^^83gfoSetFOqZWQK!Kl4OpTvYQj?!i xP!Y@*Q8D3{(z4kaG-rNzXPKJ07-5R#sB~S diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes index 59e1e2f0ccc6a5e5165fffb2b95c8fa7e56439c3..d6450df48f2e2a0fffd7a5a37ffba7d70413c7e4 100644 GIT binary patch delta 56 zcmbPmfN=s4)iC-@teqq!(Av0Eu(fFk=hCK`3`{E-XEHLaWSGgsw6t-vGUG!#)|`V3 M2i9!ncVJWi0AfoNVE_OC delta 54 zcmbPmfN=s4)iAnEteqqxxP)`5;7kUlm5ehPnN~8)WMWz>uvwGwq21&QjBKoF2N@15 K+syC4r~m*3+Y%K3 diff --git a/examples/state_machine.nes b/examples/state_machine.nes index 948a84aada64cfd484298f59ee7985f268d6abc2..b8cbf27afead51b199ceed682921ee48ee4dd8ae 100644 GIT binary patch delta 136 zcmbPmfN=s4)i7pGtTpC{K6sMp0<#at#2xZ_DXmQ_16muGa4v0}$-uOdVJ0KfOeUtK zO*5I9o^e9pT1MuRauZlrGWh(M_+OtEV-3M1Peh^$LAd$({DzK8N mm05x5(B!L()r?+~1DT{4oi^t%&9G(dJj8I|6R@ delta 55 zcmbPmfN=s4)i6GtSbL3Ca0%y9!O80wB`23Nsxfv?p2uj&xOejfMk~9?1x#$LtcMs5 LxNhcmU{nAAQG*jE diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes index 15844ed3a898dd8102eafc6675f153c7619e80a2..f6178bc768ebdadc268cc94df13489f9280fbcc1 100644 GIT binary patch delta 78 zcmbPmfOWzF)`lsJ4UG0v8yOdB3$!*Y6>M!>!nw3@CIiz-hM9~^Gntr{Hq2yZdd3NX gKGP1g3apxIzuldYajzX~{2_(|^SARmFfu9t0Dq<&XaE2J delta 75 zcmbPmfOWzF)`lsJ4UCpk8yOd>3NGPXDmasYX(hufvKhbFf+IQTt1uvDnZ{v46 QR@Flc2U0ilA7E4f00C+htYlEQcR+!mAw%Kb=D!Z?_N=0Z7!HJP=5JtB004L3Cb|Fs delta 91 zcmbPmfN8=3rVR}ajPEBmI=EQeKd`c53Fpd+nG8%T8D=sv&17O)Dmasw=@};k`b;{| uDzIwq`vVC}1w2+VDBL)pz|fGPaAPyGBfI^?1U6RwLktJ}H}f|zDgXes_9IgO diff --git a/examples/war.nes b/examples/war.nes index 5020e68675a48d6fbfc460946014523c44d8710f..2e58168296a592b80e6b3d23c34fdf643b8d5f3e 100644 GIT binary patch delta 2850 zcmb7GYitx%6rS1H?apquyM0-9x3rzUXbYtj5GWGKCWwtVQK%%KBvOzcZ6X3DCK#bx zszV?$vE-Wg2+_uHO)j#CsYww`EC}6BZRr+DAArx$1~kGV2K_-&&$%<*ZkwPnX|i*_ z@0@ebx0QADQX&3;37SA9Lx@RgX`dm&59v2-@N+q4NbvKZ{aJC3H0!ja?Teg|wC2b+ zlGbw6MVmhmY_x@ljvU)*sr|P7j+pa(7Zc;@k*K8EBi)kbP@)%9AxiG0ys~mj6sU|y zOscu%)?*SAZ}&^;bJ0jI+V+8Mq+ilf6cG`BqdW=5Optu+tP3BBMg}eJ3OKM-u(z;u zu;-XCh508j{}iT7V)iMld=ksxSUt7tLlSl9!%}1jlN-jpa}E00(6=2x6E3 z$I_-SOn_sVh>;U=E|xH2MHt1f=9V182pqFgLpzMXF&@G6#S)URdOF7t;tnRLr3Q4^ zO-uZZcCAFMF=$E6E%>5k7QfnH9yBOLgHm-PS?^NUw4qZfgP<(L=S6eD+;tKIerS#vi(=rBAYmyAA2Y#VH@K*~Z7 zt2Z-bHr{*IU@c88u$|V@>^0kMEm2rUqU?aROfi?O(|zB!mL8LB(>CxvF>^+2BhU>n zM%+n^;>M8BV3gy#0T&ZEY@1*X0E4NT9*_Yr<+?bZ%m12I=tz#X2(PEp(>zW2?qV%$uE8r+L^_EsY8KeW(7H zlTB!D@NI_nis~w|(x>b{^&s(K9QHzA|}7GVkF0WniG>>*QKy zNpJ$q%Guh)t+K%q^i~qc$`3BP%<1X`z1^aBlXY47;T=df(111JCR}DHpPhpPTFoSo z#qbb{AFnuC!2=IovgyJDa7A|NR@jieV~iHZGbuxj0d*SR;O`$Ej8KN4b8 zk7Ma~;V#kta6%U5R2yc-eSJpUFy^ZNpTK5|I%ftRm=PG`j{SGs@KnZb-W~rC6uu@Q zjxt>Si#Tdcex^8-AI@rdO655%A6nt(1KEX^GoX^>3hEZsC1rPY?6L&Fe0lfQhR8L-8dPkm`Pf#5p@ zZy~&+AgT4;@Jig265dVdy&_2|?~vA61+!0??r;I%e|h(YlVsCFzMECxxtC->?$7F( z-!)?0G;h(j;5vr?M3s(SJa-F?FL}d$9)X=Kg8c5!6&Da!ASAyrYD~metIv>ifgJHF yX$Z{CJU&XD6>yxWL-W!U%?B6b(Lkj*+CC7db%^oyK*5;ekbe|9BkjJBsQd#Ky^t6H delta 2927 zcmaJ@ZBSHI7~Z>gzgb~Lz+DiydPJQTmxB1K^*l@2s+a=YoU#eMelp=Wmy=FVP^L{&-=dT zc|XoM8}5|C@V`sCw7*d)y+h3WB9XtcG@HINhwMsEXFPU`nY|5hVrFuKiO6SSqx{GH zr?!>0^fFWY4mS}KlQmOJR(*Y%1Rs0*H8XrfW10*f@wR8AU9$Ux{EaixAs;lEBPVFs zn)|)=x|B80&84^}5|xQ1(jybA(s5dy=%$mlH&$+n0yQ@hldG@0LNS?3zxT`P>S&}F zZTrAB(k~O6ap7zz+rI%2flk%K8* zFd~d#Sba-@VFZC0($Eeg2uwuKeKCb3td1?zOmQnWsXh%V>|({<1`8=x0~#x?z7F45 zslhMTxEl>h(4a(BX`NH8{S2K_I0WT2qF*cr%$==mh#4LHJ-<_ZOw8qOq<{It@$u;~5y4k%@+D04Q1+$5x?tt_qy^)puAj>yYj`o5e z8<&Ve3ILQuYPS(|z+@^wAZ!*eY&Pey;Kn{6EkIVVKtg*1g68f3-v$~L$ZSI6b$TM) zELz0tjio*^{4Hatw}kIBmN1Jc8w~{{x%X;WG?sT&o zHwHmHQGxFSTuR`uZGzbc3`*5KKmjtW+y#Gx@Otld68crNG(|Kw=fvH zmqDhz37R6Q%V=$yQr6h7W}jq>v z_XB+&tJIc;QD+xI%2?$r74xX%$u$4lPraVJU|tMtl4mz^749u#cvt8oj;KD^Q^Ok> z#gi4aEO zvLf}W8wOaBmLYasi*5q!8G`Nft&eI~oG$B(> zKh03+@pOA(L^rf18(M{76c{K-jE<)lum2Ox%J{m(#r70&!_zv4Zw}qE^NXRs?0k~A zxJ?fe8JCS)YSAykuPWj9vf=j{ePLc+a4XXF%x9E%2QKF-EnRRvs<;f(^z#rNCA^k# zln&^8V;3|piRVp;@b4Mn_sx*yNY2hCLS4pCbX7)Kp6eWZOHm-GE7{C0%t~oZMj&sa zZUVYym^g&V+w`N1%14@oiRjKc7`X`znpxF~qT7*iaA$IGr|>)n3{-f>5O!m!rq8eC zJeWTzN}|6+nFz?rB`xW7^6S_&dN?yfI!Is4%&K^Pw%_Y_@%tdjZ`Hw299wq-S{M4m z4tn*JkIfEr`YwS+k^+H%)U2$adkA#aCVH8J%m3-+3Z0+bOXbQ*lB)!|NFE6CiUMv& z%UQGR5(Nw*vs&MBEtL8bT&Lb+cv^IF=H!@2F3BT>;wpeigSlGFj}SYL!Na_63p_bh z#^@~_hS2HaWrY5M9lng}v%?7l-!XU$;hlx{uj%2Y&^0;Xy#u{hBsKLzdT4(Bf}^@S zTsHV)aeO$b!^i3H{CxL#k^wb)&s4NuFs_X~Y!6(x@C#OH>BS?mSUV<)b^ZeEWDxYW zH%BU>nZBC*H%GL|_~YuMBS)Cs-wF4k-T!cHt<~bJ3R!CSS^AEzJiU1YP&f+AN>Z7P mq^T9O)fbTZ+AjN6Tcy#qlKiU*jpe(ocdnQ^54WY&NXmbormdX- diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 516ca0c..3327865 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -178,6 +178,14 @@ pub struct IrCodeGen<'a> { /// masks; runtime divide only survives for non-constant or /// non-power-of-two divisors. div_used: bool, + /// Set to true the first time we lower an `IrOp::DrawSprite`. + /// Drives the `__oam_used` marker label. The linker reads this + /// to decide whether to splice the OAM DMA into NMI, emit the + /// `$FE`-fill OAM shadow init inside `gen_init`'s RAM clear, + /// reserve the default smiley tile at CHR tile 0, and keep the + /// default palette load. Programs that never `draw` pay zero + /// for any of those paths. + oam_used: bool, /// Source-location markers produced from [`IrOp::SourceLoc`]. /// Each entry is a `(label_name, span)` pair — the codegen /// emits a unique label-definition pseudo-op at the current @@ -357,6 +365,7 @@ impl<'a> IrCodeGen<'a> { ppu_update_used: false, mul_used: false, div_used: false, + oam_used: false, source_locs: Vec::new(), next_source_loc: 0, emit_source_locs: false, @@ -1387,6 +1396,14 @@ impl<'a> IrCodeGen<'a> { y, frame, } => { + // Drop the `__oam_used` marker so the linker knows + // to emit the OAM DMA + shadow-init + default-sprite + // machinery. Programs that never `draw` skip all of + // those paths — no DMA cycles per NMI, no $FE OAM + // shadow fill, no built-in smiley reserved in CHR, + // and the default palette is suppressed too when the + // program has no other visual output. + self.emit_oam_marker(); // Runtime OAM-cursor-based draw. Each frame handler // resets `ZP_OAM_CURSOR` to 0 after the OAM clear; a // `draw` loads the cursor into Y, writes the four @@ -2073,6 +2090,18 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit the `__oam_used` marker label at most once per program. + /// Triggered by `IrOp::DrawSprite`. The linker drops the OAM + /// DMA + shadow-init + default-sprite-tile paths when this + /// marker is absent, shaving cycles out of every NMI and bytes + /// out of PRG/CHR for programs that don't draw sprites. + fn emit_oam_marker(&mut self) { + if !self.oam_used { + self.emit_label("__oam_used"); + self.oam_used = true; + } + } + /// Emit the MMC3 `__irq_user` handler that dispatches on the /// `(current_state, scanline_step)` pair. Supports multiple /// `on scanline(N)` handlers per state — they fire in ascending diff --git a/src/linker/mod.rs b/src/linker/mod.rs index 2a482c6..261c577 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -411,10 +411,28 @@ impl Linker { total_banks, )); + // Whether the program produces any visual output. True if + // the user declared a palette / sprite / background, or if + // user code contains the `__oam_used` marker (emitted by + // `IrOp::DrawSprite`). A purely audio- or compute-only + // program is happy to leave the PPU fully silent — no + // palette load, no rendering enable, no default-sprite + // smiley in CHR — so we gate the reset-time palette + // machinery on this flag. See the sprite-chr / OAM-DMA + // gates below for the other places it cascades. + let has_visual_output = !palettes.is_empty() + || !sprites.is_empty() + || !backgrounds.is_empty() + || has_label(user_code, "__oam_used"); + // Load the initial palette. If the program declared any // `palette` blocks, use the first one; otherwise fall back // to the built-in default palette so sprites show up in a - // reasonable colour scheme without any user setup. + // reasonable colour scheme without any user setup. Skipped + // entirely for programs with no visual output — those leave + // palette RAM in its power-on state (undefined on real + // hardware, zeros under jsnes / Mesen) which is fine since + // nothing gets rendered. // // IMPORTANT: `gen_init` leaves rendering fully disabled so // these $2006/$2007 writes are safe. We re-enable rendering @@ -425,16 +443,17 @@ impl Linker { // about the first 72 bytes of a 1024-byte nametable load. if let Some(first_palette) = palettes.first() { all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label())); - } else { - // No user palette: fall back to a sensible built-in - // palette so sprites show up in a reasonable colour - // scheme without any user setup. Uses the same indirect - // loop loader as the user-palette path (reads a 32-byte - // blob through a ZP pointer) — ~20 bytes of code plus a - // 32-byte data block that gets spliced in below, versus - // the ~170 bytes the old inline-stores path cost. The - // data block lives alongside the user palette blobs so - // the label resolves in the normal assembly pass. + } else if has_visual_output { + // No user palette but the program does render something + // — fall back to a sensible built-in palette so the + // sprites show up in a reasonable colour scheme without + // any user setup. Uses the same indirect loop loader as + // the user-palette path (reads a 32-byte blob through a + // ZP pointer) — ~20 bytes of code plus a 32-byte data + // block that gets spliced in below, versus the ~170 + // bytes the old inline-stores path cost. The data block + // lives alongside the user palette blobs so the label + // resolves in the normal assembly pass. all_instructions.extend(runtime::gen_initial_palette_load(DEFAULT_PALETTE_LABEL)); } @@ -453,8 +472,13 @@ impl Linker { // rendering on. Programs with a declared background get // bg+sprites ($1E); programs without get sprites only ($10) // to preserve the pre-fix behaviour of example ROMs that - // rely on a hidden nametable. - all_instructions.extend(runtime::gen_enable_rendering(has_user_background)); + // rely on a hidden nametable. Programs with no visual + // output at all leave PPU_MASK at $00 from `gen_init` — + // the PPU stays silent, saves 4 bytes and avoids exposing + // an undefined palette on real hardware. + if has_visual_output { + all_instructions.extend(runtime::gen_enable_rendering(has_user_background)); + } // User code (var init + main loop) all_instructions.extend(user_code.iter().cloned()); @@ -571,13 +595,13 @@ impl Linker { for pal in palettes { all_instructions.extend(runtime::gen_data_block(&pal.label(), pal.colors.to_vec())); } - // When the program has no user palette, splice the built-in - // default palette blob under `__default_palette` so the - // reset-time loop loader above can resolve it. Programs - // that declare a palette fall through the user path and - // skip this entirely — saves 32 bytes of ROM data when the - // default is unused. - if palettes.is_empty() { + // When the program needs the built-in default palette (i.e. + // it produces visual output but declared no palette of its + // own), splice the 32-byte blob under `__default_palette` + // so the reset-time loop loader above can resolve it. + // Programs that declare a palette OR have no visual output + // skip this entirely. + if palettes.is_empty() && has_visual_output { all_instructions.extend(runtime::gen_data_block( DEFAULT_PALETTE_LABEL, DEFAULT_PALETTE.to_vec(), diff --git a/src/linker/tests.rs b/src/linker/tests.rs index 61f7524..47f430c 100644 --- a/src/linker/tests.rs +++ b/src/linker/tests.rs @@ -634,14 +634,17 @@ fn link_banked_chr_rom_survives_with_switchable_banks() { #[test] fn default_palette_blob_present_when_no_user_palette() { - // With no user palette, the linker emits the shared reset-time - // loop loader (which writes twice to `$2006` and loops writing - // through `$2007`) and splices a 32-byte `__default_palette` - // data block into PRG. The end-to-end ROM should contain the - // default palette bytes verbatim at some offset in the fixed - // bank. + // With no user palette but some visual output (the IR codegen + // drops an `__oam_used` marker whenever it lowers a `draw`), + // the linker emits the shared reset-time loop loader and + // splices a 32-byte `__default_palette` data block into PRG. + // The end-to-end ROM should contain the default palette bytes + // verbatim at some offset in the fixed bank. let linker = Linker::new(Mirroring::Horizontal); - let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let user_code = vec![ + Instruction::new(NOP, AM::Label("__oam_used".into())), + Instruction::new(NOP, AM::Label("__ir_main_loop".into())), + ]; let rom = linker.link(&user_code); // The first four bytes of DEFAULT_PALETTE are {0x0F, 0x00, 0x10, @@ -654,6 +657,27 @@ fn default_palette_blob_present_when_no_user_palette() { assert!(found, "default palette bytes should appear in PRG"); } +#[test] +fn default_palette_suppressed_when_no_visual_output() { + // An audio- or compute-only program (no palette declared, no + // sprites, no backgrounds, no `draw`) shouldn't pay for the + // built-in default palette: no `__default_palette` label in + // the symbol table, and the 32 bytes of palette data must not + // appear anywhere in PRG. + let linker = Linker::new(Mirroring::Horizontal); + let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let result = linker.link_banked_with_ppu_detailed(&user_code, &[], &[], &[], &[], &[], &[]); + assert!( + !result.labels.contains_key("__default_palette"), + "default palette label must not be defined without visual output" + ); + let prg = &result.rom[16..16 + 16_384]; + assert!( + !prg.windows(4).any(|w| w == [0x0F, 0x00, 0x10, 0x20]), + "default palette bytes must not appear in PRG when suppressed" + ); +} + #[test] fn no_default_palette_blob_when_user_palette_present() { // A program that declares its own palette should suppress the diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 7fbc913..9ed40e1 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -e064fdf0 132084 +dceef796 132084 diff --git a/tests/emulator/goldens/comparisons.png b/tests/emulator/goldens/comparisons.png index 9096daf8d1277685f1cc73f1c0a1433863bc755b..e0dd216d6e127fa957af49cba891032d6426b263 100644 GIT binary patch literal 1023 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5K5(!B$@kNDon~NQKIZA-7*fIb_Ta|e=tw4( z1M`ka-B0E5Y5eJaKtzK1?(>%ljLa9Ty^~k0ex-eKJ;P+FgfQkaSq+iA2CEs~tN%4x$=hW<4r~PM6v7cS+^{Mkq&YfKTde^20B!(^403YgtpMmLx v>CbGKH{w0(SZ7?pk+@)r2q(jlpz^Ez6qUB0aTdXUK>6F#)z4*}Q$iB}X#&%N literal 1129 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5K5(!B$@kNDon~NQG4OP845?szdvIf}j3Wcf z!DkJ}c8MQeTJY0-gJurT^L6fxEOHgzlMb_=c)7~_0kh}SrX^91L2DIOhD`|3_E;sV za+PaR7OUr09K!c5PTy;O{+Psz(r@={q?12*Rp-y^HO%!)*pWOU-k3{l^+pR^_`R`0E&vmyy-m~L+%KP4Z*Fh@#K0kVB zq5sbGX7%U0-|pSXE}A3Gf4s7A{mFZ?@9Vudy;qI@`>YpR-5zf z*-eRNHlY4VTDSuUN4O1l;VSoEtl$)}pq~2@|Mu_C;wD%7zBAnkisOH}9`-j-qj~Xu z?o0Xw>rWoutM<#@@fRy30eq>yH?gh}oE$DgBn}>v07mU{kWoKN=7=Zrd?^QpKiGns u63l;Z10!jP7YWICxJLutyo=5Ae;IjR?zsKN>S!&fB=L0hb6Mw<&;$VHYyC?A diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index 0dca2ac..c671279 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -68089bdb 132084 +a9a829f7 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index ee351f8..435c42c 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -7b0caf36 132084 +be86c2a2 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index 359b2c8..4bbfe8e 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -5cc6d0f5 132084 +c9c2333d 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index 778c04f..1145110 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -714f679e 132084 +b7d5b53f 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index a4eb4f4..8d0c60d 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -c1ba0463 132084 +9c881965 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.png b/tests/emulator/goldens/sfx_pitch_envelope.png index ab3d666aab2a51d615c2403a483fa4feab94dca8..2842948ccdf7d5a439ad16ba58e83449291531c9 100644 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5K5(!B$@kNDon~NQTIlKG7*fIb_F$msAqNKL z4VTPYg{#C4GX8E;t5mIIpK*m@vs6MD^O>xMNM3{0jA^C^)@bv;`_1=Z_BqR+%fNiL x8CNkCjS8ZL+Gv2_3y^TRImJwWmSYA|ErZlF_P385%6LIZ*VEO{Wt~$(695mB4|xCp literal 711 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5K5(!B$@kNDon~NQ>hpAQ45?szdytXwfP=u{ z1#kXT|KrsB5UQVWiS^8`hDdRP*Gy@#2i9=UC}rHNmvC)Va5O+@ml*1-