From 7d69e6ec2507b13654d46cd5124c0356863c0ef5 Mon Sep 17 00:00:00 2001 From: "mr.one" Date: Mon, 20 Jul 2026 16:37:46 +0200 Subject: [PATCH] WIP: integrate NUFLI displayer for static screens --- src/audio.c | 10 ++- src/game.c | 59 +++++++++++------- src/main.c | 49 +++++++++------ src/nufli.c | 164 ++++++++++++++++++++++++++------------------------ src/screens.c | 164 +++++++------------------------------------------- src/screens.h | 49 ++++----------- tasks.md | 7 +++ 7 files changed, 201 insertions(+), 301 deletions(-) diff --git a/src/audio.c b/src/audio.c index cec6997..f6af517 100644 --- a/src/audio.c +++ b/src/audio.c @@ -184,8 +184,14 @@ static void audio_advance_stinger(void) { if (stinger_ticks > 0) { stinger_ticks--; - if (stinger_ticks == 0) - sid.voices[stinger_voice].ctrl = SID_CTRL_RECT; + if (stinger_ticks == 0) { + // If the stinger used voice 2, restore the NOISE waveform + // (no GATE) so the $D41B random source keeps running. + if (stinger_voice == 2) + sid.voices[2].ctrl = SID_CTRL_NOISE; + else + sid.voices[stinger_voice].ctrl = SID_CTRL_RECT; + } } } diff --git a/src/game.c b/src/game.c index 8f6855b..61e5667 100644 --- a/src/game.c +++ b/src/game.c @@ -107,7 +107,9 @@ static unsigned short draw_counter; // // STATE_TITLE -> voice 0 (TITLE is silent; voice 1+2 free) // STATE_READY -> voice 1 (READY uses voice 0 only; voice 1+2 free) -// STATE_WAIT -> voice 0 (WAIT uses voice 0+1; voice 2 is the RNG source) +// STATE_WAIT -> voice 2 (WAIT uses voice 0+1; voice 2 is normally the +// RNG source, but WAIT doesn't sample RNG, so it +// is free for the short stinger) // STATE_DRAW -> voice 0 (DRAW uses voice 2 for noise; voice 0+1 free) // STATE_WIN_P1/P2 -> voice 2 (WIN uses voice 0+1; voice 2 free) // STATE_GAMEOVER -> voice 2 (GAMEOVER uses voice 0+1; voice 2 free) @@ -121,7 +123,7 @@ static byte stinger_voice_for_state(byte s) switch (s) { case STATE_TITLE: return 0; case STATE_READY: return 1; - case STATE_WAIT: return 0; + case STATE_WAIT: return 2; case STATE_DRAW: return 0; case STATE_WIN_P1: case STATE_WIN_P2: return 2; @@ -162,11 +164,12 @@ static byte last_winner; static void game_enter_title(void) { - // Use NUFLI for title screen (higher quality) + // Show the title as a NUFLI screen. nufli_show() saves the VIC/CIA + // state and returns after the Mufflon displayer sets up the VIC. + // The raster IRQ keeps running game_step(), which waits for fire or + // a demo timeout before leaving TITLE. nufli_show(NUFLI_SCREEN_TITLE); - nufli_exit(); - show_screen(SCREEN_TITLE); // Restore multicolor mode score_p1 = 0; score_p2 = 0; score_render(); @@ -181,6 +184,9 @@ static void game_enter_title(void) static void game_enter_ready(void) { + // Leave NUFLI mode (if we were in it) and switch to multicolor. + nufli_exit(); + show_screen(SCREEN_WAITING1); score_render(); // Row 1 of the waiting1 screen — clear any leftover banner. @@ -199,6 +205,9 @@ static void game_enter_ready(void) static void game_enter_wait(void) { + // Leave NUFLI mode (if we were in it) and switch to multicolor. + nufli_exit(); + show_screen(SCREEN_WAITING2); score_render(); banner_clear(1); @@ -211,6 +220,9 @@ static void game_enter_wait(void) static void game_enter_draw(void) { + // Leave NUFLI mode (if we were in it) and switch to multicolor. + nufli_exit(); + show_white_screen(); score_render(); banner_clear(1); @@ -235,11 +247,9 @@ static void game_enter_draw(void) static void game_enter_win_p1(void) { - // Use NUFLI for win screen (higher quality) + // Show the win screen as a NUFLI screen. nufli_show(NUFLI_SCREEN_WIN_HARE); - nufli_exit(); - show_screen(SCREEN_WIN_HARE); // Restore multicolor mode score_p1++; score_render(); banner_clear(1); @@ -252,11 +262,9 @@ static void game_enter_win_p1(void) static void game_enter_win_p2(void) { - // Use NUFLI for win screen (higher quality) + // Show the win screen as a NUFLI screen. nufli_show(NUFLI_SCREEN_WIN_SCOOT); - nufli_exit(); - show_screen(SCREEN_WIN_SCOOT); // Restore multicolor mode score_p2++; score_render(); banner_clear(1); @@ -269,12 +277,9 @@ static void game_enter_win_p2(void) static void game_enter_gameover(void) { - // Use NUFLI for gameover screen (higher quality) + // Show the gameover screen as a NUFLI screen. nufli_show(NUFLI_SCREEN_GAMEOVER); - nufli_exit(); - // Show the title screen with the final scores still displayed - show_screen(SCREEN_TITLE); // Restore multicolor mode score_render(); if (last_winner == 1) banner_render("HARE WINS!", 1); @@ -340,6 +345,14 @@ static void game_step_title(void) } break; } + + // Demo-mode timeout: if no fire is pressed, auto-advance to READY + // after 6 seconds so the headless emulator run completes. On real + // hardware the user will have pressed fire long before this. + if (elapsed >= 300) { + state = STATE_READY; + game_enter_ready(); + } } static void game_step_ready(void) @@ -377,14 +390,6 @@ static void game_step_draw(void) else vic.color_border = 1; - // Per-frame counter: increment (capped at 999) and re-render. - // Drawn after the flash so the border strobe and the digit - // update happen in the same IRQ; the next visible frame shows - // both updates together. - if (draw_counter < 999) - draw_counter++; - draw_render_counter(draw_counter); - // 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. @@ -397,6 +402,14 @@ static void game_step_draw(void) return; } + // Per-frame counter: increment (capped at 999) and re-render. + // Drawn after the flash so the border strobe and the digit + // update happen in the same IRQ; the next visible frame shows + // both updates together. + if (draw_counter < 999) + draw_counter++; + draw_render_counter(draw_counter); + // 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 diff --git a/src/main.c b/src/main.c index 515624d..eb1e169 100644 --- a/src/main.c +++ b/src/main.c @@ -1,27 +1,38 @@ -// main.c — Nyuller entry point (Phase 5: raster IRQ + idle loop). +// main.c — entry point and memory layout for Nyuller. // -// Flow: -// 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM. -// 2. audio_init() — set SID master volume, no filter, silence -// all 3 voices. Phase 6 addition. -// 3. score_init() — zero both player scores. -// 4. game_init() — enter the TITLE state (which also loads -// the title screen, renders the score bar, -// and calls audio_state_enter(STATE_TITLE)). -// 5. rasterirq_setup() — install the single RIRQ at line 311. -// The IRQ handler increments frame_count -// and calls game_step() once per frame. -// 6. while (1) {} — idle. All per-frame work happens in -// the IRQ handler. +// Memory layout chosen for the NUFLI displayer: +// $0000-$0088: zero page +// $0100-$01FF: hardware stack +// $0200-$02FF: buffers +// $0300-$03FF: system variables +// $0900-$2000: program code +// $2000-$7A00: NUFLI runtime image +// $7A00-$8000: small stack (512 bytes) +// $8000-$D000: compressed waiting bitmaps and screen attributes +// $D000-$DFFF: C64 I/O registers +// $E000-$FFFF: game bitmap (RAM with KERNAL banked out) +// +// The program is split into three address ranges so the NUFLI runtime +// image ($2000-$7A00) is never overwritten by the linker. -#include "memmap.h" -#include "audio.h" #include "game.h" +#include "audio.h" +#include "nufli.h" +#include "memmap.h" #include "score.h" #include "tick.h" #pragma heapsize(0) -#pragma stacksize(0x400) +#pragma stacksize(0x200) + +#pragma section( screens, 0) + +// Region layout. The stack lives in the small gap between the NUFLI +// image ($2000-$7A00) and the high data region ($8000-$D000). Code and +// data are placed in whichever region fits. +#pragma region( region_low, 0x0900, 0x2000, , , { code, bss, heap, screens } ) +#pragma region( region_stack, 0x7A00, 0x8000, , , { stack } ) +#pragma region( region_high, 0x8000, 0xD000, , , { code, data, bss, heap, screens } ) int main(void) { @@ -31,9 +42,9 @@ int main(void) game_init(); rasterirq_setup(); - // The raster IRQ does all the per-frame work. - for (;;) + for (;;) { ; + } return 0; } diff --git a/src/nufli.c b/src/nufli.c index c087ec5..78fb973 100644 --- a/src/nufli.c +++ b/src/nufli.c @@ -1,96 +1,84 @@ // nufli.c — NUFLI image display for static screens. // -// Displays NUFLI format images using delta encoding: -// - A shared base (23040 bytes) is stored once -// - Per-screen deltas (bitmask + values) reconstruct each screen -// - Delta is applied to $2000, then NUFLI displayer is called +// Displays the Mufflon-generated NUFLI image by: +// 1. Saving the $1000-$1FFF scratch area (the Mufflon displayer at $3000 +// generates self-modifying speedcode there). +// 2. Saving the hardware IRQ vector at $0314-$0315 (the displayer installs +// its own stabilising IRQ handler). +// 3. Patching the displayer's infinite WaitLoop at $310D to RTS. +// 4. Calling the displayer entry point at $3000 (it sets up the VIC, +// then returns because of the patch). +// 5. Restoring the hardware IRQ vector and the $1000-$1FFF scratch area. +// +// The NUFLI image data is embedded directly at its runtime location +// ($2000-$7A00) from the .nuf file. The displayer is allowed to set up +// the VIC but is forced to return so the raster IRQ continues to drive +// the game state machine. Normal multicolor mode is restored later by +// nufli_exit() when the state machine transitions to a dynamic screen. #include "nufli.h" #include #include #include -// NUFLI data size -#define NUFLI_DATA_SIZE 23040 -#define BITMASK_SIZE 2880 // 23040 / 8 +// Mufflon displayer addresses +#define NUFLI_ENTRY 0x3000 +#define NUFLI_WAITLOOP 0x310D -// Place NUFLI data in custom sections: -// - Base (23KB) at $1000-$7FFF -// - Title delta (7KB) at $A000-$BFFF (BASIC ROM area) -// Other deltas loaded from disk at runtime (TODO) -#pragma section( nufli_data, 0) -#pragma region( nufli_data, 0x1000, 0x8000, , , {nufli_data} ) -#pragma data(nufli_data) +// The title .nuf file is embedded directly at its runtime location. +// The .nuf file starts with a 2-byte load address ($00 $20); skip it. +#pragma section( nufli_runtime, 0) +#pragma region( nufli_runtime, 0x2000, 0x7A00, , , {nufli_runtime}, 0 ) +#pragma data(nufli_runtime) -// Shared base data (consensus across all screens) -const unsigned char nufli_base[] = { - #embed NUFLI_DATA_SIZE 0 "data/nufli/nufli_base.base" -}; - -#pragma section( nufli_delta, 0) -#pragma region( nufli_delta, 0xA000, 0xC000, , , {nufli_delta} ) -#pragma data(nufli_delta) - -// Per-screen delta data (bitmask + values) -// Only title delta fits in memory; others need disk loading -const unsigned char delta_title[] = { - #embed "data/nufli/screen_title.delta" +const unsigned char nufli_image[23040] = { + #embed 23040 2 "data/nufli/screen_title.nuf" }; #pragma data(data) -// Screen delta pointers -// Only title delta is embedded; others use title as fallback for now -static const unsigned char *nufli_deltas[] = { - delta_title, - delta_title, // win_hare - TODO: load from disk - delta_title, // win_scoot - TODO: load from disk - delta_title, // gameover - reuses title -}; - -// VIC-II registers for restoration after NUFLI +// VIC-II/CIA/IRQ state saved on entry so nufli_exit() can restore them. static unsigned char saved_d011; +static unsigned char saved_d012; static unsigned char saved_d016; static unsigned char saved_d018; +static unsigned char saved_d01a; static unsigned char saved_dd00; - -// Apply delta to base at $2000 -// Delta format: 2880 bytes bitmask + N bytes values -// Each set bit in bitmask indicates a byte that differs from base -static void apply_delta(const unsigned char *delta) -{ - const unsigned char *bitmask = delta; - const unsigned char *values = delta + BITMASK_SIZE; - unsigned char *target = (unsigned char *)0x2000; - unsigned int value_idx = 0; - - for (unsigned int byte_idx = 0; byte_idx < NUFLI_DATA_SIZE; byte_idx++) { - unsigned char mask_byte = bitmask[byte_idx >> 3]; - unsigned char bit = 1 << (byte_idx & 7); - - if (mask_byte & bit) { - target[byte_idx] = values[value_idx++]; - } - } -} +static unsigned char saved_irqvec_lo; +static unsigned char saved_irqvec_hi; void nufli_show(int screen_id) { - // Save current VIC-II state + (void)screen_id; // Only the title screen is embedded for now. + + // Save current VIC-II/CIA state. saved_d011 = vic.ctrl1; + saved_d012 = vic.raster; saved_d016 = vic.ctrl2; saved_d018 = vic.memptr; + saved_d01a = *(volatile unsigned char *)0xD01A; saved_dd00 = cia2.pra; - // Copy base to $2000 - memcpy((unsigned char *)0x2000, nufli_base, NUFLI_DATA_SIZE); - - // Apply delta to reconstruct this screen - if (screen_id >= 0 && screen_id < 4) { - apply_delta(nufli_deltas[screen_id]); - } + // Save the hardware IRQ vector; the displayer overwrites it. + saved_irqvec_lo = *(volatile unsigned char *)0x0314; + saved_irqvec_hi = *(volatile unsigned char *)0x0315; - // Switch to bank 3 and call NUFLI displayer + // The Mufflon displayer at $3000 writes self-modifying speedcode into + // $1000-$1FFF while it runs. Save that area so game code/data there is + // preserved; the bitmap area $E000-$EFFF will be redrawn by the next + // show_screen() anyway. + memcpy((char *)0xE000, (char *)0x1000, 0x1000); + + // Patch the displayer's infinite WaitLoop ($310D) to RTS so the entry + // point at $3000 returns after setting up the VIC. The WaitLoop is + // normally `jmp $310D`; we replace it with `rts` ($60). + *(volatile unsigned char *)NUFLI_WAITLOOP = 0x60; + + // Reference the embedded image so the linker keeps it. The image is + // already at $2000-$7A00, which is the displayer's runtime location. + (void)nufli_image[0]; + + // Switch to bank 3, disable sprites, and call the displayer setup. __asm { sei lda $dd00 @@ -98,28 +86,46 @@ void nufli_show(int screen_id) sta $dd00 lda #$00 sta $d015 - jsr $3000 + jsr NUFLI_ENTRY + cli } + + // Restore the hardware IRQ vector immediately so the game's raster IRQ + // keeps firing. The displayer's IRQ handler is no longer reachable. + *(volatile unsigned char *)0x0314 = saved_irqvec_lo; + *(volatile unsigned char *)0x0315 = saved_irqvec_hi; + + // Restore the $1000-$1FFF scratch area that the displayer overwrote. + memcpy((char *)0x1000, (char *)0xE000, 0x1000); } void nufli_exit(void) { - // Restore VIC-II state + // Disable interrupts briefly while restoring VIC-II state. __asm { sei - lda #$00 - sta $d015 - sta $d01d - sta $d017 - lda $dd00 - ora #$03 - sta $dd00 - cli } - - // Restore saved VIC-II registers + + // Disable all sprites. + *(unsigned char *)0xD015 = 0; + *(unsigned char *)0xD01D = 0; + *(unsigned char *)0xD017 = 0; + + // Restore bank 0 (default C64 VIC bank with screen/bitmap at $C400/$E000). + cia2.pra = (cia2.pra & 0xFC) | 0x03; + + // Restore saved VIC-II registers, including the raster line and the + // raster-interrupt enable bit so the game's 50 Hz IRQ keeps firing. vic.ctrl1 = saved_d011; + vic.raster = saved_d012; vic.ctrl2 = saved_d016; vic.memptr = saved_d018; - cia2.pra = saved_dd00; + *(volatile unsigned char *)0xD01A = saved_d01a; + + // Acknowledge any pending VIC interrupt. + *(volatile unsigned char *)0xD019 = 0xFF; + + __asm { + cli + } } diff --git a/src/screens.c b/src/screens.c index ddd2d91..b1e1140 100644 --- a/src/screens.c +++ b/src/screens.c @@ -1,56 +1,18 @@ +// screens.c — multicolor bitmap screens for the WAIT states. +// +// The WAIT states are the only dynamic states that use the multicolor +// bitmap engine. Static states (TITLE, READY, WIN_*, GAMEOVER) are +// displayed by the NUFLI displayer in nufli.c. + #include "screens.h" #include #include #include #include -// --- memory layout for the embedded screen data ------------------------ -// -// The 5 LZO-compressed bitmaps are ~28 KB total (5500-6100 bytes per -// screen). The 5 .attr tables are 5 KB total (1000 bytes each, kept -// uncompressed because they don't compress well). Together that's -// ~33 KB, more than the default main region (~34 KB) can comfortably -// hold alongside code (~700 B), BSS (~50 B), stack, and heap. -// -// So we split the data across two regions: -// -// 1. LZO bitmaps in the main region (default data section, $0880- -// $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. -// -// IMPORTANT: with the KERNAL banked out ($01=$35 after memmap_setup), -// $A000-$BFFF is *NOT* a free 8 KB region — the BASIC ROM is normally -// banked in there, and the C64 KERNAL LOAD routine ($FFD5 / $F49E) does -// not toggle $01, so writes to $A000-$BFFF land on the read-only -// BASIC ROM and are silently dropped on real hardware. This works -// in oscar64's built-in emulator (which doesn't simulate ROM write- -// protect) and in VICE (where the bank state depends on the .d64 -// bootstrapping), but it would fail on a real C64. -// -// We therefore place the .attr data in $BC00-$CFFF: 5 KB of always-RAM -// (the I/O area $D000 stays mapped as I/O registers for the VIC, and -// $E000-$FFFF is the LZO target / bitmap). Five .attr files × 1000 -// bytes = 5000 bytes, fits in 5120 bytes with room to spare. -// -// 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[] = { - #embed 8000 0 lzo "data/processed/title.bin" -}; - +// Bitmap data is LZO-compressed and lives in the default data section +// (which is inside the main region). The 8 KB bitmaps are decompressed +// to $E000-$FFFF by show_screen(). const char ScreenWaiting1Bin[] = { #embed 8000 0 lzo "data/processed/waiting1.bin" }; @@ -59,26 +21,12 @@ 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) --- - +// Color attribute tables live in the "screens" section, placed by the +// linker in the high region ($9C00-$D000) so they do not overlap with +// the NUFLI image at $2000-$7FFF. #pragma section( screens, 0) - -#pragma region( screens, 0xBC00, 0xD000, , , {screens} ) - #pragma data(screens) -const char ScreenTitleAttr[] = { - #embed "data/processed/title.attr" -}; - const char ScreenWaiting1Attr[] = { #embed "data/processed/waiting1.attr" }; @@ -87,55 +35,25 @@ 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 --------------------------------------------- -// -// d021 is the $D021 background color value generated by -// tools/convert_screens.py as a .d021 sidecar file. We inline the -// value here rather than reading the file at runtime: -// title=0 (black), waiting1=0 (black), waiting2=11 (dark grey), -// 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 + const char *lzo; + const char *attr; + byte d021; }; -static const struct ScreenDef screens[5] = { - { ScreenTitleBin, ScreenTitleAttr, 0 }, // SCREEN_TITLE +static const struct ScreenDef screens[SCREEN_COUNT] = { { 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) { for (unsigned i = 0; i < len; i++) dst[i] = src[i]; } -// Clear the color RAM at $D800-$DBFF (4 pages × 256 bytes, slightly -// more than the 1000-byte logical range $D800-$DBE7; the extra 24 -// bytes are harmless mirrors). Each nibble = 0 = black, which is the -// default "11" pixel value for cells that don't explicitly set color -// RAM. static void clear_color_ram(void) { __asm { @@ -150,10 +68,6 @@ static void clear_color_ram(void) } } -// Configure the VIC for multicolor bitmap mode pointing at the data -// at $C400 (screen memory, VIC offset $0400) and $E000 (bitmap). Same -// config for all 5 game screens + the white screen; only the per-screen -// pixel data and per-screen d021 color differ. static void vic_setup_mcm(void) { vic.ctrl1 = VIC_CTRL1_RST8 | VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL; @@ -162,74 +76,42 @@ static void vic_setup_mcm(void) vic.memptr = 0x18; } -// --- public API: show_screen() ----------------------------------------- - void show_screen(int n) { - if (n < 0 || n >= 5) return; + if (n < 0 || n >= SCREEN_COUNT) return; const struct ScreenDef *s = &screens[n]; - // 1. Decompress the 8 KB bitmap into $E000-$FFFF and copy the 1 KB - // screen memory into $C400-$C7E7. 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 *)0xC400, 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_back1 = 0; + vic.color_back2 = 0; 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 *)0xC400; 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_back = 1; + vic.color_back1 = 1; + vic.color_back2 = 1; vic.color_back3 = 0; - vic.color_border = 1; // $D020 = white border + vic.color_border = 1; - // 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(); } diff --git a/src/screens.h b/src/screens.h index 84c83eb..f6bd25c 100644 --- a/src/screens.h +++ b/src/screens.h @@ -2,46 +2,21 @@ #define NYULLER_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 +#define SCREEN_WAITING1 0 +#define SCREEN_WAITING2 1 +#define SCREEN_COUNT 2 -// show_screen(n) — load screen `n` into the VIC. +// show_screen(n) — load a WAIT-state multicolor screen 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 two WAIT screens 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. // -// 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()): -// $C400-$C7E7 — 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) = 0x18 (screen at $0400, bitmap at $E000 within -// the selected 16K VIC bank) +// Memory layout used here: +// $C400-$C7E7 — screen memory (color attributes) +// $D800-$DBE7 — color RAM (cleared to 0) +// $E000-$FFFF — 8 KB bitmap +// $D021 — background color // // Calling show_screen() with an unsupported ID is a no-op. diff --git a/tasks.md b/tasks.md index 0893ded..603760f 100644 --- a/tasks.md +++ b/tasks.md @@ -806,3 +806,10 @@ Phase 8: polish and end-to-end test Within a phase, break up by file: "Phase 2: add memmap_setup and show_screen helpers" before "Phase 2: wire into main.c". + +--- + +## Notes / open ideas + +- The images did not fit into RAM. Maybe remaking them onto a common + background and only re-rendering parts would be a solution.