WIP: integrate NUFLI displayer for static screens
This commit is contained in:
+7
-1
@@ -184,9 +184,15 @@ static void audio_advance_stinger(void)
|
||||
{
|
||||
if (stinger_ticks > 0) {
|
||||
stinger_ticks--;
|
||||
if (stinger_ticks == 0)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void audio_play_fault_stinger(void)
|
||||
|
||||
+36
-23
@@ -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
|
||||
|
||||
+30
-19
@@ -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;
|
||||
}
|
||||
|
||||
+83
-77
@@ -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 <string.h>
|
||||
#include <c64/vic.h>
|
||||
#include <c64/cia.h>
|
||||
|
||||
// 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);
|
||||
// Save the hardware IRQ vector; the displayer overwrites it.
|
||||
saved_irqvec_lo = *(volatile unsigned char *)0x0314;
|
||||
saved_irqvec_hi = *(volatile unsigned char *)0x0315;
|
||||
|
||||
// Apply delta to reconstruct this screen
|
||||
if (screen_id >= 0 && screen_id < 4) {
|
||||
apply_delta(nufli_deltas[screen_id]);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Switch to bank 3 and call NUFLI displayer
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
+23
-141
@@ -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 <c64/vic.h>
|
||||
#include <c64/cia.h>
|
||||
#include <oscar.h>
|
||||
#include <string.h>
|
||||
|
||||
// --- 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();
|
||||
}
|
||||
|
||||
+12
-37
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user