Phase 8: polish and end-to-end test

Banner text rendering (PRESS FIRE / HARE WINS! / SCOOT WINS!):
- src/banner.c, src/banner.h: new module; renders NUL-terminated
  ASCII text in any character row of the multicolor bitmap, using
  the same 4x8 custom font as the score bar.  Truncates to 40 chars,
  centers in the row, clears the row first.
- src/score.c, src/score.h: font extended from 8 to 16 entries —
  added P, F, W, I, N, !, space (and one reserved).  font_lookup()
  maps ASCII to font index.
- src/game.c: TITLE renders 'PRESS FIRE' in row 1 below the score
  bar; GAMEOVER renders 'HARE WINS!' or 'SCOOT WINS!' based on
  last_winner (set by game_enter_win_p1/p2).  Other states clear
  row 1 on entry.

State transition stinger:
- src/audio.c, src/audio.h: audio_play_stinger(voice, duration)
  starts a low square-wave burst that auto-cleans up via
  audio_advance_stinger (called from audio_state_step).  A 5-frame
  stinger is fired on every state transition; the per-state voice
  is chosen to avoid colliding with the new state's audio
  (TITLE/READY: voice 0/1 free, WAIT: voice 2 free, DRAW: voice 0
  free, WIN/GAMEOVER: voice 2 free).  Stinger is silenced by the
  next audio_state_enter() via the existing audio_stop() call.

TITLE border flash changed from 12.5 Hz to 1 Hz (50 on, 50 off).

Build system:
- src/build.sh: added -O0/-O1/-O2/-O3/-Os/-g flag handling.  Both
  default (-O1) and -O3 builds produce a 43913-byte .prg
  (well under the 51308-byte LOAD"*",8,1 limit).
- src/main.c: pinned #pragma stacksize(0x400) — the oscar64 default
  is the same, but pinning makes the layout predictable across
  optimization levels.  -O3 needs this exact size; larger values
  cause 'Cannot place stack section' link errors because the
  optimizer's larger code section leaves less room in the
  stack/heap gap.
- src/tick.c: marked frame_tick_handler __noinline so -O3 doesn't
  inline the entire state machine (6000+ bytes) into the IRQ
  handler.  With __noinline, the handler is 136 bytes — small
  enough for the raster line budget.
- src/game.h: marked game_step __noinline for the same reason.

The .prg is 173 blocks (out of 202 max), well within the BASIC
load area.  Both default and -O3 builds run cleanly in the oscar64
built-in emulator.
This commit is contained in:
Whack Hare Agent
2026-07-17 02:55:16 +02:00
parent a8ce56464d
commit b7bd76dee3
11 changed files with 464 additions and 31 deletions
+9 -1
View File
@@ -67,7 +67,15 @@ static RIRQCode frame_tick;
// 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)
//
// Also marked __noinline to prevent `-O3` from inlining the entire
// state machine (game_step + audio_state_step + all per-state step
// functions) into the IRQ handler. Without __noinline, the
// optimizer's aggressive inlining makes the handler 6000+ bytes
// long, which would overrun its raster-line budget and break the
// 50 Hz timing. game_step is large but called only from the IRQ,
// so the call/return overhead is negligible.
__interrupt __noinline void frame_tick_handler(void)
{
frame_count++;
game_step();