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:
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 "banner.h"
|
||||
#include "draw.h"
|
||||
#include "nufli.h"
|
||||
#include <c64/vic.h>
|
||||
#include <c64/sid.h>
|
||||
|
||||
@@ -161,26 +162,20 @@ static byte last_winner;
|
||||
|
||||
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_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);
|
||||
}
|
||||
|
||||
@@ -240,7 +235,11 @@ static void game_enter_draw(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_render();
|
||||
banner_clear(1);
|
||||
@@ -248,13 +247,16 @@ 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);
|
||||
// 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_render();
|
||||
banner_clear(1);
|
||||
@@ -262,22 +264,18 @@ 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)
|
||||
{
|
||||
// Use NUFLI for gameover screen (higher quality)
|
||||
nufli_show(NUFLI_SCREEN_GAMEOVER);
|
||||
nufli_exit();
|
||||
|
||||
// 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_screen(SCREEN_TITLE); // Restore multicolor mode
|
||||
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 +283,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);
|
||||
}
|
||||
|
||||
|
||||
+1
-21
@@ -13,13 +13,6 @@
|
||||
// and calls game_step() once per frame.
|
||||
// 6. while (1) {} — idle. All per-frame work happens in
|
||||
// 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 "audio.h"
|
||||
@@ -27,18 +20,6 @@
|
||||
#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)
|
||||
|
||||
@@ -50,8 +31,7 @@ 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.
|
||||
// The raster IRQ does all the per-frame work.
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user