Phase 5: raster IRQ + frame timing

Replace busy-wait frame counter with a 50 Hz raster IRQ at line 311
(PAL stable line).  The IRQ handler increments a 16-bit frame_count
and calls game_step() once per frame.  Per-state timing now uses
enter_frame timestamps + frame_count comparisons.

- New tick.h/tick.c: install one RIRQ via Oscar64's rirq library,
  call a __interrupt handler that bumps frame_count and runs the
  state machine.  Mask CIA 1 + CIA 2 IRQs and set RST8 (the high
  bit of the 9-bit raster register) so the IRQ fires at line 311
  not line 55.
- game.h: expose volatile frame_count, replace per-state 'frame'
  counter with enter_frame timestamps.
- game.c: use frame_count - enter_frame everywhere; sample SID
  $D41B at READY enter for a random 100..250 frame WAIT duration;
  trigger a low-square-wave stinger on SID voice 1 when DRAW
  faults out (no fire for 500 frames) and gate it off ~0.2 sec
  later via a counter decremented every frame.
- main.c: replace the busy-wait loop with rasterirq_setup() and
  an empty for(;;); idle.

(Filename is tick.c/.h not rasterirq.c/.h because the oscar64
library's own rasterirq.c does '#include "rasterirq.h"' to pull
in its own header, and that include would otherwise pick up
ours and lose NUM_IRQS.)
This commit is contained in:
ballz
2026-07-17 02:02:51 +02:00
parent 65a2e5d2a1
commit d538cf46e2
5 changed files with 464 additions and 191 deletions
+125
View File
@@ -0,0 +1,125 @@
// tick.c — install a single raster IRQ at line 311 (PAL stable
// line) that calls the per-frame game tick handler.
//
// The Oscar64 rasterirq library provides the boilerplate: ISR stub,
// table management, sort, and start. We add the two C64-specific
// bits the library doesn't do for us when running with KERNAL banked
// out:
//
// 1. Mask CIA 1 and CIA 2 IRQs (write 0x7F to $DC0D/$DD0D). This
// stops the jiffy-clock Timer A from latching an IRQ that would
// fire the moment we RTI out of the raster IRQ. (The rirq
// library only does this for the *_kernal() init paths, which
// call into the KERNAL ISR at $EA31 to acknowledge; we can't
// because KERNAL is banked out.)
//
// 2. Use rirq_init(false), which installs the rirq_isr_ram_io
// handler at the hardware IRQ vector ($FFFE/$FFFF). This
// handler does NOT call into KERNAL — it acks the raster IRQ
// (asl $d019) and returns directly via rti. Combined with the
// CIA mask, this means the only IRQ we ever service is the
// raster IRQ.
//
// The RIRQ code is a single "wait for line, then JSR frame_tick_handler,
// RTS" stub. We use rirq_call() to install the JSR.
//
// **PAL line 311 and the 9-bit raster counter.** PAL frames are 312
// lines (0..311), so the stable line 311 is outside the 8-bit
// $D012 range. The VIC's raster register is 9 bits: the high bit
// is bit 7 of $D011 (VIC_CTRL1_RST8), and the low 8 bits are $D012.
// The oscar64 rirq library's `rirq_set(n, row, code)` takes a `byte
// row` (0..255) and writes (row - 1) to $D012 internally, so it
// cannot directly address line 311. The workaround is:
//
// - Pass row = 56 to rirq_set (i.e. 311 - 256 + 1, where the +1
// accounts for the library's "one line below" convention).
// - Set VIC_CTRL1_RST8 = 1 after rirq_sort and BEFORE enabling
// CPU IRQ, so the raster comparison becomes (1 << 8) | 55 = 311.
//
// We can't use rirq_start() because it clears RST8 and overwrites
// $D012 with 100 (a "kick start" line that lets the first IRQ fire
// quickly). Instead we do the equivalent of rirq_start inline, with
// RST8 left at 1 and $D012 left at the rirq_sort value.
//
// **File naming.** This file is named tick.c (not rasterirq.c)
// because the oscar64 library's rasterirq.c does `#include
// "rasterirq.h"` to find its own rasterirq.h, and that include
// resolves relative to the compile CWD. If our header were also
// named rasterirq.h, the library would pick up ours and the
// NUM_IRQS / RIRQCode defines would be missing. The naming is
// purely a workaround for the library's include style.
#include "tick.h"
#include "game.h"
#include <c64/vic.h>
#include <c64/cia.h>
#include <c64/rasterirq.h>
// The single RIRQ code slot. One IRQ = one wait + one JSR + one RTS.
static RIRQCode frame_tick;
// The per-frame handler, called from the raster IRQ at line 311.
//
// Marked __interrupt so the compiler saves/restores any zero-page
// registers the function (or game_step) uses. This matches the
// autocrawler.c pattern in the oscar64 samples. Note that this
// function is NOT the 6502 ISR — the rirq_isr_ram_io stub installed
// by rirq_init is the actual ISR. This function is called via JSR
// from the rirq_isr, and returns with RTS. A/X/Y are saved by the
// rirq_isr, so we can clobber them freely.
__interrupt void frame_tick_handler(void)
{
frame_count++;
game_step();
}
void rasterirq_setup(void)
{
// 1. Mask all CIA 1 and CIA 2 interrupt sources. The ICR at
// $DC0D/$DD0D is a set/clear register: bit 7 = 0 means
// "clear", bits 0-4 = 0x1F means "clear all source mask
// bits". Writing 0x7F disables every source. A second
// write acknowledges any latched IRQ; reading would do the
// same but writing is fine.
cia1.icr = 0x7f;
cia2.icr = 0x7f;
cia1.icr = 0x7f;
cia2.icr = 0x7f;
// 2. Install the raster IRQ system. false = use the hardware
// IRQ vector at $FFFE, no KERNAL continuation (KERNAL is
// banked out by memmap_setup).
rirq_init(false);
// 3. Build the RIRQ code: a single call to frame_tick_handler.
// size=1 = one op slot. rirq_call at index 0 replaces the
// STY $xxxx stub at offset 9 with a JSR frame_tick_handler.
// The resulting code is: wait + JSR handler + RTS.
rirq_build(&frame_tick, 1);
rirq_call(&frame_tick, 0, frame_tick_handler);
// 4. Place this RIRQ at line 56 in the rirq library's 8-bit
// view. Combined with RST8=1 (set in step 6 below) the
// actual raster comparison becomes 256 + (56 - 1) = 311.
rirq_set(0, 56, &frame_tick);
// 5. Sort the RIRQ list. This also writes $D012 = 56 - 1 = 55
// (the low byte of the 9-bit row 311) and sets nextIRQ = 0.
rirq_sort();
// 6. Set the high bit of the 9-bit raster register. This
// makes the VIC compare the raster counter against 256 + 55
// = 311 instead of just 55. Must happen before we enable
// CPU IRQ (CLI), otherwise the first IRQ might fire at the
// wrong line.
vic.ctrl1 |= VIC_CTRL1_RST8;
// 7. Start the raster IRQ. We can't use rirq_start() because
// it would clear RST8 and overwrite $D012 with 100. Instead
// we do the same thing minus those two writes: acknowledge
// any pending VIC IRQ, then enable CPU IRQ.
__asm {
asl $d019
cli
}
}