From 2fd3a6e54548f643b211d6ea5ddc285beab6a69f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 17:58:36 +0000 Subject: [PATCH] IR codegen: clear OAM shadow at frame handler entry Each frame handler now begins with a short 64-slot loop that writes \$FE to the Y-position byte of every OAM entry, hiding any sprites the previous frame drew that the current frame doesn't. Without this, stopping a \`draw\` call one frame leaves the sprite lingering at its last position forever. Cost: ~384 cycles per frame (2.3% of the ~16 666 cycles available between vblanks). The per-frame \`draw\` calls overwrite the slots they actually use, so the clear is free at runtime for sprites that ARE drawn. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3 --- src/codegen/ir_codegen.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index d3ae8bb..67b7792 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -356,6 +356,24 @@ impl<'a> IrCodeGen<'a> { self.emit_label(&format!("__ir_fn_{}", func.name)); + // At the start of every frame handler, clear the OAM shadow + // buffer so stale sprites from the previous frame (or from a + // different state's handler) don't linger on screen. We set + // the Y position byte of every OAM entry to $FE (off-screen). + // The actual drawing code overwrites the slots it needs. + if self.in_frame_handler { + let clear_loop = format!("__ir_oam_clear_{}", func.name); + self.emit(LDX, AM::Immediate(0)); + self.emit(LDA, AM::Immediate(0xFE)); + self.emit_label(&clear_loop); + self.emit(STA, AM::AbsoluteX(0x0200)); + self.emit(INX, AM::Implied); + self.emit(INX, AM::Implied); + self.emit(INX, AM::Implied); + self.emit(INX, AM::Implied); + self.emit(BNE, AM::LabelRelative(clear_loop)); + } + for block in &func.blocks { self.gen_block(block); }