mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
linker+runtime: code review cleanup of the seven gates
* 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
This commit is contained in:
parent
53c454669d
commit
e5bc325a71
3 changed files with 39 additions and 21 deletions
|
|
@ -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())));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Instruction> {
|
||||
let NmiOptions {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue