Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d69e6ec25 | ||
|
|
4774ff036e | ||
|
|
f4b344cccd | ||
|
|
1d7554cd39 |
+16
@@ -25,6 +25,22 @@ oscar64/build/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# python venvs
|
||||
tools/img-convert/venv/
|
||||
|
||||
# mufflon source (not a submodule, from SVN repo)
|
||||
mufflon/
|
||||
|
||||
# mufflon build output
|
||||
build/mufflon
|
||||
|
||||
# NUFLI generated files (BMP intermediates, .nuf output, result/error maps)
|
||||
src/data/nufli/*.bmp
|
||||
src/data/nufli/*.nuf
|
||||
|
||||
# temp / comparison output
|
||||
tmp/
|
||||
|
||||
# editor / IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -9,6 +9,9 @@ ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||
OSCAR64_DIR := $(ROOT)/oscar64
|
||||
OSCAR64_BIN := $(OSCAR64_DIR)/bin/oscar64
|
||||
BUILD_DIR := $(ROOT)/build
|
||||
MUFFLON_DIR := $(ROOT)/mufflon
|
||||
MUFFLON_BIN := $(BUILD_DIR)/mufflon
|
||||
TOOLS_DIR := $(ROOT)/tools
|
||||
|
||||
SRC := main.c
|
||||
PRG := $(BUILD_DIR)/nyuller.prg
|
||||
@@ -17,6 +20,10 @@ LOG := $(BUILD_DIR)/vice.log
|
||||
PID_FILE := $(BUILD_DIR)/vice.pid
|
||||
SRC_DIR := $(ROOT)/src
|
||||
|
||||
# --- NUFLI source images -----------------------------------
|
||||
NUFLI_SRC_DIR := $(ROOT)/source_images
|
||||
NUFLI_OUT_DIR := $(SRC_DIR)/data/nufli
|
||||
|
||||
# --- oscar64 flags ----------------------------------------
|
||||
# Override with: make OPT=O3 (or O0/O1/O2/Os/g)
|
||||
OPT ?= O1
|
||||
@@ -31,7 +38,8 @@ HDR_FILES := $(wildcard $(SRC_DIR)/*.h)
|
||||
|
||||
# --- phony targets ----------------------------------------
|
||||
.PHONY: help compile run run-vice run-vice-cycle \
|
||||
play play-cycle kill clean ensure-build-dir ensure-oscar64
|
||||
play play-cycle kill clean ensure-build-dir ensure-oscar64 \
|
||||
ensure-mufflon nufli nufli-clean
|
||||
|
||||
# ============================================================
|
||||
# help (default)
|
||||
@@ -61,6 +69,10 @@ help:
|
||||
@echo " Clean"
|
||||
@echo " make clean remove build/ artifacts and VICE log/pid files"
|
||||
@echo ""
|
||||
@echo " NUFLI images"
|
||||
@echo " make nufli convert source PNGs → NUFLI assembly data"
|
||||
@echo " make nufli-clean remove NUFLI generated files"
|
||||
@echo ""
|
||||
@echo " Output (in build/)"
|
||||
@echo " nyuller.prg loadable C64 program"
|
||||
@echo " nyuller.d64 disk image wrapping nyuller.prg (for VICE autostart)"
|
||||
@@ -101,10 +113,79 @@ ensure-oscar64:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# ensure-mufflon — download source + build if missing
|
||||
# ============================================================
|
||||
ensure-mufflon: ensure-build-dir
|
||||
@if [ ! -f "$(MUFFLON_DIR)/mufflon.c" ]; then \
|
||||
echo "mufflon source not found; downloading from CSDb..."; \
|
||||
mkdir -p "$(MUFFLON_DIR)"; \
|
||||
curl -sL "https://csdb.dk/getinternalfile.php/180585/Mufflon1.0-source+GUI+Bonus.zip" -o "$(BUILD_DIR)/mufflon.zip"; \
|
||||
cd "$(BUILD_DIR)" && unzip -qo mufflon.zip "Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.c" "Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.h"; \
|
||||
cp "$(BUILD_DIR)/Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.c" "$(MUFFLON_DIR)/"; \
|
||||
cp "$(BUILD_DIR)/Mufflon1.0-source+Bonus/mufflon-source+GUI/mufflon.h" "$(MUFFLON_DIR)/"; \
|
||||
rm -rf "$(BUILD_DIR)/Mufflon1.0-source+Bonus" "$(BUILD_DIR)/mufflon.zip"; \
|
||||
fi
|
||||
@if [ ! -f "$(MUFFLON_DIR)/mufflon.c" ]; then \
|
||||
echo "error: mufflon source download failed" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -x "$(MUFFLON_BIN)" ]; then \
|
||||
echo "building mufflon -> $(MUFFLON_BIN)"; \
|
||||
cd "$(MUFFLON_DIR)" && gcc -o "$(MUFFLON_BIN)" -lm -O5 -ffast-math mufflon.c; \
|
||||
fi
|
||||
@if [ ! -x "$(MUFFLON_BIN)" ]; then \
|
||||
echo "error: $(MUFFLON_BIN) is still missing after build" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# NUFLI image pipeline
|
||||
# ============================================================
|
||||
|
||||
# Source screen images (PNG)
|
||||
NUFLI_SCREENS := screen_title screen_waiting1 screen_waiting_2 screen_win_hare screen_win_scoot
|
||||
|
||||
# Generated files
|
||||
NUFLI_NUF_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .nuf,$(NUFLI_SCREENS)))
|
||||
NUFLI_DELTA_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .delta,$(NUFLI_SCREENS)))
|
||||
NUFLI_BASE_FILE := $(NUFLI_OUT_DIR)/nufli_base.base
|
||||
|
||||
# Build all NUFLI images (delta encoded)
|
||||
nufli: $(NUFLI_BASE_FILE) $(NUFLI_DELTA_FILES)
|
||||
@echo "NUFLI delta-encoded images built in $(NUFLI_OUT_DIR)/"
|
||||
@ls -la $(NUFLI_BASE_FILE) $(NUFLI_DELTA_FILES) | awk '{print $$5, $$9}'
|
||||
|
||||
# Clean NUFLI artifacts
|
||||
nufli-clean:
|
||||
@rm -f $(NUFLI_OUT_DIR)/*.nuf $(NUFLI_OUT_DIR)/*.delta $(NUFLI_OUT_DIR)/*.base
|
||||
@rm -f $(NUFLI_OUT_DIR)/*.bmp $(NUFLI_OUT_DIR)/*.asm $(NUFLI_OUT_DIR)/*.h
|
||||
@echo "cleaned NUFLI images"
|
||||
|
||||
# Explicit rules for each screen (no pattern chain, no intermediate files)
|
||||
# Apply scanlines: darken every other row for CRT effect + smaller NUFLI output
|
||||
define NUFLI_RULES
|
||||
$(NUFLI_OUT_DIR)/$(1).bmp: $(NUFLI_SRC_DIR)/$(1).png | ensure-build-dir
|
||||
@mkdir -p $(NUFLI_OUT_DIR)
|
||||
@echo "converting $$< -> $$@ (with scanlines)"
|
||||
@python3 $(TOOLS_DIR)/apply_scanlines.py $$< $$@
|
||||
|
||||
$(NUFLI_OUT_DIR)/$(1).nuf: $(NUFLI_OUT_DIR)/$(1).bmp | ensure-mufflon
|
||||
@echo "converting $$< -> $$@"
|
||||
@$(MUFFLON_BIN) $$< -o $$@ --shutup
|
||||
endef
|
||||
|
||||
$(foreach screen,$(NUFLI_SCREENS),$(eval $(call NUFLI_RULES,$(screen))))
|
||||
|
||||
# Delta encoding: all .nuf files -> base + per-screen deltas
|
||||
$(NUFLI_BASE_FILE) $(NUFLI_DELTA_FILES) &: $(NUFLI_NUF_FILES)
|
||||
@echo "delta encoding $(words $(NUFLI_NUF_FILES)) screens..."
|
||||
@python3 $(TOOLS_DIR)/nufli_delta.py $(NUFLI_OUT_DIR)/nufli_base $(NUFLI_NUF_FILES)
|
||||
|
||||
# ============================================================
|
||||
# $(PRG) — compile main.c → nyuller.prg
|
||||
# ============================================================
|
||||
$(PRG): $(SRC_FILES) $(HDR_FILES) | ensure-build-dir ensure-oscar64
|
||||
$(PRG): $(SRC_FILES) $(HDR_FILES) $(NUFLI_BASE_FILE) $(NUFLI_DELTA_FILES) | ensure-build-dir ensure-oscar64
|
||||
@echo "compiling $(SRC) with $(OSCAR64_BIN) -> $(BUILD_DIR)/"
|
||||
cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)"
|
||||
|
||||
|
||||
+8
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+45
-35
@@ -28,6 +28,7 @@
|
||||
#include "score.h"
|
||||
#include "banner.h"
|
||||
#include "draw.h"
|
||||
#include "nufli.h"
|
||||
#include <c64/vic.h>
|
||||
#include <c64/sid.h>
|
||||
|
||||
@@ -106,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)
|
||||
@@ -120,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;
|
||||
@@ -161,31 +164,29 @@ static byte last_winner;
|
||||
|
||||
static void game_enter_title(void)
|
||||
{
|
||||
show_screen(SCREEN_TITLE);
|
||||
// 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);
|
||||
|
||||
score_p1 = 0;
|
||||
score_p2 = 0;
|
||||
score_render();
|
||||
// "PRESS FIRE" prompt: drawn in row 1 (below the score bar) of
|
||||
// the title screen. Re-rendered on every TITLE entry so it
|
||||
// stays visible after any state that may have overwritten the
|
||||
// row (only GAMEOVER writes row 1, but show_screen() re-loads
|
||||
// the full title bitmap which overwrites it with the title
|
||||
// art — so we always re-render the prompt here).
|
||||
banner_render("PRESS FIRE", 1);
|
||||
enter_frame = frame_count;
|
||||
title_input = TITLE_IDLE;
|
||||
title_first_frame = 0;
|
||||
// Border starts white (the "PRESS FIRE" prompt is visible).
|
||||
// The per-frame flash in game_step_title() toggles it at 1 Hz
|
||||
// (50 frames on, 50 frames off).
|
||||
vic.color_border = 1;
|
||||
audio_state_enter(STATE_TITLE);
|
||||
// 5-frame transition stinger on voice 0 (free in TITLE).
|
||||
audio_play_stinger(stinger_voice_for_state(STATE_TITLE), STINGER_DURATION);
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -204,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);
|
||||
@@ -216,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);
|
||||
@@ -240,7 +247,9 @@ static void game_enter_draw(void)
|
||||
|
||||
static void game_enter_win_p1(void)
|
||||
{
|
||||
show_screen(SCREEN_WIN_HARE);
|
||||
// Show the win screen as a NUFLI screen.
|
||||
nufli_show(NUFLI_SCREEN_WIN_HARE);
|
||||
|
||||
score_p1++;
|
||||
score_render();
|
||||
banner_clear(1);
|
||||
@@ -248,13 +257,14 @@ static void game_enter_win_p1(void)
|
||||
vic.color_border = 0;
|
||||
last_winner = 1;
|
||||
audio_state_enter(STATE_WIN_P1);
|
||||
// 5-frame transition stinger on voice 2 (free in WIN).
|
||||
audio_play_stinger(stinger_voice_for_state(STATE_WIN_P1), STINGER_DURATION);
|
||||
}
|
||||
|
||||
static void game_enter_win_p2(void)
|
||||
{
|
||||
show_screen(SCREEN_WIN_SCOOT);
|
||||
// Show the win screen as a NUFLI screen.
|
||||
nufli_show(NUFLI_SCREEN_WIN_SCOOT);
|
||||
|
||||
score_p2++;
|
||||
score_render();
|
||||
banner_clear(1);
|
||||
@@ -262,22 +272,15 @@ static void game_enter_win_p2(void)
|
||||
vic.color_border = 0;
|
||||
last_winner = 2;
|
||||
audio_state_enter(STATE_WIN_P2);
|
||||
// 5-frame transition stinger on voice 2 (free in WIN).
|
||||
audio_play_stinger(stinger_voice_for_state(STATE_WIN_P2), STINGER_DURATION);
|
||||
}
|
||||
|
||||
static void game_enter_gameover(void)
|
||||
{
|
||||
// Show the title screen with the final scores still displayed
|
||||
// (5 : x or x : 5) and the winner banner ("HARE WINS!" or
|
||||
// "SCOOT WINS!") in row 1 below the score bar. Scores are not
|
||||
// reset here — they reset on the next TITLE entry.
|
||||
show_screen(SCREEN_TITLE);
|
||||
// Show the gameover screen as a NUFLI screen.
|
||||
nufli_show(NUFLI_SCREEN_GAMEOVER);
|
||||
|
||||
score_render();
|
||||
// "HARE WINS!" or "SCOOT WINS!" — driven by last_winner set in
|
||||
// game_enter_win_p1/p2. show_screen(SCREEN_TITLE) just
|
||||
// reloaded the title bitmap, so row 1 currently has the title
|
||||
// art; banner_render() will clear and overwrite it.
|
||||
if (last_winner == 1)
|
||||
banner_render("HARE WINS!", 1);
|
||||
else
|
||||
@@ -285,7 +288,6 @@ static void game_enter_gameover(void)
|
||||
enter_frame = frame_count;
|
||||
vic.color_border = 0;
|
||||
audio_state_enter(STATE_GAMEOVER);
|
||||
// 5-frame transition stinger on voice 2 (free in GAMEOVER).
|
||||
audio_play_stinger(stinger_voice_for_state(STATE_GAMEOVER), STINGER_DURATION);
|
||||
}
|
||||
|
||||
@@ -343,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)
|
||||
@@ -380,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.
|
||||
@@ -400,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
|
||||
|
||||
+29
-38
@@ -1,46 +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)
|
||||
//
|
||||
// In Phase 4 step 6 was `while (1) game_step();` — a busy-wait that
|
||||
// called game_step as fast as the CPU could, so the "frame counter"
|
||||
// was CPU-bound. In Phase 5 the busy-wait is gone: game_step is
|
||||
// called from the raster IRQ at exactly 50 Hz, so state durations
|
||||
// are wall-clock-bound (60 frames = 1.2 sec, etc.) regardless of
|
||||
// what the CPU is doing between IRQs.
|
||||
// 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"
|
||||
|
||||
// We don't malloc, so the heap is unused. Setting it to 0 frees the
|
||||
// space for the screen data in the main region.
|
||||
//
|
||||
// The stack is set to 0x400 (1 KB) — this is the oscar64 default,
|
||||
// pinned explicitly so the layout is predictable across `-O1` /
|
||||
// `-O3` builds. With `-O3` the code section is larger, so the
|
||||
// data section's spillover into the default stack/heap gap region
|
||||
// leaves only ~0x400 for both. Anything larger (0x600+) makes
|
||||
// `-O3` fail to link with "Cannot place stack section". 1 KB is
|
||||
// plenty because the oscar64 software stack lives in zero-page
|
||||
// (0xF7-0xFF, 9 bytes per the -O3 default); the spillover area
|
||||
// only needs to hold the few locals/params that don't fit in ZP.
|
||||
#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)
|
||||
{
|
||||
@@ -50,10 +42,9 @@ int main(void)
|
||||
game_init();
|
||||
rasterirq_setup();
|
||||
|
||||
// The raster IRQ does all the per-frame work. The main loop
|
||||
// is a deliberate spin: nothing to do between IRQs.
|
||||
for (;;)
|
||||
for (;;) {
|
||||
;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// nufli.c — NUFLI image display for static screens.
|
||||
//
|
||||
// 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>
|
||||
|
||||
// Mufflon displayer addresses
|
||||
#define NUFLI_ENTRY 0x3000
|
||||
#define NUFLI_WAITLOOP 0x310D
|
||||
|
||||
// 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)
|
||||
|
||||
const unsigned char nufli_image[23040] = {
|
||||
#embed 23040 2 "data/nufli/screen_title.nuf"
|
||||
};
|
||||
|
||||
#pragma data(data)
|
||||
|
||||
// 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;
|
||||
static unsigned char saved_irqvec_lo;
|
||||
static unsigned char saved_irqvec_hi;
|
||||
|
||||
void nufli_show(int screen_id)
|
||||
{
|
||||
(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;
|
||||
|
||||
// Save the hardware IRQ vector; the displayer overwrites it.
|
||||
saved_irqvec_lo = *(volatile unsigned char *)0x0314;
|
||||
saved_irqvec_hi = *(volatile unsigned char *)0x0315;
|
||||
|
||||
// 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
|
||||
and #$fc
|
||||
sta $dd00
|
||||
lda #$00
|
||||
sta $d015
|
||||
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)
|
||||
{
|
||||
// Disable interrupts briefly while restoring VIC-II state.
|
||||
__asm {
|
||||
sei
|
||||
}
|
||||
|
||||
// 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;
|
||||
*(volatile unsigned char *)0xD01A = saved_d01a;
|
||||
|
||||
// Acknowledge any pending VIC interrupt.
|
||||
*(volatile unsigned char *)0xD019 = 0xFF;
|
||||
|
||||
__asm {
|
||||
cli
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// nufli.h — NUFLI image display with delta encoding.
|
||||
#ifndef NYULLER_NUFLI_H
|
||||
#define NYULLER_NUFLI_H
|
||||
|
||||
// Screen IDs
|
||||
#define NUFLI_SCREEN_TITLE 0
|
||||
#define NUFLI_SCREEN_WIN_HARE 1
|
||||
#define NUFLI_SCREEN_WIN_SCOOT 2
|
||||
#define NUFLI_SCREEN_GAMEOVER 3
|
||||
|
||||
// Display a NUFLI screen by applying its delta to the shared base.
|
||||
// screen_id: 0=title, 1=win_hare, 2=win_scoot, 3=gameover
|
||||
void nufli_show(int screen_id);
|
||||
|
||||
// Exit NUFLI mode and restore normal video.
|
||||
void nufli_exit(void);
|
||||
|
||||
#pragma compile("nufli.c")
|
||||
|
||||
#endif
|
||||
+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.
|
||||
|
||||
|
||||
@@ -360,6 +360,131 @@ These items were done after Phase 8 was marked complete:
|
||||
|
||||
---
|
||||
|
||||
## NUFLI Integration Plan
|
||||
|
||||
**Goal:** Replace the current multicolor bitmap screens with NUFLI format
|
||||
for static screens (title, win, gameover). NUFLI provides 320×200 resolution
|
||||
with ~10+ colors per block vs the current 160×200 with 4 colors per cell.
|
||||
|
||||
### Architecture
|
||||
|
||||
**Hybrid approach:**
|
||||
- **Static screens** (title, win_hare, win_scoot, gameover): NUFLI format
|
||||
- **Animated screens** (waiting1, waiting2, draw): Keep current multicolor bitmap
|
||||
|
||||
**Why hybrid:**
|
||||
- NUFLI consumes 100% CPU during display (no game logic possible)
|
||||
- NUFLI uses all 8 sprites (no sprites for game objects)
|
||||
- Static screens don't need game logic, so NUFLI is perfect
|
||||
|
||||
### Art Direction: Shared Backgrounds
|
||||
|
||||
**Problem:** Current source images have completely different backgrounds.
|
||||
Only 2-4% pixel-identical across screens. NUFLI output is only 40% shared.
|
||||
|
||||
**Solution:** All screens should share the same background (or a small set).
|
||||
Only foreground elements (characters, text, UI) should vary between screens.
|
||||
|
||||
**Recommendation for artist:**
|
||||
1. Pick one background (e.g., the sunset/landscape from the title screen)
|
||||
2. Use it as the base for ALL static screens
|
||||
3. Only vary foreground elements (characters, text overlays)
|
||||
4. With shared backgrounds, NUFLI overlap should reach 80-90%
|
||||
|
||||
**Expected impact:** With shared backgrounds, storing one base + per-screen
|
||||
delta becomes practical (~10KB base + ~5KB per screen delta vs 23KB each).
|
||||
|
||||
### Scanline Rendering
|
||||
|
||||
**Problem:** Each NUFLI screen is 23KB. Even with shared backgrounds,
|
||||
fitting multiple screens in 64KB RAM is tight.
|
||||
|
||||
**Solution:** Render with scanlines (every other line dark/black).
|
||||
This creates a retro CRT aesthetic while reducing the effective pixel data.
|
||||
|
||||
**Approach:**
|
||||
1. Pre-process source images: darken every other scanline before conversion
|
||||
2. The scanlined image has ~50% dark pixels → NUFLI bitmap has more uniform blocks
|
||||
3. Mufflon's conversion produces more shared data between screens
|
||||
4. Visual quality: retro CRT look, acceptable for static screens
|
||||
|
||||
**Implementation:**
|
||||
- `tools/apply_scanlines.py` pre-processes PNGs before Mufflon conversion
|
||||
- `make nufli` pipeline: PNG → scanlined BMP → Mufflon → .nuf → delta encoding
|
||||
|
||||
### Delta Encoding
|
||||
|
||||
**Problem:** Even with scanlines, storing all NUFLI screens in 64KB RAM is tight.
|
||||
|
||||
**Solution:** Delta encoding with bitmask format:
|
||||
- Store a shared base (consensus across all screens) once
|
||||
- Store per-screen deltas as bitmask + differing values
|
||||
|
||||
**Format:**
|
||||
- Base: 23040 bytes (consensus data)
|
||||
- Delta: 2880 bytes bitmask + N bytes values
|
||||
- Each set bit in bitmask = byte differs from base
|
||||
|
||||
**Results (with scanlines):**
|
||||
- Raw: 5 × 23040 = 115200 bytes
|
||||
- Encoded: 23040 (base) + 45969 (deltas) = 69009 bytes
|
||||
- Savings: 40.1%
|
||||
|
||||
**Memory layout:**
|
||||
- Base (23KB) at $1000-$7FFF (NUFLI display region)
|
||||
- Title delta (7KB) at $A000-$BFFF (BASIC ROM area)
|
||||
- Other deltas: TODO (disk loading or art direction to reduce size)
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### Step 1: NUFLI display routine integration
|
||||
- [ ] Create `src/nufli.h` with display function declarations
|
||||
- [ ] Create `src/nufli.c` with display routine wrapper:
|
||||
- `nufli_show(const unsigned char *data)` — loads data to $2000, calls SYS 12288
|
||||
- `nufli_exit()` — restores normal video mode
|
||||
- [ ] Add `nufli_display.asm` to `src/` (6502 assembly for bank switching + JSR $3000)
|
||||
|
||||
#### Step 2: Screen state machine updates
|
||||
- [ ] Modify `game_enter_title()` to use NUFLI for title screen
|
||||
- [ ] Modify `game_enter_win_p1/p2()` to use NUFLI for win screens
|
||||
- [ ] Modify `game_enter_gameover()` to use NUFLI for gameover screen
|
||||
- [ ] Keep existing multicolor for waiting/draw screens
|
||||
|
||||
#### Step 3: Memory management
|
||||
- [ ] Ensure NUFLI data ($2000-$7FFF) doesn't conflict with game code
|
||||
- [ ] Verify screen RAM at $C400 doesn't overlap with NUFLI bitmap
|
||||
- [ ] Test sprite pointer setup (NUFLI uses bank 3 sprites)
|
||||
|
||||
#### Step 4: State transitions
|
||||
- [ ] Implement `nufli_exit()` to restore VIC-II state before returning to game
|
||||
- [ ] Ensure raster IRQ is re-enabled after NUFLI display
|
||||
- [ ] Test TITLE→READY transition (NUFLI→multicolor)
|
||||
|
||||
#### Step 5: Build pipeline integration
|
||||
- [x] ✅ `make nufli` generates .asm files from source PNGs
|
||||
- [x] ✅ NUFLI .asm files are build dependencies
|
||||
- [ ] Add `#pragma embed` or linker includes for NUFLI data
|
||||
- [ ] Verify total binary size fits in C64 memory
|
||||
|
||||
### Memory Map (NUFLI mode)
|
||||
```
|
||||
$2000-$7FFF: NUFLI data (bitmap + sprites + color tables)
|
||||
$8000-$9FFF: Game code (oscar64 default)
|
||||
$C000-$C3FF: Screen RAM (for multicolor screens)
|
||||
$C400-$C7FF: Screen RAM (for NUFLI underlays)
|
||||
$E000-$FFFF: Bitmap RAM (for multicolor screens)
|
||||
```
|
||||
|
||||
### Verify
|
||||
- `make nufli` generates all 5 screen .asm files
|
||||
- `make compile` builds successfully with NUFLI data included
|
||||
- `make run`: title screen displays in NUFLI quality (320×200, many colors)
|
||||
- Press fire → transitions to multicolor waiting screen
|
||||
- Win → displays NUFLI win screen
|
||||
- Gameover → displays NUFLI gameover screen
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Code review fixes (pending)
|
||||
|
||||
Findings from the second round of 4-agent parallel code review.
|
||||
@@ -681,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.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply scanline effect: darken every odd row for CRT aesthetic."""
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
img = Image.open(src).resize((320, 200), Image.LANCZOS).convert('RGB')
|
||||
px = img.load()
|
||||
for y in range(200):
|
||||
if y % 2 == 1:
|
||||
for x in range(320):
|
||||
r, g, b = px[x, y]
|
||||
px[x, y] = (r // 4, g // 4, b // 4)
|
||||
img.save(dst)
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert NUFLI .nuf binary to oscar64-compatible assembly/include files.
|
||||
|
||||
Usage:
|
||||
python3 nuf_to_asm.py input.nuf output_base
|
||||
|
||||
Output files:
|
||||
output_base.asm - Assembly data file
|
||||
output_base.h - C header with extern declarations
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def nuf_to_asm(nuf_data, output_base):
|
||||
"""Convert NUFLI binary to assembly data."""
|
||||
out_path = Path(output_base)
|
||||
|
||||
# Skip 2-byte load address header
|
||||
data = nuf_data[2:]
|
||||
|
||||
# Assembly data file
|
||||
with open(out_path.with_suffix('.asm'), 'w') as f:
|
||||
f.write("; NUFLI image data - generated by nuf_to_asm.py\n")
|
||||
f.write("; Load at $2000, display with SYS 12288 ($3000)\n")
|
||||
f.write(f"; Total size: {len(data)} bytes\n\n")
|
||||
f.write(".segment \"NUFLI_DATA\"\n\n")
|
||||
|
||||
# Export the data
|
||||
f.write(".export _nufli_data\n")
|
||||
f.write(".export _nufli_size\n\n")
|
||||
|
||||
f.write("_nufli_data:\n")
|
||||
|
||||
# Write data in rows of 16 bytes
|
||||
for i in range(0, len(data), 16):
|
||||
chunk = data[i:i+16]
|
||||
hex_bytes = ', '.join(f'${b:02x}' for b in chunk)
|
||||
f.write(f" .byte {hex_bytes}\n")
|
||||
|
||||
f.write(f"\n_nufli_size = {len(data)}\n")
|
||||
|
||||
# C header file
|
||||
with open(out_path.with_suffix('.h'), 'w') as f:
|
||||
f.write("/* NUFLI image data - generated by nuf_to_asm.py */\n")
|
||||
f.write(f"#ifndef {out_path.name.upper().replace('.', '_')}_H\n")
|
||||
f.write(f"#define {out_path.name.upper().replace('.', '_')}_H\n\n")
|
||||
f.write(f"/* NUFLI data size: {len(data)} bytes */\n")
|
||||
f.write(f"extern const unsigned char nufli_data[{len(data)}];\n")
|
||||
f.write(f"extern const unsigned int nufli_size;\n\n")
|
||||
f.write("/* Display NUFLI image */\n")
|
||||
f.write("void nufli_display(void);\n\n")
|
||||
f.write("#endif\n")
|
||||
|
||||
print(f"Wrote {out_path.with_suffix('.asm')} ({len(data)} bytes)")
|
||||
print(f"Wrote {out_path.with_suffix('.h')}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Convert NUFLI .nuf binary to oscar64 assembly"
|
||||
)
|
||||
ap.add_argument("input", help="Input .nuf file")
|
||||
ap.add_argument("output_base", help="Output base path (no extension)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
in_path = Path(args.input)
|
||||
if not in_path.exists():
|
||||
print(f"Error: {in_path} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
data = in_path.read_bytes()
|
||||
nuf_to_asm(data, args.output_base)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NUFLI delta encoder — extract shared base + per-screen deltas.
|
||||
|
||||
Given multiple .nuf files, finds the consensus base (most common byte
|
||||
at each position) and generates per-screen delta files.
|
||||
|
||||
Output format:
|
||||
base.bin - consensus data (23040 bytes)
|
||||
*.delta - per-screen patches using bitmask:
|
||||
2880 bytes bitmask (1 bit per byte position)
|
||||
N bytes of differing values (in order)
|
||||
|
||||
Usage:
|
||||
python3 nufli_delta.py output_base screen1.nuf screen2.nuf ...
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
|
||||
NUFLI_SIZE = 23040
|
||||
BITMASK_SIZE = (NUFLI_SIZE + 7) // 8 # 2880 bytes
|
||||
|
||||
|
||||
def load_nuf(path):
|
||||
"""Load .nuf file, skip 2-byte load address."""
|
||||
data = Path(path).read_bytes()
|
||||
if len(data) == NUFLI_SIZE + 2:
|
||||
return data[2:]
|
||||
elif len(data) == NUFLI_SIZE:
|
||||
return data
|
||||
else:
|
||||
print(f"Warning: {path} is {len(data)} bytes, expected {NUFLI_SIZE} or {NUFLI_SIZE + 2}")
|
||||
return data[:NUFLI_SIZE]
|
||||
|
||||
|
||||
def find_base(screens):
|
||||
"""Find consensus base: most common byte at each position."""
|
||||
base = bytearray(NUFLI_SIZE)
|
||||
for i in range(NUFLI_SIZE):
|
||||
counts = Counter()
|
||||
for s in screens:
|
||||
counts[s[i]] += 1
|
||||
base[i] = counts.most_common(1)[0][0]
|
||||
return bytes(base)
|
||||
|
||||
|
||||
def compute_delta_bitmask(base, screen):
|
||||
"""Compute delta between base and screen using bitmask format.
|
||||
Returns (bitmask, values) where bitmask indicates which bytes differ."""
|
||||
bitmask = bytearray(BITMASK_SIZE)
|
||||
values = bytearray()
|
||||
for i in range(NUFLI_SIZE):
|
||||
if base[i] != screen[i]:
|
||||
byte_idx = i // 8
|
||||
bit_idx = i % 8
|
||||
bitmask[byte_idx] |= (1 << bit_idx)
|
||||
values.append(screen[i])
|
||||
return bytes(bitmask), bytes(values)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="NUFLI delta encoder")
|
||||
ap.add_argument("output_base", help="Output base path (no extension)")
|
||||
ap.add_argument("inputs", nargs="+", help="Input .nuf files")
|
||||
args = ap.parse_args()
|
||||
|
||||
if len(args.inputs) < 2:
|
||||
print("Error: need at least 2 input files for delta encoding")
|
||||
sys.exit(1)
|
||||
|
||||
# Load all screens
|
||||
screens = []
|
||||
names = []
|
||||
for path in args.inputs:
|
||||
screens.append(load_nuf(path))
|
||||
names.append(Path(path).stem)
|
||||
|
||||
# Find consensus base
|
||||
base = find_base(screens)
|
||||
out_path = Path(args.output_base)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write base
|
||||
base_file = out_path.with_suffix(".base")
|
||||
base_file.write_bytes(base)
|
||||
print(f"Base: {base_file} ({len(base)} bytes)")
|
||||
|
||||
# Compute and write deltas
|
||||
total_delta_bytes = 0
|
||||
for name, screen in zip(names, screens):
|
||||
bitmask, values = compute_delta_bitmask(base, screen)
|
||||
delta_data = bitmask + values
|
||||
delta_file = out_path.parent / f"{name}.delta"
|
||||
delta_file.write_bytes(delta_data)
|
||||
total_delta_bytes += len(delta_data)
|
||||
print(f" {name}.delta: {len(values)} patches, {len(delta_data)} bytes "
|
||||
f"(bitmask={len(bitmask)} + values={len(values)})")
|
||||
|
||||
# Summary
|
||||
total_raw = len(screens) * NUFLI_SIZE
|
||||
total_encoded = len(base) + total_delta_bytes
|
||||
print(f"\nTotal raw: {total_raw} bytes")
|
||||
print(f"Total encoded: {total_encoded} bytes (base={len(base)} + deltas={total_delta_bytes})")
|
||||
print(f"Savings: {total_raw - total_encoded} bytes ({100 * (1 - total_encoded / total_raw):.1f}%)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user