feat(badapple): host-tested movie decoder

This commit is contained in:
arti committed 2026-11-01 00:24:20 +02:00
1 parent 5a1f8429f0
commit beb45942ad
6 files changed
+329

No files matched your search

+157
View File
@@ -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;
}
+31
View File
@@ -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
Binary file not shown.
@@ -0,0 +1,85 @@
#include <stdio.h>
#include <stdlib.h>
#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;
}
+33
View File
@@ -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())
+23
View File
@@ -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