Phase 4: state machine skeleton (TITLE→READY→WAIT→DRAW→WIN)

Implement the full state machine from GAME.md §9 with no audio yet,
fixed (not random) durations, and the basic score-update + reset logic
needed to drive the GAMEOVER transition.

src/game.h / src/game.c:
  - enum: STATE_TITLE, STATE_READY, STATE_WAIT, STATE_DRAW,
    STATE_WIN_P1, STATE_WIN_P2, STATE_GAMEOVER
  - byte state global + game_init() / game_step() pair
  - per-state enter (show_screen + score_render + reset frame counter)
    and step (check inputs, advance counter, transition) functions
  - TITLE: flash border at 25 Hz, debounce both-fire-within-8-frames
  - READY: 60 frames, WAIT: 100, DRAW: 500-frame fault timeout,
    WIN: 100 frames, GAMEOVER: 300 frames
  - DRAW uses rising-edge detection so holding fire from before
    DRAW doesn't auto-trigger a win
  - WIN_P1/WIN_P2 increment the score and re-render the score bar
  - GAMEOVER shows the title screen with scores reset to 0/0

src/screens.c / src/screens.h:
  - show_screen(n) now handles all 5 SCREEN_* IDs via a per-screen
    descriptor table (5 × 8000-byte bitmaps + 5 × 1000-byte attrs)
  - LZO-compress the 8000-byte bitmaps at build time so the 5 screens
    fit in the main region; the 5 .attr tables go in a custom 'screens'
    region at $A000-$C000 (BASIC ROM area, banked out as RAM)
  - new show_white_screen() fills the bitmap with 0s, sets $D021/
    $D022/$D023/$D020 to white, for the all-white DRAW screen

src/main.c:
  - simplified to: memmap_setup → score_init → game_init → while (1)
    game_step()
  - sets #pragma heapsize(0) since the program doesn't use malloc

Build: ./build.sh -e — .prg is 43.9 KB, well under the 51 KB BASIC
load limit. The oscar64 emulator runs the state machine indefinitely
(both fire buttons read as pressed in the emulator, so the game
cycles through the states); no crash.

Memory layout (from .map):
  $0801-$0853  startup
  $0880-$0E51  code (1489 bytes)
  $0E51-$7FE4  data: 5 LZO bitmaps + small tables (28723 bytes)
  $7FE4-$7FEC  BSS: state, frame, title_input, etc.
  $7FF0-$9000  heap (16 bytes)
  $9000-$A000  stack
  $A000-$B388  custom 'screens' region: 5 .attr tables (5000 bytes)
This commit is contained in:
ballz
2026-07-17 01:50:03 +02:00
parent 9f086c3a4f
commit 65a2e5d2a1
5 changed files with 602 additions and 113 deletions
+292
View File
@@ -0,0 +1,292 @@
// game.c — Whack Hare! state machine.
//
// 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).
//
// 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".
#include "game.h"
#include "screens.h"
#include "input.h"
#include "score.h"
#include <c64/vic.h>
// 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;
// --- TITLE-specific input state ----------------------------------------
//
// The TITLE state waits for "both fire buttons pressed simultaneously".
// The de-bounce rule (GAME.md §9): the two buttons must be pressed
// within 8 frames of each other; if not, the game requires both
// buttons to be released before re-trying. The protocol:
//
// IDLE -> on (both pressed at once) -> READY
// -> on (one pressed) -> FIRST_HELD
// FIRST_HELD -> on (other pressed, within 8 fr) -> READY
// -> on (timeout reached) -> WAIT_RELEASE
// -> on (both released) -> IDLE
// WAIT_RELEASE -> on (both released) -> IDLE
#define TITLE_IDLE 0
#define TITLE_FIRST_HELD 1
#define TITLE_WAIT_RELEASE 2
static byte title_input;
static unsigned short title_first_frame;
// --- DRAW-specific input state -----------------------------------------
//
// The DRAW state needs rising-edge detection: holding fire from before
// DRAW starts (e.g. during WAIT) must not auto-trigger a win. We
// track the previous frame's state for each port. Initialized on
// entry to DRAW (in game_enter_draw()).
static char draw_was_pressed[2];
// --- per-state enter functions -----------------------------------------
//
// Each "enter" function:
// - calls show_screen() (or show_white_screen() for DRAW)
// - calls score_render()
// - resets `frame` to 0
// - 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)
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;
}
static void game_enter_ready(void)
{
show_screen(SCREEN_WAITING1);
score_render();
frame = 0;
vic.color_border = 0;
}
static void game_enter_wait(void)
{
show_screen(SCREEN_WAITING2);
score_render();
frame = 0;
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;
}
static void game_enter_win_p1(void)
{
show_screen(SCREEN_WIN_HARE);
score_p1++;
score_render();
frame = 0;
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;
}
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;
}
// --- 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;
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;
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_WAIT_RELEASE:
if (none) {
title_input = TITLE_IDLE;
}
break;
}
}
static void game_step_ready(void)
{
if (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();
}
}
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;
}
// 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();
}
}
}
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();
}
}
}
static void game_step_gameover(void)
{
if (frame >= 300) {
state = STATE_TITLE;
game_enter_title();
}
}
// --- public API --------------------------------------------------------
void game_init(void)
{
state = STATE_TITLE;
game_enter_title();
}
void game_step(void)
{
frame++;
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;
}
}
+61
View File
@@ -0,0 +1,61 @@
#ifndef WHACK_HARE_GAME_H
#define WHACK_HARE_GAME_H
// game.h — Whack Hare! state machine.
//
// States (see game.c for the per-state behavior and the transition
// diagram from GAME.md §9):
//
// 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_DRAW — white screen, wait for first fire (or 500-frame
// fault timeout)
// STATE_WIN_P1 — "Hare won" screen, 100 frames, +1 to player 1
// STATE_WIN_P2 — "Scoot won" screen, 100 frames, +1 to player 2
// STATE_GAMEOVER — title screen with scores reset, 300 frames
//
// Transitions:
//
// TITLE --both fire (within 8 frames)--> READY
// READY --60 frames--------------------> WAIT
// WAIT --100 frames-------------------> DRAW
// DRAW --port 1 fire (rising edge)---> WIN_P1
// DRAW --port 0 fire (rising edge)---> WIN_P2
// DRAW --500 frames (fault)----------> TITLE
// WIN_P1 --100 frames + score < 5-----> READY
// WIN_P1 --100 frames + score == 5----> GAMEOVER
// WIN_P2 --100 frames + score < 5-----> READY
// WIN_P2 --100 frames + score == 5----> GAMEOVER
// GAMEOVER --300 frames (scores reset)--> TITLE
#define STATE_TITLE 0
#define STATE_READY 1
#define STATE_WAIT 2
#define STATE_DRAW 3
#define STATE_WIN_P1 4
#define STATE_WIN_P2 5
#define STATE_GAMEOVER 6
// 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.
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() also drives per-state visual updates that need to run
// every frame (currently just the TITLE border flash at 25 Hz).
void game_step(void);
#pragma compile("game.c")
#endif
+23 -23
View File
@@ -1,39 +1,39 @@
// main.c — Whack Hare! entry point (Phase 3: score bar). // main.c — Whack Hare! entry point (Phase 4: state machine skeleton).
// //
// Flow: // Flow:
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM. // 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
// 2. show_screen(TITLE) — copy the title bitmap into $E000 and // 2. score_init() — zero both player scores.
// configure the VIC for multicolor bitmap // 3. game_init() — enter the TITLE state (which also loads
// mode. // the title screen and renders the score
// 3. score_init() — zero both player scores. // bar).
// 4. score_render() — overlay the score bar on the top 8 rows. // 4. while (1) game_step() — the state machine runs forever.
// 5. Loop: poll joystick port 1 for fire. When pressed, restore
// the C64's default memory config and return from main() (which
// ends the program).
// //
// Build: ./build.sh (compile to src/build/whack_hare.prg) // In Phase 4 this is a busy-wait loop: game_step() is called in a
// Run: ./build.sh -e (headless, in oscar64's built-in emulator) // tight loop with no real timing. The "frame counter" inside game.c
// ./build.sh -v (VICE x64, interactive, needs a display) // 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.
#include "memmap.h" #include "memmap.h"
#include "screens.h" #include "game.h"
#include "input.h"
#include "score.h" #include "score.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.
#pragma heapsize(0)
int main(void) int main(void)
{ {
memmap_setup(); memmap_setup();
show_screen(SCREEN_TITLE);
score_init(); score_init();
score_render(); game_init();
// Phase 3: just poll for fire on port 1 (the Hare / right joystick). while (1)
// In Phase 4 this becomes a 50 Hz raster IRQ handler that drives the game_step();
// full state machine.
while (!input_fire(1))
;
memmap_restore();
return 0; return 0;
} }
+182 -71
View File
@@ -1,74 +1,131 @@
#include "screens.h" #include "screens.h"
#include <c64/vic.h> #include <c64/vic.h>
#include <c64/cia.h> #include <c64/cia.h>
#include <oscar.h>
#include <string.h> #include <string.h>
// Embedded screen data. In Phase 2 only the title screen is wired in; // --- memory layout for the embedded screen data ------------------------
// the other 4 will be added in Phase 4 (state machine) when we need
// them. The arrays are const so they live in the ROM/load-image part
// of the .prg, not in BSS.
// //
// Screen memory layout (in multicolor bitmap mode) per // The 5 LZO-compressed bitmaps are ~28 KB total (5500-6100 bytes per
// cebix-vic-article §3.7.3.4: // screen). The 5 .attr tables are 5 KB total (1000 bytes each, kept
// "00" pixel -> $D021 (background, per-screen) // uncompressed because they don't compress well). Together that's
// "01" pixel -> high nibble of screen memory byte // ~33 KB, more than the default main region (~34 KB) can comfortably
// "10" pixel -> low nibble of screen memory byte // hold alongside code (~700 B), BSS (~50 B), stack, and heap.
// "11" pixel -> color RAM nibble (we set to 0 = black)
// //
// convert_screens.py's .attr file stores the screen memory byte: // So we split the data across two regions:
// bits 0-3 = "10" color //
// bits 4-7 = "01" color // 1. LZO bitmaps in the main region (default data section, $0880-
// (the "11" color is not stored; color RAM defaults to black at boot). // $9000). They're read-only, used once per state transition, and
// they go right after the code so the linker can pack them
// tightly.
//
// 2. .attr tables in a custom "screens" region at $A000-$C000 (the
// BASIC ROM area, which is banked out as RAM after
// memmap_setup() and is not used by the trampoline). This is
// 8 KB, more than enough for 5 × 1000 = 5 KB of attr data.
//
// The trampoline is in the code section (around $08B3-$08FD), not at
// $A000+ — the $A000+ entries in the .map are sstack (subroutine
// stack) declarations of size 0, not actual data. So the entire
// $A000-$C000 range is free for our use.
//
// We don't need a heap for this program (no malloc). We set
// heapsize(0) below in main.c to maximize the room available for data.
// --- embedded bitmap data (LZO-compressed, in main region) ------------
const char ScreenTitleBin[] = { const char ScreenTitleBin[] = {
#embed "data/processed/title.bin" #embed 8000 0 lzo "data/processed/title.bin"
}; };
const char ScreenWaiting1Bin[] = {
#embed 8000 0 lzo "data/processed/waiting1.bin"
};
const char ScreenWaiting2Bin[] = {
#embed 8000 0 lzo "data/processed/waiting2.bin"
};
const char ScreenWinHareBin[] = {
#embed 8000 0 lzo "data/processed/win_hare.bin"
};
const char ScreenWinScootBin[] = {
#embed 8000 0 lzo "data/processed/win_scoot.bin"
};
// --- embedded .attr data (uncompressed, in custom "screens" region) ---
#pragma section( screens, 0)
#pragma region( screens, 0xA000, 0xC000, , , {screens} )
#pragma data(screens)
const char ScreenTitleAttr[] = { const char ScreenTitleAttr[] = {
#embed "data/processed/title.attr" #embed "data/processed/title.attr"
}; };
// The $D021 background color for the title screen. Generated by const char ScreenWaiting1Attr[] = {
// tools/convert_screens.py as a separate .d021 sidecar, but the value #embed "data/processed/waiting1.attr"
// is a single small integer (0..15) and we hardcode it here rather };
// than read the file at runtime. When we add the other 4 screens in
// Phase 4 we'll fold the d021 value into a per-screen descriptor
// struct so the compiler can't constant-fold it.
#define SCREEN_TITLE_D021 0
// Copy `len` bytes from `src` to `dst`. const char ScreenWaiting2Attr[] = {
#embed "data/processed/waiting2.attr"
};
const char ScreenWinHareAttr[] = {
#embed "data/processed/win_hare.attr"
};
const char ScreenWinScootAttr[] = {
#embed "data/processed/win_scoot.attr"
};
#pragma data(data)
// --- per-screen descriptor ---------------------------------------------
// //
// Both source and destination are 16-bit-addressed memory regions in // d021 is the $D021 background color value generated by
// the C64. Using a simple byte-by-byte copy (rather than the c64 // tools/convert_screens.py as a .d021 sidecar file. We inline the
// memcpy helper, which is in the runtime and pulls in more code) keeps // value here rather than reading the file at runtime:
// the .prg small. The compiler turns this into a tight loop; for // title=0 (black), waiting1=0 (black), waiting2=11 (dark grey),
// 8000 bytes that's well under a frame at 1 MHz. // win_hare=0 (black), win_scoot=0 (black).
struct ScreenDef {
const char *lzo; // LZO-compressed bitmap
const char *attr; // raw screen memory (1000 bytes)
byte d021; // background color
};
static const struct ScreenDef screens[5] = {
{ ScreenTitleBin, ScreenTitleAttr, 0 }, // SCREEN_TITLE
{ ScreenWaiting1Bin, ScreenWaiting1Attr, 0 }, // SCREEN_WAITING1
{ ScreenWaiting2Bin, ScreenWaiting2Attr, 11 }, // SCREEN_WAITING2
{ ScreenWinHareBin, ScreenWinHareAttr, 0 }, // SCREEN_WIN_HARE
{ ScreenWinScootBin, ScreenWinScootAttr, 0 }, // SCREEN_WIN_SCOOT
};
// --- helpers -----------------------------------------------------------
// Copy `len` bytes from `src` to `dst`. Used for the .attr data
// (1000 bytes) and for the show_white_screen() bitmap clear. The
// compiler turns this into a tight loop; for 1000 bytes that's well
// under a frame at 1 MHz.
static void copy_bytes(const char *src, char *dst, unsigned len) static void copy_bytes(const char *src, char *dst, unsigned len)
{ {
for (unsigned i = 0; i < len; i++) for (unsigned i = 0; i < len; i++)
dst[i] = src[i]; dst[i] = src[i];
} }
void show_screen(int n) // Clear the color RAM at $D800-$DBFF (4 pages × 256 bytes, slightly
{ // more than the 1000-byte logical range $D800-$DBE7; the extra 24
// 1. Write the new pixel/attribute data into the destination // bytes are harmless mirrors). Each nibble = 0 = black, which is the
// while the VIC is still in its old mode. This avoids a // default "11" pixel value for cells that don't explicitly set color
// visible garbage frame during the mode switch. // RAM.
// static void clear_color_ram(void)
// 2. Then flip the VIC into multicolor bitmap mode pointing at
// the new data. The VIC's line buffer absorbs the transition
// cleanly enough for our purposes.
if (n == SCREEN_TITLE)
{
copy_bytes(ScreenTitleBin, (char *)0xE000, sizeof(ScreenTitleBin));
copy_bytes(ScreenTitleAttr, (char *)0xD000, sizeof(ScreenTitleAttr));
// Color RAM (the "11" color per cell). We don't have per-cell
// "11" data in the .attr file, so zero it (= black). This
// reduces each cell from 4 to 3 distinct colors but matches
// what convert_screens.py produces.
__asm
{ {
__asm {
lda #0 lda #0
ldx #4 ldx #4
ldy #0 ldy #0
@@ -81,34 +138,88 @@ void show_screen(int n)
dex dex
bne L0 bne L0
} }
}
// Background color ($D021, the "00" color). The title screen // Configure the VIC for multicolor bitmap mode pointing at the data
// uses color 0 (black); per-screen .d021 files are // at $D000 (screen memory) and $E000 (bitmap). Same config for all
// documentation, the value is inlined here. // 5 game screens + the white screen; only the per-screen pixel data
vic.color_back = SCREEN_TITLE_D021; // and per-screen d021 color differ.
vic.color_border = 0; static void vic_setup_mcm(void)
{
// 3. Now flip the VIC into multicolor bitmap mode.
//
// We set the registers explicitly rather than calling
// vic_setmode() so the yscroll/xscroll bits stay 0
// (vic_setmode() hardcodes yscroll=3, which we don't want
// for a non-scrolling display).
//
// ctrl1 = BMM(1) | DEN(1) | RSEL(1) | yscroll(0) = 0x38
// ctrl2 = MCM(1) | CSEL(1) | xscroll(0) = 0x18
//
// CIA2 PRA low 2 bits = 0 -> VIC bank 0 = CPU $C000-$FFFF
// (both our $D000 screen and $E000 bitmap live there).
//
// memptr (D018) = 0x48
// bits 7-4 = (0xD000 >> 6) & 0xF0 = 0x40 -> screen at $D000
// bits 3-1 = (0xE000 >> 10) & 0x0E = 0x08 -> bitmap upper 8K
// bit 0 = 0 -> unused in BMM
vic.ctrl1 = VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL; vic.ctrl1 = VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL;
vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL; vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL;
cia2.pra = (cia2.pra & 0xfc) | 0x00; cia2.pra = (cia2.pra & 0xfc) | 0x00;
vic.memptr = 0x48; vic.memptr = 0x48;
} }
// Other SCREEN_* IDs: reserved for Phase 4.
// --- public API: show_screen() -----------------------------------------
void show_screen(int n)
{
if (n < 0 || n >= 5) return;
const struct ScreenDef *s = &screens[n];
// 1. Decompress the 8 KB bitmap into $E000-$FFFF and copy the 1 KB
// screen memory into $D000-$D3E7. Both happen while the VIC
// is still in its old mode (or, on the very first call, in
// whatever state memmap_setup() left it). We do the attr
// copy first so the visible region stays coherent for as long
// as possible during the bitmap decompression.
copy_bytes(s->attr, (char *)0xD000, 1000);
oscar_expand_lzo((char *)0xE000, s->lzo);
// 2. Clear color RAM (the "11" color per cell; we don't have
// per-cell "11" data in the .attr files, so it stays 0).
clear_color_ram();
// 3. Set the per-screen colors.
vic.color_back = s->d021;
vic.color_back1 = 0; // unused in the 5 menu screens (no "01" pixels)
vic.color_back2 = 0; // unused in the 5 menu screens (no "10" pixels)
vic.color_back3 = 0;
vic.color_border = 0;
// 4. Flip the VIC into multicolor bitmap mode.
vic_setup_mcm();
}
// --- public API: show_white_screen() -----------------------------------
void show_white_screen(void)
{
// Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is
// a "00" code (which uses $D021). At ~1 byte per 2-3 cycles via
// a simple loop, this is ~5-8 ms, well under a 20 ms frame.
char *p = (char *)0xE000;
for (unsigned i = 0; i < 8000; i++)
p[i] = 0;
// Clear screen memory for tidiness. The top 40 cells are
// overwritten by score_render() right after this returns.
char *sm = (char *)0xD000;
for (unsigned i = 0; i < 1000; i++)
sm[i] = 0;
// Clear color RAM. With the bitmap = 0 there are no "11" pixels,
// so this is technically unnecessary, but doing it keeps the
// score bar cells predictable (score_render() will set the top 40
// cells' color RAM to 1 = white).
clear_color_ram();
// Whole screen white, including the border. $D021 is the "00"
// color and is the only one that matters for the body of the
// screen (bitmap is 0); $D022 and $D023 are set to white too in
// case any stray "01" / "10" pixel ever appears. $D020 is the
// visible border around the bitmap.
vic.color_back = 1; // $D021 = white
vic.color_back1 = 1; // $D022 = white (for any "01" pixel)
vic.color_back2 = 1; // $D023 = white (for any "10" pixel)
vic.color_back3 = 0;
vic.color_border = 1; // $D020 = white border
// Same VIC mode config as show_screen() — the body of the
// bitmap happens to be 0, but the score bar overlay (drawn by
// score_render()) writes to the top 8 rows and expects the VIC
// to be in multicolor bitmap mode.
vic_setup_mcm();
} }
+28 -3
View File
@@ -43,12 +43,37 @@
// memptr (D018) = 0x48 (screen at $D000, bitmap at $E000 within // memptr (D018) = 0x48 (screen at $D000, bitmap at $E000 within
// the selected 16K VIC bank) // the selected 16K VIC bank)
// //
// For Phase 2 only SCREEN_TITLE has embedded data; the other IDs are // Calling show_screen() with an unsupported ID is a no-op.
// reserved for Phase 4 (state machine). Calling show_screen() with an
// unsupported ID is a no-op.
void show_screen(int n); void show_screen(int n);
// show_white_screen() — switch to the DRAW screen: solid white fill
// over the entire visible area (including the score bar, which becomes
// invisible).
//
// Implementation:
// 1. Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is
// a "00" code (which uses $D021).
// 2. Clear the 1 KB screen memory at $D000-$D3E7 (so any leftover
// "01" / "10" cell values are 0, in case the bitmap ever contains
// a non-zero pixel).
// 3. Clear the 1 KB color RAM at $D800-$DBE7.
// 4. Set $D021 (background) = white, $D020 (border) = white. Also
// set $D022 / $D023 to white for safety (they're not used for
// "00" pixels, but any stray "01" / "10" pixel would be black
// otherwise).
// 5. Same VIC mode config as show_screen().
//
// The score bar overlay (drawn by score_render() in game.c) writes
// "00" (black, from $D021) and "11" (white, from color RAM) pixels in
// the top 8 rows. With $D021 and color RAM both white, the entire
// score bar is white and invisible against the white background.
// The bitmap rewrite on the score bar cells is harmless; it just
// produces more white.
//
// Called by game.c on entry to STATE_DRAW.
void show_white_screen(void);
#pragma compile("screens.c") #pragma compile("screens.c")
#endif #endif