Phase 2: display the title screen (mmap, vic, screen swap)

Replace helloworld.c with main.c. Add memmap_setup/restore (banks out
KERNAL+BASIC+CHAR ROMs so $E000-$FFFF is free for the 8 KB bitmap and
$D000-$DFFF is I/O), show_screen(n) which copies the title .bin to
$E000 and the .attr to $D000 (screen memory in multicolor bitmap mode)
then flips the VIC into BMM=1 MCM=1 CSEL=1 RSEL=1 DEN=1 with bitmap
base CB13=1 and screen base VM13-VM10=4 ($D018 = 0x48), and
input_fire(port) which reads bit 4 of CIA1 $DC00/$DC01.

build.sh default target is now main.c -> whack_hare.prg (still
builds helloworld explicitly via the existing -c flag if needed).

Verified: ./build.sh -e runs the title screen in oscar64's built-in
emulator and exits cleanly. .map shows code at $0880-$09A6 and
embedded title data at $09A7-$2CCF, well within the 38 KB main
region. .prg is 9428 bytes, well under the 202-block LOAD"*",8,1
limit.

Concerns for Phase 3 (score bar): the score bar will overlay the top
8 pixel rows of the bitmap ($E000-$E13F, 320 bytes). show_screen()
will continue to copy the full .bin to $E000; score_render() will be
called immediately after and overwrite the top 320 bytes. This keeps
show_screen() dumb and lets the score bar be re-rendered on state
change without re-copying the whole 8 KB bitmap.
This commit is contained in:
ballz
2026-07-17 01:27:28 +02:00
parent 33d628aa65
commit 4d697ad8f1
8 changed files with 285 additions and 12 deletions
+15 -12
View File
@@ -2,17 +2,17 @@
# build.sh — compile and optionally run a single C64 program with Oscar64. # build.sh — compile and optionally run a single C64 program with Oscar64.
# #
# Usage: # Usage:
# ./build.sh # compile helloworld.c → build/helloworld.prg # ./build.sh # compile main.c → build/whack_hare.prg
# ./build.sh -e # run in oscar64's built-in emulator (headless, fast) # ./build.sh -e # run in oscar64's built-in emulator (headless, fast)
# ./build.sh -v # run in VICE x64 (interactive, needs a display) # ./build.sh -v # run in VICE x64 (interactive, needs a display)
# ./build.sh -V # run in VICE x64sc (interactive, cycle-exact, slow) # ./build.sh -V # run in VICE x64sc (interactive, cycle-exact, slow)
# ./build.sh -c # just compile # ./build.sh -c # just compile
# #
# Output (in ./build/): # Output (in ./build/):
# helloworld.prg — loadable C64 program (run with x64, VICE, or real hw) # whack_hare.prg — loadable C64 program (run with x64, VICE, or real hw)
# helloworld.asm — full 6502 listing # whack_hare.asm — full 6502 listing
# helloworld.map — region/section/object placement # whack_hare.map — region/section/object placement
# helloworld.lbl — VICE monitor label commands # whack_hare.lbl — VICE monitor label commands
# #
# For the development loop, use -e (oscar64's built-in emulator). It runs # For the development loop, use -e (oscar64's built-in emulator). It runs
# without a display, needs no ROMs, and is faster than VICE. VICE is for # without a display, needs no ROMs, and is faster than VICE. VICE is for
@@ -46,7 +46,10 @@ if [ ! -x "$OSCAR64_BIN" ]; then
fi fi
# --- compile + optionally run -------------------------------------------- # --- compile + optionally run --------------------------------------------
SRC=helloworld.c # Phase 2+ entry point. helloworld.c is kept around as a minimal sanity
# check; build it explicitly with `./build.sh -c helloworld` (TODO if
# needed). Default is the real game.
SRC=main.c
EMU_CMD="" EMU_CMD=""
for arg in "$@"; do for arg in "$@"; do
@@ -62,15 +65,15 @@ done
echo "compiling $SRC with $OSCAR64_BIN -> $BUILD_DIR/" echo "compiling $SRC with $OSCAR64_BIN -> $BUILD_DIR/"
# -o puts the .prg in the build dir; the other artifacts (.asm, .map, .lbl) # -o puts the .prg in the build dir; the other artifacts (.asm, .map, .lbl)
# follow automatically since they share the base name. # follow automatically since they share the base name.
"$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/helloworld.prg" "$SRC" "$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/whack_hare.prg" "$SRC"
case "$EMU_CMD" in case "$EMU_CMD" in
"") "")
# compile only # compile only
;; ;;
"oscar64") "oscar64")
echo "running helloworld.prg in oscar64's built-in emulator" echo "running whack_hare.prg in oscar64's built-in emulator"
"$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/helloworld.prg" -e "$SRC" "$OSCAR64_BIN" -i="$OSCAR64_DIR/include" -o="$BUILD_DIR/whack_hare.prg" -e "$SRC"
;; ;;
"x64"|"x64sc") "x64"|"x64sc")
if ! command -v "$EMU_CMD" >/dev/null 2>&1; then if ! command -v "$EMU_CMD" >/dev/null 2>&1; then
@@ -81,9 +84,9 @@ case "$EMU_CMD" in
echo "warning: no \$DISPLAY set; VICE may not render correctly" >&2 echo "warning: no \$DISPLAY set; VICE may not render correctly" >&2
echo " for headless testing use -e (oscar64's built-in emulator)" >&2 echo " for headless testing use -e (oscar64's built-in emulator)" >&2
fi fi
echo "running helloworld.prg in VICE ($EMU_CMD)" echo "running whack_hare.prg in VICE ($EMU_CMD)"
"$EMU_CMD" "$BUILD_DIR/helloworld.prg" "$EMU_CMD" "$BUILD_DIR/whack_hare.prg"
;; ;;
esac esac
echo "done: $BUILD_DIR/helloworld.prg" echo "done: $BUILD_DIR/whack_hare.prg"
+9
View File
@@ -0,0 +1,9 @@
#include "input.h"
static const unsigned short cia_port_base[2] = { 0xDC00, 0xDC01 };
char input_fire(int port)
{
volatile char *p = (volatile char *)cia_port_base[port & 1];
return ((*p) & 0x10) == 0;
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef WHACK_HARE_INPUT_H
#define WHACK_HARE_INPUT_H
// input_fire(port) — returns 1 if the fire button on the given joystick
// port is currently pressed, 0 otherwise.
//
// port = 0 → CIA1 PRA at $DC00 (left joystick, "Scoot", player 2)
// port = 1 → CIA1 PRB at $DC01 (right joystick, "Hare", player 1)
//
// Active low: the button is "pressed" when bit 4 of the CIA port is 0.
// The direction bits (0..3) are read but masked off — only the fire
// bit is examined. Bits 5..7 of the CIA ports are timer A/B outputs
// and the paddles, which we ignore here.
//
// No debounce — this is a raw poll. Call it once per frame from a
// raster IRQ or game loop.
char input_fire(int port);
#pragma compile("input.c")
#endif
+33
View File
@@ -0,0 +1,33 @@
// main.c — Whack Hare! entry point (Phase 2: display title screen).
//
// Flow:
// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM.
// 2. show_screen(TITLE) — copy the title bitmap into $E000 and
// configure the VIC for multicolor bitmap
// mode.
// 3. 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)
// Run: ./build.sh -e (headless, in oscar64's built-in emulator)
// ./build.sh -v (VICE x64, interactive, needs a display)
#include "memmap.h"
#include "screens.h"
#include "input.h"
int main(void)
{
memmap_setup();
show_screen(SCREEN_TITLE);
// Phase 2: just poll for fire on port 1 (the Hare / right joystick).
// In Phase 4 this becomes a 50 Hz raster IRQ handler that drives the
// full state machine.
while (!input_fire(1))
;
memmap_restore();
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#include "memmap.h"
void memmap_setup(void)
{
mmap_trampoline();
mmap_set(MMAP_RAM);
mmap_set(MMAP_NO_ROM);
}
void memmap_restore(void)
{
mmap_set(MMAP_ROM);
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef WHACK_HARE_MEMMAP_H
#define WHACK_HARE_MEMMAP_H
#include <c64/memmap.h>
// memmap_setup() — bank out the KERNAL + BASIC + CHAR ROMs so we get
// the $E000-$FFFF region as free RAM for the multicolor bitmap, and
// $D000-$DFFF shows I/O (not the CHAR ROM) so the VIC can be configured
// and color RAM is accessible.
//
// Idempotent: call once at startup, before any VIC config or bitmap
// copy. After this returns, the program code is running out of the
// 38 KB contiguous region at $0900-$A000 (the default Oscar64 main
// region; verified in the .map file).
//
// memmap_restore() puts the C64 back in its power-on memory config.
// Call it just before exit, so a soft-reset / second-run of the
// program behaves the way the user expects (BASIC prompt etc.).
void memmap_setup(void);
void memmap_restore(void);
#pragma compile("memmap.c")
#endif
+114
View File
@@ -0,0 +1,114 @@
#include "screens.h"
#include <c64/vic.h>
#include <c64/cia.h>
#include <string.h>
// Embedded screen data. In Phase 2 only the title screen is wired in;
// 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
// cebix-vic-article §3.7.3.4:
// "00" pixel -> $D021 (background, per-screen)
// "01" pixel -> high nibble of screen memory byte
// "10" pixel -> low nibble of screen memory byte
// "11" pixel -> color RAM nibble (we set to 0 = black)
//
// convert_screens.py's .attr file stores the screen memory byte:
// bits 0-3 = "10" color
// bits 4-7 = "01" color
// (the "11" color is not stored; color RAM defaults to black at boot).
const char ScreenTitleBin[] = {
#embed "data/processed/title.bin"
};
const char ScreenTitleAttr[] = {
#embed "data/processed/title.attr"
};
// The $D021 background color for the title screen. Generated by
// tools/convert_screens.py as a separate .d021 sidecar, but the value
// 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`.
//
// Both source and destination are 16-bit-addressed memory regions in
// the C64. Using a simple byte-by-byte copy (rather than the c64
// memcpy helper, which is in the runtime and pulls in more code) keeps
// the .prg small. The compiler turns this into a tight loop; for
// 8000 bytes that's well under a frame at 1 MHz.
static void copy_bytes(const char *src, char *dst, unsigned len)
{
for (unsigned i = 0; i < len; i++)
dst[i] = src[i];
}
void show_screen(int n)
{
// 1. Write the new pixel/attribute data into the destination
// while the VIC is still in its old mode. This avoids a
// visible garbage frame during the mode switch.
//
// 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
{
lda #0
ldx #4
ldy #0
L0: sta $d800, y
sta $d900, y
sta $da00, y
sta $db00, y
iny
bne L0
dex
bne L0
}
// Background color ($D021, the "00" color). The title screen
// uses color 0 (black); per-screen .d021 files are
// documentation, the value is inlined here.
vic.color_back = SCREEN_TITLE_D021;
vic.color_border = 0;
// 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.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL;
cia2.pra = (cia2.pra & 0xfc) | 0x00;
vic.memptr = 0x48;
}
// Other SCREEN_* IDs: reserved for Phase 4.
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef WHACK_HARE_SCREENS_H
#define WHACK_HARE_SCREENS_H
// Screen IDs. Pass to show_screen() to switch to a new screen.
#define SCREEN_TITLE 0
#define SCREEN_WAITING1 1
#define SCREEN_WAITING2 2
#define SCREEN_WIN_HARE 3
#define SCREEN_WIN_SCOOT 4
// show_screen(n) — load screen `n` into the VIC.
//
// The 5 game screens (title, waiting1, waiting2, win_hare, win_scoot)
// are 160x200 multicolor bitmaps, 8000 bytes of pixel data each. Each
// screen has a 1000-byte color attribute table (one byte per 4x8 cell)
// and a single-byte $D021 background color value.
//
// The pixel data and color attribute table are produced by
// tools/convert_screens.py (see its docstring for the exact format).
// This function copies the appropriate ones into place and configures
// the VIC to display them.
//
// Memory layout used here (after memmap_setup()):
// $D000-$D3E7 — screen memory (1000 bytes; the "color attributes")
// Per cebix-vic-article §3.7.3.4, in multicolor bitmap
// mode the screen memory byte holds the "01" color in
// its high nibble and the "10" color in its low
// nibble. The "11" color comes from color RAM at
// $D800+cell; we leave color RAM zeroed (black) for
// now since the .attr files don't store it (see
// tools/convert_screens.py for why).
// $D800-$DBE7 — color RAM (1000 nibbles). Cleared to 0 here.
// $E000-$FFFF — 8 KB bitmap (the .bin data).
// $D021 — background color (the "00" color in the multicolor
// scheme). Set to the per-screen value from the
// .d021 sidecar file.
//
// VIC config written here:
// bank = 0 (CIA2 PRA low 2 bits = 0, selects CPU $C000-$FFFF)
// ctrl1 = BMM | DEN | RSEL (multicolor bitmap, display on, 25 rows,
// no vertical scroll)
// ctrl2 = MCM | CSEL (multicolor, 40 columns, no horiz scroll)
// memptr (D018) = 0x48 (screen at $D000, bitmap at $E000 within
// the selected 16K VIC bank)
//
// For Phase 2 only SCREEN_TITLE has embedded data; the other IDs are
// reserved for Phase 4 (state machine). Calling show_screen() with an
// unsupported ID is a no-op.
void show_screen(int n);
#pragma compile("screens.c")
#endif