1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

Codegen: MMC3 on_scanline IRQ dispatch (minimal)

Wires \`on scanline(N)\` handlers through IR lowering and codegen:

- IR lowering: each scanline handler becomes a regular IR function
  named \`{state}_scanline_{N}\`
- IR codegen: when any scanline handler exists, emits MMC3 IRQ setup
  at program start (\$C000 latch, \$C001 reload, \$E001 enable, CLI)
  and a \`__irq_user\` handler that saves registers, acknowledges via
  \$E000, JSRs the scanline handler, restores registers, and RTIs
- Linker: vector table prefers \`__irq_user\` over the default \`__irq\`
  stub when both exist

Scope of this first pass is intentionally minimal: supports ONE
scanline handler per program (the first one found in IR function
order). Per-state dispatch and multi-scanline reload will come later.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 16:22:54 +00:00
parent 281198cbb9
commit 359241d906
No known key found for this signature in database
4 changed files with 123 additions and 2 deletions

View file

@ -185,6 +185,22 @@ impl<'a> IrCodeGen<'a> {
} }
} }
// 2b. If the program has any `on scanline` handlers, set up
// the MMC3 IRQ counter. We pick the first scanline handler's
// line count as the latch value. Only supports a single-
// scanline-per-program setup for now.
let scanline_info = find_first_scanline_handler(ir);
if let Some((_state_name, line)) = &scanline_info {
// Write (line-1) to $C000 (scanline latch), any value to
// $C001 (reload counter), any value to $E001 (enable IRQ).
self.emit(LDA, AM::Immediate(line.saturating_sub(1)));
self.emit(STA, AM::Absolute(0xC000));
self.emit(STA, AM::Absolute(0xC001));
self.emit(STA, AM::Absolute(0xE001));
// Enable interrupts (CLI) so the IRQ can fire.
self.emit(CLI, AM::Implied);
}
// 3. Main dispatch loop // 3. Main dispatch loop
let main_loop = "__ir_main_loop".to_string(); let main_loop = "__ir_main_loop".to_string();
self.emit_label(&main_loop); self.emit_label(&main_loop);
@ -218,6 +234,36 @@ impl<'a> IrCodeGen<'a> {
self.gen_function(func); self.gen_function(func);
} }
// 5. If we have scanline handlers, emit an IRQ handler that
// saves registers, ACKs the MMC3 IRQ, JSRs into the first
// scanline handler, restores registers, and RTIs. The linker
// picks up `__irq_user` and uses it as the IRQ vector instead
// of the default stub.
if let Some((state_name, line)) = scanline_info {
self.emit_label("__irq_user");
// Save registers onto the stack.
self.emit(PHA, AM::Implied);
self.emit(TXA, AM::Implied);
self.emit(PHA, AM::Implied);
self.emit(TYA, AM::Implied);
self.emit(PHA, AM::Implied);
// Acknowledge and reload the MMC3 IRQ counter.
self.emit(STA, AM::Absolute(0xE000)); // disable / ack
self.emit(STA, AM::Absolute(0xE001)); // re-enable
// JSR into the scanline handler. We don't dispatch by
// state yet — the first scanline handler found is used
// unconditionally.
let handler = format!("{state_name}_scanline_{line}");
self.emit(JSR, AM::Label(format!("__ir_fn_{handler}")));
// Restore registers and return from interrupt.
self.emit(PLA, AM::Implied);
self.emit(TAY, AM::Implied);
self.emit(PLA, AM::Implied);
self.emit(TAX, AM::Implied);
self.emit(PLA, AM::Implied);
self.emit(RTI, AM::Implied);
}
self.instructions self.instructions
} }
@ -583,6 +629,22 @@ enum CmpKind {
GtEq, GtEq,
} }
/// Scan the IR functions for the first scanline handler (named
/// `{state}_scanline_{line}`) and return the state name and line.
fn find_first_scanline_handler(ir: &IrProgram) -> Option<(String, u8)> {
for func in &ir.functions {
// Match the pattern `<state>_scanline_<N>` by splitting on
// the last `_scanline_`. This keeps it simple and avoids
// re-threading scanline info through lowering.
if let Some((state_name, tail)) = func.name.rsplit_once("_scanline_") {
if let Ok(line) = tail.parse::<u8>() {
return Some((state_name.to_string(), line));
}
}
}
None
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -768,6 +830,35 @@ mod more_tests {
assert!(writes_slot1, "second draw should use OAM slot 1"); assert!(writes_slot1, "second draw should use OAM slot 1");
} }
#[test]
fn ir_codegen_scanline_emits_mmc3_setup_and_irq_user() {
let insts = lower_and_gen(
r#"
game "T" { mapper: MMC3 }
state Main {
on frame { wait_frame }
on scanline(100) { wait_frame }
}
start Main
"#,
);
// MMC3 latch write (LDA #99; STA $C000)
let has_latch = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0xC000));
assert!(has_latch, "should write to MMC3 latch $C000");
// __irq_user label should be emitted
let has_irq_user = insts
.iter()
.any(|i| matches!(&i.mode, AM::Label(l) if l == "__irq_user"));
assert!(has_irq_user, "should emit __irq_user label");
// The scanline handler function should exist
let has_handler = insts
.iter()
.any(|i| matches!(&i.mode, AM::Label(l) if l == "__ir_fn_Main_scanline_100"));
assert!(has_handler, "should emit scanline handler function");
}
#[test] #[test]
fn ir_codegen_transition_writes_state_index() { fn ir_codegen_transition_writes_state_index() {
let insts = lower_and_gen( let insts = lower_and_gen(

View file

@ -215,6 +215,14 @@ impl LoweringContext {
if let Some(on_frame) = &state.on_frame { if let Some(on_frame) = &state.on_frame {
self.lower_handler(&format!("{}_frame", state.name), on_frame, state); self.lower_handler(&format!("{}_frame", state.name), on_frame, state);
} }
// Lower each scanline handler as a function named
// `{state}_scanline_{N}`. The IR codegen will generate the MMC3
// IRQ dispatch wrapper separately.
for (line, block) in &state.on_scanline {
let name = format!("{}_scanline_{line}", state.name);
self.lower_handler(&name, block, state);
}
} }
fn lower_handler(&mut self, name: &str, block: &Block, state: &StateDecl) { fn lower_handler(&mut self, name: &str, block: &Block, state: &StateDecl) {

View file

@ -130,10 +130,17 @@ impl Linker {
} }
prg.resize(vector_offset, 0xFF); prg.resize(vector_offset, 0xFF);
// Write vector table // Write vector table. IR codegen emits a richer IRQ handler
// under `__irq_user` when the program has scanline handlers;
// prefer that over the generic RTI stub at `__irq`.
let nmi_addr = result.labels.get("__nmi").copied().unwrap_or(0xC000); let nmi_addr = result.labels.get("__nmi").copied().unwrap_or(0xC000);
let reset_addr = result.labels.get("__reset").copied().unwrap_or(0xC000); let reset_addr = result.labels.get("__reset").copied().unwrap_or(0xC000);
let irq_addr = result.labels.get("__irq").copied().unwrap_or(0xC000); let irq_addr = result
.labels
.get("__irq_user")
.or_else(|| result.labels.get("__irq"))
.copied()
.unwrap_or(0xC000);
prg.extend_from_slice(&nmi_addr.to_le_bytes()); prg.extend_from_slice(&nmi_addr.to_le_bytes());
prg.extend_from_slice(&reset_addr.to_le_bytes()); prg.extend_from_slice(&reset_addr.to_le_bytes());

View file

@ -141,6 +141,21 @@ fn program_with_functions() {
rom::validate_ines(&rom_data).expect("should be valid iNES"); rom::validate_ines(&rom_data).expect("should be valid iNES");
} }
#[test]
fn program_with_on_scanline_mmc3() {
let source = r#"
game "Scanline" { mapper: MMC3 }
var sx: u8 = 0
state Main {
on frame { wait_frame }
on scanline(120) { scroll(sx, 0) }
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test] #[test]
fn program_with_structs() { fn program_with_structs() {
let source = r#" let source = r#"