From beb45942ad439aa734162666209dcc6ea3164005 Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Sun, 1 Nov 2026 00:24:20 +0200 Subject: [PATCH] feat(badapple): host-tested movie decoder --- bad-apple/player/movie.c | 157 +++++++++++++++++++++ bad-apple/player/movie.h | 31 ++++ bad-apple/player/tests/fixture.bin | Bin 0 -> 66 bytes bad-apple/player/tests/test_movie_decode.c | 85 +++++++++++ bad-apple/tests/make_fixture.py | 33 +++++ bad-apple/tests/test_c_decoder.py | 23 +++ 6 files changed, 329 insertions(+) create mode 100644 bad-apple/player/movie.c create mode 100644 bad-apple/player/movie.h create mode 100644 bad-apple/player/tests/fixture.bin create mode 100644 bad-apple/player/tests/test_movie_decode.c create mode 100644 bad-apple/tests/make_fixture.py create mode 100644 bad-apple/tests/test_c_decoder.py diff --git a/bad-apple/player/movie.c b/bad-apple/player/movie.c new file mode 100644 index 0000000..5348fd7 --- /dev/null +++ b/bad-apple/player/movie.c @@ -0,0 +1,157 @@ +/* movie.c -- pure decoder for the BAPL tile-delta movie format. + * + * C89-compatible: the 1987 TMS34010 compiler has no stdint.h and no mixed + * declarations. The host test compiles this same file with gcc -std=c99. + */ +#include "movie.h" + +#define HEADER_SIZE 36 +#define INDEX_ENTRY_SIZE 6 + +static unsigned int rd16(const unsigned char *p) +{ + return (unsigned int)p[0] | ((unsigned int)p[1] << 8); +} + +static unsigned long rd32(const unsigned char *p) +{ + return (unsigned long)p[0] | ((unsigned long)p[1] << 8) + | ((unsigned long)p[2] << 16) | ((unsigned long)p[3] << 24); +} + +int movie_open(const unsigned char *data, unsigned long len, + struct movie_header *header) +{ + unsigned int version; + + if (data == 0 || header == 0 || len < HEADER_SIZE) { + return -1; + } + if (data[0] != 'B' || data[1] != 'A' || data[2] != 'P' || data[3] != 'L') { + return -1; + } + version = rd16(data + 4); + if (version != 1) { + return -1; + } + header->width = rd16(data + 6); + header->height = rd16(data + 8); + header->scale = rd16(data + 10); + header->tile = rd16(data + 12); + header->tiles_x = rd16(data + 14); + header->tiles_y = rd16(data + 16); + header->frame_count = (unsigned int)rd32(data + 18); + header->index_offset = (unsigned int)rd32(data + 22); + header->data_offset = (unsigned int)rd32(data + 26); + header->source_fps = rd16(data + 30); + header->display_hz = rd16(data + 32); + if (header->scale == 0 || header->tile == 0 + || header->tile > 32 || header->tiles_x == 0 + || header->tiles_y == 0) { + return -1; + } + if ((unsigned long)header->index_offset + + (unsigned long)header->frame_count * INDEX_ENTRY_SIZE > len) { + return -1; + } + return 0; +} + +int movie_decode_frame(const unsigned char *data, unsigned long len, + unsigned int frame, + const struct movie_header *header, + movie_fill_fn fill, void *ctx) +{ + unsigned long index_off; + unsigned long off; + unsigned int count; + unsigned int i; + unsigned int j; + unsigned int tile_index; + unsigned int packed; + unsigned int bg; + unsigned int rects; + unsigned int tx; + unsigned int ty; + unsigned int value; + unsigned int x0; + unsigned int y0; + unsigned int w; + unsigned int h; + int sx; + int sy; + + if (data == 0 || header == 0 || fill == 0) { + return -1; + } + if (frame >= header->frame_count) { + return -1; + } + index_off = (unsigned long)header->index_offset + + (unsigned long)frame * INDEX_ENTRY_SIZE; + if (index_off + INDEX_ENTRY_SIZE > len) { + return -1; + } + off = rd32(data + index_off); + if (off + 2 > len) { + return -1; + } + count = rd16(data + off); + off += 2; + + for (i = 0; i < count; i++) { + if (off + 4 > len) { + return -1; + } + tile_index = rd16(data + off); + packed = rd16(data + off + 2); + off += 4; + if (tile_index >= header->tiles_x * header->tiles_y) { + return -1; + } + bg = packed >> 15; + rects = packed & 0x7FFF; + if (off + (unsigned long)rects * 3 > len) { + return -1; + } + tx = tile_index % header->tiles_x; + ty = tile_index / header->tiles_x; + + /* The whole tile first: the old pixels outside every new rect must + * go away. The C test skips this first call per tile. */ + sx = (int)(tx * header->tile * header->scale); + sy = (int)(ty * header->tile * header->scale); + fill(sx, sy, (int)(header->tile * header->scale), + (int)(header->tile * header->scale), (int)bg, ctx); + + for (j = 0; j < rects; j++) { + value = (unsigned int)data[off] + | ((unsigned int)data[off + 1] << 8) + | ((unsigned int)data[off + 2] << 16); + off += 3; + x0 = value & 0x1F; + y0 = (value >> 5) & 0x1F; + w = ((value >> 10) & 0x1F) + 1; + h = ((value >> 15) & 0x1F) + 1; + sx = (int)((tx * header->tile + x0) * header->scale); + sy = (int)((ty * header->tile + y0) * header->scale); + fill(sx, sy, (int)(w * header->scale), + (int)(h * header->scale), (int)(1 - bg), ctx); + } + } + return 0; +} + +unsigned int movie_checksum(const unsigned char *data, unsigned long len) +{ + unsigned long sum = 0; + unsigned long i; + + if (data == 0) { + return 0; + } + for (i = 0; i < len; i++) { + sum = (sum + data[i]) & 0xFFFFFFFFUL; + } + return (unsigned int)sum; +} diff --git a/bad-apple/player/movie.h b/bad-apple/player/movie.h new file mode 100644 index 0000000..ef694c5 --- /dev/null +++ b/bad-apple/player/movie.h @@ -0,0 +1,31 @@ +#ifndef BADAPPLE_MOVIE_H +#define BADAPPLE_MOVIE_H + +struct movie_header { + unsigned int width; + unsigned int height; + unsigned int scale; + unsigned int tile; + unsigned int tiles_x; + unsigned int tiles_y; + unsigned int frame_count; + unsigned int index_offset; + unsigned int data_offset; + unsigned int source_fps; + unsigned int display_hz; +}; + +typedef void (*movie_fill_fn)(int x, int y, int w, int h, int color, + void *ctx); + +int movie_open(const unsigned char *data, unsigned long len, + struct movie_header *header); + +int movie_decode_frame(const unsigned char *data, unsigned long len, + unsigned int frame, + const struct movie_header *header, + movie_fill_fn fill, void *ctx); + +unsigned int movie_checksum(const unsigned char *data, unsigned long len); + +#endif diff --git a/bad-apple/player/tests/fixture.bin b/bad-apple/player/tests/fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..c50c42d90d7cbae43dc46296d96a43abdb3d45c3 GIT binary patch literal 66 zcmZ>A4DexOa9~hiU;<-CAkDy_0>lPDY{TFIWic{X0$Gee2qGE&@Pn9)4T>y`0H6c} Ap#T5? literal 0 HcmV?d00001 diff --git a/bad-apple/player/tests/test_movie_decode.c b/bad-apple/player/tests/test_movie_decode.c new file mode 100644 index 0000000..5537e44 --- /dev/null +++ b/bad-apple/player/tests/test_movie_decode.c @@ -0,0 +1,85 @@ +#include +#include + +#include "../movie.h" + +struct ctx { + int frame; + int last_tile; + int have_last; +}; + +static void report_fill(int x, int y, int w, int h, int color, void *opaque) +{ + struct ctx *ctx = (struct ctx *)opaque; + int lx = x / 2; + int ly = y / 2; + int lw = w / 2; + int lh = h / 2; + int tx = lx / 32; + int ty = ly / 32; + int tile = ty * 2 + tx; + + /* The decoder's first call for a tile is its background. */ + if (!ctx->have_last || tile != ctx->last_tile) { + ctx->last_tile = tile; + ctx->have_last = 1; + return; + } + printf("%d %d %d %d %d %d %d\n", ctx->frame, tile, 1 - color, + lx % 32, ly % 32, lw, lh); +} + +int main(int argc, char **argv) +{ + FILE *file; + long size; + unsigned char *data; + struct movie_header header; + unsigned int frame; + struct ctx ctx; + + if (argc != 2) { + fprintf(stderr, "usage: %s movie.bin\n", argv[0]); + return 2; + } + file = fopen(argv[1], "rb"); + if (file == 0) { + fprintf(stderr, "cannot open %s\n", argv[1]); + return 2; + } + fseek(file, 0, SEEK_END); + size = ftell(file); + fseek(file, 0, SEEK_SET); + data = (unsigned char *)malloc((size_t)size); + if (data == 0) { + fclose(file); + return 2; + } + if (fread(data, 1, (size_t)size, file) != (size_t)size) { + fclose(file); + free(data); + return 2; + } + fclose(file); + + if (movie_open(data, (unsigned long)size, &header) != 0) { + fprintf(stderr, "movie_open failed\n"); + free(data); + return 1; + } + printf("frames=%u\n", header.frame_count); + for (frame = 0; frame < header.frame_count; frame++) { + ctx.frame = (int)frame; + ctx.last_tile = -1; + ctx.have_last = 0; + if (movie_decode_frame(data, (unsigned long)size, frame, &header, + report_fill, &ctx) != 0) { + fprintf(stderr, "decode failed at frame %u\n", frame); + free(data); + return 1; + } + } + free(data); + return 0; +} diff --git a/bad-apple/tests/make_fixture.py b/bad-apple/tests/make_fixture.py new file mode 100644 index 0000000..c3457eb --- /dev/null +++ b/bad-apple/tests/make_fixture.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Write the two-frame fixture the C decoder test reads.""" +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from pipeline.movie import ( + FrameRecord, + MovieHeader, + Rect, + TileRecord, + write_movie, +) + +OUT = ROOT / "player" / "tests" / "fixture.bin" + + +def main() -> int: + frames = [ + FrameRecord(1, 0, [TileRecord(0, 0, [Rect(0, 0, 32, 32)])]), + FrameRecord(1, 0, [TileRecord(1, 1, [Rect(1, 1, 2, 3)])]), + ] + header = MovieHeader(64, 32, 2, 32, 2, 1, len(frames), 60, 72) + OUT.parent.mkdir(parents=True, exist_ok=True) + write_movie(str(OUT), frames, header) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bad-apple/tests/test_c_decoder.py b/bad-apple/tests/test_c_decoder.py new file mode 100644 index 0000000..4db6bb8 --- /dev/null +++ b/bad-apple/tests/test_c_decoder.py @@ -0,0 +1,23 @@ +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_c_decoder_matches_python(tmp_path): + subprocess.run([sys.executable, str(ROOT / "tests" / "make_fixture.py")], + check=True) + exe = tmp_path / "decode" + subprocess.run([ + "gcc", "-std=c99", "-Wall", "-Werror", + str(ROOT / "player" / "movie.c"), + str(ROOT / "player" / "tests" / "test_movie_decode.c"), + "-o", str(exe), + ], check=True) + out = subprocess.run( + [str(exe), str(ROOT / "player" / "tests" / "fixture.bin")], + check=True, capture_output=True, text=True).stdout + assert "frames=2" in out + assert "0 0 0 0 0 32 32" in out + assert "1 1 1 1 1 2 3" in out