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:
parent
281198cbb9
commit
359241d906
4 changed files with 123 additions and 2 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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#"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue