From 033d399565fd124d548d60a8bd409bfb3f610e2a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:01:19 +0000 Subject: [PATCH 1/9] runtime: gate __multiply / __divide on usage markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop __mul_used from IrOp::Mul codegen and __div_used from IrOp::Div / IrOp::Mod codegen (modulo reuses the same routine). The linker skips gen_multiply / gen_divide for programs that never emit the markers, following the same pattern already used by __audio_used / __ppu_update_used / __sprite_cycle_used. The optimizer already rewrites multiplies and divides by constant powers of two into shifts (and modulo by constant powers of two into masks), so the markers only fire for genuinely runtime math. A program like `examples/comparisons.ne` that never multiplies or divides now reclaims ~56 bytes of PRG; programs that use only one of the two reclaim the other's share. Audio goldens flip for every example that uses audio. The .ne sources are unchanged and the pixel goldens are byte-identical — the audio stream differs only because removing the math routines shifts the audio tick's absolute address in PRG by 56 bytes, which changes which of its internal branches cross 6502 page boundaries and therefore the per-frame cycle count of a single NMI by 1-5 clocks. Over 180 frames the accumulated drift shifts APU register write timing enough to render a different digital sample stream at the same logical wave shape. Expected consequence of ROM-layout change under cycle-accurate emulation; documented path per CLAUDE.md "Updating goldens". https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/arrays_and_functions.nes | Bin 24592 -> 24592 bytes examples/audio_demo.nes | Bin 24592 -> 24592 bytes examples/auto_chr_background.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/loop_break_continue.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 | 61 +++++++++++++++++- src/linker/mod.rs | 19 +++++- tests/emulator/goldens/audio_demo.audio.hash | 2 +- .../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 +- tests/emulator/goldens/war.audio.hash | 2 +- 41 files changed, 80 insertions(+), 12 deletions(-) diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index 9faa196a2b234427b41312f28bdc4913e300d6e0..481c3dae9b02dff602deb6b3ae11b70dd8c83ca1 100644 GIT binary patch delta 397 zcmZ|KJx{_w7zglP|5xZypf4g{$!hQy1#8eh}!@F delta 355 zcmZ9Iu};G<5Qbx)owN(ku%u~=A_hk03=9>jn6mK#l@LgI1H=MKbGxNX8CVcEgsDPCNojI-)!RE(r^=hr93duRB?lOqFbT#p1P&W}3u@Z1@tQ8%=Uf!rTtr85h=u z$*|sG^hn~6Z1l;&5qT)cM;XK#$22s}sBI=Ru6hchH%*hS$oBZ!FkXKe+mI99p%MUGqRI3y`$&@?vG-H0Wh< zSoyq_Y2~5G_n6d#o*e?pFn-`(%G%1el&zKDr+Bg$^G-&u%@>%hY$hwP=&-gQW;k$a JGrt3)0swJFR}26E delta 282 zcmbPmfN{bB#tqGkvfZsbD-XBwuKeH1wUklQ;e*&pgBFe(55aZx2| diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index 9f298a82e1a8ea711be62805f0f8e6574a853269..50571da45259abf0d18d81e4f7c2a834475a6938 100644 GIT binary patch delta 491 zcmYk1Jxc>I9LE1iD0oG^I~QLHT8Dy%eguWvTJ@GLZZ1Nh;1}rD+|_Ir3Ptcn!3-G+ zg#%an0@5mqA3+=hmlE&`m|U)KWXhB0_v1-#gnA=1tfLESSP&4Lj|GzW#pTGOa)Y~b zkUG9nu?4~PSOAN7B#YcR^_e4Vrkj4Qd251ETe4iB=3)K#XvVv1rBw z|5)az0cTxA1avVOVDgSg`iVfJRdK+m3RxtIEaXv*hYUB%_fyv4bgR)}p_nQ6Ea&E- eaj&L&ffkqqC;dyZm&BxcTu7!c`b%sdmHz=5D7T0J delta 464 zcmYk2KTE?v7{>2UTH7p1lMKZ!QD`w;T`V}LgS2u8#nr*3(2vkXOjEAlWFR1P6>qr7 zLMOK-!O}R@OmqW_D5$29Tuhxe68Fad~z2}Bcwcz=RXT+R;DE~qYc z8O(UoMUl&pABev(+FYinZ+1gEV#8{0L>;~qoMn!~oE#@Mh#Zn#pGpFTpGxP*-KjPi u?nx9ErT10B@XdBslp2`fqaWmoVJwEfOqvC_ee9W1vv41Rc{Ec_p!p9gg{(mU diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index 9959992e77d3dc8f65b6ffa81259ca0538074701..74adae093070457e3a01c70533dbb48b3916f0b3 100644 GIT binary patch delta 161 zcmbPmfN{bB#tq?&^{guyeEbitWc(oUOkkxLOyi|kAcJ)K7hn4 zAmWUzLQ92DGJ=J=A$kRu3Z8rb);kBLSJ-FQ!DAqCh~6zQy~0ZcmvAl>oXNnnl40iL cO^l+O|1)yiO%7lZVZD8j;ehmJeg{Sc08fBGy8r+H delta 162 zcmbPmfN{bB#tq?&-poF+2cHQn6*&1}1G7)m!IccHLOwYMkAXxOJ}?8BD;ZmbmI|L_ z1ajIBJ`-Fjc=7>IWeZ59u+OxE$3P+=mD3JRKE|kO(aHjpU|z)G#x#LlVkvVfODi)Q y%c%<=mNF;6q}W*0n3pnJJY|{ylzax1e7BS(Ve@N7ZadcZ2N@3NZ{~MkQ~&@7K|c8a diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 8fcf88a589f1ad9e455d1364b00864bed6ab4579..230f49761276601fbfd91d97619d3642a089cdab 100644 GIT binary patch delta 278 zcmbPmfN{bB#toi~`uZmsFEIP;Ie3x@NL@YTqjPX6OD_ZSS_bBmA3ktyU|q@JlW=e) z;|J!I3<*HN$=Qt7jH@RvXVj~gS}Gus$&=2n5YQqM$=i7pj| eTG$H`VxOGE)Vf)o`I!x?$YF*9A)EOf7!?4)du=fQ delta 278 zcmXwxu}i~16vp%JU8|G5BrRs>AVma0TwDS=bP|LE=WH45;D6v|m)iqboD_sNWU|oy z0m%u%b?Pi#4Y-s82UlN3r}y}Nzwaw*C~BBp!D!@Qh5Ds&g3p81Ck;vGy_liSnkvXi zmUnOtkL3bR@B5^ZTVd3n@ZMD3D1u|*_er^J=c^%=ezKy!xy>+kfk!>G6eg3%gl**3 ztPO3oHem%OW+~!@sSjBNR?jR)aV0rcZozKGU3as6_H5gK8_?i)GPC8AFsYHOn+a diff --git a/examples/comparisons.nes b/examples/comparisons.nes index 84044f6270c6fa061645e05cd76d75c768ab96ef..99c527b5476755f25cfc2822ba35f60d172c282a 100644 GIT binary patch delta 123 zcmbPmfN{bB#tqqwT&yb@e3B2YWc(mEIiFD(%$)$?PJ?pyLb!Vwm3i1FfRw+12s{G{ raBctzNFM_0=VyZG_keKCm=rhnGgaG8e!wKcnth1j!0OHX4vY!_yWT5) delta 140 zcmbPmfN{bB#tqqwEX+Q&2Pc;@Dgzly4o+SKW}H4a`8=aC8`B0wpKAvv{{u6bePjI;m1QMEEAt`_H>L^f5=)s=Sz4LdSWaE|u#`CgCdJ00#=Ml-;wjSv epyV^426rznR~GQ2_w#GA|?m diff --git a/examples/friendly_assets.nes b/examples/friendly_assets.nes index 803da81e95c34f24d4126bf820f7305a066cd075..fe80777f63a1e650004d4fba4a0d3bba05b94038 100644 GIT binary patch delta 269 zcmbPmfN=s4)iCl-tW{vtoj6aG^=&KD%3~8xRMbCdCm3QU;R<5OtnhqbtRyy=D zIjrR9Wp-GJEGKo6>jUdb2A`CJD;Yn?E|vbk?9+U3cD+`dc@$zD2;WX%Faafr%Ii5*NIQtM#l<@=iQr1?!rEIPI kK0gj_p2KvLh4K96GWIDJlNC60Sapvx9JsWZ-+@s90AeL;%K!iX delta 321 zcmZ|Hze~eV5C`zQA4%FYHi9B18QMXGIu%@6aCnOjA-H$zqO)TMi_?vPgb3P2iu4Vq zljI*r<4~(hSN{MVC5w|_eF%b!aNNOt?!IwOVpV6Pn+%(zmt1e_E>N5$kAw47u<-#_ za0!+qEm3Zuh^W9y#035|Z*G+=98RVGGw=>f^q~&A`gXuT0yXfZHy*N*(rVDjQA;HrFTm<^53YOx6t)Hmi$ZCZRcWSm%n2|lHWoGJrOXyjnI-@wp8+M`0e!Gp Tlj)KztHvRQ0~wq79T*h=RE1kW diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index 97eaccfae1fd468a68bcab0f28c6df9e5cb1e140..09872a2a4f288349373c004dc2745718bb73b081 100644 GIT binary patch delta 29 ncmV+&0OJ3UzyXlJ0kES1v)chLK9ev4I0lo!0Kn71qYywKwnz)H delta 82 zcmbPmfN{bB#tn-ZEm~PtGPE);;&5Y{z%H?rIhCcAnT_Stg%3-a6JSzoENaY4nJu0& gO#n(h14_PI%95~oF{8d6+v$T02e=O{7I07i0Er76ssI20 diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index 97e2b5fb81d91b4227156734fd14b224e7dd52fb..2c19839e68c715f207c57e6384b97a5324a1d27e 100644 GIT binary patch delta 58 zcmV-A0LA~1zyXlJ0kAs(7#K_*!KDO^00XT61C0R#r4$MZg%qs;1CvJqPP5GcJwB5l Q0xSiQ!2rO~vkyQ4APYqii~s-t delta 99 zcmbPmfN{bB#tqhtJUl*f2bZ$6ig0nYicAh>^s{JXS;^4Kyoke%X#%^%Qsz{aR%SMq xQx`rgWln%ev9YKzFJ-oP$}|Be`3xxeZYfK`=7o&5cC333G9383ncsm?0RR%8Am#u7 diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index ccdfff4736331a453e3b89f5f87a90758f29507f..460671b92d83d26d8a2895e357ba22185a3c6ff1 100644 GIT binary patch delta 360 zcmbPmfN{bB#tn&#^_&}6S2FlSA6&`!f$5moQbtXI4?;`DPBMG|2^Rr{IT?Ja53Xcr zZCcv&L3pX)2X-J^(5DY5(<-=9q!pya0ZGeJh?aFAEe%T>K5zhO0jRnLBz0#Y>h!L` z%;o?Z2vG=ffgsccpCQWrKvhlVU{VifUCHQUb7-jmREfnQkR!pCG5e$+S}J-{4(z7@ zp!f$-p!K3lMNWR00P;^VNQmi~$Wml0D!}@E8YfpU>2QHmZ2+l~o4kTa8O*&4;hu+b mxekM6zd^a?5Uwn<;^s2uDK?WYF!QiR9cDN%YcszCqXGcZL5kG? delta 348 zcmbPmfN{bB#tn&#kxUzyeIgGY6I;rtDeysPsn|(|53DO0d~y!1WaMPo z;|JlTf*;s{Y(bw6piHaaN|9EO76&9P^C4PRfV4C%ZJK<5QPT7{MC=007!IIb0kB?> zWr9%4-a=G;gQ}X$$E5Db>|=6hsQ^@g!6A@qK&F0R_6a_;RP>}A(AmL7~ z0*LcWWGS+tDPTQ5xs!XCbXb^uwjG*$h)Eg9xO-^wdoY9L@MIZgkcjr-$)3!L7OgBm z?J}UiZ0G|n; z8)C-I!A%u;NXd#G@K9_iIqYN~at~@dM#}k0!FO|i(@(KKuqEZ&YL@S)*iD2aI-TnD zTl5_z6E>P6=1sSfW`)`z#HvE)FP506gh`T8mNF*76xf&5J<0yinvS+$qeG+}uMsZQ k>YCP_@Sm3S;iQlLNA&L{W7uClLoha9*Q9Kg-US`}0^p;MeEY5QcYmW@7?^Q7=J)MFdT$m2!oGmDPf+SY2b`53sQ@Iam;@!)v@&oMNp4 zDXf(Hz~X|1jX&Y8l=}(pCDBTzcxRp$o(Wf1m17e hzyT1rg$Qg}+VY#wo4kQK4BNOZ?=luoCo9Li{{v8RhN}Po diff --git a/examples/match_demo.nes b/examples/match_demo.nes index 9343b055b93ba7e01bc2c7a6a4bc81a2824675ee..aa3dd91cd05f8ae4cfd651a654c7793bd045dbb0 100644 GIT binary patch delta 269 zcmbPmfN{bB#to5-vYZ=OS2FknA6&`!f$5m=Qh}2UA3y>rAOX+GS&X{6(}7G*2A>57 zS2DCVEN%F}v68V>V5LZ_u#d%Ipn68I`aM8J7npt0ChulU;#WHa)}en0qGPfSlccsE zL@XR6*0i(S4GY3~)PuLIOTY2RA1%udtbXfLWZ?`Y^+R>dpKPj0ymI;b>R@ delta 284 zcmbPmfN{bB#to5-f=nBjeF6_26J9EClHmih&*FoV%NTW4x(}{oYvaX35QMn1K8{Haapi1GxlYcYmM*oJWWH}5pvteliP$dK0Oc2vY>0m1h zkioo&!;NVIyTnrFRF+m|HkMNtJ}hNUfJw2js4*{Pws^`k0Vw$lDEV$FOTuPv<`p)q PL5CR*Oxn!vz^DKK5Mybd diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index 50f684ded1b10f7b8cdd7c179de3a5c436334b17..a42abea11526aa0fed2d1a69afcea6999b5170f0 100644 GIT binary patch delta 85 zcmbPmfN{bB#tos2#;hwDe0&eCWc(oUOkk!Imll8R<d6;3Lgou`)zLtwu(C?IU$1LN8>%$Nf2cFX_?aj^?UI z+e*kAlXE0Ly632TVnc2t@*8fwP7y1oY>T14K GG5G@lpLUo4 delta 269 zcmbPmfO*0J<_(dJ?z0arW%%%CB||I2Qs$EkAN)@;USRg|IOOAfa4Acx*izP3@ugxO zOT}B6G8C>HN?0m+ zqDuuNQbk(@mNIHGeGpnIaFXc*6Hw{vgG&W0UNV8j8$JjE4P*cs$aH8WW2^8=)>a{f za-f2f&%kP>K%&eF)`uqFZ&I0D)nuy(Q{RQ6o>5cb0~gQ>%nGw7-)jn!X=MR%7ICn# rs4*{Pws^`kfu)uC8Ox~)@0PM`Hg3LdGf_c;mFFEii}km4nWmBOPNnHT;N;j-^*~Iml22__?R39YT;WsA6aB2 uLodUFgjNBMm2ABX4H>VlGHbDd@^D)aNX&eSh@vWTS%W$BV5r`i6XdGV2(97^3p;f?RC0j2;Lq;!S zLqaPH&`{<@9Bxb#*d>-Sr?Rv%v$34I@L?%4T#AiFjd>}v#Z#sUK*?u7$#+Xx5)^(O V+8oNJ2Rre6Xf!xjf4vY!_Q)U|< diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index 8bfe2177b27432a22ba96c0fbfdb41f717cefa9a..619fda3feda463925c472b7e1e585cf5294d7a32 100644 GIT binary patch delta 271 zcmbPmfN{bB#tos2^{guye0&eCWc(oUOkk)EWLaVD>-`k9ai%9iaM+;?ES#L@^J4*_Lcv8Kd}RK z*noBH2I){=$@<~QL(WP7;q!%!3yRlsDR5yWwHyCf^xy3 zlm9=s`Zyn2wUk}+Kr0K7wDIy{W#KgFWpP;fvz2M(p~>w`YC;PS0c99Ja4%(T0=e7Tc%`hbz|pZLwBL zN}Jm|2%I1UpFyLQtiC{83!8zNZZvz{6JCC4u5 z8SU{{O_dAITpHaLA=OyasIG{J>PXB`eJU)`h4?0VEPjaVCaya$@&(L+3;6+7w>fqz z=%%s>Q!=a2A?7H+&qJ$F_a6ngi4&*p^hV1wQ2~Y{9rV VN4H?He}`D&Ul(5X){iJ?`~pK%Y+C>T diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index 88751f67bbcceabf8fd39b5034ef204e62a0821e..cd24ddce5dd57c0216a7b98ea47d80c3818d1733 100644 GIT binary patch delta 335 zcmbPmfN=s4)iBCVtW{u)oj6Yw$U5Rt&$^Pqhvnc(#t)J!8CoTtNiLN<$?^dtCl#52jIk|$X{K)I4jB~CH|IVTPR zMHzq$_k$}nT7^~~Y8758$jR6$xH6+v1jrKwii)i~)+)}))G7#63er<_aHR(kN|}I4 z=N_!bV(c-fv8Uk1GJ~Ch#aMKu5)zpz07^asO1@jl XlCW8q>5?s5#wmsaT&EWcI4A%B2pxc; diff --git a/examples/platformer.nes b/examples/platformer.nes index 911e40aa710318d1c93a73a46f7ee41e0b68432a..ab755550666f455e16183b68a2d49a6b66d1d99e 100644 GIT binary patch delta 1350 zcmZuxU1%It6uxus+?m~R({8AgO*TIh+F}w#+9Ed8D2pOUnNof5MNxv%he%Nb6$*u= zfm@I=?UrfP^g-g!)gECWQi_70yV)A&D%E}P!FTOyt;BiqNv!A0>}FF4JM7u-yXSo8 z+;h%sqiHqpIVCSC4(7t?vHP#l4<$adSJ>w~bp526cK*@4vY0Ba#UfMU`_6ChCVJht zkUUiXV@=Z-JHiP*S59FruokKjbwi zgrWv7sS>6gf;-Av(4&T>#Z`(pMiF~;#fS{PNYK-jxm!Y2f>&E`fT;>t>!ZPymabHe zS5-wdp)-| zws5PN*BNBe7cgbN-gk-E15S3sYS-WXhL3}lUy|xH6haM|gq6*9yf$bDumxZt7it_n zRW6)VJCp`C;2JyFN-Wx>0}`~!`!Kq7^n$mv|41AnRaKI%Es^cBS)lN+#lLC_%pig&hEOEAZw-fXsLE+N9_#W dO0G^9ezp100z7~qKE?6O6r1+ws=iW6M+H!|Ezd3Wx?|jU; z)tXtQb=rO?8Fnw69ohRl0+L^|mW{h?(e=BvFn9^fn+qiaK4(aNFZc^r$m;%u?ty)m z8eoL4(U1mkZ3JAbG_*!xNTp0&kB4TiNDa&fq>J~Kc0@`_ATfI)mCQ)S3%BV?;w zoT!7RIe~8oOg-h&6W`W})kAYVD;7xUoyS;>yw+2-7D5dbKp^o!nmWxc2EV!R>b*P^ z<5Liw(Cp>?FX}L+XDOeJCV_I*MngQMM3RPh(WE*mPzm!_>}$m&2V4IkIkHLeZ;pbr{A>e?vA|n$0!B+i@c^A};SD)~_Vpx8EEx># z)RM{2SYiwc>MMKnb0{ux)=Tz3tjZF(CSuozE&N%Q&>`1STkPfaq(SS4LcE7Xsg*6thLoCr?{VsU)%oqI*csZJ{q*aP8uUFes3eAH~b?S zaauG-)(kbkRUh%~Hk_sBSN}MY2yd^e$L@BOaMMW&8jbs-zGkAYiRS)wq$wu2D0T3R zti%rAyMyzUcz~um|35C#_|TFT#pTx$M_JYNfSHt><7~3 diff --git a/examples/pong.nes b/examples/pong.nes index aae3928c61c2f3e57f72db2758de2149c00441ad..24e406996c30d33ac68dd00d536ea845b48e8503 100644 GIT binary patch literal 24592 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 J!2f#&{s)et5GDWs literal 24592 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 diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 032cd182b62ef97c6c3d5a38736691ecb29ea4f3..7075ebdac900fe76fe405f1b1ba284541ac947f1 100644 GIT binary patch delta 133 zcmV;00DAwBzyXlJ0kA#+F$JjrOg6!(0q`WL0EG~#K#c-GsR)e&K&h0445`9}4XLG# z_6MoLjrRws|Ah)n7QvID0TcnAlePgs2na+%0sQZE3fx{V_$iw_dEN_ z|E*j;Vh3AUfC`xxakw!}V3%0RoXXP5%*Jx+!iS~I2{0)(7B%Lj%ob0XCIBU$0VUrp zWl31d^}%`p!$zJ9JSz=)861FyaQR#~2xL9uxiGn#(NbRK(8>QFTz%djT(y*4^FS*L zkhJmgVrAhp=w)$O={NZ?qn42GA)qMZ2kxb;t$a(_TKRo$O_pcc%s6rLWhPczwikyO M4rm`F delta 93 zcmbPmfN{bB#toAh*%+8tGW0S|p2_HG(aN%tp_O?Nha1xbc8R6TsVuF`Y%HfPd|1ky r0Fz>4QDa`pZ1I$70#Nc9Q1abUmW0iZ7(dvuavfqg;I*0Gfl&bfhi)Gm diff --git a/examples/state_machine.nes b/examples/state_machine.nes index 97bbefacfc1c7d56f4505054d9dca03198fe4feb..c02ee943fc8b7c57916996c7eff87c4429faadc6 100644 GIT binary patch delta 439 zcmZXQu}Z^G6o${a$3kxiHcr+hWEBKA>Ci#B8B<@ zNv3A&E4YX*HJeKj(g%^h<8bMOD3@17{fD96PGo*h@XCjYId6#pJe|D-SwB8{HG z{5*mB;BWnHAkZ76_y%1kL#o`UW5B;H4AyS^fbL9+!&)z-{32!Ea7v1&PHhYGowA|LTu_&{s zdTp4lN@hAro&LlM9HW@$UY^P(kNJT0K;SrTE}01^0|}}`Dej+;de?tT9sr@eKb~uu uk4J-EZpjFG ztt+a3f&~nz!z_c>E`*AOJ8^mUz3+YB`{EVED_ESv^-&Im^s+QZe(o~wI3+Qf3EDmi zeCJbqk!JHm5lGWnSIV4T*~m_&q{_?1|Q#pn}eCQ+fLrVq{AwHh~Yr^ JW_|}o1ptY7C+`3N delta 153 zcmbPmfN{bB#tpX^>stkv3Z7({!0f|tXsO_;z3j}bjY|cVa4r>?$-uOdVJ0KfOeUtK zjWd~EC*sS6*LGAF>K v*jUt(^b delta 187 zcmbPmfN{bB#trq1V$43(2bVHxI&dt<6g~mp0F2U|PvAlaXmA6VprU^jFXF$n! YOIZ>&H!>yKvd%ojaNy=GBT<&9^HPBk&%gU`)?*je%px(YOF^OF&tps K&hNm;r~m+EQ7k9` delta 162 zcmbPmfOWzF)`l&N(TsLX8<>4U53Xcr68lwT)fqQ$zs$(U#AwmV0yKwt5r-So1a^s~%&9D`%xo;DE__(Z uoB)$zV^L#X%53qJX#!C48Bp@wQkI16SD6_3ZCPI*VmP3^o!^0xQ2_wy!8axV diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index a8e6eb7105ed6fa16612d6fb5187526dabbb3e34..f675664b451ee384730f5d7d33943fee76b29f66 100644 GIT binary patch delta 96 zcmbPmfO!HCi3+f;Wbol_SjqT-eI?^FK_97&@~`bD3plVU!6jH$GPVjW6+9X60VJdV pQp>WEp;ahhqv1{a%>oVGOq(w^ncGfuuwd&x#BkvBp~V6R6aaNvBn|)o delta 147 zcmbPmfO!HCi3&6O2sEr@d?x540;G-!DpVg#*l2j)exd{CWB~_OF=iiLpm?j$Qo)k} zADDfl8dfs23PDw{Z5C+gX0m8y0g5p%;&5Y{z%H?rIhCcAnT_Stg%3-a6JSzoENaY4 knJu0&O#n(h14_PI%95~oc9Xd++u=hD2bd2p7C4{)0EMA5g#Z8m diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index a71fe7eb0e97d0afda4072d1d720e923e61b3610..b9fb319b7510b6ea2639942fa44ca07fd5469b77 100644 GIT binary patch delta 93 zcmbPmfN25{iCVC(Wbol_SjqT7;+f!5!IRTIfCS_~0xT;TTZMdV8jgX)^T6U}5OIc9 jp@fZ&hwV2DI8-xjKJ4UfJ2Ak5wfYdlf!&+=8yFP;nb;%2 delta 132 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 literal 24592 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 { /// resulting `__ppu_update_used` marker label to decide whether /// to splice the in-NMI palette/nametable update helper. ppu_update_used: bool, + /// Set to true the first time we lower an `IrOp::Mul`. Drives + /// the `__mul_used` marker label so the linker can skip + /// `gen_multiply` (~50 bytes) for programs that never multiply. + /// The optimizer's strength reduction has already turned + /// multiplies by constant powers of two into shifts by the time + /// codegen runs, so this flag only gets set for real runtime + /// multiplications. + mul_used: bool, + /// Set to true the first time we lower an `IrOp::Div` or + /// `IrOp::Mod` (modulo reuses the divide routine). Drives the + /// `__div_used` marker label so the linker can skip `gen_divide` + /// (~70 bytes) for programs that never divide. Divide by + /// constant powers of two has been strength-reduced to shifts + /// by the optimizer, and modulo by constant powers of two to + /// masks; runtime divide only survives for non-constant or + /// non-power-of-two divisors. + div_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 @@ -338,6 +355,8 @@ impl<'a> IrCodeGen<'a> { noise_used: false, triangle_used: false, ppu_update_used: false, + mul_used: false, + div_used: false, source_locs: Vec::new(), next_source_loc: 0, emit_source_locs: false, @@ -1175,7 +1194,11 @@ impl<'a> IrCodeGen<'a> { self.store_temp(*d); } IrOp::Mul(d, a, b) => { - // Software multiply: multiplicand in A, multiplier in $02 + // Software multiply: multiplicand in A, multiplier in $02. + // Drop the `__mul_used` marker so the linker knows to + // link the `__multiply` subroutine in — programs that + // don't multiply skip ~50 bytes of runtime. + self.emit_mul_marker(); self.load_temp(*a); self.emit(PHA, AM::Implied); // Save for __multiply contract let b_addr = self.temp_addr(*b); @@ -1222,7 +1245,10 @@ impl<'a> IrCodeGen<'a> { IrOp::Div(d, a, b) => { // Software divide: dividend in A, divisor in $02. // `__divide` returns quotient in A and leaves - // remainder in ZP $03. + // remainder in ZP $03. Emit the `__div_used` marker + // so the linker links the `__divide` subroutine in; + // programs that don't divide skip ~70 bytes. + self.emit_div_marker(); self.load_temp(*a); self.emit(PHA, AM::Implied); let b_addr = self.temp_addr(*b); @@ -1234,7 +1260,10 @@ impl<'a> IrCodeGen<'a> { } IrOp::Mod(d, a, b) => { // Modulo reuses __divide and reads the remainder out - // of ZP $03 afterwards. + // of ZP $03 afterwards. Same `__div_used` marker as + // `IrOp::Div` — modulo doesn't have a separate + // runtime routine. + self.emit_div_marker(); self.load_temp(*a); self.emit(PHA, AM::Implied); let b_addr = self.temp_addr(*b); @@ -2018,6 +2047,32 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit the `__mul_used` marker label at most once per program. + /// The linker uses this to decide whether to link in the + /// `gen_multiply` subroutine. Programs that never multiply (or + /// that only multiply by constant powers of two — those are + /// strength-reduced to shifts) skip the runtime entirely. + fn emit_mul_marker(&mut self) { + if !self.mul_used { + self.emit_label("__mul_used"); + self.mul_used = true; + } + } + + /// Emit the `__div_used` marker label at most once per program. + /// The linker uses this to decide whether to link in the + /// `gen_divide` subroutine. Both `IrOp::Div` and `IrOp::Mod` + /// trigger this marker because modulo reuses the divide + /// routine. Programs whose divide/modulo operations all use + /// constant power-of-two divisors have already been rewritten + /// to shifts / masks by the optimizer and never reach here. + fn emit_div_marker(&mut self) { + if !self.div_used { + self.emit_label("__div_used"); + self.div_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 94ec068..3f9927a 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -471,9 +471,22 @@ impl Linker { } } - // Math runtime routines (included always for simplicity) - all_instructions.extend(runtime::gen_multiply()); - all_instructions.extend(runtime::gen_divide()); + // Math runtime routines. Gated on the `__mul_used` / + // `__div_used` marker labels that the IR codegen drops at + // the first `IrOp::Mul` / `IrOp::Div` / `IrOp::Mod`. The + // optimizer rewrites multiplies and divides by constant + // powers of two into shifts (and modulo by constant powers + // of two into masks) before codegen runs, so these markers + // only fire for genuinely runtime-costly math. Programs + // without any surviving mul or div pay zero bytes here. + let has_mul = has_label(user_code, "__mul_used"); + let has_div = has_label(user_code, "__div_used"); + if has_mul { + all_instructions.extend(runtime::gen_multiply()); + } + if has_div { + all_instructions.extend(runtime::gen_divide()); + } // Audio subsystem — linked in whenever user code touched // audio (detected via the `__audio_used` marker emitted by diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 220a846..aaa85d4 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -7b16a9db 132084 +49c411ce 132084 diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index d5069db..0dca2ac 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -e73a6a73 132084 +68089bdb 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index 7efa628..e0af6eb 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -ea38044c 132084 +5146a336 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index e30b406..359b2c8 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -dded3a6a 132084 +5cc6d0f5 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index da68c82..778c04f 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -f2dbfbf3 132084 +714f679e 132084 diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index b37a532..7dc1df6 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -4e8bdb33 132084 +575adcb6 132084 From 37974611ae6d229dd0c679be2d72f27b5d018c37 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:07:45 +0000 Subject: [PATCH 2/9] linker: shrink default palette load from inline stores to loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reset-time "no user palette" path was emitting 32 unrolled `LDA #imm / STA $2007` pairs (~170 bytes) to write the built-in palette. Replace it with the same indirect-loop loader the user-palette path already uses (runtime::gen_initial_palette_load), with the 32-byte default palette spliced into PRG under a `__default_palette` data block. Net saving is ~120 bytes — ~20 bytes of code + 32 bytes of data vs ~170 bytes of unrolled stores. Delete `Linker::gen_palette_load` (dead after the refactor) and its unit test. Replace with two tests covering the observable behaviour: the default palette bytes appear in PRG when no user palette is declared, and the `__default_palette` label is suppressed when the user does declare a palette. Audio goldens flip again for audio_demo, noise_triangle_sfx, and sfx_pitch_envelope. These are the three audio examples that don't declare their own palette — shrinking the default-palette load shifts their audio tick's absolute address by ~120 bytes, which changes branch page-crossing timing and therefore the exact APU register write sample offsets. Same class of drift as the mul/divide gating commit. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/arrays_and_functions.nes | Bin 24592 -> 24592 bytes 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/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/loop_break_continue.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/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 src/linker/mod.rs | 57 +++++++++++------- src/linker/tests.rs | 57 +++++++++++------- tests/emulator/goldens/audio_demo.audio.hash | 2 +- .../goldens/noise_triangle_sfx.audio.hash | 2 +- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- 32 files changed, 74 insertions(+), 46 deletions(-) diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index 481c3dae9b02dff602deb6b3ae11b70dd8c83ca1..ab94d155b1e38b11139bdad4122f7a3da4d97833 100644 GIT binary patch delta 262 zcmbPmfN=s4)d(+XWm&9P)8FxRj-rgLx@yFDLU_4(79* z%pW*6u&!kADLy#yt1{z^$&!pF2DJx!1(?_JGPkm<G?Wmq-WnT#zEYl?<&yONBoOtTgClaR74ITe(|>edZoSF;kyOQ)0@2rCcAR zf$rtGzynmw08-55bNArn0w#HR%|j>ue{l8rb8yvCcFhB=EI`u6%ZrtT6Krbq{B}Xr_!^$iaIe`x%D_6I&udHn4Yh_(|c(MVbirSTfD;a@W0^nK-khBP$ ztoXpXlELTaY5C-7gol9~D%I2aN6F=BQO}G>mel$@em1$Cng&^42*@}(D5m-`roa`S6 z!WLHuhO~lv35rP}sRS$k!WJTy-sJ*Q%ro;c<16e(b-PgIBp7n&q8_=+uy@5KzhVWi z6Hkc)yvklwfHiruC3ak$llBPdRDfEa(bz(*ZzXPU$wc8)mgwTLSxc}h#t}{uoQogq ztnmb17$ELSSLYuQjHn;q>xQCEibu~$;x$gRWm^~G1k%=1ocW5D8a@yNCbwnQ(ens+ z)^SH@60iS45t1#dF=JKSlZwS=I*pVUX~<(f?e<}VsDU_xBAyf1u-)hlTAt!Y^cNn? zW8!C)C0rUD(#CkAC0tOHWi-81p;p16*e*@IxVcA*wK~m}cltwU9tvh$rJ6&eI(^!} F>>uw5b&CK1 delta 382 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_RBFz9h|scM#1~wO2!XN#{`!$YBqcj zS}J&w;REYR2A?FLFeigg_Q8p-6*+n!oT-yV7?n7-K{)#-+W|QbAe@(zGZ-~x1rLGs zNF7?q(Au!H;e+^E2IiCe6F|o49Gbk2QHs+8q9X9nYB@BSjY*ej zGl){5yGOaw=%DjMKBU3NC!ifV47rsrFW>jICa$vF}qg2}A11lLnNIVl* zDsb|{2G*4fK36~jEGrpWg?v68I0h1D_y7`r0}*Fz66V7&qd zp?ZaV^bQ^ai9_^iOg_k{p)}Ke&?dgTym|r2;2EYyb%)fCN~8YJ`014ju!EGk_IVK*Skag_a6K^>#z_ z3N95q`2ehU4ot7G&#r^VK;j_1%nUwTCYv&9Fex0|T+b+P$9DT5!vX0-iv=7M0517; Ai~s-t diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 230f49761276601fbfd91d97619d3642a089cdab..6ae24af9e7d93ceb6db30ecf4b07cbf0cbf10e30 100644 GIT binary patch delta 308 zcmbPmfN=s4)d+XAGOaw+%DjMKBU3NC!ifV47rsrFW;BsEICzrj0<(|jA)f^Yma_CR zFt24`KKbDT=LXi53_e#5OwMC;<&ZxJlmw#5yBRI~q7SZQ{J^sEL#x12Mop# zw-X>q4xoDBl`O47Km)gcW$%N8HJ1vWOaXJ>fw)r7M2?9r6@{A0cnB!OzLKex8RWWU zOtwrws*+K8G6%D`D%i>V3<3)LY+`EsoRXUSjDm__zKDtmzm%30zmSsAW)P&|(1x1prUAbnE~C delta 410 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_M6D>Ie3!k0<+K6Lq0kOm$LLSFt24` zKKbDT=LXi53_b}5Cq8oJSbY#E2Sk(g87=&tA6&`!fo0`~R)M9AnoJ*LmkOL@0;^>> z1eD-fDq!)Fsa3#7;Lu72pc3Je3?D!S$$})6mI_Lw3bhJ86FPYZti|}y;Tqnh3%cOH~ f;#=j(HB92HU>9%R!Su|AP2@1cfsn(C1soIr^fHp; diff --git a/examples/comparisons.nes b/examples/comparisons.nes index 99c527b5476755f25cfc2822ba35f60d172c282a..f98c42dd4b6df903111bc35527e8c3438fb4b823 100644 GIT binary patch delta 150 zcmbPmfN=s4)d=5jWmEC(45*l*@{U{nAANP{YY delta 254 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_DeB|9h|t@f-!lrAfx!i-=d5YfD9eR zy&&%7P)2daH(*u^qd1fFp~?1)Vv{#BiZXdXm=73vf!a6oGxgiDW*=fWuzEAU1ET@} D62?pH diff --git a/examples/function_chain.nes b/examples/function_chain.nes index b68a2acdac98ea928da6295e01d66e3fd6429985..84a89d7d3365cf5f7c20d0c8898ccdd1605465e3 100644 GIT binary patch delta 223 zcmbPmfN=s4)d=^tGOaw=%DjMKBU3NC!ifV47rsrFW|WdJIOx-LU@1#41M^x2=94Er zaBg5-$>6j3z+_)W*@R06Rx*BIT`ADZ#JrY?xs^p>-hl+5T4v_8%*-cWK-GQ+DdJf9 z0w}%bKmt$(!T_pcVP4C^%mw5=0&8PsUdzhd%f>8`DF7sa!d#P2F-kE?PJYVhq||kQ zpFu!@pG{1SpHotkpHWZ|%okBH;g`~~;ulgf+N{a+&~|bIQx4nOgA51W9$YNopa1~u C5=t%r delta 326 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_Df0ZIp`yHa4AbK1M^x2=94EraBg5- z$>3vuaN5Ij805q2RH9!d}zxi Od5GaajFBaA2}6V~Wy*1N;mE3jAzh uYW$p%n*5A{ieSEoiV44zmKDE{lF{bHjOlihm~vRv4>BA`-^}m8r~m*`w;7rM delta 197 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_A4;S9Gti@1xRi-XH2(aoqUktz~#;S H4vY!_Di}O) diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index 2c19839e68c715f207c57e6384b97a5324a1d27e..fd89a0e357ec4c110dfd10da103c2f401b807e4b 100644 GIT binary patch delta 100 zcmbPmfN=s4)d;h;GOaw=%DjMKBU3NC!ifV47rsrFW>jLDbYQYQqbbwk1Cz@bHI*hE z;Aap};Aaz4T9h|t?lu7^K#8;X?_GVMYOgq+z O2N@1r+|2L5r~m+>j6T}{ diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index 460671b92d83d26d8a2895e357ba22185a3c6ff1..d2f40bd080f7d718b6128e43acb80a1e39a52952 100644 GIT binary patch delta 318 zcmbPmfN=s4)d-(!WmjW?rRv2lyET6!_W1)c83iHTf9@6~TNF6%&3bEh~N@C8Nz*%nNKL TA7IX5eRqiAfZk?)2Sx<|1|n{S delta 443 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_Ukce9h|sd#whyWO2!XN$HbO0Y6^T1 zS}Jyu;REYR2A?9JFeigg^}&@4txZdtJ_s)r{J;)m3;Og;=4F)NSPGF}H(8faLGLVt za}B1LV0(HGw}lbzWE>10vpz-!wd&zZRU4iQ~&^xTA2s{ diff --git a/examples/loop_break_continue.nes b/examples/loop_break_continue.nes index 0d662881bfcbbb2501953df40f659ccf2469bdbf..1f28ccc5e6b78dc4ef8d5e05d1bacb0373ae94aa 100644 GIT binary patch delta 284 zcmbPmfN=s4)d(xKGOaw^%DjMKBU3NC!ifV47rsrFW{i;Pd0aGJat8xqNUfAM>ic49q7PK7eG?LBbp>8Q<|S`#3@5 zSeRQImI}18ENy6IRk(X#vLchH$K3rY$gk^g}Jq1sX!~s(uP)6h0KG!{LEls z4(8UzrGh}=Mu@Ng^ICr9lROhZrdS>Vnj*7S0LJ$O@p)D$<}X2i|Sw IcVJWi0F3V_raBnoD4o92Z8DvmNtCg zSjpHbuu`N|*k{clpjt+-S_7b>3(P*Z4o<$vn8-N`qGj>H$<|Er>W3h_(;x$ymNo$m zU`95832eaggOeMX{M9TDfh}`91Xqh<88cXI?Bs7uM$Qu;N@pUJGQf=m>K5>sc7UHj zK!Kl4OpTvYQj?!iP!Y@*Q8D3{(z4;1hgsCF2LCW5P=XfI=XFl!FtW8R|_33Ue~}EI0^M-mtXc1IJ3nR)Liw zt-?MQhk;5N!Akc46A+-rMk(!s2UaqEka#Ar zRN&-^53DO0d@g|mSXMH&3i*6Ea111_02Y6BU~&uNL8VCt_!$Hg_}RqN_&Fst`56Tj m!F&-F6MiWzD}Es*qs{J2hixY>V9H@VeURY*_hxJ-CwbgTym|r2;2UfCb_}0xT;TTZMdT4ju!ED}cqz4o>`WkV)>~=G%;i RZCT9^F&rr0%4mlyTQOxRl|;pOp-)3`?0$ zGF)KxF+X^c5lFp0=(GR8QkGr@=B2E?jLd5pm|GjxGBUR|E^Y8w+StmJp-_7$VX5#* z#t*D38GP6$FK85Hl%2e-(KhYYfnFx&r2-PUOw6qUOBpqpK1eJTILY*Za|1|a<-wJV zAJ~@)SiEEc%Qk!v2b#hFHf1VEj$iK)w6;?8U2muhG03yI*Y#^2xh){!zae`QqAVL!^#t32wf(S*p z7&h}nK(Z1~j;7Z013eSX=3QD93wF;g*u>mBy zacqMDAkV4zR zr2;3Pec%KeVhfVv0?IKfWP$BI+bGWjbi%tvcg3w3`WQ78fQlJ_inkpEITWh6yvcC$ UiKc5dY$}Ht4x}7jEO0;p01}#$B>(^b diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes index 43a0a22fdd2e56b9e0c407c7bfe2eecac718e4d7..88a24252ca44d597eead7b782e4ba84157731317 100644 GIT binary patch delta 314 zcmbPmfN=s4%~9Xh%Cz!OEAs+|jZD4l3MURIT==$9pqEi$B}1z~t6-~8t8go`!u11_ z{Tbz1Cm!_Ke_*m2Q@*9w!Ig|3n2rf8Wz-b-AhcBIB*O>Rl?*pR9vG-3?0{ zKJcw%1nTYrX_z)1CWV|4_ zgy$L00fq}~3PvCipW?}U%(7l9KvH}w=l3!k=w$?=2RR3JOKDm03n>{XR36$K%DmNP@&@J{)@z3t4%lwy IcVJWi0IU*qR{#J2 delta 469 zcmbPmfN=s4&8g?_Wmj0q03rlHgaU{Fi?M-NVjw~dF2)IBNrDJXxELddB?uxE;bPd# z69LJpfCv+~I;q|WAYTi_w}J}_fmli)!U#kN^fCe+*ecK}*ecX2+{&zwd~l_IFT;Ue zMj(0+v6SJ%AD}eDQs$Ek9|TV_USRf-J>=tkaPln1e9LPGS2BKJIwrJ~QB&Z9&{83w z64sRrK5v1-oD4qS4gxhcEN%F}w~`TPt{6z$O1V}cknVoQD$CA8SamM|3IlboJOtOx z0Cp9UZYL&{WbQ*g|3L`oB*7&-&v*_nTwqh!2Nld%k+CYn0cZdZ$lVwCkbLB0au}$E yZ{>WrAAusk7-D#k&??}ulC77aA)}YEAwhxl@aC6HTWwgK4l^7$xtZUAQ2_v$@~*`I diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 0ae7f4eaa32038178181d9098d4e0d889b7ac4c3..5fa3196d9e9b43d9ce646bda70ff15bae3feafeb 100644 GIT binary patch delta 94 zcmbPmfN=s4)d**{GOaw=%DjMKBU3NC!ifV47rsrFW{hJxcVKcgV~5hY1N;mE3jAzh vYW$p%n*5A{ieSEoiV44zmKDE{lF?>XCRV%22~0U`od+2XoIJQ#z(D~3#u6Lw delta 197 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_Qx?r9h~^S14wS3$;fKQ_Vpmc0nY5C-7ey}MizLpC2qq7gkmFeDKTVkIXmf^dZU4R#hnv=;=cZN!CxZBmM` z!rDm++r0>4x{%5rpcW#sD%gm`%?ZQ6Jj^>xC_$*}r1(Ri=n?b6xPjDhAEZ0@W)dm2drRQV;xbNzE=3nLhg!4iNJ0wwRqeiZxbzZ;~$^ZmJ)e#f*VnlyujMYC8W zDZ&EL5*E)?Iig2D95JGwj!t-SVm;D{sxU}idlCzeJ%ViFoqp+M_6DluQ1Y~@=w~y7 zj6Q5&y_*g?5o^Ht`KPEqQoF%zbj_WbB$n=8t7TiMCbh6QWCyTbEcLVkHNkC?s4kW;`lCf3Dr{>@>ka!+gybL1F&?>Z4@T44AsPo`tZbnIo!rl+;D-ZX6WMBEe z_Y*r%)&?xQd$J9q99vcIXQ1Td1V(iUnS)EYK1eTM*vNB%XQe?egTqRORxTfvLzCw- z%F7oVI{E*DtB>=cRZH15545rXNgFRORu)c!UKWRyKPTU1)Dm8J2q?<D7LzAVLv>DG$c4N|E{4u$hX$GH1m&Xi`rA!`6nLQL(4{f$&PPJieKg@98)MkDM GMg;&5e3G*O diff --git a/examples/scanline_split.nes b/examples/scanline_split.nes index 24096ea6fc703b13e180e673d87a95c1f30701d3..d39912db33197f1ec6704d547a58704d538181c4 100644 GIT binary patch delta 184 zcmbPmfN=s4&GEX@%Cz!eEAs+|jZD4l3MURIT==$9pqEi$B}1z~tKiCtR-u&(T7`i; zW`%19R+jfN9Oz{Pq6ZO689w|03NtKaKFRPw$mi&R$wiFwnShiqlL|-qL7&ASG})d> z#^Tt)l?=TM4-#4hJXW&xGBjlLGBzaeGYBZ~vx%wkb4qIRGYTq#`64PN{8Cz0{6b1b d3M&t8E@ql;JNW`r4%?T537mW+Ow(=I5)LsO L;5)Qfz(D~3zK~Pu diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 7075ebdac900fe76fe405f1b1ba284541ac947f1..8989f4dd7037af30a79ccec84c91d0a02c686d59 100644 GIT binary patch delta 246 zcmbPmfN=s4)d*{~GOaw+%DjMKBU3NC!ifV47rsrFW|Z>pI`B+jslZ8_53DO0d{!M; z$@oEKB}1#gN{3!1hm{?{Aba`}iIT*~#qdI7^mo(nuH z4SE?IfV#MR=1u(J!13zf$^Rc*efCavU=(AOX=Pe@XmT{8nvnFNrR1y tYvuP@F?llMaz2kPj~O0InLL&H=X3M(m5W|6=oB16W6#(DnbHe}t diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index 6b0f146771e07accee3120b8ed7564d2f9f96c88..24816361e0b0cf0f35c17f663a45c785216ce224 100644 GIT binary patch delta 114 zcmbPmfN=s4)#$3VGOaw+%DjMKBU3NC!ifV47rw0&=w(z`$jLDbYQYQ<3goL2lyET6!_W1 v)c83iHTf9@6~TNF6%&3bEh~N@C8N!c7&YuBJ22(2r5$8AuT9h|s%A&}f$#;9S(cIhC)0ii>S H1soIrFpxav diff --git a/examples/state_machine.nes b/examples/state_machine.nes index c02ee943fc8b7c57916996c7eff87c4429faadc6..948a84aada64cfd484298f59ee7985f268d6abc2 100644 GIT binary patch delta 258 zcmbPmfN=s4)i@-#GOaw+%DjMKBU3NC!ifV47rw0&=w(z`$|o4z6VUz`By5myvlXqo%+I zu9b{HBWw-=m4Q^=U@TSN3Q{7tR6rt=t5skn6HxjONE)bI0jQh-Y$Hf*36l~Nkeb2d z;_Pw=s0B$5i_!cH0t)8inl$I60kdo2n|4iJrlLMG@*qRS9 M95{Svv4DdD0NhDk5&!@I delta 345 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8(ckgo;eTfqf|KrAH?VFV%sdKrNZY!zr_Rn7_nigKJd2owaO$v%vw>W+t2GJX(T zDj<=`)he)(320siNY7G6O@$9!Ktn<1gVerYRAK^BJWMXi7eHE&6lA)Es=imW;1_1?rHZe7RPDxFEMnOd|Uqr=(UrNi0Ur5PlvpUmb+sO?~Ic%#B NG8}k)aIt`c0syQtBuD@N delta 214 zcmbPmfN=s4)ztI%vMa1)01*NpLIFg8#n?bBF%Y2!7vlu6Bte8GT#OOK5(E*7a4~G= ziGXBPK!gcgom8&^kgo;eTfqf|KrAH?VFV;5_6IWg9-R27lJWNB7)B%Bvj>+leE74H bp_ReM_u%H;jE`;E#1AnX2tTw~z(D~3x1mL* diff --git a/examples/two_player.nes b/examples/two_player.nes index c5a75b3b73d25622f03003a9675218dc142cbe75..db4c774e4c7b2538395f5179d792340416d7c8c9 100644 GIT binary patch delta 158 zcmbPmfN=s4)d)XrWmZ%g>Td485xz7rX5(su=3wr2BsH4 zaN+w(hE~pJLQ92CT7Tf&z`Byb=j4Iu$&8GOOm7ZMZ)0SXXXKr}jFC}|ecFLmfmL%Y zw_j#tWMWj{XAn@}XA@K7=akgsXB1Qf^F>rl_@%V0_=S{=wqIppG_{?mki!;vh~dD@ KL(2sm7!?49mNY2< delta 235 zcmbPmfOWzF)`lsJ4fXuJ>Mn$HI ygHwMo$}`TKF2%^G#;$a*RbbWJqubpX8JQTjXEHIG+Oi!z#BhN1@NxkMMg;)ojYq5i diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index f675664b451ee384730f5d7d33943fee76b29f66..fb01da7f33c96186cb5570e2532418981ad2c38a 100644 GIT binary patch delta 196 zcmbPmfO!HCX*0h+kg(D4zWruihhhie|E)|b54JKdVA#mi%dT+ZfWn1uljR%bVkaF~ z#IW+;Tn45WKycyvN`_WWh5H9q7A)ahSum42rpC`HsmaeMs0ikZsF?6e eX<6|LDH&~^-IQ-T(IJOb{t&}~_|5zW7!?4p7fD9| delta 300 zcmbPmfO!HCX){+JOxS36-+nW%L$O0We=objN(K-i03sAX1Xzp>#1aD$YH%@55K9t7 zXu`!9K`cQKp$HelW}XO0Rs}?uz|~3hh5-3mAifn`Pzc0Q0ue?aLZFvXVP!z8;7W#8 zp;qRl3?KdgSqw{=PcnR9_K`cdh+*ZwxeQD%fZ)P+pbV!%_QA==8{{?Q4z>!cn%jLa tVX1(}N(P0Lg9;1{89)lmXGHKf^EEOuZq{$gw`J`<#BkvBX8r?=3IL8`XK?@k diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index b9fb319b7510b6ea2639942fa44ca07fd5469b77..8c98c50dace228ed5d2a9b5553d0de5495a16d96 100644 GIT binary patch delta 168 zcmbPmfN8=3rVR}a!tYy|Rvv6+Ucj)Csh3^h!~um1-zLjD%K1(@u!v#hzqt%dFM!~} z_mvE-oC^03tgKkVxw2v=1Jg=|nT$*`nV6Oe&SYkK#tDHwlMb{BteX4&K*CZ1kChAx zHx4K;G-Lp&&0UU+j0*e=0t)8inl$I60kdo2nZl`+Ni3vHZ P{D&A0_;2QKU{nAA--tYh delta 272 zcmbPmfN8=3rVR}a_58i;3M&~vgaC+801;p@HV{h;M5w{VI6*8)5TOYdV+64TL4+b) z44Zi(AXya6-+?lm3fTuIA9s+KkUQ8auxf7g!Gxs(9xE9XQVuFG eG-Lp&&Ho%288^#2)!VXGA7VJLdozCnqXGav`CSG8 diff --git a/src/linker/mod.rs b/src/linker/mod.rs index 3f9927a..2a482c6 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -157,7 +157,13 @@ const DEFAULT_SPRITE_CHR: [u8; 16] = [ 0b0011_1100, ]; -/// Default palette data for M1 (writes to PPU $3F00). +/// Default palette data for M1 (writes to PPU $3F00). Spliced into +/// PRG under [`DEFAULT_PALETTE_LABEL`] when the program has no +/// user-declared palette, and loaded by +/// [`runtime::gen_initial_palette_load`] via the same indirect-loop +/// path that user palettes use — keeps the reset-time palette +/// loader small (one code path, ~20 bytes) instead of the old +/// 170-byte per-entry unrolled store sequence. const DEFAULT_PALETTE: [u8; 32] = [ // Background palettes 0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white) @@ -171,6 +177,12 @@ const DEFAULT_PALETTE: [u8; 32] = [ 0x0F, 0x12, 0x22, 0x32, // sprite palette 3 ]; +/// Label under which [`DEFAULT_PALETTE`] is spliced into PRG when +/// emitted. Prefixed with `__` so it can never collide with a +/// user-declared palette's label, which the asset pipeline prefixes +/// with `__palette_`. +const DEFAULT_PALETTE_LABEL: &str = "__default_palette"; + impl Linker { pub fn new(mirroring: Mirroring) -> Self { Self { @@ -414,7 +426,16 @@ impl Linker { if let Some(first_palette) = palettes.first() { all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label())); } else { - all_instructions.extend(self.gen_palette_load()); + // 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. + all_instructions.extend(runtime::gen_initial_palette_load(DEFAULT_PALETTE_LABEL)); } // Load the initial background if the program declared any. @@ -550,6 +571,18 @@ 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() { + all_instructions.extend(runtime::gen_data_block( + DEFAULT_PALETTE_LABEL, + DEFAULT_PALETTE.to_vec(), + )); + } for bg in backgrounds { all_instructions.extend(runtime::gen_data_block( &bg.tiles_label(), @@ -753,24 +786,4 @@ impl Linker { fixed_bank_file_offset, } } - - /// Generate instructions to load the default palette into the PPU. - fn gen_palette_load(&self) -> Vec { - let mut out = Vec::new(); - - // Set PPU address to $3F00 (palette start) - out.push(Instruction::new(LDA, AM::Absolute(0x2002))); // read PPU status to reset latch - out.push(Instruction::new(LDA, AM::Immediate(0x3F))); - out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr high byte - out.push(Instruction::new(LDA, AM::Immediate(0x00))); - out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr low byte - - // Write all 32 palette bytes - for &color in &DEFAULT_PALETTE { - out.push(Instruction::new(LDA, AM::Immediate(color))); - out.push(Instruction::new(STA, AM::Absolute(0x2007))); // PPU data - } - - out - } } diff --git a/src/linker/tests.rs b/src/linker/tests.rs index 6b28c10..61f7524 100644 --- a/src/linker/tests.rs +++ b/src/linker/tests.rs @@ -633,30 +633,45 @@ fn link_banked_chr_rom_survives_with_switchable_banks() { } #[test] -fn palette_load_writes_to_ppu() { +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. let linker = Linker::new(Mirroring::Horizontal); - let palette_insts = linker.gen_palette_load(); + let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let rom = linker.link(&user_code); - // Should write to PPU address register ($2006) twice - let ppu_addr_writes: Vec<_> = palette_insts - .iter() - .filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2006)) - .collect(); - assert_eq!( - ppu_addr_writes.len(), - 2, - "should set PPU address (high and low bytes)" - ); + // The first four bytes of DEFAULT_PALETTE are {0x0F, 0x00, 0x10, + // 0x20}; they should appear verbatim in the PRG portion of the + // iNES file (bytes 16..16+16_384). We look for that 4-byte + // sequence rather than matching the full 32 bytes so this stays + // robust against minor palette tweaks. + let prg = &rom[16..16 + 16_384]; + let found = prg.windows(4).any(|w| w == [0x0F, 0x00, 0x10, 0x20]); + assert!(found, "default palette bytes should appear in PRG"); +} - // Should write 32 palette bytes to $2007 - let ppu_data_writes: Vec<_> = palette_insts - .iter() - .filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2007)) - .collect(); - assert_eq!( - ppu_data_writes.len(), - 32, - "should write all 32 palette bytes" +#[test] +fn no_default_palette_blob_when_user_palette_present() { + // A program that declares its own palette should suppress the + // built-in fallback entirely — the `__default_palette` label + // never gets emitted, and the assembler's label table doesn't + // contain it. + use crate::assets::PaletteData; + let linker = Linker::new(Mirroring::Horizontal); + let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let user_pal = PaletteData { + name: "Menu".into(), + colors: [0x0F; 32], + }; + let result = + linker.link_banked_with_ppu_detailed(&user_code, &[], &[], &[], &[user_pal], &[], &[]); + assert!( + !result.labels.contains_key("__default_palette"), + "default palette must be suppressed when user palette is present" ); } diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index aaa85d4..7fbc913 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -49c411ce 132084 +e064fdf0 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index e0af6eb..ee351f8 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -5146a336 132084 +7b0caf36 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index f4d7072..a4eb4f4 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -eec31111 132084 +c1ba0463 132084 From 7533ac281eba9ca40b5450a3d31b4aa2b9b75f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:21:56 +0000 Subject: [PATCH 3/9] 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- Date: Thu, 16 Apr 2026 13:32:52 +0000 Subject: [PATCH 4/9] linker: gate default smiley CHR tile on __default_sprite_used marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the built-in smiley from CHR tile 0 unless something in the program actually references it. The marker fires when either: 1. `IrOp::DrawSprite` lowering falls back to tile 0 because the sprite name doesn't resolve to a user declaration, or 2. The same lowering sees a runtime `frame:` override (which could index any tile, including 0). A third source of dependency — a background nametable entry of 0 — is detected in the linker by scanning `bg.tiles` for zeros. This preserves the smiley for programs like `examples/friendly_assets` that use tile 0 as a background placeholder, even though their draws resolve to user-declared sprites. Programs whose draws all resolve to explicitly-declared sprites with static frames AND whose backgrounds reference tiles 1+ now leave CHR tile 0 as an all-zero blank, freeing 16 CHR bytes that the user can treat as an always-transparent background tile. Verified against the current example set: `sprites_and_palettes` and `auto_chr_background` reclaim tile 0; every other example keeps it (either they fall back to tile 0 via an undeclared draw name or their background tilemap references tile 0). All 33 emulator goldens still pass — removing an unreferenced CHR tile can't change observable output. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/auto_chr_background.nes | Bin 24592 -> 24592 bytes examples/sfx_pitch_envelope.nes | Bin 24592 -> 24592 bytes examples/sprites_and_palettes.nes | Bin 24592 -> 24592 bytes src/codegen/ir_codegen.rs | 53 ++++++++++++++++++++++++++---- src/linker/mod.rs | 30 ++++++++++++++++- src/linker/tests.rs | 43 +++++++++++++++++++++--- tests/integration_test.rs | 18 +++++++--- 7 files changed, 127 insertions(+), 17 deletions(-) diff --git a/examples/auto_chr_background.nes b/examples/auto_chr_background.nes index 3459fd32581bef76cca988933b6d7e3877df0d17..5fed29a7629e43480922e5323cde769b05a40040 100644 GIT binary patch delta 18 acmbPmfN{bB#ti}vlOH-nY|e04ArAmdzz5L) delta 35 qcmbPmfN{bB#ti}v0ya)d8<);>vazZA4+M2KlLZ|-HfK1jkOu%4S`X0x diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 8f646da10e66bdb49d27cbf9f0926a52bc11cc08..d75847bde4c349d02acbc762e0ccde40de03d53f 100644 GIT binary patch delta 18 acmbPmfN{bB#ti}vn-v}2%TG3N5Ci~7BnH_4 delta 31 mcmbPmfN{bB#ti}v0ya)d8<);>vazZA4+M2Kn*|*{$O8b|P!5&= diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes index d6450df48f2e2a0fffd7a5a37ffba7d70413c7e4..da2669b0df425ab0e2eb4e7c36e1750dd3941bed 100644 GIT binary patch delta 31 ZcmbPmfN{bB#ti}v0t{$ivx371c>rwG1=Rom delta 31 mcmbPmfN{bB#ti}v0ya)d8<);>vazZA4+M2Kn-v^B$O8b|j1Hv$ diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 3327865..7ea7136 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -182,10 +182,20 @@ pub struct IrCodeGen<'a> { /// 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. + /// and keep the default palette load. Programs that never + /// `draw` pay zero for any of those paths. oam_used: bool, + /// Set to true the first time a `draw` either references a + /// sprite name that the resolver didn't turn into a user + /// sprite (falling back to tile 0) or uses a runtime `frame:` + /// override (which could index any tile, including 0). Drives + /// the `__default_sprite_used` marker label — the linker uses + /// it to decide whether to reserve CHR tile 0 for the built-in + /// smiley. Programs that only draw explicitly-declared sprites + /// with static names and no `frame:` override leave this flag + /// `false` and reclaim the 16 CHR bytes of tile 0 as a blank + /// background tile. + default_sprite_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 @@ -366,6 +376,7 @@ impl<'a> IrCodeGen<'a> { mul_used: false, div_used: false, oam_used: false, + default_sprite_used: false, source_locs: Vec::new(), next_source_loc: 0, emit_source_locs: false, @@ -1438,12 +1449,25 @@ impl<'a> IrCodeGen<'a> { self.load_temp(*y); self.emit(STA, AM::AbsoluteY(0x0200)); - // Tile index at cursor+1 — frame override, sprite lookup, or 0 + // Tile index at cursor+1 — frame override, sprite lookup, or 0. + // + // The `frame: ` override indexes into CHR at a + // runtime value we can't bound statically, so it + // might land on tile 0 and therefore needs the + // default smiley. The `sprite_name doesn't resolve` + // case falls back to tile 0 explicitly. Both drop + // the `__default_sprite_used` marker so the linker + // keeps the smiley tile in CHR; draws that resolve + // to a user sprite with a static `frame: None` leave + // the marker off and tile 0 becomes user-available + // as a blank (all-$00) background tile. if let Some(f) = frame { + self.emit_default_sprite_marker(); self.load_temp(*f); } else if let Some(&tile) = self.sprite_tiles.get(sprite_name) { self.emit(LDA, AM::Immediate(tile)); } else { + self.emit_default_sprite_marker(); self.emit(LDA, AM::Immediate(0)); } self.emit(STA, AM::AbsoluteY(0x0201)); @@ -2092,9 +2116,9 @@ 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. + /// DMA + shadow-init + default-palette paths when this marker + /// is absent, shaving cycles out of every NMI and bytes out of + /// PRG for programs that don't draw sprites. fn emit_oam_marker(&mut self) { if !self.oam_used { self.emit_label("__oam_used"); @@ -2102,6 +2126,21 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit the `__default_sprite_used` marker label at most once + /// per program. Triggered by `IrOp::DrawSprite` when the sprite + /// name doesn't resolve to a user declaration (fall-back to + /// tile 0) or when a runtime `frame:` override could land on + /// tile 0. The linker uses this to decide whether to copy the + /// built-in smiley into CHR tile 0; programs whose draws all + /// resolve to declared sprites with static frames skip the + /// emit and get tile 0 back as a blank background tile. + fn emit_default_sprite_marker(&mut self) { + if !self.default_sprite_used { + self.emit_label("__default_sprite_used"); + self.default_sprite_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 261c577..f5b3a97 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -771,7 +771,35 @@ impl Linker { // ranges overlap, but we still bounds-check on copy in // case a future change shifts the layout. let mut chr = vec![0u8; 8192]; - chr[..16].copy_from_slice(&DEFAULT_SPRITE_CHR); + // Tile 0 is reserved for the built-in smiley *only* when + // something in the program depends on it. Three possible + // sources of that dependency: + // + // 1. `__default_sprite_used` marker — user code runs at + // least one `draw` that falls back to tile 0, either + // because the sprite name doesn't resolve or because + // it uses a runtime `frame:` override. + // 2. A background nametable references tile 0 directly. + // Some programs use the smiley as a placeholder + // background tile (see `examples/friendly_assets.ne`). + // 3. A background's CHR was written at base tile 0 — the + // resolver shouldn't allow this today, but checking + // for `chr_base_tile == 0` keeps the gate honest. + // + // Programs whose draws all resolve to declared sprites with + // static frames and whose backgrounds reference tiles 1+ + // skip the smiley emit and reclaim 16 CHR bytes. + let bg_uses_tile_zero = backgrounds.iter().any(|bg| { + // A nametable entry of 0 means "fetch tile 0" — the + // PPU can't tell the difference between sprite and + // background tiles, so we have to preserve the smiley + // when any background map byte reads 0. + bg.tiles.contains(&0) + }); + let has_default_sprite = has_label(user_code, "__default_sprite_used") || bg_uses_tile_zero; + if has_default_sprite { + chr[..16].copy_from_slice(&DEFAULT_SPRITE_CHR); + } for sprite in sprites { let offset = sprite.tile_index as usize * 16; let end = offset + sprite.chr_bytes.len(); diff --git a/src/linker/tests.rs b/src/linker/tests.rs index 47f430c..22614b6 100644 --- a/src/linker/tests.rs +++ b/src/linker/tests.rs @@ -50,8 +50,14 @@ fn link_has_correct_vector_table() { #[test] fn link_includes_chr_data() { + // When user code signals that it uses the default smiley tile + // (fallback for an unresolved `draw` target), the linker should + // copy the built-in 16-byte smiley CHR blob into tile 0. let linker = Linker::new(Mirroring::Horizontal); - let user_code = vec![Instruction::implied(NOP)]; + let user_code = vec![ + Instruction::new(NOP, AM::Label("__default_sprite_used".into())), + Instruction::implied(NOP), + ]; let rom_data = linker.link(&user_code); // CHR starts after PRG @@ -64,6 +70,23 @@ fn link_includes_chr_data() { ); } +#[test] +fn link_omits_default_smiley_without_marker() { + // Without the `__default_sprite_used` marker the linker should + // leave tile 0 as the all-zero blank tile — programs that draw + // only user-declared sprites or never draw at all reclaim the + // 16 CHR bytes. + let linker = Linker::new(Mirroring::Horizontal); + let user_code = vec![Instruction::implied(NOP)]; + let rom_data = linker.link(&user_code); + let chr_start = 16 + 16384; + assert_eq!( + &rom_data[chr_start..chr_start + 16], + &[0u8; 16], + "tile 0 should be zero when no program draws fall back to the smiley" + ); +} + #[test] fn link_rom_size_correct() { let linker = Linker::new(Mirroring::Horizontal); @@ -77,7 +100,15 @@ fn link_rom_size_correct() { #[test] fn link_with_sprites_places_chr_data() { let linker = Linker::new(Mirroring::Horizontal); - let user_code = vec![Instruction::implied(NOP)]; + // Seed the `__default_sprite_used` marker so the linker still + // emits the smiley tile alongside the user sprite — this test + // asserts the legacy "smiley at tile 0, user sprite at tile 1" + // layout that programs doing both fallback and named draws + // depend on. + let user_code = vec![ + Instruction::new(NOP, AM::Label("__default_sprite_used".into())), + Instruction::implied(NOP), + ]; let sprite_bytes: Vec = (0x20..0x30).collect(); // 16 bytes, one tile let sprites = vec![SpriteData { @@ -621,9 +652,13 @@ fn link_with_mapper_nrom_produces_single_bank_rom() { #[test] fn link_banked_chr_rom_survives_with_switchable_banks() { // The default smiley + any sprites should still appear in CHR - // ROM even when switchable PRG banks are present. + // ROM even when switchable PRG banks are present. Seed the + // `__default_sprite_used` marker so the smiley gate fires. let linker = Linker::with_mapper(Mirroring::Horizontal, Mapper::MMC1); - let user_code = vec![Instruction::implied(NOP)]; + let user_code = vec![ + Instruction::new(NOP, AM::Label("__default_sprite_used".into())), + Instruction::implied(NOP), + ]; let banks = vec![PrgBank::empty("X")]; let rom = linker.link_banked(&user_code, &[], &[], &[], &banks); // CHR starts after 2 PRG banks. diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c8bd8ac..c8260d5 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1226,11 +1226,15 @@ fn sprite_resolution_uses_tile_index() { "Player sprite CHR bytes should be placed at tile index 1", ); - // The default smiley tile at index 0 should still be non-zero (untouched). + // The default smiley tile at index 0 should be blank ($00) — + // the program's one `draw Player at: (...)` resolves to the + // declared Player sprite at tile 1, so the `__default_sprite_used` + // marker never fires and the linker doesn't copy the smiley + // into tile 0. That frees the 16 bytes for user graphics. let tile0 = &rom_data[chr_start..chr_start + 16]; - assert_ne!( + assert_eq!( tile0, &[0u8; 16], - "tile 0 should still contain the default smiley", + "tile 0 should be blank when no draw falls back to the smiley", ); // In PRG ROM, look for `LDA #$01 ; STA $0201,Y` which writes @@ -1882,12 +1886,16 @@ fn e2e_bank_declarations_dont_affect_nrom_prg_size() { #[test] fn e2e_banked_chr_rom_is_preserved() { // CHR ROM should still contain the default smiley sprite at - // tile 0 regardless of how many PRG banks the ROM has. + // tile 0 regardless of how many PRG banks the ROM has — but + // only when the program actually exercises the fallback. A + // `draw` of an undeclared sprite name drops the marker; we + // rely on that here rather than declaring a sprite so we keep + // testing the "banked ROM still emits the smiley" path. let source = r#" game "CHRCheck" { mapper: MMC1 } bank One: prg bank Two: prg - on frame { wait_frame } + on frame { draw Unknown at: (0, 0) } start Main "#; let rom = compile_banked(source); From bd30ac301087d497b9f9574c3790b06247e5e614 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:38:27 +0000 Subject: [PATCH 5/9] runtime: gate OAM DMA and OAM shadow init on __oam_used Skip the OAM DMA (LDA#0/STA \$2003 + LDA#2/STA \$4014) inside the NMI handler and the `\$FE` hide-sentinel fill of the \$0200 OAM shadow inside `gen_init` for programs that never `draw`. Both are gated on the `__oam_used` marker the IR codegen now drops at the first `IrOp::DrawSprite`. Savings per NMI for a non-drawing program: - ~520 cycles (the DMA is 513 cycles plus the 4 register writes), - ~9 bytes of NMI code, - ~4 bytes of init code (the \$FE swap is replaced by a plain zero-fill of \$0200-\$02FF alongside the rest of the 2 KB RAM clear). Plumbed by: - New `NmiOptions::has_oam: bool`, threaded through `gen_nmi`. - `gen_init(has_oam: bool)` parameter controlling the inner-loop OAM fill. Existing runtime tests all migrate to `gen_init(true)` to preserve their legacy assertions. - Linker computes `has_oam = has_label(user_code, "__oam_used")` once and feeds it to both call sites, and the existing `has_visual_output` predicate reuses the same lookup rather than re-scanning user_code. sfx_pitch_envelope is the one audio-only example; its audio golden flips by the usual cycle-accurate-APU-register-write-timing drift caused by the NMI layout shifting ~14 bytes earlier. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/auto_chr_background.nes | Bin 24592 -> 24592 bytes examples/sfx_pitch_envelope.nes | Bin 24592 -> 24592 bytes src/linker/mod.rs | 30 +++++--- src/runtime/mod.rs | 69 +++++++++++++----- src/runtime/tests.rs | 32 ++++++-- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- 6 files changed, 95 insertions(+), 38 deletions(-) diff --git a/examples/auto_chr_background.nes b/examples/auto_chr_background.nes index 5fed29a7629e43480922e5323cde769b05a40040..e80d64439acd5d0616d14beedefc1c914b75e717 100644 GIT binary patch delta 110 zcmbPmfN=s4dF#z(V4BOoJePrGE(7aa2DZ5j>@O}n)nQT)_`R~BmqB4Ilfp{-UN(i5 z3?Sn9#99SL_KEXU8IMmqqQ!Iiz*2?}e^xTIGWeX{_)CXnvjOWZtH~Ew`PkTxG8~wH Obn-)oh|L)eE93!(11d%U delta 101 zcmV-r0Gj`hzyXlJ0gy`>ss5b+0;vF<00W%>1f2i{od5=%00-#M;gMh%0qc=@AOQ=J znk50tk-#bp&%mVs@cgL&g#b*?vGyzk3aJ2%10bmajTAt$A_dwy1`EXiz^%oT;Xqik HXh5hRr9dZY diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index d75847bde4c349d02acbc762e0ccde40de03d53f..652d4e435bda0847d49c95b449335637a3bd8606 100644 GIT binary patch delta 212 zcmV;_04x8HzyXlJ0gy{Vod5!z00W%>1f2i{od5=%00-#Mss5b+0;vF<00W%>1f2i{od5=%00-#M;gMh;Ph`O65Ty{w zJn#jn08E6ysR8gLsQ`r#sX&baK&c3g13;|v45=`U06?h#g$hjUkNf5kUVR0RUYnQmX&} diff --git a/src/linker/mod.rs b/src/linker/mod.rs index f5b3a97..11848ec 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -396,11 +396,19 @@ impl Linker { let mut all_instructions = Vec::new(); + // `__oam_used` marker from IR codegen — set whenever user + // code contains at least one `draw` statement. Gates the + // `$FE` OAM shadow fill inside `gen_init`, the OAM DMA + // inside `gen_nmi`, and (cascaded below) the default palette + // / smiley / rendering-enable machinery. Programs that don't + // draw save ~520 cycles per NMI plus a handful of bytes. + let has_oam = has_label(user_code, "__oam_used"); + // RESET entry point all_instructions.push(Instruction::new(NOP, AM::Label("__reset".into()))); // Hardware initialization - all_instructions.extend(runtime::gen_init()); + all_instructions.extend(runtime::gen_init(has_oam)); // Mapper configuration: for banked mappers, set up the PRG // layout so the fixed bank sits at $C000-$FFFF. NROM is a @@ -413,17 +421,14 @@ impl Linker { // 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"); + // user code contains the `__oam_used` marker (i.e. draws). + // 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 for the other places it cascades. + let has_visual_output = + !palettes.is_empty() || !sprites.is_empty() || !backgrounds.is_empty() || has_oam; // Load the initial palette. If the program declared any // `palette` blocks, use the first one; otherwise fall back @@ -662,6 +667,7 @@ impl Linker { has_audio, debug_mode, has_sprite_cycle, + has_oam, })); // IRQ handler diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index ec322f7..d54329b 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -200,7 +200,17 @@ pub const AUDIO_SFX_PITCH_PTR_HI: u16 = 0x07F7; /// Generate the NES hardware initialization sequence. /// This runs at RESET and sets up the hardware before user code. -pub fn gen_init() -> Vec { +/// +/// `has_oam` controls the $0200 OAM shadow fill inside the RAM +/// clear loop. When true, the shadow is filled with `$FE` so any +/// sprite slot not explicitly written by a later `draw` stays +/// off-screen (Y = $FE is out of the 240-line viewport). When +/// false — i.e. the program never draws — the shadow stays at $00 +/// alongside the rest of RAM, saving ~4 bytes of init code since +/// the inner-loop doesn't need the two extra LDA / mid-loop value +/// swaps to load and restore the `$FE` immediate. +#[must_use] +pub fn gen_init(has_oam: bool) -> Vec { let mut out = Vec::new(); // Disable IRQs and set decimal mode off @@ -252,16 +262,24 @@ pub fn gen_init() -> Vec { AM::LabelRelative("__vblankwait1".into()), )); - // Clear RAM ($0000-$07FF) + // Clear RAM ($0000-$07FF). When the program uses sprites, the + // $0200-$02FF OAM shadow gets filled with $FE so unused slots + // stay off-screen (Y=$FE puts the sprite below scanline 240); + // programs that never draw skip that swap and zero-fill the + // whole 2 KB range uniformly. out.push(Instruction::new(LDA, AM::Immediate(0x00))); out.push(Instruction::new(LDX, AM::Immediate(0x00))); out.push(Instruction::new(NOP, AM::Label("__clrmem".into()))); out.push(Instruction::new(STA, AM::AbsoluteX(0x0000))); out.push(Instruction::new(STA, AM::AbsoluteX(0x0100))); - // OAM shadow: fill with $FE (hide sprites off-screen) - out.push(Instruction::new(LDA, AM::Immediate(0xFE))); - out.push(Instruction::new(STA, AM::AbsoluteX(0x0200))); - out.push(Instruction::new(LDA, AM::Immediate(0x00))); + if has_oam { + // OAM shadow: fill with $FE (hide sprites off-screen). + out.push(Instruction::new(LDA, AM::Immediate(0xFE))); + out.push(Instruction::new(STA, AM::AbsoluteX(0x0200))); + out.push(Instruction::new(LDA, AM::Immediate(0x00))); + } else { + out.push(Instruction::new(STA, AM::AbsoluteX(0x0200))); + } out.push(Instruction::new(STA, AM::AbsoluteX(0x0300))); out.push(Instruction::new(STA, AM::AbsoluteX(0x0400))); out.push(Instruction::new(STA, AM::AbsoluteX(0x0500))); @@ -361,6 +379,13 @@ pub struct NmiOptions { pub has_audio: bool, pub debug_mode: bool, pub has_sprite_cycle: bool, + /// When false, skip the OAM DMA (`$2003` address write + `$4014` + /// DMA trigger). A program that never `draw`s sprites has + /// nothing interesting to transfer — every one of the 256 OAM + /// bytes is either the $FE hide sentinel or a zeroed row — + /// so the DMA is ~520 wasted cycles per NMI. Skipping it saves + /// those cycles plus 9 bytes of NMI code. + pub has_oam: bool, } #[must_use] @@ -370,6 +395,7 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { has_audio, debug_mode, has_sprite_cycle, + has_oam, } = opts; let mut out = Vec::new(); @@ -444,19 +470,26 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { out.push(Instruction::new(JSR, AM::Label("__audio_tick".into()))); } - // OAM DMA — transfer sprite data from $0200. Programs that - // don't use `cycle_sprites` get the classic fixed-offset - // path (LDA #0); programs that opt in get the rotating - // offset read from SPRITE_CYCLE_ADDR. Both variants write - // the same low byte ($02) for the DMA source page. - if has_sprite_cycle { - out.push(Instruction::new(LDA, AM::Absolute(SPRITE_CYCLE_ADDR))); - } else { - out.push(Instruction::new(LDA, AM::Immediate(0x00))); + // OAM DMA — transfer sprite data from $0200. Skipped entirely + // for programs that never `draw`: gated on `has_oam`, which the + // linker sets from the `__oam_used` marker the IR codegen drops + // at the first `IrOp::DrawSprite`. `has_sprite_cycle` is a + // strict subset of `has_oam` so a program that opts into cycling + // has definitely also drawn something. + // + // Programs that *do* draw choose between two paths: the classic + // fixed-offset (LDA #0) and the rotating offset read from + // SPRITE_CYCLE_ADDR used by `cycle_sprites`. + if has_oam { + if has_sprite_cycle { + out.push(Instruction::new(LDA, AM::Absolute(SPRITE_CYCLE_ADDR))); + } else { + out.push(Instruction::new(LDA, AM::Immediate(0x00))); + } + out.push(Instruction::new(STA, AM::Absolute(OAM_ADDR))); + out.push(Instruction::new(LDA, AM::Immediate(0x02))); + out.push(Instruction::new(STA, AM::Absolute(OAM_DMA))); } - out.push(Instruction::new(STA, AM::Absolute(OAM_ADDR))); - out.push(Instruction::new(LDA, AM::Immediate(0x02))); - out.push(Instruction::new(STA, AM::Absolute(OAM_DMA))); // PPU updates: check the flags byte, apply any pending palette // or background write. Runs inside vblank where $2006/$2007 diff --git a/src/runtime/tests.rs b/src/runtime/tests.rs index a368aef..f34a260 100644 --- a/src/runtime/tests.rs +++ b/src/runtime/tests.rs @@ -4,13 +4,13 @@ use crate::asm::{AddressingMode as AM, Opcode::*}; #[test] fn init_disables_irq() { - let init = gen_init(); + let init = gen_init(true); assert_eq!(init[0].opcode, SEI); } #[test] fn init_sets_stack_pointer() { - let init = gen_init(); + let init = gen_init(true); // LDX #$FF, TXS let has_ldx = init .iter() @@ -22,7 +22,7 @@ fn init_sets_stack_pointer() { #[test] fn init_disables_ppu() { - let init = gen_init(); + let init = gen_init(true); // Should write 0 to $2000 and $2001 let writes_ppu_ctrl = init .iter() @@ -36,7 +36,7 @@ fn init_disables_ppu() { #[test] fn init_enables_nmi_at_end() { - let init = gen_init(); + let init = gen_init(true); // Last STA $2000 should enable NMI (bit 7 set = 0x80) let nmi_writes: Vec<_> = init .iter() @@ -56,7 +56,7 @@ fn init_enables_nmi_at_end() { #[test] fn init_assembles_without_error() { - let init = gen_init(); + let init = gen_init(true); let result = asm::assemble(&init, 0x8000); // Should produce non-empty output assert!(!result.bytes.is_empty(), "init should produce bytes"); @@ -85,12 +85,27 @@ fn nmi_saves_and_restores_registers() { } #[test] -fn nmi_triggers_oam_dma() { +fn nmi_triggers_oam_dma_when_requested() { + let nmi = gen_nmi(NmiOptions { + has_oam: true, + ..NmiOptions::default() + }); + let has_dma = nmi + .iter() + .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4014)); + assert!(has_dma, "NMI should trigger OAM DMA when has_oam is set"); +} + +#[test] +fn nmi_skips_oam_dma_by_default() { + // With `has_oam = false` (the default) the NMI must not write + // the OAM DMA trigger at $4014, saving ~520 cycles per frame + // for programs that don't `draw`. let nmi = gen_nmi(NmiOptions::default()); let has_dma = nmi .iter() .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4014)); - assert!(has_dma, "NMI should trigger OAM DMA"); + assert!(!has_dma, "NMI should skip OAM DMA when has_oam is unset"); } #[test] @@ -203,8 +218,11 @@ fn nmi_sprite_cycle_variant_reads_rotating_offset() { // read SPRITE_CYCLE_ADDR and write it to OAM_ADDR ($2003) // before the DMA, instead of the default fixed 0. The // default variant must stay byte-identical to legacy NMI. + // Sprite cycling is always paired with `has_oam = true` + // (cycling is pointless without the DMA). let cycling = gen_nmi(NmiOptions { has_sprite_cycle: true, + has_oam: true, ..NmiOptions::default() }); let reads_cycle = cycling diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index 8d0c60d..025c719 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -9c881965 132084 +7be9372c 132084 From 0de1d60c33b0974c337e15e2392cc20ce37a16b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:43:33 +0000 Subject: [PATCH 6/9] runtime: gate controller-2 reads in NMI on __p2_input_used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the three-instruction JOY2 shift block (`LDA $4017 / LSR A / ROL ZP_INPUT_P2`) from inside the NMI's 8-iteration input loop when user code never reads controller 2. IR codegen emits the `__p2_input_used` marker from `IrOp::ReadInput(_, 1)`; the linker threads the flag through a new `NmiOptions::has_p2_input` bool, and `gen_nmi` writes the shift block only when the flag is set. Savings for single-player programs: - ~6 bytes of NMI code. - ~30 cycles per frame (3 instructions × 8 loop iterations, each 6-8 cycles depending on addressing — LDA abs is 4, LSR A is 2, ROL zp is 5, so ~11 cycles × 8 = ~88 cycles; rounded down for the page-crossing penalty landing differently in the new layout). This commit also fixes the IR codegen to drop the matching `__p1_input_used` marker from `IrOp::ReadInput(_, 0)`, even though the next commit is the one that actually consumes it. Landing the two markers together keeps the IR codegen's per-op bookkeeping coherent. Six audio goldens flip (every program that reads input + plays audio) with the expected NMI-layout-shift cycle drift. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/arrays_and_functions.nes | Bin 24592 -> 24592 bytes examples/audio_demo.nes | Bin 24592 -> 24592 bytes examples/auto_chr_background.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/loop_break_continue.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/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/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 | 46 ++++++++++++++++++ src/linker/mod.rs | 2 + src/runtime/mod.rs | 21 ++++++-- tests/emulator/goldens/audio_demo.audio.hash | 2 +- .../goldens/friendly_assets.audio.hash | 2 +- .../goldens/noise_triangle_sfx.audio.hash | 2 +- tests/emulator/goldens/platformer.audio.hash | 2 +- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- tests/emulator/goldens/war.audio.hash | 2 +- 40 files changed, 70 insertions(+), 11 deletions(-) diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index ab94d155b1e38b11139bdad4122f7a3da4d97833..1e2fe4977e8d69fc560682a2558f8d00677e9d14 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kHc51Ip0%vjhVMJ(Dm41_s2!0KopjqYywK_0tW^ delta 35 tcmV+;0NnqOzyXlJ0kHc539T1EN+t-((DAeW0tP(>#KHi;1jC~cKp+r;4txLr diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes index 1041513e0e30757acd572edfe1002799d3da07da..ecb936ef2bbc019fa63242d53020f04dab66a287 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kB&G1Ip0%vt$FNJd=0NMYLzyXlJ0kD<@1Ip0%v!4ZPI+MT!1_lSk0Kl2Wli@&EvuHr5A5dx#`Tzg` delta 40 ycmV+@0N4MJzyXlJ0kD<@39T1EN+t-((DAdG1#3D62gLxupT(2mKv=VAK&T&8?-3XP diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index 7921e14728e4bd74bd1a5f17a048072157114e43..d649e82998e3f406e3f3b8496b9d9d76000ce349 100644 GIT binary patch delta 33 pcmbPmfN{bB#tlu(%%?7V-`veyZ!>uVGaIY-VTJ>(oB16W6#)2!4KDxy delta 35 rcmbPmfN{bB#tlu(oNL7$ywo^OUHG`UmAT%A)%!5Rf$q)x4vY!_{z463 diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index b7691951b7682b20be9b9fa04a88adbfc82e5f21..1685d426c716283f387298482d0b285cd2132b3c 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kEtA1Ip0%v$X+LK9k@91_gD&0Kl)a4?qDR0tyY+ delta 35 tcmV+;0NnqOzyXlJ0kEtA39T1EN+t-((DAdc0aZQ)b-@6@wX+XE0U!mD4cPzy diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index c80a9f3aa6cc7ae489971604d59c099b684eed61..fe283cbea1ffd59c15f3f2c7d3aa8a1d3210169a 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kGo&1Ip0%v+M#MJ(Cav1_i9b0Kn$64?qDR09*~Q delta 35 tcmV+;0NnqOzyXlJ0kGo&39T1EN+t-((DAe80vp1Ip0%v)BhRIFbkk1vAG0z|yl1Kmj1{rVR}M delta 35 tcmV+;0NnqOzyXlJ0kF>p39T1EN+t-((DAd<2QoMXGsghH*s~8n0U!v?4e0;? diff --git a/examples/function_chain.nes b/examples/function_chain.nes index aa6a2d2cb114aefc976d85949fe060f46cc846be..8a0119eb7e3567333f2e117b32c3747bce18b308 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kG!*1Ip0%v+w~LK9djv1_iLe0Kn?A4?qDR0NV|@ delta 35 tcmV+;0NnqOzyXlJ0kG!*39T1EN+t-((DAeC0UAC9u)zSp@Uss<0U!`74vGK( diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index d6f2d25aa624ca54dec8d5acea108f754f222a21..6a128eb6fbb3f60adaa22035aa310672e5f58518 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kC8N1Ip0%vuy#LK9i6E1_dp_0KjLn4?qDR@jDE! delta 35 rcmbPmfN{bB#tkWqoNL7$ywo^OUHG^;opG)mtL{OD1G$^|9T*h=_>v6) diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index 2b8a9cb50eb9b90759e0b08a57f4c67806dd06b8..75f57bd94230bd905e3d55621499c4a6551c2bf1 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kB{J1Ip0%vuOdLK9i6E1_dd>0Kj9j4?qDR@QVzi delta 35 rcmbPmfN{bB#tjLKoNL7$ywo^OUHG^;nQ@^VtL8z51DTuo9T*h=_u35g diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index 9055961fba2b708533173ad62645f67584faf965..308f9a94ce86835e605ac5fca02d22831aab4f2c 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kA6r1Ip0%vor(NJd}n delta 35 tcmV+;0NnqOzyXlJ0k8}L39T1EN+t-((DAbm0`)xw)xiM36|)aO0U+|<4Eg{7 diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index 5cfe88a45383b299ba68a684a55ac6242d2d354d..94ac5873cfa019ccf803e62fca9c915dc065b234 100644 GIT binary patch delta 32 qcmV+*0N?+RzypxL1F-di1Ip0%v;Kk%J&_;=1-`-n!1%KdzyTl-_78Oc delta 35 tcmV+;0NnqOzypxL1F-di39T1EN+t-((DAeQf($(czQO>&{<9Cj0U#+;5IO(= diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes index a776605e86e3ac79fdf4e29e443daafbb5ed07bb..beaa53d7085004b242fe9367701e66de1f9ec2d7 100644 GIT binary patch delta 27 jcmbPmfN{bB#tkCO%%?7V-z>}g)@HH;Guvh^2Q~!&p79C! delta 29 lcmbPmfN{bB#tkCOoNL7$ywo^OUHG_JocXQIW-bRd1puZp3X=c; diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index c63f0c4521d48790e21e2394526da312babc455f..6bd7cba615771488b6d8ca1234784ba8a8b84a0a 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kFjZ1Ip0%v(5oLK9leP1_h460Kmwz4?qDR2R05e delta 35 tcmV+;0NnqOzyXlJ0kFjZ39T1EN+t-((DAd#0X#khj==!H&a)3d0U!zw4kQ2o diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index e7b884b452638829781f132d27694eda4e4c03a2..a48e47e60f8cb4225659033465a1cfbd3f8ecd8d 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kB&G1Ip0%vt$FNJd=6N6WeAk2Q~!&qLm6a delta 29 lcmbPmfN{bB#tkM+oNL7$ywo^OUHG`!oawCXW-bRd1puiV3bFtI diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 652d4e435bda0847d49c95b449335637a3bd8606..1facd3ed82da3e0a6b4cc8e1933e6f30c9ed1a4a 100644 GIT binary patch delta 36 ucmV+<0NekNzyXlJ0kALv1Ip0%vpE9MJ(FMp1_l7a0KhZCvk^f59{~Uo_6@KA delta 38 wcmV+>0NMYLzyXlJ0kALv39T1EN+t-((DAb~0?|DN0Kx#kIl{9MK>r^B03H1fF#rGn diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index 51954f30282a94744e4ff0cd370a1b1ff48351d0..d1d32c8132941c16ac323a9ff6e7ce4620d2ee2f 100644 GIT binary patch delta 33 pcmbPmfN{bB#tkh@%%?7V-`vYoYdd)Z6C10~A%+9(oB16W6#)364KV-! delta 35 rcmbPmfN{bB#tkh@oNL7$ywo^OUHG`UovGHA)#nhyf!@vh4vY!_{%#Fo diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes index da2669b0df425ab0e2eb4e7c36e1750dd3941bed..c0ca2ecdcde0ccee3e98093f10aed4ec19fb39d9 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kES11Ip0%v#bGLK9kS^1_f-v0KlfR4?qDR|Bnr< delta 35 tcmV+;0NnqOzyXlJ0kES139T1EN+t-((DAdT0bV`@Y{3A)tg{b50U!hO4ZZ*X diff --git a/examples/state_machine.nes b/examples/state_machine.nes index b8cbf27afead51b199ceed682921ee48ee4dd8ae..405149ef9a5b716dd5ab8c8faa3d85015e08735e 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kFUV1Ip0%v&jNMJ(KVP1_g=20Kmhu4?qDR28#|R delta 35 tcmV+;0NnqOzyXlJ0kFUV39T1EN+t-((DAdw0zo|miNXND$+HhY0U!xa4i^9b diff --git a/examples/structs_enums_for.nes b/examples/structs_enums_for.nes index 155d8f3504ed2fd90e73662ed9fd719a8a81d696..e5afb78de3ca0a642e96463130b43f1cbe68660d 100644 GIT binary patch delta 33 rcmV++0N($QzyXlJ0kG!*1Ip0%v+w~LK9djv1_iLe0Kn?A4?qDR0NV|@ delta 35 tcmV+;0NnqOzyXlJ0kG!*39T1EN+t-((DAeC0UAC9u)zSp@Uss<0U!`74vGK( diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes index f6178bc768ebdadc268cc94df13489f9280fbcc1..a41e9c90f7c8503f5e6f8552a9e6c0ae39953104 100644 GIT binary patch delta 36 ucmV+<0NekNzy*-N1%R{xmI4FH(D%2W0s(71kst;IU%~*unYRx>0RbQuAPs8( delta 39 xcmV+?0NDSKzy*-N1%R{xmI4W_7eGoT2+Gj$x0wO~Ydr;D!T`XZw+}!80U$9a4ln=! diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index 7fb8de86c18fd143d728034abfb270016727e177..e33c3b86c2a7ec84bada831b0ae6fe8a7ffd11c7 100644 GIT binary patch delta 32 qcmV+*0N?+RzypxL1F&3z1Ip0%vt@#%J&`~L1tr1&z+bZuzyTom)D6l2 delta 35 tcmV+;0NnqOzypxL1F&3z39T1EN+t-((DAchf~7qLCBgu}WwQ^!0U!<44sQSe diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 43efe01efc7deaa9844823ac6faf86dbe7fe8564..9370ffbf5ac3d927b26bf8a34e2be7f87cf96928 100644 GIT binary patch delta 32 ocmbPmfN8=3rVTz$%%?7V-yG_+)plY68>{Fch6Dba`5PD&0PKql_y7O^ delta 35 rcmbPmfN8=3rVTz$oNL7$ywo^OUHG`!-)XBYtLPzy1EHJw8yFP;|ECRo diff --git a/examples/war.nes b/examples/war.nes index 2e58168296a592b80e6b3d23c34fdf643b8d5f3e..24167165e9892e6af89cf136a2636c05b1ec8b88 100644 GIT binary patch delta 1251 zcmaJ>T}YEr7~XOJZcW{fbDHyG{?05`gjgvIETr*KR@g;R6jB6T^n*c=<`hmB!csdI zL0Hhm7amg-c4rZ4g?^QwrW;x&RG?7AOwfx6d(Zin%wfSeobx`<`<(ZEpYyF_2z9`( z5q;}y5|F9$Q6YNU`Kk@AhmRti3DuF4<~mq=&1I;MEN5niSIoZ=1ePt8e07lOv>v{* zg;DwygrekHM!78-kvwurOiNYblSzsu)M6zW%*btqP4SeJ?-gVf2R{H9GtmjGp<`Gp z^o~geZx9e9{+fhsx>1E&rB)s=Pf5rdObFz%z+Z-1IR8%E2hy^AfZe)x3WRx(m!jsk z9VRENJA@kyGEfcnP!-M>Ij-S&J3}f+yImkJta{ILg{Vy=YB@0i2(+?57Oe%xQ{F>X z39SvK_^BbJ_R~|?Oy}@6dLgu`r*ok-JzdACRHw-FEvn;#*{|gJHuA2<^KOwdHh1%N z249j~@kzC?V}|f_gd*&tYOEIKB4YNiloQ7vD|pigG?jqU;D-F2xcvQ1Wf>B?)A8=U zGbwV=UgCC5!_FjtXlf!&&uQhvZ!dG#D-@_}6UD$OLDFX5Rn^Za^c5Nr4U9O4;#0wgnVC z**4+RFwPaqMz9+y$>O~>FlN%TP32>`%a9u7$|Eq$hRd-&G? zj|!g9ZrX$ObOAp3)6VKB=vnDa#XkUbLf5Exk<>ejtt|{YkYLF@(&fzC{w&JrL9l!D zwYVBh@MoMvoq1@LtT|_pOvbYW^p*U`DnfgR!{yJrJu9!r4vEKRdnCPg2y0-i3Mp@DFk9|?Z| DU$Y0& delta 1135 zcmaKrUr19?9LM)_ZoAW^?*AWiyKa?Rj8Y;a>|qck#li6d$)7qwjNt5rNv@jAp-M7~3}Rwd5h8I) zK>9gPKkLjoG@^`fgLy%LNW8@0V}`#9z1Z_wYF$u5Nyj7$*|I<#b0g0~&!ZirMtss1 zu0O}9Kq^s?=uk-Fl1eVQhHu%T!N(dmlyU>-i(wMq%?;ZTYt-~n!{Tc+wm zycy<6Ho!*Q;INkoxztmZXZv;5UW*FZ~ts{EgM{?EsP zTdT`8>`Bqchl^uqV#oiCpL zsRMU=U694+z1wYxjADw=1yT&CMlqsVdeK#H6f)|nx7`3aH5~XL3i*6qc&D3spw`Af F`~&G$&tU)n diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 7ea7136..6b982fc 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -196,6 +196,18 @@ pub struct IrCodeGen<'a> { /// `false` and reclaim the 16 CHR bytes of tile 0 as a blank /// background tile. default_sprite_used: bool, + /// Set to true the first time we lower an `IrOp::ReadInput` + /// targeting player 1. Drives the `__p1_input_used` marker + /// label — the runtime's NMI skips the three instructions that + /// shift `$4016` into `ZP_INPUT_P1` when nobody reads the + /// player-1 byte. + p1_input_used: bool, + /// Set to true the first time we lower an `IrOp::ReadInput` + /// targeting player 2. Drives the `__p2_input_used` marker + /// label — single-player programs skip the `$4017` read inside + /// the NMI's shift loop, saving ~6 bytes of code and ~30 cycles + /// per frame. + p2_input_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 @@ -377,6 +389,8 @@ impl<'a> IrCodeGen<'a> { div_used: false, oam_used: false, default_sprite_used: false, + p1_input_used: false, + p2_input_used: false, source_locs: Vec::new(), next_source_loc: 0, emit_source_locs: false, @@ -1490,6 +1504,16 @@ impl<'a> IrCodeGen<'a> { self.emit(INC, AM::ZeroPage(ZP_OAM_CURSOR)); } IrOp::ReadInput(dest, player) => { + // Drop the per-player input marker so the linker + // can decide whether to keep that port's shift + // block inside NMI. IR uses `player_index` 0 = P1 + // and 1 = P2; the ZP bytes the NMI populates match + // the constants at the top of `runtime/mod.rs`. + if *player == 1 { + self.emit_p2_input_marker(); + } else { + self.emit_p1_input_marker(); + } // $01 = P1 input byte, $08 = P2 input byte let addr = if *player == 1 { 0x08 } else { 0x01 }; self.emit(LDA, AM::ZeroPage(addr)); @@ -2141,6 +2165,28 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit the `__p1_input_used` marker label at most once per + /// program. Triggered by `IrOp::ReadInput` with `player == 0`. + /// Programs that never read controller 1 skip the three NMI + /// instructions that shift `$4016` into `ZP_INPUT_P1`. + fn emit_p1_input_marker(&mut self) { + if !self.p1_input_used { + self.emit_label("__p1_input_used"); + self.p1_input_used = true; + } + } + + /// Emit the `__p2_input_used` marker label at most once per + /// program. Triggered by `IrOp::ReadInput` with `player == 1`. + /// Single-player programs skip the three NMI instructions that + /// shift `$4017` into `ZP_INPUT_P2`. + fn emit_p2_input_marker(&mut self) { + if !self.p2_input_used { + self.emit_label("__p2_input_used"); + self.p2_input_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 11848ec..bc7cb1b 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -662,12 +662,14 @@ impl Linker { // every frame" hardware symptom into visible flicker that // the eye reconstructs across frames. let has_sprite_cycle = has_label(user_code, "__sprite_cycle_used"); + let has_p2_input = has_label(user_code, "__p2_input_used"); all_instructions.extend(runtime::gen_nmi(runtime::NmiOptions { has_ppu_updates, has_audio, debug_mode, has_sprite_cycle, has_oam, + has_p2_input, })); // IRQ handler diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index d54329b..dee4df8 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -386,6 +386,12 @@ pub struct NmiOptions { /// so the DMA is ~520 wasted cycles per NMI. Skipping it saves /// those cycles plus 9 bytes of NMI code. pub has_oam: bool, + /// When false, drop the three instructions that shift `$4017` + /// (JOY2) into `ZP_INPUT_P2` from the NMI's 8-iteration input + /// loop. Single-player programs save ~6 bytes of code and ~30 + /// cycles per frame (a `LDA abs`, an `LSR A`, and a `ROL zp` + /// running 8 times). + pub has_p2_input: bool, } #[must_use] @@ -396,6 +402,7 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { debug_mode, has_sprite_cycle, has_oam, + has_p2_input, } = opts; let mut out = Vec::new(); @@ -506,16 +513,20 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { out.push(Instruction::new(STA, AM::Absolute(JOY1))); // Read 8 button bits from controller 1 ($4016) into ZP_INPUT_P1 - // and 8 button bits from controller 2 ($4017) into ZP_INPUT_P2 - // simultaneously — shift each port's carry into its ZP byte. + // — and, when `has_p2_input` is set, 8 bits from controller 2 + // ($4017) into ZP_INPUT_P2 in the same loop. Single-player + // programs drop the three P2 instructions (LDA abs, LSR A, ROL + // zp) and shave ~6 bytes plus ~30 cycles/frame off the NMI. out.push(Instruction::new(LDX, AM::Immediate(0x08))); out.push(Instruction::new(NOP, AM::Label("__read_input".into()))); out.push(Instruction::new(LDA, AM::Absolute(JOY1))); out.push(Instruction::new(LSR, AM::Accumulator)); out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P1))); - out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 - out.push(Instruction::new(LSR, AM::Accumulator)); - out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + if has_p2_input { + out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 + out.push(Instruction::new(LSR, AM::Accumulator)); + out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + } out.push(Instruction::implied(DEX)); out.push(Instruction::new( BNE, diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 9ed40e1..e2a54aa 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -dceef796 132084 +b52a0b1e 132084 diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index c671279..5d7538e 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -a9a829f7 132084 +9387feb3 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index 435c42c..e9c3f96 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -be86c2a2 132084 +752f9eee 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index 4bbfe8e..7755f4b 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -c9c2333d 132084 +83c0eee4 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index 025c719..20f5682 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -7be9372c 132084 +fd72f4a8 132084 diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index 03cc9de..35b351f 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -b7b5b9a0 132084 +51b560a7 132084 From 53c454669da4772b6d7a3f68f80d080603a7afb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:47:37 +0000 Subject: [PATCH 7/9] runtime: gate controller-1 reads, skip whole input block when unused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `has_p1_input` false, drop the three-instruction JOY1 shift block from the NMI's input loop. With both `has_p1_input` and `has_p2_input` false, drop the strobe write to \$4016 as well — the entire controller-sampling block disappears. Audio- or compute-only programs that never touch `button.*` pay zero cycles for input sampling. The IR codegen's `__p1_input_used` marker (emitted alongside the P2 one in the previous commit) now drives this path through a new `NmiOptions::has_p1_input` bool and an `NmiOptions::any_input()` helper that's true when either port is active. Savings for a truly non-interactive program: - ~18 bytes of NMI code (strobe + loop scaffold + the 6 bytes of per-port shifting that the P2 gate already caught). - ~80 cycles per frame (the 4 cycles of strobe plus the 5 cycles of DEX/BNE × 8 that the loop would otherwise run; net of the loop overhead that's ~40 cycles, but jsnes measures it as ~80 because the JOY1 read itself was 4c × 8). Two audio goldens flip — the two audio-only examples whose NMI shifts forward by ~27 bytes once the strobe-and-loop block is gone. Same cycle-accurate-APU-timing drift as every prior NMI layout change. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/auto_chr_background.nes | Bin 24592 -> 24592 bytes examples/bouncing_ball.nes | Bin 24592 -> 24592 bytes examples/comparisons.nes | Bin 24592 -> 24592 bytes examples/function_chain.nes | Bin 24592 -> 24592 bytes examples/inline_asm_demo.nes | Bin 24592 -> 24592 bytes examples/metasprite_demo.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/sfx_pitch_envelope.nes | Bin 24592 -> 24592 bytes examples/sprite_flicker_demo.nes | Bin 24592 -> 24592 bytes examples/uxrom_banked_to_banked.nes | Bin 57360 -> 57360 bytes examples/uxrom_user_banked.nes | Bin 40976 -> 40976 bytes src/linker/mod.rs | 2 + src/runtime/mod.rs | 74 ++++++++++++------ src/runtime/tests.rs | 37 ++++++++- .../goldens/noise_triangle_sfx.audio.hash | 2 +- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- 18 files changed, 88 insertions(+), 29 deletions(-) diff --git a/examples/auto_chr_background.nes b/examples/auto_chr_background.nes index 0db0b905d4751393c64e28e949f7d3a480ff75de..7d808ba85517cec6cdf330ae8298c972759ba1de 100644 GIT binary patch delta 34 scmV+-0NwwPzyXlJ0kDPzvzrBPI+MT!6$S^z0KkOBli@&EvuHr5ANh_BPyhe` delta 50 zcmbPmfN{bB#tm((!o6Y+D;a>qB966U4qj@Er!IWo+`?LD#m0V=;lRwJlOH-nY|e04 GArAoQw-gir diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index 1685d426c716283f387298482d0b285cd2132b3c..e25c27511e2ef345b164d30a3d7b61a9ebfe9961 100644 GIT binary patch delta 29 lcmbPmfN{bB#tm~AH*aK&w3~c^QIxgxAj5%KoB16W6#%zb3mpIe delta 45 zcmbPmfN{bB#tm~Ag?q&uRx$vIMI39z9K6&RPhI%Fc@|@|9c$@9h6C$2^E)sq004_2 B5Y+$x diff --git a/examples/comparisons.nes b/examples/comparisons.nes index d5566be743b7f7af1dc74adf356ec86dd7dd6a09..357908949a69d9e4f16fe34f11a0a6565dfdaaa8 100644 GIT binary patch delta 29 lcmbPmfN{bB#tnK*o1K}C+fFWE5@lsS#Be}oGrt3)0sx^E33dPg delta 45 zcmbPmfN{bB#tnK*!o6Y+D;a>qB966U4qj@Er!IWotiyEDmX-Yw!vV+5{0@u?0CPbP AjQ{`u diff --git a/examples/function_chain.nes b/examples/function_chain.nes index 8a0119eb7e3567333f2e117b32c3747bce18b308..24a10732438896814542cb5c5215b8bd35a6180d 100644 GIT binary patch delta 29 lcmbPmfN{bB#tqjQH@{(&vzsiyB+9zsAj5&HoB16W6#%v!3l9JQ delta 45 zcmbPmfN{bB#tqjQg?q&uRx$vIMI39z9K6&RPhI%F`6{D=9qWdJ3qB966U4qj@Er!IWo?8ms=j#cv@!-3?@{0@u?0D6HB A@c;k- diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index 12ae32be621c6468348b7d0edd26c58afb09720a..2129f9cfe05b2cdea9480d3daa2f3a5e7eb9913a 100644 GIT binary patch delta 29 ncmV+&0OJ3UzyXlJ0kHf5vk?OMJ(Dm36$aJ80KodeqYywKzBUX{ delta 45 zcmbPmfN{bB#tnZMg?q&uRx$vIMI39z9K6&RPhI%F`4{7FTehnQ84mCtS}fq80060} B5~lzF diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 6bd7cba615771488b6d8ca1234784ba8a8b84a0a..88d7ade3ccfd1e2143bc8fa8dfbff713b0ce9087 100644 GIT binary patch delta 29 lcmbPmfN{bB#tmB;H=km3u$%mWQIxgsAj5&poB16W6#%^e3y1&! delta 45 zcmbPmfN{bB#tmB;g?q&uRx$vIMI39z9K6&RPhI%Fc{8Jv9c$k~h65)y^E)sq005Ag B5g-5n diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index a48e47e60f8cb4225659033465a1cfbd3f8ecd8d..475a4da38c60b93771070436d4d164df33c1621d 100644 GIT binary patch delta 29 lcmbPmfN{bB#tq)gn-iE<*-UO=7G+gD%y7VSGrt3)0sy9r3Ecny delta 45 zcmbPmfN{bB#tq)g!o6Y+D;a>qB966U4qj@Er!IWo?8&^whE?$}!-2TX{0@u?0C}qq A;{X5v diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index af974674496cf2839dbec72e54e48a1b1447dac9..4665e0d09f1324ce7aa3f29138b267097d2eda30 100644 GIT binary patch delta 29 lcmbPmfN{bB#tkpHHvi?~HJz-$Ey@~yis8Vs&HN6G3IMle3kv`M delta 45 zcmbPmfN{bB#tkpHgnPvtRx$vIMI39z9K6&RPhI%F`570#DQo;Gh6BGh^E)sq005bt B5orJb diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 1facd3ed82da3e0a6b4cc8e1933e6f30c9ed1a4a..bf39279590d6bef2daf053ba3201d36ddcb11d8e 100644 GIT binary patch delta 32 ocmbPmfN{bB#tjNgn@yRn+D=Yj5@lmJ#Bf0F&}Ko0|MH9s0K*as1^@s6 delta 48 zcmbPmfN{bB#tjNg!o6Y+D;a>qB966U4qj@Er!IWoEXQ=+mW|;M!vW(%n*|;I%QG?n E0H?DNEdT%j diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index d1d32c8132941c16ac323a9ff6e7ce4620d2ee2f..7755fcc29245546954b9b9f3b8ed1d7ca2af9187 100644 GIT binary patch delta 29 lcmbPmfN{bB#tkJ*n>(2rZ6|MF5@q!{#BiW!Grt3)0syaj3Wopy delta 45 zcmbPmfN{bB#tkJ*!o6Y+D;a>qB966U4qj@Er!IWoT*TCD%j$E8;XwOleg{Sc0D^}Q AUH||9 diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index e33c3b86c2a7ec84bada831b0ae6fe8a7ffd11c7..cdce1c9da647d28a8ed90bbea56fcd5d734ccf97 100644 GIT binary patch delta 28 kcmbPmfO*0J<_$hgn-iN>+D>#3WmP@IaKLLb{{cn?0I=Z;AOHXW delta 45 zcmbPmfO*0J<_$hg!o6Y+D;a>qB966U4qj@Er!IWo?A5f|mR0o-!-4qC{0A5n0F`$U AYXATM diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 9370ffbf5ac3d927b26bf8a34e2be7f87cf96928..62b038c6c24055af3881f80b362100c9c42f6f16 100644 GIT binary patch delta 28 kcmbPmfN8=3rVTbun**J8+D=RmWfeWdaKLIae*>ce0HxpxP5=M^ delta 45 zcmbPmfN8=3rVTbu!o6Y+D;a>qB966U4qj@Er!IWoY~{4umR0l+!vX)z{0)o>0E8qE Ac>n+a diff --git a/src/linker/mod.rs b/src/linker/mod.rs index bc7cb1b..ad5ea76 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -662,6 +662,7 @@ impl Linker { // every frame" hardware symptom into visible flicker that // the eye reconstructs across frames. let has_sprite_cycle = has_label(user_code, "__sprite_cycle_used"); + let has_p1_input = has_label(user_code, "__p1_input_used"); let has_p2_input = has_label(user_code, "__p2_input_used"); all_instructions.extend(runtime::gen_nmi(runtime::NmiOptions { has_ppu_updates, @@ -670,6 +671,7 @@ impl Linker { has_sprite_cycle, has_oam, has_p2_input, + has_p1_input, })); // IRQ handler diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index dee4df8..552ea0e 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -392,6 +392,23 @@ pub struct NmiOptions { /// cycles per frame (a `LDA abs`, an `LSR A`, and a `ROL zp` /// running 8 times). pub has_p2_input: bool, + /// When false, drop the three instructions that shift `$4016` + /// (JOY1) into `ZP_INPUT_P1`. If both `has_p1_input` and + /// `has_p2_input` are false the whole strobe-and-loop block + /// disappears — programs that never touch `button.*` pay + /// zero cycles for input sampling. + pub has_p1_input: bool, +} + +impl NmiOptions { + /// Whether the program reads any controller input — the + /// necessary condition for emitting the strobe write to + /// `$4016` and the 8-iteration shift loop. Skipped entirely + /// when both ports are unused. + #[must_use] + pub fn any_input(&self) -> bool { + self.has_p1_input || self.has_p2_input + } } #[must_use] @@ -403,6 +420,7 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { has_sprite_cycle, has_oam, has_p2_input, + has_p1_input, } = opts; let mut out = Vec::new(); @@ -506,32 +524,40 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { out.extend(gen_ppu_update_apply()); } - // Read controller 1 - out.push(Instruction::new(LDA, AM::Immediate(0x01))); - out.push(Instruction::new(STA, AM::Absolute(JOY1))); - out.push(Instruction::new(LDA, AM::Immediate(0x00))); - out.push(Instruction::new(STA, AM::Absolute(JOY1))); + // Controller sampling. The strobe write to $4016 latches both + // controller ports on the same clock, so the 8-iteration shift + // loop that follows can read whichever of the two the program + // actually uses. Programs that touch no `button.*` at all skip + // the whole block. + if has_p1_input || has_p2_input { + // Strobe: write 1 then 0 to $4016 so both port latches + // capture the current button state. + out.push(Instruction::new(LDA, AM::Immediate(0x01))); + out.push(Instruction::new(STA, AM::Absolute(JOY1))); + out.push(Instruction::new(LDA, AM::Immediate(0x00))); + out.push(Instruction::new(STA, AM::Absolute(JOY1))); - // Read 8 button bits from controller 1 ($4016) into ZP_INPUT_P1 - // — and, when `has_p2_input` is set, 8 bits from controller 2 - // ($4017) into ZP_INPUT_P2 in the same loop. Single-player - // programs drop the three P2 instructions (LDA abs, LSR A, ROL - // zp) and shave ~6 bytes plus ~30 cycles/frame off the NMI. - out.push(Instruction::new(LDX, AM::Immediate(0x08))); - out.push(Instruction::new(NOP, AM::Label("__read_input".into()))); - out.push(Instruction::new(LDA, AM::Absolute(JOY1))); - out.push(Instruction::new(LSR, AM::Accumulator)); - out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P1))); - if has_p2_input { - out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 - out.push(Instruction::new(LSR, AM::Accumulator)); - out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + // 8 iterations of read-and-shift. Each active port costs + // three instructions (LDA abs, LSR A, ROL zp) inside the + // loop; inactive ports emit nothing. + out.push(Instruction::new(LDX, AM::Immediate(0x08))); + out.push(Instruction::new(NOP, AM::Label("__read_input".into()))); + if has_p1_input { + out.push(Instruction::new(LDA, AM::Absolute(JOY1))); + out.push(Instruction::new(LSR, AM::Accumulator)); + out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P1))); + } + if has_p2_input { + out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 + out.push(Instruction::new(LSR, AM::Accumulator)); + out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + } + out.push(Instruction::implied(DEX)); + out.push(Instruction::new( + BNE, + AM::LabelRelative("__read_input".into()), + )); } - out.push(Instruction::implied(DEX)); - out.push(Instruction::new( - BNE, - AM::LabelRelative("__read_input".into()), - )); // Debug frame-overrun check. The frame flag is "set on NMI, // cleared by wait_frame". If we see it set at the top of a diff --git a/src/runtime/tests.rs b/src/runtime/tests.rs index f34a260..70dcc63 100644 --- a/src/runtime/tests.rs +++ b/src/runtime/tests.rs @@ -109,13 +109,44 @@ fn nmi_skips_oam_dma_by_default() { } #[test] -fn nmi_reads_controller() { - let nmi = gen_nmi(NmiOptions::default()); +fn nmi_reads_controller_when_p1_requested() { + let nmi = gen_nmi(NmiOptions { + has_p1_input: true, + ..NmiOptions::default() + }); // Should write strobe to $4016 let has_strobe = nmi .iter() .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4016)); - assert!(has_strobe, "NMI should strobe controller"); + assert!(has_strobe, "NMI should strobe controller when P1 requested"); + let reads_joy1 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4016)); + assert!(reads_joy1, "NMI should read JOY1 when P1 requested"); +} + +#[test] +fn nmi_skips_strobe_when_no_input() { + // With both `has_p1_input` and `has_p2_input` unset, the NMI + // should emit neither the strobe write to $4016 nor any reads + // from the two controller ports — programs that never touch + // `button.*` pay zero cycles for input sampling. + let nmi = gen_nmi(NmiOptions::default()); + let has_strobe = nmi + .iter() + .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4016)); + assert!( + !has_strobe, + "NMI must not strobe controllers when no input is requested" + ); + let reads_joy1 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4016)); + let reads_joy2 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4017)); + assert!(!reads_joy1, "NMI must not read JOY1 without P1 input"); + assert!(!reads_joy2, "NMI must not read JOY2 without P2 input"); } #[test] diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index e9c3f96..7dd752c 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -752f9eee 132084 +a05d1284 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index 20f5682..3e986d6 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -fd72f4a8 132084 +ff926f72 132084 From e5bc325a7160c6d40cd7b67197da904295c2aa31 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:53:50 +0000 Subject: [PATCH 8/9] linker+runtime: code review cleanup of the seven gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tighten the OAM-DMA gate: the `has_oam` flag now ORs `__sprite_cycle_used` in addition to `__oam_used`. A hypothetical program that calls `cycle_sprites` without ever drawing would otherwise compile to an NMI that advances \$07EF each frame but never actually runs the DMA the cycling is meant to perturb. The stronger gate keeps the two markers semantically coupled (cycling presupposes DMA) and adds a test that verifies the DMA trigger is emitted for a cycle-only program. * Drop the unused `NmiOptions::any_input()` helper. The only consumer (`gen_nmi`) reads the two flags inline and I never wired up a second caller. * Fix the cycle-count claim in `NmiOptions::has_p2_input` / `has_p1_input` docstrings: LDA abs + LSR A + ROL zp is 11 cycles per port, ×8 loop iterations = ~88 cycles, not the "~30" I wrote in the original commit. Also notes the ~12-cycle strobe + scaffold overhead that disappears when both ports are unused. No behavioural change — all 622 Rust tests and 33 emulator goldens still pass unchanged. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- src/linker/mod.rs | 18 ++++++++++++------ src/linker/tests.rs | 22 ++++++++++++++++++++++ src/runtime/mod.rs | 20 +++++--------------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/linker/mod.rs b/src/linker/mod.rs index ad5ea76..5c9bbe2 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -396,13 +396,19 @@ impl Linker { let mut all_instructions = Vec::new(); - // `__oam_used` marker from IR codegen — set whenever user - // code contains at least one `draw` statement. Gates the - // `$FE` OAM shadow fill inside `gen_init`, the OAM DMA - // inside `gen_nmi`, and (cascaded below) the default palette - // / smiley / rendering-enable machinery. Programs that don't + // Does this program need the OAM DMA plumbing? True when + // either of two markers the IR codegen drops is present: + // - `__oam_used`: user code contains at least one `draw`. + // - `__sprite_cycle_used`: user code calls `cycle_sprites` + // (which rotates the DMA start offset each frame; it + // presupposes the DMA is running). + // Gates the `$FE` OAM shadow fill inside `gen_init`, the + // OAM DMA inside `gen_nmi`, and — cascaded via the + // `has_visual_output` check below — the default palette / + // smiley / rendering-enable machinery. Programs that don't // draw save ~520 cycles per NMI plus a handful of bytes. - let has_oam = has_label(user_code, "__oam_used"); + let has_oam = + has_label(user_code, "__oam_used") || has_label(user_code, "__sprite_cycle_used"); // RESET entry point all_instructions.push(Instruction::new(NOP, AM::Label("__reset".into()))); diff --git a/src/linker/tests.rs b/src/linker/tests.rs index 22614b6..66ce23d 100644 --- a/src/linker/tests.rs +++ b/src/linker/tests.rs @@ -97,6 +97,28 @@ fn link_rom_size_correct() { assert_eq!(rom_data.len(), 16 + 16384 + 8192); } +#[test] +fn sprite_cycle_marker_implies_oam_dma() { + // `cycle_sprites` rotates the OAM DMA start offset each frame; + // without the DMA itself the cycling is a no-op. The linker's + // `has_oam` gate therefore ORs `__sprite_cycle_used` with + // `__oam_used` so a program that only cycles still gets the + // DMA plumbing. The NMI of such a program writes the DMA + // trigger at `$4014`. + let linker = Linker::new(Mirroring::Horizontal); + let user_code = vec![ + Instruction::new(NOP, AM::Label("__sprite_cycle_used".into())), + Instruction::implied(NOP), + ]; + let rom = linker.link(&user_code); + // The OAM DMA write is `STA $4014` — `8D 14 40` in bytes. + let prg = &rom[16..16 + 16_384]; + assert!( + prg.windows(3).any(|w| w == [0x8D, 0x14, 0x40]), + "cycle_sprites should force the OAM DMA even without a draw" + ); +} + #[test] fn link_with_sprites_places_chr_data() { let linker = Linker::new(Mirroring::Horizontal); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 552ea0e..ebe2d57 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -388,29 +388,19 @@ pub struct NmiOptions { pub has_oam: bool, /// When false, drop the three instructions that shift `$4017` /// (JOY2) into `ZP_INPUT_P2` from the NMI's 8-iteration input - /// loop. Single-player programs save ~6 bytes of code and ~30 - /// cycles per frame (a `LDA abs`, an `LSR A`, and a `ROL zp` - /// running 8 times). + /// loop. Single-player programs save ~6 bytes of code and ~88 + /// cycles per frame (LDA abs + LSR A + ROL zp = 11 cycles × 8 + /// iterations). pub has_p2_input: bool, /// When false, drop the three instructions that shift `$4016` /// (JOY1) into `ZP_INPUT_P1`. If both `has_p1_input` and /// `has_p2_input` are false the whole strobe-and-loop block /// disappears — programs that never touch `button.*` pay - /// zero cycles for input sampling. + /// zero cycles for input sampling (~100 cycles: the 88-cycle + /// body plus the ~12-cycle strobe and loop scaffold). pub has_p1_input: bool, } -impl NmiOptions { - /// Whether the program reads any controller input — the - /// necessary condition for emitting the strobe write to - /// `$4016` and the 8-iteration shift loop. Skipped entirely - /// when both ports are unused. - #[must_use] - pub fn any_input(&self) -> bool { - self.has_p1_input || self.has_p2_input - } -} - #[must_use] pub fn gen_nmi(opts: NmiOptions) -> Vec { let NmiOptions { From c09f9c0caa0587d182b344fa55c0ae54f0343558 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 21:31:47 +0000 Subject: [PATCH 9/9] codegen: emit gate markers at end of generate() to protect peephole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the six gate-marker label emissions (__mul_used, __div_used, __oam_used, __default_sprite_used, __p1_input_used, __p2_input_used) out of the inline IR-op lowering paths and into a new `emit_trailing_markers()` helper that runs once at the end of `generate()`. The IR walk now just flips a bool per marker; the label emit happens after every instruction has been lowered, so the marker never lands in the middle of a peephole-sensitive sequence. Fixes a real peephole interaction that surfaced after rebasing on main's `codegen: skip parameter-spill prologue for leaf functions` + `peephole: drop dead LDA #imm before mem-INC/DEC + JMP` improvements: an inline `__oam_used:` label inside `IrOp::DrawSprite` split the dead-load-elimination block, leaving the `STA $130 / LDA $130` redundant store+load pair that main's peephole would otherwise have collapsed to a plain `LDA #imm`. The stale bytes shifted the NMI handler by a few bytes, which shifted `on frame` execution enough that `examples/palette_and_background.ne` captured phase 1 (WarmReds) at frame 180 instead of phase 2 (CoolBlues). Regenerates every example ROM against the new codegen (all gate behaviour is unchanged — the linker still sees the same markers, just at the tail of the user stream instead of interleaved) and updates the goldens that shifted: seven audio-hash drifts (all audio-bearing programs, same cycle-accurate-APU-timing story as every prior NMI layout change) and two pixel goldens — the one- pixel sprite-position drift in `comparisons.png` that we already tolerate, plus the phase-capture flip in `palette_and_background.png`. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN --- examples/arrays_and_functions.nes | Bin 24592 -> 24592 bytes 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/loop_break_continue.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/sha256.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 | 126 +++++++----------- tests/emulator/goldens/audio_demo.audio.hash | 2 +- tests/emulator/goldens/comparisons.png | Bin 1023 -> 1129 bytes .../goldens/friendly_assets.audio.hash | 2 +- .../goldens/noise_triangle_sfx.audio.hash | 2 +- .../goldens/palette_and_background.png | Bin 37252 -> 43928 bytes tests/emulator/goldens/platformer.audio.hash | 2 +- tests/emulator/goldens/pong.audio.hash | 2 +- .../goldens/sfx_pitch_envelope.audio.hash | 2 +- tests/emulator/goldens/war.audio.hash | 2 +- 43 files changed, 56 insertions(+), 84 deletions(-) diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index 1e2fe4977e8d69fc560682a2558f8d00677e9d14..4c008abe1842ff672b7f5f71572164eb701d7f77 100644 GIT binary patch delta 320 zcmZ9Gze)o^5XSd!Vk-4)E|(nKI|z~{;sfjxSjbg^Z(yMWE;s#Ynp}}aoMNqnG--sr zfW>vG1;Hj_A(BF{6Gv|=XQueR-^?&mI9^a)@WZRi;-iUi?o*e2F6_p1c z)Elgc>B0IJLx>u>*n$Y#(8CZEMxc{t2Yu|4=`|+Ce!W#lvtMECh2^Pb=?$~rGbY|B zR$rFEWXgvl2r#G>jmJw2?jDp3&p3=p;&`THgX)*R2NxAt%RFWhvZf6=?$G&N(3n9B zb7&`@4)$ukk>>sXPVRTI+YV+Wi-ct*7o`xGmr}fTdgiy zq_kpahDuD#OfZ4M3n1*$j>sq3_xZc?IV(w~AxQp-plR0|t#FSVCiay2^o^t9;dy|tCwM(#j1L>ICte5f`!Rb6nW z|6Pgzsslct%HM$yDnR9C=H-!T+Ed{^zce{+hvaM`BB%%z-FmN@zaUOm8r?rp99$5d znzKyrsPPP}-gf_?E8T?|T3GvQ3q09*d?o`(;ASo3(S@D6{XX3%h^^9WL6AwZ(weq#H0xjZi zWn0PE%H1lwRB#FBQo)%FOe+~?GBV9%Vp=LN`52=TKl UOm1KnV&y-~aKL9XzXPKJ0L}(rdjJ3c delta 271 zcmbPmfN=s4)vz+QGOav3v9ZkfW-HIiL#@0k|F?22Wz=-|AhuGWm&suzM=!I($|opt z0v|+Hu5M*t`FXMyqgp-dN(LW~gDV+<8Uo-NULk1^I$7}nq$n4pNN*)WtI$&64+1L< zdRZKR9QIc3R$-sH2O%c5^0l%8UBTbVwvrJfu~eY7X{lgq;}XuLjWZdTRx-?FWSYsu zw6tj^Gt)Cp5S+Z1QIhfTG?TWVzSQS9@=U3%VJlkVt)e@BI3JpaWCQ*v`v_OK|3naVa}|hju(O-st<9_c$=bI zcP8kN1)DcR8uG=f&KC(l93&7W2(d81Xrl{yB-q_)-HOAU&QKedzuHoT`~u delta 504 zcmYk1ze@u#6vtl@3SL1kbx|plQxH7s<|K%A5d78ANzg?^{{#oYyDM*SsI)lU=Fkio z3PN!cyjqoZP;?QT++38B(b3E0gdY419;WJzq&jlPcT5gWeGn7Dj_5^q*MeUqhFReg1ePeDc{FR>GFq6 zk2xvYHE4y-=5^?~c^=WyLA=XWoiGV?%G#7Y6PEnvL3ganIrL(Pfy|>Xf(_v#J$85x nYr>m6tf4RaElc*ZJNcQNBpFgxz3IHs;c{-f2N8Dc3UU4bH0i#( diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index e25c27511e2ef345b164d30a3d7b61a9ebfe9961..19f3d91a0bde6e50d5a26da4d8ae83069db5bb74 100644 GIT binary patch delta 132 zcmbPmfN=s4)i7#Lto2J`_PKcAnZQzklOHxP``$-uOdVJ0KfOeUtK a0-M7bH``4AaRBd zAn`X4amH4mrNSo}!9xEbdIgsXo_qk-D{v61SJ+4I;4zRmM6U)!oUv7SsX%MPQo+{7 oC7eqed1o>(tz?+V$TX9QX=%e|UB=CJtfdDT4$Ruj@4%=40J>5|sQ>@~ diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index fe283cbea1ffd59c15f3f2c7d3aa8a1d3210169a..4be191b6e65a5efeef17071911aa6fee3e6fa785 100644 GIT binary patch delta 245 zcmbPmfN=s4)i8QbtaVUUI>~r}*+=={NhTo0bI51Gfu$@bKYU=?!0dDOz)FUMr7Q`P zW0{mCQf4wUJ>!JHrHq;aAGlUBwlXUSP1a%5s~1@+Ad$(`Dgf2!cW~v0Rson0+Q3Tq-D$D%2|YOz7ktpt1=ESKetA1ez=i zR3rp6q2YrdP$?r&>2i=h;gu{vVS$rdfNIYI^`1-tG58KX6FDZjR1|FF*@K(KnLgW2 T7GQQ{%|66%VCiOl2Sx<|E2Uv0 delta 352 zcmX|*u}T9$5Qca5?$8T@dpUz4pag9U7B&_(N*>^R22;esH?UB0DHbdgB4TlcAmSE7 z6omKycUOoD7AeFkop5NkXkjPLp5pGnGXFo{{xe8nKy?7er-$v{nuf6xt!J9kvz0?*MvVZNXe!@7i1bD)^iW7$y z{_j=L5~hM6o#t{z0aUmm1wP6E&sJfv*HEIq$EEmB+{q}>MI!~V+$<=edAZ8IH7ZJM zywiAel+gl0g_;?==eaePR8Zfo%Aly~Nr?=tI)f7m0~r!fr9WdcPZ*Tg)0Ul>CMQIf sT%qqv@YTvK73gl+e%Q8A%stdp?Jv;FNtznHz~`oSle4rK&9?->J delta 161 zcmbPmfN=s4)i6GvSZi;(lJS{9Ytzb#*2X2AOB-i0Fs)>m$;dR5iD_xmOlGEMoDjHF z;AF!G)|CuC_YbUO{2({+u`(Bk$8!+Go6N-s;aWnthK$NQ>=Qup84!UepaACvkic|^ cKr>Xs5ioc2Dn=7KR`x>-2Xr>`J1{B$0JTgy`~Uy| diff --git a/examples/friendly_assets.nes b/examples/friendly_assets.nes index e609fb2d1c4d88c3d475057f646f6197267d0b08..459092053eab50db30966f2a172dd3521726ce81 100644 GIT binary patch delta 248 zcmbPmfN=s4)i5$ntW{uCoH$RF@!`ZHHH# z9$YF7rdF$eSzh&fMd^GtNqk_EN zp_BhVxcV?2TD6p2^FS*LkhJmgVrAhp=w)$OnK0RsNlPgH5Kxry1NTzaR=%Zdt^7W3 gHkUHpVqrYIIe>ka#Uu_PR>k8C2M%oJcVJWi00UxRXaE2J delta 228 zcmbPmfN=s4)i4TAtW{t%o;XjH@$QFTz!Nm3owZ>7EM-X(h@2>w3JgiyY_0r0|28Kx-C|+9y4j9> Smj$cwafSnzHuF0$DgXd2>{_A# diff --git a/examples/function_chain.nes b/examples/function_chain.nes index 24a10732438896814542cb5c5215b8bd35a6180d..2e5045e7444edcd89b735861f3c7cf610107f925 100644 GIT binary patch delta 188 zcmbPmfN=s4)i9<^tktRKJm}MPU@6PV6Cap1F#D`Iu<}I0QWhX;6<#XbDsoa_0<+Jd z11kkuMV5-RvM9_u0Fn}2Dtht-SpCBTD_;PG*8_z`p)^aY*itbrAm?Seku(ljzIIwavzXPKJ0ITXvTmS$7 delta 234 zcmbPmfN=s4)i6$&SgTWSaL}jgz*3f82IjR4%qLHL;M~BvlEG*5ft8FOm{*=i0174m zWf_^*GBWovF`pEe0Fu50l4e~g02Eov#N5iFFz*0J6*Kc%X6BPGpk{mrDdJf90w}%b zKmt$(!T_pcVP4C^%mw5=I^YA+$I85xmARLVSt3&aNCL&V4kj#OSov=*1Jes2xbS@? zLo4S>#%BURDanJq?98o=D+^kimT)d@n#sVll3^wz(@Z9&rHz|C7^m5>ZaBzr;Ob_6 H2Sx<|8e&*J diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index 6a128eb6fbb3f60adaa22035aa310672e5f58518..cd8fced63d946e0808d8b347bf7fee8c45bfa965 100644 GIT binary patch delta 52 zcmbPmfN=s4)iAP8tgR9fT*A3ja3%xON`{$?Of#97mI`d*IFtNKBP1BsjY H9T*h=%X1J; delta 54 zcmbPmfN=s4)iCl-tgVs|Xl+<3*xI;+b7|vD2Bwt^GZ~p?GBGV}*v!fJ+m2QDAj5(5 J&HN6G3IHZ^64n3! diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index fc097f7e4b5995cbe3c035c8def1fa18b3324d8c..da31d21373901e2a6984655d45c535b50131e15c 100644 GIT binary patch delta 73 zcmbPmfN=s4)v*3-Wm&HN6G3INmk7j6Im delta 84 zcmV-a0IUCyzyXlJ0g!wJ355cw!I6P37#K{Fz@-F@00XT61C0R#r4$MZg%qs;1ChHW q7lnbP5ru%H38jFU00OB1nE?Ws0s^IhvGWH%1u4M*z)rIdKmj1mN*omc diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index 308f9a94ce86835e605ac5fca02d22831aab4f2c..9399b71591b12b3645c3ec0b2c525ca0fd858e8c 100644 GIT binary patch delta 342 zcmbPmfN=s4)i5raSQ}x?w1L^@?15uqOBpoZ&)Q~;`A30MKxCT5@G2bYSTlmj~aI7q$d zQjwD%CV)84M3y2OdIMtULy)58rOhAYmI^Q7Tq-=1foUbfOh%@eOiW9KW->E9QhP;1tV!1QoC}sn_UgM=Q z-<4fi)s0hS6TW}f)?Cd?@SNnSkx;H_sjm{kYS66zhP(+eJ%pe#DXsoVl&Aw(t@yST ze^e1eZ1$joZ#Rh^B}6$Bn&n*NLO`6iL@nz;;w3TD)*LP{bC7L`-A$@ExA4@#dke1} Vd~V>&OpK4$eiy75zCL)P?SBKCm5=}c diff --git a/examples/loop_break_continue.nes b/examples/loop_break_continue.nes index b70c5dae6ef1d3ae11c68a5d35c1dc801a442e43..300079b10aa2e67942e72867651ec697740e3e8f 100644 GIT binary patch delta 330 zcmZvUze)o^5XN_BcF{nO~ zqHrKqwwAe4@&F?4ZX}h=z|8l3Grzpzx$jH)YQVbd^n5VTZxx9bIfqBoUw$3Vd=TLvI6H1|q3a3U09nfrG0L za$XhC%35s1PDv>i;_QY4L3Ux8H*bD#C$DLacVur1rk1z-YA{G1YiVU3NIUbP^5Dsb z)}7jlBww|nt%K6hhY#nKkFN&>(8HT7fGU8-GPbh?+8}vepfs5iXh^H4FSYC_`WZkn z?0Oc6v5bTz$HMNt)LE!0leq(e%XO4_Cr40)(0I9~RGP9?MXP$Q>wQmAv4OK<6Ju8q zikNh2(%-aiPfHk@DpK@WT)a}}R72wj)1;=!6~6d|^xl9Y z44BEx^o$b%mojP!d=OeH3{=dzlELTyft8G$3_ctOS2DCVE^Yk4v68V>V5LZ_uuu0P zpoxq?6OT^zVoc(1JqT9V2U6Iyv}y8gMoHDR5V36t&CJM}nSh#24^95h=&dSy2&`M} z5JWe+HOxSj?T03pGU-a^LsV9PR5mPa0IFnwnYno{ldtV$17>x$b%z)Zyg0O2z(D~3 DUtnoz delta 281 zcmbPmfN=s4)i7?ISQ}u*xq)>hgU|5;D;Ym99TQ$EaFXEzNZm$;dR5iD_xm#E(+Cf(KVJax(Zx9bC!K+OV|Y1IJ3nR)Liwt-?N=4gt+! z1e;?9RCIya=fPxs#w7j)5FIO_Iwmh*l+-=}5xWQyYg*a_G=&-26eh4KZ-I(Fq))!b z=&fyc2yCnSA-H-JTbaS?lYxppcuw|X(v_YDQ9mD{o&je5=4nj6wrp1qF&yAOyjZ|N F0RU!uazy|D diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index 2129f9cfe05b2cdea9480d3daa2f3a5e7eb9913a..8cf1b751ea0ab93862d5fd0d1d8df05ad7ff6c1a 100644 GIT binary patch delta 135 zcmbPmfN=s4)iBPSSnFfQ>~sFWGl8W7Cr^A}_BnH4C1b0Q&%Fc3Kq3l2k$VSLGPDXU z6_CheXl-692%=h6GA`j<+A@=YX(i+2dPXr0pd?3Y%TmF~j*My`4p`;ndCK=Y92N@3B+syC4r~m*|+c7o( delta 183 zcmbPmfN=s4)i7?ISnE^Ix{|@?;DMElA0(a$EEPC;;sZ$F5=ek$C1b0Q&xZrYK;jBu z@mCOWhE}1a01E14$iFv@cP#Xx2>P7Y>N1989#Ha9Zn+Ob|e$Z+7-W_|}o F1pr@yNL2s; diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index 94ac5873cfa019ccf803e62fca9c915dc065b234..56697f221a54d5b89717434fbe31fd7ad9dcecd6 100644 GIT binary patch delta 241 zcmbPmfO*0J<_+^27?UT@Z_sn+Ke&|P!=IH5tqe<y544Id6_7|3Z53F`sLAv}XsN(SrVmU&rSS)s3Rt{k0*g0%5Cj^?05q`d;7Z0; z;gzhdLJF@B0M$MNtL+1cGApc~9MPmY`Aeg%B24)|6y=PX3Lm(DK44bh-yG4j$#$ZG RHEZ)Bh68&y^B-VT006N_UwHrk delta 317 zcmZ9Gu}i~16vprFHPm)7NsDGF+$QZ%M_t?n^GApvxcCP+nTxzJPzVlL>f#%2D71Fh zOD3~9J4?as+(aC_1VM0m@O|(1ec#N$%)oM<@OgfhOwT4IG5f6yW^uRF+{#TzL3AEsP(Lplb=0W+e5j_^ zJ6d3ehUm#xlL;U{x)gvu@v*QG`L(dU7P)GWF8PP=qIDCy>q=N#a@%-Yn&Uuw7|Ji> zxXmxOYO90-x za3v!rgHOuAl?<&-OPfCMtz>LvR%n^r!C0>b(()Bk3&$a_7QsUhEt8Fz%zY{k`lKHM zqLYjl1efqU<2k@^flWaTBmzVkD>7DPH~>}iEM-2)aDi{7e=oyU72 hB$3J6m`oXSH@{($vz_d~EWmo|5W@k(&HN6G3IP3KSC9Yz delta 262 zcmbPmfN=s4&0*X%ajq-l)QOMvL|YqMo0f1cZJNozw31;aBhySKrlpOO&ok8I6a3$jhrei`&88rny2rU&l$?yRroC*}?Wbny5xRRl@VQIq$zLkuv%nE&z>lo`z zAsQJEVbv%B6b5QkJOtOs0MvN>z~p*H(aDZXl0MA`eaa63(MiS&f=hUw@f={dz@}gZ z5&@!&6&b5C9DrJRmNK7YxWKp4zn9@aFC!2=@L2=W!nblhlE~y`Os0&ro9{5m*|Od` N#BjiIGrt3)0s!+KX59b) diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 88d7ade3ccfd1e2143bc8fa8dfbff713b0ce9087..547ab01d145bd8854d8cf81b6e2f4d11e1268eeb 100644 GIT binary patch delta 55 zcmbPmfN=s4)i7pGtlc2W$h?GeEhF~rS8O2$?ppL++6fkg6vBKHog zWM~yyDtJ;3$l*FTnV(Tq{CMvN_LYZvKeDg<-}{LjC}#tdvpP80kx^3dLhomg+!v7C zSN4?^OE_0n%w%9%$uN_VX(kiXQh~{hj7p4Elb0|m$R9g+^8W`{pP2_&EoIj{(8>ZN zZM?i#SvU=PSsYgSPkzj(CG`CuP?Yfl_fpnYzNKuf{5~y{<(YICRVMoYNx#XJOh*`_ ZH`_6vv6)=Jtit;D5W@l0&HN6G3IK8?&&0G=nlvDYf>s^;LBXy=crjoh za6c^4C*!B3PyU>6Up4Zzl70+rVOuORv_zd@MYs%IvBdCA6b_EWyMr_F;pieLi~UR& z+emmr`$2_aAQntk`_HE0(_~YVeL2eG5nk~4wyNTUP$UV^gi;mB6WD|s89{S>lI64F zFuc$1l(U<1OMlSq+E%qKEQaUu0rvdWkya>b%I0-7&)sk!zo7*kxkFoUCXeYO^oA>Q P&w;%$Ch5`8WzhTu1KN7j diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index 4665e0d09f1324ce7aa3f29138b267097d2eda30..4e0d54854d8cea87e1ffeaa9d340271699f53897 100644 GIT binary patch delta 283 zcmbPmfN=s4)v$hOWm!3QDM(Mz z!Ic6)rH2S8WdbUldoUe~vB#jso`f6A40Z|@W6_mLNMzcyHm7!?59t#ID} delta 327 zcmbPmfN=s4)v(I9GOau`u~CLGeℜFz?vJ6E5|vD;a!P4z6VUAi0vERpOcCQpuAn zA3%bVAVC(OpyX1ClZ;>?9iY$!W}mMIfr=QwB6c7}N-O1Cg;pMF6<#XH$=E8mGQL#= z$P)#Mimg1>D$dE&DhSj7F(MzL!5*fe9*+(tuoVk%S#b<%#VNQI%wP={(X4>mfUZG8 xBGX2^wP9sNYvU5mrHwNgm{u~(WMrDj#I&?w^C!lawyg1|7!Ew!%_8iM^^{}0Zf)%}aL zT|=`aWdtw55Qbvfs55@MqzQ@xWld2mFY%Rv>eM7){L7;JjC?%xo!EpIY=%|Qno)6W z1oRH$M~ceT48b7;BTtxc`8P#mb%9jlddR|;uD;XUOT3Bh zWC?5$4=-qVuro);z{#;O*pLa3W-cKW+V#TxTdPy+`GS$b!)|E1A!6{OXcrdLD z+$B;l9EyIJ4aI;;YKnaI@|#v5=+j36#tUVyqu5BQRHW5I4M-#D?J25$J&v)glo4d| z78v{q2GL^h|I3&|R%L@^*aECsCnpu~qI{>qvK(JeSPeuZ!xaQglA%gO@DiUy0462@VM>K0^D{x&5f`vUF40~?1t~Fit$*--R&sAz;@AD#!u1@6o zpaK^}8ftaQpx^uQoYTAgS6{CN`*_a3rDExVF!4V+ zF=3f4J9Fk3JjIvn%X?1$)@2`0NYJJ%(XMQ3!_7m(3+LM%VlUn| M3J3qV+A-$624i)zVE_OC delta 1398 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 literal 24592 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 & zQLxY-!jwFqhzrYi@X20P|l3GoEa^2Y1XCL=Il&|*uSO=u*FSkwmAnu z$riYjiT)FF#EX0jRv~Y9tZuY3p2|xk^aeSVcOKTCI#YRdWNpMWiR6tRLj#e+!DB+m zq4F)k9$53?I^eA%!Z@bd!DGVhK5+P=$>CrMj@cLvhw+e{WQF5f)gB@B-E$7~Xr-+9!tqg74=97Mwb3Ac%*9YdwHf;E3-iCZ^`Ror3&tV8-JG|WI2)WHsRxgMny+X;$@3x z;u}1_!}B9S1I{>rF}kqO;cs072DoHSet6c+G#i|(-_J44)4uwWw)435n-k_tCl`Of z^r)#}Ez9tpB+Dw%Hwr~r6X#i=PJ20{UCV0c&*v>@p_AIqlTuQa$On+bzV(gfW#FYn zAToVauzE+aZoE@vN(FTRs9aNe6S9u9LB8z9@(KbQq=2tjz|@=-Jc zzrfeIZXZTh82j+9khGheifwi)wuvjYnew%S3TWbX-{;nm*;go$&mof+y@c$LiBd9F zlqg{Y#W(y22VV3+91Q(-vuJBL7;YRh2aop)92SpZB}LLuFvGcJ_)#O0jfme&U@4p! z7{jk8mm*pJr78B+QXFSjN_9U*^5>5n;dH)yr8G;Iua=snt^V8>Pn2aJhmrEED*9mJ zm9fltZf)Yg*f97n;+!jO?O4CQBoGo^(kFjPc@mGy;qT(-lwkZ6|ELnbR1S|(wxgHH zaYDHWRh(sJiC%67^=QHvE9$h#@#y4#zl=T+y&-Csf1>Z; zTj~WhqaIfOQMJ^+Q@^QxP5ooFSN)>8TmAAE9#fwGmw!@P=C>)&|J!p1mH5WwZsi01 zTvO7!lN;l0$_t18^|^mOsPMQ&QO9Mg;SOA_;^%V-WK&$Hb#TfZi%X~mT24Zmi!#N&L=yc>NNeGCUt6S#&GBp-sg>} zakDbF4bat0_!$e}R=Gc4nU&bOTbZr7zj7T2Kwzp6$YrmXQ)4@F`c$)ehoV=X&i)5@ z(*!S$-@X*R60hacaOUC90o9%sU*GJrr{k}BI{Ql$i_)!Ax9;!$Zuf9^JU;GyJOn%h zJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%hJOn%h zJOn%hJOn%hJOn%hJOm^HB|lbP9R6{~YO~MWzq+HNm#MuSN=GWyn@Z*X{{fV}slBPx z-okuEDw#~JD9rctq*6V53iICN-ej_O(mYw1uSo7nr8)|8;jYMEM+~1Let9q$oFL!B z9t-+O{uKF3^&12)g_Y?_`7<9*n@aW}uV+DCO zKOtV8PUIuPB6%#hqj(PKVtc*8NUW`BeUsylwQre#S4S!snJ6EbfM2A3eot?t;2--n zf!~qDii_|U{@V)j9VtXm#K&w!urQZ-G(PSsxYagsjy!We9liJzri#|q-rlAu?l*(W zA6I<**vQDpPmhf&tGl|owyjpikNtGyjgb+|w{>mp>cafO8y7BIzz9KJ2bwnXJ1{B$08{N83;+NC delta 102 zcmbPmfN=s4&0)MXaqc1sjn;;h60MC(IF~lgWMEp!Fq4sKCKJ=rhRK19QL42EeO7`{ z#)^zp84fEMT6vZ-pJceex3avK;Xp4V5IyiYGx-9eDdYOhY)t=bS$`d5IMA_~-+@s9 E0E`wY2LJ#7 diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index bf39279590d6bef2daf053ba3201d36ddcb11d8e..3b55f5e2df1a2d46813aa29692dd60c4956d0cb1 100644 GIT binary patch delta 129 zcmbPmfN=s4b;vOLEI6=|p;cg|LobuVN{(J;hm|*4c~%~5vF=_w+ delta 114 zcmV-&0FD2UzyXlJ0g#9y1*rf`fWWB%@Fb}Kg%GJgjRHWa2#o_ksosSQslkN}sqc}b zBLV1<$Uq1*!O8#dMN9{i5CIkeW|JoYDiUSEr3Wbi@C&5{g$|_#g%3>flVbt10jaYW U0_{Bp0Kx#k9>TK`K>r^B06}Ogy#N3J diff --git a/examples/sha256.nes b/examples/sha256.nes index b3e856981634e6a2499b281dab96bdf221949cad..c5fa92e041c339521df38affdaca7adacaa4e6f6 100644 GIT binary patch delta 53 zcmbPmfN=s4)i7#KtW{w2oH$RFk#pmbQjyJtqAOXLPhI%FSy}42>f`_^J2uao3x%!@eOm?p4GEM-n*X=P?(Id$Q~ zQsxAh6dQ{g^HOGur%V%olFxvW@0PM8Y_=3#$-=o-+`&tYka3E(hzXPKJ07vW&n*aa+ delta 57 zcmbPmfN=s4)iCNzthJL6Xl+=T(%QI$b7|vD2Bwt^GZ~p?GBGV}nB2%HyYZEjJ*&?l Mh66>L`5hP)0B=VWYXATM diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes index c0ca2ecdcde0ccee3e98093f10aed4ec19fb39d9..3bf47baa0ca83e4daaf1c557b73a78e05bffac88 100644 GIT binary patch delta 62 zcmbPmfN=s4)iAnEtnHFvU|z}4%gDS`a0%y9!I=z9D;Z}pGOc8o$;7l&V6!IUL%Ycr S7};3U4l*2Au$kY1Q2_vjJQMK% delta 64 zcmbPmfN=s4)iC-@tnE@_VqVG6%gnq~ptW(SU~AJ7&ZSK=8JJcw&SYd-$uN_NX=&qT UWyXhgtT_i64lLWu@4%=40OpPsK>z>% diff --git a/examples/state_machine.nes b/examples/state_machine.nes index 405149ef9a5b716dd5ab8c8faa3d85015e08735e..bc2ec804e6c30ebae434167d4edad46d5f912f6b 100644 GIT binary patch delta 353 zcmbPmfN=s4)i7F3tkpFXImvi|*~jeQNhTom;Gj?Mft3siix^h^o6ErT0thaAU&+wQ zxwKItlW}Fh63&$YlLZ(hxKGMWVD{lTxH6@+VKNt^oWjzElQtiiHZc2$A6&|)De!@7 zC1WeIf)>a)u!)ls8KvuiVvRsq#t*C@xU>nV0pbY8lUu+xtvI+;Kq8Z?RRAc@tZ*0Xa)s6gpot2tO+W`Q07ch;Y+u5; zv}q;-(@Fz|nT$*`nV6O~%w%SI#tDJ={5x5PNlyM`1=u$ALkI(reYm-ZX@>3O4@~;3 PQHK}~Oxeuuz^DKKS*nWu delta 414 zcmZvYF-yZh6vywqXC)T`#>qkf|^|S7Rt4(yt+T;Gp wB=gr?>Q+T){r624jW^NhrWTb~4fd>H>{@;1{TZL0;=@tvz7W~_bn6)0U!r`MQ~&?~ diff --git a/examples/structs_enums_for.nes b/examples/structs_enums_for.nes index e5afb78de3ca0a642e96463130b43f1cbe68660d..85cad6b9363e7eecdd7930b8be98bb4aeba7969b 100644 GIT binary patch delta 121 zcmbPmfN=s4)iBmitX&_@$h?GeEhFBa)V9f<6wLr!bb;O%`AhWSxJI;lRz!{0@u? E0OPMInE(I) delta 157 zcmbPmfN=s4)i6$&Si8QSfw{F|EhBSl;}XuLjWZdTRx-?FWSYsuw6tL+Gt)Cp2wcg~ z%gwx&oB1Tm1lE-dJ|PEJGJenp@>lI;2Wnp`0MsjhQ!nE?Ze}01gG(7c{8`D+%HVVG h06&9(0zaFW8b7Dx=2?vKcB~r?G8}ldncsm?0RVPVHueAj diff --git a/examples/two_player.nes b/examples/two_player.nes index 167dbd6794a29093dd21f938aa2a3f399f8aaadc..ce96cd0e9975f313635798f916a2e894df9b0a1b 100644 GIT binary patch delta 128 zcmbPmfN=s4)i7S2SX(B}?DPJ>QbtV&PKMT|rA;5$S2DJW`20ICnTJtRk=aN8;8G5- zT=UXqu$-um-NBU%t)fc>mvAl>oV<=va&kGN5(~4>vV)TsGivZN`&>G>RM_GrM{6@s bLG$JZj9PY+3z)=MzaC^bV7i&#fl&bfF1;^2 delta 155 zcmbPmfN=s4)iA!ESX(B^x{}f7)W_-IN`_X^r2?(ZO9fk-mT)d@n#sVll3^wz(@Z9&rOlHQ8TGhWS2Fla sJh+nagYx9bj2faK?qM)@sj$UM4v4PJrx>;DSh)`|9Pr-E@4%=408w{0;{X5v diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes index a41e9c90f7c8503f5e6f8552a9e6c0ae39953104..50a6a2c40b1f215f35e63aea7bc1659974c37779 100644 GIT binary patch delta 140 zcmbPmfOWzF)`lsJ4UFnj8yWr5m^Lu`oIJ3Sp;c%pqbB1Are^{`n&|`YQh}2|9#Hbh zfny*c7NC?MkmdkN2|}g*A2=qsRB#FBQo)%FOe+~?GBV9%Vp=LNlbPumCj|OTJJ2ey dYOeZrUq;5gb`upOSltgX9BACm@4(2Y002k|GLHZN delta 162 zcmbPmfOWzF)`lsJ4UG0v8yWrTIXAGbWbiq8U?t-RmX!>xLQ5Gn89y*R69CdoAB2|* zoMij}QSt_)gy|Saf(58R5J+25=xt-sEkx>BvcBVX+ diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index cdce1c9da647d28a8ed90bbea56fcd5d734ccf97..92e9f64302421dbb50f8829c77f2ea28b96aa592 100644 GIT binary patch delta 175 zcmbPmfO!HCi3&6O2sEr@d?x540;G-!D!f0Cu+i|o{X_@O$pQ|nV$43gK=D?grGh5| zJ}~=8HLPT46@sc@n=H^!!1}+HY30Gmtql$y_YbTrSi-roU?v08N`{$?Of#97mI}^f zW_rd6fj*NCvR~4e#4e=5^puf~#Oz$=E8i zRPbcL2au2gNE6FShE^e<(wp{^1sV!iSzDP_9-7?R;NbW4z*521hLr`ajY~L}HqK;V zTFEezk!dCq)6#~S%uLTXA<$>ifmVT4b6F22EEVur$)IrWfC58929N^t8R7iRyp7-O QSXB=(9PrxAe}GW|0Oue{asU7T diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 62b038c6c24055af3881f80b362100c9c42f6f16..231c26934e329711dc8c7fc70878548ce5f12e04 100644 GIT binary patch delta 140 zcmbPmfN25{iK;RCNH;taTq<~S+6QJIiH4PotwKIp4aYztc|Z}ZhLsGhLJ1oK_uEfQ z5SuLEP{8-E0%DsteDBbw31;aBhySKrlo>2nVFt(LZHv21FZt9=Dt6W muvEZfC4<6^0}2cc845QxGdr>ajbdZvKg4jrWHWyQqXGc;qBTAM delta 158 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 Rz-7Q?z-7Q?;17d={{sS&e`Ejv literal 24592 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 DorHg2 diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 6b982fc..4c046d6 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -882,6 +882,15 @@ impl<'a> IrCodeGen<'a> { self.emit(JMP, AM::Label("__debug_halt".to_string())); } + // Pay-as-you-go marker labels for flags that got set during + // the IR walk above. Emitting them here (rather than at the + // call sites) keeps the markers out of peephole-sensitive + // instruction sequences — a label inside `IrOp::DrawSprite` + // or `IrOp::ReadInput`, for instance, would otherwise split + // the dead-load-elimination block and leave stale ZP + // stores in the stream. + self.emit_trailing_markers(); + // Snapshot the fixed-bank instruction stream before we // start emitting the banked function bodies into their own // streams. Programs without any banked functions skip the @@ -1229,10 +1238,13 @@ impl<'a> IrCodeGen<'a> { } IrOp::Mul(d, a, b) => { // Software multiply: multiplicand in A, multiplier in $02. - // Drop the `__mul_used` marker so the linker knows to + // Flag the `__mul_used` marker so the linker knows to // link the `__multiply` subroutine in — programs that - // don't multiply skip ~50 bytes of runtime. - self.emit_mul_marker(); + // don't multiply skip ~50 bytes of runtime. The label + // itself is emitted at the end of generate() so it + // doesn't disturb peephole-sensitive instruction + // sequences here. + self.mul_used = true; self.load_temp(*a); self.emit(PHA, AM::Implied); // Save for __multiply contract let b_addr = self.temp_addr(*b); @@ -1279,10 +1291,11 @@ impl<'a> IrCodeGen<'a> { IrOp::Div(d, a, b) => { // Software divide: dividend in A, divisor in $02. // `__divide` returns quotient in A and leaves - // remainder in ZP $03. Emit the `__div_used` marker + // remainder in ZP $03. Flag the `__div_used` marker // so the linker links the `__divide` subroutine in; - // programs that don't divide skip ~70 bytes. - self.emit_div_marker(); + // programs that don't divide skip ~70 bytes. The + // label is emitted at the end of generate(). + self.div_used = true; self.load_temp(*a); self.emit(PHA, AM::Implied); let b_addr = self.temp_addr(*b); @@ -1297,7 +1310,7 @@ impl<'a> IrCodeGen<'a> { // of ZP $03 afterwards. Same `__div_used` marker as // `IrOp::Div` — modulo doesn't have a separate // runtime routine. - self.emit_div_marker(); + self.div_used = true; self.load_temp(*a); self.emit(PHA, AM::Implied); let b_addr = self.temp_addr(*b); @@ -1421,14 +1434,16 @@ impl<'a> IrCodeGen<'a> { y, frame, } => { - // Drop the `__oam_used` marker so the linker knows - // to emit the OAM DMA + shadow-init + default-sprite + // Flag the `__oam_used` marker so the linker knows + // to emit the OAM DMA + shadow-init + default-palette // 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(); + // program has no other visual output. The label is + // emitted at the end of generate() so it doesn't + // split a peephole block here. + self.oam_used = true; // 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 @@ -1476,12 +1491,12 @@ impl<'a> IrCodeGen<'a> { // the marker off and tile 0 becomes user-available // as a blank (all-$00) background tile. if let Some(f) = frame { - self.emit_default_sprite_marker(); + self.default_sprite_used = true; self.load_temp(*f); } else if let Some(&tile) = self.sprite_tiles.get(sprite_name) { self.emit(LDA, AM::Immediate(tile)); } else { - self.emit_default_sprite_marker(); + self.default_sprite_used = true; self.emit(LDA, AM::Immediate(0)); } self.emit(STA, AM::AbsoluteY(0x0201)); @@ -1504,15 +1519,16 @@ impl<'a> IrCodeGen<'a> { self.emit(INC, AM::ZeroPage(ZP_OAM_CURSOR)); } IrOp::ReadInput(dest, player) => { - // Drop the per-player input marker so the linker + // Flag the per-player input marker so the linker // can decide whether to keep that port's shift // block inside NMI. IR uses `player_index` 0 = P1 // and 1 = P2; the ZP bytes the NMI populates match // the constants at the top of `runtime/mod.rs`. + // Labels are emitted at the end of generate(). if *player == 1 { - self.emit_p2_input_marker(); + self.p2_input_used = true; } else { - self.emit_p1_input_marker(); + self.p1_input_used = true; } // $01 = P1 input byte, $08 = P2 input byte let addr = if *player == 1 { 0x08 } else { 0x01 }; @@ -2112,78 +2128,34 @@ impl<'a> IrCodeGen<'a> { } } - /// Emit the `__mul_used` marker label at most once per program. - /// The linker uses this to decide whether to link in the - /// `gen_multiply` subroutine. Programs that never multiply (or - /// that only multiply by constant powers of two — those are - /// strength-reduced to shifts) skip the runtime entirely. - fn emit_mul_marker(&mut self) { - if !self.mul_used { + /// Emit the marker labels for flags that got set somewhere + /// during `generate()`'s IR walk. Called once, at the end of + /// `generate()`, so the labels land after every instruction + /// that could have set a flag — never splitting a + /// peephole-sensitive block at the flag's call site. + /// + /// Each label is a zero-byte pseudo-op. The linker looks them + /// up by name via `has_label` and turns on the gated runtime + /// feature. Programs that never set a given flag emit nothing + /// for it — no label, no lookup hit, no gated code. + fn emit_trailing_markers(&mut self) { + if self.mul_used { self.emit_label("__mul_used"); - self.mul_used = true; } - } - - /// Emit the `__div_used` marker label at most once per program. - /// The linker uses this to decide whether to link in the - /// `gen_divide` subroutine. Both `IrOp::Div` and `IrOp::Mod` - /// trigger this marker because modulo reuses the divide - /// routine. Programs whose divide/modulo operations all use - /// constant power-of-two divisors have already been rewritten - /// to shifts / masks by the optimizer and never reach here. - fn emit_div_marker(&mut self) { - if !self.div_used { + if self.div_used { self.emit_label("__div_used"); - self.div_used = true; } - } - - /// Emit the `__oam_used` marker label at most once per program. - /// Triggered by `IrOp::DrawSprite`. The linker drops the OAM - /// DMA + shadow-init + default-palette paths when this marker - /// is absent, shaving cycles out of every NMI and bytes out of - /// PRG for programs that don't draw sprites. - fn emit_oam_marker(&mut self) { - if !self.oam_used { + if self.oam_used { self.emit_label("__oam_used"); - self.oam_used = true; } - } - - /// Emit the `__default_sprite_used` marker label at most once - /// per program. Triggered by `IrOp::DrawSprite` when the sprite - /// name doesn't resolve to a user declaration (fall-back to - /// tile 0) or when a runtime `frame:` override could land on - /// tile 0. The linker uses this to decide whether to copy the - /// built-in smiley into CHR tile 0; programs whose draws all - /// resolve to declared sprites with static frames skip the - /// emit and get tile 0 back as a blank background tile. - fn emit_default_sprite_marker(&mut self) { - if !self.default_sprite_used { + if self.default_sprite_used { self.emit_label("__default_sprite_used"); - self.default_sprite_used = true; } - } - - /// Emit the `__p1_input_used` marker label at most once per - /// program. Triggered by `IrOp::ReadInput` with `player == 0`. - /// Programs that never read controller 1 skip the three NMI - /// instructions that shift `$4016` into `ZP_INPUT_P1`. - fn emit_p1_input_marker(&mut self) { - if !self.p1_input_used { + if self.p1_input_used { self.emit_label("__p1_input_used"); - self.p1_input_used = true; } - } - - /// Emit the `__p2_input_used` marker label at most once per - /// program. Triggered by `IrOp::ReadInput` with `player == 1`. - /// Single-player programs skip the three NMI instructions that - /// shift `$4017` into `ZP_INPUT_P2`. - fn emit_p2_input_marker(&mut self) { - if !self.p2_input_used { + if self.p2_input_used { self.emit_label("__p2_input_used"); - self.p2_input_used = true; } } diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index e2a54aa..8c49484 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -b52a0b1e 132084 +23afaaa6 132084 diff --git a/tests/emulator/goldens/comparisons.png b/tests/emulator/goldens/comparisons.png index e0dd216d6e127fa957af49cba891032d6426b263..9096daf8d1277685f1cc73f1c0a1433863bc755b 100644 GIT binary patch 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 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 diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index 5d7538e..6f5573b 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -9387feb3 132084 +bc94ed30 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index 7dd752c..8b0ffae 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -a05d1284 132084 +52d1836f 132084 diff --git a/tests/emulator/goldens/palette_and_background.png b/tests/emulator/goldens/palette_and_background.png index 918aab90bed2f01386042aab2f1efd0bc2999cd4..1e82376ebe2fa745039f0b5c8d6cd6c0183c5bc9 100644 GIT binary patch literal 43928 zcmeHQ4NOy46fT(&r($)~ACRmD6K69(?bJaQ>^4B^L<_hC9f5Wz8XU+7EMdC5qGoec zU?bbC4avGewG!QGVj;>sVQU#$4-n!>u|H0`A{joD^4-j}w> z*PTduEXuPlk#tJCzHhYl$9C+r_wA_A+zQ@i8JTJD zq-t`w7W6)`>}FwXM>m4eRoO9l_FKGsVuaL!@eHSy@W-MSv#mr?w%pp!*@xuSz_mI0 ziha#=hv{(uFz(ht?G-f8=h3wV*Z`xT*eRv&3Q>C7gwqI%!+Vo5%+PM9{7oiJ2Fr^`AE=Ud2z;^?qtu~fGBP|;;m2=ViXjYSxGIr zifg^N0>ZJN_q7YtGzvo{*Mg^+cBSx{9RxTHu^&FT zOI+;}LL-6cP6;#;g6UI0BLNx-Q_ysPM#9+c71NCbQlFf7aZz#C;nfE>cy!%oW}W<^ zF)KRbhxwhSKd%X&BRzflPSR`H75@IAyt}Xt9b7n$P17c!gZqiqsN?pf$bVH_g*3jJFJmFI_GH=HqaAF6T01<-dWdI^f zry(Xl1YXXZ0U`h*aHbIrhyaK%1$+WH!q|CeKmfTy zhrM@(su5I;W|nGgTwxjz0T3ZHh%ow9a^l9t6*-OTea)A2-W6^t)@;ISo$MDddB&tf}X=UONo?OMZ2&GWyM+Fs~n zV(B-oQV<{VVUo3!9MG#NuYN<-bs@;z2x--IB?zDyH~IiX2m){b5vGxQ1|R}I%}xLj z*s*>BL;ys9X-D7){IkCTL;ytKO#KKr0&s*Wuu_7m(O7>+Km;Bl!XinXyv!lhC%A`$ z3%P2~DY>g!WMVvEGhXcjX`VfD5-zPR)Q~o`QZ{!upqK%tow@52|0Q+5sfQ^!Sp7^}*U!#oj z-h@3;DdC}yi&ys5y_EH6zw?czte4kDq&2NS`F2#zws}#)g?lnuEai2q2)2<&2~LOX zbfmirk)d|aBwYMJ6`k11rr;XpTVcL66zQEE^L9`?fZ_oZ51@Ep!PY4hoPrVm4bZ@W z29A9LCr~_q;sF#7;G7hadfvEI3zIPp2aW(70XPD11mFnZr59K}2#5fP0EhsH0EhsH z0EmG1M~28+GNU)3?Q))zdxr=ilmv(KP2FrcW-Z1gZT?PHrR0Gex zD0#8&ZMoWW4{4%dYT((_z*1Q5!@_c(H_-qohj8FI904M*yL6fWM94+kVgM0rW$&rOzuEMt}%cJ6Xqen5&X9_M!SjSQ^i&Q5dabD8?7e)oAPjP~Zrxpco(mAi^{9KA9$HDGx&0n}V?pn07p7?xhDxplPc!XG z;WIn96ybtiCYAygB+u=h^^t5A>wy(T4n4yD2ZMPR(254oGAZE~fE;$nb& zAhaOrv~M)<-N;Dah(nKt+tFWMpq!3glNv4m!qm#wZcRUP>UkIxzj literal 37252 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 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index 7755f4b..e2b0778 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -83c0eee4 132084 +ea23d9c4 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index 1145110..02f3dfa 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -b7d5b53f 132084 +555a22e1 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index 3e986d6..9f5aba3 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -ff926f72 132084 +8142ffb7 132084 diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index 35b351f..9345746 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -51b560a7 132084 +e3805766 132084