diff --git a/docs/platformer.gif b/docs/platformer.gif index 80e19ed..4b982f7 100644 Binary files a/docs/platformer.gif and b/docs/platformer.gif differ diff --git a/docs/pong.gif b/docs/pong.gif index 652cd7e..5ec8fe0 100644 Binary files a/docs/pong.gif and b/docs/pong.gif differ diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index e6d5d1e..0db9cab 100644 Binary files a/examples/arrays_and_functions.nes and b/examples/arrays_and_functions.nes differ diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes index b72ed10..a8dee48 100644 Binary files a/examples/audio_demo.nes and b/examples/audio_demo.nes differ diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index 6e75b08..00b2609 100644 Binary files a/examples/bitwise_ops.nes and b/examples/bitwise_ops.nes differ diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index 0c92f57..9959992 100644 Binary files a/examples/bouncing_ball.nes and b/examples/bouncing_ball.nes differ diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 3e72b7f..8fcf88a 100644 Binary files a/examples/coin_cavern.nes and b/examples/coin_cavern.nes differ diff --git a/examples/comparisons.nes b/examples/comparisons.nes index c09cb40..84044f6 100644 Binary files a/examples/comparisons.nes and b/examples/comparisons.nes differ diff --git a/examples/friendly_assets.nes b/examples/friendly_assets.nes index 33b199c..803da81 100644 Binary files a/examples/friendly_assets.nes and b/examples/friendly_assets.nes differ diff --git a/examples/function_chain.nes b/examples/function_chain.nes index 830bc33..287a713 100644 Binary files a/examples/function_chain.nes and b/examples/function_chain.nes differ diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index 1f2ebe9..5a859af 100644 Binary files a/examples/logic_ops.nes and b/examples/logic_ops.nes differ diff --git a/examples/loop_break_continue.nes b/examples/loop_break_continue.nes index fe55a6c..f2d50c6 100644 Binary files a/examples/loop_break_continue.nes and b/examples/loop_break_continue.nes differ diff --git a/examples/match_demo.nes b/examples/match_demo.nes index 65f247f..9343b05 100644 Binary files a/examples/match_demo.nes and b/examples/match_demo.nes differ diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index c445d4d..50f684d 100644 Binary files a/examples/metasprite_demo.nes and b/examples/metasprite_demo.nes differ diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index d599c05..7f250d2 100644 Binary files a/examples/mmc1_banked.nes and b/examples/mmc1_banked.nes differ diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes index 99754ac..cd48257 100644 Binary files a/examples/mmc3_per_state_split.nes and b/examples/mmc3_per_state_split.nes differ diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index d96c03a..8bfe217 100644 Binary files a/examples/noise_triangle_sfx.nes and b/examples/noise_triangle_sfx.nes differ diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index d65391d..88751f6 100644 Binary files a/examples/palette_and_background.nes and b/examples/palette_and_background.nes differ diff --git a/examples/platformer.nes b/examples/platformer.nes index 26296bb..911e40a 100644 Binary files a/examples/platformer.nes and b/examples/platformer.nes differ diff --git a/examples/pong.nes b/examples/pong.nes index 0c1ac7c..c8b76a0 100644 Binary files a/examples/pong.nes and b/examples/pong.nes differ diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 3b85da7..032cd18 100644 Binary files a/examples/sfx_pitch_envelope.nes and b/examples/sfx_pitch_envelope.nes differ diff --git a/examples/sha256.nes b/examples/sha256.nes index 542f205..89e4d45 100644 Binary files a/examples/sha256.nes and b/examples/sha256.nes differ diff --git a/examples/state_machine.nes b/examples/state_machine.nes index 9d5856f..97bbefa 100644 Binary files a/examples/state_machine.nes and b/examples/state_machine.nes differ diff --git a/examples/structs_enums_for.nes b/examples/structs_enums_for.nes index 1618599..5ba4263 100644 Binary files a/examples/structs_enums_for.nes and b/examples/structs_enums_for.nes differ diff --git a/examples/two_player.nes b/examples/two_player.nes index 4e5c36e..455be48 100644 Binary files a/examples/two_player.nes and b/examples/two_player.nes differ diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes index 3b3aac3..a5e6030 100644 Binary files a/examples/uxrom_banked.nes and b/examples/uxrom_banked.nes differ diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index b4ebeec..a8e6eb7 100644 Binary files a/examples/uxrom_banked_to_banked.nes and b/examples/uxrom_banked_to_banked.nes differ diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 552aa1a..a71fe7e 100644 Binary files a/examples/uxrom_user_banked.nes and b/examples/uxrom_user_banked.nes differ diff --git a/examples/war.nes b/examples/war.nes index 012fadc..b2e5323 100644 Binary files a/examples/war.nes and b/examples/war.nes differ diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index fe01a5b..85fa954 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -965,7 +965,48 @@ impl<'a> IrCodeGen<'a> { fn gen_block(&mut self, block: &IrBasicBlock) { self.emit_label(&format!("__ir_blk_{}", block.label)); - for op in &block.ops { + // Look for the canonical "compare-then-branch" fusion + // candidate: the block's last op is an 8-bit `CmpX`, its + // destination temp is the same one the terminator + // branches on, and that temp has no other uses. When + // it's a fit, we can replace the boolean materialization + // (LDA #0 / JMP / LDA #1 + BNE) with a direct branch on + // the flags `CMP` already set — about 6 cycles + 5 bytes + // saved per condition. + let fuse_cmp_branch = block + .ops + .last() + .and_then(|op| match (op, &block.terminator) { + ( + IrOp::CmpEq(d, a, b) + | IrOp::CmpNe(d, a, b) + | IrOp::CmpLt(d, a, b) + | IrOp::CmpGt(d, a, b) + | IrOp::CmpLtEq(d, a, b) + | IrOp::CmpGtEq(d, a, b), + IrTerminator::Branch(cond, true_lbl, false_lbl), + ) if cond == d && self.use_counts.get(d).copied().unwrap_or(0) == 1 => { + let kind = match op { + IrOp::CmpEq(..) => CmpKind::Eq, + IrOp::CmpNe(..) => CmpKind::Ne, + IrOp::CmpLt(..) => CmpKind::Lt, + IrOp::CmpGt(..) => CmpKind::Gt, + IrOp::CmpLtEq(..) => CmpKind::LtEq, + IrOp::CmpGtEq(..) => CmpKind::GtEq, + _ => unreachable!(), + }; + Some((*a, *b, kind, true_lbl.clone(), false_lbl.clone())) + } + _ => None, + }); + + let body_ops = if fuse_cmp_branch.is_some() { + &block.ops[..block.ops.len() - 1] + } else { + &block.ops[..] + }; + + for op in body_ops { self.gen_op(op); // After each op runs, decrement use counts for its // source temps. When a count hits zero the temp's slot @@ -976,6 +1017,32 @@ impl<'a> IrCodeGen<'a> { self.retire_op_sources(op); } + if let Some((a, b, kind, true_lbl, false_lbl)) = fuse_cmp_branch { + // Emit the fused compare + branch *first*. Retiring + // a/b before the emit would free their slots while + // the values are still live — `load_temp(a)` would + // then re-allocate `a` to whatever slot the free + // list pops next, which contains stale data. + self.gen_cmp_branch(a, b, kind, &true_lbl, &false_lbl); + // Now that the CMP has read both operands, drop their + // use counts the same way `retire_op_sources` would + // for a non-fused Cmp op. The destination temp's + // single use was the (skipped) Branch terminator — + // retire it too so its slot returns to the free list. + self.dec_use(a); + self.dec_use(b); + if let IrOp::CmpEq(d, ..) + | IrOp::CmpNe(d, ..) + | IrOp::CmpLt(d, ..) + | IrOp::CmpGt(d, ..) + | IrOp::CmpLtEq(d, ..) + | IrOp::CmpGtEq(d, ..) = block.ops.last().unwrap() + { + self.dec_use(*d); + } + return; + } + // The terminator may also reference a temp (branch // condition, return value). Those temps die after the // terminator runs; retire them here so they don't leak @@ -986,6 +1053,94 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit a fused "compare + direct branch" sequence. Replaces + /// the canonical boolean-materialization-then-branch pattern + /// the IR emits for `if x < N { ... }`-style conditions, when + /// `gen_block` detects the comparison's result feeds straight + /// into the block's terminating branch and isn't read again. + /// + /// 6502 relative branches have a ±128-byte range. The fused + /// branch's destination is the next basic block, which in + /// larger programs is often farther than that. To stay safe + /// regardless of distance we emit the *inverted* predicate + /// as a short branch that skips over a `JMP true_target`, + /// with the false target reached by a fall-through `JMP`. + /// Both `JMP`s are absolute addressing and have no range + /// limit; the relative branches only have to clear the next + /// 3 bytes (the `JMP`), which always fits. + /// + /// For the simple cases (`Eq`, `Ne`, `Lt`, `GtEq`) one + /// inverted branch does it. For `Gt` and `LtEq` the predicate + /// is a disjunction/conjunction of two flag conditions, so + /// the inversion needs two branches. + fn gen_cmp_branch( + &mut self, + a: IrTemp, + b: IrTemp, + kind: CmpKind, + true_label: &str, + false_label: &str, + ) { + self.load_temp(a); + let b_addr = self.temp_addr(b); + self.emit(CMP, AM::ZeroPage(b_addr)); + + let suffix = self.local_label_suffix(); + let skip_label = format!("__ir_cmp_skip_{suffix}"); + let true_blk = format!("__ir_blk_{true_label}"); + let false_blk = format!("__ir_blk_{false_label}"); + + match kind { + CmpKind::Eq => { + // Predicate true ↔ Z set; inverted = BNE. + self.emit(BNE, AM::LabelRelative(skip_label.clone())); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + } + CmpKind::Ne => { + self.emit(BEQ, AM::LabelRelative(skip_label.clone())); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + } + CmpKind::Lt => { + // Predicate true ↔ C clear; inverted = BCS. + self.emit(BCS, AM::LabelRelative(skip_label.clone())); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + } + CmpKind::GtEq => { + self.emit(BCC, AM::LabelRelative(skip_label.clone())); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + } + CmpKind::Gt => { + // Predicate true ↔ (C set AND Z clear). On Z + // set, fall through to false. On C clear, fall + // through too. Otherwise jump. + self.emit(BEQ, AM::LabelRelative(skip_label.clone())); + self.emit(BCC, AM::LabelRelative(skip_label.clone())); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + } + CmpKind::LtEq => { + // Predicate true ↔ (C clear OR Z set). On + // either, jump to true. Otherwise fall through. + let to_true = format!("__ir_cmp_t_{suffix}"); + self.emit(BEQ, AM::LabelRelative(to_true.clone())); + self.emit(BCC, AM::LabelRelative(to_true.clone())); + self.emit(JMP, AM::Label(false_blk.clone())); + self.emit_label(&to_true); + self.emit(JMP, AM::Label(true_blk)); + self.emit_label(&skip_label); + // Already emitted JMP false_blk in the match + // arm; skip the trailing fall-through one below. + return; + } + } + // Fall-through path lands here for the false case. + self.emit(JMP, AM::Label(false_blk)); + } + #[allow(clippy::too_many_lines)] fn gen_op(&mut self, op: &IrOp) { match op { @@ -3900,21 +4055,30 @@ mod more_tests { #[test] fn ir_codegen_local_label_suffix_is_bank_namespaced() { // When two banked functions in *different* banks both emit - // a local label like `__ir_cmp_e_`, the suffix has to - // include the bank name so the linker's discovery pass - // (which checks for cross-bank label collisions) doesn't - // panic on the second occurrence. Without the namespacing - // step, this exact program used to fail at link time with - // `duplicate label '__ir_cmp_e_8' across switchable banks`. + // a local label, the suffix has to include the bank name + // so the linker's discovery pass (which checks for cross- + // bank label collisions) doesn't panic on the second + // occurrence. Without the namespacing step, this exact + // program used to fail at link time with `duplicate label + // '__ir_shift_loop_8' across switchable banks`. + // + // We use a shift-by-variable here instead of an `if` + // because the cmp/branch fusion in `gen_block` collapses + // most `if` patterns into direct branches without local + // labels. Variable-amount shifts always need a loop label + // (gen_shift_var emits one), which gives the test a + // stable label-emission pattern regardless of future + // codegen optimizations. let (prog, _) = parser::parse( r#" game "T" { mapper: UxROM } var x: u8 = 0 + var n: u8 = 0 bank A { - fun a_fn() { if x == 0 { x = 1 } } + fun a_fn() { x = x << n } } bank B { - fun b_fn() { if x == 0 { x = 2 } } + fun b_fn() { x = x << n } } on frame { a_fn() b_fn() } start Main @@ -3931,7 +4095,7 @@ mod more_tests { .expect("A stream") .iter() .filter_map(|i| match &i.mode { - AM::Label(l) if l.contains("__ir_cmp") => Some(l.clone()), + AM::Label(l) if l.contains("__ir_shift") => Some(l.clone()), _ => None, }) .collect(); @@ -3940,17 +4104,17 @@ mod more_tests { .expect("B stream") .iter() .filter_map(|i| match &i.mode { - AM::Label(l) if l.contains("__ir_cmp") => Some(l.clone()), + AM::Label(l) if l.contains("__ir_shift") => Some(l.clone()), _ => None, }) .collect(); assert!( !a_labels.is_empty(), - "bank A should emit at least one cmp label" + "bank A should emit at least one shift label" ); assert!( !b_labels.is_empty(), - "bank B should emit at least one cmp label" + "bank B should emit at least one shift label" ); for a in &a_labels { assert!( diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 178a8d6..220a846 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -95b6d6d4 132084 +7b16a9db 132084 diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index cf37b43..d5069db 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -c49fe29d 132084 +e73a6a73 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index 5c09402..7efa628 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -8fd9f7d2 132084 +ea38044c 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index e804230..e30b406 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -f5c17e60 132084 +dded3a6a 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index a2cbdb1..366ceee 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -9b06b3fc 132084 +a7ce9d12 132084 diff --git a/tests/emulator/goldens/pong.png b/tests/emulator/goldens/pong.png index 1ad7402..cb226ca 100644 Binary files a/tests/emulator/goldens/pong.png and b/tests/emulator/goldens/pong.png differ diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index a0cd7b3..f4d7072 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -61bd9e17 132084 +eec31111 132084 diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index 77ed459..92f256b 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -62c8ca6b 132084 +d6cf6f66 132084