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 -35
View File
@@ -2,32 +2,54 @@
// //
// See game.h for the transition diagram and game_init() / game_step() // See game.h for the transition diagram and game_init() / game_step()
// documentation. Each state has an "enter" action (set the screen, // documentation. Each state has an "enter" action (set the screen,
// reset the frame counter, reset per-state state) and a "step" action // reset per-state state, sample any randomness) and a "step" action
// (check inputs, advance the frame counter, transition). // (check inputs, compute elapsed frames against enter_frame, decide
// whether to transition).
// //
// In Phase 4 the main loop is `while (1) game_step();`. In Phase 5 // In Phase 4 the main loop was `while (1) game_step();` and the
// the body of the main loop will be replaced by a raster IRQ handler // per-state "frame counter" was just a count of how many times
// that calls game_step() on line 311. Either way, game_step() is the // game_step had been called since entering the current state — so
// only thing that needs to run "once per frame". // state durations were CPU-bound, not wall-clock-bound.
//
// In Phase 5 the busy-wait body of the main loop is gone. Instead,
// a raster IRQ at line 311 (PAL stable line) runs at exactly 50 Hz
// and calls game_step() once per frame. All state durations are
// now wall-clock-bound: a state that should last 60 frames lasts
// 60 × 20 ms = 1.2 seconds, regardless of what the CPU is doing
// between IRQs. The 16-bit global frame_count (incremented by the
// IRQ handler *before* calling game_step) is the new "frame
// counter"; per-state timing uses an enter_frame timestamp captured
// on entry.
#include "game.h" #include "game.h"
#include "screens.h" #include "screens.h"
#include "input.h" #include "input.h"
#include "score.h" #include "score.h"
#include <c64/vic.h> #include <c64/vic.h>
#include <c64/sid.h>
// --- globals ------------------------------------------------------------
// Current state. Set by game_init() and by the per-state step // Current state. Set by game_init() and by the per-state step
// functions when a transition is triggered. Read by the switch in // functions when a transition is triggered. Read by the switch in
// game_step(). // game_step().
static byte state; static byte state;
// Frame counter for the current state. Reset to 0 by each per-state // Per-state "entered at" timestamp. Set by each per-state enter
// "enter" function. Compared against per-state constants in the step // function to the current frame_count. The per-state step functions
// functions (60 for READY, 100 for WAIT and WIN, 300 for GAMEOVER, // compute elapsed = frame_count - enter_frame to decide when to
// 500 for the DRAW fault timeout). unsigned short is enough for // transition. 16 bits is enough for 65535 frames = ~22 minutes at
// 65535 frames = ~22 minutes at 50 Hz; the longest single state is // 50 Hz; the longest single state is 500 frames (DRAW fault), so 16
// 500 frames (DRAW fault), so 16 bits is more than enough. // bits is more than enough for a single state.
static unsigned short frame; static unsigned short enter_frame;
// WAIT random duration (in frames). Sampled in game_enter_ready from
// SID $D41B (the oscillator 3 register, effectively random).
// 100 + (sid.random % 150) gives a 2.0-5.0 second suspense window
// at 50 Hz. The randomness is sampled at READY enter (not WAIT
// enter) so the value is stable for the duration of READY (and the
// subsequent WAIT).
static unsigned short wait_duration_frames;
// --- TITLE-specific input state ---------------------------------------- // --- TITLE-specific input state ----------------------------------------
// //
@@ -56,21 +78,33 @@ static unsigned short title_first_frame;
// entry to DRAW (in game_enter_draw()). // entry to DRAW (in game_enter_draw()).
static char draw_was_pressed[2]; static char draw_was_pressed[2];
// --- fault stinger -----------------------------------------------------
//
// When the DRAW state's 500-frame fault timeout fires, we trigger a
// short low-square-wave stinger on SID voice 1. This counter is
// decremented every frame in game_step() (not in any per-state step,
// so it cleans up even if we transition out of TITLE before the
// stinger would naturally end). When it reaches 0, we gate voice 1
// off. ~10 frames = ~0.2 sec at 50 Hz.
static byte fault_stinger_ticks;
// --- per-state enter functions ----------------------------------------- // --- per-state enter functions -----------------------------------------
// //
// Each "enter" function: // Each "enter" function:
// - calls show_screen() (or show_white_screen() for DRAW) // - calls show_screen() (or show_white_screen() for DRAW)
// - calls score_render() // - calls score_render()
// - resets `frame` to 0 // - sets enter_frame = frame_count (the per-state timestamp)
// - resets any per-state state (e.g. title_input, draw_was_pressed) // - resets any per-state state (e.g. title_input, draw_was_pressed)
// - sets the border color (TITLE flashes it; DRAW is all-white; // - sets the border color (TITLE flashes it; DRAW is all-white;
// other states are black) // other states are black)
// - samples the SID random for WAIT duration (in READY enter, not
// WAIT enter, per the Phase 5 spec)
static void game_enter_title(void) static void game_enter_title(void)
{ {
show_screen(SCREEN_TITLE); show_screen(SCREEN_TITLE);
score_render(); score_render();
frame = 0; enter_frame = frame_count;
title_input = TITLE_IDLE; title_input = TITLE_IDLE;
title_first_frame = 0; title_first_frame = 0;
// Border starts white (the "PRESS FIRE" prompt is visible). // Border starts white (the "PRESS FIRE" prompt is visible).
@@ -81,7 +115,12 @@ static void game_enter_ready(void)
{ {
show_screen(SCREEN_WAITING1); show_screen(SCREEN_WAITING1);
score_render(); score_render();
frame = 0; enter_frame = frame_count;
// Sample SID oscillator 3 ($D41B) for the upcoming WAIT duration.
// 100 + (sid.random % 150) frames = 2.0..5.0 sec at 50 Hz. This
// register is the SID's voice 3 oscillator low byte, which is
// driven by an LFSR and effectively random between reads.
wait_duration_frames = 100 + (sid.random % 150);
vic.color_border = 0; vic.color_border = 0;
} }
@@ -89,7 +128,7 @@ static void game_enter_wait(void)
{ {
show_screen(SCREEN_WAITING2); show_screen(SCREEN_WAITING2);
score_render(); score_render();
frame = 0; enter_frame = frame_count;
vic.color_border = 0; vic.color_border = 0;
} }
@@ -97,7 +136,7 @@ static void game_enter_draw(void)
{ {
show_white_screen(); show_white_screen();
score_render(); score_render();
frame = 0; enter_frame = frame_count;
vic.color_border = 1; // white border matches the white screen vic.color_border = 1; // white border matches the white screen
draw_was_pressed[0] = 0; draw_was_pressed[0] = 0;
draw_was_pressed[1] = 0; draw_was_pressed[1] = 0;
@@ -108,7 +147,7 @@ static void game_enter_win_p1(void)
show_screen(SCREEN_WIN_HARE); show_screen(SCREEN_WIN_HARE);
score_p1++; score_p1++;
score_render(); score_render();
frame = 0; enter_frame = frame_count;
vic.color_border = 0; vic.color_border = 0;
} }
@@ -117,7 +156,7 @@ static void game_enter_win_p2(void)
show_screen(SCREEN_WIN_SCOOT); show_screen(SCREEN_WIN_SCOOT);
score_p2++; score_p2++;
score_render(); score_render();
frame = 0; enter_frame = frame_count;
vic.color_border = 0; vic.color_border = 0;
} }
@@ -130,10 +169,40 @@ static void game_enter_gameover(void)
score_p1 = 0; score_p1 = 0;
score_p2 = 0; score_p2 = 0;
score_render(); score_render();
frame = 0; enter_frame = frame_count;
vic.color_border = 0; vic.color_border = 0;
} }
// --- fault stinger helpers ---------------------------------------------
static void game_trigger_fault_stinger(void)
{
// Low square wave on voice 1. ~100 Hz is in the "low buzz"
// range, not a musical note — appropriate for an "aborted
// round" sting. Attack=0, decay=0, sustain=15 (max), release=0
// means: the note is full volume the instant the gate goes on
// and stays full volume until gated off (no decay). We gate
// off after ~10 frames from game_step()'s cleanup counter.
sid.voices[1].freq = SID_FREQ_PAL(100);
sid.voices[1].attdec = 0x00;
sid.voices[1].susrel = 0xf0;
sid.voices[1].ctrl = SID_CTRL_RECT | SID_CTRL_GATE;
fault_stinger_ticks = 10;
}
static void game_advance_fault_stinger(void)
{
// Called once per frame from game_step(). Decrements the
// counter and gates voice 1 off when it reaches zero. Putting
// this here (rather than in game_step_title) means the stinger
// cleans up even if we leave TITLE during the 0.2 sec window.
if (fault_stinger_ticks > 0) {
fault_stinger_ticks--;
if (fault_stinger_ticks == 0)
sid.voices[1].ctrl = SID_CTRL_RECT;
}
}
// --- per-state step functions ------------------------------------------ // --- per-state step functions ------------------------------------------
static void game_step_title(void) static void game_step_title(void)
@@ -142,7 +211,8 @@ static void game_step_title(void)
// The visible effect is a 12.5 Hz blink on the border around the // The visible effect is a 12.5 Hz blink on the border around the
// title screen image. Phase 8 will replace this with actual // title screen image. Phase 8 will replace this with actual
// "PRESS FIRE" text rendered into the bitmap. // "PRESS FIRE" text rendered into the bitmap.
if ((frame & 1) == 0) unsigned short elapsed = frame_count - enter_frame;
if ((elapsed & 1) == 0)
vic.color_border ^= 1; vic.color_border ^= 1;
char p1 = input_fire(1); // Hare (port 1) char p1 = input_fire(1); // Hare (port 1)
@@ -157,13 +227,13 @@ static void game_step_title(void)
game_enter_ready(); game_enter_ready();
} else if (p1 || p2) { } else if (p1 || p2) {
title_input = TITLE_FIRST_HELD; title_input = TITLE_FIRST_HELD;
title_first_frame = frame; title_first_frame = frame_count;
} }
break; break;
case TITLE_FIRST_HELD: case TITLE_FIRST_HELD:
if (both) { if (both) {
if (frame - title_first_frame <= 8) { if (frame_count - title_first_frame <= 8) {
state = STATE_READY; state = STATE_READY;
game_enter_ready(); game_enter_ready();
} else { } else {
@@ -171,7 +241,7 @@ static void game_step_title(void)
} }
} else if (none) { } else if (none) {
title_input = TITLE_IDLE; title_input = TITLE_IDLE;
} else if (frame - title_first_frame > 8) { } else if (frame_count - title_first_frame > 8) {
// Held too long without the other button following. // Held too long without the other button following.
title_input = TITLE_WAIT_RELEASE; title_input = TITLE_WAIT_RELEASE;
} }
@@ -188,7 +258,7 @@ static void game_step_title(void)
static void game_step_ready(void) static void game_step_ready(void)
{ {
if (frame >= 60) { if (frame_count - enter_frame >= 60) {
state = STATE_WAIT; state = STATE_WAIT;
game_enter_wait(); game_enter_wait();
} }
@@ -196,9 +266,12 @@ static void game_step_ready(void)
static void game_step_wait(void) static void game_step_wait(void)
{ {
// Phase 4: fixed 100 frames. Phase 5: random 100-250 from // Random 100..250 frames, sampled in game_enter_ready. We
// PEEK(0xD41B) sampled on READY enter. // intentionally do NOT read input here — pressing fire during
if (frame >= 100) { // WAIT is "cheating" and the game ignores it (GAME.md §9). The
// DRAW state still does rising-edge detection, so any fire
// pressed here is not remembered.
if (frame_count - enter_frame >= wait_duration_frames) {
state = STATE_DRAW; state = STATE_DRAW;
game_enter_draw(); game_enter_draw();
} }
@@ -206,9 +279,13 @@ static void game_step_wait(void)
static void game_step_draw(void) static void game_step_draw(void)
{ {
unsigned short elapsed = frame_count - enter_frame;
// 500-frame fault timeout (10 sec at 50 Hz). If neither player // 500-frame fault timeout (10 sec at 50 Hz). If neither player
// fires in 10 sec, abort the round and go back to TITLE. // fires in 10 sec, abort the round, trigger a short stinger on
if (frame > 500) { // SID voice 1, and go back to TITLE. No point awarded.
if (elapsed > 500) {
game_trigger_fault_stinger();
state = STATE_TITLE; state = STATE_TITLE;
game_enter_title(); game_enter_title();
return; return;
@@ -236,7 +313,7 @@ static void game_step_draw(void)
static void game_step_win_p1(void) static void game_step_win_p1(void)
{ {
if (frame >= 100) { if (frame_count - enter_frame >= 100) {
if (score_p1 >= 5) { if (score_p1 >= 5) {
state = STATE_GAMEOVER; state = STATE_GAMEOVER;
game_enter_gameover(); game_enter_gameover();
@@ -249,7 +326,7 @@ static void game_step_win_p1(void)
static void game_step_win_p2(void) static void game_step_win_p2(void)
{ {
if (frame >= 100) { if (frame_count - enter_frame >= 100) {
if (score_p2 >= 5) { if (score_p2 >= 5) {
state = STATE_GAMEOVER; state = STATE_GAMEOVER;
game_enter_gameover(); game_enter_gameover();
@@ -262,7 +339,7 @@ static void game_step_win_p2(void)
static void game_step_gameover(void) static void game_step_gameover(void)
{ {
if (frame >= 300) { if (frame_count - enter_frame >= 300) {
state = STATE_TITLE; state = STATE_TITLE;
game_enter_title(); game_enter_title();
} }
@@ -270,15 +347,28 @@ static void game_step_gameover(void)
// --- public API -------------------------------------------------------- // --- public API --------------------------------------------------------
// Definition of the global frame_count declared in game.h. Lives in
// BSS so it's zero at startup; the raster IRQ handler increments it.
volatile unsigned short frame_count;
void game_init(void) void game_init(void)
{ {
state = STATE_TITLE; state = STATE_TITLE;
// enter_frame is set by game_enter_title below; the very first
// game_step() call happens in the same raster IRQ that
// increments frame_count from 0 to 1, so enter_frame=0 there
// is fine (elapsed = 1 on the first call).
game_enter_title(); game_enter_title();
} }
void game_step(void) void game_step(void)
{ {
frame++; // Per-frame work that must happen regardless of state: tick down
// the fault-stinger counter and gate voice 1 off when it
// reaches zero. Doing this here (not in any per-state step)
// guarantees the stinger cleans up even if we leave TITLE
// during its 0.2 sec lifetime.
game_advance_fault_stinger();
switch (state) { switch (state) {
case STATE_TITLE: game_step_title(); break; case STATE_TITLE: game_step_title(); break;
+35 -14
View File
@@ -8,7 +8,8 @@
// //
// STATE_TITLE — title screen, flashing border, wait for both fire // STATE_TITLE — title screen, flashing border, wait for both fire
// STATE_READY — "ready" screen, 60 frames (1.2 s at 50 Hz) // STATE_READY — "ready" screen, 60 frames (1.2 s at 50 Hz)
// STATE_WAIT — "wait" screen, 100 frames (2 s; Phase 5: random) // STATE_WAIT — "wait" screen, 100..250 frames (random, from
// SID $D41B sampled on READY enter)
// STATE_DRAW — white screen, wait for first fire (or 500-frame // STATE_DRAW — white screen, wait for first fire (or 500-frame
// fault timeout) // fault timeout)
// STATE_WIN_P1 — "Hare won" screen, 100 frames, +1 to player 1 // STATE_WIN_P1 — "Hare won" screen, 100 frames, +1 to player 1
@@ -19,7 +20,7 @@
// //
// TITLE --both fire (within 8 frames)--> READY // TITLE --both fire (within 8 frames)--> READY
// READY --60 frames--------------------> WAIT // READY --60 frames--------------------> WAIT
// WAIT --100 frames-------------------> DRAW // WAIT --random 100..250 frames------> DRAW
// DRAW --port 1 fire (rising edge)---> WIN_P1 // DRAW --port 1 fire (rising edge)---> WIN_P1
// DRAW --port 0 fire (rising edge)---> WIN_P2 // DRAW --port 0 fire (rising edge)---> WIN_P2
// DRAW --500 frames (fault)----------> TITLE // DRAW --500 frames (fault)----------> TITLE
@@ -28,6 +29,13 @@
// WIN_P2 --100 frames + score < 5-----> READY // WIN_P2 --100 frames + score < 5-----> READY
// WIN_P2 --100 frames + score == 5----> GAMEOVER // WIN_P2 --100 frames + score == 5----> GAMEOVER
// GAMEOVER --300 frames (scores reset)--> TITLE // GAMEOVER --300 frames (scores reset)--> TITLE
//
// Timing: the per-state frame counter is replaced (Phase 5) by a
// 16-bit global frame_count incremented by the raster IRQ at 50 Hz
// (line 311). Per-state step actions compute elapsed =
// frame_count - enter_frame to decide when to transition.
#include <c64/types.h>
#define STATE_TITLE 0 #define STATE_TITLE 0
#define STATE_READY 1 #define STATE_READY 1
@@ -37,23 +45,36 @@
#define STATE_WIN_P2 5 #define STATE_WIN_P2 5
#define STATE_GAMEOVER 6 #define STATE_GAMEOVER 6
// frame_count — global 50 Hz frame counter. Incremented by the
// raster IRQ handler in rasterirq.c (see rasterirq.h) once per
// frame, *before* calling game_step(). So at the time game_step()
// runs, frame_count is the current frame number (1 on the first
// call after power-on, 2 on the second, etc.).
//
// Marked volatile because the IRQ handler is the writer and the
// game-step functions are the readers. 16 bits = 65535 frames =
// ~22 minutes at 50 Hz; the longest single state is 500 frames
// (DRAW fault), so 16 bits is plenty for one state. We never read
// frame_count from the main loop (the main loop is `while (1) {}`
// in Phase 5) so torn-read races are not a concern.
extern volatile unsigned short frame_count;
// game_init() — set up the state machine and enter the TITLE state. // game_init() — set up the state machine and enter the TITLE state.
// Call once at startup, after memmap_setup() and score_init(). // Call once at startup, after memmap_setup() and score_init() and
// Calls show_screen(SCREEN_TITLE) and score_render() as part of the // rasterirq_setup(). Calls show_screen(SCREEN_TITLE) and
// TITLE entry action. // score_render() as part of the TITLE entry action.
void game_init(void); void game_init(void);
// game_step() — advance the state machine by one frame. // game_step() — advance the state machine by one frame. Called from
// // the raster IRQ handler at line 311. Per-state step actions read
// In Phase 4 the main loop calls this in a tight busy-wait. In // both fire buttons, compute elapsed frames against the per-state
// Phase 5 a raster IRQ handler at line 311 will call it at 50 Hz. // enter_frame timestamp, and trigger state transitions (each
// Per-state step actions read both fire buttons, advance the per-state // transition calls the new state's "enter" action immediately, so
// frame counter, and trigger state transitions (each transition calls // the next step operates on the new state).
// the new state's "enter" action immediately, so the next step
// operates on the new state).
// //
// game_step() also drives per-state visual updates that need to run // game_step() also drives per-state visual updates that need to run
// every frame (currently just the TITLE border flash at 25 Hz). // every frame (currently just the TITLE border flash at 25 Hz and
// the fault-stinger audio gate-off counter).
void game_step(void); void game_step(void);
#pragma compile("game.c") #pragma compile("game.c")
+18 -13
View File
@@ -1,4 +1,4 @@
// main.c — Whack Hare! entry point (Phase 4: state machine skeleton). // main.c — Whack Hare! entry point (Phase 5: raster IRQ + idle loop).
// //
// Flow: // Flow:
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM. // 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
@@ -6,21 +6,23 @@
// 3. game_init() — enter the TITLE state (which also loads // 3. game_init() — enter the TITLE state (which also loads
// the title screen and renders the score // the title screen and renders the score
// bar). // bar).
// 4. while (1) game_step() — the state machine runs forever. // 4. rasterirq_setup() — install the single RIRQ at line 311.
// The IRQ handler increments frame_count
// and calls game_step() once per frame.
// 5. while (1) {} — idle. All per-frame work happens in
// the IRQ handler.
// //
// In Phase 4 this is a busy-wait loop: game_step() is called in a // In Phase 4 step 5 was `while (1) game_step();` — a busy-wait that
// tight loop with no real timing. The "frame counter" inside game.c // called game_step as fast as the CPU could, so the "frame counter"
// is just a count of how many times game_step() has been called, so // was CPU-bound. In Phase 5 the busy-wait is gone: game_step is
// the state durations are CPU-bound, not wall-clock-bound. // called from the raster IRQ at exactly 50 Hz, so state durations
// // are wall-clock-bound (60 frames = 1.2 sec, etc.) regardless of
// In Phase 5 the busy-wait body of the main loop becomes a raster // what the CPU is doing between IRQs.
// IRQ handler that runs at 50 Hz, and game_step() is called from
// the IRQ. The function signature doesn't change — only the call
// site does — so the game logic is the same in both phases.
#include "memmap.h" #include "memmap.h"
#include "game.h" #include "game.h"
#include "score.h" #include "score.h"
#include "tick.h"
// We don't malloc, so the heap is unused. Setting it to 0 frees the // We don't malloc, so the heap is unused. Setting it to 0 frees the
// space for the screen data in the main region. // space for the screen data in the main region.
@@ -31,9 +33,12 @@ int main(void)
memmap_setup(); memmap_setup();
score_init(); score_init();
game_init(); game_init();
rasterirq_setup();
while (1) // The raster IRQ does all the per-frame work. The main loop
game_step(); // is a deliberate spin: nothing to do between IRQs.
for (;;)
;
return 0; return 0;
} }
+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
}
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef WHACK_HARE_TICK_H
#define WHACK_HARE_TICK_H
// tick.h — 50 Hz raster IRQ setup for the game tick.
//
// A single RIRQ is installed at raster line 311 (PAL stable line, right
// after vertical blank, before any badlines). The IRQ handler runs
// once per frame and does two things:
//
// 1. Increments the global 16-bit frame_count (50 Hz).
// 2. Calls game_step() — the state machine + per-state step actions
// read both joysticks, update the score bar, and run the
// flashing-text animations.
//
// All game timing is now driven by frame_count comparisons. No more
// busy-wait. The main() loop is an empty `while (1) {}`.
//
// CIA 1 and CIA 2 IRQs are masked at setup so the jiffy-clock handler
// doesn't fire nested inside the raster IRQ. We bank out the KERNAL
// at startup (memmap_setup) so we use rirq_init(false) — the hardware
// IRQ vector, no KERNAL continuation.
//
// (We can't name this file "rasterirq.h" because the oscar64
// library's rasterirq.c does `#include "rasterirq.h"` to find its
// own rasterirq.h, and that include would resolve to our header
// instead — see the comment in tick.c for details.)
void rasterirq_setup(void);
#pragma compile("tick.c")
#endif