From a04abd101a36017178c992d2f7970d16c84f48f9 Mon Sep 17 00:00:00 2001 From: immibis Date: Wed, 19 Feb 2025 04:49:32 +0100 Subject: [PATCH] Before we add more rooms, some rudimentary image compression - decreases executable size (therefore time to upload to the pinetab) about 10x --- Makefile | 13 ++------- compress_sprite.py | 59 ++++++++++++++++++++++++++++++++++++++++ decompress_sprite.cpp | 62 ++++++++++++++++++++++++++++++++++++++++++ engine.h | 7 +++++ pinetab2_framework.cpp | 2 +- textbox.cpp | 7 +++-- 6 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 compress_sprite.py create mode 100644 decompress_sprite.cpp diff --git a/Makefile b/Makefile index 7b044db..6615937 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) -OBJS = pinetab2_framework.o game.o textbox.o inventory.o +OBJS = pinetab2_framework.o game.o textbox.o inventory.o decompress_sprite.o SPRITES = $(patsubst $(PROJECT_ROOT)sprites/%.xcf,%,$(wildcard $(PROJECT_ROOT)sprites/*.xcf)) NAVMESHES = $(patsubst $(PROJECT_ROOT)navmesh/%.tmx,%,$(wildcard $(PROJECT_ROOT)navmesh/*.tmx)) @@ -44,15 +44,8 @@ pinetab2_framework: $(OBJS) clean: rm -fr pinetab2_framework $(OBJS) $(EXTRA_CLEAN) -sprite_%.o: $(PROJECT_ROOT)sprites/%.xcf - gimp -in -b '(let ((image (car (gimp-xcf-load 0 "$<" "$(notdir $<)")))) ;\ - ;(gimp-image-scale image 1280 800) ;\ - (gimp-image-rotate image ROTATE-90) ;\ - (let ((layer (car (gimp-image-merge-visible-layers image CLIP-TO-IMAGE)))) ;\ - (plug-in-colors-channel-mixer RUN-NONINTERACTIVE image layer 0 0 0 1 0 1 0 1 0 0) ; swap red and blue channels \ - (file-raw-save 1 image layer "$(patsubst %.o,%.raw,$@)" "$(patsubst %.o,%.raw,$@)") ;\ - ) ;\ - )' -b '(gimp-quit 0)' +sprite_%.o: %.png $(PROJECT_ROOT)compress_sprite.py + python3 "$(PROJECT_ROOT)compress_sprite.py" "$<" $(patsubst %.o,%.raw,$@) $(OBJCOPY) -I binary --rename-section .data=.rodata,alloc,load,readonly,data,contents $(patsubst %.o,%.raw,$@) $@ rm $(patsubst %.o,%.raw,$@) @# symbols are _binary_sprite_%_raw_start / _end / _size diff --git a/compress_sprite.py b/compress_sprite.py new file mode 100644 index 0000000..bb1fdfb --- /dev/null +++ b/compress_sprite.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +import sys +import struct +import PIL.Image +_, inpath, outpath = sys.argv + +image = PIL.Image.open(inpath) +assert image.mode == "RGBA", "image mode is "+image.mode + +# We measure width and height prior to the rotation +width, height = image.width, image.height + +image = image.transpose(PIL.Image.Transpose.ROTATE_270) + +with open(outpath, "wb") as out: + out.write(struct.pack("=HH", width, height)) + for channel in "BGRA": + pixelbytes = image.getchannel(channel).tobytes() + def rlegroups(): + curpixel=None + count=0 + litrun = b"" + for pixel in pixelbytes: + if curpixel is None: + curpixel = pixel + count = 1 + elif curpixel != pixel: + assert count > 0 + if count <= 2: + if len(litrun)+count > 127: + assert len(litrun) > 0 + yield litrun + litrun = b"" + litrun += bytes([curpixel]) * count + else: + if litrun != b"": + assert len(litrun) > 0 + yield litrun + litrun = b"" + yield (curpixel, count) + curpixel = pixel + count = 1 + else: + count += 1 + if litrun != b"": + assert len(litrun) > 0 + yield litrun + if count > 0: + yield (curpixel, count) + for group in rlegroups(): + if type(group) == bytes: + out.write(struct.pack("=B", len(group)+128) + group) + else: + pixel, count = group + assert count > 0 + if count >= 128: + out.write(struct.pack("=BIB", 0, count, pixel)) + else: + out.write(struct.pack("=BB", count, pixel)) diff --git a/decompress_sprite.cpp b/decompress_sprite.cpp new file mode 100644 index 0000000..a8400c1 --- /dev/null +++ b/decompress_sprite.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include "engine.h" +#include + +// TODO: even better compression using zlib or something - or vector graphics + +#define MAX_SPRITES 5000 +static struct sprite sprites[MAX_SPRITES]; +static int numsprites; + +struct sprite *get_decompressed_sprite(const unsigned char *spritedata) { + for(int i = 0; i < numsprites; i++) { + if(sprites[i].original_data_source == spritedata) + return &sprites[i]; + } + struct sprite *s = &sprites[numsprites++]; + s->width = *(uint16_t*)(spritedata + 0); + s->height = *(uint16_t*)(spritedata + 2); + s->original_data_source = spritedata; + spritedata += 4; + + s->pixels = (uint32_t*)malloc(s->width*s->height*4); + if(!s->pixels) error(1, 0, "get_decompressed_sprite: memory allocation failed"); + + for(int channel = 0; channel < 4; channel++) { + unsigned char *pixels = (unsigned char*)s->pixels; + pixels += channel; + unsigned int pixelsleft = s->width * s->height; + while(pixelsleft > 0) { + unsigned char control = *spritedata++; + if(control & 0x80) { + control &= 0x7f; + if(control > pixelsleft) error(1, 0, "corrupted compressed sprite A %u > %u", (unsigned int)control, (unsigned int)pixelsleft); + for(int i = 0; i < control; i++) { + *pixels = *spritedata++; + pixels += 4; + } + pixelsleft -= control; + } else { + uint32_t count; + if(control == 0) { + count = *(uint32_t*)spritedata; + spritedata += 4; + } else { + count = control; + } + unsigned char value = *spritedata++; + if(count > pixelsleft) error(1, 0, "corrupted compressed sprite B %u > %u", (unsigned int)count, (unsigned int)pixelsleft); + for(uint32_t i = 0; i < count; i++) { + *pixels = value; + pixels += 4; + } + pixelsleft -= count; + } + } + } + return s; +} + diff --git a/engine.h b/engine.h index 0fb7e8d..2fe6fb0 100644 --- a/engine.h +++ b/engine.h @@ -83,6 +83,13 @@ void push_scene_textbox(const char *text); void create_standard_inventory(scene *s); void standard_inventory_onclick(struct object *obj); +struct sprite { + int width, height; + uint32_t *pixels; + const unsigned char *original_data_source; +}; +struct sprite *get_decompressed_sprite(const unsigned char *spritedata); + // used during rendering // pixel is 0xRRGGBB extern uint32_t *curfb; diff --git a/pinetab2_framework.cpp b/pinetab2_framework.cpp index b65ab5d..6da73ab 100644 --- a/pinetab2_framework.cpp +++ b/pinetab2_framework.cpp @@ -221,7 +221,7 @@ struct object *scene_add_object(struct scene *sc, int id, int x, int y, int widt objects[i].y = y; objects[i].width = width; objects[i].height = height; - objects[i].pixels = pixels; + objects[i].pixels = pixels ? (const char*)get_decompressed_sprite((const unsigned char*)pixels)->pixels : nullptr; return &objects[i]; } } diff --git a/textbox.cpp b/textbox.cpp index 7ef181c..269f6f5 100644 --- a/textbox.cpp +++ b/textbox.cpp @@ -7,7 +7,7 @@ static const char *textbox_text; static int tb_width_chars, tb_height_chars; static int visible_chars = 0; -extern const char _binary_sprite_font_raw_start[]; +extern const unsigned char _binary_sprite_font_raw_start[]; @@ -16,6 +16,9 @@ extern const char _binary_sprite_font_raw_start[]; #define LINESPACE 8 static void textbox_render_fn(struct scene *s) { + + struct sprite *fontsprite = get_decompressed_sprite(_binary_sprite_font_raw_start); + int boxheight = tb_height_chars*(CHARSIZE + LINESPACE) - LINESPACE; int boxwidth = tb_width_chars*(CHARSIZE + HSPACE) - HSPACE; int x = (1280 - tb_width_chars*CHARSIZE)/2; @@ -31,7 +34,7 @@ static void textbox_render_fn(struct scene *s) { x = xleft; y += CHARSIZE + LINESPACE; } else { - blit(x, y, CHARSIZE, CHARSIZE, (*str - 32)*(CHARSIZE*CHARSIZE) + (uint32_t*)_binary_sprite_font_raw_start); + blit(x, y, CHARSIZE, CHARSIZE, (*str - 32)*(CHARSIZE*CHARSIZE) + (uint32_t*)fontsprite->pixels); x += CHARSIZE + HSPACE; } }