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:
+254
-164
@@ -2,32 +2,54 @@
|
||||
//
|
||||
// See game.h for the transition diagram and game_init() / game_step()
|
||||
// documentation. Each state has an "enter" action (set the screen,
|
||||
// reset the frame counter, reset per-state state) and a "step" action
|
||||
// (check inputs, advance the frame counter, transition).
|
||||
// reset per-state state, sample any randomness) and a "step" action
|
||||
// (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
|
||||
// the body of the main loop will be replaced by a raster IRQ handler
|
||||
// that calls game_step() on line 311. Either way, game_step() is the
|
||||
// only thing that needs to run "once per frame".
|
||||
// In Phase 4 the main loop was `while (1) game_step();` and the
|
||||
// per-state "frame counter" was just a count of how many times
|
||||
// game_step had been called since entering the current state — so
|
||||
// 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 "screens.h"
|
||||
#include "input.h"
|
||||
#include "score.h"
|
||||
#include <c64/vic.h>
|
||||
#include <c64/sid.h>
|
||||
|
||||
// --- globals ------------------------------------------------------------
|
||||
|
||||
// Current state. Set by game_init() and by the per-state step
|
||||
// functions when a transition is triggered. Read by the switch in
|
||||
// game_step().
|
||||
static byte state;
|
||||
|
||||
// Frame counter for the current state. Reset to 0 by each per-state
|
||||
// "enter" function. Compared against per-state constants in the step
|
||||
// functions (60 for READY, 100 for WAIT and WIN, 300 for GAMEOVER,
|
||||
// 500 for the DRAW fault timeout). unsigned short is enough for
|
||||
// 65535 frames = ~22 minutes at 50 Hz; the longest single state is
|
||||
// 500 frames (DRAW fault), so 16 bits is more than enough.
|
||||
static unsigned short frame;
|
||||
// Per-state "entered at" timestamp. Set by each per-state enter
|
||||
// function to the current frame_count. The per-state step functions
|
||||
// compute elapsed = frame_count - enter_frame to decide when to
|
||||
// transition. 16 bits is enough for 65535 frames = ~22 minutes at
|
||||
// 50 Hz; the longest single state is 500 frames (DRAW fault), so 16
|
||||
// bits is more than enough for a single state.
|
||||
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 ----------------------------------------
|
||||
//
|
||||
@@ -56,237 +78,305 @@ static unsigned short title_first_frame;
|
||||
// entry to DRAW (in game_enter_draw()).
|
||||
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 -----------------------------------------
|
||||
//
|
||||
// Each "enter" function:
|
||||
// - calls show_screen() (or show_white_screen() for DRAW)
|
||||
// - 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)
|
||||
// - sets the border color (TITLE flashes it; DRAW is all-white;
|
||||
// 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)
|
||||
{
|
||||
show_screen(SCREEN_TITLE);
|
||||
score_render();
|
||||
frame = 0;
|
||||
title_input = TITLE_IDLE;
|
||||
title_first_frame = 0;
|
||||
// Border starts white (the "PRESS FIRE" prompt is visible).
|
||||
vic.color_border = 1;
|
||||
show_screen(SCREEN_TITLE);
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
title_input = TITLE_IDLE;
|
||||
title_first_frame = 0;
|
||||
// Border starts white (the "PRESS FIRE" prompt is visible).
|
||||
vic.color_border = 1;
|
||||
}
|
||||
|
||||
static void game_enter_ready(void)
|
||||
{
|
||||
show_screen(SCREEN_WAITING1);
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 0;
|
||||
show_screen(SCREEN_WAITING1);
|
||||
score_render();
|
||||
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;
|
||||
}
|
||||
|
||||
static void game_enter_wait(void)
|
||||
{
|
||||
show_screen(SCREEN_WAITING2);
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 0;
|
||||
show_screen(SCREEN_WAITING2);
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
vic.color_border = 0;
|
||||
}
|
||||
|
||||
static void game_enter_draw(void)
|
||||
{
|
||||
show_white_screen();
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 1; // white border matches the white screen
|
||||
draw_was_pressed[0] = 0;
|
||||
draw_was_pressed[1] = 0;
|
||||
show_white_screen();
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
vic.color_border = 1; // white border matches the white screen
|
||||
draw_was_pressed[0] = 0;
|
||||
draw_was_pressed[1] = 0;
|
||||
}
|
||||
|
||||
static void game_enter_win_p1(void)
|
||||
{
|
||||
show_screen(SCREEN_WIN_HARE);
|
||||
score_p1++;
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 0;
|
||||
show_screen(SCREEN_WIN_HARE);
|
||||
score_p1++;
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
vic.color_border = 0;
|
||||
}
|
||||
|
||||
static void game_enter_win_p2(void)
|
||||
{
|
||||
show_screen(SCREEN_WIN_SCOOT);
|
||||
score_p2++;
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 0;
|
||||
show_screen(SCREEN_WIN_SCOOT);
|
||||
score_p2++;
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
vic.color_border = 0;
|
||||
}
|
||||
|
||||
static void game_enter_gameover(void)
|
||||
{
|
||||
// Show the title screen, but with the scores reset to 0/0 (the
|
||||
// score bar makes this visually obvious: it's the title screen
|
||||
// with an empty score bar).
|
||||
show_screen(SCREEN_TITLE);
|
||||
score_p1 = 0;
|
||||
score_p2 = 0;
|
||||
score_render();
|
||||
frame = 0;
|
||||
vic.color_border = 0;
|
||||
// Show the title screen, but with the scores reset to 0/0 (the
|
||||
// score bar makes this visually obvious: it's the title screen
|
||||
// with an empty score bar).
|
||||
show_screen(SCREEN_TITLE);
|
||||
score_p1 = 0;
|
||||
score_p2 = 0;
|
||||
score_render();
|
||||
enter_frame = frame_count;
|
||||
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 ------------------------------------------
|
||||
|
||||
static void game_step_title(void)
|
||||
{
|
||||
// Flash the border at 25 Hz (toggle every 2 frames at 50 Hz).
|
||||
// The visible effect is a 12.5 Hz blink on the border around the
|
||||
// title screen image. Phase 8 will replace this with actual
|
||||
// "PRESS FIRE" text rendered into the bitmap.
|
||||
if ((frame & 1) == 0)
|
||||
vic.color_border ^= 1;
|
||||
// Flash the border at 25 Hz (toggle every 2 frames at 50 Hz).
|
||||
// The visible effect is a 12.5 Hz blink on the border around the
|
||||
// title screen image. Phase 8 will replace this with actual
|
||||
// "PRESS FIRE" text rendered into the bitmap.
|
||||
unsigned short elapsed = frame_count - enter_frame;
|
||||
if ((elapsed & 1) == 0)
|
||||
vic.color_border ^= 1;
|
||||
|
||||
char p1 = input_fire(1); // Hare (port 1)
|
||||
char p2 = input_fire(0); // Scoot (port 0)
|
||||
char both = p1 && p2;
|
||||
char none = !p1 && !p2;
|
||||
char p1 = input_fire(1); // Hare (port 1)
|
||||
char p2 = input_fire(0); // Scoot (port 0)
|
||||
char both = p1 && p2;
|
||||
char none = !p1 && !p2;
|
||||
|
||||
switch (title_input) {
|
||||
case TITLE_IDLE:
|
||||
if (both) {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
} else if (p1 || p2) {
|
||||
title_input = TITLE_FIRST_HELD;
|
||||
title_first_frame = frame;
|
||||
}
|
||||
break;
|
||||
switch (title_input) {
|
||||
case TITLE_IDLE:
|
||||
if (both) {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
} else if (p1 || p2) {
|
||||
title_input = TITLE_FIRST_HELD;
|
||||
title_first_frame = frame_count;
|
||||
}
|
||||
break;
|
||||
|
||||
case TITLE_FIRST_HELD:
|
||||
if (both) {
|
||||
if (frame - title_first_frame <= 8) {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
} else {
|
||||
title_input = TITLE_WAIT_RELEASE;
|
||||
}
|
||||
} else if (none) {
|
||||
title_input = TITLE_IDLE;
|
||||
} else if (frame - title_first_frame > 8) {
|
||||
// Held too long without the other button following.
|
||||
title_input = TITLE_WAIT_RELEASE;
|
||||
}
|
||||
// else: one still held, other not yet, within 8 frames.
|
||||
break;
|
||||
case TITLE_FIRST_HELD:
|
||||
if (both) {
|
||||
if (frame_count - title_first_frame <= 8) {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
} else {
|
||||
title_input = TITLE_WAIT_RELEASE;
|
||||
}
|
||||
} else if (none) {
|
||||
title_input = TITLE_IDLE;
|
||||
} else if (frame_count - title_first_frame > 8) {
|
||||
// Held too long without the other button following.
|
||||
title_input = TITLE_WAIT_RELEASE;
|
||||
}
|
||||
// else: one still held, other not yet, within 8 frames.
|
||||
break;
|
||||
|
||||
case TITLE_WAIT_RELEASE:
|
||||
if (none) {
|
||||
title_input = TITLE_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TITLE_WAIT_RELEASE:
|
||||
if (none) {
|
||||
title_input = TITLE_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_ready(void)
|
||||
{
|
||||
if (frame >= 60) {
|
||||
state = STATE_WAIT;
|
||||
game_enter_wait();
|
||||
}
|
||||
if (frame_count - enter_frame >= 60) {
|
||||
state = STATE_WAIT;
|
||||
game_enter_wait();
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_wait(void)
|
||||
{
|
||||
// Phase 4: fixed 100 frames. Phase 5: random 100-250 from
|
||||
// PEEK(0xD41B) sampled on READY enter.
|
||||
if (frame >= 100) {
|
||||
state = STATE_DRAW;
|
||||
game_enter_draw();
|
||||
}
|
||||
// Random 100..250 frames, sampled in game_enter_ready. We
|
||||
// intentionally do NOT read input here — pressing fire during
|
||||
// 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;
|
||||
game_enter_draw();
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_draw(void)
|
||||
{
|
||||
// 500-frame fault timeout (10 sec at 50 Hz). If neither player
|
||||
// fires in 10 sec, abort the round and go back to TITLE.
|
||||
if (frame > 500) {
|
||||
state = STATE_TITLE;
|
||||
game_enter_title();
|
||||
return;
|
||||
}
|
||||
unsigned short elapsed = frame_count - enter_frame;
|
||||
|
||||
// First to fire wins. Rising-edge detection so holding fire from
|
||||
// before DRAW doesn't auto-trigger a win (e.g. if the player
|
||||
// presses during WAIT and keeps it held). P1 wins ties (matches
|
||||
// the "fire1 first" branch in GAME.md §9).
|
||||
char now_p1 = input_fire(1);
|
||||
char now_p2 = input_fire(0);
|
||||
char edge_p1 = now_p1 && !draw_was_pressed[0];
|
||||
char edge_p2 = now_p2 && !draw_was_pressed[1];
|
||||
draw_was_pressed[0] = now_p1;
|
||||
draw_was_pressed[1] = now_p2;
|
||||
// 500-frame fault timeout (10 sec at 50 Hz). If neither player
|
||||
// fires in 10 sec, abort the round, trigger a short stinger on
|
||||
// SID voice 1, and go back to TITLE. No point awarded.
|
||||
if (elapsed > 500) {
|
||||
game_trigger_fault_stinger();
|
||||
state = STATE_TITLE;
|
||||
game_enter_title();
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge_p1) {
|
||||
state = STATE_WIN_P1;
|
||||
game_enter_win_p1();
|
||||
} else if (edge_p2) {
|
||||
state = STATE_WIN_P2;
|
||||
game_enter_win_p2();
|
||||
}
|
||||
// First to fire wins. Rising-edge detection so holding fire from
|
||||
// before DRAW doesn't auto-trigger a win (e.g. if the player
|
||||
// presses during WAIT and keeps it held). P1 wins ties (matches
|
||||
// the "fire1 first" branch in GAME.md §9).
|
||||
char now_p1 = input_fire(1);
|
||||
char now_p2 = input_fire(0);
|
||||
char edge_p1 = now_p1 && !draw_was_pressed[0];
|
||||
char edge_p2 = now_p2 && !draw_was_pressed[1];
|
||||
draw_was_pressed[0] = now_p1;
|
||||
draw_was_pressed[1] = now_p2;
|
||||
|
||||
if (edge_p1) {
|
||||
state = STATE_WIN_P1;
|
||||
game_enter_win_p1();
|
||||
} else if (edge_p2) {
|
||||
state = STATE_WIN_P2;
|
||||
game_enter_win_p2();
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_win_p1(void)
|
||||
{
|
||||
if (frame >= 100) {
|
||||
if (score_p1 >= 5) {
|
||||
state = STATE_GAMEOVER;
|
||||
game_enter_gameover();
|
||||
} else {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
}
|
||||
}
|
||||
if (frame_count - enter_frame >= 100) {
|
||||
if (score_p1 >= 5) {
|
||||
state = STATE_GAMEOVER;
|
||||
game_enter_gameover();
|
||||
} else {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_win_p2(void)
|
||||
{
|
||||
if (frame >= 100) {
|
||||
if (score_p2 >= 5) {
|
||||
state = STATE_GAMEOVER;
|
||||
game_enter_gameover();
|
||||
} else {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
}
|
||||
}
|
||||
if (frame_count - enter_frame >= 100) {
|
||||
if (score_p2 >= 5) {
|
||||
state = STATE_GAMEOVER;
|
||||
game_enter_gameover();
|
||||
} else {
|
||||
state = STATE_READY;
|
||||
game_enter_ready();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void game_step_gameover(void)
|
||||
{
|
||||
if (frame >= 300) {
|
||||
state = STATE_TITLE;
|
||||
game_enter_title();
|
||||
}
|
||||
if (frame_count - enter_frame >= 300) {
|
||||
state = STATE_TITLE;
|
||||
game_enter_title();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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)
|
||||
{
|
||||
state = STATE_TITLE;
|
||||
game_enter_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();
|
||||
}
|
||||
|
||||
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) {
|
||||
case STATE_TITLE: game_step_title(); break;
|
||||
case STATE_READY: game_step_ready(); break;
|
||||
case STATE_WAIT: game_step_wait(); break;
|
||||
case STATE_DRAW: game_step_draw(); break;
|
||||
case STATE_WIN_P1: game_step_win_p1(); break;
|
||||
case STATE_WIN_P2: game_step_win_p2(); break;
|
||||
case STATE_GAMEOVER: game_step_gameover(); break;
|
||||
}
|
||||
switch (state) {
|
||||
case STATE_TITLE: game_step_title(); break;
|
||||
case STATE_READY: game_step_ready(); break;
|
||||
case STATE_WAIT: game_step_wait(); break;
|
||||
case STATE_DRAW: game_step_draw(); break;
|
||||
case STATE_WIN_P1: game_step_win_p1(); break;
|
||||
case STATE_WIN_P2: game_step_win_p2(); break;
|
||||
case STATE_GAMEOVER: game_step_gameover(); break;
|
||||
}
|
||||
}
|
||||
|
||||
+35
-14
@@ -8,7 +8,8 @@
|
||||
//
|
||||
// STATE_TITLE — title screen, flashing border, wait for both fire
|
||||
// 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
|
||||
// fault timeout)
|
||||
// STATE_WIN_P1 — "Hare won" screen, 100 frames, +1 to player 1
|
||||
@@ -19,7 +20,7 @@
|
||||
//
|
||||
// TITLE --both fire (within 8 frames)--> READY
|
||||
// READY --60 frames--------------------> WAIT
|
||||
// WAIT --100 frames-------------------> DRAW
|
||||
// WAIT --random 100..250 frames------> DRAW
|
||||
// DRAW --port 1 fire (rising edge)---> WIN_P1
|
||||
// DRAW --port 0 fire (rising edge)---> WIN_P2
|
||||
// DRAW --500 frames (fault)----------> TITLE
|
||||
@@ -28,6 +29,13 @@
|
||||
// WIN_P2 --100 frames + score < 5-----> READY
|
||||
// WIN_P2 --100 frames + score == 5----> GAMEOVER
|
||||
// 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_READY 1
|
||||
@@ -37,23 +45,36 @@
|
||||
#define STATE_WIN_P2 5
|
||||
#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.
|
||||
// Call once at startup, after memmap_setup() and score_init().
|
||||
// Calls show_screen(SCREEN_TITLE) and score_render() as part of the
|
||||
// TITLE entry action.
|
||||
// Call once at startup, after memmap_setup() and score_init() and
|
||||
// rasterirq_setup(). Calls show_screen(SCREEN_TITLE) and
|
||||
// score_render() as part of the TITLE entry action.
|
||||
void game_init(void);
|
||||
|
||||
// game_step() — advance the state machine by one frame.
|
||||
//
|
||||
// In Phase 4 the main loop calls this in a tight busy-wait. In
|
||||
// Phase 5 a raster IRQ handler at line 311 will call it at 50 Hz.
|
||||
// Per-state step actions read both fire buttons, advance the per-state
|
||||
// frame counter, and trigger state transitions (each transition calls
|
||||
// the new state's "enter" action immediately, so the next step
|
||||
// operates on the new state).
|
||||
// game_step() — advance the state machine by one frame. Called from
|
||||
// the raster IRQ handler at line 311. Per-state step actions read
|
||||
// both fire buttons, compute elapsed frames against the per-state
|
||||
// enter_frame timestamp, and trigger state transitions (each
|
||||
// transition calls 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
|
||||
// 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);
|
||||
|
||||
#pragma compile("game.c")
|
||||
|
||||
+18
-13
@@ -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:
|
||||
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
|
||||
@@ -6,21 +6,23 @@
|
||||
// 3. game_init() — enter the TITLE state (which also loads
|
||||
// the title screen and renders the score
|
||||
// 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
|
||||
// tight loop with no real timing. The "frame counter" inside game.c
|
||||
// is just a count of how many times game_step() has been called, so
|
||||
// the state durations are CPU-bound, not wall-clock-bound.
|
||||
//
|
||||
// In Phase 5 the busy-wait body of the main loop becomes a raster
|
||||
// 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.
|
||||
// In Phase 4 step 5 was `while (1) game_step();` — a busy-wait that
|
||||
// called game_step as fast as the CPU could, so the "frame counter"
|
||||
// was CPU-bound. In Phase 5 the busy-wait is gone: game_step is
|
||||
// called from the raster IRQ at exactly 50 Hz, so state durations
|
||||
// are wall-clock-bound (60 frames = 1.2 sec, etc.) regardless of
|
||||
// what the CPU is doing between IRQs.
|
||||
|
||||
#include "memmap.h"
|
||||
#include "game.h"
|
||||
#include "score.h"
|
||||
#include "tick.h"
|
||||
|
||||
// We don't malloc, so the heap is unused. Setting it to 0 frees the
|
||||
// space for the screen data in the main region.
|
||||
@@ -31,9 +33,12 @@ int main(void)
|
||||
memmap_setup();
|
||||
score_init();
|
||||
game_init();
|
||||
rasterirq_setup();
|
||||
|
||||
while (1)
|
||||
game_step();
|
||||
// The raster IRQ does all the per-frame work. The main loop
|
||||
// is a deliberate spin: nothing to do between IRQs.
|
||||
for (;;)
|
||||
;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+125
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user