From 37974611ae6d229dd0c679be2d72f27b5d018c37 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 13:07:45 +0000 Subject: [PATCH] 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