Implement NUFLI delta encoding with scanlines
- Add tools/apply_scanlines.py for CRT scanline effect - Add tools/nufli_delta.py for delta encoding (base + bitmask deltas) - Delta format: 2880 byte bitmask + N byte values per screen - 40.1% size reduction vs raw NUFLI (69KB vs 115KB for 5 screens) - Base (23KB) at 000-FFF, title delta (7KB) at - - Other deltas use title as fallback (TODO: disk loading) - Update tasks.md with scanline and delta encoding documentation
This commit is contained in:
@@ -148,39 +148,44 @@ NUFLI_SCREENS := screen_title screen_waiting1 screen_waiting_2 screen_win_hare s
|
|||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
NUFLI_NUF_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .nuf,$(NUFLI_SCREENS)))
|
NUFLI_NUF_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .nuf,$(NUFLI_SCREENS)))
|
||||||
NUFLI_ASM_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .asm,$(NUFLI_SCREENS)))
|
NUFLI_DELTA_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .delta,$(NUFLI_SCREENS)))
|
||||||
NUFLI_HDR_FILES := $(addprefix $(NUFLI_OUT_DIR)/,$(addsuffix .h,$(NUFLI_SCREENS)))
|
NUFLI_BASE_FILE := $(NUFLI_OUT_DIR)/nufli_base.base
|
||||||
|
|
||||||
# Build all NUFLI images
|
# Build all NUFLI images (delta encoded)
|
||||||
nufli: $(NUFLI_ASM_FILES) $(NUFLI_HDR_FILES)
|
nufli: $(NUFLI_BASE_FILE) $(NUFLI_DELTA_FILES)
|
||||||
@echo "NUFLI images built in $(NUFLI_OUT_DIR)/"
|
@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
|
# Clean NUFLI artifacts
|
||||||
nufli-clean:
|
nufli-clean:
|
||||||
@rm -f $(NUFLI_OUT_DIR)/*.nuf $(NUFLI_OUT_DIR)/*.asm $(NUFLI_OUT_DIR)/*.h
|
@rm -f $(NUFLI_OUT_DIR)/*.nuf $(NUFLI_OUT_DIR)/*.delta $(NUFLI_OUT_DIR)/*.base
|
||||||
@rm -f $(NUFLI_OUT_DIR)/*.bmp
|
@rm -f $(NUFLI_OUT_DIR)/*.bmp $(NUFLI_OUT_DIR)/*.asm $(NUFLI_OUT_DIR)/*.h
|
||||||
@echo "cleaned NUFLI images"
|
@echo "cleaned NUFLI images"
|
||||||
|
|
||||||
# PNG → BMP (Mufflon needs BMP input)
|
# Explicit rules for each screen (no pattern chain, no intermediate files)
|
||||||
$(NUFLI_OUT_DIR)/%.bmp: $(NUFLI_SRC_DIR)/%.png | ensure-build-dir
|
# 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)
|
@mkdir -p $(NUFLI_OUT_DIR)
|
||||||
@echo "converting $< -> $@"
|
@echo "converting $$< -> $$@ (with scanlines)"
|
||||||
@python3 -c "from PIL import Image; Image.open('$<').resize((320,200), Image.LANCZOS).save('$@')"
|
@python3 $(TOOLS_DIR)/apply_scanlines.py $$< $$@
|
||||||
|
|
||||||
# BMP → NUFLI (.nuf)
|
$(NUFLI_OUT_DIR)/$(1).nuf: $(NUFLI_OUT_DIR)/$(1).bmp | ensure-mufflon
|
||||||
$(NUFLI_OUT_DIR)/%.nuf: $(NUFLI_OUT_DIR)/%.bmp | ensure-mufflon
|
@echo "converting $$< -> $$@"
|
||||||
@echo "converting $< -> $@"
|
@$(MUFFLON_BIN) $$< -o $$@ --shutup
|
||||||
@$(MUFFLON_BIN) $< -o $@ --shutup
|
endef
|
||||||
|
|
||||||
# NUFLI → Assembly (.asm + .h)
|
$(foreach screen,$(NUFLI_SCREENS),$(eval $(call NUFLI_RULES,$(screen))))
|
||||||
$(NUFLI_OUT_DIR)/%.asm $(NUFLI_OUT_DIR)/%.h: $(NUFLI_OUT_DIR)/%.nuf
|
|
||||||
@echo "converting $< -> $@ + $(basename $@).h"
|
# Delta encoding: all .nuf files -> base + per-screen deltas
|
||||||
@python3 $(TOOLS_DIR)/nuf_to_asm.py $< $(NUFLI_OUT_DIR)/$*
|
$(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) — compile main.c → nyuller.prg
|
||||||
# ============================================================
|
# ============================================================
|
||||||
$(PRG): $(SRC_FILES) $(HDR_FILES) $(NUFLI_ASM_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)/"
|
@echo "compiling $(SRC) with $(OSCAR64_BIN) -> $(BUILD_DIR)/"
|
||||||
cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)"
|
cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
/* NUFLI image data - generated by nuf_to_asm.py */
|
|
||||||
#ifndef SCREEN_TITLE_H
|
|
||||||
#define SCREEN_TITLE_H
|
|
||||||
|
|
||||||
/* NUFLI data size: 23040 bytes */
|
|
||||||
extern const unsigned char nufli_data[23040];
|
|
||||||
extern const unsigned int nufli_size;
|
|
||||||
|
|
||||||
/* Display NUFLI image */
|
|
||||||
void nufli_display(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
/* NUFLI image data - generated by nuf_to_asm.py */
|
|
||||||
#ifndef SCREEN_WAITING1_H
|
|
||||||
#define SCREEN_WAITING1_H
|
|
||||||
|
|
||||||
/* NUFLI data size: 23040 bytes */
|
|
||||||
extern const unsigned char nufli_data[23040];
|
|
||||||
extern const unsigned int nufli_size;
|
|
||||||
|
|
||||||
/* Display NUFLI image */
|
|
||||||
void nufli_display(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
/* NUFLI image data - generated by nuf_to_asm.py */
|
|
||||||
#ifndef SCREEN_WAITING_2_H
|
|
||||||
#define SCREEN_WAITING_2_H
|
|
||||||
|
|
||||||
/* NUFLI data size: 23040 bytes */
|
|
||||||
extern const unsigned char nufli_data[23040];
|
|
||||||
extern const unsigned int nufli_size;
|
|
||||||
|
|
||||||
/* Display NUFLI image */
|
|
||||||
void nufli_display(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
/* NUFLI image data - generated by nuf_to_asm.py */
|
|
||||||
#ifndef SCREEN_WIN_HARE_H
|
|
||||||
#define SCREEN_WIN_HARE_H
|
|
||||||
|
|
||||||
/* NUFLI data size: 23040 bytes */
|
|
||||||
extern const unsigned char nufli_data[23040];
|
|
||||||
extern const unsigned int nufli_size;
|
|
||||||
|
|
||||||
/* Display NUFLI image */
|
|
||||||
void nufli_display(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
/* NUFLI image data - generated by nuf_to_asm.py */
|
|
||||||
#ifndef SCREEN_WIN_SCOOT_H
|
|
||||||
#define SCREEN_WIN_SCOOT_H
|
|
||||||
|
|
||||||
/* NUFLI data size: 23040 bytes */
|
|
||||||
extern const unsigned char nufli_data[23040];
|
|
||||||
extern const unsigned int nufli_size;
|
|
||||||
|
|
||||||
/* Display NUFLI image */
|
|
||||||
void nufli_display(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+21
-24
@@ -28,6 +28,7 @@
|
|||||||
#include "score.h"
|
#include "score.h"
|
||||||
#include "banner.h"
|
#include "banner.h"
|
||||||
#include "draw.h"
|
#include "draw.h"
|
||||||
|
#include "nufli.h"
|
||||||
#include <c64/vic.h>
|
#include <c64/vic.h>
|
||||||
#include <c64/sid.h>
|
#include <c64/sid.h>
|
||||||
|
|
||||||
@@ -161,26 +162,20 @@ static byte last_winner;
|
|||||||
|
|
||||||
static void game_enter_title(void)
|
static void game_enter_title(void)
|
||||||
{
|
{
|
||||||
show_screen(SCREEN_TITLE);
|
// Use NUFLI for title screen (higher quality)
|
||||||
|
nufli_show(NUFLI_SCREEN_TITLE);
|
||||||
|
nufli_exit();
|
||||||
|
|
||||||
|
show_screen(SCREEN_TITLE); // Restore multicolor mode
|
||||||
score_p1 = 0;
|
score_p1 = 0;
|
||||||
score_p2 = 0;
|
score_p2 = 0;
|
||||||
score_render();
|
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);
|
banner_render("PRESS FIRE", 1);
|
||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
title_input = TITLE_IDLE;
|
title_input = TITLE_IDLE;
|
||||||
title_first_frame = 0;
|
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;
|
vic.color_border = 1;
|
||||||
audio_state_enter(STATE_TITLE);
|
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);
|
audio_play_stinger(stinger_voice_for_state(STATE_TITLE), STINGER_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +235,11 @@ static void game_enter_draw(void)
|
|||||||
|
|
||||||
static void game_enter_win_p1(void)
|
static void game_enter_win_p1(void)
|
||||||
{
|
{
|
||||||
show_screen(SCREEN_WIN_HARE);
|
// Use NUFLI for win screen (higher quality)
|
||||||
|
nufli_show(NUFLI_SCREEN_WIN_HARE);
|
||||||
|
nufli_exit();
|
||||||
|
|
||||||
|
show_screen(SCREEN_WIN_HARE); // Restore multicolor mode
|
||||||
score_p1++;
|
score_p1++;
|
||||||
score_render();
|
score_render();
|
||||||
banner_clear(1);
|
banner_clear(1);
|
||||||
@@ -248,13 +247,16 @@ static void game_enter_win_p1(void)
|
|||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
last_winner = 1;
|
last_winner = 1;
|
||||||
audio_state_enter(STATE_WIN_P1);
|
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);
|
audio_play_stinger(stinger_voice_for_state(STATE_WIN_P1), STINGER_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_win_p2(void)
|
static void game_enter_win_p2(void)
|
||||||
{
|
{
|
||||||
show_screen(SCREEN_WIN_SCOOT);
|
// Use NUFLI for win screen (higher quality)
|
||||||
|
nufli_show(NUFLI_SCREEN_WIN_SCOOT);
|
||||||
|
nufli_exit();
|
||||||
|
|
||||||
|
show_screen(SCREEN_WIN_SCOOT); // Restore multicolor mode
|
||||||
score_p2++;
|
score_p2++;
|
||||||
score_render();
|
score_render();
|
||||||
banner_clear(1);
|
banner_clear(1);
|
||||||
@@ -262,22 +264,18 @@ static void game_enter_win_p2(void)
|
|||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
last_winner = 2;
|
last_winner = 2;
|
||||||
audio_state_enter(STATE_WIN_P2);
|
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);
|
audio_play_stinger(stinger_voice_for_state(STATE_WIN_P2), STINGER_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_enter_gameover(void)
|
static void game_enter_gameover(void)
|
||||||
{
|
{
|
||||||
|
// Use NUFLI for gameover screen (higher quality)
|
||||||
|
nufli_show(NUFLI_SCREEN_GAMEOVER);
|
||||||
|
nufli_exit();
|
||||||
|
|
||||||
// Show the title screen with the final scores still displayed
|
// Show the title screen with the final scores still displayed
|
||||||
// (5 : x or x : 5) and the winner banner ("HARE WINS!" or
|
show_screen(SCREEN_TITLE); // Restore multicolor mode
|
||||||
// "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);
|
|
||||||
score_render();
|
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)
|
if (last_winner == 1)
|
||||||
banner_render("HARE WINS!", 1);
|
banner_render("HARE WINS!", 1);
|
||||||
else
|
else
|
||||||
@@ -285,7 +283,6 @@ static void game_enter_gameover(void)
|
|||||||
enter_frame = frame_count;
|
enter_frame = frame_count;
|
||||||
vic.color_border = 0;
|
vic.color_border = 0;
|
||||||
audio_state_enter(STATE_GAMEOVER);
|
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);
|
audio_play_stinger(stinger_voice_for_state(STATE_GAMEOVER), STINGER_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-21
@@ -13,13 +13,6 @@
|
|||||||
// and calls game_step() once per frame.
|
// and calls game_step() once per frame.
|
||||||
// 6. while (1) {} — idle. All per-frame work happens in
|
// 6. while (1) {} — idle. All per-frame work happens in
|
||||||
// the IRQ handler.
|
// the IRQ handler.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
#include "memmap.h"
|
#include "memmap.h"
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
@@ -27,18 +20,6 @@
|
|||||||
#include "score.h"
|
#include "score.h"
|
||||||
#include "tick.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 heapsize(0)
|
||||||
#pragma stacksize(0x400)
|
#pragma stacksize(0x400)
|
||||||
|
|
||||||
@@ -50,8 +31,7 @@ int main(void)
|
|||||||
game_init();
|
game_init();
|
||||||
rasterirq_setup();
|
rasterirq_setup();
|
||||||
|
|
||||||
// The raster IRQ does all the per-frame work. The main loop
|
// The raster IRQ does all the per-frame work.
|
||||||
// is a deliberate spin: nothing to do between IRQs.
|
|
||||||
for (;;)
|
for (;;)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
};
|
||||||
|
|
||||||
|
#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
|
||||||
|
static unsigned char saved_d011;
|
||||||
|
static unsigned char saved_d016;
|
||||||
|
static unsigned char saved_d018;
|
||||||
|
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++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void nufli_show(int screen_id)
|
||||||
|
{
|
||||||
|
// Save current VIC-II state
|
||||||
|
saved_d011 = vic.ctrl1;
|
||||||
|
saved_d016 = vic.ctrl2;
|
||||||
|
saved_d018 = vic.memptr;
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to bank 3 and call NUFLI displayer
|
||||||
|
__asm {
|
||||||
|
sei
|
||||||
|
lda $dd00
|
||||||
|
and #$fc
|
||||||
|
sta $dd00
|
||||||
|
lda #$00
|
||||||
|
sta $d015
|
||||||
|
jsr $3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void nufli_exit(void)
|
||||||
|
{
|
||||||
|
// Restore 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
|
||||||
|
vic.ctrl1 = saved_d011;
|
||||||
|
vic.ctrl2 = saved_d016;
|
||||||
|
vic.memptr = saved_d018;
|
||||||
|
cia2.pra = saved_dd00;
|
||||||
|
}
|
||||||
+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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include "memmap.h"
|
||||||
|
#include "nufli.h"
|
||||||
|
#include <c64/vic.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memmap_setup();
|
||||||
|
|
||||||
|
// Just try to access the NUFLI data
|
||||||
|
const unsigned char *data = nufli_get_screen(0);
|
||||||
|
|
||||||
|
// Infinite loop
|
||||||
|
for (;;)
|
||||||
|
;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "memmap.h"
|
||||||
|
#include "nufli.h"
|
||||||
|
#include <c64/vic.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memmap_setup();
|
||||||
|
|
||||||
|
// Try to display NUFLI image
|
||||||
|
const unsigned char *data = nufli_get_screen(0);
|
||||||
|
nufli_show(data);
|
||||||
|
|
||||||
|
// Infinite loop
|
||||||
|
for (;;)
|
||||||
|
;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#include "memmap.h"
|
||||||
|
#include <c64/vic.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memmap_setup();
|
||||||
|
|
||||||
|
// Set border color to red
|
||||||
|
vic.color_border = 2;
|
||||||
|
|
||||||
|
// Infinite loop
|
||||||
|
for (;;)
|
||||||
|
;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -377,6 +377,64 @@ with ~10+ colors per block vs the current 160×200 with 4 colors per cell.
|
|||||||
- NUFLI uses all 8 sprites (no sprites for game objects)
|
- NUFLI uses all 8 sprites (no sprites for game objects)
|
||||||
- Static screens don't need game logic, so NUFLI is perfect
|
- 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
|
### Implementation Steps
|
||||||
|
|
||||||
#### Step 1: NUFLI display routine integration
|
#### Step 1: NUFLI display routine integration
|
||||||
|
|||||||
@@ -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,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