diff --git a/data/.DS_Store b/data/.DS_Store new file mode 100644 index 00000000..2b86eef4 Binary files /dev/null and b/data/.DS_Store differ diff --git a/data/DIGISND1.DAT b/data/DIGISND1.DAT index 94bba78f..e588dea2 100644 Binary files a/data/DIGISND1.DAT and b/data/DIGISND1.DAT differ diff --git a/data/DIGISND2.DAT b/data/DIGISND2.DAT index 383df928..13b3a6cf 100644 Binary files a/data/DIGISND2.DAT and b/data/DIGISND2.DAT differ diff --git a/data/DIGISND3.DAT b/data/DIGISND3.DAT index 959df282..70585f74 100644 Binary files a/data/DIGISND3.DAT and b/data/DIGISND3.DAT differ diff --git a/data/FAT.DAT b/data/FAT.DAT new file mode 100644 index 00000000..5cf10fd1 Binary files /dev/null and b/data/FAT.DAT differ diff --git a/data/IBM_SND1.DAT b/data/IBM_SND1.DAT new file mode 100644 index 00000000..568d85f6 Binary files /dev/null and b/data/IBM_SND1.DAT differ diff --git a/data/IBM_SND2.DAT b/data/IBM_SND2.DAT new file mode 100644 index 00000000..225c4b92 Binary files /dev/null and b/data/IBM_SND2.DAT differ diff --git a/data/KID.DAT b/data/KID.DAT new file mode 100644 index 00000000..cb74de44 Binary files /dev/null and b/data/KID.DAT differ diff --git a/data/LEVELS.DAT b/data/LEVELS.DAT new file mode 100644 index 00000000..3211153c Binary files /dev/null and b/data/LEVELS.DAT differ diff --git a/data/MIDISND1.DAT b/data/MIDISND1.DAT index ba2e3b49..006e6692 100644 Binary files a/data/MIDISND1.DAT and b/data/MIDISND1.DAT differ diff --git a/data/MIDISND2.DAT b/data/MIDISND2.DAT index 08bc69e9..8eacc9d0 100644 Binary files a/data/MIDISND2.DAT and b/data/MIDISND2.DAT differ diff --git a/data/PRINCE.DAT b/data/PRINCE.DAT new file mode 100644 index 00000000..05bc4706 Binary files /dev/null and b/data/PRINCE.DAT differ diff --git a/data/PV.DAT b/data/PV.DAT new file mode 100644 index 00000000..ced42450 Binary files /dev/null and b/data/PV.DAT differ diff --git a/data/SHADOW.DAT b/data/SHADOW.DAT new file mode 100644 index 00000000..cda84971 Binary files /dev/null and b/data/SHADOW.DAT differ diff --git a/data/SKEL.DAT b/data/SKEL.DAT new file mode 100644 index 00000000..77810031 Binary files /dev/null and b/data/SKEL.DAT differ diff --git a/data/TITLE.DAT b/data/TITLE.DAT new file mode 100644 index 00000000..782fe360 Binary files /dev/null and b/data/TITLE.DAT differ diff --git a/data/VDUNGEON.DAT b/data/VDUNGEON.DAT new file mode 100644 index 00000000..6916f433 Binary files /dev/null and b/data/VDUNGEON.DAT differ diff --git a/data/VIZIER.DAT b/data/VIZIER.DAT new file mode 100644 index 00000000..1e3b7181 Binary files /dev/null and b/data/VIZIER.DAT differ diff --git a/data/VPALACE.DAT b/data/VPALACE.DAT new file mode 100644 index 00000000..7f2d4e59 Binary files /dev/null and b/data/VPALACE.DAT differ diff --git a/gba/.DS_Store b/gba/.DS_Store new file mode 100644 index 00000000..117c365f Binary files /dev/null and b/gba/.DS_Store differ diff --git a/gba/Makefile b/gba/Makefile new file mode 100644 index 00000000..cd769c0a --- /dev/null +++ b/gba/Makefile @@ -0,0 +1,118 @@ +#--------------------------------------------------------------------------------- +# SDLPoP for Game Boy Advance (devkitARM) +#--------------------------------------------------------------------------------- +ifeq ($(strip $(DEVKITARM)),) +$(error "DEVKITARM is not set. Install devkitPro/devkitARM and source devkitarm.") +endif + +include $(DEVKITARM)/gba_rules + +#--------------------------------------------------------------------------------- +TARGET := SDLPoP_gba +BUILD := build +SOURCES := src ../src +INCLUDES := include ../src +DATA := data +ASSETS := assets + +GAME_TITLE := SDLPOP +GAME_CODE := SLPP +MAKER_CODE := PP +GAME_VER := 0 + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -mthumb -mthumb-interwork +CFLAGS := -g -Wall -O2 -mcpu=arm7tdmi -mtune=arm7tdmi -fomit-frame-pointer \ + -ffast-math -fno-strict-aliasing \ + $(ARCH) +CFLAGS += $(INCLUDE) -DARM9 -D__GBA__ -DGBA -DPOP_GBA + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +ASFLAGS := -g $(ARCH) +LDFLAGS := -g $(ARCH) -Wl,-Map,$(notdir $*.map) \ + -L$(CURDIR) -B$(CURDIR) \ + -Wl,--wrap=free \ + -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc \ + -Wl,--wrap=snprintf -Wl,--wrap=vsnprintf \ + -Wl,--wrap=sprintf -Wl,--wrap=vsprintf + +LIBS := -lmm -lgba -lm +LIBDIRS := $(LIBGBA) + +# gba_rules invokes $(LD) with gcc-style flags + -specs=gba.specs, so LD must +# be the gcc driver, not bare ld. +LD := $(CC) + +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- +export OUTPUT := $(CURDIR)/$(TARGET) +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) \ + $(foreach dir,$(ASSETS),$(CURDIR)/$(dir)) +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) \ + $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.bin))) + +# Exclude SDLPoP source files that we replace or that pull in dependencies +# we can't satisfy on GBA. The rest of the upstream code compiles unchanged. +EXCLUDE_CFILES := main.c midi.c opl3.c lighting.c screenshot.c \ + replay.c stb_vorbis.c +CFILES := $(filter-out $(EXCLUDE_CFILES),$(CFILES)) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +.PHONY: $(BUILD) clean all + +#--------------------------------------------------------------------------------- +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +clean: + @echo Cleaning... + @rm -fr $(BUILD) $(TARGET).elf $(TARGET).gba $(TARGET).sav + +#--------------------------------------------------------------------------------- +else + +DEPENDS := $(OFILES:.o=.d) + +$(OUTPUT).gba : $(OUTPUT).elf + $(SILENTCMD)$(OBJCOPY) -O binary $< $@ + $(SILENTCMD)gbafix $@ -t$(GAME_TITLE) -c$(GAME_CODE) -m$(MAKER_CODE) -r$(GAME_VER) + @echo built ... $(notdir $@) + +# Override the default link rule: we use our own linker script that puts +# .data / .bss in EWRAM, and our own specs so the stock gba_cart.ld is not +# picked up (which would conflict and force .data into IWRAM). +$(OUTPUT).elf : $(OFILES) + $(SILENTMSG) linking cartridge + $(SILENTCMD)$(LD) $(LDFLAGS) -B$(CURDIR)/.. -specs=$(CURDIR)/../gba_ewram.specs \ + $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + +$(OFILES_SRC) : $(HFILES) + +%_bin.h %.bin.o : %.bin + @echo $(notdir $<) + $(SILENTCMD)$(bin2o) + +-include $(DEPENDS) + +endif +#--------------------------------------------------------------------------------- diff --git a/gba/README.md b/gba/README.md new file mode 100644 index 00000000..69e72b9a --- /dev/null +++ b/gba/README.md @@ -0,0 +1,153 @@ +# SDLPoP — Game Boy Advance port (devkitPro/devkitARM) + +This is a port of [SDLPoP](https://github.com/NagyD/SDLPoP) (Prince of Persia) +to the Nintendo Game Boy Advance, using **devkitARM** + **libgba** + +**maxmod**. + +It is built from the upstream sources with: + +- a **SDL2 compatibility shim** (`gba/include/SDL2/SDL.h`, + `gba/src/sdl_shim.c`) covering the surface, palette, rwops, event, timing + and audio APIs that SDLPoP touches; +- **direct hardware drivers** for Mode-4 video (`gba_video.c`), DirectSound A + PCM audio (`gba_audio.c`), buttons (`gba_input.c`), 60 Hz VBlank ticking + (`gba_time.c`), 32 KB SRAM saves (`gba_save.c`), and ROM-resident data + blobs (`gba_file.c`); +- **`#ifndef __GBA__`** guards around a handful of subsystems that won't fit + on a 16.78 MHz ARM7 with 256 KB EWRAM (OPL3, MIDI, stb_vorbis, lighting, + screenshot, menu, replay, quicksave, fade, fast-forward, debug cheats, + dark transition, teleports). + +The unmodified upstream source files in `../src` compile as-is once +`__GBA__` is defined and the SDL include path is the shim. + +## Limitations vs. desktop SDLPoP + +- **Resolution**: 240×160 (GBA hardware) vs. 320×200 (original). A camera + scroll keeps the prince in view. The original game world still runs at + 320×192 internally. +- **Audio**: digitized SFX only (DIGISND*.DAT), played as 8-bit PCM via + DirectSound A on DMA1 at 18 157 Hz. Adlib music, PC-speaker beeps, MIDI + and OGG music are **dropped**. You can optionally prerender music as + raw 8-bit signed PCM and feed it via `gba_music_play()` from a mod. +- **Save**: the original `PRINCE.SAV` file is redirected to cartridge SRAM + (32 KB). Emulators persist this as `SDLPoP_gba.sav`. +- **No menu / replay / screenshot / lighting / fade / fast-forward / debug + cheats**: dropped to save code size and RAM. +- **Hall of Fame**: writes to SRAM (same blob namespace as save). + +## Build prerequisites + +1. **devkitPro** with the `gba-dev` group: + ```bash + # macOS (with Homebrew): + brew install --cask devkitpro/devkitpro/devkitpro-pacman + sudo dkp-pacman -S gba-dev + # Linux: see https://devkitpro.org/wiki/Getting_Started + ``` +2. Source the devkitARM environment: + ```bash + source /opt/devkitpro/devkitarm.sh # or wherever you installed it + ``` +3. Have a copy of the **Prince of Persia v1.0/1.1 DOS data files** + (`KID`, `PRINCE`, `TITLE`, `LEVELS`, `GUARD*`, `V*`, `DIGISND*.DAT`, + etc.). The SDLPoP source ships them in `../data/`. + +## Build + +```bash +cd gba +./tools/prep_data.sh # copies ../data/* -> data/*.bin +make +``` + +Produces `SDLPoP_gba.gba`, ready to run in any GBA emulator (mGBA, no$gba, +VBA-M) or to flash to a flashcart. + +## Layout + +``` +gba/ +├── Makefile # devkitARM build +├── README.md # this file +├── include/ +│ ├── SDL2/SDL.h # minimal SDL2 shim header (used in place of real SDL2) +│ ├── SDL2/SDL_image.h # IMG_Load returns NULL +│ └── gba_port.h # cross-module bindings +├── src/ +│ ├── sdl_shim.c # SDL_Surface, blits, palette, RWops, audio, timing +│ ├── gba_main.c # entry point (main): init libgba, call pop_main() +│ ├── gba_video.c # Mode-4 framebuffer + viewport panning +│ ├── gba_audio.c # DMA1 8-bit PCM mixer (6 voices) +│ ├── gba_sound_bridge.c # overrides SDLPoP's play_sound_from_buffer +│ ├── gba_input.c # KEYINPUT -> SDL scancodes +│ ├── gba_time.c # VBlank tick counter + SDLPoP frame timer +│ ├── gba_file.c # fopen/fread over linked DAT blobs, save -> SRAM +│ ├── gba_save.c # SRAM read/write + Fletcher-32 checksum +│ └── gba_viewport.c # camera follows the prince +├── tools/ +│ └── prep_data.sh # copies ../data/* to data/*.bin +└── data/ # populated by prep_data.sh +``` + +## Source modifications + +Three upstream files have **small** `__GBA__`-gated additions: + +- `src/common.h` — preprocessor macros for `fopen`/`fread`/`stat` (re-route + to `gba_fopen` etc). +- `src/config.h` — disable `USE_LIGHTING`, `USE_SCREENSHOT`, `USE_MENU`, + `USE_REPLAY`, `USE_QUICKSAVE`, `USE_FADE`, `USE_DARK_TRANSITION`, + `USE_TELEPORTS`, `USE_AUTO_INPUT_MODE`, `USE_FAST_FORWARD`, + `USE_DEBUG_CHEATS` on `__GBA__`. The fixes are also gated by + `DISABLE_ALL_FIXES`. +- `src/seg009.c` — wraps the SDL audio implementation (`#ifndef __GBA__`) + and the SDL_Renderer-based `set_gr_mode`/`update_screen` with an `#else` + branch that creates an 8 bpp paletted surface and feeds it to + `gba_present_surface()`. + +The other ~33 000 lines of upstream source compile unmodified. + +## Controls + +| GBA button | SDL scancode | Action | +|------------|--------------|-----------------------| +| D-Pad | Arrow keys | Move / jump / crouch | +| A | Shift | Grab / draw sword | +| B | Space | (unused / pause) | +| L | Ctrl | Chop / fight strike | +| R | Alt | Modifier | +| Start | Enter | Advance dialog | +| Select | Backspace | Cancel | + +## What is NOT yet wired + +- **Music**: only SFX is fed to the mixer. To add music, place a raw 8-bit + signed PCM stream as `data/music.bin` and call + `gba_music_play(music_bin, music_bin_size, 1)` at the start of each + level (see `gba_audio.c`). A music conversion pipeline isn't included. +- **Sound resampling**: digitized SFX is played at the rate stored in the + DIGISND header; the mixer plays at a fixed 18 157 Hz, so pitch is + approximate. To improve, add a nearest-neighbour resampler in + `play_digi_sound()` in `gba_sound_bridge.c`. +- **The `chtab` sprite cache** is loaded into EWRAM on demand by upstream + code. With KID + PRINCE + GUARD + VPALACE + VDUNGEON simultaneously + active you can exceed 256 KB; if you hit OOM crashes, see + `load_chtab_from_file` in `seg009.c` and consider releasing earlier + chtabs or moving them into IWRAM/ROM. +- **Lighting**, **fade**, **dark room transition** — disabled. If you want + fade-in/out, re-enable `USE_FADE` and live with a slower CPU budget. + +## Troubleshooting + +- `gba.h: file not found` from your IDE — your editor is finding the host + SDL2/clang headers, not devkitARM's. Build with `make`, not the IDE. +- `undefined reference to '_kid_bin_size'` at link time — you didn't run + `./tools/prep_data.sh`, so the linker has no blobs to attach. Run it + and rebuild. +- The ROM boots to a black screen — typical first-run hang because some + sprite chtab failed to load. Try with the original `data/` files only, + no mods. +- Saves don't persist in your emulator — make sure the emulator is + configured to write `.sav` files alongside the ROM (mGBA does by + default; some emulators need `--sram`). diff --git a/gba/SDLPoP_gba.gba b/gba/SDLPoP_gba.gba new file mode 100755 index 00000000..6209e734 Binary files /dev/null and b/gba/SDLPoP_gba.gba differ diff --git a/gba/bin2c.c b/gba/bin2c.c new file mode 100644 index 00000000..73dc2532 --- /dev/null +++ b/gba/bin2c.c @@ -0,0 +1,288 @@ +/* +BSD 2-Clause License + +Copyright (c) 2016, Rafael Kitover +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include +#include +#include +#ifndef _MSC_VER +#include +#endif +#include + +#define BUF_SIZE 4096 +#define WBUF_SIZE BUF_SIZE * sizeof(wchar_t) +#define MSG_SIZE 256 + +const char* version = "0.3"; + +const char* msg_prefix = "bin2c: "; + +void format_perror(const char* fmt, va_list args) { + static char error_str[MSG_SIZE]; + static char fmt_str[MSG_SIZE]; +#if __STDC_WANT_SECURE_LIB__ + strcpy_s(fmt_str, sizeof(fmt_str), msg_prefix); + strncat_s(fmt_str, sizeof(fmt_str), fmt, MSG_SIZE - strlen(msg_prefix)); + vsprintf_s(error_str, MSG_SIZE, fmt_str, args); +#else + strcpy(fmt_str, msg_prefix); + strncat(fmt_str, fmt, MSG_SIZE - strlen(msg_prefix)); + vsnprintf(error_str, MSG_SIZE, fmt_str, args); +#endif + perror(error_str); +} + +void die(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + format_perror(fmt, args); + va_end(args); + exit(1); +} + +void* safe_malloc(size_t size) { + void* allocated = malloc(size); + if (!allocated) { + die("out of memory"); + } + return allocated; +} + +char* file_name_to_identifier(const char* file_name_cstr) { + wchar_t* file_name = safe_malloc(WBUF_SIZE); + wchar_t* identifier = safe_malloc(WBUF_SIZE); + char* identifier_cstr = safe_malloc(WBUF_SIZE); + wchar_t* file_name_ptr = file_name; + wchar_t* identifier_ptr = identifier; + wchar_t* file_name_end = NULL; + size_t file_name_len = 0; + int between_tokens = 0; + +#if __STDC_WANT_SECURE_LIB__ + mbstowcs_s(&file_name_len, file_name, WBUF_SIZE, file_name_cstr, BUF_SIZE - 1); +#else + file_name_len = mbstowcs(file_name, file_name_cstr, BUF_SIZE - 1); +#endif + if (file_name_len == (size_t)(-1)) { + die("cannot convert '%s' to locale representation", file_name_cstr); + } + + *identifier = 0; + + file_name_end = file_name + file_name_len + 1; + + while (file_name_ptr < file_name_end) { + if (iswalnum(*file_name_ptr)) { + *identifier_ptr = *file_name_ptr; + identifier_ptr++; + between_tokens = 0; + } + else if (!between_tokens) { +#if __STDC_WANT_SECURE_LIB__ + size_t identifier_ptr_sz = 0; + mbstowcs_s(&identifier_ptr_sz, identifier_ptr, WBUF_SIZE, "_", 1); +#else + mbstowcs(identifier_ptr, "_", 1); +#endif + identifier_ptr++; + between_tokens = 1; + } + + file_name_ptr++; + } + + /* terminate identifier, on _ or on next position */ + if (between_tokens) identifier_ptr--; + + *identifier_ptr = 0; + +#if __STDC_WANT_SECURE_LIB__ + size_t identifier_cstr_sz = 0; + wcstombs_s(&identifier_cstr_sz, identifier_cstr, BUF_SIZE, identifier, WBUF_SIZE - 1); + if (identifier_cstr_sz == (size_t)(-1)) +#else + if (wcstombs(identifier_cstr, identifier, WBUF_SIZE - 1) == (size_t)(-1)) +#endif + die("failed to convert wide character string to bytes"); + + free(file_name); + free(identifier); + + return identifier_cstr; +} + +void usage(int err) { + FILE* stream = err ? stderr : stdout; + + fputs( +"Usage: bin2c [IN_FILE [OUT_FILE [VAR_NAME]]]\n" +"Write IN_FILE as a C array of bytes named VAR_NAME into OUT_FILE.\n" +"\n" +"By default, STDIN is the input and STDOUT is the output, either can be explicitly specified with -.\n" +"\n" +"The default VAR_NAME is the IN_FILE name converted to an identifier, or \"resource_data\"\n" +"if it's STDIN.\n" +"\n" +" -h, --help Show this help screen and exit.\n" +" -v, --version Print version and exit.\n" +"\n" +"Examples:\n" +" # write data from file ./compiled-resources.xrs into header file ./resources.h using variable name resource_data\n" +" bin2c ./compiled-resources.xrs ./resources.h resource_data\n" +" # write data from STDIN to STDOUT with \"resource_data\" as the VAR_NAME\n" +" bin2c\n" +" # write data from ./compiled-resources.xrs to STDOUT with \"compiled_resources_xrs\" as the VAR_NAME\n" +" bin2c ./compiled-resources.xrs\n" +" # write data from ./compiled-resources.xrs to ./resources.h with \"compiled_resources_xrs\" as the VAR_NAME\n" +" bin2c ./compiled-resources.xrs ./resources.h\n" +"\n" +"Project homepage and documentation: <http://github.com/rkitover/bin2c>\n" + , stream); +} + +void die_usage(const char* fmt, ...) { + static char fmt_str[MSG_SIZE]; + va_list args; + va_start(args, fmt); +#if __STDC_WANT_SECURE_LIB__ + strcpy_s(fmt_str, sizeof(fmt_str), msg_prefix); +#else + strcpy(fmt_str, msg_prefix); +#endif + // Need to reserve 1 byte for the newline. +#if __STDC_WANT_SECURE_LIB__ + strncat_s(fmt_str, sizeof(fmt_str), fmt, MSG_SIZE - strlen(msg_prefix) - 1); + strcat_s(fmt_str, sizeof(fmt_str), "\n"); +#else + strncat(fmt_str, fmt, MSG_SIZE - strlen(msg_prefix) - 1); + strcat(fmt_str, "\n"); +#endif + vfprintf(stderr, fmt_str, args); + va_end(args); + usage(1); + exit(1); +} + +void exit_usage(int exit_code) { + usage(exit_code); + exit(exit_code); +} + +int main(int argc, const char** argv) { + const char* usage_opts[] = {"-h", "--help"}; + const char* version_opts[] = {"-v", "--version"}; + const char* in_file_name = argc >= 2 ? argv[1] : NULL; + const char* out_file_name = argc >= 3 ? argv[2] : NULL; + const char* var_name = argc >= 4 ? argv[3] : NULL; + char* computed_identifier = NULL; + size_t i = 0; + int file_pos = 0; + size_t bytes_read = 0; + unsigned char* buf = safe_malloc(BUF_SIZE); + FILE *in_file, *out_file; + + setlocale(LC_ALL, ""); + + if (argc > 4) + die_usage("invalid number of arguments"); + + if (argc >= 2) { + for (i = 0; i < (sizeof(usage_opts)/sizeof(char*)); i++) { + if (!strcmp(argv[1], usage_opts[i])) exit_usage(0); + } + + for (i = 0; i < (sizeof(version_opts)/sizeof(char*)); i++) { + if (!strcmp(argv[1], version_opts[i])) { + printf("bin2c %s\n", version); + return 0; + } + } + } + + if (!in_file_name || !strcmp(in_file_name, "-")) { + in_file = stdin; + } else { +#if __STDC_WANT_SECURE_LIB__ + fopen_s(&in_file, in_file_name, "rb"); +#else + in_file = fopen(in_file_name, "rb"); +#endif + if (!in_file) { + die("can't open input file '%s'", in_file_name); + } + } + + if (!out_file_name || !strcmp(out_file_name, "-")) { + out_file = stdout; + } else { +#if __STDC_WANT_SECURE_LIB__ + fopen_s(&out_file, out_file_name, "w"); +#else + out_file = fopen(out_file_name, "w"); +#endif + if (!out_file) { + die("can't open output file '%s'", out_file_name); + } + } + + if (in_file_name && !var_name) + var_name = computed_identifier = file_name_to_identifier(in_file_name); + + fprintf(out_file, "/* generated from %s: do not edit */\n" + "const unsigned char %s[] = {\n", + in_file_name ? in_file_name : "resource data", + var_name ? var_name : "resource_data" + ); + + bytes_read = fread(buf, 1, BUF_SIZE, in_file); + while (bytes_read != 0) { + for (i = 0; i < bytes_read; i++) { + char* comma = bytes_read < BUF_SIZE && i == bytes_read - 1 ? "" : ","; + + fprintf(out_file, "0x%02x%s", buf[i], comma); + + file_pos++; + if (file_pos % 16 == 0) { + fputc('\n', out_file); + } + } + bytes_read = fread(buf, 1, BUF_SIZE, in_file); + } + + fputs("};\n", out_file); + + free(buf); + free(computed_identifier); + fclose(in_file); + fclose(out_file); + + return 0; +} diff --git a/gba/data/DIGISND1.DAT b/gba/data/DIGISND1.DAT new file mode 100644 index 00000000..e588dea2 Binary files /dev/null and b/gba/data/DIGISND1.DAT differ diff --git a/gba/data/DIGISND2.DAT b/gba/data/DIGISND2.DAT new file mode 100644 index 00000000..13b3a6cf Binary files /dev/null and b/gba/data/DIGISND2.DAT differ diff --git a/gba/data/DIGISND3.DAT b/gba/data/DIGISND3.DAT new file mode 100644 index 00000000..70585f74 Binary files /dev/null and b/gba/data/DIGISND3.DAT differ diff --git a/gba/data/FAT.DAT b/gba/data/FAT.DAT new file mode 100644 index 00000000..5cf10fd1 Binary files /dev/null and b/gba/data/FAT.DAT differ diff --git a/gba/data/GUARD.DAT b/gba/data/GUARD.DAT new file mode 100644 index 00000000..753f54b9 Binary files /dev/null and b/gba/data/GUARD.DAT differ diff --git a/gba/data/GUARD1.DAT b/gba/data/GUARD1.DAT new file mode 100644 index 00000000..a13c0b06 Binary files /dev/null and b/gba/data/GUARD1.DAT differ diff --git a/gba/data/GUARD2.DAT b/gba/data/GUARD2.DAT new file mode 100644 index 00000000..ddc3ba6c Binary files /dev/null and b/gba/data/GUARD2.DAT differ diff --git a/gba/data/IBM_SND1.DAT b/gba/data/IBM_SND1.DAT new file mode 100644 index 00000000..568d85f6 Binary files /dev/null and b/gba/data/IBM_SND1.DAT differ diff --git a/gba/data/IBM_SND2.DAT b/gba/data/IBM_SND2.DAT new file mode 100644 index 00000000..225c4b92 Binary files /dev/null and b/gba/data/IBM_SND2.DAT differ diff --git a/gba/data/KID.DAT b/gba/data/KID.DAT new file mode 100644 index 00000000..cb74de44 Binary files /dev/null and b/gba/data/KID.DAT differ diff --git a/gba/data/LEVELS.DAT b/gba/data/LEVELS.DAT new file mode 100644 index 00000000..3211153c Binary files /dev/null and b/gba/data/LEVELS.DAT differ diff --git a/gba/data/MIDISND1.DAT b/gba/data/MIDISND1.DAT new file mode 100644 index 00000000..006e6692 Binary files /dev/null and b/gba/data/MIDISND1.DAT differ diff --git a/gba/data/MIDISND2.DAT b/gba/data/MIDISND2.DAT new file mode 100644 index 00000000..8eacc9d0 Binary files /dev/null and b/gba/data/MIDISND2.DAT differ diff --git a/gba/data/PRINCE.DAT b/gba/data/PRINCE.DAT new file mode 100644 index 00000000..05bc4706 Binary files /dev/null and b/gba/data/PRINCE.DAT differ diff --git a/gba/data/PV.DAT b/gba/data/PV.DAT new file mode 100644 index 00000000..ced42450 Binary files /dev/null and b/gba/data/PV.DAT differ diff --git a/gba/data/SHADOW.DAT b/gba/data/SHADOW.DAT new file mode 100644 index 00000000..cda84971 Binary files /dev/null and b/gba/data/SHADOW.DAT differ diff --git a/gba/data/SKEL.DAT b/gba/data/SKEL.DAT new file mode 100644 index 00000000..77810031 Binary files /dev/null and b/gba/data/SKEL.DAT differ diff --git a/gba/data/TITLE.DAT b/gba/data/TITLE.DAT new file mode 100644 index 00000000..782fe360 Binary files /dev/null and b/gba/data/TITLE.DAT differ diff --git a/gba/data/VDUNGEON.DAT b/gba/data/VDUNGEON.DAT new file mode 100644 index 00000000..6916f433 Binary files /dev/null and b/gba/data/VDUNGEON.DAT differ diff --git a/gba/data/VIZIER.DAT b/gba/data/VIZIER.DAT new file mode 100644 index 00000000..1e3b7181 Binary files /dev/null and b/gba/data/VIZIER.DAT differ diff --git a/gba/data/VPALACE.DAT b/gba/data/VPALACE.DAT new file mode 100644 index 00000000..7f2d4e59 Binary files /dev/null and b/gba/data/VPALACE.DAT differ diff --git a/gba/data/digisnd1.bin b/gba/data/digisnd1.bin new file mode 100644 index 00000000..229b464e Binary files /dev/null and b/gba/data/digisnd1.bin differ diff --git a/gba/data/digisnd2.bin b/gba/data/digisnd2.bin new file mode 100644 index 00000000..c82304cd Binary files /dev/null and b/gba/data/digisnd2.bin differ diff --git a/gba/data/digisnd3.bin b/gba/data/digisnd3.bin new file mode 100644 index 00000000..2f0f006d Binary files /dev/null and b/gba/data/digisnd3.bin differ diff --git a/gba/data/fat.bin b/gba/data/fat.bin new file mode 100644 index 00000000..5ca4a765 Binary files /dev/null and b/gba/data/fat.bin differ diff --git a/gba/data/font.bin b/gba/data/font.bin new file mode 100644 index 00000000..1b1cb4d4 Binary files /dev/null and b/gba/data/font.bin differ diff --git a/gba/data/guard.bin b/gba/data/guard.bin new file mode 100644 index 00000000..63001ac7 Binary files /dev/null and b/gba/data/guard.bin differ diff --git a/gba/data/guard1.bin b/gba/data/guard1.bin new file mode 100644 index 00000000..a13c0b06 Binary files /dev/null and b/gba/data/guard1.bin differ diff --git a/gba/data/guard2.bin b/gba/data/guard2.bin new file mode 100644 index 00000000..ddc3ba6c Binary files /dev/null and b/gba/data/guard2.bin differ diff --git a/gba/data/ibm_snd1.bin b/gba/data/ibm_snd1.bin new file mode 100644 index 00000000..568d85f6 Binary files /dev/null and b/gba/data/ibm_snd1.bin differ diff --git a/gba/data/ibm_snd2.bin b/gba/data/ibm_snd2.bin new file mode 100644 index 00000000..225c4b92 Binary files /dev/null and b/gba/data/ibm_snd2.bin differ diff --git a/gba/data/kid.bin b/gba/data/kid.bin new file mode 100644 index 00000000..4c674dcc Binary files /dev/null and b/gba/data/kid.bin differ diff --git a/gba/data/levels.bin b/gba/data/levels.bin new file mode 100644 index 00000000..3211153c Binary files /dev/null and b/gba/data/levels.bin differ diff --git a/gba/data/midisnd1.bin b/gba/data/midisnd1.bin new file mode 100644 index 00000000..006e6692 Binary files /dev/null and b/gba/data/midisnd1.bin differ diff --git a/gba/data/midisnd2.bin b/gba/data/midisnd2.bin new file mode 100644 index 00000000..8eacc9d0 Binary files /dev/null and b/gba/data/midisnd2.bin differ diff --git a/gba/data/musicpcm.bin b/gba/data/musicpcm.bin new file mode 100644 index 00000000..cf74fc56 Binary files /dev/null and b/gba/data/musicpcm.bin differ diff --git a/gba/data/prince.bin b/gba/data/prince.bin new file mode 100644 index 00000000..14d1c159 Binary files /dev/null and b/gba/data/prince.bin differ diff --git a/gba/data/pv.bin b/gba/data/pv.bin new file mode 100644 index 00000000..da60a6a2 Binary files /dev/null and b/gba/data/pv.bin differ diff --git a/gba/data/shadow.bin b/gba/data/shadow.bin new file mode 100644 index 00000000..56554a2d Binary files /dev/null and b/gba/data/shadow.bin differ diff --git a/gba/data/skel.bin b/gba/data/skel.bin new file mode 100644 index 00000000..48e4d3ad Binary files /dev/null and b/gba/data/skel.bin differ diff --git a/gba/data/title.bin b/gba/data/title.bin new file mode 100644 index 00000000..db65b318 Binary files /dev/null and b/gba/data/title.bin differ diff --git a/gba/data/vdungeon.bin b/gba/data/vdungeon.bin new file mode 100644 index 00000000..47bfc1db Binary files /dev/null and b/gba/data/vdungeon.bin differ diff --git a/gba/data/vizier.bin b/gba/data/vizier.bin new file mode 100644 index 00000000..d5f85c5e Binary files /dev/null and b/gba/data/vizier.bin differ diff --git a/gba/data/vpalace.bin b/gba/data/vpalace.bin new file mode 100644 index 00000000..ac0d28cb Binary files /dev/null and b/gba/data/vpalace.bin differ diff --git a/gba/gba_ewram.ld b/gba/gba_ewram.ld new file mode 100644 index 00000000..d73c94d3 --- /dev/null +++ b/gba/gba_ewram.ld @@ -0,0 +1,241 @@ +/* SDLPoP for GBA: relocates the giant SDLPoP .data/.bss out of the 32 KB + IWRAM into the 256 KB EWRAM. Based on devkitARM's stock gba_cart.ld v1.5. + Sections kept in IWRAM: .iwram (explicit), small overlays, stack/IRQ. */ + +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm") +OUTPUT_ARCH(arm) +ENTRY(_start) + +MEMORY { + rom : ORIGIN = 0x08000000, LENGTH = 32M + iwram : ORIGIN = 0x03000000, LENGTH = 32K + ewram : ORIGIN = 0x02000000, LENGTH = 256K +} + +__text_start = ORIGIN(rom); +__eheap_end = ORIGIN(ewram) + LENGTH(ewram); +__iwram_start = ORIGIN(iwram); +__iwram_top = ORIGIN(iwram) + LENGTH(iwram); +__sp_irq = __iwram_top - 0x060; +__sp_usr = __sp_irq - 0x0a0; +__irq_flags = 0x03007ff8; + +SECTIONS +{ + . = __text_start; + + .crt0 : { KEEP (*(.crt0)) . = ALIGN(4); } >rom = 0xff + .init : { KEEP (*(SORT_NONE(.init))) } >rom + .plt : { *(.plt) . = ALIGN(4); } >rom + + .text : + { + *(EXCLUDE_FILE (*.iwram*) .text*) + *(.gnu.linkonce.t.*) + KEEP (*(.text.*personality*)) + *(.gnu.warning) + *(.glue_7t) *(.glue_7) *(.vfp11_veneer) + . = ALIGN(4); + } >rom = 0xff + + __text_end = .; + + .fini : { KEEP (*(.fini)) . = ALIGN(4); } >rom = 0 + + .rodata : + { + *(.rodata) + *all.rodata*(*) + *(.roda) + *(.rodata.*) + *(.gnu.linkonce.r*) + SORT(CONSTRUCTORS) + . = ALIGN(4); + } >rom = 0xff + + .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >rom + __exidx_start = .; + .ARM.exidx : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) } >rom + __exidx_end = .; + + .ctors : + { + KEEP (*crtbegin.o(.ctors)) + KEEP (*(EXCLUDE_FILE (*crtend.o) .ctors)) + KEEP (*(SORT(.ctors.*))) + KEEP (*(.ctors)) + . = ALIGN(4); + } >rom = 0 + + .dtors : + { + KEEP (*crtbegin.o(.dtors)) + KEEP (*(EXCLUDE_FILE (*crtend.o) .dtors)) + KEEP (*(SORT(.dtors.*))) + KEEP (*(.dtors)) + . = ALIGN(4); + } >rom = 0 + + .eh_frame : { KEEP (*(.eh_frame)) . = ALIGN(4); } >rom = 0 + .gcc_except_table : { *(.gcc_except_table) . = ALIGN(4); } >rom = 0 + + __iwram_lma = .; + + .iwram __iwram_start : AT (__iwram_lma) + { + __iwram_start__ = ABSOLUTE(.); + *(.iwram .iwram*) + *iwram.*(.text* .data*) + . = ALIGN(4); + __iwram_end__ = ABSOLUTE(.); + } >iwram = 0xff + + __data_lma = __iwram_lma + SIZEOF(.iwram); + __iwram_overlay_lma = __data_lma; + __iwram_overlay_start = .; + __iwram_overlay_end = .; + __load_start_iwram0 = __iwram_overlay_start; + __load_stop_iwram0 = __iwram_overlay_end; + __load_start_iwram1 = __iwram_overlay_start; + __load_stop_iwram1 = __iwram_overlay_end; + __load_start_iwram2 = __iwram_overlay_start; + __load_stop_iwram2 = __iwram_overlay_end; + __load_start_iwram3 = __iwram_overlay_start; + __load_stop_iwram3 = __iwram_overlay_end; + __load_start_iwram4 = __iwram_overlay_start; + __load_stop_iwram4 = __iwram_overlay_end; + __load_start_iwram5 = __iwram_overlay_start; + __load_stop_iwram5 = __iwram_overlay_end; + __load_start_iwram6 = __iwram_overlay_start; + __load_stop_iwram6 = __iwram_overlay_end; + __load_start_iwram7 = __iwram_overlay_start; + __load_stop_iwram7 = __iwram_overlay_end; + __load_start_iwram8 = __iwram_overlay_start; + __load_stop_iwram8 = __iwram_overlay_end; + __load_start_iwram9 = __iwram_overlay_start; + __load_stop_iwram9 = __iwram_overlay_end; + + . = ORIGIN(ewram); + + /* .bss in EWRAM */ + .bss ALIGN(4) (NOLOAD) : + { + __bss_start = ABSOLUTE(.); + __bss_start__ = ABSOLUTE(.); + *(.dynbss) + *(.gnu.linkonce.b*) + *(.bss*) + *(COMMON) + . = ALIGN(4); + __bss_end__ = ABSOLUTE(.); + } >ewram + + /* .data in EWRAM. LMA in ROM so crt0 can copy it in. */ + .data ALIGN(4) : AT (__data_lma) + { + __data_start__ = ABSOLUTE(.); + *(.data*) + *(.gnu.linkonce.d*) + CONSTRUCTORS + . = ALIGN(4); + } >ewram = 0xff + + __preinit_lma = __data_lma + SIZEOF(.data); + + .preinit_array ALIGN(4) : AT (__preinit_lma) + { + __preinit_array_start = ABSOLUTE(.); + KEEP (*(.preinit_array)) + __preinit_array_end = ABSOLUTE(.); + } >ewram + + __init_lma = __preinit_lma + SIZEOF(.preinit_array); + + .init_array ALIGN(4) : AT (__init_lma) + { + __init_array_start = ABSOLUTE(.); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array)) + __init_array_end = ABSOLUTE(.); + } >ewram + + __fini_lma = __init_lma + SIZEOF(.init_array); + + .fini_array ALIGN(4) : AT (__fini_lma) + { + __fini_array_start = ABSOLUTE(.); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array)) + __fini_array_end = ABSOLUTE(.); + } >ewram + + __jcr_lma = __fini_lma + SIZEOF(.fini_array); + .jcr ALIGN(4) : AT (__jcr_lma) { KEEP (*(.jcr)) } >ewram + + __data_end__ = ABSOLUTE(.); + + /* The 'ewram' section the upstream crt0 expects. Place after .data. + __ewram_start must be defined here, NOT at ORIGIN(ewram): crt0 uses + __ewram_end - __ewram_start as the byte count for an LMA->VMA copy, + so starting it at 0x02000000 would memcpy ~96 KB of garbage from + past the end of the ROM over .bss / .data and prevent boot. */ + . = ALIGN(4); + __ewram_lma = __jcr_lma + SIZEOF(.jcr); + __ewram_start = .; + .ewram : AT (__ewram_lma) + { + *(.ewram*) + . = ALIGN(4); + __ewram_end = ABSOLUTE(.); + } >ewram = 0xff + + __pad_lma = __ewram_lma + SIZEOF(.ewram); + + .sbss ALIGN(4) (NOLOAD) : + { + __sbss_start__ = ABSOLUTE(.); + *(.sbss*) + . = ALIGN(4); + __sbss_end__ = ABSOLUTE(.); + __end__ = ABSOLUTE(.); + __eheap_start = ABSOLUTE(.); + __iheap_start = ABSOLUTE(__iwram_end__); + } >ewram + + .pad ALIGN(4) : AT (__pad_lma) + { + LONG(0x52416b64) + LONG(0x4d) + . = ALIGN(4); + } = 0xff + __rom_end__ = __pad_lma + SIZEOF(.pad); + + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + .debug_info 0 : { *(.debug_info) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + .stack 0x80000 : { _stack = .; *(.stack) } + .note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) } + .ARM.attributes 0 : { KEEP (*(.ARM.attributes)) } + /DISCARD/ : { *(.note.GNU-stack) } +} diff --git a/gba/gba_ewram.specs b/gba/gba_ewram.specs new file mode 100644 index 00000000..4f66602a --- /dev/null +++ b/gba/gba_ewram.specs @@ -0,0 +1,7 @@ +%include + +*link: ++ -T gba_ewram.ld%s --gc-sections --no-warn-rwx-segments + +*startfile: +gba_crt0%O%s crti%O%s crtbegin%O%s diff --git a/gba/include/.DS_Store b/gba/include/.DS_Store new file mode 100644 index 00000000..5939e392 Binary files /dev/null and b/gba/include/.DS_Store differ diff --git a/gba/include/SDL2/SDL.h b/gba/include/SDL2/SDL.h new file mode 100644 index 00000000..db6a8e03 --- /dev/null +++ b/gba/include/SDL2/SDL.h @@ -0,0 +1,514 @@ +/* Minimal SDL2 surface/event/audio/timer shim for GBA (devkitARM). + Only the subset actually exercised by SDLPoP is implemented. + This header is included instead of the real SDL2/SDL.h when building for GBA. */ +#ifndef SDL_GBA_SHIM_H +#define SDL_GBA_SHIM_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int8_t Sint8; +typedef uint8_t Uint8; +typedef int16_t Sint16; +typedef uint16_t Uint16; +typedef int32_t Sint32; +typedef uint32_t Uint32; +typedef int64_t Sint64; +typedef uint64_t Uint64; +typedef Uint8 SDL_bool; + +#define SDL_TRUE 1 +#define SDL_FALSE 0 + +#define SDL_LIL_ENDIAN 1234 +#define SDL_BIG_ENDIAN 4321 +#define SDL_BYTEORDER SDL_LIL_ENDIAN + +#define SDL_INIT_VIDEO 0x00000020u +#define SDL_INIT_AUDIO 0x00000010u +#define SDL_INIT_JOYSTICK 0x00000200u +#define SDL_INIT_GAMECONTROLLER 0x00002000u +#define SDL_INIT_TIMER 0x00000001u +#define SDL_INIT_HAPTIC 0x00001000u +#define SDL_INIT_EVERYTHING 0x0000FFFFu +#define SDL_INIT_NOPARACHUTE 0x00100000u + +#define SDL_HINT_RENDER_VSYNC "SDL_RENDER_VSYNC" +#define SDL_HINT_RENDER_SCALE_QUALITY "SDL_RENDER_SCALE_QUALITY" + +#define SDL_ALPHA_OPAQUE 255 +#define SDL_ALPHA_TRANSPARENT 0 +#define SDL_SRCCOLORKEY 0x00001000 + +typedef struct SDL_Color { + Uint8 r, g, b, a; +} SDL_Color; + +/* PoP sprites are 4bpp (16 colours/sprite). The screen surface is 8bpp but its + palette is never populated (we use the global PoP palette[] for hardware + BG_PAL). Cutting from 256 colours inline to 16 saves ~960 bytes per surface + instance — multiplied by ~100+ surfaces (font + sprites), this is the + difference between fitting on the GBA's 256 KB EWRAM or not. */ +#define SDL_PALETTE_SIZE 16 +typedef struct SDL_Palette { + int ncolors; + SDL_Color colors[SDL_PALETTE_SIZE]; +} SDL_Palette; + +typedef struct SDL_PixelFormat { + Uint32 format; + SDL_Palette* palette; + Uint8 BitsPerPixel; + Uint8 BytesPerPixel; + Uint32 Rmask, Gmask, Bmask, Amask; +} SDL_PixelFormat; + +typedef struct SDL_Rect { + int x, y, w, h; +} SDL_Rect; + +typedef enum { + SDL_BLENDMODE_NONE = 0, + SDL_BLENDMODE_BLEND = 1, + SDL_BLENDMODE_ADD = 2, + SDL_BLENDMODE_MOD = 3 +} SDL_BlendMode; + +typedef struct SDL_Surface { + Uint32 flags; + SDL_PixelFormat* format; + int w, h; + int pitch; + void* pixels; + SDL_Rect clip_rect; + int refcount; + + /* Internal: palette/format storage attached to the surface. */ + SDL_PixelFormat _format; + SDL_Palette _palette; + int has_colorkey; + Uint32 colorkey; + Uint8 alpha_mod; + SDL_BlendMode blend_mode; + + /* GBA: sprite stored as pointer-to-ROM image_data_type instead of a + decoded 8bpp pixel buffer. Set by decode_image; consumed by + SDL_BlitSurface, which decompresses on the fly into a scratch. */ + Uint8 gba_rom_compressed; /* 1 if pixels points to ROM */ + Uint8 gba_palette_offset; /* row*16 baked at blit time */ + Uint8 gba_depth; /* bpp from DAT header */ + Uint8 gba_cmeth; /* compression method 0..4 */ +} SDL_Surface; + +typedef struct SDL_Window { int _; } SDL_Window; +typedef struct SDL_Renderer { int _; } SDL_Renderer; +typedef struct SDL_Texture { int _; } SDL_Texture; +typedef struct SDL_RWops SDL_RWops; +typedef struct SDL_GameController { int _; } SDL_GameController; +typedef struct SDL_Joystick { int _; } SDL_Joystick; +typedef struct SDL_Haptic { int _; } SDL_Haptic; +typedef int SDL_TimerID; + +/* Pixel formats we report */ +#define SDL_PIXELFORMAT_INDEX8 0x13000801u +#define SDL_PIXELFORMAT_RGB24 0x17101803u +#define SDL_PIXELFORMAT_ARGB8888 0x16362004u + +#define SDL_ISPIXELFORMAT_INDEXED(f) ((f) == SDL_PIXELFORMAT_INDEX8) + +/* Endian swap (we're always little endian on GBA/ARM7). */ +static inline Uint16 SDL_Swap16(Uint16 x) { return (Uint16)((x<<8)|(x>>8)); } +static inline Uint32 SDL_Swap32(Uint32 x) { + return ((x<<24)&0xff000000u)|((x<<8)&0x00ff0000u)| + ((x>>8)&0x0000ff00u)|((x>>24)&0x000000ffu); +} +#define SDL_SwapLE16(x) ((Uint16)(x)) +#define SDL_SwapLE32(x) ((Uint32)(x)) +#define SDL_SwapBE16(x) SDL_Swap16(x) +#define SDL_SwapBE32(x) SDL_Swap32(x) + +/* Lifecycle */ +int SDL_Init(Uint32 flags); +int SDL_InitSubSystem(Uint32 flags); +void SDL_Quit(void); +void SDL_SetHint(const char* name, const char* value); +const char* SDL_GetError(void); +void SDL_SetError(const char* fmt, ...); + +/* Surfaces */ +SDL_Surface* SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, + Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask); +void SDL_FreeSurface(SDL_Surface* s); +int SDL_LockSurface(SDL_Surface* s); +void SDL_UnlockSurface(SDL_Surface* s); +int SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color); +int SDL_BlitSurface(SDL_Surface* src, const SDL_Rect* srcrect, + SDL_Surface* dst, SDL_Rect* dstrect); +#define SDL_UpperBlit SDL_BlitSurface +int SDL_BlitScaled(SDL_Surface* src, const SDL_Rect* srcrect, + SDL_Surface* dst, SDL_Rect* dstrect); +int SDL_SetColorKey(SDL_Surface* s, int flag, Uint32 key); +int SDL_SetSurfaceAlphaMod(SDL_Surface* s, Uint8 alpha); +int SDL_SetSurfaceBlendMode(SDL_Surface* s, SDL_BlendMode mode); +int SDL_SetClipRect(SDL_Surface* s, const SDL_Rect* rect); +int SDL_SetPaletteColors(SDL_Palette* palette, const SDL_Color* colors, + int firstcolor, int ncolors); +SDL_Surface* SDL_ConvertSurfaceFormat(SDL_Surface* src, Uint32 fmt, Uint32 flags); +SDL_Surface* SDL_ConvertSurface(SDL_Surface* src, const SDL_PixelFormat* fmt, Uint32 flags); +int SDL_SetSurfacePalette(SDL_Surface* s, SDL_Palette* palette); +Uint32 SDL_MapRGB(const SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b); +Uint32 SDL_MapRGBA(const SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b, Uint8 a); +const char* SDL_GetPixelFormatName(Uint32 fmt); + +/* RWops (only memory-backed flavor is implemented) */ +struct SDL_RWops { + Sint64 (*size)(SDL_RWops*); + Sint64 (*seek)(SDL_RWops*, Sint64, int); + size_t (*read)(SDL_RWops*, void*, size_t, size_t); + size_t (*write)(SDL_RWops*, const void*, size_t, size_t); + int (*close)(SDL_RWops*); + Uint32 type; + union { + struct { const Uint8* base; const Uint8* here; const Uint8* stop; } mem; + } hidden; +}; +#define RW_SEEK_SET 0 +#define RW_SEEK_CUR 1 +#define RW_SEEK_END 2 +SDL_RWops* SDL_RWFromConstMem(const void* mem, int size); +SDL_RWops* SDL_RWFromMem(void* mem, int size); +SDL_RWops* SDL_RWFromFile(const char* file, const char* mode); +int SDL_RWclose(SDL_RWops* rw); +size_t SDL_RWread(SDL_RWops* rw, void* ptr, size_t size, size_t maxnum); +size_t SDL_RWwrite(SDL_RWops* rw, const void* ptr, size_t size, size_t num); +Sint64 SDL_RWseek(SDL_RWops* rw, Sint64 ofs, int whence); +Sint64 SDL_RWtell(SDL_RWops* rw); +Sint64 SDL_RWsize(SDL_RWops* rw); + +/* Timing */ +Uint32 SDL_GetTicks(void); +Uint64 SDL_GetPerformanceCounter(void); +Uint64 SDL_GetPerformanceFrequency(void); +void SDL_Delay(Uint32 ms); + +/* Events / input */ +#define SDL_NUM_SCANCODES 256 +typedef int SDL_Scancode; +enum { + SDL_SCANCODE_UNKNOWN = 0, + SDL_SCANCODE_A = 4, SDL_SCANCODE_B, SDL_SCANCODE_C, SDL_SCANCODE_D, + SDL_SCANCODE_E, SDL_SCANCODE_F, SDL_SCANCODE_G, SDL_SCANCODE_H, + SDL_SCANCODE_I, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, + SDL_SCANCODE_M, SDL_SCANCODE_N, SDL_SCANCODE_O, SDL_SCANCODE_P, + SDL_SCANCODE_Q, SDL_SCANCODE_R, SDL_SCANCODE_S, SDL_SCANCODE_T, + SDL_SCANCODE_U, SDL_SCANCODE_V, SDL_SCANCODE_W, SDL_SCANCODE_X, + SDL_SCANCODE_Y, SDL_SCANCODE_Z, + SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, + SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8, + SDL_SCANCODE_9, SDL_SCANCODE_0, + SDL_SCANCODE_RETURN, SDL_SCANCODE_ESCAPE, SDL_SCANCODE_BACKSPACE, + SDL_SCANCODE_TAB, SDL_SCANCODE_SPACE, + SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS, + SDL_SCANCODE_LEFTBRACKET, SDL_SCANCODE_RIGHTBRACKET, SDL_SCANCODE_BACKSLASH, + SDL_SCANCODE_NONUSHASH, SDL_SCANCODE_SEMICOLON, SDL_SCANCODE_APOSTROPHE, + SDL_SCANCODE_GRAVE, SDL_SCANCODE_COMMA, SDL_SCANCODE_PERIOD, SDL_SCANCODE_SLASH, + SDL_SCANCODE_CAPSLOCK, + SDL_SCANCODE_F1, SDL_SCANCODE_F2, SDL_SCANCODE_F3, SDL_SCANCODE_F4, + SDL_SCANCODE_F5, SDL_SCANCODE_F6, SDL_SCANCODE_F7, SDL_SCANCODE_F8, + SDL_SCANCODE_F9, SDL_SCANCODE_F10, SDL_SCANCODE_F11, SDL_SCANCODE_F12, + SDL_SCANCODE_PRINTSCREEN, SDL_SCANCODE_SCROLLLOCK, SDL_SCANCODE_PAUSE, + SDL_SCANCODE_INSERT, SDL_SCANCODE_HOME, SDL_SCANCODE_PAGEUP, + SDL_SCANCODE_DELETE, SDL_SCANCODE_END, SDL_SCANCODE_PAGEDOWN, + SDL_SCANCODE_RIGHT, SDL_SCANCODE_LEFT, SDL_SCANCODE_DOWN, SDL_SCANCODE_UP, + SDL_SCANCODE_NUMLOCKCLEAR, + SDL_SCANCODE_KP_DIVIDE, SDL_SCANCODE_KP_MULTIPLY, SDL_SCANCODE_KP_MINUS, + SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_KP_ENTER, + SDL_SCANCODE_KP_1, SDL_SCANCODE_KP_2, SDL_SCANCODE_KP_3, SDL_SCANCODE_KP_4, + SDL_SCANCODE_KP_5, SDL_SCANCODE_KP_6, SDL_SCANCODE_KP_7, SDL_SCANCODE_KP_8, + SDL_SCANCODE_KP_9, SDL_SCANCODE_KP_0, + SDL_SCANCODE_KP_PERIOD, SDL_SCANCODE_NONUSBACKSLASH, SDL_SCANCODE_APPLICATION, + SDL_SCANCODE_POWER, SDL_SCANCODE_KP_EQUALS, + SDL_SCANCODE_F13, SDL_SCANCODE_F14, SDL_SCANCODE_F15, SDL_SCANCODE_F16, + SDL_SCANCODE_F17, SDL_SCANCODE_F18, SDL_SCANCODE_F19, SDL_SCANCODE_F20, + SDL_SCANCODE_F21, SDL_SCANCODE_F22, SDL_SCANCODE_F23, SDL_SCANCODE_F24, + SDL_SCANCODE_EXECUTE, SDL_SCANCODE_HELP, SDL_SCANCODE_MENU, SDL_SCANCODE_SELECT, + SDL_SCANCODE_STOP, SDL_SCANCODE_AGAIN, SDL_SCANCODE_UNDO, SDL_SCANCODE_CUT, + SDL_SCANCODE_COPY, SDL_SCANCODE_PASTE, SDL_SCANCODE_FIND, SDL_SCANCODE_MUTE, + SDL_SCANCODE_VOLUMEUP, SDL_SCANCODE_VOLUMEDOWN, + SDL_SCANCODE_LCTRL = 224, SDL_SCANCODE_LSHIFT, SDL_SCANCODE_LALT, SDL_SCANCODE_LGUI, + SDL_SCANCODE_RCTRL, SDL_SCANCODE_RSHIFT, SDL_SCANCODE_RALT, SDL_SCANCODE_RGUI, + SDL_SCANCODE_CLEAR = 156, + SDL_SCANCODE_AUDIOMUTE = 240, +}; +typedef int SDL_Keycode; + +typedef enum { + SDL_FIRSTEVENT = 0, + SDL_QUIT = 0x100, + SDL_KEYDOWN = 0x300, + SDL_KEYUP = 0x301, + SDL_TEXTEDITING = 0x302, + SDL_TEXTINPUT = 0x303, + SDL_MOUSEMOTION = 0x400, + SDL_MOUSEBUTTONDOWN= 0x401, + SDL_MOUSEBUTTONUP = 0x402, + SDL_MOUSEWHEEL = 0x403, + SDL_JOYAXISMOTION = 0x600, + SDL_JOYBUTTONDOWN = 0x603, + SDL_JOYBUTTONUP = 0x604, + SDL_CONTROLLERAXISMOTION = 0x650, + SDL_CONTROLLERBUTTONDOWN = 0x651, + SDL_CONTROLLERBUTTONUP = 0x652, + SDL_CONTROLLERDEVICEADDED= 0x653, + SDL_CONTROLLERDEVICEREMOVED= 0x654, + SDL_WINDOWEVENT = 0x200, + SDL_USEREVENT = 0x8000, +} SDL_EventType; + +typedef struct { Uint32 type; Uint32 timestamp; } SDL_CommonEvent; +typedef struct { + Uint32 type; Uint32 timestamp; Uint32 windowID; + Uint8 state; Uint8 repeat; Uint8 padding2, padding3; + struct { SDL_Scancode scancode; SDL_Keycode sym; Uint16 mod; Uint32 unused; } keysym; +} SDL_KeyboardEvent; +typedef struct { + Uint32 type; Uint32 timestamp; Uint32 windowID; char text[32]; +} SDL_TextInputEvent; +typedef struct { + Uint32 type; Uint32 timestamp; Uint32 windowID; + Uint8 event; Uint8 padding1, padding2, padding3; + Sint32 data1, data2; +} SDL_WindowEvent; +typedef struct { + Uint32 type; Uint32 timestamp; int which; + Uint8 button; Uint8 state; Uint8 padding1, padding2; + Sint32 x, y; +} SDL_MouseButtonEvent; +typedef struct { + Uint32 type; Uint32 timestamp; int which; Sint32 x, y; + Uint32 direction; +} SDL_MouseWheelEvent; +typedef struct { + Uint32 type; Uint32 timestamp; int which; + Uint8 axis; Uint8 padding1, padding2, padding3; + Sint16 value; Uint16 padding4; +} SDL_ControllerAxisEvent; +typedef struct { + Uint32 type; Uint32 timestamp; int which; + Uint8 button; Uint8 state; Uint8 padding1, padding2; +} SDL_ControllerButtonEvent; +typedef struct { + Uint32 type; Uint32 timestamp; int which; +} SDL_ControllerDeviceEvent; +typedef struct { + Uint32 type; Uint32 timestamp; Uint32 windowID; + Sint32 code; void* data1; void* data2; +} SDL_UserEvent; +enum { + KMOD_NONE = 0, + KMOD_LSHIFT = 0x0001, KMOD_RSHIFT = 0x0002, + KMOD_LCTRL = 0x0040, KMOD_RCTRL = 0x0080, + KMOD_LALT = 0x0100, KMOD_RALT = 0x0200, + KMOD_LGUI = 0x0400, KMOD_RGUI = 0x0800, + KMOD_NUM = 0x1000, KMOD_CAPS = 0x2000, + KMOD_SHIFT = KMOD_LSHIFT | KMOD_RSHIFT, + KMOD_CTRL = KMOD_LCTRL | KMOD_RCTRL, + KMOD_ALT = KMOD_LALT | KMOD_RALT, + KMOD_GUI = KMOD_LGUI | KMOD_RGUI, +}; +typedef SDL_ControllerAxisEvent SDL_JoyAxisEvent; +typedef SDL_ControllerButtonEvent SDL_JoyButtonEvent; + +typedef union SDL_Event { + Uint32 type; + SDL_CommonEvent common; + SDL_KeyboardEvent key; + SDL_TextInputEvent text; + SDL_WindowEvent window; + SDL_MouseButtonEvent button; + SDL_MouseWheelEvent wheel; + SDL_ControllerAxisEvent caxis; + SDL_ControllerButtonEvent cbutton; + SDL_ControllerDeviceEvent cdevice; + SDL_JoyAxisEvent jaxis; + SDL_JoyButtonEvent jbutton; + SDL_UserEvent user; + Uint8 padding[56]; +} SDL_Event; + +int SDL_PollEvent(SDL_Event* event); +int SDL_PushEvent(SDL_Event* event); +const Uint8* SDL_GetKeyboardState(int* numkeys); +Uint32 SDL_GetMouseState(int* x, int* y); +const char* SDL_GetScancodeName(SDL_Scancode scancode); +int SDL_ShowCursor(int toggle); +void SDL_StartTextInput(void); +void SDL_StopTextInput(void); +int SDL_SetTextInputRect(const SDL_Rect* rect); +#define SDL_DISABLE 0 +#define SDL_ENABLE 1 + +/* Window/renderer/texture: kept as opaque stubs (we render directly to GBA VRAM). */ +#define SDL_WINDOWPOS_UNDEFINED 0x1FFF0000u +#define SDL_WINDOW_RESIZABLE 0x00000020u +#define SDL_WINDOW_ALLOW_HIGHDPI 0x00002000u +#define SDL_WINDOW_FULLSCREEN_DESKTOP 0x00001001u +#define SDL_RENDERER_SOFTWARE 0x00000001u +#define SDL_RENDERER_ACCELERATED 0x00000002u +#define SDL_RENDERER_TARGETTEXTURE 0x00000008u +#define SDL_TEXTUREACCESS_TARGET 2 +#define SDL_TEXTUREACCESS_STREAMING 1 + +SDL_Window* SDL_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags); +void SDL_DestroyWindow(SDL_Window* w); +SDL_Renderer* SDL_CreateRenderer(SDL_Window* w, int index, Uint32 flags); +void SDL_DestroyRenderer(SDL_Renderer* r); +SDL_Texture* SDL_CreateTexture(SDL_Renderer* r, Uint32 fmt, int access, int w, int h); +void SDL_DestroyTexture(SDL_Texture* t); +int SDL_RenderClear(SDL_Renderer* r); +int SDL_RenderCopy(SDL_Renderer* r, SDL_Texture* t, const SDL_Rect* src, const SDL_Rect* dst); +void SDL_RenderPresent(SDL_Renderer* r); +int SDL_SetRenderTarget(SDL_Renderer* r, SDL_Texture* t); +int SDL_UpdateTexture(SDL_Texture* t, const SDL_Rect* rect, const void* px, int pitch); +int SDL_RenderSetLogicalSize(SDL_Renderer* r, int w, int h); +int SDL_RenderSetIntegerScale(SDL_Renderer* r, SDL_bool enable); +int SDL_RenderGetLogicalSize(SDL_Renderer* r, int* w, int* h); +void SDL_RenderGetScale(SDL_Renderer* r, float* scaleX, float* scaleY); +void SDL_RenderGetViewport(SDL_Renderer* r, SDL_Rect* rect); +int SDL_GetRendererOutputSize(SDL_Renderer* r, int* w, int* h); +int SDL_GetWindowFlags(SDL_Window* w); +void SDL_SetWindowFullscreen(SDL_Window* w, Uint32 flags); +void SDL_SetWindowIcon(SDL_Window* w, SDL_Surface* icon); +typedef struct { const char* name; Uint32 flags; Uint32 num_texture_formats; Uint32 texture_formats[16]; int max_texture_width; int max_texture_height; } SDL_RendererInfo; +int SDL_GetRendererInfo(SDL_Renderer* r, SDL_RendererInfo* info); +void SDL_GL_GetDrawableSize(SDL_Window* w, int* a, int* b); +void SDL_GetWindowSize(SDL_Window* w, int* a, int* b); +int SDL_UpdateRect(SDL_Surface* s, int x, int y, int w, int h); +int SDL_Flip(SDL_Surface* s); + +/* Audio (PCM via DMA1; AudioCVT is no-op as we feed 8-bit signed PCM already) */ +typedef struct { + int freq; Uint16 format; Uint8 channels; Uint8 silence; + Uint16 samples; Uint16 padding; Uint32 size; + void (*callback)(void* userdata, Uint8* stream, int len); + void* userdata; +} SDL_AudioSpec; +#define AUDIO_S8 0x8008 +#define AUDIO_U8 0x0008 +#define AUDIO_S16SYS 0x8010 +#define AUDIO_S16LSB 0x8010 +typedef Uint16 SDL_AudioFormat; +typedef struct { + int needed; + SDL_AudioFormat src_format, dst_format; + double rate_incr; + Uint8* buf; int len, len_cvt, len_mult; double len_ratio; + void (*filters[10])(void); + int filter_index; +} SDL_AudioCVT; +int SDL_OpenAudio(SDL_AudioSpec* desired, SDL_AudioSpec* obtained); +void SDL_CloseAudio(void); +void SDL_PauseAudio(int on); +void SDL_LockAudio(void); +void SDL_UnlockAudio(void); +int SDL_GetAudioStatus(void); +int SDL_BuildAudioCVT(SDL_AudioCVT* cvt, SDL_AudioFormat src_fmt, Uint8 src_ch, int src_rate, + SDL_AudioFormat dst_fmt, Uint8 dst_ch, int dst_rate); +int SDL_ConvertAudio(SDL_AudioCVT* cvt); + +/* Joystick / haptic — minimal stubs */ +int SDL_NumJoysticks(void); +SDL_GameController* SDL_GameControllerOpen(int index); +void SDL_GameControllerClose(SDL_GameController*); +SDL_bool SDL_IsGameController(int index); +SDL_GameController* SDL_GameControllerFromInstanceID(int); +SDL_Joystick* SDL_JoystickOpen(int); +int SDL_GameControllerAddMappingsFromFile(const char* file); +SDL_Haptic* SDL_HapticOpen(int); +int SDL_HapticRumbleInit(SDL_Haptic*); +int SDL_HapticRumblePlay(SDL_Haptic*, float, Uint32); + +enum { + SDL_CONTROLLER_AXIS_LEFTX = 0, + SDL_CONTROLLER_AXIS_LEFTY, + SDL_CONTROLLER_AXIS_RIGHTX, + SDL_CONTROLLER_AXIS_RIGHTY, + SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SDL_CONTROLLER_AXIS_TRIGGERRIGHT, + SDL_CONTROLLER_AXIS_MAX +}; +enum { + SDL_CONTROLLER_BUTTON_A=0, SDL_CONTROLLER_BUTTON_B, SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_Y, SDL_CONTROLLER_BUTTON_BACK, SDL_CONTROLLER_BUTTON_GUIDE, + SDL_CONTROLLER_BUTTON_START, SDL_CONTROLLER_BUTTON_LEFTSTICK, + SDL_CONTROLLER_BUTTON_RIGHTSTICK, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, SDL_CONTROLLER_BUTTON_MAX +}; + +#define SDL_BUTTON_LEFT 1 +#define SDL_BUTTON_RIGHT 3 +#define SDL_BUTTON_X1 4 + +/* Window events */ +enum { + SDL_WINDOWEVENT_NONE = 0, + SDL_WINDOWEVENT_SHOWN, + SDL_WINDOWEVENT_HIDDEN, + SDL_WINDOWEVENT_EXPOSED, + SDL_WINDOWEVENT_MOVED, + SDL_WINDOWEVENT_RESIZED, + SDL_WINDOWEVENT_SIZE_CHANGED, + SDL_WINDOWEVENT_MINIMIZED, + SDL_WINDOWEVENT_MAXIMIZED, + SDL_WINDOWEVENT_RESTORED, + SDL_WINDOWEVENT_ENTER, + SDL_WINDOWEVENT_LEAVE, + SDL_WINDOWEVENT_FOCUS_GAINED, + SDL_WINDOWEVENT_FOCUS_LOST, + SDL_WINDOWEVENT_CLOSE +}; +#define SDL_APPACTIVE 0 +#define SDL_APPINPUTFOCUS 0 + +/* Timers (1 shot via tag — SDLPoP only uses one global timer) */ +typedef Uint32 (*SDL_TimerCallback)(Uint32 interval, void* param); +SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback cb, void* param); +int SDL_RemoveTimer(SDL_TimerID id); +void SDL_PumpEvents(void); + +/* iconv / strings */ +char* SDL_iconv_string(const char* tocode, const char* fromcode, const char* inbuf, size_t inbytesleft); +void SDL_free(void* mem); +size_t SDL_wcslen(const Uint16* w); +size_t SDL_strlen(const char* s); + +/* Compile-time assertion. */ +#define SDL_COMPILE_TIME_ASSERT(name, x) typedef int SDL_dummy_##name[(x) * 2 - 1] + +/* Version helper */ +typedef struct { Uint8 major, minor, patch; } SDL_version; +#define SDL_VERSION(v) do { (v)->major=2; (v)->minor=0; (v)->patch=5; } while(0) +#define SDL_GetVersion SDL_VERSION +#define SDL_VERSION_ATLEAST(a,b,c) ((2*10000+0*100+5) >= ((a)*10000+(b)*100+(c))) + +#define SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING "SDL_WINDOWS_DISABLE_THREAD_NAMING" + +/* Removed in SDL2 but referenced by SDLPoP under #ifdef paths we never enable. */ +#define SDL_EnableKeyRepeat(a,b) 0 +#define SDL_EnableUNICODE(x) 0 +#define SDL_WM_SetCaption(a,b) ((void)0) + +#ifdef __cplusplus +} +#endif + +#endif /* SDL_GBA_SHIM_H */ diff --git a/gba/include/SDL2/SDL_image.h b/gba/include/SDL2/SDL_image.h new file mode 100644 index 00000000..e68fdb31 --- /dev/null +++ b/gba/include/SDL2/SDL_image.h @@ -0,0 +1,16 @@ +/* SDL_image stub: SDLPoP only uses IMG_Load for icon.png — we drop that. */ +#ifndef SDL_IMAGE_GBA_SHIM_H +#define SDL_IMAGE_GBA_SHIM_H +#include "SDL.h" +#ifdef __cplusplus +extern "C" { +#endif +static inline SDL_Surface* IMG_Load(const char* file) { (void)file; return NULL; } +static inline SDL_Surface* IMG_Load_RW(SDL_RWops* src, int freesrc) { + (void)src; (void)freesrc; return NULL; +} +static inline const char* IMG_GetError(void) { return ""; } +#ifdef __cplusplus +} +#endif +#endif diff --git a/gba/include/gba_port.h b/gba/include/gba_port.h new file mode 100644 index 00000000..2ee1816d --- /dev/null +++ b/gba/include/gba_port.h @@ -0,0 +1,112 @@ +#ifndef GBA_PORT_H +#define GBA_PORT_H +/* Cross-module bindings between SDLPoP, the SDL shim, and hardware support + code. None of this is exposed to the upstream code — it sees pure SDL. */ +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Tick / frame timer -------------------------------------------------- */ +uint32_t gba_get_ticks_ms(void); +void gba_delay_ms(uint32_t ms); +void gba_timer_install(uint32_t interval_ms); +void gba_timer_uninstall(void); +void gba_timer_fire_cb(void); /* called by VBlank handler */ +void gba_vblank_isr(void); + +/* --- Input --------------------------------------------------------------- */ +void gba_input_init(void); +void gba_input_poll(void); +void gba_set_key(int scancode, int down); + +/* --- Display: present 240x160 8bpp surface as Mode 4 framebuffer --------- */ +void gba_video_init(void); +int gba_present_surface(SDL_Surface* s); +void gba_apply_palette(const SDL_Color* colors, int first, int n); +void gba_set_viewport(int x, int y); /* offset in pixels into source surface */ +void gba_get_viewport(int* x, int* y); +/* When non-zero, gba_camera_update() leaves the viewport untouched so the + pause/settings menu can pin it (the menu is laid out for the full 320px + world, wider than the 240px screen; draw_menu() pans the viewport itself). */ +extern int gba_menu_active; + +/* --- Audio: 8-bit PCM mixer over DirectSound A (DMA1) --------------------- */ +void gba_audio_init(const SDL_AudioSpec* desired); +void gba_audio_shutdown(void); +void gba_audio_set_paused(int on); +void gba_audio_lock(void); +void gba_audio_unlock(void); +int gba_audio_status(void); +void gba_audio_mix_into(int8_t* out, int frames); + +/* --- File I/O over linked data blobs ------------------------------------- */ +typedef struct gba_blob { + const char* name; /* canonical name e.g. "TITLE" or "data/TITLE.DAT" */ + const uint8_t* bytes; + size_t size; +} gba_blob_t; +const gba_blob_t* gba_find_blob(const char* name); +FILE* gba_fopen(const char* path, const char* mode); +int gba_access(const char* path); + +/* If `fp` is backed by a ROM blob (i.e. it was opened from a linked .bin + data file), return a direct pointer into ROM at the file's CURRENT position + plus how many bytes are still readable from that point. Returns NULL if the + handle is a writable save buffer or unknown. + The current position is what was set by the last fseek/fread; the caller + typically does fseek(offset)+fread(checksum) then asks for the pointer. */ +const uint8_t* gba_file_rom_ptr(FILE* fp, size_t* out_avail); + +/* Heap-safe free: ignores pointers in the ROM address range. SDLPoP's + load_from_opendats_alloc may now return pointers directly into ROM + (no copy); calling libc free() on them would corrupt the heap. */ +void gba_free_safe(void* p); + +/* Static-pool allocator (gba_file.c). The GBA port layer calls these directly + so its own code uses no malloc/calloc/realloc tokens; upstream src/*.c is + routed onto the same pool via the linker's --wrap. */ +void* gba_alloc(size_t n); +void* gba_calloc(size_t nmemb, size_t size); +void* gba_realloc(void* p, size_t n); +void gba_free(void* p); + +/* Allocate a struct-only SDL_Surface (no pixel buffer); caller points + ->pixels at sprite data fixed in cartridge ROM. Used by decode_image so no + throwaway per-sprite pixel buffer is allocated (avoids heap fragmentation). */ +struct SDL_Surface; +struct SDL_Surface* gba_new_rom_surface(int w, int h, int bpp); + +/* Visual panic: flood the screen with a palette colour and halt (gba_video.c). + Used by fatal paths so failures show a distinct colour instead of hanging. */ +void gba_panic(uint8_t color_idx); + +/* Decode a `gba_rom_compressed` SDL_Surface (one whose pixels field is a + ROM-resident image_data_type pointer rather than a decoded 8bpp buffer) + into a static scratch buffer, returning a pointer to the 8bpp pixels. + Layout: w*h contiguous bytes, row stride = w. Caller must NOT free. + Returns NULL if the source is too large for the scratch. + Single-threaded use: a second call invalidates the previous result. */ +uint8_t* gba_decode_compressed_surface(SDL_Surface* src); + +/* --- SRAM save ----------------------------------------------------------- */ +#define GBA_SRAM_MAGIC 0x504F5031u /* 'POP1' */ +typedef struct gba_sram_header { + uint32_t magic; + uint32_t version; + uint32_t payload_len; + uint32_t checksum; +} gba_sram_header_t; + +int gba_sram_write(const void* data, size_t len); +int gba_sram_read(void* data, size_t cap, size_t* out_len); +void gba_sram_erase(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/gba/src/gba_audio.c b/gba/src/gba_audio.c new file mode 100644 index 00000000..cea332c9 --- /dev/null +++ b/gba/src/gba_audio.c @@ -0,0 +1,170 @@ +/* PCM audio mixer for GBA. + - DirectSound A on DMA1, fed by FIFO from a 304-sample ping-pong buffer. + - Timer 0 reload chosen for ~18157 Hz playback (close to original PoP SFX). + - Up to MAX_VOICES simultaneous voices, each 8-bit signed PCM. + - One voice slot is reserved for music streaming (long pre-rendered PCM). + - The SDL shim's OpenAudio callback is honoured if SDLPoP installs one + (used by the original midi_callback path on desktop). On GBA we mostly + play sounds directly via voice_start() — see gba_sfx_play() below. */ +#include +#include +#include "gba_port.h" + +#define MIX_HZ 18157 +#define BUF_FRAMES 304 /* matches ~one VBlank at 18157 Hz (18157/60) */ +#define MAX_VOICES 6 + +typedef struct { + const int8_t* data; + uint32_t length; + uint32_t pos; + uint8_t volume; /* 0..255 */ + uint8_t loop; + uint8_t active; + uint8_t pad; +} voice_t; + +static voice_t s_voices[MAX_VOICES]; +static int8_t s_mix_a[BUF_FRAMES] __attribute__((aligned(4))); +static int8_t s_mix_b[BUF_FRAMES] __attribute__((aligned(4))); +static int s_active_buf; +static int s_paused; +static int s_locked; +static int s_initialized; + +static SDL_AudioSpec s_spec; + +static void start_dma(void) { + REG_DMA1CNT = 0; + REG_SOUNDCNT_H = SNDA_VOL_100 | SNDA_R_ENABLE | SNDA_L_ENABLE | SNDA_RESET_FIFO; + REG_SOUNDCNT_X = SNDSTAT_ENABLE; + + /* Timer 0 reload for 18157 Hz: ((16*1024*1024)/18157) = ~924. */ + REG_TM0CNT_L = 65536 - (16777216 / MIX_HZ); + REG_TM0CNT_H = TIMER_START; + + /* Kick off DMA1 with the first half. */ + REG_DMA1SAD = (u32)s_mix_a; + REG_DMA1DAD = (u32)0x040000A0; /* FIFO_A */ + REG_DMA1CNT = DMA_DST_FIXED | DMA_REPEAT | DMA_SRC_INC | DMA32 | + DMA_SPECIAL | DMA_ENABLE | 1; + s_active_buf = 0; +} + +void gba_audio_init(const SDL_AudioSpec* desired) { + if (desired) s_spec = *desired; + memset(s_voices, 0, sizeof(s_voices)); + memset(s_mix_a, 0, sizeof(s_mix_a)); + memset(s_mix_b, 0, sizeof(s_mix_b)); + s_paused = 0; + start_dma(); + s_initialized = 1; +} + +void gba_audio_shutdown(void) { + REG_DMA1CNT = 0; + REG_SOUNDCNT_X = 0; +} + +void gba_audio_set_paused(int on) { + s_paused = on; + if (on) memset(s_mix_a, 0, sizeof(s_mix_a)), memset(s_mix_b, 0, sizeof(s_mix_b)); +} +void gba_audio_lock(void) { s_locked = 1; } +void gba_audio_unlock(void) { s_locked = 0; } +int gba_audio_status(void) { return s_paused ? 0 : 1; } + +/* SFX/music API used by sound.c on the GBA target. */ +int gba_sfx_play(const int8_t* data, uint32_t length, int loop, uint8_t volume) { + if (!data || !length) return -1; + int slot = -1; + for (int i = 0; i < MAX_VOICES - 1; ++i) { + if (!s_voices[i].active) { slot = i; break; } + } + if (slot < 0) slot = 0; /* steal voice 0 */ + s_voices[slot].data = data; + s_voices[slot].length = length; + s_voices[slot].pos = 0; + s_voices[slot].volume = volume; + s_voices[slot].loop = (uint8_t)loop; + s_voices[slot].active = 1; + return slot; +} +void gba_music_play(const int8_t* data, uint32_t length, int loop) { + int slot = MAX_VOICES - 1; + s_voices[slot].data = data; + s_voices[slot].length = length; + s_voices[slot].pos = 0; + s_voices[slot].volume = 128; + s_voices[slot].loop = (uint8_t)loop; + s_voices[slot].active = 1; +} +void gba_music_stop(void) { + s_voices[MAX_VOICES - 1].active = 0; +} +void gba_sfx_stop_all(void) { + for (int i = 0; i < MAX_VOICES - 1; ++i) s_voices[i].active = 0; +} +int gba_sfx_any_playing(void) { + for (int i = 0; i < MAX_VOICES; ++i) if (s_voices[i].active) return 1; + return 0; +} + +void gba_audio_mix_into(int8_t* out, int frames) { + /* Sum into 16-bit accumulator then clip to int8. */ + int16_t acc[BUF_FRAMES]; + memset(acc, 0, sizeof(int16_t) * frames); + + for (int v = 0; v < MAX_VOICES; ++v) { + voice_t* vp = &s_voices[v]; + if (!vp->active) continue; + const int8_t* sp = vp->data; + uint32_t pos = vp->pos, len = vp->length; + int vol = vp->volume; + for (int i = 0; i < frames; ++i) { + if (pos >= len) { + if (vp->loop) { pos = 0; } + else { vp->active = 0; break; } + } + acc[i] += (int16_t)((int)sp[pos++] * vol >> 8); + } + vp->pos = pos; + } + + for (int i = 0; i < frames; ++i) { + int16_t v = acc[i]; + if (v > 127) v = 127; + if (v < -128) v = -128; + out[i] = (int8_t)v; + } +} + +/* Called from VBlank ISR — swap the DMA source buffer and refill the idle one. */ +void gba_audio_on_vblank(void) { + /* IRQs are enabled before gba_audio_init() runs (so VBlank-driven SDLPoP + timing is available immediately). Skip any DMA poking and the 608-byte + stack-allocated mix buffer until audio is actually set up. */ + if (!s_initialized) return; + if (s_paused) return; + if (s_locked) return; + + int8_t* fill_buf; + if (s_active_buf == 0) { + /* DMA is on A — refill B and switch source on next wrap */ + fill_buf = s_mix_b; + gba_audio_mix_into(fill_buf, BUF_FRAMES); + REG_DMA1CNT = 0; + REG_DMA1SAD = (u32)s_mix_b; + REG_DMA1CNT = DMA_DST_FIXED | DMA_REPEAT | DMA_SRC_INC | DMA32 | + DMA_SPECIAL | DMA_ENABLE | 1; + s_active_buf = 1; + } else { + fill_buf = s_mix_a; + gba_audio_mix_into(fill_buf, BUF_FRAMES); + REG_DMA1CNT = 0; + REG_DMA1SAD = (u32)s_mix_a; + REG_DMA1CNT = DMA_DST_FIXED | DMA_REPEAT | DMA_SRC_INC | DMA32 | + DMA_SPECIAL | DMA_ENABLE | 1; + s_active_buf = 0; + } +} diff --git a/gba/src/gba_debug.c b/gba/src/gba_debug.c new file mode 100644 index 00000000..4d65291c --- /dev/null +++ b/gba/src/gba_debug.c @@ -0,0 +1,71 @@ +/* Debug logging for GBA emulators. Emits via BOTH channels so it works on + whichever the emulator supports: + 1. mGBA / visualboyadvance-m debug register protocol: + 0x04FFF780 (u16) write 0xC0DE to enable (reads back 0x1DEA), + 0x04FFF600 (256B) string buffer, + 0x04FFF700 (u16) write 0x100|level to flush (level 0..4). + 2. AGBPrint (classic VBA): protect 0x09FE2FFE, get 0x09FE20FC, + put 0x09FE20FE, buffer 0x09FE2000 (2 chars/word), flush = write + 32-bit 0x08FD0000 to 0x09FE20F8. Enable emulator with --agb-print. + Both are harmless no-ops on real hardware / unsupported emulators. */ +#include +#include +#include +#include + +/* ---- mGBA registers ---- */ +#define MGBA_ENABLE (*(volatile unsigned short*)0x04FFF780) +#define MGBA_FLAGS (*(volatile unsigned short*)0x04FFF700) +#define MGBA_STRING ((volatile char*)0x04FFF600) +static int s_mgba = 0; +volatile int gba_trace = 0; /* TEMP: gate verbose decode tracing */ + +/* ---- AGBPrint ---- */ +#define AGB_PROTECT (*(volatile unsigned short*)0x09FE2FFE) +#define AGB_GET (*(volatile unsigned short*)0x09FE20FC) +#define AGB_PUT (*(volatile unsigned short*)0x09FE20FE) +#define AGB_BUF ((volatile unsigned short*)0x09FE2000) + +int gba_mgba_open(void) { + MGBA_ENABLE = 0xC0DE; + s_mgba = (MGBA_ENABLE == 0x1DEA) ? 1 : 0; + /* reset AGBPrint cursor */ + AGB_PROTECT = 0x20; + AGB_GET = 0; + AGB_PUT = 0; + AGB_PROTECT = 0; + return s_mgba; +} + +static void agb_putc(char c) { + AGB_PROTECT = 0x20; + unsigned short put = AGB_PUT; + volatile unsigned short* w = &AGB_BUF[put >> 1]; + unsigned short v = *w; + if (put & 1) v = (v & 0x00FF) | ((unsigned short)(unsigned char)c << 8); + else v = (v & 0xFF00) | (unsigned char)c; + *w = v; + AGB_PUT = (unsigned short)(put + 1); + AGB_PROTECT = 0; +} + +void gba_log(const char* fmt, ...) { + char buf[200]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + int n = (int)strlen(buf); + if (n > 255) n = 255; + + /* mGBA: copy + flush at level 3 (info) */ + for (int i = 0; i < n; ++i) MGBA_STRING[i] = buf[i]; + MGBA_STRING[n] = 0; + MGBA_FLAGS = 0x100 | 3; + + /* AGBPrint: stream chars + flush */ + for (int i = 0; i < n; ++i) agb_putc(buf[i]); + agb_putc('\n'); + *(volatile unsigned*)0x09FE20F8 = 0x08FD0000u; + AGB_PROTECT = 0x20; AGB_GET = 0; AGB_PUT = 0; AGB_PROTECT = 0; +} diff --git a/gba/src/gba_file.c b/gba/src/gba_file.c new file mode 100644 index 00000000..d18d5324 --- /dev/null +++ b/gba/src/gba_file.c @@ -0,0 +1,535 @@ +/* File I/O over linked data blobs on GBA. + We never open real files on the cartridge. fopen() is intercepted by the + common.h preprocessor (#define fopen gba_fopen) so unchanged upstream code + that does fopen("KID","rb") gets a memory-backed FILE*. + + Each DAT file (KID, PRINCE, LEVELS, ...) is converted by `gbafix bin2s` + (handled by the Makefile) into _start/_end/_size symbols. We list them in + the table below; the linker will resolve them from data/.bin.o. */ +#include +#include +#include +#include +#include +#include "gba_port.h" + +/* bin2s exports `_bin[]` (start) and `_bin_end[]` for each blob. + We compute the size as (end - start). */ +#define BLOB_DECL(sym) \ + extern const uint8_t sym[]; \ + extern const uint8_t sym##_end[]; +BLOB_DECL(kid_bin) +BLOB_DECL(prince_bin) +BLOB_DECL(title_bin) +BLOB_DECL(levels_bin) +BLOB_DECL(guard_bin) +BLOB_DECL(guard1_bin) +BLOB_DECL(guard2_bin) +BLOB_DECL(vpalace_bin) +BLOB_DECL(vdungeon_bin) +BLOB_DECL(vizier_bin) +BLOB_DECL(fat_bin) +BLOB_DECL(skel_bin) +BLOB_DECL(shadow_bin) +BLOB_DECL(pv_bin) +BLOB_DECL(digisnd1_bin) +BLOB_DECL(digisnd2_bin) +BLOB_DECL(digisnd3_bin) +BLOB_DECL(ibm_snd1_bin) +BLOB_DECL(ibm_snd2_bin) +BLOB_DECL(midisnd1_bin) +BLOB_DECL(midisnd2_bin) +BLOB_DECL(font_bin) +#undef BLOB_DECL + +static const struct { + const char* name; + const uint8_t* data; + const uint8_t* end; +} s_blobs[] = { +#define ENT(sym, name) { name, sym, sym##_end } + ENT(kid_bin, "KID"), ENT(kid_bin, "KID.DAT"), + ENT(prince_bin, "PRINCE"), ENT(prince_bin, "PRINCE.DAT"), + ENT(title_bin, "TITLE"), ENT(title_bin, "TITLE.DAT"), + ENT(levels_bin, "LEVELS"), ENT(levels_bin, "LEVELS.DAT"), + ENT(guard_bin, "GUARD"), ENT(guard_bin, "GUARD.DAT"), + ENT(guard1_bin, "GUARD1"), ENT(guard1_bin, "GUARD1.DAT"), + ENT(guard2_bin, "GUARD2"), ENT(guard2_bin, "GUARD2.DAT"), + ENT(vpalace_bin, "VPALACE"), ENT(vpalace_bin, "VPALACE.DAT"), + ENT(vdungeon_bin, "VDUNGEON"), ENT(vdungeon_bin, "VDUNGEON.DAT"), + ENT(vizier_bin, "VIZIER"), ENT(vizier_bin, "VIZIER.DAT"), + ENT(fat_bin, "FAT"), ENT(fat_bin, "FAT.DAT"), + ENT(skel_bin, "SKEL"), ENT(skel_bin, "SKEL.DAT"), + ENT(shadow_bin, "SHADOW"), ENT(shadow_bin, "SHADOW.DAT"), + ENT(pv_bin, "PV"), ENT(pv_bin, "PV.DAT"), + ENT(digisnd1_bin, "DIGISND1.DAT"), + ENT(digisnd2_bin, "DIGISND2.DAT"), + ENT(digisnd3_bin, "DIGISND3.DAT"), + ENT(ibm_snd1_bin, "IBM_SND1"), ENT(ibm_snd1_bin, "IBM_SND1.DAT"), + ENT(ibm_snd2_bin, "IBM_SND2"), ENT(ibm_snd2_bin, "IBM_SND2.DAT"), + ENT(midisnd1_bin, "MIDISND1"), ENT(midisnd1_bin, "MIDISND1.DAT"), + ENT(midisnd2_bin, "MIDISND2"), ENT(midisnd2_bin, "MIDISND2.DAT"), + ENT(font_bin, "font"), +#undef ENT +}; + +/* Strip any "data/" or "../" prefix and uppercase the filename so case + variations work. Returns a pointer into a static buffer. */ +static const char* canonicalize(const char* path) { + static char buf[64]; + const char* p = path; + /* trim leading directories */ + const char* slash = strrchr(p, '/'); + if (slash) p = slash + 1; + /* dot-extensions stay case-insensitive but exact match is preferred */ + size_t n = strlen(p); + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + for (size_t i = 0; i < n; ++i) { + buf[i] = (char)toupper((unsigned char)p[i]); + } + buf[n] = '\0'; + /* Special case: "font" stays lower-case in the table. */ + if (n == 4 && memcmp(buf, "FONT", 4) == 0) memcpy(buf, "font", 4); + return buf; +} + +const gba_blob_t* gba_find_blob(const char* path) { + static gba_blob_t out; + const char* key = canonicalize(path); + for (unsigned i = 0; i < sizeof(s_blobs)/sizeof(s_blobs[0]); ++i) { + if (strcmp(s_blobs[i].name, key) == 0) { + out.name = s_blobs[i].name; + out.bytes = s_blobs[i].data; + out.size = (size_t)(s_blobs[i].end - s_blobs[i].data); + return &out; + } + } + return NULL; +} + +int gba_access(const char* path) { + return gba_find_blob(path) ? 0 : -1; +} + +/* --- Memory-backed FILE* ----------------------------------------------- */ +/* We don't have a real libc FILE on devkitARM newlib that we can subclass. + Instead we maintain a parallel handle table keyed by the FILE* address. */ + +#define SAVE_FILE_NAME "PRINCE.SAV" +#define SAVE_BUF_CAP 4096 + +typedef struct { + const uint8_t* base; /* read-only blob backing, NULL for save buffer */ + uint8_t* wbuf; /* write-mode scratch (points into s_save_buf) */ + size_t wcap; + size_t size; + size_t pos; + int in_use; + int is_write; + int is_save; + FILE* public_handle; +} gba_file_t; + +#define MAX_FILES 8 + +extern int gba_sram_read(void* data, size_t cap, size_t* out_len); +extern int gba_sram_write(const void* data, size_t len); +static gba_file_t s_files[MAX_FILES]; + +/* Single static save scratch buffer. Only one PRINCE.SAV handle is ever open + at a time (open -> read/write -> close), so a single static buffer suffices + and we avoid a heap allocation. s_save_in_use guards against the (never + reached in practice) case of two concurrent save handles. */ +static uint8_t s_save_buf[SAVE_BUF_CAP] __attribute__((aligned(4))); +static int s_save_in_use; + +/* Per-slot dummy byte: gives each FILE* a unique, stable address without + allocating. find_handle() keys on this pointer. */ +static uint8_t s_handle_tokens[MAX_FILES]; + +static gba_file_t* find_handle(FILE* fp) { + for (int i = 0; i < MAX_FILES; ++i) + if (s_files[i].in_use && s_files[i].public_handle == fp) return &s_files[i]; + return NULL; +} + +static int strcasecmp_local(const char* a, const char* b) { + while (*a && *b) { + int ca = tolower((unsigned char)*a++); + int cb = tolower((unsigned char)*b++); + if (ca != cb) return ca - cb; + } + return tolower((unsigned char)*a) - tolower((unsigned char)*b); +} + +static int path_is_save(const char* p) { + const char* base = strrchr(p, '/'); + base = base ? base + 1 : p; + return strcasecmp_local(base, SAVE_FILE_NAME) == 0; +} + +static gba_file_t* alloc_slot(void) { + for (int i = 0; i < MAX_FILES; ++i) { + if (!s_files[i].in_use) { + memset(&s_files[i], 0, sizeof(s_files[i])); + s_files[i].in_use = 1; + /* Unique, stable, allocation-free handle: the slot's token byte. */ + s_files[i].public_handle = (FILE*)&s_handle_tokens[i]; + return &s_files[i]; + } + } + return NULL; +} + +FILE* gba_fopen(const char* path, const char* mode) { + int writing = (mode && (strchr(mode, 'w') || strchr(mode, 'a'))); + int saving = path_is_save(path); + + if (saving) { + if (s_save_in_use) return NULL; /* only one save handle at a time */ + gba_file_t* h = alloc_slot(); + if (!h) return NULL; + h->is_save = 1; + h->wcap = SAVE_BUF_CAP; + h->wbuf = s_save_buf; + s_save_in_use = 1; + memset(s_save_buf, 0, SAVE_BUF_CAP); + if (writing) { + h->is_write = 1; + h->size = 0; + } else { + size_t got = 0; + if (gba_sram_read(h->wbuf, h->wcap, &got) == 0) { + h->size = got; + } else { + /* no save present */ + s_save_in_use = 0; + h->in_use = 0; + return NULL; + } + } + return h->public_handle; + } + + if (writing) return NULL; /* no general writable filesystem on GBA */ + + const gba_blob_t* blob = gba_find_blob(path); + if (!blob) return NULL; + gba_file_t* h = alloc_slot(); + if (!h) return NULL; + h->base = blob->bytes; + h->size = blob->size; + return h->public_handle; +} + +int gba_fclose(FILE* fp) { + gba_file_t* h = find_handle(fp); + if (!h) return EOF; + if (h->is_save && h->is_write) { + gba_sram_write(h->wbuf, h->size); + } + if (h->is_save) s_save_in_use = 0; + memset(h, 0, sizeof(*h)); + return 0; +} + +size_t gba_fread(void* buf, size_t sz, size_t n, FILE* fp) { + gba_file_t* h = find_handle(fp); + if (!h) return 0; + size_t want = sz * n; + size_t have = (h->size > h->pos) ? h->size - h->pos : 0; + if (want > have) want = have; + const uint8_t* src = h->is_save ? h->wbuf : h->base; + memcpy(buf, src + h->pos, want); + h->pos += want; + return sz ? (want / sz) : 0; +} + +size_t gba_fwrite(const void* buf, size_t sz, size_t n, FILE* fp) { + gba_file_t* h = find_handle(fp); + if (!h || !h->is_save || !h->is_write) return 0; + size_t want = sz * n; + if (h->pos + want > h->wcap) want = h->wcap - h->pos; + memcpy(h->wbuf + h->pos, buf, want); + h->pos += want; + if (h->pos > h->size) h->size = h->pos; + return sz ? (want / sz) : 0; +} + +int gba_fseek(FILE* fp, long ofs, int whence) { + gba_file_t* h = find_handle(fp); + if (!h) return -1; + long target; + if (whence == SEEK_SET) target = ofs; + else if (whence == SEEK_CUR) target = (long)h->pos + ofs; + else if (whence == SEEK_END) target = (long)h->size + ofs; + else return -1; + if (target < 0) target = 0; + if ((size_t)target > h->size) target = (long)h->size; + h->pos = (size_t)target; + return 0; +} + +long gba_ftell(FILE* fp) { + gba_file_t* h = find_handle(fp); + return h ? (long)h->pos : -1; +} + +int gba_feof(FILE* fp) { + gba_file_t* h = find_handle(fp); + return h && h->pos >= h->size; +} + +const uint8_t* gba_file_rom_ptr(FILE* fp, size_t* out_avail) { + gba_file_t* h = find_handle(fp); + if (!h || !h->base) { + if (out_avail) *out_avail = 0; + return NULL; + } + size_t avail = (h->size > h->pos) ? h->size - h->pos : 0; + if (out_avail) *out_avail = avail; + return h->base + h->pos; +} + +/* ---- Static-pool allocator ----------------------------------------------- + Replaces newlib's heap with a fixed-size linked-block allocator backed by + a BSS-resident pool. Goals: + * No sbrk / no growing heap that competes with stack. + * Bounded RAM usage so we can fit on the GBA's 256 KB EWRAM. + * Free-list reuse so chtab churn between levels doesn't leak. + * Pointers from gba_file_rom_ptr (>= 0x08000000) are silently ignored on + free() — they live in cartridge ROM and have no allocator metadata. + The allocator is intentionally simple (first-fit, single coalesce on free); + PoP is single-threaded so we don't need locking. */ + +/* Peak boot-time demand, with ROM-direct sprites (no per-sprite pixel buffer): + onscreen_surface_ 320x192x8bpp .......... ~61 KB + ~250 SDL_Surface structs @ ~170 B ......... ~45 KB + chtab tables / dat_type handles ........... ~10 KB + peel surfaces + copyprot dialog scratch ... ~15 KB + i.e. ~130 KB peak. The old 96 KB pool ran dry mid-sprite-load, so + SDL_CreateRGBSurface returned NULL and the boot hung on a black screen. + 160 KB gives ~30 KB of headroom. This is the ONLY general allocator in the + build: newlib's heap is replaced by a tiny bounded static sbrk (below), and + the public malloc/calloc/realloc/free are --wrap'd onto this pool, so the + ROM performs no growing-heap allocation. */ +#define GBA_POOL_BYTES (196 * 1024) +static uint8_t gba_pool[GBA_POOL_BYTES] __attribute__((aligned(8))); + +extern void gba_panic(uint8_t color_idx); /* gba_video.c: solid screen + halt */ + +typedef struct block_hdr { + uint32_t size; /* payload size in bytes, bit 0 = in-use */ + struct block_hdr* prev; /* doubly-linked free list (only meaningful when free) */ + struct block_hdr* next; +} block_hdr_t; + +#define HDR_SIZE ((uint32_t)sizeof(block_hdr_t)) +#define ALIGN_UP(n) (((n) + 7u) & ~7u) + +static block_hdr_t* g_free_list = NULL; +static int g_pool_inited = 0; + +static void pool_init(void) { + block_hdr_t* h = (block_hdr_t*)gba_pool; + h->size = (GBA_POOL_BYTES - HDR_SIZE) & ~1u; + h->prev = NULL; + h->next = NULL; + g_free_list = h; + g_pool_inited = 1; +} + +static inline block_hdr_t* hdr_of(void* p) { + return (block_hdr_t*)((uint8_t*)p - HDR_SIZE); +} +static inline void* payload_of(block_hdr_t* h) { + return (void*)((uint8_t*)h + HDR_SIZE); +} + +static void freelist_remove(block_hdr_t* h) { + if (h->prev) h->prev->next = h->next; + else g_free_list = h->next; + if (h->next) h->next->prev = h->prev; +} +static void freelist_push(block_hdr_t* h) { + h->prev = NULL; + h->next = g_free_list; + if (g_free_list) g_free_list->prev = h; + g_free_list = h; +} + +/* First-fit over the free list; returns a payload or NULL (no panic). */ +static void* pool_first_fit(uint32_t need) { + for (block_hdr_t* h = g_free_list; h != NULL; h = h->next) { + if ((h->size & ~1u) >= need) { + uint32_t blk = h->size & ~1u; + freelist_remove(h); + /* Split if the remainder is big enough to hold a header + 8B. */ + if (blk >= need + HDR_SIZE + 16) { + block_hdr_t* rem = (block_hdr_t*)((uint8_t*)h + HDR_SIZE + need); + rem->size = (blk - need - HDR_SIZE) & ~1u; + freelist_push(rem); + h->size = need | 1u; + } else { + h->size = blk | 1u; + } + return payload_of(h); + } + } + return NULL; +} + +/* Full bidirectional defragmentation. __wrap_free() only coalesces with the + *following* block, so per-frame churn (e.g. the peel surfaces created while + the kid runs) shreds the pool into many tiny free blocks — we observed 24 KB + free split into 64 fragments, largest only 948 B, which OOM-panicked a peel + alloc mid-run. This walks the pool in physical order and merges every run of + adjacent free blocks, rebuilding the free list. Run on alloc failure. */ +static void pool_coalesce_all(void) { + g_free_list = NULL; + uint8_t* end = gba_pool + GBA_POOL_BYTES; + block_hdr_t* h = (block_hdr_t*)gba_pool; + while ((uint8_t*)h < end) { + uint32_t hsz = h->size & ~1u; + block_hdr_t* nextp = (block_hdr_t*)((uint8_t*)h + HDR_SIZE + hsz); + if (h->size & 1u) { /* in use: skip */ + h = nextp; + continue; + } + /* free: absorb all immediately-following free blocks */ + while ((uint8_t*)nextp < end && !(nextp->size & 1u)) { + hsz += HDR_SIZE + (nextp->size & ~1u); + nextp = (block_hdr_t*)((uint8_t*)h + HDR_SIZE + hsz); + } + h->size = hsz & ~1u; /* mark free, store merged size */ + freelist_push(h); + h = nextp; + } +} + +void* __wrap_malloc(size_t n) { + if (!g_pool_inited) pool_init(); + if (n == 0) return NULL; + uint32_t need = ALIGN_UP((uint32_t)n); + + void* p = pool_first_fit(need); + if (p) return p; + + /* No single block big enough — defragment and try once more before + giving up. This recovers the fragmented-but-ample case. */ + pool_coalesce_all(); + p = pool_first_fit(need); + if (p) return p; + + /* Genuinely out of pool. Halt visibly so the condition is diagnosable + rather than letting upstream deref a NULL surface. */ + gba_panic(3); + return NULL; +} + +void* __wrap_calloc(size_t nmemb, size_t size) { + size_t n = nmemb * size; + void* p = __wrap_malloc(n); + if (p) memset(p, 0, n); + return p; +} + +void __wrap_free(void* p) { + if (!p) return; + if ((uintptr_t)p >= 0x08000000u) return; /* ROM pointer: no-op */ + if ((uintptr_t)p < (uintptr_t)gba_pool || + (uintptr_t)p >= (uintptr_t)(gba_pool + GBA_POOL_BYTES)) return; /* not ours */ + block_hdr_t* h = hdr_of(p); + h->size &= ~1u; + /* Coalesce with the immediately-following block if it's also free. */ + block_hdr_t* nxt = (block_hdr_t*)((uint8_t*)h + HDR_SIZE + (h->size & ~1u)); + if ((uint8_t*)nxt < (gba_pool + GBA_POOL_BYTES) && !(nxt->size & 1u)) { + freelist_remove(nxt); + h->size += HDR_SIZE + (nxt->size & ~1u); + } + freelist_push(h); +} + +void* __wrap_realloc(void* p, size_t n) { + if (!p) return __wrap_malloc(n); + if (n == 0) { __wrap_free(p); return NULL; } + /* GBA path: assume in-pool, copy & free. PoP build doesn't actually + exercise this — replay/midi/stb_vorbis are excluded. */ + if ((uintptr_t)p >= 0x08000000u) { + void* q = __wrap_malloc(n); + if (q) memcpy(q, p, n); + return q; + } + block_hdr_t* h = hdr_of(p); + uint32_t old = h->size & ~1u; + if (old >= n) return p; + void* q = __wrap_malloc(n); + if (!q) return NULL; + memcpy(q, p, old); + __wrap_free(p); + return q; +} + +void gba_free_safe(void* p) { __wrap_free(p); } + +/* Named entry points so the GBA port layer (sdl_shim.c, gba_file.c) can call + the static pool explicitly, without using the malloc/calloc/realloc tokens. + Upstream src/*.c still uses those tokens, transparently routed here by the + linker's --wrap. */ +void* gba_alloc(size_t n) { return __wrap_malloc(n); } +void* gba_calloc(size_t nmemb, size_t sz) { return __wrap_calloc(nmemb, sz); } +void* gba_realloc(void* p, size_t n) { return __wrap_realloc(p, n); } +void gba_free(void* p) { __wrap_free(p); } + +/* ---- Bounded static newlib heap ----------------------------------------- + Newlib's *printf family can reach _malloc_r (e.g. _dtoa for %f, or + __submore), which goes through _sbrk_r -> the heap window the linker + reserved between __end__ and __eheap_end. That window is static on GBA but + unbounded up to end-of-EWRAM, and competes with our pool. Override _sbrk_r + with a tiny fixed static arena so any such allocation is bounded and fully + static; the ROM then contains no growing heap at all. SDLPoP's GBA path + formats only ints/strings, so this is rarely (if ever) touched. */ +#define GBA_NEWLIB_HEAP_BYTES (8 * 1024) +static uint8_t s_newlib_heap[GBA_NEWLIB_HEAP_BYTES] __attribute__((aligned(8))); + +struct _reent; +void* _sbrk_r(struct _reent* reent, ptrdiff_t incr) { + (void)reent; + static uint8_t* brk = s_newlib_heap; + uint8_t* prev = brk; + if (incr < 0) { + brk += incr; + if (brk < s_newlib_heap) brk = s_newlib_heap; + return brk; + } + if (prev + incr > s_newlib_heap + sizeof(s_newlib_heap)) { + return (void*)-1; /* ENOMEM; newlib malloc returns NULL */ + } + brk += incr; + return prev; +} + +int gba_fgetc(FILE* fp) { + gba_file_t* h = find_handle(fp); + if (!h || h->pos >= h->size) return EOF; + const uint8_t* src = h->is_save ? h->wbuf : h->base; + return (int)src[h->pos++]; +} + +int gba_fputc(int c, FILE* fp) { (void)c; (void)fp; return EOF; } +int gba_ferror(FILE* fp) { (void)fp; return 0; } + +/* Stat-shaped emulation: SDLPoP uses stat() to distinguish files from + directories. On GBA every blob is a "regular file"; nothing is a directory. + The actual struct stat layout is provided by newlib; we just fill mode and + size at the standard offsets via the public API. */ +#include +#undef stat /* common.h redefines this; undefine for the wrapper body */ +int gba_stat_compat(const char* path, struct stat* out) { + const gba_blob_t* blob = gba_find_blob(path); + if (!blob || !out) return -1; + memset(out, 0, sizeof(*out)); + out->st_mode = 0100000u; /* S_IFREG */ + out->st_size = (long)blob->size; + return 0; +} diff --git a/gba/src/gba_input.c b/gba/src/gba_input.c new file mode 100644 index 00000000..4a6b93eb --- /dev/null +++ b/gba/src/gba_input.c @@ -0,0 +1,53 @@ +/* GBA button -> SDLPoP scancode mapping. + Mapping is chosen so the on-screen actions match the PC keyboard defaults: + D-Pad -> arrow keys (move, jump, crouch) + A -> Shift (grab/draw sword/pick up) + B -> Space (no-op pause / map back to escape menu) + L -> Ctrl (chop, fight) + R -> Alt (modifier) + Start -> Enter (advance dialog/menus) + Select -> Backspace (cancel / quicksave toggle) +*/ +#include +#include +#include "gba_port.h" + +extern void gba_set_key(int scancode, int down); + +static u16 s_prev_keys; + +static const struct { u16 mask; int scancode; } s_map[] = { + { KEY_LEFT, SDL_SCANCODE_LEFT }, + { KEY_RIGHT, SDL_SCANCODE_RIGHT }, + { KEY_UP, SDL_SCANCODE_UP }, + { KEY_DOWN, SDL_SCANCODE_DOWN }, + { KEY_A, SDL_SCANCODE_LSHIFT }, + { KEY_B, SDL_SCANCODE_SPACE }, + { KEY_L, SDL_SCANCODE_LCTRL }, + { KEY_R, SDL_SCANCODE_LALT }, + { KEY_START, SDL_SCANCODE_RETURN }, + { KEY_SELECT, SDL_SCANCODE_BACKSPACE }, +}; + +void gba_input_init(void) { + s_prev_keys = 0; +} + +void gba_input_poll(void) { + /* KEYINPUT bits are 0 when pressed; XOR-flip to "pressed = 1". */ + u16 keys = ~REG_KEYINPUT & 0x03FF; + u16 changed = keys ^ s_prev_keys; + if (!changed) return; + + /* DEBUG (visible with --agb-print): show raw button state each change so we + can tell whether the emulator is delivering presses to the ROM at all. */ + { extern void gba_log(const char*,...); gba_log("KEYINPUT raw=0x%03x pressed=0x%03x", (unsigned)(REG_KEYINPUT & 0x3FF), (unsigned)keys); } + + for (unsigned i = 0; i < sizeof(s_map)/sizeof(s_map[0]); ++i) { + u16 m = s_map[i].mask; + if (changed & m) { + gba_set_key(s_map[i].scancode, (keys & m) ? 1 : 0); + } + } + s_prev_keys = keys; +} diff --git a/gba/src/gba_main.c b/gba/src/gba_main.c new file mode 100644 index 00000000..e0c471e9 --- /dev/null +++ b/gba/src/gba_main.c @@ -0,0 +1,58 @@ +/* GBA entry point. Replaces src/main.c (which is excluded by the Makefile). */ +#include +#include +#include +#include "common.h" +#include "gba_port.h" + +extern void pop_main(void); +extern void gba_input_init(void); +extern void gba_video_init(void); +extern void gba_vblank_isr(void); + +/* g_argc / g_argv are defined upstream in data.c via the BODY/data.h + mechanism — do NOT redeclare them here. We just point them at a dummy + argv before pop_main(). */ +extern int g_argc; +extern char** g_argv; +static char* s_argv0[] = { (char*)"sdlpop" }; + +/* GamePak wait-state control register (libgba doesn't define it). */ +#define REG_WAITCNT_ (*(volatile unsigned short*)0x04000204) + +int main(void) { + /* Enable GamePak prefetch + fastest wait states (WS0 3,1 / prefetch on). + The BIOS leaves this at 0 (slowest: 4,2 cycles, no prefetch), which + roughly doubles the cost of every instruction fetched from cartridge + ROM — and all the hot blit/composite/decode loops run from ROM. This + is the cheapest, largest single speedup for the port. */ + REG_WAITCNT_ = 0x4317; + + /* Force the single-byte "C" locale. This newlib is built multibyte-capable + and was defaulting to a UTF-8 locale, so *printf-to-string (vsnprintf et + al.) scanned the format string through __utf8_mbtowc — which spun + forever, freezing the game the moment gba_log() ran on the first move. + MB_CUR_MAX==1 makes vfprintf take the fast single-byte path. */ + setlocale(LC_ALL, "C"); + + g_argc = 1; + g_argv = s_argv0; + + /* Enable AGBPrint / mGBA debug logging (visible in VBA-M / mGBA when run + with --agb-print). Harmless on real hardware. gba_log() is available + from gba_debug.c for future debugging. */ + extern int gba_mgba_open(void); + gba_mgba_open(); + + gba_video_init(); + gba_input_init(); + + irqInit(); + irqSet(IRQ_VBLANK, gba_vblank_isr); + irqEnable(IRQ_VBLANK); + + pop_main(); + + while (1) VBlankIntrWait(); + return 0; +} diff --git a/gba/src/gba_printf.c b/gba/src/gba_printf.c new file mode 100644 index 00000000..cf773b31 --- /dev/null +++ b/gba/src/gba_printf.c @@ -0,0 +1,119 @@ +/* Minimal, self-contained printf-to-string family for the GBA build. + * + * devkitARM's newlib vfprintf-to-string (_svfprintf_r / __ssputs_r / + * __ssprint_r / __locale_mb_cur_max) spins forever on this target — it froze + * the game whenever a snprintf ran during gameplay (e.g. a resource filename + * built in the open_dat path on the first move). Rather than fight newlib's + * locale/multibyte machinery, route the game's snprintf/vsnprintf/sprintf/ + * vsprintf through this tiny formatter via the Makefile's -Wl,--wrap. + * + * Supports the specifiers SDLPoP actually uses: %d %i %u %x %X %c %s %p %%, + * an optional field width with optional '0' pad, and the 'l'/'h' length + * modifiers (parsed and ignored — values fit in int/long the same here). + * Floating point (%f) is not used on GBA (only under CHECK_TIMING). + */ +#include +#include +#include + +struct sink { char* buf; size_t cap; size_t len; }; + +static void emit(struct sink* s, char c) { + if (s->len + 1 < s->cap) s->buf[s->len] = c; + s->len++; +} +static void emit_str(struct sink* s, const char* p, int width, int left, char pad) { + int n = 0; + const char* q = p ? p : "(null)"; + while (q[n]) n++; + if (!left) for (int i = n; i < width; ++i) emit(s, pad); + for (int i = 0; i < n; ++i) emit(s, q[i]); + if (left) for (int i = n; i < width; ++i) emit(s, ' '); +} +static void emit_num(struct sink* s, unsigned long v, int base, int is_neg, + int upper, int width, int left, char pad) { + char tmp[32]; + int n = 0; + const char* digs = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + if (v == 0) tmp[n++] = '0'; + while (v) { tmp[n++] = digs[v % base]; v /= base; } + if (is_neg) tmp[n++] = '-'; + /* zero-pad goes after the sign; space-pad before. */ + int total = n; + if (!left && pad == '0') { + /* print sign first, then zeros */ + int sign = is_neg ? 1 : 0; + if (is_neg) { emit(s, '-'); n--; } + for (int i = total; i < width; ++i) emit(s, '0'); + for (int i = n - 1; i >= 0; --i) emit(s, tmp[i]); + (void)sign; + return; + } + if (!left) for (int i = total; i < width; ++i) emit(s, ' '); + for (int i = n - 1; i >= 0; --i) emit(s, tmp[i]); + if (left) for (int i = total; i < width; ++i) emit(s, ' '); +} + +int __wrap_vsnprintf(char* buf, size_t size, const char* fmt, va_list ap) { + struct sink s = { buf, size, 0 }; + for (const char* p = fmt; *p; ++p) { + if (*p != '%') { emit(&s, *p); continue; } + ++p; + int left = 0; char pad = ' '; int width = 0; + for (;; ++p) { + if (*p == '-') left = 1; + else if (*p == '0') pad = '0'; + else if (*p == '+' || *p == ' ' || *p == '#') { /* ignore */ } + else break; + } + while (*p >= '0' && *p <= '9') { width = width * 10 + (*p - '0'); ++p; } + if (*p == '.') { ++p; while (*p >= '0' && *p <= '9') ++p; } /* precision: skip */ + int lng = 0; + while (*p == 'l' || *p == 'h' || *p == 'z') { if (*p == 'l') lng++; ++p; } + switch (*p) { + case 'd': case 'i': { + long v = lng ? va_arg(ap, long) : (long)va_arg(ap, int); + int neg = v < 0; + unsigned long uv = neg ? (unsigned long)(-v) : (unsigned long)v; + emit_num(&s, uv, 10, neg, 0, width, left, pad); + } break; + case 'u': { + unsigned long v = lng ? va_arg(ap, unsigned long) : (unsigned long)va_arg(ap, unsigned int); + emit_num(&s, v, 10, 0, 0, width, left, pad); + } break; + case 'x': case 'X': { + unsigned long v = lng ? va_arg(ap, unsigned long) : (unsigned long)va_arg(ap, unsigned int); + emit_num(&s, v, 16, 0, *p == 'X', width, left, pad); + } break; + case 'p': { + uintptr_t v = (uintptr_t)va_arg(ap, void*); + emit(&s, '0'); emit(&s, 'x'); + emit_num(&s, (unsigned long)v, 16, 0, 0, width, left, pad); + } break; + case 'c': emit(&s, (char)va_arg(ap, int)); break; + case 's': emit_str(&s, va_arg(ap, const char*), width, left, ' '); break; + case '%': emit(&s, '%'); break; + case '\0': goto done; + default: emit(&s, '%'); emit(&s, *p); break; + } + } +done: + if (s.cap) s.buf[s.len < s.cap ? s.len : s.cap - 1] = '\0'; + return (int)s.len; +} + +int __wrap_snprintf(char* buf, size_t size, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); + int r = __wrap_vsnprintf(buf, size, fmt, ap); + va_end(ap); + return r; +} +int __wrap_vsprintf(char* buf, const char* fmt, va_list ap) { + return __wrap_vsnprintf(buf, (size_t)0x7fffffff, fmt, ap); +} +int __wrap_sprintf(char* buf, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); + int r = __wrap_vsnprintf(buf, (size_t)0x7fffffff, fmt, ap); + va_end(ap); + return r; +} diff --git a/gba/src/gba_save.c b/gba/src/gba_save.c new file mode 100644 index 00000000..ba9c103b --- /dev/null +++ b/gba/src/gba_save.c @@ -0,0 +1,60 @@ +/* GBA SRAM save layer. + The cart's SRAM is 32 KB at 0x0E000000, byte-only access. gbafix tags the + ROM as having SRAM so emulators will create/persist a .sav file. + + Layout: + [0..15] gba_sram_header { magic, version, payload_len, fletcher32 } + [16..N+15] opaque payload + + SDLPoP writes to a file named "PRINCE.SAV" (see save_file in seg000.c). + We intercept that file name in gba_file.c — for "wb" mode we buffer writes + to a RAM scratch and commit to SRAM on fclose; for "rb" we populate a RAM + copy from SRAM at fopen. All other names route to read-only ROM blobs. +*/ +#include +#include "gba_port.h" + +#define SRAM_BASE ((volatile uint8_t*)0x0E000000) +#define SRAM_CAP 0x8000u + +static uint32_t fletcher32(const uint8_t* d, size_t n) { + uint32_t s1 = 0xFFFF, s2 = 0xFFFF; + while (n) { + size_t t = n > 359 ? 359 : n; + n -= t; + while (t--) { s1 = (s1 + *d++) % 0xFFFF; s2 = (s2 + s1) % 0xFFFF; } + } + return (s2 << 16) | s1; +} + +int gba_sram_write(const void* data, size_t len) { + if (len + sizeof(gba_sram_header_t) > SRAM_CAP) return -1; + gba_sram_header_t hdr = { + .magic = GBA_SRAM_MAGIC, + .version = 1, + .payload_len = (uint32_t)len, + .checksum = fletcher32((const uint8_t*)data, len), + }; + const uint8_t* hb = (const uint8_t*)&hdr; + for (size_t i = 0; i < sizeof(hdr); ++i) SRAM_BASE[i] = hb[i]; + const uint8_t* p = (const uint8_t*)data; + for (size_t i = 0; i < len; ++i) SRAM_BASE[sizeof(hdr) + i] = p[i]; + return 0; +} + +int gba_sram_read(void* data, size_t cap, size_t* out_len) { + gba_sram_header_t hdr; + uint8_t* hb = (uint8_t*)&hdr; + for (size_t i = 0; i < sizeof(hdr); ++i) hb[i] = SRAM_BASE[i]; + if (hdr.magic != GBA_SRAM_MAGIC) return -1; + if (hdr.payload_len > cap) return -2; + uint8_t* p = (uint8_t*)data; + for (size_t i = 0; i < hdr.payload_len; ++i) p[i] = SRAM_BASE[sizeof(hdr) + i]; + if (fletcher32(p, hdr.payload_len) != hdr.checksum) return -3; + if (out_len) *out_len = hdr.payload_len; + return 0; +} + +void gba_sram_erase(void) { + for (size_t i = 0; i < 16; ++i) SRAM_BASE[i] = 0; +} diff --git a/gba/src/gba_sound_bridge.c b/gba/src/gba_sound_bridge.c new file mode 100644 index 00000000..4a4e5807 --- /dev/null +++ b/gba/src/gba_sound_bridge.c @@ -0,0 +1,203 @@ +/* GBA sound bridge: replaces the audio entry points called by SDLPoP. + The original implementations live in seg009.c and require AUDIO_S16SYS + stereo @ 44.1 kHz mixing, plus OPL3/MIDI/OGG — all infeasible on a + 16.78 MHz ARM7. Here we ignore the SDL callback path completely and + feed our DMA1 mixer directly. */ +#include +#include +#include +#include "common.h" +#include "gba_port.h" + +extern int gba_sfx_play(const int8_t* data, uint32_t length, int loop, uint8_t volume); +extern void gba_music_play(const int8_t* data, uint32_t length, int loop); +extern void gba_music_stop(void); +extern void gba_sfx_stop_all(void); +extern int gba_sfx_any_playing(void); + +/* Mirrors seg009.c state symbols so check_sound_playing() compiles. + sound_names is owned by upstream data.c; do NOT redeclare it here. */ +short speaker_playing = 0; +short digi_playing = 0; +short midi_playing = 0; +short ogg_playing = 0; +SDL_AudioSpec* digi_audiospec = NULL; +byte* digi_buffer = NULL; +byte* digi_remaining_pos = NULL; +int digi_remaining_length = 0; +int wave_version = -1; +int digi_unavailable = 0; +const int digi_samplerate = 18157; +short square_wave_state = 4000; +float square_wave_samples_since_last_flip = 0.0f; +extern char** sound_names; + +#ifndef __GBA__ +extern int is_sound_on; +extern int enable_music; +#endif + +void speaker_sound_stop(void) { gba_sfx_stop_all(); speaker_playing = 0; } +void stop_digi(void) { gba_sfx_stop_all(); digi_playing = 0; } +void stop_ogg(void) { gba_music_stop(); ogg_playing = 0; } +void stop_midi(void) { gba_music_stop(); midi_playing = 0; } + +void stop_sounds(void) { + gba_sfx_stop_all(); + gba_music_stop(); + speaker_playing = digi_playing = midi_playing = ogg_playing = 0; +} + +void init_digi(void) { + if (digi_audiospec != NULL) return; + static SDL_AudioSpec spec; + spec.freq = digi_samplerate; + spec.format = AUDIO_S8; + spec.channels = 1; + spec.samples = 304; + spec.silence = 0; + spec.callback = NULL; + spec.userdata = NULL; + digi_audiospec = &spec; + gba_audio_init(&spec); +} + +void turn_music_on_off(byte on) { enable_music = on; if (!on) gba_music_stop(); } +void turn_sound_on_off(byte on) { is_sound_on = on; if (!on) gba_sfx_stop_all(); } +int check_sound_playing(void) { return gba_sfx_any_playing(); } + +/* digi sound buffer: PoP 1.3+ wraps a digi_new_type whose samples are + unsigned 8-bit. tools/preproc_dat subtracts 128 from each sample at + build time and stamps bit 15 of digi_new.unknown2 as a sentinel; the + runtime simply hands the ROM pointer straight to the mixer. Without + pre-conversion we'd need a per-active-sound scratch (64 KB of EWRAM + on the old design), so an unstamped buffer is dropped rather than + played back inverted. */ +void play_digi_sound(sound_buffer_type* buffer) { + if (!buffer) return; + init_digi(); + stop_digi(); + + const int8_t* samples; + uint32_t count; + if ((buffer->type & 7) == sound_digi_converted) { + samples = (const int8_t*)buffer->converted.samples; + count = (uint32_t)buffer->converted.length; + } else if ((buffer->type & 7) == sound_digi) { + uint16_t unk2 = SDL_SwapLE16(buffer->digi_new.unknown2); + if (!(unk2 & 0x8000)) return; + samples = (const int8_t*)buffer->digi_new.samples; + count = SDL_SwapLE16(buffer->digi_new.sample_count); + } else { + return; + } + digi_playing = 1; + gba_sfx_play(samples, count, 0, 200); +} + +void play_speaker_sound(sound_buffer_type* buffer) { + (void)buffer; /* dropped on GBA — PC speaker square wave isn't worth the cost */ +} + +/* Music: PoP's MIDI tracks can't be FM-synthesized live on the ARM7, so they + are pre-rendered to signed 8-bit 18157 Hz mono PCM on the host by + gba/tools/render_music.c and embedded as the `musicpcm_bin` blob. The blob + is a small index table keyed by sound id, followed by the concatenated PCM + (see render_music.c for the exact layout). We look up the track being played + and feed it to the dedicated music voice. */ +extern const uint8_t musicpcm_bin[]; +extern const uint8_t musicpcm_bin_end[]; + +/* Read a little-endian u32 byte-wise (blob alignment is not guaranteed). */ +static uint32_t mpcm_rd32(const uint8_t* p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static const int8_t* find_music_pcm(int sound_id, uint32_t* out_len) { + const uint8_t* base = musicpcm_bin; + if ((size_t)(musicpcm_bin_end - musicpcm_bin) < 8) return NULL; + if (mpcm_rd32(base) != 0x4D43504Du) return NULL; /* 'MPCM' */ + uint32_t count = mpcm_rd32(base + 4); + const uint8_t* e = base + 8; + for (uint32_t i = 0; i < count; ++i, e += 12) { + if ((int)mpcm_rd32(e) == sound_id) { + uint32_t off = mpcm_rd32(e + 4); + *out_len = mpcm_rd32(e + 8); + return (const int8_t*)(base + off); + } + } + return NULL; +} + +/* Recover which sound index this buffer is: scan sound_pointers[] (robust even + when the caller didn't update current_sound, e.g. show_title's story sounds). + Falls back to the global current_sound. */ +static int sound_index_of(sound_buffer_type* buffer) { + for (int i = 0; i < 58; ++i) { + if (sound_pointers[i] == buffer) return i; + } + return (int)current_sound; +} + +void play_midi_sound(sound_buffer_type* buffer) { + if (!buffer) return; + init_digi(); + uint32_t len = 0; + const int8_t* pcm = find_music_pcm(sound_index_of(buffer), &len); + if (pcm && len) { + gba_music_play(pcm, len, 0); /* PoP music is one-shot, no looping */ + midi_playing = 1; + } +} +void play_ogg_sound (sound_buffer_type* buffer) { (void)buffer; } + +void play_sound_from_buffer(sound_buffer_type* buffer) { + if (!buffer) return; + switch (buffer->type & 7) { + case sound_digi: + case sound_digi_converted: + play_digi_sound(buffer); + break; + case sound_midi: + play_midi_sound(buffer); + break; + case sound_speaker: + case sound_ogg: + default: + /* silently dropped */ + break; + } +} + +void free_sound(sound_buffer_type* buffer) { + if (!buffer) return; + gba_free(buffer); /* safe on NULL / ROM / non-pool pointers */ +} + +void load_sound_names(void) { + /* On GBA we don't read names.txt — music names list isn't used. */ +} + +/* Replacement for seg009.c's load_sound (which is inside #ifndef __GBA__). + We do NOT copy samples into EWRAM: load_from_opendats_alloc() returns a + pointer straight into cartridge ROM for DAT-resident resources (see the + #ifdef __GBA__ path in seg009.c), and the digi samples were already + converted to signed 8-bit (with the 0x8000 sentinel) at build time by + tools/preproc_dat. So the returned ROM buffer is handed directly to the + DMA mixer by play_digi_sound(). free_sound() no-ops on ROM pointers. */ +extern void* load_from_opendats_alloc(int resource, const char* extension, + data_location* out_result, int* out_size); +sound_buffer_type* load_sound(int index) { + init_digi(); /* make sure the DMA mixer is running */ + return (sound_buffer_type*)load_from_opendats_alloc(index + 10000, "bin", NULL, NULL); +} + +/* These were defined in seg009.c but are also referenced from elsewhere. */ +void generate_square_wave(byte* s, float f, int n) { (void)s; (void)f; (void)n; } +void speaker_callback(void* u, Uint8* s, int n) { (void)u; (void)s; (void)n; } +void digi_callback (void* u, Uint8* s, int n) { (void)u; (void)s; (void)n; } +void ogg_callback (void* u, Uint8* s, int n) { (void)u; (void)s; (void)n; } +void audio_callback (void* u, Uint8* s, int n) { (void)u; (void)s; (void)n; } +void midi_callback (void* u, Uint8* s, int n) { (void)u; (void)s; (void)n; } +void init_midi(void) { } diff --git a/gba/src/gba_time.c b/gba/src/gba_time.c new file mode 100644 index 00000000..c7e2426c --- /dev/null +++ b/gba/src/gba_time.c @@ -0,0 +1,39 @@ +/* Tick counter and VBlank wait. Timer 0 increments at 16384 Hz so a 32-bit + millisecond counter is enough for ~49 days. */ +#include +#include "gba_port.h" + +static volatile uint32_t s_vblank_count; +static uint32_t s_timer_interval_ms; +static uint32_t s_timer_next_fire; + +void gba_vblank_isr(void) { + s_vblank_count++; + /* Mix one VBlank's worth of audio (the audio buffer is sized to match). */ + extern void gba_audio_on_vblank(void); + gba_audio_on_vblank(); + + if (s_timer_interval_ms && s_vblank_count >= s_timer_next_fire) { + s_timer_next_fire = s_vblank_count + (s_timer_interval_ms * 60 + 999) / 1000; + gba_timer_fire_cb(); + } +} + +uint32_t gba_get_ticks_ms(void) { + /* 60 Hz VBlank: ms = count * 1000 / 60. Use 32-bit arithmetic carefully. */ + uint32_t c = s_vblank_count; + return (c * 1000u + 30u) / 60u; +} + +void gba_delay_ms(uint32_t ms) { + uint32_t target = s_vblank_count + (ms * 60 + 999) / 1000; + while (s_vblank_count < target) VBlankIntrWait(); +} + +void gba_timer_install(uint32_t interval_ms) { + s_timer_interval_ms = interval_ms; + s_timer_next_fire = s_vblank_count + (interval_ms * 60 + 999) / 1000; +} +void gba_timer_uninstall(void) { + s_timer_interval_ms = 0; +} diff --git a/gba/src/gba_video.c b/gba/src/gba_video.c new file mode 100644 index 00000000..3e72be8e --- /dev/null +++ b/gba/src/gba_video.c @@ -0,0 +1,129 @@ +/* GBA Mode 4 (240x160 @ 8bpp paletted) front-end for the SDL shim. + Single framebuffer: VRAM page A (0x6000000) is the one and only surface, + always displayed. The game maintains the full frame in onscreen_surface_ + and we copy it here once per frame right after VBlank. (The old A/B page + flip is gone.) */ +#include +#include +#include "gba_port.h" + +#define MODE4_FB_A ((u16*)0x06000000) +#define BG_PAL ((u16*)0x05000000) + +static int s_vp_x, s_vp_y; + +/* Palette-row offset for the next decode_image call. Set by + load_sprites_from_file before its inner load_image loop; reset to 0 after. + Read by decode_image to bake row*16 into each non-zero sprite pixel value + so 8bpp blits land at the correct slot of the global palette[]. */ +int gba_decode_palette_offset = 0; + +void gba_video_init(void) { + /* IMPORTANT: GBA VRAM and PALRAM ignore 8-bit writes (or duplicate the + byte into both halves of a u16). newlib's memset writes byte-at-a-time + on unaligned tails, so we use explicit 32-bit stores here. */ + volatile u32* pageA = (volatile u32*)0x06000000; + volatile u32* pal32 = (volatile u32*)0x05000000; + for (int i = 0; i < (240*160)/4; ++i) pageA[i] = 0; + for (int i = 0; i < 512/4; ++i) pal32[i] = 0; + + /* Single, always-displayed page A — no BACKBUFFER bit, never flipped. */ + SetMode(MODE_4 | BG2_ON); + /* Centre the 240x160 viewport over the 320x192 PoP world so the first + frame shows the playable area instead of the top-left padding/status. */ + s_vp_x = (320 - 240) / 2; /* 40 */ + s_vp_y = (192 - 160) / 2; /* 16 */ +} + +/* Visual panic screen: flood the screen with a solid color and halt. Used by + our quit() override and assertion paths so we don't fall off the end into + newlib's exit() loop. */ +void gba_panic(uint8_t color_idx) { + BG_PAL[color_idx] = RGB5(31, 0, 0); /* red */ + u16 fill = (u16)(color_idx | (color_idx << 8)); + for (int i = 0; i < 240*160/2; ++i) MODE4_FB_A[i] = fill; + REG_DISPCNT = MODE_4 | BG2_ON; + while (1) VBlankIntrWait(); +} + +void gba_set_viewport(int x, int y) { s_vp_x = x; s_vp_y = y; } +void gba_get_viewport(int* x, int* y) { if (x) *x = s_vp_x; if (y) *y = s_vp_y; } + +void gba_apply_palette(const SDL_Color* colors, int first, int n) { + for (int i = 0; i < n; ++i) { + int idx = first + i; + if ((unsigned)idx >= 256) break; + u8 r = colors[i].r >> 3; + u8 g = colors[i].g >> 3; + u8 b = colors[i].b >> 3; + BG_PAL[idx] = RGB5(r, g, b); + } +} + +/* SDLPoP maintains its own rgb_type palette[256] in seg009.c via set_pal*(); + it does not always sync that to SDL_Palette. We read both and let SDL_Palette + override where it's been populated. */ +struct rgb_type_ext { uint8_t r, g, b; }; +extern struct rgb_type_ext palette[256]; + +/* Copy a clipped 240x160 view from the (possibly larger) source surface to + the back framebuffer page, then flip pages on VBlank. */ +int gba_present_surface(SDL_Surface* s) { + if (!s || !s->pixels || s->format->BytesPerPixel != 1) return -1; + + /* Upload SDLPoP's authoritative palette[] to BG_PAL. Values are 6-bit + VGA DAC (0..63), shift by 1 to map to GBA's 5-bit (0..31). The surface's + own SDL_Palette is intentionally NOT applied: onscreen_surface_ is + blitted to from sprite surfaces but its palette is never populated, so + reading it would clobber BG_PAL with all-zero colors -> black screen. + Only re-upload when palette[] actually changed (it changes per level / + torch flicker, not every frame) — present() is called several times per + game frame, so this avoids 256 RGB conversions + PALRAM writes each. */ + static struct rgb_type_ext s_pal_cache[256]; + if (memcmp(s_pal_cache, palette, sizeof(s_pal_cache)) != 0) { + memcpy(s_pal_cache, palette, sizeof(s_pal_cache)); + for (int i = 0; i < 256; ++i) { + u8 r = palette[i].r >> 1; + u8 g = palette[i].g >> 1; + u8 b = palette[i].b >> 1; + if (r > 31) r = 31; + if (g > 31) g = 31; + if (b > 31) b = 31; + BG_PAL[i] = RGB5(r, g, b); + } + } + + int sx = s_vp_x, sy = s_vp_y; + if (sx < 0) sx = 0; + if (sy < 0) sy = 0; + if (sx > s->w - 240) sx = s->w - 240 > 0 ? s->w - 240 : 0; + if (sy > s->h - 160) sy = s->h - 160 > 0 ? s->h - 160 : 0; + + int rows = s->h - sy; if (rows > 160) rows = 160; + int cols = s->w - sx; if (cols > 240) cols = 240; + + /* Wait for VBlank, then write straight into the single displayed page so + the copy starts in the blank interval. Mode 4 VRAM only accepts 16-bit + writes (two pixels at once); pack pairs from the source row. */ + VBlankIntrWait(); + u16* dst = MODE4_FB_A; + for (int y = 0; y < rows; ++y) { + const u8* sp = (const u8*)s->pixels + (size_t)(sy + y) * s->pitch + sx; + u16* dp = dst + (size_t)y * 120; + int x = 0; + for (; x + 1 < cols; x += 2) { + dp[x >> 1] = (u16)(sp[x] | (sp[x + 1] << 8)); + } + if (x < cols) { + /* odd-pixel column: read-modify-write to preserve neighbor */ + u16 v = dp[x >> 1]; + dp[x >> 1] = (v & 0xFF00) | sp[x]; + } + } + /* zero-out unused right/bottom if source is smaller than 240x160 */ + for (int y = rows; y < 160; ++y) { + u16* dp = dst + (size_t)y * 120; + for (int x = 0; x < 120; ++x) dp[x] = 0; + } + return 0; +} diff --git a/gba/src/gba_viewport.c b/gba/src/gba_viewport.c new file mode 100644 index 00000000..7b7772d1 --- /dev/null +++ b/gba/src/gba_viewport.c @@ -0,0 +1,54 @@ +/* Viewport pan/clamp for the 240x160 GBA screen. + The PoP game world renders into a 320x192 offscreen surface (one room). + We track the prince's logical X position and clamp the camera so he stays + in a centre band. Called once per frame from a hook in seg009.c update path. */ +#include "gba_port.h" + +extern void gba_set_viewport(int x, int y); +extern void gba_get_viewport(int* x, int* y); + +#define WORLD_W 320 +#define WORLD_H 192 +#define VIEW_W 240 +#define VIEW_H 160 + +/* The prince's ACTUAL drawn pixel position in the 320x192 onscreen buffer, + recorded each frame by draw_mid() (see src/seg008.c, #ifdef __GBA__). This is + the real blit position, unlike Char.x which is PoP's internal coordinate and + does not map linearly to buffer pixels. gba_kid_valid stays 0 until the prince + has been drawn at least once (menus/cutscenes), in which case we keep the + centred default. */ +int gba_kid_px = 0, gba_kid_py = 0, gba_kid_valid = 0; + +/* Set by draw_menu() (src/menu.c) while the pause/settings menu is open. The + menu pins the viewport itself (see gba_menu_set_viewport there), so the + per-frame camera follow below must not fight it. */ +int gba_menu_active = 0; + +static int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } + +void gba_camera_update(void) { + if (gba_menu_active) return; /* menu owns the viewport while open */ + + /* Default: centre the 240x160 viewport over the 320x192 world. */ + int target_x = (WORLD_W - VIEW_W) / 2; /* 40 */ + int target_y = (WORLD_H - VIEW_H) / 2; /* 16 */ + + if (gba_kid_valid) { + /* Centre on the prince's drawn pixel position, clamped to the world. + This follows him on BOTH axes (the X range is 0..80, the Y range is + 0..32 — the room is only 32px taller than the view). */ + target_x = clampi(gba_kid_px - VIEW_W / 2, 0, WORLD_W - VIEW_W); + target_y = clampi(gba_kid_py - VIEW_H / 2, 0, WORLD_H - VIEW_H); + } + + /* Follow responsively: close most of the gap each frame (a quarter-step, + min 1px) so running/falling stays framed without 1px-per-frame lag, yet + still glides rather than hard-snapping. */ + int cx, cy; + gba_get_viewport(&cx, &cy); + int dx = target_x - cx, dy = target_y - cy; + cx += (dx > 0 ? (dx + 3) / 4 : -((-dx + 3) / 4)); + cy += (dy > 0 ? (dy + 3) / 4 : -((-dy + 3) / 4)); + gba_set_viewport(cx, cy); +} diff --git a/gba/src/hc_font_decoded.c b/gba/src/hc_font_decoded.c new file mode 100644 index 00000000..7935308c --- /dev/null +++ b/gba/src/hc_font_decoded.c @@ -0,0 +1,310 @@ +/* generated from /tmp/hcfont_dec.bin: do not edit */ +const unsigned char hc_font_decoded[] = { +0x20,0x83,0x07,0x00,0x02,0x00,0x01,0x00,0x01,0x00,0xd2,0x00,0xdb,0x00,0xef,0x00, +0x04,0x01,0x34,0x01,0x6b,0x01,0xa9,0x01,0xe7,0x01,0xf6,0x01,0x18,0x02,0x3a,0x02, +0x6a,0x02,0x94,0x02,0xb2,0x02,0xc8,0x02,0xdc,0x02,0x1a,0x03,0x4a,0x03,0x7a,0x03, +0xaa,0x03,0xda,0x03,0x11,0x04,0x41,0x04,0x71,0x04,0xa1,0x04,0xd1,0x04,0x01,0x05, +0x13,0x05,0x31,0x05,0x5a,0x05,0x74,0x05,0x9d,0x05,0xcd,0x05,0xfd,0x05,0x2d,0x06, +0x5d,0x06,0x8d,0x06,0xbd,0x06,0xe6,0x06,0x0f,0x07,0x3f,0x07,0x6f,0x07,0x91,0x07, +0xc1,0x07,0xf8,0x07,0x21,0x08,0x5f,0x08,0x8f,0x08,0xbf,0x08,0xef,0x08,0x1f,0x09, +0x4f,0x09,0x7f,0x09,0xaf,0x09,0xdf,0x09,0x0f,0x0a,0x4d,0x0a,0x7d,0x0a,0xad,0x0a, +0xeb,0x0a,0x0d,0x0b,0x4b,0x0b,0x6d,0x0b,0x85,0x0b,0xbb,0x0b,0xcd,0x0b,0xfd,0x0b, +0x2d,0x0c,0x5d,0x0c,0x8d,0x0c,0xbd,0x0c,0xe6,0x0c,0x22,0x0d,0x52,0x0d,0x66,0x0d, +0x90,0x0d,0xc0,0x0d,0xd4,0x0d,0x12,0x0e,0x42,0x0e,0x72,0x0e,0xae,0x0e,0xea,0x0e, +0x1a,0x0f,0x4a,0x0f,0x73,0x0f,0xa3,0x0f,0xd3,0x0f,0x11,0x10,0x41,0x10,0x7d,0x10, +0xad,0x10,0xcf,0x10,0xe3,0x10,0x05,0x11,0x19,0x11,0x50,0x11,0x87,0x11,0x0d,0x12, +0xab,0x12,0x01,0x00,0x03,0x00,0x00,0x70,0x00,0x00,0x00,0x07,0x00,0x02,0x00,0x01, +0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x03, +0x00,0x05,0x00,0x01,0x70,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x06,0x00,0x07,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00, +0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x07,0x00,0x07,0x00,0x01,0x70,0x00,0x00,0x00,0x01,0x00,0x00, +0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x01,0x01,0x00,0x01,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x07,0x00,0x08,0x00,0x01, +0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x08,0x00,0x01,0x70,0x00, +0x00,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x00,0x01, +0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x03,0x00,0x03,0x00,0x01,0x70,0x00,0x01,0x01, +0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x04,0x00,0x01,0x70,0x00,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x04,0x00,0x01,0x70,0x01,0x01, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00, +0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x06,0x00,0x07,0x00,0x01,0x70, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x00, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01, +0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x06,0x00,0x06,0x00,0x01,0x70, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x08,0x00,0x03,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01, +0x01,0x00,0x04,0x00,0x04,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x07,0x00,0x02,0x00,0x01,0x70,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x07,0x00,0x08,0x00, +0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01, +0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01, +0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x07,0x00,0x01,0x70, +0x00,0x00,0x00,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x06,0x00,0x02,0x00,0x01,0x70,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x08,0x00,0x03,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x01,0x01,0x00, +0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x07,0x00,0x05,0x00,0x01,0x70,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x05,0x00,0x04,0x00,0x01,0x70, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x01,0x07,0x00,0x05,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x05, +0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x05,0x00,0x01,0x70,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x07, +0x00,0x04,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01, +0x01,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x07,0x00,0x07,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x05,0x00,0x01,0x70,0x01,0x01, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01, +0x01,0x07,0x00,0x08,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x07, +0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x07, +0x00,0x06,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01, +0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x07, +0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x07, +0x00,0x08,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00, +0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01, +0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x00,0x08, +0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x04,0x00,0x01, +0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00, +0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x07,0x00,0x08, +0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x04,0x00,0x01, +0x70,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x00,0x01,0x01,0x08,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0x00,0x04,0x00,0x01, +0x70,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x05, +0x00,0x01,0x70,0x00,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x09,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01, +0x01,0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x07,0x00,0x02,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x01,0x01,0x01,0x09,0x00,0x04,0x00,0x01,0x70,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00, +0x07,0x00,0x06,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x07,0x00,0x02,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x01,0x07,0x00,0x08,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01, +0x01,0x00,0x09,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x09,0x00, +0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x07,0x00,0x06,0x00,0x01,0x70, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00, +0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x05,0x00,0x01,0x70, +0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00, +0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01, +0x01,0x01,0x01,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01, +0x01,0x00,0x00,0x07,0x00,0x08,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01, +0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x07,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00, +0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x09,0x00,0x06,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01, +0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x07,0x00,0x06, +0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01, +0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x07,0x00,0x04, +0x00,0x01,0x70,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x01, +0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x07, +0x00,0x02,0x00,0x01,0x70,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01, +0x01,0x01,0x01,0x07,0x00,0x04,0x00,0x01,0x70,0x01,0x01,0x00,0x00,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x00,0x02,0x00,0x07,0x00,0x01,0x70,0x00,0x01,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x00,0x07,0x00,0x07,0x00,0x01,0x70,0x00, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01, +0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x00,0x01,0x01,0x00,0x00,0x01, +0x01,0x00,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x00,0x00, +0x07,0x00,0x07,0x00,0x01,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01, +0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x00,0x00,0x00,0x00, +0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x10,0x00,0x02,0x70,0x00,0x01,0x01, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00, +0x00,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01, +0x01,0x00,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00, +0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x13, +0x00,0x03,0x70,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x01, +0x00,0x00,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01, +0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x01,0x01, +0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x00,0x01,0x01,0x01,0x01, +0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x01, +0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x01,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x08,0x00,0x11,0x00,0x03, +0x70,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x00,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x00,0x00, +0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x00,0x01,0x00,0x01, +0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x00, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01, +0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x00, +0x01,0x00,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x01,0x00,0x00, +0x01,0x01,0x01,0x00,0x00,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01, +0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00}; diff --git a/gba/src/sdl_shim.c b/gba/src/sdl_shim.c new file mode 100644 index 00000000..70adfa54 --- /dev/null +++ b/gba/src/sdl_shim.c @@ -0,0 +1,624 @@ +/* SDL2 shim implementation for GBA. */ +#include +#include +#include +#include +#include "gba_port.h" + +static char sdl_error[256]; + +const char* SDL_GetError(void) { return sdl_error; } +void SDL_SetError(const char* fmt, ...) { (void)fmt; sdl_error[0] = 0; } +void SDL_free(void* p) { gba_free(p); } + +int SDL_Init(Uint32 flags) { (void)flags; sdl_error[0] = 0; return 0; } +int SDL_InitSubSystem(Uint32 flags) { return SDL_Init(flags); } +void SDL_Quit(void) { } +void SDL_SetHint(const char* a, const char* b) { (void)a; (void)b; } +void SDL_PumpEvents(void) { gba_input_poll(); } + +/* ---- pixel format helpers ---- */ +static void format_init(SDL_PixelFormat* f, SDL_Palette* p, int bpp) { + f->BitsPerPixel = (Uint8)bpp; + f->BytesPerPixel = (Uint8)((bpp + 7) / 8); + f->palette = (bpp == 8) ? p : NULL; + f->format = (bpp == 8) ? SDL_PIXELFORMAT_INDEX8 : + (bpp == 24) ? SDL_PIXELFORMAT_RGB24 : SDL_PIXELFORMAT_ARGB8888; + f->Rmask = 0x000000ff; f->Gmask = 0x0000ff00; + f->Bmask = 0x00ff0000; f->Amask = (bpp == 32) ? 0xff000000u : 0; +} + +Uint32 SDL_MapRGB(const SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b) { + if (fmt->BitsPerPixel == 8 && fmt->palette) { + int best = 0, bestd = 0x7fffffff; + const SDL_Color* c = fmt->palette->colors; + for (int i = 0; i < fmt->palette->ncolors; ++i) { + int dr = (int)c[i].r - r, dg = (int)c[i].g - g, db = (int)c[i].b - b; + int d = dr*dr + dg*dg + db*db; + if (d < bestd) { bestd = d; best = i; } + } + return (Uint32)best; + } + return ((Uint32)b << 16) | ((Uint32)g << 8) | r; +} +Uint32 SDL_MapRGBA(const SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + (void)a; return SDL_MapRGB(fmt, r, g, b); +} +const char* SDL_GetPixelFormatName(Uint32 fmt) { + switch (fmt) { + case SDL_PIXELFORMAT_INDEX8: return "SDL_PIXELFORMAT_INDEX8"; + case SDL_PIXELFORMAT_RGB24: return "SDL_PIXELFORMAT_RGB24"; + case SDL_PIXELFORMAT_ARGB8888: return "SDL_PIXELFORMAT_ARGB8888"; + } + return "?"; +} + +/* ---- surfaces ---- */ +SDL_Surface* SDL_CreateRGBSurface(Uint32 flags, int w, int h, int depth, + Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask) { + (void)flags; (void)Rmask; (void)Gmask; (void)Bmask; (void)Amask; + SDL_Surface* s = (SDL_Surface*)gba_calloc(1, sizeof(SDL_Surface)); + if (!s) return NULL; + s->w = w; s->h = h; + int bpp = depth > 0 ? depth : 8; + s->pitch = w * ((bpp + 7) / 8); + s->pixels = gba_calloc(1, (size_t)s->pitch * h); + if (!s->pixels) { gba_free(s); return NULL; } + s->_palette.ncolors = SDL_PALETTE_SIZE; + format_init(&s->_format, &s->_palette, bpp); + s->format = &s->_format; + s->clip_rect.x = 0; s->clip_rect.y = 0; + s->clip_rect.w = w; s->clip_rect.h = h; + s->refcount = 1; s->alpha_mod = 255; + s->blend_mode = SDL_BLENDMODE_NONE; + s->has_colorkey = 0; + return s; +} +/* GBA: allocate ONLY the SDL_Surface struct (no pixel buffer); the caller + points ->pixels straight at the sprite data fixed in cartridge ROM. + decode_image uses this for every sprite, so we never allocate (and free) a + throwaway width*height buffer per sprite. That temp buffer used to leave a + free-list hole between each persistent surface struct, growing the free list + without bound and making first-fit malloc go quadratic — which stalled level + loading (hundreds of optgraf/environment sprites) on a frozen screen. */ +SDL_Surface* gba_new_rom_surface(int w, int h, int bpp) { + SDL_Surface* s = (SDL_Surface*)gba_calloc(1, sizeof(SDL_Surface)); + if (!s) return NULL; + if (bpp <= 0) bpp = 8; + s->w = w; s->h = h; + s->pitch = w * ((bpp + 7) / 8); + s->pixels = NULL; /* caller assigns the ROM pointer */ + s->_palette.ncolors = SDL_PALETTE_SIZE; + format_init(&s->_format, &s->_palette, bpp); + s->format = &s->_format; + s->clip_rect.x = 0; s->clip_rect.y = 0; + s->clip_rect.w = w; s->clip_rect.h = h; + s->refcount = 1; s->alpha_mod = 255; + s->blend_mode = SDL_BLENDMODE_NONE; + s->has_colorkey = 0; + return s; +} + +void SDL_FreeSurface(SDL_Surface* s) { + if (!s) return; + if (--s->refcount > 0) return; + /* GBA ROM-backed sprite: pixels points into cartridge ROM, must not + be freed. __wrap_free already early-outs on the ROM range, but skip + explicitly so the heap allocator doesn't even see it. */ + if (!s->gba_rom_compressed) gba_free(s->pixels); + gba_free(s); +} +int SDL_LockSurface(SDL_Surface* s) { (void)s; return 0; } +void SDL_UnlockSurface(SDL_Surface* s){ (void)s; } + +int SDL_SetColorKey(SDL_Surface* s, int flag, Uint32 key) { + if (!s) return -1; + s->has_colorkey = flag ? 1 : 0; + s->colorkey = key; + return 0; +} +int SDL_SetSurfaceAlphaMod(SDL_Surface* s, Uint8 a) { if(s) s->alpha_mod = a; return 0; } +int SDL_SetSurfaceBlendMode(SDL_Surface* s, SDL_BlendMode m) { if(s) s->blend_mode = m; return 0; } +int SDL_SetClipRect(SDL_Surface* s, const SDL_Rect* r) { + if (!s) return -1; + if (!r) { s->clip_rect.x=0; s->clip_rect.y=0; s->clip_rect.w=s->w; s->clip_rect.h=s->h; } + else s->clip_rect = *r; + return 0; +} +int SDL_SetPaletteColors(SDL_Palette* p, const SDL_Color* colors, int first, int n) { + if (!p) return -1; + if (first < 0) first = 0; + if (first >= SDL_PALETTE_SIZE) return 0; + if (first + n > SDL_PALETTE_SIZE) n = SDL_PALETTE_SIZE - first; + memcpy(&p->colors[first], colors, sizeof(SDL_Color) * n); + if (p->ncolors < first + n) p->ncolors = first + n; + return 0; +} + +/* clip to dst clip_rect */ +static void clip_rects(const SDL_Surface* src, const SDL_Rect* sr, + const SDL_Surface* dst, SDL_Rect* dr, + SDL_Rect* out_src, SDL_Rect* out_dst) { + SDL_Rect s = sr ? *sr : (SDL_Rect){0, 0, src->w, src->h}; + SDL_Rect d = dr ? *dr : (SDL_Rect){0, 0, s.w, s.h}; + if (d.w == 0) d.w = s.w; + if (d.h == 0) d.h = s.h; + /* match dst clip */ + int cx0 = dst->clip_rect.x, cy0 = dst->clip_rect.y; + int cx1 = cx0 + dst->clip_rect.w, cy1 = cy0 + dst->clip_rect.h; + int dx0 = d.x, dy0 = d.y, dx1 = d.x + d.w, dy1 = d.y + d.h; + if (dx0 < cx0) { s.x += cx0 - dx0; s.w -= cx0 - dx0; dx0 = cx0; } + if (dy0 < cy0) { s.y += cy0 - dy0; s.h -= cy0 - dy0; dy0 = cy0; } + if (dx1 > cx1) { s.w -= dx1 - cx1; dx1 = cx1; } + if (dy1 > cy1) { s.h -= dy1 - cy1; dy1 = cy1; } + if (s.w < 0) s.w = 0; + if (s.h < 0) s.h = 0; + d.x = dx0; d.y = dy0; d.w = dx1 - dx0; d.h = dy1 - dy0; + *out_src = s; *out_dst = d; +} + +/* Sprite decode scratch — only used as a fallback for sprites that did + NOT get pre-decoded by tools/preproc_dat. In normal operation this + covers just the built-in hc_font_data glyphs (max ~16x16 / 256 bytes + 8bpp), so a tiny scratch is enough. Any DAT-loaded sprite hits the + pre-decoded fast path in SDL_BlitSurface and never touches this. */ +#define GBA_SPRITE_DECOMPR_BYTES 1024 +#define GBA_SPRITE_UNPACK_BYTES 2048 +static uint8_t gba_decompr_buf [GBA_SPRITE_DECOMPR_BYTES] __attribute__((aligned(4))); +static uint8_t gba_unpacked_buf[GBA_SPRITE_UNPACK_BYTES] __attribute__((aligned(4))); + +extern void decompr_img(uint8_t* dest, const void* source, int decomp_size, + int cmeth, int stride); +extern uint8_t* conv_to_8bpp(uint8_t* in_data, int width, int height, + int stride, int depth); +extern int calc_stride(void* image_data); + +uint8_t* gba_decode_compressed_surface(SDL_Surface* src) { + /* Walks the compressed image_data_type at src->pixels (ROM), decodes + into gba_unpacked_buf, applies the sprite's palette-row offset, and + returns the unpacked buffer. Caller treats it as a contiguous w*h + 8bpp image with rows of length src->w. */ + int w = src->w, h = src->h; + int depth = src->gba_depth; + int cmeth = src->gba_cmeth; + int stride = (depth * w + 7) / 8; + int dest_size = stride * h; + if (dest_size > GBA_SPRITE_DECOMPR_BYTES || (w * h) > GBA_SPRITE_UNPACK_BYTES) return NULL; + memset(gba_decompr_buf, 0, dest_size); + /* image_data_type in ROM: {height u16, width u16, flags u16, data[]}. + decompr_img takes a pointer to it and reads .data via offset. */ + decompr_img(gba_decompr_buf, src->pixels, dest_size, cmeth, stride); + /* Inline conv_to_8bpp directly into gba_unpacked_buf so we don't malloc. */ + int pixels_per_byte = 8 / depth; + int mask = (1 << depth) - 1; + for (int y = 0; y < h; ++y) { + const uint8_t* in_pos = gba_decompr_buf + y * stride; + uint8_t* out_pos = gba_unpacked_buf + y * w; + int x_pixel = 0; + for (int x_byte = 0; x_byte < stride; ++x_byte) { + uint8_t v = *in_pos++; + int shift = 8; + for (int p = 0; p < pixels_per_byte && x_pixel < w; ++p, ++x_pixel) { + shift -= depth; + *out_pos++ = (v >> shift) & mask; + } + } + } + /* Do NOT bake the palette-row offset here — SDL_BlitSurface applies + it per-pixel during the blit so the same scratch contents work for + any destination, and so the fast path for pre-decoded sprites + (which also have raw 0..15 values in their ROM-backed pixels) goes + through the same blit logic. */ + return gba_unpacked_buf; +} + +int SDL_BlitSurface(SDL_Surface* src, const SDL_Rect* srcrect, + SDL_Surface* dst, SDL_Rect* dstrect) { + if (!src || !dst) return -1; + /* GBA: decode ROM-resident sprites into the scratch buffer first; from + here on src->pixels behaves like a normal 8bpp surface. */ + uint8_t* saved_pixels = NULL; + int saved_pitch = 0; + if (src->gba_rom_compressed) { + uint8_t* unpacked = gba_decode_compressed_surface(src); + if (unpacked == NULL) return -1; + saved_pixels = (uint8_t*)src->pixels; + saved_pitch = src->pitch; + src->pixels = unpacked; + src->pitch = src->w; + } + + SDL_Rect s, d; + clip_rects(src, srcrect, dst, dstrect, &s, &d); + if (s.w <= 0 || s.h <= 0) { + if (saved_pixels) { src->pixels = saved_pixels; src->pitch = saved_pitch; } + return 0; + } + int sbpp = src->format->BytesPerPixel; + int dbpp = dst->format->BytesPerPixel; + int n = (s.w < d.w ? s.w : d.w); + int rows = (s.h < d.h ? s.h : d.h); + + if (sbpp == 1 && dbpp == 1) { + /* Source pixels are raw 0..15 values (4bpp expanded, or 8bpp + that happens to use only the bottom nibble — PoP sprites are + 4bpp). We map each non-zero pixel to its slot in the global + PoP palette[] by adding the per-surface row offset baked at + decode_image time. Pixel 0 is the universal colorkey and + stays transparent. */ + int has_key = src->has_colorkey; + Uint8 key = (Uint8)src->colorkey; + Uint8 off = src->gba_palette_offset; + + for (int y = 0; y < rows; ++y) { + const Uint8* sp = (const Uint8*)src->pixels + (size_t)(s.y + y) * src->pitch + s.x; + Uint8* dp = (Uint8*)dst->pixels + (size_t)(d.y + y) * dst->pitch + d.x; + if (off != 0) { + if (has_key) { + for (int x = 0; x < n; ++x) { + Uint8 v = sp[x]; + if (v == key) continue; + dp[x] = (Uint8)(off + (v & 0x0F)); + } + } else { + for (int x = 0; x < n; ++x) { + dp[x] = (Uint8)(off + (sp[x] & 0x0F)); + } + } + } else if (has_key) { + for (int x = 0; x < n; ++x) { + Uint8 v = sp[x]; + if (v != key) dp[x] = v; + } + } else { + memcpy(dp, sp, (size_t)n); + } + } + } else { + /* generic 1->N or N->N copy; we mostly stay in 8bpp on GBA */ + for (int y = 0; y < rows; ++y) { + const Uint8* sp = (const Uint8*)src->pixels + (size_t)(s.y + y) * src->pitch + s.x * sbpp; + Uint8* dp = (Uint8*)dst->pixels + (size_t)(d.y + y) * dst->pitch + d.x * dbpp; + int bytes = n * (sbpp < dbpp ? sbpp : dbpp); + memcpy(dp, sp, (size_t)bytes); + } + } + if (dstrect) *dstrect = d; + if (saved_pixels) { src->pixels = saved_pixels; src->pitch = saved_pitch; } + return 0; +} + +int SDL_BlitScaled(SDL_Surface* src, const SDL_Rect* sr, + SDL_Surface* dst, SDL_Rect* dr) { + /* On GBA, "scaling" just becomes a clipped blit — final output is 240x160. */ + return SDL_BlitSurface(src, sr, dst, dr); +} + +int SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color) { + if (!dst) return -1; + SDL_Rect r = rect ? *rect : (SDL_Rect){0, 0, dst->w, dst->h}; + int cx0 = dst->clip_rect.x, cy0 = dst->clip_rect.y; + int cx1 = cx0 + dst->clip_rect.w, cy1 = cy0 + dst->clip_rect.h; + int x0 = r.x < cx0 ? cx0 : r.x; + int y0 = r.y < cy0 ? cy0 : r.y; + int x1 = r.x + r.w > cx1 ? cx1 : r.x + r.w; + int y1 = r.y + r.h > cy1 ? cy1 : r.y + r.h; + if (x1 <= x0 || y1 <= y0) return 0; + int bpp = dst->format->BytesPerPixel; + for (int y = y0; y < y1; ++y) { + Uint8* dp = (Uint8*)dst->pixels + (size_t)y * dst->pitch + x0 * bpp; + if (bpp == 1) memset(dp, (int)color, (size_t)(x1 - x0)); + else { + for (int x = 0; x < x1 - x0; ++x) { + memcpy(dp + x * bpp, &color, (size_t)bpp); + } + } + } + return 0; +} + +SDL_Surface* SDL_ConvertSurfaceFormat(SDL_Surface* src, Uint32 fmt, Uint32 flags) { + (void)flags; + if (!src) return NULL; + /* If caller wants the same format, refcount-bump and return. Otherwise + we MUST actually allocate a new buffer at the target pixel size — the + upstream method_3_blit_mono path converts 8bpp font glyphs to + ARGB8888 and then writes 4 bytes per pixel into the returned buffer. + The previous stub that just incremented refcount caused a 4x heap + overrun on every text character drawn, scribbling over malloc'd + data and producing the "stuck on partial frame" behaviour. */ + if (fmt == src->format->format) { + src->refcount++; + return src; + } + int dst_bpp = (fmt == SDL_PIXELFORMAT_INDEX8) ? 8 : + (fmt == SDL_PIXELFORMAT_RGB24) ? 24 : 32; + SDL_Surface* dst = SDL_CreateRGBSurface(0, src->w, src->h, dst_bpp, + 0, 0, 0, 0); + if (!dst) return NULL; + if (src->has_colorkey) SDL_SetColorKey(dst, 1, src->colorkey); + + if (src->format->BytesPerPixel == 1 && dst_bpp == 32) { + /* 8bpp paletted -> ARGB8888 via the source palette. Memory layout + (little-endian, our shim): byte0=R, byte1=G, byte2=B, byte3=A. */ + const SDL_Color* pal = src->format->palette ? src->format->palette->colors : NULL; + for (int y = 0; y < src->h; ++y) { + const Uint8* sp = (const Uint8*)src->pixels + (size_t)y * src->pitch; + Uint8* dp = (Uint8*)dst->pixels + (size_t)y * dst->pitch; + for (int x = 0; x < src->w; ++x) { + Uint8 idx = sp[x]; + SDL_Color c = pal ? pal[idx] : (SDL_Color){idx, idx, idx, 255}; + dp[0] = c.r; dp[1] = c.g; dp[2] = c.b; dp[3] = c.a ? c.a : 255; + dp += 4; + } + } + } else { + /* generic same-bpp copy fallback */ + int min_bpp = src->format->BytesPerPixel < dst->format->BytesPerPixel ? + src->format->BytesPerPixel : dst->format->BytesPerPixel; + for (int y = 0; y < src->h; ++y) { + const Uint8* sp = (const Uint8*)src->pixels + (size_t)y * src->pitch; + Uint8* dp = (Uint8*)dst->pixels + (size_t)y * dst->pitch; + for (int x = 0; x < src->w; ++x) + memcpy(dp + x * dst->format->BytesPerPixel, + sp + x * src->format->BytesPerPixel, min_bpp); + } + } + return dst; +} +SDL_Surface* SDL_ConvertSurface(SDL_Surface* src, const SDL_PixelFormat* fmt, Uint32 flags) { + (void)fmt; (void)flags; + if (!src) return NULL; + /* Return a copy of src so that callers can free both independently. */ + SDL_Surface* dst = SDL_CreateRGBSurface(0, src->w, src->h, + src->format->BitsPerPixel, 0, 0, 0, 0); + if (!dst) return NULL; + if (src->format->palette) { + SDL_SetPaletteColors(dst->format->palette, src->format->palette->colors, + 0, src->format->palette->ncolors); + } + if (src->has_colorkey) SDL_SetColorKey(dst, 1, src->colorkey); + memcpy(dst->pixels, src->pixels, (size_t)src->pitch * src->h); + return dst; +} +int SDL_SetSurfacePalette(SDL_Surface* s, SDL_Palette* palette) { + if (!s || !palette) return -1; + SDL_SetPaletteColors(&s->_palette, palette->colors, 0, palette->ncolors); + return 0; +} + +/* ---- RWops, memory-backed ---- */ +static Sint64 mem_size(SDL_RWops* rw) { return rw->hidden.mem.stop - rw->hidden.mem.base; } +static Sint64 mem_seek(SDL_RWops* rw, Sint64 ofs, int whence) { + const Uint8* p = rw->hidden.mem.here; + if (whence == RW_SEEK_SET) p = rw->hidden.mem.base + ofs; + else if (whence == RW_SEEK_CUR) p = rw->hidden.mem.here + ofs; + else if (whence == RW_SEEK_END) p = rw->hidden.mem.stop + ofs; + if (p < rw->hidden.mem.base) p = rw->hidden.mem.base; + if (p > rw->hidden.mem.stop) p = rw->hidden.mem.stop; + rw->hidden.mem.here = p; + return p - rw->hidden.mem.base; +} +static size_t mem_read(SDL_RWops* rw, void* ptr, size_t size, size_t n) { + size_t avail = (size_t)(rw->hidden.mem.stop - rw->hidden.mem.here) / (size ? size : 1); + size_t got = n < avail ? n : avail; + memcpy(ptr, rw->hidden.mem.here, got * size); + rw->hidden.mem.here += got * size; + return got; +} +static size_t mem_write(SDL_RWops* rw, const void* ptr, size_t size, size_t n) { + (void)rw; (void)ptr; (void)size; (void)n; return 0; +} +/* Static RWops pool: SDLPoP only ever holds a handful of memory RWops open at + once (font/data loads), so a small fixed pool avoids any allocation. */ +#define RWOPS_POOL 8 +static SDL_RWops s_rwops[RWOPS_POOL]; +static Uint8 s_rwops_used[RWOPS_POOL]; + +static int mem_close(SDL_RWops* rw) { + for (int i = 0; i < RWOPS_POOL; ++i) { + if (rw == &s_rwops[i]) { s_rwops_used[i] = 0; break; } + } + return 0; +} +SDL_RWops* SDL_RWFromConstMem(const void* m, int sz) { + SDL_RWops* rw = NULL; + for (int i = 0; i < RWOPS_POOL; ++i) { + if (!s_rwops_used[i]) { s_rwops_used[i] = 1; rw = &s_rwops[i]; break; } + } + if (!rw) return NULL; + memset(rw, 0, sizeof(*rw)); + rw->size = mem_size; rw->seek = mem_seek; rw->read = mem_read; + rw->write = mem_write; rw->close = mem_close; + rw->hidden.mem.base = rw->hidden.mem.here = (const Uint8*)m; + rw->hidden.mem.stop = rw->hidden.mem.base + sz; + return rw; +} +SDL_RWops* SDL_RWFromMem(void* m, int sz) { return SDL_RWFromConstMem(m, sz); } +SDL_RWops* SDL_RWFromFile(const char* file, const char* mode) { (void)file; (void)mode; return NULL; } +int SDL_RWclose(SDL_RWops* rw) { return rw && rw->close ? rw->close(rw) : 0; } +size_t SDL_RWread(SDL_RWops* rw, void* p, size_t s, size_t n) { return rw->read(rw, p, s, n); } +size_t SDL_RWwrite(SDL_RWops* rw, const void* p, size_t s, size_t n) { return rw->write(rw, p, s, n); } +Sint64 SDL_RWseek(SDL_RWops* rw, Sint64 o, int w) { return rw->seek(rw, o, w); } +Sint64 SDL_RWtell(SDL_RWops* rw) { return rw->seek(rw, 0, RW_SEEK_CUR); } +Sint64 SDL_RWsize(SDL_RWops* rw) { return rw->size(rw); } + +/* ---- Timing ---- */ +Uint64 SDL_GetPerformanceCounter(void) { return (Uint64)gba_get_ticks_ms() * 1000ULL; } +Uint64 SDL_GetPerformanceFrequency(void) { return 1000000ULL; } +Uint32 SDL_GetTicks(void) { return gba_get_ticks_ms(); } +void SDL_Delay(Uint32 ms) { gba_delay_ms(ms); } + +/* ---- Cursor / text input — no-ops on GBA ---- */ +int SDL_ShowCursor(int t) { (void)t; return 0; } +void SDL_StartTextInput(void) { } +void SDL_StopTextInput(void) { } +int SDL_SetTextInputRect(const SDL_Rect* r){ (void)r; return 0; } + +/* ---- Window/renderer/texture stubs (we draw directly to GBA VRAM) ---- */ +static SDL_Window _win; +static SDL_Renderer _rend; +static SDL_Texture _tex[4]; +SDL_Window* SDL_CreateWindow(const char* t, int x, int y, int w, int h, Uint32 f) { + (void)t; (void)x; (void)y; (void)w; (void)h; (void)f; return &_win; +} +void SDL_DestroyWindow(SDL_Window* w) { (void)w; } +SDL_Renderer* SDL_CreateRenderer(SDL_Window* w, int i, Uint32 f) { (void)w; (void)i; (void)f; return &_rend; } +void SDL_DestroyRenderer(SDL_Renderer* r) { (void)r; } +SDL_Texture* SDL_CreateTexture(SDL_Renderer* r, Uint32 fmt, int acc, int w, int h) { + (void)r; (void)fmt; (void)acc; (void)w; (void)h; + static int idx = 0; + return &_tex[idx++ & 3]; +} +void SDL_DestroyTexture(SDL_Texture* t) { (void)t; } +int SDL_RenderClear(SDL_Renderer* r) { (void)r; return 0; } +int SDL_RenderCopy(SDL_Renderer* r, SDL_Texture* t, const SDL_Rect* a, const SDL_Rect* b) { + (void)r; (void)t; (void)a; (void)b; return 0; +} +void SDL_RenderPresent(SDL_Renderer* r) { (void)r; } +int SDL_SetRenderTarget(SDL_Renderer* r, SDL_Texture* t) { (void)r; (void)t; return 0; } +int SDL_UpdateTexture(SDL_Texture* t, const SDL_Rect* r, const void* p, int pitch) { + (void)t; (void)r; (void)p; (void)pitch; return 0; +} +int SDL_RenderSetLogicalSize(SDL_Renderer* r, int w, int h) { (void)r; (void)w; (void)h; return 0; } +int SDL_RenderSetIntegerScale(SDL_Renderer* r, SDL_bool e) { (void)r; (void)e; return 0; } +int SDL_RenderGetLogicalSize(SDL_Renderer* r, int* w, int* h){ (void)r; if(w)*w=240; if(h)*h=160; return 0; } +/* No scaling/letterboxing on GBA: 1:1 to the 240x160 framebuffer. */ +void SDL_RenderGetScale(SDL_Renderer* r, float* sx, float* sy){ (void)r; if(sx)*sx=1.0f; if(sy)*sy=1.0f; } +void SDL_RenderGetViewport(SDL_Renderer* r, SDL_Rect* rect){ (void)r; if(rect){ rect->x=0; rect->y=0; rect->w=240; rect->h=160; } } +int SDL_GetRendererOutputSize(SDL_Renderer* r, int* w, int* h){ (void)r; if(w)*w=240; if(h)*h=160; return 0; } +int SDL_GetWindowFlags(SDL_Window* w) { (void)w; return 0; } +void SDL_SetWindowFullscreen(SDL_Window* w, Uint32 f) { (void)w; (void)f; } +void SDL_SetWindowIcon(SDL_Window* w, SDL_Surface* s) { (void)w; (void)s; } +int SDL_GetRendererInfo(SDL_Renderer* r, SDL_RendererInfo* info) { + (void)r; + if (info) { + info->name = "gba"; + info->flags = SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE; + info->num_texture_formats = 1; + info->texture_formats[0] = SDL_PIXELFORMAT_INDEX8; + info->max_texture_width = 240; + info->max_texture_height = 160; + } + return 0; +} +void SDL_GL_GetDrawableSize(SDL_Window* w, int* a, int* b) { (void)w; if(a)*a=240; if(b)*b=160; } +void SDL_GetWindowSize (SDL_Window* w, int* a, int* b) { (void)w; if(a)*a=240; if(b)*b=160; } +int SDL_UpdateRect(SDL_Surface* s, int x, int y, int w, int h) { + (void)x; (void)y; (void)w; (void)h; + return gba_present_surface(s); +} +int SDL_Flip(SDL_Surface* s) { return gba_present_surface(s); } + +/* ---- Audio (only minimal API; the real mixer is in gba_audio.c) ---- */ +int SDL_OpenAudio(SDL_AudioSpec* desired, SDL_AudioSpec* obtained) { + if (obtained) *obtained = *desired; + gba_audio_init(desired); + return 0; +} +void SDL_CloseAudio(void) { gba_audio_shutdown(); } +void SDL_PauseAudio(int on) { gba_audio_set_paused(on); } +void SDL_LockAudio(void) { gba_audio_lock(); } +void SDL_UnlockAudio(void) { gba_audio_unlock(); } +int SDL_GetAudioStatus(void) { return gba_audio_status(); } +int SDL_BuildAudioCVT(SDL_AudioCVT* cvt, SDL_AudioFormat sf, Uint8 sc, int sr, + SDL_AudioFormat df, Uint8 dc, int dr) { + (void)sf; (void)sc; (void)sr; (void)df; (void)dc; (void)dr; + if (cvt) { cvt->needed = 0; cvt->len_mult = 1; cvt->len_ratio = 1.0; } + return 0; +} +int SDL_ConvertAudio(SDL_AudioCVT* cvt) { if (cvt) cvt->len_cvt = cvt->len; return 0; } + +/* ---- Joystick / haptic — full stubs ---- */ +int SDL_NumJoysticks(void) { return 0; } +SDL_GameController* SDL_GameControllerOpen(int i) { (void)i; return NULL; } +void SDL_GameControllerClose(SDL_GameController* c){ (void)c; } +SDL_bool SDL_IsGameController(int i) { (void)i; return SDL_FALSE; } +SDL_GameController* SDL_GameControllerFromInstanceID(int i) { (void)i; return NULL; } +SDL_Joystick* SDL_JoystickOpen(int i) { (void)i; return NULL; } +int SDL_GameControllerAddMappingsFromFile(const char* f) { (void)f; return 0; } +SDL_Haptic* SDL_HapticOpen(int i) { (void)i; return NULL; } +int SDL_HapticRumbleInit(SDL_Haptic* h) { (void)h; return 0; } +int SDL_HapticRumblePlay(SDL_Haptic* h, float a, Uint32 b) { (void)h; (void)a; (void)b; return 0; } + +/* ---- Timers (one shot for VBlank ticking; we wire SDLPoP's frame timer here) ---- */ +static SDL_TimerCallback s_tcb; +static Uint32 s_tinterval; +static void* s_tparam; +SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback cb, void* p) { + s_tcb = cb; s_tinterval = interval; s_tparam = p; + gba_timer_install(interval); + return 1; +} +int SDL_RemoveTimer(SDL_TimerID id) { (void)id; s_tcb = NULL; gba_timer_uninstall(); return 1; } + +/* Called by gba_timer_tick() (VBlank handler) at the configured cadence. */ +void gba_timer_fire_cb(void) { + if (s_tcb) { + Uint32 next = s_tcb(s_tinterval, s_tparam); + if (!next) { s_tcb = NULL; gba_timer_uninstall(); } + else if (next != s_tinterval) { s_tinterval = next; gba_timer_install(next); } + } +} + +/* ---- iconv / wchar — not used on GBA path ---- */ +char* SDL_iconv_string(const char* a, const char* b, const char* c, size_t d) { + (void)a; (void)b; (void)c; (void)d; return NULL; +} +size_t SDL_wcslen(const Uint16* w) { + const Uint16* p = w; while (*p) ++p; return (size_t)(p - w); +} +size_t SDL_strlen(const char* s) { return strlen(s); } + +/* ---- Event queue ---- */ +#define EVQ_CAP 32 +static SDL_Event s_evq[EVQ_CAP]; +static int s_evq_head, s_evq_tail; + +int SDL_PollEvent(SDL_Event* event) { + gba_input_poll(); + if (s_evq_head == s_evq_tail) return 0; + if (event) *event = s_evq[s_evq_head]; + s_evq_head = (s_evq_head + 1) % EVQ_CAP; + return 1; +} +int SDL_PushEvent(SDL_Event* event) { + int next = (s_evq_tail + 1) % EVQ_CAP; + if (next == s_evq_head) return 0; + s_evq[s_evq_tail] = *event; + s_evq_tail = next; + return 1; +} + +static Uint8 s_keystate[SDL_NUM_SCANCODES]; +const Uint8* SDL_GetKeyboardState(int* numkeys) { + if (numkeys) *numkeys = SDL_NUM_SCANCODES; + return s_keystate; +} +/* GBA has no mouse: report (0,0) with no buttons held so the menu falls back to + D-pad/button navigation. */ +Uint32 SDL_GetMouseState(int* x, int* y) { + if (x) *x = 0; + if (y) *y = 0; + return 0; +} +/* The controls menu prints key-binding names. We don't ship a scancode-name + table on GBA, so return an empty string (the numeric code is shown too). */ +const char* SDL_GetScancodeName(SDL_Scancode scancode) { + (void)scancode; + return ""; +} +void gba_set_key(int scancode, int down) { + if ((unsigned)scancode >= SDL_NUM_SCANCODES) return; + Uint8 was = s_keystate[scancode]; + s_keystate[scancode] = down ? 1 : 0; + if ((!was) != (!down)) { + SDL_Event e; memset(&e, 0, sizeof(e)); + e.type = down ? SDL_KEYDOWN : SDL_KEYUP; + e.key.timestamp = SDL_GetTicks(); + e.key.state = down ? 1 : 0; + e.key.keysym.scancode = scancode; + e.key.keysym.sym = scancode; + SDL_PushEvent(&e); + } +} diff --git a/gba/tools/bin2c b/gba/tools/bin2c new file mode 100755 index 00000000..f47e133e Binary files /dev/null and b/gba/tools/bin2c differ diff --git a/gba/tools/predecomp_font b/gba/tools/predecomp_font new file mode 100755 index 00000000..9f70b2be Binary files /dev/null and b/gba/tools/predecomp_font differ diff --git a/gba/tools/predecomp_font.c b/gba/tools/predecomp_font.c new file mode 100644 index 00000000..1062ca78 --- /dev/null +++ b/gba/tools/predecomp_font.c @@ -0,0 +1,217 @@ +/* Pre-decompress a PoP raw font (rawfont_type) at build time. + * + * The GBA runtime can blit pre-decoded sprites straight from ROM, but the + * built-in fonts (hc_font_data / hc_small_font_data) ship compressed, so + * decode_image() used to flag them gba_rom_compressed and the per-blit + * decoder ran on every glyph — and a malformed/edge glyph spun that decoder + * forever, freezing the game the moment any text was drawn (e.g. the first + * move). This tool decodes every glyph to depth=8 / cmeth=0 once, so at + * runtime decode_image() takes its no-decode fast path. + * + * Input/output are raw rawfont_type blobs: + * byte first_char; + * byte last_char; + * short height_above_baseline, height_below_baseline; + * short space_between_lines, space_between_chars; + * word offsets[n_chars]; // byte offset (from blob start) of each glyph + * ... glyph image records (image_data_type: {word h; word w; word flags; data[]}) + * + * Usage: predecomp_font in.bin out.bin + * Build: cc -O2 -o predecomp_font predecomp_font.c + */ +#include +#include +#include +#include + +typedef uint8_t byte; +typedef int8_t sbyte; +typedef uint16_t word; + +/* --- decoders: identical to tools/preproc_dat.c (size-tracking) --------- */ +static int rle_lr(byte* dst, const byte* src, int src_avail, int dst_len) { + int s = 0, d = 0; + while (d < dst_len) { + if (s >= src_avail) return -1; + sbyte count = (sbyte)src[s++]; + if (count >= 0) { + int n = count + 1; + while (n-- > 0 && d < dst_len) { if (s >= src_avail) return -1; dst[d++] = src[s++]; } + } else { + int n = -count; + if (s >= src_avail) return -1; + byte v = src[s++]; + while (n-- > 0 && d < dst_len) dst[d++] = v; + } + } + return s; +} +static int rle_ud(byte* dst, const byte* src, int src_avail, int dst_len, int width, int height) { + int s = 0, rem_height = height, rem_length = dst_len, dest_end = dst_len - 1, w_step = width - 1; + byte* dest_pos = dst; + if (rem_length <= 0) return s; + while (rem_length > 0) { + if (s >= src_avail) return -1; + sbyte count = (sbyte)src[s++]; + int n; byte v = 0; int rep = 0; + if (count >= 0) n = count + 1; else { n = -count; if (s >= src_avail) return -1; v = src[s++]; rep = 1; } + while (n > 0 && rem_length > 0) { + if (!rep) { if (s >= src_avail) return -1; } + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = rep ? v : src[s++]; + dest_pos++; dest_pos += w_step; + if (--rem_height == 0) { dest_pos -= dest_end; rem_height = height; } + rem_length--; n--; + } + } + return s; +} +static int lzg_lr(byte* dst, const byte* src, int src_avail, int dst_len) { + byte window[0x400]; memset(window, 0, sizeof(window)); + int wp = 0x400 - 0x42, rem = dst_len, s = 0, d = 0; word mask = 0; + while (rem > 0) { + mask >>= 1; + if ((mask & 0xFF00) == 0) { if (s >= src_avail) return -1; mask = (word)src[s++] | 0xFF00; } + if (mask & 1) { + if (s >= src_avail) return -1; + byte v = src[s++]; window[wp++] = v; if (d >= dst_len) return -1; dst[d++] = v; + if (wp >= 0x400) wp = 0; rem--; + } else { + if (s + 1 >= src_avail) return -1; + word ci = src[s++]; ci = (ci << 8) | src[s++]; + int cs = ci & 0x3FF, cl = (ci >> 10) + 3; + while (rem > 0 && cl > 0) { + byte v = window[cs]; window[wp++] = v; if (d >= dst_len) return -1; dst[d++] = v; + if (++cs >= 0x400) cs = 0; if (wp >= 0x400) wp = 0; rem--; cl--; + } + } + } + return s; +} +static int lzg_ud(byte* dst, const byte* src, int src_avail, int dst_len, int stride, int height) { + byte window[0x400]; memset(window, 0, sizeof(window)); + int wp = 0x400 - 0x42, rem_h = height, rem_len = dst_len, dest_end = dst_len - 1, s = 0; word mask = 0; + byte* dest_pos = dst; + while (rem_len > 0) { + mask >>= 1; + if ((mask & 0xFF00) == 0) { if (s >= src_avail) return -1; mask = (word)src[s++] | 0xFF00; } + if (mask & 1) { + if (s >= src_avail) return -1; + byte v = src[s++]; window[wp++] = v; + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = v; if (wp >= 0x400) wp = 0; + dest_pos += stride; if (--rem_h == 0) { dest_pos -= dest_end; rem_h = height; } rem_len--; + } else { + if (s + 1 >= src_avail) return -1; + word ci = src[s++]; ci = (ci << 8) | src[s++]; + int cs = ci & 0x3FF, cl = (ci >> 10) + 3; + while (rem_len > 0 && cl > 0) { + byte v = window[cs]; window[wp++] = v; + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = v; if (++cs >= 0x400) cs = 0; if (wp >= 0x400) wp = 0; + dest_pos += stride; if (--rem_h == 0) { dest_pos -= dest_end; rem_h = height; } rem_len--; cl--; + } + } + } + return s; +} +static void unpack_to_8bpp(byte* dst, const byte* src, int width, int height, int stride, int depth) { + int ppb = 8 / depth, mask = (1 << depth) - 1; + for (int y = 0; y < height; ++y) { + const byte* in = src + (size_t)y * stride; + byte* out = dst + (size_t)y * width; + int x = 0; + for (int xb = 0; xb < stride; ++xb) { + byte v = *in++; int shift = 8; + for (int p = 0; p < ppb && x < width; ++p, ++x) { shift -= depth; *out++ = (byte)((v >> shift) & mask); } + } + } +} + +static byte* slurp(const char* path, int* n) { + FILE* f = fopen(path, "rb"); if (!f) return NULL; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + byte* b = malloc((size_t)sz); if (!b) { fclose(f); return NULL; } + if (fread(b, 1, (size_t)sz, f) != (size_t)sz) { free(b); fclose(f); return NULL; } + fclose(f); *n = (int)sz; return b; +} +static word rd16(const byte* p) { return (word)(p[0] | (p[1] << 8)); } +static void wr16(byte* p, word v) { p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; } + +int main(int argc, char** argv) { + if (argc != 3) { fprintf(stderr, "usage: %s in.bin out.bin\n", argv[0]); return 2; } + int in_size = 0; + byte* in = slurp(argv[1], &in_size); + if (!in) { perror(argv[1]); return 1; } + if (in_size < 10) { fprintf(stderr, "%s: too small for a rawfont\n", argv[1]); return 1; } + + int first = in[0], last = in[1]; + int n = last - first + 1; + if (n < 1 || n > 256) { fprintf(stderr, "bad char range %d..%d\n", first, last); return 1; } + int header = 2 + 8; /* first,last + 4 shorts */ + if (header + n * 2 > in_size) { fprintf(stderr, "offsets truncated\n"); return 1; } + + /* Output: same 10-byte header, fresh offset table, then decoded glyphs. */ + int cap = header + n * 2 + n * (6 + 400 * 256) ; /* generous upper bound */ + byte* out = calloc(1, (size_t)cap); + if (!out) { perror("calloc"); return 1; } + memcpy(out, in, header); /* copy first,last + spacing shorts */ + int cur = header + n * 2; /* glyph records start after offsets */ + int decoded = 0, passthrough = 0; + + for (int i = 0; i < n; ++i) { + word off = rd16(in + header + i * 2); + wr16(out + header + i * 2, (word)cur); /* record this glyph's new offset */ + if (off + 6 > (word)in_size) { /* missing glyph: emit 1x1 blank */ + wr16(out + cur + 0, 1); wr16(out + cur + 2, 1); + wr16(out + cur + 4, (word)(7 << 12)); out[cur + 6] = 0; cur += 7; continue; + } + const byte* g = in + off; + int gh = rd16(g + 0), gw = rd16(g + 2), flags = rd16(g + 4); + int depth = ((flags >> 12) & 7) + 1; + int cmeth = (flags >> 8) & 0x0F; + int h = gh ? gh : 1; /* decode_image treats h==0 as 1 */ + const byte* cdata = g + 6; + int cdata_avail = in_size - (int)(off + 6); + if (gw < 1 || gw > 400 || h > 256 || depth < 1 || depth > 8 || cmeth > 4 || cdata_avail < 0) { + /* Unclassifiable: keep the original record verbatim. */ + int rest = in_size - (int)off; + if (rest < 6) rest = 6; + memcpy(out + cur, g, (size_t)rest); + cur += rest; passthrough++; continue; + } + int stride = (depth * gw + 7) / 8; + int dsize = stride * h; + byte* dec = calloc(1, (size_t)(dsize > 0 ? dsize : 1)); + int used = -1; + switch (cmeth) { + case 0: if (cdata_avail >= dsize) { memcpy(dec, cdata, (size_t)dsize); used = dsize; } break; + case 1: used = rle_lr(dec, cdata, cdata_avail, dsize); break; + case 2: used = rle_ud(dec, cdata, cdata_avail, dsize, stride, h); break; + case 3: used = lzg_lr(dec, cdata, cdata_avail, dsize); break; + case 4: used = lzg_ud(dec, cdata, cdata_avail, dsize, stride, h); break; + } + /* Emit a depth=8 cmeth=0 record (blank-filled if decode failed). */ + wr16(out + cur + 0, (word)h); + wr16(out + cur + 2, (word)gw); + wr16(out + cur + 4, (word)((flags & 0x00FF) | (7 << 12))); + byte* px = out + cur + 6; + if (used >= 0) { + if (depth == 8) memcpy(px, dec, (size_t)gw * h); + else unpack_to_8bpp(px, dec, gw, h, stride, depth); + decoded++; + } else { + memset(px, 0, (size_t)gw * h); /* couldn't decode: transparent glyph */ + } + cur += 6 + gw * h; + free(dec); + } + + FILE* f = fopen(argv[2], "wb"); + if (!f) { perror(argv[2]); return 1; } + fwrite(out, 1, (size_t)cur, f); + fclose(f); + fprintf(stderr, "%s: %d glyphs decoded, %d passed through, %d bytes\n", + argv[2], decoded, passthrough, cur); + return 0; +} diff --git a/gba/tools/prep_data.sh b/gba/tools/prep_data.sh new file mode 100755 index 00000000..0d640074 --- /dev/null +++ b/gba/tools/prep_data.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Prepare DAT files for the GBA build. +# +# Each upstream sprite DAT is funneled through `preproc_dat` (built on +# demand from preproc_dat.c) before being written out as gba/data/.bin. +# preproc_dat detects image_data_type resources inside the DAT and rewrites +# them with the compression and bitpack already unrolled, so the runtime can +# blit them straight from ROM without per-blit decode scratch. +# +# bin2s (devkitPro tool) names symbols after the *file*: a file named +# kid.bin becomes `kid_bin` / `kid_bin_end` / `kid_bin_size`. +# +# Usage: +# ./tools/prep_data.sh +# Run from inside the gba/ directory. + +set -euo pipefail + +cd "$(dirname "$0")/.." + +DST="data" +TOOL_SRC="tools/preproc_dat.c" +TOOL_BIN="tools/preproc_dat" +mkdir -p "$DST" + +# Build the host tool if missing or out of date. +if [ ! -x "$TOOL_BIN" ] || [ "$TOOL_SRC" -nt "$TOOL_BIN" ]; then + echo "Building host tool: $TOOL_BIN" + cc -O2 -Wall -Wno-pointer-sign -o "$TOOL_BIN" "$TOOL_SRC" +fi + +# Resolve a source DAT (case-insensitive .DAT / .dat). Checks gba/data/ +# first, then ../data/. Echoes the path or returns failure. +find_src() { + local stem="$1" + for d in "$DST" "../data"; do + for ext in "DAT" "dat"; do + if [ -f "$d/$stem.$ext" ]; then + echo "$d/$stem.$ext" + return 0 + fi + done + if [ -f "$d/$stem" ] && [ "$d" != "$DST" ]; then + echo "$d/$stem" + return 0 + fi + done + return 1 +} + +# Process one DAT through preproc_dat into the target .bin. +process_dat() { + local stem="$1" # upstream stem, e.g. KID + local outname="$2" # lower-case symbol-safe stem, e.g. kid + local src + if ! src="$(find_src "$stem")"; then + echo " warning: no DAT for $stem" >&2 + return 0 + fi + "$TOOL_BIN" "$src" "$DST/$outname.bin" +} + +# Sound DATs / LEVELS contain no image_data_type records and don't need +# preprocessing; copy them across as-is. +copy_dat() { + local stem="$1" + local outname="$2" + local src + if ! src="$(find_src "$stem")"; then + echo " warning: no DAT for $stem" >&2 + return 0 + fi + cp "$src" "$DST/$outname.bin" + echo " $src -> $DST/$outname.bin (passthrough)" +} + +echo "Preprocessing sprite DATs (decompress + 8bpp expand) ..." +process_dat "KID" "kid" +process_dat "PRINCE" "prince" +process_dat "TITLE" "title" +process_dat "GUARD" "guard" +process_dat "GUARD1" "guard1" +process_dat "GUARD2" "guard2" +process_dat "VPALACE" "vpalace" +process_dat "VDUNGEON" "vdungeon" +process_dat "VIZIER" "vizier" +process_dat "FAT" "fat" +process_dat "SKEL" "skel" +process_dat "SHADOW" "shadow" +process_dat "PV" "pv" + +echo "Pre-converting digi sound samples (unsigned -> signed) ..." +process_dat "DIGISND1" "digisnd1" +process_dat "DIGISND2" "digisnd2" +process_dat "DIGISND3" "digisnd3" + +echo "Copying remaining DATs unchanged ..." +copy_dat "LEVELS" "levels" +for s in IBM_SND1 IBM_SND2 MIDISND1 MIDISND2; do + out="$(echo "$s" | tr '[:upper:]' '[:lower:]')" + copy_dat "$s" "$out" +done + +# Font file (rarely present in DAT form). When absent the runtime falls +# back to hc_font_data baked into the ROM. +for cand in "$DST/font" "$DST/font.dat" "../data/font" "../data/font.dat"; do + if [ -f "$cand" ]; then + "$TOOL_BIN" "$cand" "$DST/font.bin" 2>/dev/null || cp "$cand" "$DST/font.bin" + break + fi +done + +# Pre-decompress the built-in font so the GBA runtime never runs the per-blit +# decoder on glyphs. predecomp_font expands every glyph to depth=8/cmeth=0; +# bin2c bakes the result into src/hc_font_decoded.c (used by load_font()). +echo "Pre-decompressing built-in font (predecomp_font + bin2c) ..." +FONT_TOOL="tools/predecomp_font" +if [ ! -x "$FONT_TOOL" ] || [ "tools/predecomp_font.c" -nt "$FONT_TOOL" ]; then + cc -O2 -w -o "$FONT_TOOL" tools/predecomp_font.c +fi +if [ ! -x tools/bin2c ] || [ "bin2c.c" -nt tools/bin2c ]; then + cc -O2 -w -o tools/bin2c bin2c.c +fi +# Extract the hc_font_data[] byte array from the upstream source. +perl -0777 -ne 'if(/hc_font_data\[\]\s*=\s*\{(.*?)\}\s*;/s){my $b=$1; print chr(hex($1)) while $b=~/0x([0-9A-Fa-f]+)/g}' \ + ../src/seg009.c > /tmp/hcfont_raw.bin +if [ -s /tmp/hcfont_raw.bin ]; then + "$FONT_TOOL" /tmp/hcfont_raw.bin /tmp/hcfont_dec.bin + tools/bin2c /tmp/hcfont_dec.bin src/hc_font_decoded.c hc_font_decoded + echo " -> src/hc_font_decoded.c" +else + echo " warning: could not extract hc_font_data from ../src/seg009.c" >&2 +fi + +echo "Done." +du -sh "$DST" 2>/dev/null || true diff --git a/gba/tools/preproc_dat b/gba/tools/preproc_dat new file mode 100755 index 00000000..adc501b8 Binary files /dev/null and b/gba/tools/preproc_dat differ diff --git a/gba/tools/preproc_dat.c b/gba/tools/preproc_dat.c new file mode 100644 index 00000000..f6eeee7e --- /dev/null +++ b/gba/tools/preproc_dat.c @@ -0,0 +1,494 @@ +/* Pre-decompress sprite resources in a PoP DAT file. + * + * For each resource we can confidently identify as an image_data_type + * (compression method 0..4, depth 1..8, sane height/width, decoder + * produces exactly the expected output size from at most the payload + * bytes available), rewrite it as a depth=8 cmeth=0 record holding the + * already-decoded 8bpp pixels. All other resources pass through + * unchanged. The output DAT shares the input's table layout but with + * new offsets/sizes; the runtime can fast-path the pre-decoded ones + * and skip the per-blit decompression scratch buffers. + * + * Usage: preproc_dat input.dat output.dat + * + * Builds with any host C compiler: + * cc -O2 -o preproc_dat preproc_dat.c + */ +#include +#include +#include +#include +#include + +typedef uint8_t byte; +typedef int8_t sbyte; +typedef uint16_t word; +typedef uint32_t dword; + +/* DAT header / table entry layout (pragma packed in upstream types.h). */ +#pragma pack(push, 1) +typedef struct { + uint32_t table_offset; + uint16_t table_size; +} dat_header_t; +typedef struct { + uint16_t id; + uint32_t offset; + uint16_t size; +} dat_res_t; +typedef struct { + uint16_t height; + uint16_t width; + uint16_t flags; + /* uint8_t data[]; */ +} image_hdr_t; +#pragma pack(pop) + +/* --- Decompressors, ported verbatim from src/seg009.c (size-tracking + * versions: each returns -1 on overrun so the caller can reject inputs + * that aren't really compressed images). ------------------------------ */ + +static int decompress_rle_lr(byte* dst, const byte* src, int src_avail, int dst_len) { + int s = 0, d = 0; + while (d < dst_len) { + if (s >= src_avail) return -1; + sbyte count = (sbyte)src[s++]; + if (count >= 0) { + int n = count + 1; + while (n-- > 0 && d < dst_len) { + if (s >= src_avail) return -1; + dst[d++] = src[s++]; + } + } else { + int n = -count; + if (s >= src_avail) return -1; + byte v = src[s++]; + while (n-- > 0 && d < dst_len) dst[d++] = v; + } + } + return s; +} + +static int decompress_rle_ud(byte* dst, const byte* src, int src_avail, + int dst_len, int width, int height) { + /* Same as in seg009.c: the destination is filled column-by-column; + * rem_length counts physical writes. */ + int s = 0; + int rem_height = height; + int rem_length = dst_len; + byte* dest_pos = dst; + /* per upstream: dest_end_offset = dst_len - 1, used to rewind to next col */ + int dest_end = dst_len - 1; + int w_step = width - 1; + if (rem_length <= 0) return s; + while (rem_length > 0) { + if (s >= src_avail) return -1; + sbyte count = (sbyte)src[s++]; + if (count >= 0) { + int n = count + 1; + while (n > 0 && rem_length > 0) { + if (s >= src_avail) return -1; + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = src[s++]; + dest_pos++; + dest_pos += w_step; + rem_height--; + if (rem_height == 0) { + dest_pos -= dest_end; + rem_height = height; + } + rem_length--; + n--; + } + } else { + int n = -count; + if (s >= src_avail) return -1; + byte v = src[s++]; + while (n > 0 && rem_length > 0) { + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = v; + dest_pos++; + dest_pos += w_step; + rem_height--; + if (rem_height == 0) { + dest_pos -= dest_end; + rem_height = height; + } + rem_length--; + n--; + } + } + } + return s; +} + +static int decompress_lzg_lr(byte* dst, const byte* src, int src_avail, int dst_len) { + byte window[0x400]; + memset(window, 0, sizeof(window)); + int wp = 0x400 - 0x42; + int rem = dst_len; + int s = 0, d = 0; + word mask = 0; + while (rem > 0) { + mask >>= 1; + if ((mask & 0xFF00) == 0) { + if (s >= src_avail) return -1; + mask = (word)(src[s++]) | 0xFF00; + } + if (mask & 1) { + if (s >= src_avail) return -1; + byte v = src[s++]; + window[wp++] = v; + if (d >= dst_len) return -1; + dst[d++] = v; + if (wp >= 0x400) wp = 0; + rem--; + } else { + if (s + 1 >= src_avail) return -1; + word ci = src[s++]; + ci = (ci << 8) | src[s++]; + int cs = ci & 0x3FF; + int cl = (ci >> 10) + 3; + while (rem > 0 && cl > 0) { + byte v = window[cs]; + window[wp++] = v; + if (d >= dst_len) return -1; + dst[d++] = v; + cs++; + if (cs >= 0x400) cs = 0; + if (wp >= 0x400) wp = 0; + rem--; + cl--; + } + } + } + return s; +} + +static int decompress_lzg_ud(byte* dst, const byte* src, int src_avail, + int dst_len, int stride, int height) { + byte window[0x400]; + memset(window, 0, sizeof(window)); + int wp = 0x400 - 0x42; + int rem_h = height; + int rem_len = dst_len; + int dest_end = dst_len - 1; + int s = 0; + byte* dest_pos = dst; + word mask = 0; + while (rem_len > 0) { + mask >>= 1; + if ((mask & 0xFF00) == 0) { + if (s >= src_avail) return -1; + mask = (word)(src[s++]) | 0xFF00; + } + if (mask & 1) { + if (s >= src_avail) return -1; + byte v = src[s++]; + window[wp++] = v; + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = v; + if (wp >= 0x400) wp = 0; + dest_pos += stride; + rem_h--; + if (rem_h == 0) { + dest_pos -= dest_end; + rem_h = height; + } + rem_len--; + } else { + if (s + 1 >= src_avail) return -1; + word ci = src[s++]; + ci = (ci << 8) | src[s++]; + int cs = ci & 0x3FF; + int cl = (ci >> 10) + 3; + while (rem_len > 0 && cl > 0) { + byte v = window[cs]; + window[wp++] = v; + if (dest_pos < dst || dest_pos >= dst + dst_len) return -1; + *dest_pos = v; + cs++; + if (cs >= 0x400) cs = 0; + if (wp >= 0x400) wp = 0; + dest_pos += stride; + rem_h--; + if (rem_h == 0) { + dest_pos -= dest_end; + rem_h = height; + } + rem_len--; + cl--; + } + } + } + return s; +} + +/* Expand bitpacked depth-bpp source into 8bpp output (one byte per pixel). */ +static void unpack_to_8bpp(byte* dst, const byte* src, int width, int height, + int stride, int depth) { + int ppb = 8 / depth; + int mask = (1 << depth) - 1; + for (int y = 0; y < height; ++y) { + const byte* in = src + (size_t)y * stride; + byte* out = dst + (size_t)y * width; + int x = 0; + for (int xb = 0; xb < stride; ++xb) { + byte v = *in++; + int shift = 8; + for (int p = 0; p < ppb && x < width; ++p, ++x) { + shift -= depth; + *out++ = (byte)((v >> shift) & mask); + } + } + } +} + +/* Try to recognize a sound_buffer_type wrapping a digi_new_type (the PoP + * 1.3 wave format used in DIGISND*.DAT) and convert its samples in + * place from unsigned 8-bit to signed 8-bit. Returns a newly-allocated + * payload + size on success, NULL otherwise. + * + * Layout (pack(1)): + * byte type; // == 1 (sound_digi) + * word sample_rate; // 4000..48000 in practice + * byte sample_size; // == 8 + * word sample_count; + * word unknown; + * word unknown2; + * byte samples[sample_count]; + * Total payload = 10 + sample_count. + * + * A sentinel is left in unknown2's high bit so the runtime can tell + * pre-converted samples from raw ones. */ +static byte* try_convert_digi_sound(const byte* entry_buf, int entry_size, + int* out_size) { + if (entry_size < 1 + 10 + 1) return NULL; + const byte* payload = entry_buf + 1; + int payload_size = entry_size - 1; + if (payload_size < 11) return NULL; + if (payload[0] != 1) return NULL; /* not sound_digi */ + uint16_t sample_rate = (uint16_t)(payload[1] | (payload[2] << 8)); + uint8_t sample_size = payload[3]; + uint16_t sample_count = (uint16_t)(payload[4] | (payload[5] << 8)); + if (sample_rate < 4000 || sample_rate > 48000) return NULL; + if (sample_size != 8) return NULL; + if (sample_count == 0) return NULL; + if (payload_size != 10 + (int)sample_count) return NULL; + byte* out = (byte*)malloc((size_t)payload_size); + if (!out) return NULL; + memcpy(out, payload, 10); + /* Sentinel: set high bit of unknown2 to mark "samples already int8". */ + out[9] = (byte)(payload[9] | 0x80); + for (int i = 0; i < (int)sample_count; ++i) { + out[10 + i] = (byte)((int)payload[10 + i] - 128); + } + *out_size = payload_size; + return out; +} + +/* --- DAT walking ----------------------------------------------------- */ + +/* Try to interpret entry as an image; on success allocate and return a + * new payload (raw 8bpp), write its size to *out_size, and the original + * checksum byte to *out_chk. Returns NULL if not classifiable as image + * (caller should pass entry through unchanged). */ +static byte* try_decode_image(const byte* entry_buf, int entry_size, + int* out_size) { + if (entry_size < 1 + (int)sizeof(image_hdr_t)) return NULL; + /* entry_buf[0] = checksum byte; payload starts at +1 */ + const byte* payload = entry_buf + 1; + int payload_size = entry_size - 1; + if (payload_size < (int)sizeof(image_hdr_t)) return NULL; + image_hdr_t hdr; + memcpy(&hdr, payload, sizeof(hdr)); + int height = hdr.height; + int width = hdr.width; + int flags = hdr.flags; + int depth = ((flags >> 12) & 7) + 1; + int cmeth = (flags >> 8) & 0x0F; + if (height < 1 || height > 256) return NULL; + if (width < 1 || width > 400) return NULL; + if (depth < 1 || depth > 8) return NULL; + if (cmeth > 4) return NULL; + int stride = (depth * width + 7) / 8; + int decoded_size = stride * height; + if (decoded_size <= 0 || decoded_size > 1024 * 1024) return NULL; + const byte* cdata = payload + sizeof(image_hdr_t); + int cdata_size = payload_size - (int)sizeof(image_hdr_t); + if (cdata_size < 0) return NULL; + byte* decoded = (byte*)malloc((size_t)decoded_size); + if (!decoded) return NULL; + memset(decoded, 0, (size_t)decoded_size); + int used = -1; + switch (cmeth) { + case 0: + /* RAW: payload must match expected size exactly. */ + if (cdata_size != decoded_size) { free(decoded); return NULL; } + memcpy(decoded, cdata, (size_t)decoded_size); + used = decoded_size; + break; + case 1: + used = decompress_rle_lr(decoded, cdata, cdata_size, decoded_size); + break; + case 2: + used = decompress_rle_ud(decoded, cdata, cdata_size, decoded_size, stride, height); + break; + case 3: + used = decompress_lzg_lr(decoded, cdata, cdata_size, decoded_size); + break; + case 4: + used = decompress_lzg_ud(decoded, cdata, cdata_size, decoded_size, stride, height); + break; + } + if (used < 0) { free(decoded); return NULL; } + /* Decompression succeeded and consumed at most payload_size bytes. + * Now expand to 8bpp. */ + byte* out = (byte*)malloc(sizeof(image_hdr_t) + (size_t)width * height); + if (!out) { free(decoded); return NULL; } + image_hdr_t oh; + oh.height = (uint16_t)height; + oh.width = (uint16_t)width; + /* Mark as depth=8 (bits 12..14 = 7), cmeth=0 (bits 8..11 = 0). */ + oh.flags = (uint16_t)((flags & 0x00FF) | (7 << 12)); + memcpy(out, &oh, sizeof(oh)); + if (depth == 8) { + memcpy(out + sizeof(oh), decoded, (size_t)width * height); + } else { + unpack_to_8bpp(out + sizeof(oh), decoded, width, height, stride, depth); + } + free(decoded); + *out_size = (int)sizeof(image_hdr_t) + width * height; + return out; +} + +/* Read whole file. */ +static byte* slurp(const char* path, int* out_size) { + FILE* f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + byte* buf = (byte*)malloc((size_t)n); + if (!buf) { fclose(f); return NULL; } + if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; } + fclose(f); + *out_size = (int)n; + return buf; +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s input.dat output.dat\n", argv[0]); + return 2; + } + int in_size = 0; + byte* in_buf = slurp(argv[1], &in_size); + if (!in_buf) { perror(argv[1]); return 1; } + if (in_size < (int)sizeof(dat_header_t)) { + fprintf(stderr, "%s: too small to be a DAT, copying unchanged\n", argv[1]); + FILE* f = fopen(argv[2], "wb"); + if (!f) { perror(argv[2]); return 1; } + fwrite(in_buf, 1, (size_t)in_size, f); + fclose(f); + return 0; + } + dat_header_t hdr; + memcpy(&hdr, in_buf, sizeof(hdr)); + if ((int)hdr.table_offset > in_size || + (int)(hdr.table_offset + 2) > in_size) { + fprintf(stderr, "%s: malformed (table outside file), copying unchanged\n", argv[1]); + FILE* f = fopen(argv[2], "wb"); + fwrite(in_buf, 1, (size_t)in_size, f); fclose(f); + return 0; + } + uint16_t res_count; + memcpy(&res_count, in_buf + hdr.table_offset, 2); + int table_bytes = 2 + res_count * (int)sizeof(dat_res_t); + if ((int)(hdr.table_offset + table_bytes) > in_size) { + fprintf(stderr, "%s: malformed (table truncated), copying unchanged\n", argv[1]); + FILE* f = fopen(argv[2], "wb"); + fwrite(in_buf, 1, (size_t)in_size, f); fclose(f); + return 0; + } + dat_res_t* entries = (dat_res_t*)malloc((size_t)res_count * sizeof(dat_res_t)); + if (!entries) { perror("malloc"); return 1; } + memcpy(entries, in_buf + hdr.table_offset + 2, + (size_t)res_count * sizeof(dat_res_t)); + + /* Reserve 6 bytes for the (rewritten) header; resources start there. */ + int cur = 6; + int cap = in_size * 2 + 64; + byte* out = (byte*)malloc((size_t)cap); + if (!out) { perror("malloc"); return 1; } + memset(out, 0, 6); + + dat_res_t* new_entries = (dat_res_t*)malloc((size_t)res_count * sizeof(dat_res_t)); + int decoded_count = 0; + + for (int i = 0; i < res_count; ++i) { + dat_res_t e = entries[i]; + new_entries[i].id = e.id; + /* offset[i] points at the checksum byte. */ + if ((int)(e.offset + 1 + e.size) > in_size) { + /* malformed entry: copy as-is */ + int sz = 1 + e.size; + while (cur + sz > cap) { cap *= 2; out = (byte*)realloc(out, (size_t)cap); } + memcpy(out + cur, in_buf + e.offset, (size_t)sz); + new_entries[i].offset = (uint32_t)cur; + new_entries[i].size = e.size; + cur += sz; + continue; + } + const byte* eb = in_buf + e.offset; + int new_size = 0; + byte* repl = try_decode_image(eb, 1 + e.size, &new_size); + if (!repl) repl = try_convert_digi_sound(eb, 1 + e.size, &new_size); + if (repl) { + /* Write checksum + new payload. */ + int sz = 1 + new_size; + while (cur + sz > cap) { cap *= 2; out = (byte*)realloc(out, (size_t)cap); } + out[cur] = eb[0]; + memcpy(out + cur + 1, repl, (size_t)new_size); + new_entries[i].offset = (uint32_t)cur; + new_entries[i].size = (uint16_t)new_size; + cur += sz; + free(repl); + decoded_count++; + } else { + int sz = 1 + e.size; + while (cur + sz > cap) { cap *= 2; out = (byte*)realloc(out, (size_t)cap); } + memcpy(out + cur, eb, (size_t)sz); + new_entries[i].offset = (uint32_t)cur; + new_entries[i].size = e.size; + cur += sz; + } + } + + /* Write the new table, then patch the header. */ + int new_table_offset = cur; + int need = cur + 2 + res_count * (int)sizeof(dat_res_t); + while (need > cap) { cap *= 2; out = (byte*)realloc(out, (size_t)cap); } + uint16_t rc = res_count; + memcpy(out + cur, &rc, 2); cur += 2; + memcpy(out + cur, new_entries, (size_t)res_count * sizeof(dat_res_t)); + cur += res_count * (int)sizeof(dat_res_t); + + dat_header_t newh; + newh.table_offset = (uint32_t)new_table_offset; + newh.table_size = (uint16_t)(2 + res_count * (int)sizeof(dat_res_t)); + memcpy(out, &newh, sizeof(newh)); + + FILE* f = fopen(argv[2], "wb"); + if (!f) { perror(argv[2]); return 1; } + if (fwrite(out, 1, (size_t)cur, f) != (size_t)cur) { perror("fwrite"); return 1; } + fclose(f); + fprintf(stderr, "%s -> %s: %d/%d resources pre-decoded, %d -> %d bytes\n", + argv[1], argv[2], decoded_count, res_count, in_size, cur); + + free(out); + free(new_entries); + free(entries); + free(in_buf); + return 0; +} diff --git a/gba/tools/render_music b/gba/tools/render_music new file mode 100755 index 00000000..15fa11a6 Binary files /dev/null and b/gba/tools/render_music differ diff --git a/gba/tools/render_music.c b/gba/tools/render_music.c new file mode 100644 index 00000000..9c7db517 --- /dev/null +++ b/gba/tools/render_music.c @@ -0,0 +1,255 @@ +/* render_music — host-side MIDI -> 8-bit PCM renderer for the GBA port. + * + * The GBA build can't run the OPL3 FM synth live, and PoP's music exists + * only as MIDI (in MIDISND1.DAT / MIDISND2.DAT). This tool reuses the exact + * desktop synthesis code (src/midi.c + src/opl3.c) to render each music track + * to signed 8-bit, mono, 18157 Hz PCM — the format the GBA DMA mixer consumes + * (see gba/src/gba_audio.c) — and packs them into a single blob, gba/data/ + * musicpcm.bin, which the Makefile embeds in ROM via bin2s. + * + * Layout of musicpcm.bin (all integers little-endian; GBA is LE): + * u32 magic = 'M''P''C''M' (0x4D43504D) + * u32 count + * count x { u32 sound_id; u32 offset; u32 length } // offset from blob base + * ... concatenated PCM payloads ... + * + * Usage: render_music + * e.g. render_music data data/musicpcm.bin (run from gba/) + * + * Builds with any host C compiler + SDL2 *headers* (no SDL2 lib needed): + * cc -O2 $(sdl2-config --cflags) -I../src -o render_music \ + * render_music.c ../../src/midi.c ../../src/opl3.c -lm + */ +#define SDL_MAIN_HANDLED 1 +#include +#include +#include +#include + +#include "common.h" /* types.h (sound_buffer_type, dat_*), proto.h, data.h */ + +#define TARGET_HZ 18157 /* must match MIX_HZ in gba/src/gba_audio.c */ +#define MAX_SOUND_ID 58 +#define MAX_SECONDS 90 /* safety cap per track */ +#define TARGET_PEAK 120 /* normalize each track to this 8-bit peak */ +#define MUSICPCM_MAGIC 0x4D43504Du /* 'M','P','C','M' */ + +/* ---- globals that src/midi.c expects (declared extern in data.h / midi.c) -- */ +short midi_playing = 0; +static SDL_AudioSpec g_spec; +SDL_AudioSpec* digi_audiospec = &g_spec; +int digi_unavailable = 0; +byte is_sound_on = 1; +byte enable_music = 1; +word current_sound = 0; + +/* ---- SDL stubs (we link the headers only, not the SDL2 library) ----------- */ +void SDL_LockAudio(void) {} +void SDL_UnlockAudio(void) {} +void SDL_PauseAudio(int p) { (void)p; } +#undef SDL_memset +void* SDL_memset(void* dst, int c, size_t len) { return memset(dst, c, len); } +void quit(int code) { exit(code); } +void init_digi(void) { /* spec is configured in main() */ } + +/* --- Minimal DAT chain, mirroring src/seg009.c open_dat/load_from_opendats - */ +typedef struct host_dat { + struct host_dat* next; + FILE* fp; + byte* table; /* res_count (u16) + entries[] (id u16, off u32, size u16) */ + int table_size; +} host_dat; + +static host_dat* g_chain = NULL; +static const char* g_datadir = "."; + +dat_type* open_dat(const char* filename, int optional) { + (void)optional; + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", g_datadir, filename); + FILE* fp = fopen(path, "rb"); + host_dat* d = (host_dat*)calloc(1, sizeof(host_dat)); + d->next = g_chain; + g_chain = d; + if (fp) { + dat_header_type hdr; + if (fread(&hdr, 6, 1, fp) == 1) { + d->table_size = (int)SDL_SwapLE16(hdr.table_size); + d->table = (byte*)malloc(d->table_size); + if (d->table && + fseek(fp, (long)SDL_SwapLE32(hdr.table_offset), SEEK_SET) == 0 && + fread(d->table, d->table_size, 1, fp) == 1) { + d->fp = fp; + } else { + free(d->table); d->table = NULL; + fclose(fp); + } + } else { + fclose(fp); + } + } + /* We return an opaque non-NULL token; midi.c only passes it to close_dat. */ + return (dat_type*)d; +} + +void close_dat(dat_type* pointer) { + host_dat** prev = &g_chain; + for (host_dat* c = g_chain; c; prev = &c->next, c = c->next) { + if ((dat_type*)c == pointer) { + *prev = c->next; + if (c->fp) fclose(c->fp); + free(c->table); + free(c); + return; + } + } +} + +void* load_from_opendats_alloc(int resource, const char* extension, + data_location* out_result, int* out_size) { + (void)extension; + if (out_result) *out_result = data_none; + if (out_size) *out_size = 0; + for (host_dat* d = g_chain; d; d = d->next) { + if (!d->fp || !d->table) continue; + word res_count = SDL_SwapLE16(*(word*)d->table); + const byte* e = d->table + 2; + for (int i = 0; i < res_count; ++i, e += 8) { + word id = SDL_SwapLE16(*(word*)e); + if (id != resource) continue; + dword off = SDL_SwapLE32(*(dword*)(e + 2)); + word size = SDL_SwapLE16(*(word*)(e + 6)); + byte chk; + if (fseek(d->fp, (long)off, SEEK_SET) || fread(&chk, 1, 1, d->fp) != 1) + return NULL; + void* area = malloc(size); + if (!area) return NULL; + if (size && fread(area, size, 1, d->fp) != 1) { free(area); return NULL; } + if (out_result) *out_result = data_DAT; + if (out_size) *out_size = size; + return area; + } + } + return NULL; +} + +/* --- PCM accumulation ------------------------------------------------------ */ +typedef struct { byte* data; size_t len, cap; } buf_t; +static void buf_put(buf_t* b, byte v) { + if (b->len == b->cap) { b->cap = b->cap ? b->cap * 2 : 65536; b->data = realloc(b->data, b->cap); } + b->data[b->len++] = v; +} + +/* Render the currently-loaded MIDI (midi_playing must be 1) to mono int8 PCM. + Returns malloc'd signed-8-bit samples in *out_pcm / *out_len (caller frees). */ +static void render_current_track(int8_t** out_pcm, int* out_len) { + enum { CHUNK = 4096 }; + short stereo[CHUNK * 2]; + short* mono16 = NULL; size_t mono_len = 0, mono_cap = 0; + long max_frames = (long)TARGET_HZ * MAX_SECONDS; + long produced = 0; + int peak = 1; + + while (midi_playing && produced < max_frames) { + memset(stereo, 0, sizeof(stereo)); + midi_callback(NULL, (Uint8*)stereo, CHUNK * 4); + for (int i = 0; i < CHUNK; ++i) { + int m = (stereo[i * 2] + stereo[i * 2 + 1]) / 2; + if (mono_len == mono_cap) { + mono_cap = mono_cap ? mono_cap * 2 : (CHUNK * 8); + mono16 = realloc(mono16, mono_cap * sizeof(short)); + } + mono16[mono_len++] = (short)m; + int a = m < 0 ? -m : m; + if (a > peak) peak = a; + } + produced += CHUNK; + } + /* Normalize peak -> TARGET_PEAK, convert to signed 8-bit. */ + int8_t* pcm = malloc(mono_len ? mono_len : 1); + for (size_t i = 0; i < mono_len; ++i) { + int v = (mono16[i] * TARGET_PEAK) / peak; + if (v > 127) v = 127; if (v < -128) v = -128; + pcm[i] = (int8_t)v; + } + /* Trim trailing silence. */ + size_t n = mono_len; + while (n > 0 && pcm[n - 1] == 0) --n; + free(mono16); + *out_pcm = pcm; + *out_len = (int)n; +} + +static void put_u32(buf_t* b, dword v) { + buf_put(b, v & 0xFF); buf_put(b, (v >> 8) & 0xFF); + buf_put(b, (v >> 16) & 0xFF); buf_put(b, (v >> 24) & 0xFF); +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + g_datadir = argv[1]; + + g_spec.freq = TARGET_HZ; + g_spec.format = AUDIO_S8; + g_spec.channels = 1; + g_spec.samples = 304; + + /* Keep the MIDI DATs open for the whole run; init_midi() handles PRINCE.DAT + (instruments) itself. load_from_opendats_alloc searches the chain. */ + open_dat("MIDISND1.DAT", 0); + open_dat("MIDISND2.DAT", 0); + + /* Collected results. */ + int ids[MAX_SOUND_ID]; + int8_t* pcms[MAX_SOUND_ID]; + int lens[MAX_SOUND_ID]; + int count = 0; + + for (int index = 0; index < MAX_SOUND_ID; ++index) { + int size = 0; + sound_buffer_type* buffer = + (sound_buffer_type*)load_from_opendats_alloc(index + 10000, "bin", NULL, &size); + if (!buffer) continue; + if ((buffer->type & 7) != sound_midi) { free(buffer); continue; } + + current_sound = (word)index; + play_midi_sound(buffer); /* parses MIDI, sets midi_playing = 1 */ + if (!midi_playing) { free(buffer); continue; } + + int8_t* pcm; int len; + render_current_track(&pcm, &len); + free(buffer); + if (len <= 0) { free(pcm); continue; } + + ids[count] = index; pcms[count] = pcm; lens[count] = len; + fprintf(stderr, " track %2d -> %d samples (%.2fs)\n", + index, len, (double)len / TARGET_HZ); + ++count; + } + + /* Pack the blob. */ + buf_t out = {0}; + put_u32(&out, MUSICPCM_MAGIC); + put_u32(&out, (dword)count); + dword data_base = 8 + (dword)count * 12; + dword cursor = data_base; + for (int i = 0; i < count; ++i) { + put_u32(&out, (dword)ids[i]); + put_u32(&out, cursor); + put_u32(&out, (dword)lens[i]); + cursor += (dword)lens[i]; + } + for (int i = 0; i < count; ++i) + for (int j = 0; j < lens[i]; ++j) + buf_put(&out, (byte)pcms[i][j]); + + FILE* f = fopen(argv[2], "wb"); + if (!f) { perror(argv[2]); return 1; } + fwrite(out.data, 1, out.len, f); + fclose(f); + fprintf(stderr, "%s: %d music tracks, %zu bytes\n", argv[2], count, out.len); + return 0; +} diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..aea2d736 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee8da733..55793c37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,10 @@ -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 3.5) project(SDLPoP) +# SDL3 + SDL3_image is the default backend. Configure with -DUSE_SDL3=OFF to +# build against SDL2 + SDL2_image instead. +option(USE_SDL3 "Build against SDL3 instead of SDL2" ON) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -std=c99") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration") @@ -79,6 +83,8 @@ set(SOURCE_FILES screenshot.c menu.c stb_vorbis.c + sdl2_to_sdl3.c + sdl2_to_sdl3.h icon.rc ) @@ -115,6 +121,20 @@ else() ${CMAKE_SOURCE_DIR}/../SDLPoP.ini ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/prince.app/Contents/MacOS/SDLPoP.ini) endif() +if(USE_SDL3) + target_compile_definitions(prince PRIVATE USE_SDL3) + if(WIN32) + target_link_libraries(prince mingw32 SDL3 SDL3_image) + elseif(APPLE) + target_link_libraries(prince SDL3 SDL3_image m) + else() # Linux, *BSD, etc. + include(FindPkgConfig) + pkg_search_module(SDL3 REQUIRED sdl3) + pkg_search_module(SDL3IMAGE REQUIRED sdl3-image) + target_include_directories(prince PRIVATE ${SDL3_INCLUDE_DIRS} ${SDL3IMAGE_INCLUDE_DIRS}) + target_link_libraries(prince PRIVATE ${SDL3_LIBRARIES} ${SDL3IMAGE_LIBRARIES} m) + endif() +else() if(WIN32) target_link_libraries(prince mingw32 SDL2main SDL2 SDL2_image) elseif(APPLE) @@ -138,3 +158,4 @@ elseif(PSP) # Linux, *BSD, etc. else() target_link_libraries(prince SDL2 SDL2_image m) endif() +endif() diff --git a/src/Makefile b/src/Makefile index 03301d35..4f7cb467 100644 --- a/src/Makefile +++ b/src/Makefile @@ -3,8 +3,8 @@ CC = gcc RM = rm -f -HFILES = common.h config.h data.h proto.h types.h -OBJ = main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o +HFILES = common.h config.h data.h proto.h types.h sdl2_to_sdl3.h +OBJ = main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o sdl2_to_sdl3.o BIN = ../prince OS := $(shell uname) @@ -12,8 +12,17 @@ OS := $(shell uname) CPPFLAGS += -Wall -D_GNU_SOURCE=1 CFLAGS += -std=c99 -O2 -LIBS := $(shell pkg-config --libs sdl2 SDL2_image) -CFLAGS += $(shell pkg-config --cflags sdl2 SDL2_image) +# SDL3 + SDL3_image is the default backend. Build against SDL2 instead with: +# make SDL2=1 +ifdef SDL2 +SDL_PKGS := sdl2 SDL2_image +else +SDL_PKGS := sdl3 sdl3-image +CPPFLAGS += -DUSE_SDL3 +endif + +LIBS := $(shell pkg-config --libs $(SDL_PKGS)) +CFLAGS += $(shell pkg-config --cflags $(SDL_PKGS)) ifeq ($(OS),Darwin) CPPFLAGS += -D_THREAD_SAFE -DOSX diff --git a/src/Makefile.amigaos4 b/src/Makefile.amigaos4 index 892efcaa..ef5629f7 100755 --- a/src/Makefile.amigaos4 +++ b/src/Makefile.amigaos4 @@ -6,10 +6,10 @@ CC = ppc-amigaos-gcc STRIP = ppc-amigaos-strip RM = rm -f -HFILES = common.h config.h data.h proto.h types.h +HFILES = common.h config.h data.h proto.h types.h sdl2_to_sdl3.h OBJ = main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o \ seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o \ -midi.o opl3.o stb_vorbis.o +midi.o opl3.o stb_vorbis.o sdl2_to_sdl3.o OS := $(shell uname) @@ -19,9 +19,14 @@ else AMIGADATE = $(shell date +"%-d.%-m.%Y") endif +# SDL3 is the default backend. Build against SDL2 with: make -f Makefile.amigaos4 SDL2=1 +ifdef SDL2 LIBS = -lSDL2_image -ltiff -lwebp -lpng -ljpeg -lz -lm -lvorbisfile -lvorbis -lSDL2 -lpthread -#LIBS = -lSDL2_image -ltiff -lwebp -lpng -ljpeg -lz -lm -lvorbisfile -lvorbis -lSDL2 -lpthread -athread=native -use-dynld INCS = -gstabs -ISDK:Local/newlib/include/SDL2 -D__AMIGADATE__=\"$(AMIGADATE)\" -D__USE_INLINE__ +else +LIBS = -lSDL3_image -ltiff -lwebp -lpng -ljpeg -lz -lm -lvorbisfile -lvorbis -lSDL3 -lpthread +INCS = -gstabs -ISDK:Local/newlib/include/SDL3 -D__AMIGADATE__=\"$(AMIGADATE)\" -D__USE_INLINE__ -DUSE_SDL3 +endif CFLAGS += $(INCS) -Wall -std=gnu99 diff --git a/src/Makefile.macos b/src/Makefile.macos new file mode 100644 index 00000000..f42e4f24 --- /dev/null +++ b/src/Makefile.macos @@ -0,0 +1,50 @@ +# Project: SDLPoP + +CC = gcc +RM = rm -f + +HFILES = common.h config.h data.h proto.h types.h sdl2_to_sdl3.h +OBJ = main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o sdl2_to_sdl3.o +BIN = ../prince + +OS := $(shell uname) + +CPPFLAGS += -Wall -D_GNU_SOURCE=1 +CFLAGS += -std=c99 -O2 -arch arm64 -arch x86_64 +LDFLAGS += -arch arm64 -arch x86_64 + +# SDL3 + SDL3_image is the default backend. Build against SDL2 instead with: +# make SDL2=1 +ifdef SDL2 +SDL_PKGS := sdl2 SDL2_image +else +SDL_PKGS := sdl3 sdl3-image +CPPFLAGS += -DUSE_SDL3 +endif + +LIBS := $(shell pkg-config --libs $(SDL_PKGS)) +CFLAGS += $(shell pkg-config --cflags $(SDL_PKGS)) + +ifeq ($(OS),Darwin) +CPPFLAGS += -D_THREAD_SAFE -DOSX +endif + +all: $(BIN) + +clean: + $(RM) $(OBJ) $(BIN) + +install: + ./install.sh + +uninstall: + ./uninstall.sh + +$(BIN): $(OBJ) + $(CC) $(LDFLAGS) $(OBJ) -o $@ $(LIBS) -lm + +%.o: %.c $(HFILES) + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -c $< + +.PHONY: all clean + diff --git a/src/NMakefile b/src/NMakefile index 5415101b..ba01543d 100644 --- a/src/NMakefile +++ b/src/NMakefile @@ -1,8 +1,15 @@ -OBJ = main.obj data.obj seg000.obj seg001.obj seg002.obj seg003.obj seg004.obj seg005.obj seg006.obj seg007.obj seg008.obj seg009.obj seqtbl.obj replay.obj options.obj lighting.obj screenshot.obj menu.obj midi.obj opl3.obj stb_vorbis.c +OBJ = main.obj data.obj seg000.obj seg001.obj seg002.obj seg003.obj seg004.obj seg005.obj seg006.obj seg007.obj seg008.obj seg009.obj seqtbl.obj replay.obj options.obj lighting.obj screenshot.obj menu.obj midi.obj opl3.obj stb_vorbis.c sdl2_to_sdl3.obj +# SDL3 is the default backend. Build against SDL2 with: nmake /f NMakefile SDL2=yes +!IF "$(SDL2)" == "yes" sdl = "..\SDL2-2.0.4" LIBS = $(sdl)\lib\x86\SDL2main.lib $(sdl)\lib\x86\SDL2.lib $(sdl)\lib\x86\SDL2_image.lib +!ELSE +sdl = "..\SDL3" +LIBS = $(sdl)\lib\x86\SDL3.lib $(sdl)\lib\x86\SDL3_image.lib +cflags = $(cflags) /DUSE_SDL3 +!ENDIF cc = cl /c link = link /subsystem:windows diff --git a/src/SDL_PoP_iconset/.DS_Store b/src/SDL_PoP_iconset/.DS_Store new file mode 100644 index 00000000..1e86151f Binary files /dev/null and b/src/SDL_PoP_iconset/.DS_Store differ diff --git a/src/SDL_PoP_iconset/SDL_PoP.info b/src/SDL_PoP_iconset/SDL_PoP.info new file mode 100644 index 00000000..60f30de1 Binary files /dev/null and b/src/SDL_PoP_iconset/SDL_PoP.info differ diff --git a/src/SDL_PoP_iconset/SDL_PoP/icon1.info b/src/SDL_PoP_iconset/SDL_PoP/icon1.info new file mode 100644 index 00000000..7de1499c Binary files /dev/null and b/src/SDL_PoP_iconset/SDL_PoP/icon1.info differ diff --git a/src/SDL_PoP_iconset/SDL_PoP/icon2.info b/src/SDL_PoP_iconset/SDL_PoP/icon2.info new file mode 100644 index 00000000..27b420fc Binary files /dev/null and b/src/SDL_PoP_iconset/SDL_PoP/icon2.info differ diff --git a/src/SDL_PoP_iconset/SDL_PoP/icon3.info b/src/SDL_PoP_iconset/SDL_PoP/icon3.info new file mode 100644 index 00000000..d89a847f Binary files /dev/null and b/src/SDL_PoP_iconset/SDL_PoP/icon3.info differ diff --git a/src/build.bat b/src/build.bat index ca6a598d..b44c2777 100644 --- a/src/build.bat +++ b/src/build.bat @@ -17,20 +17,37 @@ if ERRORLEVEL 1 ( exit /b ) -:: To override the directory for SDL2 library files, simply set the SDL environment variable in the command shell. -:: (You could do that from the command-line, or from a wrapper script that calls this one.) +:: SDL3 is the default backend. To build against SDL2 instead, set the +:: SDLPOP_SDL2 environment variable (e.g. "set SDLPOP_SDL2=1") before running. +:: Override the SDL directory by setting the SDL3 (or SDL2) environment variable. +if not [%SDLPOP_SDL2%]==[] goto sdl2_setup + +:sdl3_setup +if [%SDL3%]==[] ( + set SDL3=..\..\SDL3 +) +set SDL_DIR=%SDL3% +set SDL_LIBS=SDL3.lib SDL3_image.lib +set SDL_DEFINES=-DUSE_SDL3 +goto sdl_done + +:sdl2_setup if [%SDL2%]==[] ( set SDL2=..\..\SDL2-2.0.6 ) +set SDL_DIR=%SDL2% +set SDL_LIBS=SDL2main.lib SDL2.lib SDL2_image.lib +set SDL_DEFINES= -if not exist %SDL2% ( - echo Problem^: Could not find SDL2 directory. - echo Tried to look here^: %SDL2% +:sdl_done +if not exist %SDL_DIR% ( + echo Problem^: Could not find the SDL directory. + echo Tried to look here^: %SDL_DIR% echo, - echo To specify the SDL2 directory, set the SDL2 environment variable. + echo To specify it, set the SDL3 (or SDL2) environment variable. echo Example command: - echo set "SDL2=C:\work\libraries\SDL2-2.0.6" + echo set "SDL3=C:\work\libraries\SDL3" exit /b ) @@ -52,9 +69,9 @@ set BuildTypeCompilerFlags= /MT /O2 set PreprocessorDefinitions= :compile -set SourceFiles= main.c data.c seg000.c seg001.c seg002.c seg003.c seg004.c seg005.c seg006.c seg007.c seg008.c seg009.c seqtbl.c replay.c options.c lighting.c screenshot.c menu.c midi.c opl3.c stb_vorbis.c -set CommonCompilerFlags= /nologo /MP /fp:fast /GR- /wd4048 %PreprocessorDefinitions% /I"%SDL2%\include" -set CommonLinkerFlags= /subsystem:windows,5.01 /libpath:"%SDL2%\lib\%VSCMD_ARG_TGT_ARCH%" SDL2main.lib SDL2.lib SDL2_image.lib Shell32.lib icon.res /out:..\prince.exe +set SourceFiles= main.c data.c seg000.c seg001.c seg002.c seg003.c seg004.c seg005.c seg006.c seg007.c seg008.c seg009.c seqtbl.c replay.c options.c lighting.c screenshot.c menu.c midi.c opl3.c stb_vorbis.c sdl2_to_sdl3.c +set CommonCompilerFlags= /nologo /MP /fp:fast /GR- /wd4048 %PreprocessorDefinitions% %SDL_DEFINES% /I"%SDL_DIR%\include" +set CommonLinkerFlags= /subsystem:windows,5.01 /libpath:"%SDL_DIR%\lib\%VSCMD_ARG_TGT_ARCH%" %SDL_LIBS% Shell32.lib icon.res /out:..\prince.exe rc /nologo /fo icon.res icon.rc cl %BuildTypeCompilerFlags% %CommonCompilerFlags% %SourceFiles% /link %CommonLinkerFlags% diff --git a/src/common.h b/src/common.h index 34d0ef74..f9faa302 100644 --- a/src/common.h +++ b/src/common.h @@ -34,6 +34,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -61,6 +62,45 @@ extern "C" { #include "proto.h" #include "data.h" +#ifdef __GBA__ +/* On GBA there is no filesystem; data files live in ROM as linked blobs. + These macros route every libc-style file call to the GBA backend. */ +FILE* gba_fopen(const char* path, const char* mode); +int gba_fclose(FILE* fp); +size_t gba_fread(void* buf, size_t sz, size_t n, FILE* fp); +size_t gba_fwrite(const void* buf, size_t sz, size_t n, FILE* fp); +int gba_fseek(FILE* fp, long ofs, int whence); +long gba_ftell(FILE* fp); +int gba_feof(FILE* fp); +int gba_fgetc(FILE* fp); +int gba_fputc(int c, FILE* fp); +int gba_ferror(FILE* fp); +int gba_access(const char* path); + +#define fopen gba_fopen +#define fclose gba_fclose +#define fread gba_fread +#define fwrite gba_fwrite +#define fseek gba_fseek +#define ftell gba_ftell +#define feof gba_feof +#define fgetc gba_fgetc +#define fputc gba_fputc +#define ferror gba_ferror +#define access(p, m) gba_access(p) +#define remove(p) ((void)(p), 0) +#define perror(s) ((void)(s)) +#define mkdir(a, b) ((void)0, -1) + +/* stat() is harder to override (it's a function-like macro on some toolchains), + so we provide a wrapper that the few SDLPoP callers actually use. */ +#define stat(p, s) gba_stat_compat((p), (s)) +#define S_ISREG(m) (((m) & 0170000) == 0100000) +#define S_ISDIR(m) (0) +struct stat; +int gba_stat_compat(const char* path, struct stat* st); +#endif /* __GBA__ */ + #ifndef MAX #define MAX(a,b) ((a)>(b)?(a):(b)) #endif diff --git a/src/config.h b/src/config.h index ab5b488c..06453856 100644 --- a/src/config.h +++ b/src/config.h @@ -27,8 +27,22 @@ The authors of this program may be contacted at https://forum.princed.org #define strcasecmp _stricmp #endif +#ifdef __GBA__ +/* Disable everything that won't fit on a 16.78 MHz ARM7 with 256 KB EWRAM, + set BEFORE the rest of config.h sets defaults. */ +#define DISABLE_ALL_FIXES +#define POP_MAX_PATH 64 +#define POP_MAX_OPTIONS_SIZE 128 +#define SDLPOP_NO_FILE_IO 1 +#define _GBA_CONFIG_TRIM 1 +#endif + +#ifndef POP_MAX_PATH #define POP_MAX_PATH 256 +#endif +#ifndef POP_MAX_OPTIONS_SIZE #define POP_MAX_OPTIONS_SIZE 256 +#endif #define SDLPOP_VERSION "1.24 RC" #define WINDOW_TITLE "Prince of Persia (SDLPoP) v" SDLPOP_VERSION @@ -38,9 +52,11 @@ The authors of this program may be contacted at https://forum.princed.org #define SHARE_PATH "/usr/share" #endif +#ifndef __GBA__ // Enable or disable fading. // Fading used to be very buggy, but now it works correctly. #define USE_FADE +#endif // Enable or disable the potions level. (copy protection) #define USE_COPYPROT @@ -58,8 +74,10 @@ The authors of this program may be contacted at https://forum.princed.org // Disabled, because it introduces some timing bugs. //#define USE_COMPAT_TIMER +#ifndef __GBA__ // Enable quicksave/load feature. #define USE_QUICKSAVE +#endif // Try to let time keep running out when quickloading. (similar to Ctrl+A) // Technically, the 'remaining time' is still restored, but with a penalty for elapsed time (up to 1 minute). @@ -340,32 +358,42 @@ The authors of this program may be contacted at https://forum.princed.org // Enable debug cheats (with command-line argument "debug") // "[" and "]" : nudge x position by one pixel // "T" : display remaining time in minutes, seconds and ticks +#ifndef __GBA__ #define USE_DEBUG_CHEATS +#endif + // Darken those parts of the screen which are not near a torch. +#ifndef __GBA__ #define USE_LIGHTING +#endif // Enable screenshot features. +#ifndef __GBA__ #define USE_SCREENSHOT +#endif -// Automatically switch to keyboard or joystick/gamepad mode if there is input from that device. -// Useful if SDL detected a gamepad but there is none. +// Automatically switch to keyboard or joystick/gamepad mode if there is input +// from that device. Useful if SDL detected a gamepad but there is none. +#ifndef __GBA__ #define USE_AUTO_INPUT_MODE +#endif -#ifdef USE_TEXT // The menu won't work without text. - -// Display the in-game menu. +#if defined(USE_TEXT) +// Display the in-game menu. (Enabled on GBA too: the menu draws through the +// text/dialog routines that are already compiled in.) #define USE_MENU - #endif // Enable colored torches. A torch can be colored by changing its modifier in a level editor. #define USE_COLORED_TORCHES // Enable fast forwarding with the backtick key. +#ifndef __GBA__ #define USE_FAST_FORWARD +#endif // Set how much should the fast forwarding speed up the game. #define FAST_FORWARD_RATIO 10 @@ -378,12 +406,16 @@ The authors of this program may be contacted at https://forum.princed.org //#define FAST_FORWARD_MUTE // Briefly show a dark screen when changing rooms, like in the original game. +#ifndef __GBA__ #define USE_DARK_TRANSITION +#endif // Turn the balconies into teleports. // Each balcony (whose left half has a non-zero modifier) will behave as a teleport to another balcony with the same modifier. // The right half of such balconies must have modifier == 1. +#ifndef __GBA__ #define USE_TELEPORTS +#endif // Default SDL_Joystick button values diff --git a/src/data.h b/src/data.h index e1e10f5c..46d7007e 100644 --- a/src/data.h +++ b/src/data.h @@ -184,11 +184,11 @@ extern back_table_type foretable[200]; // data:463C extern back_table_type backtable[200]; // data:3D38 -extern midtable_type midtable[50]; +extern midtable_type midtable[128]; // GBA: bumped 50->128 (avoid overflow dialog) // data:5F1E -extern peel_type* peels_table[50]; +extern peel_type* peels_table[128]; // GBA: bumped 50->128 // data:4D9A -extern rect_type drects[30]; +extern rect_type drects[128]; // GBA: bumped 30->128 // data:4CB8 extern sbyte obj_direction; @@ -203,7 +203,7 @@ extern short obj_clip_right; // data:4082 extern short obj_clip_bottom; // data:34D2 -extern wipetable_type wipetable[300]; +extern wipetable_type wipetable[512]; // GBA: bumped 300->512 // data:2592 extern const byte chtab_shift[10] INIT(= {0,1,0,0,0,0,1,1,1,0}); // data:4354 @@ -400,7 +400,7 @@ extern word need_full_redraw; // data:588E extern short n_curr_objs; // data:5BAC -extern objtable_type objtable[50]; +extern objtable_type objtable[128]; // GBA: bumped 50->128 // data:5F8C extern short curr_objs[50]; @@ -732,7 +732,12 @@ extern byte enable_music INIT(= 1); extern byte enable_fade INIT(= 1); extern byte enable_flash INIT(= 1); extern byte enable_text INIT(= 1); +#ifdef __GBA__ +/* show_splash waits for Shift to be pressed; on GBA we skip it so the game just starts. */ +extern byte enable_info_screen INIT(= 0); +#else extern byte enable_info_screen INIT(= 1); +#endif extern byte enable_controller_rumble INIT(= 0); extern byte joystick_only_horizontal INIT(= 0); extern int joystick_threshold INIT(= 8000); diff --git a/src/menu.c b/src/menu.c index 469fd13e..f76eebc8 100644 --- a/src/menu.c +++ b/src/menu.c @@ -314,20 +314,31 @@ enum setting_ids { }; typedef struct setting_type { - int index; int id; - int previous, next; byte style; byte number_type; void* linked; void* required; int min, max; // for 'number'-style settings - char text[64]; - char explanation[256]; + // Stored as pointers to string literals (in .rodata) rather than inline + // buffers: nothing writes to these, and on GBA the inline char[64]+char[256] + // per entry pushed the settings tables ~56 KB over the EWRAM budget. + const char* text; + const char* explanation; names_list_type* names_list; } setting_type; -setting_type general_settings[] = { +// The settings descriptor tables are read-only at runtime (the up/down nav +// indices used to live in setting_type but are now computed on the fly in +// draw_setting, and the values themselves are reached through ->linked). On +// GBA we park them in ROM (.rodata) instead of the scarce 256 KB EWRAM. +#ifdef __GBA__ +#define MENU_SETTINGS_RODATA __attribute__((section(".rodata.menusettings"))) +#else +#define MENU_SETTINGS_RODATA +#endif + +setting_type general_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_SHOW_MENU_ON_PAUSE, .style = SETTING_STYLE_TOGGLE, .linked = &enable_pause_menu, .text = "Enable pause menu", .explanation = "Show the in-game menu when you pause the game.\n" @@ -367,7 +378,7 @@ int integer_scaling_possible = #endif ; -setting_type visuals_settings[] = { +setting_type visuals_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_FULLSCREEN, .style = SETTING_STYLE_TOGGLE, .linked = &start_fullscreen, .text = "Start fullscreen", .explanation = "Start the game in fullscreen mode.\nYou can also toggle fullscreen by pressing Alt+Enter."}, @@ -411,7 +422,7 @@ setting_type visuals_settings[] = { #endif }; -setting_type gameplay_settings[] = { +setting_type gameplay_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_ENABLE_CHEATS, .style = SETTING_STYLE_TOGGLE, .linked = &cheats_enabled, .text = "Enable cheats", .explanation = "Turn cheats on or off."/*"\nAlso, display the CHEATS option on the pause menu."*/}, @@ -656,7 +667,7 @@ NAMES_LIST(row_setting_names, {"Top", "Middle", "Bottom"}); KEY_VALUE_LIST(direction_setting_names, {{"Left", dir_FF_left}, {"Right", dir_0_right}}); extern names_list_type never_is_16_list; -setting_type mods_settings[] = { +setting_type mods_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_USE_CUSTOM_OPTIONS, .style = SETTING_STYLE_TOGGLE, .linked = &use_custom_options, .text = "Use customization options", .explanation = "Turn customization options on or off.\n(default = OFF)"}, @@ -999,7 +1010,7 @@ KEY_VALUE_LIST(guard_type_setting_names, {{"None", -1}, {"Normal", 0}, {"Fat", 1 NAMES_LIST(entry_pose_setting_names, {"Turning", "Falling", "Running"}); KEY_VALUE_LIST(off_setting_name, {{"Off", -1}}); // used for the seamless exit setting -setting_type level_settings[] = { +setting_type level_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_LEVEL_SETTINGS, .style = SETTING_STYLE_TEXT_ONLY, .required = &use_custom_options, .text = "Customize another level...", .explanation = "Select another level to customize."}, @@ -1040,7 +1051,7 @@ setting_type level_settings[] = { "Set to -1 to disable."}, }; -setting_type controls_settings[] = { +setting_type controls_settings[] MENU_SETTINGS_RODATA = { {.id = SETTING_KEY_LEFT, .style = SETTING_STYLE_KEY, .required = NULL, .linked = &key_left, .number_type = SETTING_INT, .text = "Left", @@ -1124,17 +1135,13 @@ void init_pause_menu_items(pause_menu_item_type* first_item, int item_count) { } void init_settings_list(setting_type* first_setting, int setting_count) { - if (setting_count > 0) { - for (int i = 0; i < setting_count; ++i) { - setting_type* item = first_setting + i; - item->index = i; - item->previous = (first_setting + MAX(0, i-1))->id; - item->next = (first_setting + MIN(setting_count-1, i+1))->id; - } -// setting_type* last_item = first_setting + (setting_count-1); -// first_setting->previous = last_item->id; -// last_item->next = first_setting->id; - } + // The per-setting navigation indices (index/previous/next) used to be + // precomputed and stored in each setting_type here. They are now derived on + // the fly in draw_setting() from the setting's position, which lets the + // settings tables be const/ROM-resident (see MENU_SETTINGS_RODATA). Nothing + // to initialize anymore. + (void)first_setting; + (void)setting_count; } @@ -1565,7 +1572,8 @@ char* print_setting_value_(setting_type* setting, int value, char* buffer, size_ return buffer; } -void draw_setting(setting_type* setting, rect_type* parent, int* y_offset, int inactive_text_color) { +void draw_setting(settings_area_type* settings_area, int i, rect_type* parent, int* y_offset, int inactive_text_color) { + setting_type* setting = &settings_area->settings[i]; rect_type text_rect = *parent; text_rect.top += *y_offset; int text_color = inactive_text_color; @@ -1584,18 +1592,28 @@ void draw_setting(setting_type* setting, rect_type* parent, int* y_offset, int i } if (highlighted_setting_id == setting->id) { - next_setting_id = setting->next; - previous_setting_id = setting->previous; - at_scroll_up_boundary = (setting->index == scroll_position); - at_scroll_down_boundary = (setting->index == scroll_position + 8); + // Up/down navigation targets, derived from this setting's position in the + // area (previously precomputed into setting_type by init_settings_list). + int count = settings_area->setting_count; + next_setting_id = settings_area->settings[MIN(count - 1, i + 1)].id; + previous_setting_id = settings_area->settings[MAX(0, i - 1)].id; + at_scroll_up_boundary = (i == scroll_position); + at_scroll_down_boundary = (i == scroll_position + 8); SDL_Rect dest_rect; rect_to_sdlrect(&setting_box, &dest_rect); +#ifdef __GBA__ + // No ARGB overlay on GBA: fill the 8bpp screen directly with a PoP + // palette index (SDL_MapRGB can't help — the screen surface's palette + // is never populated, the hardware palette is driven from palette[]). + SDL_FillRect(current_target_surface, &dest_rect, (Uint32)color_8_darkgray); +#else uint32_t rgb_color = SDL_MapRGBA(overlay_surface->format, 55, 55, 55, 255); if (SDL_FillRect(overlay_surface, &dest_rect, rgb_color) != 0) { sdlperror("draw_setting: SDL_FillRect"); quit(1); } +#endif rect_type left_side_of_setting_box = setting_box; left_side_of_setting_box.left = setting_box.left - 2; left_side_of_setting_box.right = setting_box.left; @@ -1782,7 +1800,7 @@ void draw_settings_area(settings_area_type* settings_area) { for (int i = 0; (i < settings_area->setting_count) && (num_drawn_settings < 9); ++i) { if (i >= scroll_position) { ++num_drawn_settings; - draw_setting(&settings_area->settings[i], &settings_area_rect, &y_offset, color_15_brightwhite); + draw_setting(settings_area, i, &settings_area_rect, &y_offset, color_15_brightwhite); } } @@ -1975,7 +1993,7 @@ void draw_confirmation_dialog(int which_dialog, const char* text) { old_highlighted_button = highlighted_button; // Need to redraw the dialog box. uint32_t clear_color = SDL_MapRGBA(current_target_surface->format, 0, 0, 0, 255); - SDL_FillRect(overlay_surface, NULL, clear_color); + SDL_FillRect(current_target_surface, NULL, clear_color); // == overlay_surface on desktop; onscreen_surface_ on GBA (overlay is NULL) draw_rect(©prot_dialog->peel_rect, color_0_black); dialog_method_2_frame(copyprot_dialog); rect_type rect; @@ -2040,7 +2058,7 @@ void draw_select_level_dialog(void) { old_edited_level_number = menu_current_level; // Need to redraw the dialog box. uint32_t clear_color = SDL_MapRGBA(current_target_surface->format, 0, 0, 0, 255); - SDL_FillRect(overlay_surface, NULL, clear_color); + SDL_FillRect(current_target_surface, NULL, clear_color); // == overlay_surface on desktop; onscreen_surface_ on GBA (overlay is NULL) draw_rect(©prot_dialog->peel_rect, color_0_black); dialog_method_2_frame(copyprot_dialog); rect_type rect; @@ -2067,10 +2085,35 @@ void draw_select_level_dialog(void) { int need_full_menu_redraw_count; +#ifdef __GBA__ +extern int gba_menu_active; // gba/src/gba_viewport.c — freezes the gameplay camera +// The menu is laid out for the full 320x192 world surface, but the GBA only +// shows a 240x160 window into it. Pin the camera (gba_menu_active) and pan the +// window so whichever part the player is interacting with is on screen: the +// centred pause menu, the left subsection column, or the wider settings panel. +static void gba_menu_set_viewport(void) { + extern void gba_set_viewport(int x, int y); + int vx = 40, vy = 16; // pause menu: centred (items are centred at x~160) + if (drawn_menu == 1) { + vy = 8; + vx = (controlled_area == 1) ? 80 : 0; // settings panel (right) vs. subsection list (left) + } + gba_set_viewport(vx, vy); +} +#endif + void draw_menu() { escape_key_suppressed = (key_states[SDL_SCANCODE_BACKSPACE] & KEYSTATE_HELD || key_states[SDL_SCANCODE_ESCAPE] & KEYSTATE_HELD); surface_type* saved_target_surface = current_target_surface; +#ifdef __GBA__ + // overlay_surface is NULL on GBA (no separate ARGB overlay / compositing + // step); draw straight into the 8bpp screen surface that update_screen() + // presents. The menu fills the screen with an opaque backdrop anyway. + current_target_surface = onscreen_surface_; + gba_menu_active = 1; +#else current_target_surface = overlay_surface; +#endif need_close_menu = false; while (!need_close_menu) { @@ -2134,6 +2177,12 @@ void draw_menu() { draw_settings_menu(); } textstate.ptr_font = saved_font; +#ifdef __GBA__ + // Pan the 240x160 window to whatever the player is on now (set after + // drawing so drawn_menu/controlled_area reflect this frame), then + // present — gba_camera_update() leaves it alone while gba_menu_active. + gba_menu_set_viewport(); +#endif if (!need_close_menu) { update_screen(); } @@ -2143,6 +2192,9 @@ void draw_menu() { } current_target_surface = saved_target_surface; +#ifdef __GBA__ + gba_menu_active = 0; // hand the viewport back to the gameplay camera +#endif } void clear_menu_controls() { diff --git a/src/options.c b/src/options.c index a9508a25..82fe627f 100644 --- a/src/options.c +++ b/src/options.c @@ -477,7 +477,11 @@ void set_options_to_default() { enable_fade = 1; enable_flash = 1; enable_text = 1; +#ifdef __GBA__ + enable_info_screen = 0; /* show_splash waits for Shift; skip on GBA */ +#else enable_info_screen = 1; +#endif start_fullscreen = 0; use_hardware_acceleration = 2; use_correct_aspect_ratio = 0; diff --git a/src/sdl2_to_sdl3.c b/src/sdl2_to_sdl3.c new file mode 100644 index 00000000..5dfd9416 --- /dev/null +++ b/src/sdl2_to_sdl3.c @@ -0,0 +1,100 @@ +/* +SDLPoP, a port/conversion of the DOS game Prince of Persia. +Copyright (C) 2013-2024 Dávid Nagy + +SDL2 -> SDL3 audio bridge (see sdl2_to_sdl3.h). + +SDL3 removed the callback-based push-audio API. The game still fills audio in a +SDL2-style callback (audio_callback in seg009.c), so we open an SDL_AudioStream +with SDL_OpenAudioDeviceStream and, whenever the device needs more data, run the +game's callback into a scratch buffer and push it to the stream. + +The stream handle and the desired spec are single shared globals: the audio +callback runs on SDL's audio thread while seg009.c and midi.c call +SDL_LockAudio/SDL_PauseAudio around state changes, so all translation units must +act on the same stream. Hence this lives in a .c file, not the header. +*/ + +#include "common.h" /* pulls in config.h + types.h + SDL3 + sdl2_to_sdl3.h (SDL_AudioSpec == compat) */ + +#ifdef USE_SDL3 + +/* Restore the real SDL3 SDL_AudioSpec for opening the device; the SDL2-facing + spec keeps its explicit typedef name compat_SDL_AudioSpec. */ +#undef SDL_AudioSpec + +static SDL_AudioStream* s_audio_stream = NULL; +static compat_SDL_AudioSpec s_audio_desired; + +static void SDLCALL compat_audio_bridge(void* userdata, SDL_AudioStream* stream, + int additional_amount, int total_amount) { + (void)userdata; (void)total_amount; + if (additional_amount <= 0) return; + Uint8* buf = (Uint8*)SDL_malloc((size_t)additional_amount); + if (buf == NULL) return; + if (s_audio_desired.callback != NULL) { + s_audio_desired.callback(s_audio_desired.userdata, buf, additional_amount); + } else { + SDL_memset(buf, s_audio_desired.silence, (size_t)additional_amount); + } + SDL_PutAudioStreamData(stream, buf, additional_amount); + SDL_free(buf); +} + +int compat_SDL_OpenAudio(compat_SDL_AudioSpec* desired, compat_SDL_AudioSpec* obtained) { + if (desired == NULL) return -1; + /* SDL2's SDL_OpenAudio implicitly initialized the audio subsystem; SDL3's + stream API does not, and the game only requests video+gamepad in SDL_Init. */ + SDL_InitSubSystem(SDL_INIT_AUDIO); + s_audio_desired = *desired; + /* Silence value the game memsets with (0x80 for unsigned 8-bit, else 0). */ + s_audio_desired.silence = SDL_AUDIO_ISUNSIGNED(desired->format) ? 0x80 : 0x00; + desired->silence = s_audio_desired.silence; + + SDL_AudioSpec spec; + SDL_zero(spec); + spec.freq = desired->freq; + spec.format = desired->format; + spec.channels = desired->channels; + + s_audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, + &spec, compat_audio_bridge, NULL); + if (s_audio_stream == NULL) return -1; + /* Streams from SDL_OpenAudioDeviceStream start paused, like SDL2's device + after SDL_OpenAudio; the game calls SDL_PauseAudio(0) to start playback. */ + if (obtained != NULL) *obtained = s_audio_desired; + return 0; +} + +void compat_SDL_PauseAudio(int pause_on) { + if (s_audio_stream == NULL) return; + if (pause_on) SDL_PauseAudioStreamDevice(s_audio_stream); + else SDL_ResumeAudioStreamDevice(s_audio_stream); +} + +void compat_SDL_LockAudio(void) { if (s_audio_stream) SDL_LockAudioStream(s_audio_stream); } +void compat_SDL_UnlockAudio(void) { if (s_audio_stream) SDL_UnlockAudioStream(s_audio_stream); } + +void compat_SDL_CloseAudio(void) { + if (s_audio_stream) { SDL_DestroyAudioStream(s_audio_stream); s_audio_stream = NULL; } +} + +void compat_SDL_AudioResampleFF(SDL_AudioFormat fmt, int channels, int src_freq, int dst_freq, + const Uint8* in, int in_len, Uint8* out, int out_len) { + SDL_AudioSpec src, dst; + SDL_zero(src); SDL_zero(dst); + src.format = fmt; src.channels = channels; src.freq = src_freq; + dst.format = fmt; dst.channels = channels; dst.freq = dst_freq; + Uint8* converted = NULL; + int converted_len = 0; + if (SDL_ConvertAudioSamples(&src, in, in_len, &dst, &converted, &converted_len)) { + int n = (out_len < converted_len) ? out_len : converted_len; + SDL_memcpy(out, converted, (size_t)n); + SDL_free(converted); + } else { + int n = (out_len < in_len) ? out_len : in_len; /* fallback: no resample */ + SDL_memcpy(out, in, (size_t)n); + } +} + +#endif /* USE_SDL3 */ diff --git a/src/sdl2_to_sdl3.h b/src/sdl2_to_sdl3.h new file mode 100644 index 00000000..a5008470 --- /dev/null +++ b/src/sdl2_to_sdl3.h @@ -0,0 +1,322 @@ +/* +SDLPoP, a port/conversion of the DOS game Prince of Persia. +Copyright (C) 2013-2024 Dávid Nagy + +SDL2 -> SDL3 compatibility shim. + +Included by types.h ONLY when USE_SDL3 is defined, AFTER the SDL3 headers (which +are themselves pulled in with SDL_ENABLE_OLD_NAMES defined). SDL3's own +SDL_oldnames.h then maps the vast majority of SDL2 names 1:1 (event types, +gamepad enums, SDL_FreeSurface, SDL_RWops, etc.). This shim only covers what that +layer cannot: + + - signature changes (SDL_CreateWindow/Renderer arg counts, SDL_RWread/write, + SDL_RenderSetLogicalSize, SDL_BlitScaled's new scale-mode arg, SDL_MapRGB); + - return-value flips: SDL3 returns `bool` (true = success) where SDL2 returned + `int` (0 = success), and SDLPoP checks `!= 0` for errors, so those calls are + wrapped to return 0/-1; + - APIs removed entirely (SDL_CreateRGBSurface, the callback-based audio API, + SDL_AudioCVT, the SDL_version struct); + - a few names oldnames maps to deprecated "remove this line" sentinels. + +Where we override a name that SDL3/oldnames also defines as a macro, we #undef +first to avoid -Wmacro-redefined. Inline wrappers are defined BEFORE the SDL2-name +macros so their bodies call the real SDL3 functions, not themselves. + +The timer-callback signature change (SDL_AddTimer) is handled by a small #ifdef on +the two callback definitions in seg000.c / seg009.c, not here. +*/ + +#ifndef SDL2_TO_SDL3_H +#define SDL2_TO_SDL3_H + +#include + +/* ---- constants oldnames/SDL3 don't provide (or map to sentinels) ---- */ +#ifndef SDL_ENABLE +#define SDL_ENABLE 1 +#endif +#ifndef SDL_DISABLE +#define SDL_DISABLE 0 +#endif + +/* SDL_INIT_TIMER / NOPARACHUTE were removed (timer is always available). */ +#undef SDL_INIT_TIMER +#define SDL_INIT_TIMER 0u +#undef SDL_INIT_NOPARACHUTE +#define SDL_INIT_NOPARACHUTE 0u + +/* oldnames maps this to a deprecated sentinel; we still pass it to a flags arg. */ +#undef SDL_WINDOW_FULLSCREEN_DESKTOP +#define SDL_WINDOW_FULLSCREEN_DESKTOP SDL_WINDOW_FULLSCREEN + +/* Renderer creation flags are gone in SDL3; keep dummy bits that + compat_SDL_CreateRenderer interprets (SOFTWARE) or ignores. */ +#undef SDL_RENDERER_SOFTWARE +#define SDL_RENDERER_SOFTWARE 0x1u +#undef SDL_RENDERER_ACCELERATED +#define SDL_RENDERER_ACCELERATED 0x2u +#undef SDL_RENDERER_TARGETTEXTURE +#define SDL_RENDERER_TARGETTEXTURE 0x4u + +/* These hints were removed (oldnames -> deprecated sentinels). Keep them as + harmless strings so SDL_SetHint() calls still compile; SDL3 ignores unknown + hints. Fuzzy/blurry scaling is reproduced via SDL_SetTextureScaleMode in + seg009.c instead. */ +#undef SDL_HINT_RENDER_SCALE_QUALITY +#define SDL_HINT_RENDER_SCALE_QUALITY "SDL_RENDER_SCALE_QUALITY" +#undef SDL_HINT_RENDER_VSYNC +#define SDL_HINT_RENDER_VSYNC "SDL_RENDER_VSYNC" +#undef SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING +#define SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING "SDL_WINDOWS_DISABLE_THREAD_NAMING" + +/* ---- types removed from SDL3 ---- */ +/* SDL_GetVersion now returns a packed int; recreate the SDL2 struct. */ +typedef struct compat_SDL_version { int major, minor, patch; } SDL_version; + +/* SDL3's SDL_AudioSpec dropped silence/samples/callback/userdata, which the game + still sets and reads, so we keep an SDL2-shaped spec and bridge it. */ +typedef void (*SDL_AudioCallback)(void* userdata, Uint8* stream, int len); +typedef struct compat_SDL_AudioSpec { + int freq; + SDL_AudioFormat format; + Uint8 channels; + Uint8 silence; + Uint16 samples; + Uint32 size; + SDL_AudioCallback callback; + void* userdata; +} compat_SDL_AudioSpec; + +/* ====================================================================== + * Inline wrappers (call the REAL SDL3 functions; SDL2-name macros below). + * ====================================================================== */ + +static inline int compat_SDL_Init(Uint32 flags) { return SDL_Init(flags) ? 0 : -1; } +static inline int compat_SDL_InitSubSystem(Uint32 flags) { return SDL_InitSubSystem(flags) ? 0 : -1; } + +static inline SDL_Surface* compat_SDL_CreateRGBSurface(Uint32 flags, int w, int h, int depth, + Uint32 rmask, Uint32 gmask, Uint32 bmask, Uint32 amask) { + (void)flags; + SDL_PixelFormat fmt = SDL_GetPixelFormatForMasks(depth, rmask, gmask, bmask, amask); + SDL_Surface* s = SDL_CreateSurface(w, h, fmt); + /* SDL3 does not auto-attach a palette to indexed surfaces; the game sets + palette entries on its 8bpp surfaces, so create one here. */ + if (s != NULL && SDL_BITSPERPIXEL(fmt) <= 8) SDL_CreateSurfacePalette(s); + return s; +} + +/* SDL2 SDL_ConvertSurface(surface, fmt*, flags) / SDL_ConvertSurfaceFormat(surface, + fmtenum, flags) both collapse to SDL3's SDL_ConvertSurface(surface, fmtenum). */ +static inline SDL_Surface* compat_SDL_ConvertSurface(SDL_Surface* src, SDL_PixelFormat fmt, Uint32 flags) { + (void)flags; + return SDL_ConvertSurface(src, fmt); +} + +static inline Uint32 compat_SDL_MapRGB(SDL_PixelFormat fmt, Uint8 r, Uint8 g, Uint8 b) { + return SDL_MapRGB(SDL_GetPixelFormatDetails(fmt), NULL, r, g, b); +} +static inline Uint32 compat_SDL_MapRGBA(SDL_PixelFormat fmt, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + return SDL_MapRGBA(SDL_GetPixelFormatDetails(fmt), NULL, r, g, b, a); +} + +static inline int compat_SDL_LockSurface(SDL_Surface* s) { return SDL_LockSurface(s) ? 0 : -1; } +static inline int compat_SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color) { + return SDL_FillSurfaceRect(dst, rect, color) ? 0 : -1; +} +static inline int compat_SDL_BlitSurface(SDL_Surface* src, const SDL_Rect* srcrect, + SDL_Surface* dst, SDL_Rect* dstrect) { + return SDL_BlitSurface(src, srcrect, dst, dstrect) ? 0 : -1; +} +static inline int compat_SDL_BlitScaled(SDL_Surface* src, const SDL_Rect* srcrect, + SDL_Surface* dst, SDL_Rect* dstrect) { + return SDL_BlitSurfaceScaled(src, srcrect, dst, dstrect, SDL_SCALEMODE_NEAREST) ? 0 : -1; +} +static inline int compat_SDL_SetColorKey(SDL_Surface* s, int enabled, Uint32 key) { + return SDL_SetSurfaceColorKey(s, enabled != 0, key) ? 0 : -1; +} +static inline int compat_SDL_SetSurfaceAlphaMod(SDL_Surface* s, Uint8 alpha) { + return SDL_SetSurfaceAlphaMod(s, alpha) ? 0 : -1; +} +static inline int compat_SDL_SetPaletteColors(SDL_Palette* palette, const SDL_Color* colors, + int firstcolor, int ncolors) { + return SDL_SetPaletteColors(palette, colors, firstcolor, ncolors) ? 0 : -1; +} + +static inline SDL_Window* compat_SDL_CreateWindow(const char* title, int x, int y, + int w, int h, Uint32 flags) { + (void)x; (void)y; /* SDL3 has no creation position (was SDL_WINDOWPOS_UNDEFINED) */ + return SDL_CreateWindow(title, w, h, flags); +} +static inline int compat_SDL_SetWindowFullscreen(SDL_Window* win, Uint32 flags) { + return SDL_SetWindowFullscreen(win, flags != 0) ? 0 : -1; +} + +static inline SDL_Renderer* compat_SDL_CreateRenderer(SDL_Window* win, int index, Uint32 flags) { + (void)index; + SDL_Renderer* r = SDL_CreateRenderer(win, (flags & SDL_RENDERER_SOFTWARE) ? "software" : NULL); + if (r != NULL) SDL_SetRenderVSync(r, 0); /* keep VSync off (timer relies on it) */ + return r; +} +/* SDL3 merged logical size + integer scaling into one presentation call with a + mode; keep them independent by re-reading the current size/mode each time. */ +static inline int compat_SDL_RenderSetLogicalSize(SDL_Renderer* r, int w, int h) { + int cw, ch; SDL_RendererLogicalPresentation m = SDL_LOGICAL_PRESENTATION_LETTERBOX; + SDL_GetRenderLogicalPresentation(r, &cw, &ch, &m); + if (m != SDL_LOGICAL_PRESENTATION_INTEGER_SCALE) m = SDL_LOGICAL_PRESENTATION_LETTERBOX; + return SDL_SetRenderLogicalPresentation(r, w, h, m) ? 0 : -1; +} +static inline int compat_SDL_RenderSetIntegerScale(SDL_Renderer* r, bool enable) { + int w, h; SDL_RendererLogicalPresentation m; + SDL_GetRenderLogicalPresentation(r, &w, &h, &m); + if (w == 0 || h == 0) { w = 320; h = 200; } + return SDL_SetRenderLogicalPresentation(r, w, h, + enable ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX) ? 0 : -1; +} +static inline void compat_SDL_RenderGetLogicalSize(SDL_Renderer* r, int* w, int* h) { + SDL_RendererLogicalPresentation m; + SDL_GetRenderLogicalPresentation(r, w, h, &m); +} + +static inline int compat_SDL_ShowCursor(int toggle) { + if (toggle) SDL_ShowCursor(); else SDL_HideCursor(); + return 0; +} + +/* SDL3 reports mouse position as float; the game uses int. */ +static inline Uint32 compat_SDL_GetMouseState(int* x, int* y) { + float fx = 0.0f, fy = 0.0f; + Uint32 buttons = (Uint32)SDL_GetMouseState(&fx, &fy); + if (x) *x = (int)fx; + if (y) *y = (int)fy; + return buttons; +} + +/* SDL_RWread/RWwrite returned a count of OBJECTS; SDL_ReadIO/WriteIO work in + bytes, so divide back by the element size to preserve the old semantics. */ +static inline size_t compat_SDL_RWread(SDL_IOStream* ctx, void* ptr, size_t size, size_t maxnum) { + if (size == 0) return 0; + return SDL_ReadIO(ctx, ptr, size * maxnum) / size; +} +static inline size_t compat_SDL_RWwrite(SDL_IOStream* ctx, const void* ptr, size_t size, size_t num) { + if (size == 0) return 0; + return SDL_WriteIO(ctx, ptr, size * num) / size; +} +static inline int compat_SDL_RWclose(SDL_IOStream* ctx) { return SDL_CloseIO(ctx) ? 0 : -1; } + +static inline void compat_SDL_GetVersion(SDL_version* v) { + int n = SDL_GetVersion(); + v->major = SDL_VERSIONNUM_MAJOR(n); + v->minor = SDL_VERSIONNUM_MINOR(n); + v->patch = SDL_VERSIONNUM_MICRO(n); +} + +/* Audio bridge + fast-forward resampler live in sdl2_to_sdl3.c (shared state). */ +extern int compat_SDL_OpenAudio(compat_SDL_AudioSpec* desired, compat_SDL_AudioSpec* obtained); +extern void compat_SDL_PauseAudio(int pause_on); +extern void compat_SDL_LockAudio(void); +extern void compat_SDL_UnlockAudio(void); +extern void compat_SDL_CloseAudio(void); +extern void compat_SDL_AudioResampleFF(SDL_AudioFormat fmt, int channels, + int src_freq, int dst_freq, const Uint8* in, int in_len, Uint8* out, int out_len); + +/* ====================================================================== + * SDL2-name macros. #undef first (oldnames/SDL3 may already define them as + * macros) to avoid -Wmacro-redefined; defined last so the wrappers above are + * never rewritten. + * ====================================================================== */ + +/* Surfaces / pixels */ +#undef SDL_CreateRGBSurface +#define SDL_CreateRGBSurface compat_SDL_CreateRGBSurface +#undef SDL_ConvertSurface +#define SDL_ConvertSurface(s,f,fl) compat_SDL_ConvertSurface((s),(f),(fl)) +#undef SDL_ConvertSurfaceFormat +#define SDL_ConvertSurfaceFormat(s,f,fl) compat_SDL_ConvertSurface((s),(f),(fl)) +#undef SDL_SetColorKey +#define SDL_SetColorKey compat_SDL_SetColorKey +#undef SDL_SetSurfaceAlphaMod +#define SDL_SetSurfaceAlphaMod compat_SDL_SetSurfaceAlphaMod +#undef SDL_SetPaletteColors +#define SDL_SetPaletteColors compat_SDL_SetPaletteColors +#undef SDL_LockSurface +#define SDL_LockSurface compat_SDL_LockSurface +#undef SDL_FillRect +#define SDL_FillRect compat_SDL_FillRect +#undef SDL_BlitSurface +#define SDL_BlitSurface compat_SDL_BlitSurface +#undef SDL_BlitScaled +#define SDL_BlitScaled compat_SDL_BlitScaled +#undef SDL_MapRGB +#define SDL_MapRGB compat_SDL_MapRGB +#undef SDL_MapRGBA +#define SDL_MapRGBA compat_SDL_MapRGBA + +/* Window / renderer */ +#undef SDL_CreateWindow +#define SDL_CreateWindow compat_SDL_CreateWindow +#undef SDL_SetWindowFullscreen +#define SDL_SetWindowFullscreen compat_SDL_SetWindowFullscreen +#undef SDL_CreateRenderer +#define SDL_CreateRenderer compat_SDL_CreateRenderer +#undef SDL_RenderSetLogicalSize +#define SDL_RenderSetLogicalSize compat_SDL_RenderSetLogicalSize +#undef SDL_RenderSetIntegerScale +#define SDL_RenderSetIntegerScale compat_SDL_RenderSetIntegerScale +#undef SDL_RenderGetLogicalSize +#define SDL_RenderGetLogicalSize compat_SDL_RenderGetLogicalSize + +/* Cursor / version */ +#undef SDL_ShowCursor +#define SDL_ShowCursor compat_SDL_ShowCursor +#undef SDL_GetMouseState +#define SDL_GetMouseState compat_SDL_GetMouseState +#undef SDL_GetVersion +#define SDL_GetVersion compat_SDL_GetVersion +#undef SDL_VERSION +#define SDL_VERSION(v) do { (v)->major = SDL_MAJOR_VERSION; (v)->minor = SDL_MINOR_VERSION; (v)->patch = SDL_MICRO_VERSION; } while (0) + +/* RWops / IO (oldnames renames these but doesn't fix the signature/return) */ +#undef SDL_RWread +#define SDL_RWread compat_SDL_RWread +#undef SDL_RWwrite +#define SDL_RWwrite compat_SDL_RWwrite +#undef SDL_RWclose +#define SDL_RWclose compat_SDL_RWclose + +/* SDL_image (SDL3_image; not covered by SDL_oldnames) */ +#undef IMG_Load_RW +#define IMG_Load_RW(rw,f) IMG_Load_IO((rw),(f)) +#undef IMG_GetError +#define IMG_GetError SDL_GetError +#undef IMG_SavePNG +#define IMG_SavePNG(s,f) (IMG_SavePNG((s),(f)) ? 0 : -1) + +/* Text input now takes the window; SDL_SetTextInputRect became SDL_SetTextInputArea. + These expand at call sites in seg009.c where `window_` (data.h) is in scope. */ +#undef SDL_StartTextInput +#define SDL_StartTextInput() SDL_StartTextInput(window_) +#undef SDL_StopTextInput +#define SDL_StopTextInput() SDL_StopTextInput(window_) +#undef SDL_SetTextInputRect +#define SDL_SetTextInputRect(r) SDL_SetTextInputArea(window_, (r), 0) + +/* Audio (init/return wrappers + the bridge) */ +#undef SDL_Init +#define SDL_Init compat_SDL_Init +#undef SDL_InitSubSystem +#define SDL_InitSubSystem compat_SDL_InitSubSystem +#undef SDL_AudioSpec +#define SDL_AudioSpec compat_SDL_AudioSpec +#undef SDL_OpenAudio +#define SDL_OpenAudio compat_SDL_OpenAudio +#undef SDL_PauseAudio +#define SDL_PauseAudio compat_SDL_PauseAudio +#undef SDL_LockAudio +#define SDL_LockAudio compat_SDL_LockAudio +#undef SDL_UnlockAudio +#define SDL_UnlockAudio compat_SDL_UnlockAudio +#undef SDL_CloseAudio +#define SDL_CloseAudio compat_SDL_CloseAudio + +#endif /* SDL2_TO_SDL3_H */ diff --git a/src/seg000.c b/src/seg000.c index 3701636f..2dc10a17 100644 --- a/src/seg000.c +++ b/src/seg000.c @@ -176,7 +176,6 @@ void init_game_main() { init_lighting(); #endif load_all_sounds(); - hof_read(); show_splash(); // added start_game(); @@ -523,8 +522,14 @@ void check_quick_op() { #endif // USE_QUICKSAVE +#ifdef USE_SDL3 +// SDL3 changed the timer callback signature to (userdata, timerID, interval). +Uint32 SDLCALL temp_shift_release_callback(void *param, SDL_TimerID timerID, Uint32 interval) { + (void)param; (void)timerID; (void)interval; +#else Uint32 temp_shift_release_callback(Uint32 interval, void *param) { - const Uint8* state = SDL_GetKeyboardState(NULL); +#endif + const KEYSTATE_PTR_T* state = SDL_GetKeyboardState(NULL); if (state[SDL_SCANCODE_LSHIFT]) key_states[SDL_SCANCODE_LSHIFT] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; if (state[SDL_SCANCODE_RSHIFT]) key_states[SDL_SCANCODE_RSHIFT] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; return 0; // causes the timer to be removed @@ -584,6 +589,22 @@ int process_key() { if (rem_min != 0 && Kid.alive > 6 && (control_shift || key == SDL_SCANCODE_RETURN)) { key = SDL_SCANCODE_A | WITH_CTRL; // Ctrl+A } +#ifdef __GBA__ + // On GBA the death "Press Button to Continue" sequence can stall before + // Kid.alive reaches the >6 "ready" state above: play_kid() only advances + // Char.alive on frames where check_sound_playing() is false, and the GBA + // SFX voices can read as still-playing (see gba_sound_bridge.c), so the + // normal restart never arms and the body never reaches its resting frame. + // Let the Start button (mapped to Enter) restart the moment the Kid is + // actually dead (alive >= 0). After the level reloads the Kid is alive + // again (alive == -1), so holding Start restarts exactly once, not in a + // loop. The persistent keystate is used instead of the single-shot `key` + // because the latter can be consumed before we get here. + else if (rem_min != 0 && Kid.alive >= 0 && current_level != 15 && + (key_states[SDL_SCANCODE_RETURN] & KEYSTATE_HELD)) { + key = SDL_SCANCODE_A | WITH_CTRL; // Ctrl+A + } +#endif #ifdef USE_REPLAY if (recording) key_press_while_recording(&key); else if (replaying) key_press_while_replaying(&key); @@ -929,6 +950,28 @@ void draw_game_frame() { need_redraw_because_flipped = 0; redraw_screen(0); } else { +#ifdef __GBA__ + /* The GBA front-end has no separate offscreen/onscreen surface: + make_offscreen_buffer() aliases onscreen_surface_, and + gba_present_surface() copies the ENTIRE camera window to VRAM + every frame. SDLPoP's incremental renderer wipes dirty tiles to + black and redraws only those tiles, relying on the desktop + dirty-rectangle copy (copy_screen_rect of `drects` only) to keep + the display correct while the shared buffer accumulates + over-cleared regions outside the dirty rects. On GBA the whole + buffer is displayed, so those over-cleared regions show as black + and peels propagate them (the "black blocks/trail" behind the + moving prince). Redraw the whole room every frame so the single + buffer is always complete. This is the same full-room draw the + game already performs on every room entry. + NOTE: redraw_screen() ends by setting exit_room_timer = 2, which + makes exit_room() a no-op for 2 frames. Calling it every frame would + permanently freeze room transitions (prince can never leave a room), + so save and restore the timer around the redraw. */ + int saved_exit_room_timer = exit_room_timer; + redraw_screen(0); + exit_room_timer = saved_exit_room_timer; +#else memset(&table_counts, 0, sizeof(table_counts)); draw_moving(); draw_tables(); @@ -946,6 +989,7 @@ void draw_game_frame() { flip_screen(offscreen_surface); } drects_count = 0; +#endif } } } @@ -1684,6 +1728,17 @@ void load_more_opt_graf(const char* filename) { // stub dat_shpl_type area; dat_type* dathandle = NULL; +#ifdef __GBA__ + /* These optgraf sprites belong to the environment chtab (palette row 5). + load_one_optgraf -> load_image -> decode_image uses our pixel-offset + global to bake row*16 into pixel values; without setting it here those + sprites would land on palette[1..15] (PoP VGA defaults: blue, green) + instead of palette[81..95] (loaded environment colours) and the level + tiles render as dark blue. */ + extern int gba_decode_palette_offset; + int saved_offset = gba_decode_palette_offset; + gba_decode_palette_offset = 5 * 16; +#endif for (short graf_index = 0; graf_index < 8; ++graf_index) { /*if (...) */ { if (dathandle == NULL) { @@ -1697,6 +1752,9 @@ void load_more_opt_graf(const char* filename) { if (dathandle != NULL) { close_dat(dathandle); } +#ifdef __GBA__ + gba_decode_palette_offset = saved_offset; +#endif } // seg000:148D @@ -1716,6 +1774,12 @@ int do_paused() { } else { read_keyb_control(); } +#ifdef __GBA__ + if (control_x != CONTROL_RELEASED || control_y != CONTROL_RELEASED || control_shift != CONTROL_RELEASED) { + extern void gba_log(const char*,...); + gba_log("control: x=%d y=%d shift=%d joyst=%d", control_x, control_y, control_shift, is_joyst_mode); + } +#endif key = process_key(); if (is_ending_sequence && is_paused) { is_paused = 0; // fix being able to pause the game during the ending sequence @@ -2318,7 +2382,7 @@ void load_title_images(int bgcolor) { color.a = 0xFF; } if (NULL != chtab_title40) { - SDL_SetPaletteColors(chtab_title40->images[0]->format->palette, &color, 14, 1); + SDL_SetPaletteColors(SURFACE_PALETTE(chtab_title40->images[0]), &color, 14, 1); } } else if (graphics_mode == gmEga || graphics_mode == gmTga) { // ... diff --git a/src/seg003.c b/src/seg003.c index f2a80b80..05908aa6 100644 --- a/src/seg003.c +++ b/src/seg003.c @@ -67,7 +67,6 @@ void play_level(int level_number) { } cutscene_func = tbl_cutscenes[custom->tbl_cutscenes_by_index[level_number]]; if (cutscene_func != NULL - #ifdef USE_REPLAY && !(recording || replaying) #endif @@ -241,8 +240,11 @@ void redraw_screen(int drawing_different_room) { //remove_flash(); if (drawing_different_room) { draw_rect(&rect_top, color_0_black); -#ifdef USE_DARK_TRANSITION +#if defined(USE_DARK_TRANSITION) && !defined(__GBA__) // Briefly show a dark screen when changing rooms, like in the original game. + // Skipped on GBA: the 100ms black flash + SDL_Delay reads as the screen + // going black on every room change; the new room is drawn immediately + // below and presented on the same frame instead. update_screen(); SDL_Delay(100); #endif diff --git a/src/seg008.c b/src/seg008.c index aabdb341..7c2bf2e1 100644 --- a/src/seg008.c +++ b/src/seg008.c @@ -847,7 +847,7 @@ int add_midtable(short chtab_id, int id, sbyte xh, sbyte xl, int ybottom, int bl return 0; } word index = midtable_count; - if (index >= 50) { + if (index >= 128) { // GBA: array bumped to 128 show_dialog("MidTable Overflow"); return 0; // added } @@ -880,7 +880,7 @@ int add_midtable(short chtab_id, int id, sbyte xh, sbyte xl, int ybottom, int bl // seg008:1208 void add_peel(int left,int right,int top,int height) { rect_type rect; - if (peels_count >= 50) { + if (peels_count >= 128) { // GBA: array bumped to 128 show_dialog("Peels OverFlow"); return /*0*/; // added } @@ -895,7 +895,7 @@ void add_peel(int left,int right,int top,int height) { // seg008:1254 void add_wipetable(sbyte layer,short left,short bottom,sbyte height,short width,sbyte color) { word index = wipetable_count; - if (index >= 300) { + if (index >= 512) { // GBA: array bumped to 512 show_dialog("WipeTable Overflow"); return /*0*/; // added } @@ -956,13 +956,53 @@ void draw_back_fore(int which_table,int index) { } +#ifdef __GBA__ +/* GBA hflip: the upstream version calls SDL_ConvertSurface() on a ROM-backed + surface and then issues one SDL_BlitSurface() per column — for a compressed + sprite that re-decodes the whole image `width` times, and the colorkey + toggling leaves the background opaque. On the slow ARM7 that mirror loop is + what froze the game the instant the prince turned to face right (the first + time any sprite is flipped). Instead we decode the source once and write the + columns out reversed in a single pass, preserving the raw 0..15 pixel values + (0 = transparent) and the palette-row offset so the normal transparent blit + in method_6_blit_img_to_scr draws it correctly. */ +extern uint8_t* gba_decode_compressed_surface(SDL_Surface* src); +SDL_Surface* hflip(SDL_Surface* input) { + int w = input->w, h = input->h; + SDL_Surface* output = SDL_CreateRGBSurface(0, w, h, 8, 0, 0, 0, 0); + if (output == NULL) return input; /* degrade to unflipped rather than crash */ + + const uint8_t* src; + int spitch; + if (input->gba_rom_compressed) { + src = gba_decode_compressed_surface(input); /* w*h raw pixels in scratch */ + if (src == NULL) { SDL_FreeSurface(output); return input; } + spitch = w; + } else { + src = (const uint8_t*)input->pixels; /* raw 8bpp in ROM */ + spitch = input->pitch; + } + + for (int y = 0; y < h; ++y) { + const uint8_t* sp = src + (size_t)y * spitch; + uint8_t* dp = (uint8_t*)output->pixels + (size_t)y * output->pitch; + for (int x = 0; x < w; ++x) dp[w - 1 - x] = sp[x]; + } + + output->gba_palette_offset = input->gba_palette_offset; + output->gba_rom_compressed = 0; + output->has_colorkey = 1; + output->colorkey = 0; + return output; +} +#else SDL_Surface* hflip(SDL_Surface* input) { int width = input->w; int height = input->h; // The simplest way to create a surface with same format as input: SDL_Surface* output = SDL_ConvertSurface(input, input->format, 0); - SDL_SetSurfacePalette(output, input->format->palette); + SDL_SetSurfacePalette(output, SURFACE_PALETTE(input)); // The copied image will be overwritten anyway. if (output == NULL) { sdlperror("hflip: SDL_ConvertSurface"); @@ -986,6 +1026,7 @@ SDL_Surface* hflip(SDL_Surface* input) { return output; } +#endif // seg008:140C @@ -1030,6 +1071,16 @@ void draw_mid(int index) { add_peel(round_xpos_to_byte(xpos, 0), round_xpos_to_byte(image->w/*width*/ + xpos, 1), ypos, image->h/*height*/); } //printf("Midtable: drawing (chtab %d, image %d) at (x=%d, y=%d)\n",chtab_id,image_id,xpos,ypos); // debug +#ifdef __GBA__ + /* Record the prince's real drawn pixel position so the GBA camera can + centre the 240x160 viewport on him (Char.x is not a buffer pixel x). */ + if (chtab_id == id_chtab_2_kid && image != NULL) { + extern int gba_kid_px, gba_kid_py, gba_kid_valid; + gba_kid_px = xpos + image->w / 2; + gba_kid_py = ypos + image->h / 2; + gba_kid_valid = 1; + } +#endif draw_image(image, mask, xpos, ypos, blit); if (chtab_flip_clip[chtab_id]) { @@ -1406,7 +1457,7 @@ void add_drect(rect_type *source) { return; } } - if (drects_count >= 30){ + if (drects_count >= 128){ // GBA: array bumped to 128 show_dialog("DRects Overflow"); return /*0*/; // added } @@ -1697,7 +1748,7 @@ void add_objtable(byte obj_type) { //printf("in add_objtable: objtable_count = %d\n",objtable_count); // debug word index = objtable_count++; //printf("in add_objtable: objtable_count = %d\n",objtable_count); // debug - if (index >= 50) { + if (index >= 128) { // GBA: array bumped to 128 show_dialog("ObjTable Overflow"); return /*0*/; // added } diff --git a/src/seg009.c b/src/seg009.c index a6c496e7..e0e3202b 100644 --- a/src/seg009.c +++ b/src/seg009.c @@ -336,8 +336,13 @@ int round_xpos_to_byte(int xpos,int round_direction) { // seg009:0C7A void quit(int exit_code) { +#ifdef __GBA__ + extern void gba_panic(unsigned char color_idx); + gba_panic(exit_code ? 1 : 2); +#else restore_stuff(); exit(exit_code); +#endif } // seg009:0C90 @@ -466,11 +471,25 @@ dat_type* open_dat(const char* filename, int optional) { if (fp != NULL) { if (fread(&dat_header, 6, 1, fp) != 1) goto failed; +#ifdef __GBA__ + /* Point dat_table directly into ROM instead of malloc+fread. Saves + ~1.7 KB per open DAT (and avoids a malloc that fails late in + the boot sequence when EWRAM is fragmented). The freed-up RAM + is exactly what was running out for VDUNGEON / KID. */ + if (fseek(fp, SDL_SwapLE32(dat_header.table_offset), SEEK_SET)) goto failed; + extern const uint8_t* gba_file_rom_ptr(FILE* fp, size_t* out_avail); + size_t avail = 0; + const uint8_t* rom = gba_file_rom_ptr(fp, &avail); + if (rom == NULL || (int)avail < SDL_SwapLE16(dat_header.table_size)) + goto failed; + dat_table = (dat_table_type*)rom; +#else dat_table = (dat_table_type*) malloc(SDL_SwapLE16(dat_header.table_size)); if (dat_table == NULL || fseek(fp, SDL_SwapLE32(dat_header.table_offset), SEEK_SET) || fread(dat_table, SDL_SwapLE16(dat_header.table_size), 1, fp) != 1) goto failed; +#endif pointer->handle = fp; pointer->dat_table = dat_table; } else if (optional == 0) { @@ -562,8 +581,24 @@ chtab_type* load_sprites_from_file(int resource,int palette_bits, int quit_on_er int n_images = shpl->n_images; size_t alloc_size = sizeof(chtab_type) + sizeof(void *) * n_images; chtab_type* chtab = (chtab_type*) malloc(alloc_size); + if (chtab == NULL) { + free(shpl); + return NULL; + } memset(chtab, 0, alloc_size); chtab->n_images = n_images; +#ifdef __GBA__ + /* Pre-compute the palette-row offset for this chtab so decode_image can + bake `row*16 + v` into each sprite pixel value. Our 8bpp blits then + write straight into the global palette[]'s correct chtab slot, where + set_loaded_palette has just installed the colours. */ + extern int gba_decode_palette_offset; + { + int rb = palette_bits, row = 0; + while (rb && !(rb & 1)) { rb >>= 1; ++row; } + gba_decode_palette_offset = rb ? (row * 16) : 0; + } +#endif for (int i = 1; i <= n_images; i++) { SDL_Surface* image = load_image(resource + i, pal_ptr); // if (image == NULL) printf(" failed"); @@ -584,7 +619,28 @@ chtab_type* load_sprites_from_file(int resource,int palette_bits, int quit_on_er // printf("\n"); chtab->images[i-1] = image; } +#ifdef __GBA__ + gba_decode_palette_offset = 0; + /* On GBA, pal_ptr lives in cartridge ROM so the earlier + `pal_ptr->row_bits = palette_bits` write was a no-op; the global + set_loaded_palette() would then iterate against a zero row mask and + never load this chtab's colours. Inline the row-walk here using the + local `palette_bits` value so the chtab's 16 vga[] colours actually + get installed into the global palette[]. */ + { + int row_bits = palette_bits; + int dest_index = 0; + int source_row = 0; + for (int dest_row = 0; dest_row < 16; ++dest_row, dest_index += 16) { + if (row_bits & (1 << dest_row)) { + set_pal_arr(dest_index, 16, pal_ptr->vga + source_row * 16); + ++source_row; + } + } + } +#else set_loaded_palette(pal_ptr); +#endif return chtab; } @@ -818,6 +874,7 @@ int calc_stride(image_data_type* image_data) { byte* conv_to_8bpp(byte* in_data, int width, int height, int stride, int depth) { byte* out_data = (byte*) malloc(width * height); + if (out_data == NULL) return NULL; int pixels_per_byte = 8 / depth; int mask = (1 << depth) - 1; for (int y = 0; y < height; ++y) { @@ -844,17 +901,63 @@ image_type* decode_image(image_data_type* image_data, dat_pal_type* palette) { int flags = SDL_SwapLE16(image_data->flags); int depth = ((flags >> 12) & 7) + 1; int cmeth = (flags >> 8) & 0x0F; +#ifdef __GBA__ + /* GBA: don't allocate a pixel buffer. Either point straight at the + pre-decoded 8bpp pixels in ROM (built by tools/preproc_dat) or, for + the few sprites that didn't get preprocessed (mostly the built-in + hc_font_data glyphs), stash the original image_data_type pointer + and let the blit decode on demand. Saves ~1 KB per sprite + the + big static scratch the legacy decoder used. */ + { + extern int gba_decode_palette_offset; + extern image_type* gba_new_rom_surface(int w, int h, int bpp); + /* Struct-only surface; ->pixels is pointed at the sprite data fixed in + ROM below. No throwaway width*height buffer is allocated per sprite + (that used to fragment the heap and stall level loading). */ + image_type* image = gba_new_rom_surface(width, height, 8); + if (image == NULL) return NULL; + if (cmeth == 0 && depth == 8) { + /* Pre-decoded: pixels point at raw 8bpp data right after the + 6-byte image_data_type header. Blits read directly from ROM. */ + image->pixels = (void*)((byte*)image_data + sizeof(image_data_type)); + image->pitch = width; + image->gba_rom_compressed = 0; + } else { + /* Legacy compressed path. */ + image->pixels = (void*)image_data; + image->pitch = width; + image->gba_rom_compressed = 1; + } + image->gba_palette_offset = (Uint8)gba_decode_palette_offset; + image->gba_depth = (Uint8)depth; + image->gba_cmeth = (Uint8)cmeth; + + SDL_Color colors[16]; + for (int i = 0; i < 16; ++i) { + colors[i].r = palette->vga[i].r << 2; + colors[i].g = palette->vga[i].g << 2; + colors[i].b = palette->vga[i].b << 2; + colors[i].a = SDL_ALPHA_OPAQUE; + } + colors[0].r = 0; colors[0].g = 0; colors[0].b = 0; + colors[0].a = SDL_ALPHA_TRANSPARENT; + SDL_SetPaletteColors(SURFACE_PALETTE(image), colors, 0, 16); + return image; + } +#else int stride = calc_stride(image_data); int dest_size = stride * height; byte* dest = (byte*) malloc(dest_size); + if (dest == NULL) return NULL; memset(dest, 0, dest_size); decompr_img(dest, image_data, dest_size, cmeth, stride); byte* image_8bpp = conv_to_8bpp(dest, width, height, stride, depth); free(dest); dest = NULL; + if (image_8bpp == NULL) return NULL; image_type* image = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); if (image == NULL) { - sdlperror("decode_image: SDL_CreateRGBSurface"); - quit(1); + free(image_8bpp); + return NULL; } if (SDL_LockSurface(image) != 0) { sdlperror("decode_image: SDL_LockSurface"); @@ -871,16 +974,13 @@ image_type* decode_image(image_data_type* image_data, dat_pal_type* palette) { colors[i].r = palette->vga[i].r << 2; colors[i].g = palette->vga[i].g << 2; colors[i].b = palette->vga[i].b << 2; - colors[i].a = SDL_ALPHA_OPAQUE; // SDL2's SDL_Color has a fourth alpha component + colors[i].a = SDL_ALPHA_OPAQUE; } - // Force 0th color to be black for non-transparent blitters. (hitpoints, shadow) - // This is needed to remove the colored rectangles around hitpoints and the shadow, when using Brain's SNES graphics for example. - colors[0].r = 0; - colors[0].g = 0; - colors[0].b = 0; + colors[0].r = 0; colors[0].g = 0; colors[0].b = 0; colors[0].a = SDL_ALPHA_TRANSPARENT; - SDL_SetPaletteColors(image->format->palette, colors, 0, 16); // SDL_SetColors = deprecated + SDL_SetPaletteColors(SURFACE_PALETTE(image), colors, 0, 16); return image; +#endif /* __GBA__ */ } // seg009:121A @@ -952,6 +1052,40 @@ void draw_image_transp(image_type* image,image_type* mask,int xpos,int ypos) { // seg009:157E int set_joy_mode() { // stub +#ifdef USE_SDL3 + // SDL3 dropped the index-based joystick/gamepad/haptic open calls; query the + // device lists and open the first one instead. + int num_joysticks = 0; + SDL_JoystickID* joystick_ids = SDL_GetJoysticks(&num_joysticks); + if (num_joysticks < 1) { + is_joyst_mode = 0; + } else { + if (gamecontrollerdb_file[0] != '\0') { + SDL_AddGamepadMappingsFromFile(gamecontrollerdb_file); + } + SDL_JoystickID first = joystick_ids[0]; + if (SDL_IsGamepad(first)) { + sdl_controller_ = SDL_OpenGamepad(first); + is_joyst_mode = (sdl_controller_ != NULL) ? 1 : 0; + } + // Not compatible with the gamepad interface: fall back to the joystick interface. + else { + sdl_joystick_ = SDL_OpenJoystick(first); + is_joyst_mode = 1; + using_sdl_joystick_interface = 1; + } + } + if (joystick_ids != NULL) SDL_free(joystick_ids); + if (enable_controller_rumble && is_joyst_mode) { + int num_haptics = 0; + SDL_HapticID* haptic_ids = SDL_GetHaptics(&num_haptics); + sdl_haptic = (num_haptics > 0) ? SDL_OpenHaptic(haptic_ids[0]) : NULL; + if (sdl_haptic != NULL) SDL_InitHapticRumble(sdl_haptic); + if (haptic_ids != NULL) SDL_free(haptic_ids); + } else { + sdl_haptic = NULL; + } +#else if (SDL_NumJoysticks() < 1) { is_joyst_mode = 0; } else { @@ -981,6 +1115,7 @@ int set_joy_mode() { } else { sdl_haptic = NULL; } +#endif is_keyboard_mode = !is_joyst_mode; return is_joyst_mode; @@ -989,7 +1124,16 @@ int set_joy_mode() { // seg009:178B surface_type* make_offscreen_buffer(const rect_type* rect) { // stub -#ifndef USE_ALPHA +#ifdef __GBA__ + /* On GBA we can't afford a second 62 KB framebuffer. Reuse + onscreen_surface_ — bump its refcount so SDLPoP can free_surface() + it like an independent buffer. Drawing happens directly on the + buffer that gba_present_surface() reads, so fade-by-blit effects + become no-ops, but the game runs and shows content. */ + (void)rect; + onscreen_surface_->refcount++; + return onscreen_surface_; +#elif !defined(USE_ALPHA) // Bit order matches onscreen buffer, good for fading. return SDL_CreateRGBSurface(0, rect->right, rect->bottom, 24, Rmsk, Gmsk, Bmsk, 0); #else @@ -1222,6 +1366,15 @@ font_type load_font_from_data(/*const*/ rawfont_type* data) { extern byte hc_small_font_data[]; void load_font(void) { +#ifdef __GBA__ + /* Use the build-time pre-decompressed font (tools/predecomp_font + bin2c, + generated into gba/src/hc_font_decoded.c). Every glyph is already + depth=8/cmeth=0, so decode_image() takes its no-decode fast path and the + per-blit decoder (gba_decode_compressed_surface) is never invoked while + drawing text. */ + extern const unsigned char hc_font_decoded[]; + hc_font = load_font_from_data((rawfont_type*)hc_font_decoded); +#else // Try to load font from a file. dat_type* dathandle = open_dat("font", 1); hc_font.chtab = load_sprites_from_file(1000, 1<<1, 0); @@ -1230,6 +1383,7 @@ void load_font(void) { // Use built-in font. hc_font = load_font_from_data((/*const*/ rawfont_type*)hc_font_data); } +#endif #ifdef USE_MENU hc_small_font = load_font_from_data((rawfont_type*)hc_small_font_data); @@ -1582,9 +1736,22 @@ void dialog_method_2_frame(dialog_type* dialog) { // seg009:0C44 void show_dialog(const char* text) { +#ifdef __GBA__ + /* show_dialog is only used for non-fatal error notices (mostly the + draw-table overflow checks in seg008). The overflowing entry has + already been dropped by the caller's early return, so there is nothing + to acknowledge. On GBA showmessage()'s "press any key" wait loop would + freeze the game forever — a held movement key never satisfies + key_test_quit(), which is exactly what hung the port on the first move. + Log it and keep running. */ + extern void gba_log(const char*, ...); + gba_log("show_dialog (dropped): %s", text); + return; +#else char string[256]; snprintf(string, sizeof(string), "%s\n\nPress any key to continue.", text); showmessage(string, 1, &key_test_quit); +#endif } // seg009:0791 @@ -1801,7 +1968,14 @@ peel_type* read_peel_from_screen(const rect_type* rect) { peel_type* result = calloc(1, sizeof(peel_type)); //memset(&result, 0, sizeof(result)); result->rect = *rect; -#ifndef USE_ALPHA +#ifdef __GBA__ + /* The GBA backbuffer is 8bpp indexed; matching that here saves ~27 KB + per peel vs. the desktop 24bpp variant, which is the difference + between fitting init_copyprot_dialog + sprite loading in EWRAM or not. */ + SDL_Surface* peel_surface = SDL_CreateRGBSurface(0, rect->right - rect->left, + rect->bottom - rect->top, 8, + 0, 0, 0, 0); +#elif !defined(USE_ALPHA) SDL_Surface* peel_surface = SDL_CreateRGBSurface(0, rect->right - rect->left, rect->bottom - rect->top, 24, Rmsk, Gmsk, Bmsk, 0); #else @@ -1854,6 +2028,7 @@ enum userevents { userevent_TIMER, }; +#ifndef __GBA__ short speaker_playing = 0; short digi_playing = 0; short midi_playing = 0; @@ -2118,6 +2293,13 @@ void audio_callback(void* userdata, Uint8* stream_orig, int len_orig) { memset(stream_orig, digi_audiospec->silence, len_orig); #else #ifdef FAST_FORWARD_RESAMPLE_SOUND +#ifdef USE_SDL3 + // SDL3 replaced SDL_AudioCVT with one-shot SDL_ConvertAudioSamples; the + // resample is done in sdl2_to_sdl3.c (where the real SDL_AudioSpec lives). + compat_SDL_AudioResampleFF(digi_audiospec->format, digi_audiospec->channels, + digi_audiospec->freq * audio_speed, digi_audiospec->freq, + stream, len, stream_orig, len_orig); +#else static SDL_AudioCVT cvt; static bool cvt_initialized = false; if (!cvt_initialized) { @@ -2139,6 +2321,7 @@ void audio_callback(void* userdata, Uint8* stream_orig, int len_orig) { memcpy(stream_orig, cvt.buf, len_orig); free(cvt.buf); cvt.buf = NULL; +#endif #else // Hack: use the beginning of the buffer instead of resampling. memcpy(stream_orig, stream, len_orig); @@ -2478,7 +2661,9 @@ void turn_sound_on_off(byte new_state) { int check_sound_playing() { return speaker_playing || digi_playing || midi_playing || ogg_playing; } +#endif // !__GBA__ +#ifndef __GBA__ void apply_aspect_ratio() { // Allow us to use a consistent set of screen co-ordinates, even if the screen size changes if (use_correct_aspect_ratio) { @@ -2537,6 +2722,10 @@ void init_scaling(void) { int access = is_renderer_targettexture_supported ? SDL_TEXTUREACCESS_TARGET : SDL_TEXTUREACCESS_STREAMING; texture_fuzzy = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGB24, access, 320*2, 200*2); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); +#ifdef USE_SDL3 + // SDL3 ignores SDL_HINT_RENDER_SCALE_QUALITY; set the scale mode per-texture. + if (texture_fuzzy != NULL) SDL_SetTextureScaleMode(texture_fuzzy, SDL_SCALEMODE_LINEAR); +#endif } target_texture = texture_fuzzy; } else if (scaling_type == 2) { @@ -2544,6 +2733,9 @@ void init_scaling(void) { SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); texture_blurry = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 320, 200); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); +#ifdef USE_SDL3 + if (texture_blurry != NULL) SDL_SetTextureScaleMode(texture_blurry, SDL_SCALEMODE_LINEAR); +#endif } target_texture = texture_blurry; } else { @@ -2613,12 +2805,18 @@ void set_gr_mode(byte grmode) { default: break; } renderer_ = SDL_CreateRenderer(window_, -1 , flags | SDL_RENDERER_TARGETTEXTURE); +#ifdef USE_SDL3 + // SDL3 removed SDL_GetRendererInfo/SDL_RendererInfo; accelerated renderers + // support render targets, so assume it's available. + is_renderer_targettexture_supported = true; +#else SDL_RendererInfo renderer_info; if (SDL_GetRendererInfo(renderer_, &renderer_info) == 0) { if (renderer_info.flags & SDL_RENDERER_TARGETTEXTURE) { is_renderer_targettexture_supported = true; } } +#endif if (use_integer_scaling) { #if SDL_VERSION_ATLEAST(2,0,5) // SDL_RenderSetIntegerScale SDL_RenderSetIntegerScale(renderer_, SDL_TRUE); @@ -2790,6 +2988,33 @@ void update_screen() { SDL_RenderCopy(renderer_, target_texture, NULL, NULL); SDL_RenderPresent(renderer_); } +#else /* __GBA__ */ +void apply_aspect_ratio(void) { } +void window_resized(void) { } +void init_overlay(void) { } +void init_scaling(void) { } +void set_gr_mode(byte grmode) { + (void)grmode; + SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); + onscreen_surface_ = SDL_CreateRGBSurface(0, 320, 192, 8, 0, 0, 0, 0); + if (!onscreen_surface_) quit(1); + graphics_mode = gmMcgaVga; +#ifdef USE_TEXT + /* Without this, hc_font.chtab is NULL, and the very first draw_text in + show_loading dereferences font->chtab->images[c], reading garbage from + BIOS and then iterating a huge image in method_3_blit_mono. */ + load_font(); +#endif +} +SDL_Surface* get_final_surface(void) { return onscreen_surface_; } +void draw_overlay(void) { } +void update_screen(void) { + extern void gba_camera_update(void); + extern int gba_present_surface(SDL_Surface*); + gba_camera_update(); + gba_present_surface(onscreen_surface_); +} +#endif /* __GBA__ */ // seg009:9289 void set_pal_arr(int start,int count,const rgb_type* array) { @@ -2967,6 +3192,20 @@ void *load_from_opendats_alloc(int resource, const char* extension, data_locatio if (out_result != NULL) *out_result = result; if (out_size != NULL) *out_size = size; if (result == data_none) return NULL; +#ifdef __GBA__ + /* DAT data already lives in cartridge ROM. Return a pointer into ROM + instead of malloc+fread'ing a copy: each chtab load would otherwise + duplicate every sprite into EWRAM (~150 KB just for kid sprites, + blowing the 158 KB heap). Callers must free with gba_free_safe(). */ + if (result == data_DAT) { + extern const uint8_t* gba_file_rom_ptr(FILE* fp, size_t* out_avail); + size_t avail = 0; + const uint8_t* rom = gba_file_rom_ptr(fp, &avail); + if (rom != NULL && (int)avail >= size) { + return (void*)rom; + } + } +#endif void* area = malloc(size); //read(fd, area, size); if (fread(area, size, 1, fp) != 1) { @@ -2992,6 +3231,20 @@ int load_from_opendats_to_area(int resource,void* area,int length, const char* e FILE* fp = NULL; load_from_opendats_metadata(resource, extension, &fp, &result, &checksum, &size, &pointer); if (result == data_none) return 0; +#ifdef __GBA__ + /* No fread: copy directly from the ROM-resident DAT bytes. */ + if (result == data_DAT) { + extern const uint8_t* gba_file_rom_ptr(FILE* fp, size_t* out_avail); + size_t avail = 0; + const uint8_t* rom = gba_file_rom_ptr(fp, &avail); + int n = MIN(size, length); + if (rom != NULL && (int)avail >= n) { + memcpy(area, rom, n); + } else { + memset(area, 0, n); + } + } +#else if (fread(area, MIN(size, length), 1, fp) != 1) { fprintf(stderr, "%s: %s, resource %d, size %d, failed: %s\n", __func__, pointer->filename, resource, @@ -2999,6 +3252,7 @@ int load_from_opendats_to_area(int resource,void* area,int length, const char* e memset(area, 0, MIN(size, length)); } if (result == data_directory) fclose(fp); +#endif /* XXX: check checksum */ return 0; } @@ -3040,6 +3294,51 @@ void method_1_blit_rect(surface_type* target_surface,surface_type* source_surfac image_type* method_3_blit_mono(image_type* image,int xpos,int ypos,int blitter,byte color) { int w = image->w; int h = image->h; +#ifdef __GBA__ + /* GBA fast path: both image (font glyph) and current_target_surface are + 8bpp indexed. The desktop path converts to ARGB8888 just to recolor, + then relies on SDL's 32->8bpp blit with the dest palette to map back + to indices. We have no destination palette, so write the requested + palette index directly. Any non-zero source pixel becomes "color"; + zero stays transparent. + Sources marked gba_rom_compressed hold a ROM image_data_type pointer + in pixels (decode_image deferred the unpack to save RAM); decode into + the shared scratch first or text/mono blits render struct headers as + gibberish. */ + const byte* src_pixels; + int src_pitch; + if (image->gba_rom_compressed) { + extern uint8_t* gba_decode_compressed_surface(SDL_Surface*); + uint8_t* unpacked = gba_decode_compressed_surface(image); + if (unpacked == NULL) return image; + src_pixels = unpacked; + src_pitch = w; + } else { + src_pixels = (const byte*)image->pixels; + src_pitch = image->pitch; + } + /* Clamp the iteration to the on-screen region. A glyph/sprite with a + corrupted huge w/h (seen on the kid's first move: a bad font image made + this loop run ~w*h≈billions of times = a hard freeze inside + draw_text_character) would otherwise spin even though every write is + bounds-checked. Iterating only the visible rows/cols caps the work at + the surface size no matter how broken the dimensions are. */ + int surf_w = current_target_surface->w; + int surf_h = current_target_surface->h; + int y0 = (ypos < 0) ? -ypos : 0; + int y1 = h; if (y1 > surf_h - ypos) y1 = surf_h - ypos; + int x0 = (xpos < 0) ? -xpos : 0; + int x1 = w; if (x1 > surf_w - xpos) x1 = surf_w - xpos; + for (int y = y0; y < y1; ++y) { + const byte* sp = src_pixels + (size_t)y * src_pitch; + byte* dp = (byte*)current_target_surface->pixels + + (size_t)(ypos + y) * current_target_surface->pitch + xpos; + for (int x = x0; x < x1; ++x) { + if (sp[x] != 0) dp[x] = color; + } + } + return image; +#else if (SDL_SetColorKey(image, SDL_TRUE, 0) != 0) { sdlperror("method_3_blit_mono: SDL_SetColorKey"); quit(1); @@ -3086,6 +3385,7 @@ image_type* method_3_blit_mono(image_type* image,int xpos,int ypos,int blitter,b SDL_FreeSurface(colored_image); return image; +#endif /* __GBA__ */ } // Workaround for a bug in SDL2 (before v2.0.4): @@ -3096,6 +3396,11 @@ bool RGB24_bug_checked = false; bool RGB24_bug_affected; bool RGB24_bug_check(void) { +#ifdef USE_SDL3 + // This is an old SDL2-specific SDL_FillRect bug; it does not occur on SDL3 + // (and test_surface->format->Rmask no longer exists). Never affected. + return false; +#else if (!RGB24_bug_checked) { // Check if the bug occurs in this version of SDL. SDL_Surface* test_surface = SDL_CreateRGBSurface(0, 1, 1, 24, 0, 0, 0, 0); @@ -3110,10 +3415,11 @@ bool RGB24_bug_check(void) { RGB24_bug_checked = true; } return RGB24_bug_affected; +#endif } int safe_SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color) { - if (dst->format->BitsPerPixel == 24 && RGB24_bug_check()) { + if (SURFACE_BPP(dst) == 24 && RGB24_bug_check()) { // In the buggy version, SDL_FillRect swaps R and B, so we swap it once more. color = ((color & 0xFF) << 16) | (color & 0xFF00) | ((color & 0xFF0000) >> 16); } @@ -3124,6 +3430,15 @@ int safe_SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color) { const rect_type* method_5_rect(const rect_type* rect,int blit,byte color) { SDL_Rect dest_rect; rect_to_sdlrect(rect, &dest_rect); +#ifdef __GBA__ + /* current_target_surface is the 8bpp screen buffer whose SDL_Palette is + empty (we drive the hardware palette from the global PoP palette[]), + so SDL_MapRGB would round-trip to index 0 for every input. The fill + value IS the PoP palette index — write it straight. */ + (void)blit; + SDL_FillRect(current_target_surface, &dest_rect, (Uint32)color); + return rect; +#else rgb_type palette_color = palette[color]; #ifndef USE_ALPHA uint32_t rgb_color = SDL_MapRGBA(current_target_surface->format, palette_color.r<<2, palette_color.g<<2, palette_color.b<<2, 0xFF); @@ -3135,23 +3450,54 @@ const rect_type* method_5_rect(const rect_type* rect,int blit,byte color) { quit(1); } return rect; +#endif } void draw_rect_with_alpha(const rect_type* rect, byte color, byte alpha) { SDL_Rect dest_rect; rect_to_sdlrect(rect, &dest_rect); +#ifdef __GBA__ + // The 8bpp screen surface can't alpha-blend. alpha==0 means "leave the + // background showing" (used so "GAME PAUSED" stays visible), so skip it; + // otherwise fill opaquely with the PoP palette index. The menu's dimming + // backdrop just becomes a solid panel — fine for a pause overlay. + if (alpha == 0) return; + SDL_FillRect(current_target_surface, &dest_rect, (Uint32)color); +#else rgb_type palette_color = palette[color]; uint32_t rgb_color = SDL_MapRGBA(overlay_surface->format, palette_color.r<<2, palette_color.g<<2, palette_color.b<<2, alpha); if (safe_SDL_FillRect(current_target_surface, &dest_rect, rgb_color) != 0) { sdlperror("draw_rect_with_alpha: SDL_FillRect"); quit(1); } +#endif } void draw_rect_contours(const rect_type* rect, byte color) { +#ifdef __GBA__ + // 8bpp screen: draw the 4 edges directly, writing the PoP palette index. + { + SDL_Rect r; + rect_to_sdlrect(rect, &r); + byte* pixels = (byte*)current_target_surface->pixels; + int pitch = current_target_surface->pitch; + int xmin = MAX(0, r.x), xmax = MIN(current_target_surface->w, r.x + r.w); + int ymin = MAX(0, r.y), ymax = MIN(current_target_surface->h, r.y + r.h); + if (xmax <= xmin || ymax <= ymin) return; + for (int x = xmin; x < xmax; ++x) { + pixels[(size_t)ymin * pitch + x] = color; + pixels[(size_t)(ymax - 1) * pitch + x] = color; + } + for (int y = ymin; y < ymax; ++y) { + pixels[(size_t)y * pitch + xmin] = color; + pixels[(size_t)y * pitch + (xmax - 1)] = color; + } + return; + } +#endif // TODO: handle 24 bit surfaces? (currently, 32 bit surface is assumed) - if (current_target_surface->format->BitsPerPixel != 32) { - printf("draw_rect_contours: not implemented for %d bit surfaces\n", current_target_surface->format->BitsPerPixel); + if (SURFACE_BPP(current_target_surface) != 32) { + printf("draw_rect_contours: not implemented for %d bit surfaces\n", SURFACE_BPP(current_target_surface)); return; } SDL_Rect dest_rect; @@ -3162,7 +3508,7 @@ void draw_rect_contours(const rect_type* rect, byte color) { sdlperror("draw_rect_contours: SDL_LockSurface"); quit(1); } - int bytes_per_pixel = current_target_surface->format->BytesPerPixel; + int bytes_per_pixel = SURFACE_BYTESPP(current_target_surface); int pitch = current_target_surface->pitch; byte* pixels = current_target_surface->pixels; int xmin = MIN(dest_rect.x, current_target_surface->w); @@ -3312,7 +3658,7 @@ image_type* method_6_blit_img_to_scr(image_type* image,int xpos,int ypos,int bli // Fix the background color of teleport images on SDL_image 2.6.2, where they are loaded as RGBA. // For transparency, paletted images need colorkeying, RGB(A) images need blending. if (blit == blitters_0_no_transp) { - if (SDL_ISPIXELFORMAT_INDEXED(image->format->format)) { + if (SDL_ISPIXELFORMAT_INDEXED(SURFACE_FMT_ENUM(image))) { SDL_SetColorKey(image, SDL_FALSE, 0); //printf("colorkey = SDL_FALSE\n"); } else { @@ -3321,7 +3667,7 @@ image_type* method_6_blit_img_to_scr(image_type* image,int xpos,int ypos,int bli } } else { - if (SDL_ISPIXELFORMAT_INDEXED(image->format->format)) { + if (SDL_ISPIXELFORMAT_INDEXED(SURFACE_FMT_ENUM(image))) { SDL_SetColorKey(image, SDL_TRUE, 0); //printf("colorkey = SDL_TRUE\n"); } else { @@ -3351,7 +3697,13 @@ int wait_time[NUM_TIMERS]; #ifdef USE_COMPAT_TIMER +#ifdef USE_SDL3 +// SDL3 changed the timer callback signature to (userdata, timerID, interval). +Uint32 SDLCALL timer_callback(void *param, SDL_TimerID timerID, Uint32 interval) { + (void)timerID; +#else Uint32 timer_callback(Uint32 interval, void *param) { +#endif SDL_Event event; memset(&event, 0, sizeof(event)); event.type = SDL_USEREVENT; @@ -3435,8 +3787,8 @@ void process_events() { switch (event.type) { case SDL_KEYDOWN: { - int modifier = event.key.keysym.mod; - int scancode = event.key.keysym.scancode; + int modifier = EVENT_KEY_MOD(event); + int scancode = EVENT_KEY_SCANCODE(event); // Handle these separately, so they won't interrupt things that are usually interrupted by a keypress. (pause, cutscene) #ifdef USE_FAST_FORWARD @@ -3493,7 +3845,10 @@ void process_events() { case SDL_SCANCODE_VOLUMEDOWN: // Why are there two mute key codes? case SDL_SCANCODE_MUTE: +#ifndef USE_SDL3 + // SDL3 makes SDL_SCANCODE_AUDIOMUTE an alias of SDL_SCANCODE_MUTE (duplicate case). case SDL_SCANCODE_AUDIOMUTE: +#endif case SDL_SCANCODE_PAUSE: break; @@ -3537,32 +3892,32 @@ void process_events() { } case SDL_KEYUP: // If Alt was held down from Alt+Tab but now it's released: stop ignoring Tab. - if (event.key.keysym.scancode == SDL_SCANCODE_TAB && ignore_tab) ignore_tab = false; + if (EVENT_KEY_SCANCODE(event) == SDL_SCANCODE_TAB && ignore_tab) ignore_tab = false; #ifdef USE_FAST_FORWARD - if (event.key.keysym.scancode == SDL_SCANCODE_GRAVE) { + if (EVENT_KEY_SCANCODE(event) == SDL_SCANCODE_GRAVE) { init_timer(BASE_FPS); // fast-forward off audio_speed = 1; break; } #endif - key_states[event.key.keysym.scancode] &= ~KEYSTATE_HELD; + key_states[EVENT_KEY_SCANCODE(event)] &= ~KEYSTATE_HELD; #ifdef USE_MENU // Prevent repeated keystrokes opening/closing the menu as long as the key is held down. - if (event.key.keysym.scancode == SDL_SCANCODE_BACKSPACE || event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { + if (EVENT_KEY_SCANCODE(event) == SDL_SCANCODE_BACKSPACE || EVENT_KEY_SCANCODE(event) == SDL_SCANCODE_ESCAPE) { escape_key_suppressed = false; } #endif break; case SDL_CONTROLLERAXISMOTION: - if (event.caxis.axis < 6) { - joy_axis[event.caxis.axis] = event.caxis.value; - if (abs(event.caxis.value) > abs(joy_axis_max[event.caxis.axis])) - joy_axis_max[event.caxis.axis] = event.caxis.value; + if (EVENT_GP_AXIS(event) < 6) { + joy_axis[EVENT_GP_AXIS(event)] = EVENT_GP_VALUE(event); + if (abs(EVENT_GP_VALUE(event)) > abs(joy_axis_max[EVENT_GP_AXIS(event)])) + joy_axis_max[EVENT_GP_AXIS(event)] = EVENT_GP_VALUE(event); #ifdef USE_AUTO_INPUT_MODE - if (!is_joyst_mode && (event.caxis.value >= joystick_threshold || event.caxis.value <= -joystick_threshold)) { + if (!is_joyst_mode && (EVENT_GP_VALUE(event) >= joystick_threshold || EVENT_GP_VALUE(event) <= -joystick_threshold)) { is_joyst_mode = 1; is_keyboard_mode = 0; } @@ -3570,7 +3925,7 @@ void process_events() { } break; case SDL_CONTROLLERDEVICEADDED: - SDL_GameControllerOpen(event.cdevice.which); + SDL_GameControllerOpen(EVENT_GP_WHICH(event)); if (gamecontrollerdb_file[0] != '\0') { SDL_GameControllerAddMappingsFromFile(gamecontrollerdb_file); } @@ -3578,23 +3933,23 @@ void process_events() { using_sdl_joystick_interface = 0; break; case SDL_CONTROLLERDEVICEREMOVED: - if (sdl_controller_ == SDL_GameControllerFromInstanceID(event.cdevice.which)) { + if (sdl_controller_ == SDL_GameControllerFromInstanceID(EVENT_GP_WHICH(event))) { sdl_controller_ = NULL; is_joyst_mode = 0; is_keyboard_mode = 1; } - SDL_GameControllerClose(SDL_GameControllerFromInstanceID(event.cdevice.which)); + SDL_GameControllerClose(SDL_GameControllerFromInstanceID(EVENT_GP_WHICH(event))); break; case SDL_CONTROLLERBUTTONDOWN: //Make sure sdl_controller_ always points to the active controller - sdl_controller_ = SDL_GameControllerFromInstanceID(event.cdevice.which); + sdl_controller_ = SDL_GameControllerFromInstanceID(EVENT_GP_WHICH(event)); #ifdef USE_AUTO_INPUT_MODE if (!is_joyst_mode) { is_joyst_mode = 1; is_keyboard_mode = 0; } #endif - switch (event.cbutton.button) + switch (EVENT_GP_BUTTON(event)) { case SDL_CONTROLLER_BUTTON_DPAD_LEFT: joy_button_states[JOYINPUT_DPAD_LEFT] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; break; // left case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: joy_button_states[JOYINPUT_DPAD_RIGHT] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; break; // right @@ -3608,9 +3963,9 @@ void process_events() { case SDL_CONTROLLER_BUTTON_START: case SDL_CONTROLLER_BUTTON_BACK: - if(event.cbutton.button == SDL_CONTROLLER_BUTTON_START) + if(EVENT_GP_BUTTON(event) == SDL_CONTROLLER_BUTTON_START) joy_button_states[JOYINPUT_START] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; - else if(event.cbutton.button == SDL_CONTROLLER_BUTTON_BACK) + else if(EVENT_GP_BUTTON(event) == SDL_CONTROLLER_BUTTON_BACK) joy_button_states[JOYINPUT_BACK] |= KEYSTATE_HELD | KEYSTATE_HELD_NEW; #ifdef USE_MENU last_key_scancode = SDL_SCANCODE_BACKSPACE; /*** bring up pause menu ***/ @@ -3623,7 +3978,7 @@ void process_events() { } break; case SDL_CONTROLLERBUTTONUP: - switch (event.cbutton.button) + switch (EVENT_GP_BUTTON(event)) { case SDL_CONTROLLER_BUTTON_DPAD_LEFT: joy_button_states[JOYINPUT_DPAD_LEFT] &= ~KEYSTATE_HELD; break; // left case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: joy_button_states[JOYINPUT_DPAD_RIGHT] &= ~KEYSTATE_HELD; break; // right @@ -3699,6 +4054,32 @@ void process_events() { } break; +#ifdef USE_SDL3 + // SDL3 has no SDL_WINDOWEVENT wrapper: window events are top-level types. +#ifdef __amigaos4__ + case SDL_EVENT_WINDOW_MINIMIZED: /* pause game */ + if (!is_menu_shown) { + last_key_scancode = SDL_SCANCODE_BACKSPACE; + } + break; + case SDL_EVENT_WINDOW_RESTORED: /* show "game paused/menu" */ + update_screen(); + break; +#endif + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + window_resized(); + // fallthrough! + case SDL_EVENT_WINDOW_EXPOSED: + update_screen(); + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + // Fix for this bug: When playing back a recording, Alt+Tabbing back to SDLPoP stops the replay if Alt is released before Tab. + { // If Alt is held down from Alt+Tab: ignore it until it's released. + const bool *state = SDL_GetKeyboardState(NULL); + if (state[SDL_SCANCODE_TAB]) ignore_tab = true; + } + break; +#else case SDL_WINDOWEVENT: // In case the user switches away while holding a key: do as if all keys were released. // (DOSBox does the same.) @@ -3743,6 +4124,7 @@ void process_events() { break; } break; +#endif case SDL_USEREVENT: if (event.user.code == userevent_TIMER /*&& event.user.data1 == (void*)timer_index*/) { #ifdef USE_COMPAT_TIMER @@ -3801,6 +4183,12 @@ void do_simple_wait(int timer_index) { if ((replaying && skipping_replay) || is_validate_mode) return; #endif update_screen(); + // Always pump events at least once per frame. On slow targets (e.g. the + // GBA) a frame's logic+render can overrun the timer budget, so + // has_timer_stopped() returns true on the first check and the loop body + // below never runs. Since this is the only process_events() call in the + // gameplay loop, skipping it freezes key_states and kills all input. + process_events(); while (! has_timer_stopped(timer_index)) { SDL_Delay(1); process_events(); @@ -3813,6 +4201,13 @@ int do_wait(int timer_index) { if ((replaying && skipping_replay) || is_validate_mode) return 0; #endif update_screen(); + // Pump events + check input at least once per frame even if the frame + // already overran the timer budget (see do_simple_wait above). + { + process_events(); + int key = do_paused(); + if (key != 0 && (word_1D63A != 0 || key == 0x1B)) return 1; + } while (! has_timer_stopped(timer_index)) { SDL_Delay(1); process_events(); @@ -4178,6 +4573,27 @@ void set_pal_256(rgb_type* source) { } #endif // USE_FADE +#ifdef __GBA__ +/* GBA: rendering ignores the per-image SDL_Palette and instead maps each sprite + pixel v -> global palette[image->gba_palette_offset + v] at blit time. So the + colour-variation palettes (guard colours, level 1.3 environment/wall variants) + that the desktop applies via SDL_SetPaletteColors have no effect unless we + write them into palette[] at the chtab's offset. Without this the guard kept + its placeholder load-time colours and drew as garbled flame-coloured blobs. + `colors` is packed r,g,b bytes in 6-bit VGA range (the same format set_pal + wants); index 0 is the transparent slot and is never drawn. */ +void set_chtab_palette(chtab_type* chtab, byte* colors, int n_colors) { + if (chtab == NULL) return; + int off = -1; + for (int i = 0; i < chtab->n_images; ++i) { + if (chtab->images[i] != NULL) { off = chtab->images[i]->gba_palette_offset; break; } + } + if (off < 0) return; + for (int i = 0; i < n_colors && (off + i) < 256; ++i) { + set_pal(off + i, colors[i*3 + 0], colors[i*3 + 1], colors[i*3 + 2]); + } +} +#else void set_chtab_palette(chtab_type* chtab, byte* colors, int n_colors) { if (chtab != NULL) { SDL_Color* scolors = (SDL_Color*) malloc(n_colors*sizeof(SDL_Color)); @@ -4202,7 +4618,7 @@ void set_chtab_palette(chtab_type* chtab, byte* colors, int n_colors) { if (current_image != NULL) { int n_colors_to_be_set = n_colors; - SDL_Palette* current_palette = current_image->format->palette; + SDL_Palette* current_palette = SURFACE_PALETTE(current_image); // Fix crashing with the guard graphics of Christmas of Persia. if (current_palette != NULL) { @@ -4221,6 +4637,7 @@ void set_chtab_palette(chtab_type* chtab, byte* colors, int n_colors) { free(scolors); } } +#endif /* __GBA__ */ int has_timer_stopped(int timer_index) { #ifdef USE_COMPAT_TIMER diff --git a/src/types.h b/src/types.h index 254b207f..3f1e677a 100644 --- a/src/types.h +++ b/src/types.h @@ -24,15 +24,60 @@ The authors of this program may be contacted at https://forum.princed.org #define STB_VORBIS_HEADER_ONLY #include "stb_vorbis.c" -#if !defined(_MSC_VER) -# include -# include +#ifdef USE_SDL3 +// SDL3 + SDL3_image. SDL_ENABLE_OLD_NAMES makes SDL3's own SDL_oldnames.h map the +// SDL2 symbol names 1:1; sdl2_to_sdl3.h then handles the rest (signature/return +// changes, removed APIs, the audio model). See src/sdl2_to_sdl3.h. +# define SDL_ENABLE_OLD_NAMES +# if !defined(_MSC_VER) +# include +# include +# else +# include +# include +# endif +# include "sdl2_to_sdl3.h" #else +# if !defined(_MSC_VER) +# include +# include +# else // These headers for SDL seem to be the pkgconfig/meson standard as per the // latest versions. If the old ones should be used, the ifdef must be used -// to compare versions. -# include -# include +// to compare versions. +# include +# include +# endif +#endif + +// Version-neutral accessors for things that moved/changed between SDL2 and SDL3 +// (SDL3 made SDL_Surface.format a plain enum, flattened the key event, renamed +// the gamepad event fields, and changed the keyboard-state element type). These +// expand to the native form on each backend so the shared code stays single-path. +#ifdef USE_SDL3 +# define SURFACE_BPP(s) SDL_BITSPERPIXEL((s)->format) +# define SURFACE_BYTESPP(s) SDL_BYTESPERPIXEL((s)->format) +# define SURFACE_PALETTE(s) SDL_GetSurfacePalette(s) +# define SURFACE_FMT_ENUM(s) ((s)->format) +# define EVENT_KEY_SCANCODE(e) ((e).key.scancode) +# define EVENT_KEY_MOD(e) ((e).key.mod) +# define EVENT_GP_AXIS(e) ((e).gaxis.axis) +# define EVENT_GP_VALUE(e) ((e).gaxis.value) +# define EVENT_GP_BUTTON(e) ((e).gbutton.button) +# define EVENT_GP_WHICH(e) ((e).gdevice.which) +# define KEYSTATE_PTR_T bool +#else +# define SURFACE_BPP(s) ((s)->format->BitsPerPixel) +# define SURFACE_BYTESPP(s) ((s)->format->BytesPerPixel) +# define SURFACE_PALETTE(s) ((s)->format->palette) +# define SURFACE_FMT_ENUM(s) ((s)->format->format) +# define EVENT_KEY_SCANCODE(e) ((e).key.keysym.scancode) +# define EVENT_KEY_MOD(e) ((e).key.keysym.mod) +# define EVENT_GP_AXIS(e) ((e).caxis.axis) +# define EVENT_GP_VALUE(e) ((e).caxis.value) +# define EVENT_GP_BUTTON(e) ((e).cbutton.button) +# define EVENT_GP_WHICH(e) ((e).cdevice.which) +# define KEYSTATE_PTR_T Uint8 #endif #if SDL_BYTEORDER != SDL_LIL_ENDIAN