diff --git a/buildroot-external/board/retrostone2/linux.fragment b/buildroot-external/board/retrostone2/linux.fragment index 534d36b..d133369 100644 --- a/buildroot-external/board/retrostone2/linux.fragment +++ b/buildroot-external/board/retrostone2/linux.fragment @@ -50,6 +50,8 @@ CONFIG_INPUT_EVDEV=y CONFIG_INPUT_JOYSTICK=y CONFIG_JOYSTICK_ADC=y CONFIG_JOYSTICK_XPAD=y +# rsos-gamepad: the built-in controls as one virtual gamepad for native games +CONFIG_INPUT_UINPUT=y CONFIG_INPUT_KEYBOARD=y CONFIG_KEYBOARD_GPIO=y CONFIG_KEYBOARD_GPIO_POLLED=y diff --git a/buildroot-external/board/retrostone2/post-build.sh b/buildroot-external/board/retrostone2/post-build.sh index 7945035..b9ae0ed 100755 --- a/buildroot-external/board/retrostone2/post-build.sh +++ b/buildroot-external/board/retrostone2/post-build.sh @@ -49,6 +49,10 @@ mkdir -p "$TARGET/usr/libexec/rsos" # (Settings > Power > Memory speed; rsos-boot-ok, tools/rsos-memtest.c). "$CC" -O2 -s -Wall -Wextra -Werror -o "$TARGET/usr/libexec/rsos/memtest" \ "$BOARD_DIR/tools/rsos-memtest.c" +# /usr/bin/rsos-gamepad: the built-in controls as one standard gamepad for +# native games that read SDL game controllers (docs/native-games.md, +# tools/rsos-gamepad.c; needs CONFIG_INPUT_UINPUT) +"$CC" -O2 -s -Wall -Wextra -Werror -o "$TARGET/usr/bin/rsos-gamepad" "$BOARD_DIR/tools/rsos-gamepad.c" # --- The boot loader, for "Install to internal storage" on the Pro # (rsos-storage install writes it at 8 KiB on the eMMC): the image's own diff --git a/buildroot-external/board/retrostone2/tools/rsos-gamepad.c b/buildroot-external/board/retrostone2/tools/rsos-gamepad.c new file mode 100644 index 0000000..e7e98ba --- /dev/null +++ b/buildroot-external/board/retrostone2/tools/rsos-gamepad.c @@ -0,0 +1,272 @@ +/* + * rsos-gamepad - the RetroStone2's built-in controls as one standard gamepad + * (uinput), for native games that read SDL game controllers (docs/native-games.md). + * + * The controls are three evdev devices: "RetroStone2 Buttons" (face, D-pad, + * Start/Select, optional C/Z), "RetroStone2 Shoulder Buttons" and the optional + * analog stick ("analog-stick", which SDL does not even take for a joystick). + * This reads them (no grab: the menu reads them too, for Select+Start) and + * writes "RetroStoneOS Gamepad": the Linux gamepad layout by position (bottom + * = BTN_SOUTH), C/Z as the stick clicks, the D-pad as hat 0, the stick as + * ABS_X/ABS_Y. The launch script starts it in the game's process group, so + * the menu stops it with the game, and gives SDL its mapping: + * + * SDL_GAMECONTROLLERCONFIG= RSOS_GAMEPAD_SDL_MAPPING below + * SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 (the raw gpio-keys) + * + * rsos-gamepad [--print-mapping] + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "RetroStoneOS Gamepad" +#define VENDOR 0x1209 /* pid.codes test vendor: no real device to clash with */ +#define PRODUCT 0x5253 /* "RS" */ +#define VERSION 0x0001 +/* SDL's GUID for it with the name CRC left 0 (matches any name): bus + * BUS_VIRTUAL (0x06), vendor, product, version, little-endian 16-bit words; + * buttons numbered in key code order (BTN_SOUTH 0x130 ... BTN_THUMBR 0x13e) */ +#define RSOS_GAMEPAD_SDL_MAPPING \ + "06000000091200005352000001000000," NAME ",a:b0,b:b1,y:b2,x:b3,leftshoulder:b4," \ + "rightshoulder:b5,lefttrigger:b6,righttrigger:b7,back:b8,start:b9,leftstick:b10," \ + "rightstick:b11,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,leftx:a0,lefty:a1," \ + "platform:Linux," + +#define MAX_SRC 4 + +static const int out_keys[] = { + BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_TL2, BTN_TR2, + BTN_SELECT, BTN_START, BTN_THUMBL, BTN_THUMBR, +}; + +static volatile sig_atomic_t quit; + +static void on_signal(int sig) +{ + (void)sig; + quit = 1; +} + +static void emit(int fd, int type, int code, int value) +{ + struct input_event ev; + + memset(&ev, 0, sizeof(ev)); + ev.type = (unsigned short)type; + ev.code = (unsigned short)code; + ev.value = value; + if (write(fd, &ev, sizeof(ev)) < 0 && errno != EAGAIN) + fprintf(stderr, "rsos-gamepad: write: %s\n", strerror(errno)); +} + +static int create_pad(void) +{ + struct uinput_setup us; + struct uinput_abs_setup abs; + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK | O_CLOEXEC); + + if (fd < 0) { + fprintf(stderr, "rsos-gamepad: /dev/uinput: %s\n", strerror(errno)); + return -1; + } + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_EVBIT, EV_SYN); + for (size_t i = 0; i < sizeof(out_keys) / sizeof(out_keys[0]); i++) + ioctl(fd, UI_SET_KEYBIT, out_keys[i]); + memset(&abs, 0, sizeof(abs)); + abs.absinfo.minimum = -32767; + abs.absinfo.maximum = 32767; + abs.code = ABS_X; + ioctl(fd, UI_SET_ABSBIT, ABS_X); + ioctl(fd, UI_ABS_SETUP, &abs); + abs.code = ABS_Y; + ioctl(fd, UI_SET_ABSBIT, ABS_Y); + ioctl(fd, UI_ABS_SETUP, &abs); + abs.absinfo.minimum = -1; + abs.absinfo.maximum = 1; + abs.code = ABS_HAT0X; + ioctl(fd, UI_SET_ABSBIT, ABS_HAT0X); + ioctl(fd, UI_ABS_SETUP, &abs); + abs.code = ABS_HAT0Y; + ioctl(fd, UI_SET_ABSBIT, ABS_HAT0Y); + ioctl(fd, UI_ABS_SETUP, &abs); + + memset(&us, 0, sizeof(us)); + us.id.bustype = BUS_VIRTUAL; + us.id.vendor = VENDOR; + us.id.product = PRODUCT; + us.id.version = VERSION; + snprintf(us.name, sizeof(us.name), "%s", NAME); + if (ioctl(fd, UI_DEV_SETUP, &us) < 0 || ioctl(fd, UI_DEV_CREATE) < 0) { + fprintf(stderr, "rsos-gamepad: cannot create the device: %s\n", strerror(errno)); + close(fd); + return -1; + } + return fd; +} + +struct src { + int fd; + bool stick; + struct input_absinfo ax, ay; +}; + +static int open_sources(struct src *s) +{ + DIR *d = opendir("/dev/input"); + struct dirent *de; + int n = 0; + + if (!d) + return 0; + while ((de = readdir(d)) && n < MAX_SRC) { + char path[300], name[128] = ""; + int fd; + + if (strncmp(de->d_name, "event", 5)) + continue; + snprintf(path, sizeof(path), "/dev/input/%s", de->d_name); + fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) + continue; + if (ioctl(fd, EVIOCGNAME(sizeof(name) - 1), name) < 0 || + (strcmp(name, "RetroStone2 Buttons") && strcmp(name, "RetroStone2 Shoulder Buttons") && + strcmp(name, "analog-stick"))) { + close(fd); + continue; + } + memset(&s[n], 0, sizeof(s[n])); + s[n].fd = fd; + s[n].stick = !strcmp(name, "analog-stick") && ioctl(fd, EVIOCGABS(ABS_X), &s[n].ax) == 0 && + ioctl(fd, EVIOCGABS(ABS_Y), &s[n].ay) == 0 && s[n].ax.maximum != s[n].ax.minimum && + s[n].ay.maximum != s[n].ay.minimum; + fprintf(stderr, "rsos-gamepad: %s (%s)\n", path, name); + n++; + } + closedir(d); + return n; +} + +static bool in_range(int v, const struct input_absinfo *a, bool near_centre) +{ + int lo = a->minimum < a->maximum ? a->minimum : a->maximum; + int hi = a->minimum < a->maximum ? a->maximum : a->minimum; + + if (near_centre) + return abs(v - (lo + (hi - lo) / 2)) <= (hi - lo) / 4; + return v >= lo && v <= hi; +} + +/* -32767..32767 from the ADC range */ +static int scale(int v, const struct input_absinfo *a) +{ + int lo = a->minimum < a->maximum ? a->minimum : a->maximum; + int hi = a->minimum < a->maximum ? a->maximum : a->minimum; + long r = ((long)(v - lo) * 65534) / (hi - lo) - 32767; + + return r < -32767 ? -32767 : r > 32767 ? 32767 : (int)r; +} + +int main(int argc, char **argv) +{ + struct src src[MAX_SRC]; + struct pollfd pfd[MAX_SRC]; + int out, n, sx = 0, sy = 0, hat_x = 0, hat_y = 0; + bool fitted = false, dpad[4] = { false }; /* up, down, left, right */ + + if (argc > 1 && !strcmp(argv[1], "--print-mapping")) { + puts(RSOS_GAMEPAD_SDL_MAPPING); + return 0; + } + signal(SIGTERM, on_signal); + signal(SIGINT, on_signal); + signal(SIGHUP, on_signal); + n = open_sources(src); + if (n == 0) { + fprintf(stderr, "rsos-gamepad: no built-in controls found\n"); + return 1; + } + out = create_pad(); + if (out < 0) + return 1; + for (int i = 0; i < n; i++) { + pfd[i].fd = src[i].fd; + pfd[i].events = POLLIN; + if (src[i].stick) { + sx = src[i].ax.value; + sy = src[i].ay.value; + } + } + + while (!quit) { + if (poll(pfd, (nfds_t)n, 1000) <= 0) + continue; + for (int i = 0; i < n; i++) { + struct input_event ev; + bool changed = false; + + if (!(pfd[i].revents & POLLIN)) + continue; + while (read(src[i].fd, &ev, sizeof(ev)) == sizeof(ev)) { + if (ev.type == EV_KEY) { + int code = ev.code; + + changed = true; + switch (code) { + case BTN_C: code = BTN_THUMBL; break; + case BTN_Z: code = BTN_THUMBR; break; + case BTN_DPAD_UP: dpad[0] = ev.value; code = -1; break; + case BTN_DPAD_DOWN: dpad[1] = ev.value; code = -1; break; + case BTN_DPAD_LEFT: dpad[2] = ev.value; code = -1; break; + case BTN_DPAD_RIGHT: dpad[3] = ev.value; code = -1; break; + default: break; + } + if (code >= 0 && ev.value != 2) + emit(out, EV_KEY, code, ev.value != 0); + } else if (ev.type == EV_ABS && src[i].stick && (ev.code == ABS_X || ev.code == ABS_Y)) { + if (ev.code == ABS_X) + sx = ev.value; + else + sy = ev.value; + changed = true; + } + } + if (!changed) + continue; + { + int hx = dpad[3] - dpad[2], hy = dpad[1] - dpad[0]; + + if (hx != hat_x) + emit(out, EV_ABS, ABS_HAT0X, hat_x = hx); + if (hy != hat_y) + emit(out, EV_ABS, ABS_HAT0Y, hat_y = hy); + } + if (src[i].stick) { + /* the menu's rule (input.c): out of range = not fitted; + * fitted again once both axes read near the centre */ + if (!in_range(sx, &src[i].ax, false) || !in_range(sy, &src[i].ay, false)) + fitted = false; + else if (!fitted && in_range(sx, &src[i].ax, true) && in_range(sy, &src[i].ay, true)) + fitted = true; + emit(out, EV_ABS, ABS_X, fitted ? scale(sx, &src[i].ax) : 0); + emit(out, EV_ABS, ABS_Y, fitted ? scale(sy, &src[i].ay) : 0); + } + emit(out, EV_SYN, SYN_REPORT, 0); + } + } + ioctl(out, UI_DEV_DESTROY); + close(out); + return 0; +} diff --git a/buildroot-external/configs/retrostone2_defconfig b/buildroot-external/configs/retrostone2_defconfig index 3db2d23..744ae23 100644 --- a/buildroot-external/configs/retrostone2_defconfig +++ b/buildroot-external/configs/retrostone2_defconfig @@ -77,6 +77,20 @@ BR2_PACKAGE_MESA3D=y BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_LIMA=y BR2_PACKAGE_MESA3D_OPENGL_EGL=y BR2_PACKAGE_MESA3D_OPENGL_ES=y +# SDL2 on KMS/DRM + GLES2 and ALSA, for native games (docs/native-games.md) +BR2_PACKAGE_SDL2=y +BR2_PACKAGE_SDL2_KMSDRM=y +BR2_PACKAGE_SDL2_OPENGLES=y +# and the libraries of the libultraship ports (ports/starship, ports/soh: docs/native-games.md) +BR2_PACKAGE_LIBOGG=y +BR2_PACKAGE_LIBVORBIS=y +BR2_PACKAGE_LIBZIP=y +BR2_PACKAGE_TINYXML2=y +BR2_PACKAGE_SPDLOG=y +BR2_PACKAGE_JSON_FOR_MODERN_CPP=y +BR2_PACKAGE_OPUS=y +BR2_PACKAGE_OPUSFILE=y +BR2_PACKAGE_SDL2_NET=y BR2_PACKAGE_KMSCUBE=y # Bring-up tools diff --git a/buildroot-external/configs/retrostone2_release_defconfig b/buildroot-external/configs/retrostone2_release_defconfig index de6f24d..550e564 100644 --- a/buildroot-external/configs/retrostone2_release_defconfig +++ b/buildroot-external/configs/retrostone2_release_defconfig @@ -77,6 +77,20 @@ BR2_PACKAGE_MESA3D=y BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_LIMA=y BR2_PACKAGE_MESA3D_OPENGL_EGL=y BR2_PACKAGE_MESA3D_OPENGL_ES=y +# SDL2 on KMS/DRM + GLES2 and ALSA, for native games (docs/native-games.md) +BR2_PACKAGE_SDL2=y +BR2_PACKAGE_SDL2_KMSDRM=y +BR2_PACKAGE_SDL2_OPENGLES=y +# and the libraries of the libultraship ports (ports/starship, ports/soh: docs/native-games.md) +BR2_PACKAGE_LIBOGG=y +BR2_PACKAGE_LIBVORBIS=y +BR2_PACKAGE_LIBZIP=y +BR2_PACKAGE_TINYXML2=y +BR2_PACKAGE_SPDLOG=y +BR2_PACKAGE_JSON_FOR_MODERN_CPP=y +BR2_PACKAGE_OPUS=y +BR2_PACKAGE_OPUSFILE=y +BR2_PACKAGE_SDL2_NET=y BR2_PACKAGE_KMSCUBE=y # Bring-up tools diff --git a/docs/native-games.md b/docs/native-games.md new file mode 100644 index 0000000..f430a27 --- /dev/null +++ b/docs/native-games.md @@ -0,0 +1,72 @@ +# Native games + +A native game is a program built for the console itself (ARM, Linux), started without an emulator: an +open-source game engine, a source port, a homebrew game. The menu lists them as the system **Native games**. + +[ports/](../ports/README.md) has build scripts for three N64 PC ports (Super Mario 64, Star Fox 64, Ocarina of +Time), each built on a PC from the user's own ROM. No game or game data is in this repository. + +## On the SD card + +One launch script per game in `roms/native/` (subfolders are fine), with the game's own files next to it: + +``` +roms/native/ + My Game.sh <- the entry in the menu ("My Game") + mygame/ + mygame <- the program (ARM, linked against this system's libraries) + data files... +``` + +The script runs with `/bin/sh` in **its own folder** (the working directory), so it can use relative paths. It +should end with `exec` so the game is the process the menu watches: + +```sh +#!/bin/sh +cd mygame +exec ./mygame +``` + +A program that the exFAT partition does not mark executable can be started through the dynamic loader: +`exec /lib/ld-linux-armhf.so.3 ./mygame`. + +## What the game gets + +* The display, the sound and the pads, as a libretro game does: the menu drops DRM master before the launch and + takes it back afterwards. **SDL2** is installed with the KMS/DRM video driver (GLES2 on the Mali-400 through + Mesa's lima) and ALSA; the menu sets `SDL_VIDEODRIVER=kmsdrm` and `SDL_AUDIODRIVER=alsa`. +* `HOME=/data/rsos/native-home` (the root file system is read-only), with `XDG_CONFIG_HOME` and + `XDG_DATA_HOME` under it. A game that saves next to itself (its working directory) keeps its saves on the card. +* `RSOS_GAME_DIR` (the script's folder) and `RSOS_LANG` (the menu's language, e.g. `en`). +* Its output goes to `/run/rsos/game.log`, copied to `rsos/logs/` on the SD card at the next start. + +The built-in controls are three input devices (the buttons, the shoulder buttons, the optional analog stick): +a game that reads only one SDL game controller would see only one of them, and SDL does not take the stick for a +joystick at all. **`rsos-gamepad`** (RetroStone2) merges them into one standard virtual +gamepad, "RetroStoneOS Gamepad" (uinput): the buttons by position (bottom = SDL "a", left = "x"), L1/R1 the +shoulders, L2/R2 the triggers, the optional C/Z the stick clicks, the D-pad as hat 0, the stick as the left stick +(centred while it is not fitted, as in the menu). A launch script for an SDL game starts it and gives SDL its +mapping (ports/starship/Star Fox 64.sh): + +```sh +SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" +export SDL_GAMECONTROLLERCONFIG +export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 # the raw button devices +/usr/bin/rsos-gamepad & +``` + +It runs in the game's process group, so the menu stops it with the game. A game can also read the devices +itself from `/dev/input` (ports/sm64/controller_rsos.c does, to follow the N64 emulator's button settings). + +## What the menu does around it + +* **Select + Start** held for one second quits the game (SIGTERM to its process group, SIGKILL if it is still + there 5 s later). A native game has no in-game menu of ours. +* **Sleep** (power key): the game is frozen (SIGSTOP) and the backlight goes off; waking continues it. +* **Power-off**: the game gets SIGTERM, then SIGKILL after 8 s. There is no save state: the game must save on + its own. +* The **idle power-off** waits while a native game runs (it could not resume it). +* An exit code other than 0 (and other than a stop by Select+Start or the menu) shows "The game ended with an + error". + +`make check-frontend` (steps 2a-2c) covers the launch, the environment, the error message and the process group. diff --git a/frontend/Makefile b/frontend/Makefile index ec0423a..36c54c5 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -595,6 +595,38 @@ check-frontend: $(FRONTEND) $(HOST_TESTCORE) $(SPLASH) grep -q 'crashed' $(FE_LOG) grep -q 'signal 15: exiting' $(FE_LOG) grep -q 'exit 0 after' $(FE_LOG) + @echo "check-frontend: 2a. a native game: its script runs in its folder, HOME on /data, SDL on KMS" + @# the native games only: the menu starts on them + mv $(FE_CHECK_DIR)/data/roms/nes $(FE_CHECK_DIR)/data/nes.aside + rm -f $(FE_CHECK_DIR)/data/rsos/cache/systems.idx + printf 'language = en\nlast_system = native\n' > $(FE_SETTINGS) + rm -f $(FE_LOG) + mkdir -p $(FE_CHECK_DIR)/data/roms/native/files + printf 'echo "$$PWD|$$HOME|$$SDL_VIDEODRIVER|$$RSOS_GAME_DIR" > files/ran.txt\nexit $${RSOS_TEST_NATIVE_EXIT:-0}\n' \ + > "$(FE_CHECK_DIR)/data/roms/native/Native Test.sh" + $(FE_RUN) --script "wait:600 a wait:500 a wait:1500 shot:$(FE_CHECK_DIR)/native.png term" + grep -q 'launch native Native Test' $(FE_LOG) + grep -q 'native game ended: 0' $(FE_LOG) + test "$$(cat $(FE_CHECK_DIR)/data/roms/native/files/ran.txt)" = \ + "$(FE_CHECK_DIR)/data/roms/native|$(FE_CHECK_DIR)/data/rsos/native-home|kmsdrm|$(FE_CHECK_DIR)/data/roms/native" + grep -q '^=== \[.*\] launch native: .*Native Test.sh' $(FE_GAME_LOG) + @echo "check-frontend: 2b. a native game that fails: its exit code is shown" + printf 'language = en\nlast_system = native\n' > $(FE_SETTINGS) + rm -f $(FE_LOG) + RSOS_TEST_NATIVE_EXIT=3 $(FE_RUN) --script "wait:600 a wait:500 a wait:1500 term" + grep -q 'native game ended: 3' $(FE_LOG) + @echo "check-frontend: 2c. the menu stops: the native game and what it started go too" + printf 'language = en\nlast_system = native\n' > $(FE_SETTINGS) + rm -f $(FE_LOG) $(FE_CHECK_DIR)/data/roms/native/files/bg.pid + printf 'sleep 300 &\necho $$! > files/bg.pid\nwait\n' > "$(FE_CHECK_DIR)/data/roms/native/Native Test.sh" + $(FE_RUN) --script "wait:600 a wait:500 a game:wait:1000 game:term" + grep -q 'signal 15: asking the game to quit' $(FE_LOG) + grep -q 'native game ended: 143' $(FE_LOG) + test -s $(FE_CHECK_DIR)/data/roms/native/files/bg.pid + ! kill -0 $$(cat $(FE_CHECK_DIR)/data/roms/native/files/bg.pid) 2> /dev/null + rm -rf $(FE_CHECK_DIR)/data/roms/native + mv $(FE_CHECK_DIR)/data/nes.aside $(FE_CHECK_DIR)/data/roms/nes + rm -f $(FE_CHECK_DIR)/data/rsos/cache/systems.idx @echo "check-frontend: 3. charge mode, charger already unplugged: power-off" rm -f $(FE_LOG) printf 'charger\n' > $(FE_CHECK_DIR)/run/rsos/bootreason diff --git a/frontend/po/rsos.pot b/frontend/po/rsos.pot index 99f0d5f..6c156e3 100644 --- a/frontend/po/rsos.pot +++ b/frontend/po/rsos.pot @@ -266,63 +266,63 @@ msgstr "" msgid "No GPU found (lima)" msgstr "" -#: src/host/launch.c:322 +#: src/host/launch.c:427 msgid "The game was stopped" msgstr "" #. TRANSLATORS: shown by the menu after a crash; %s is the #. signal name in English ("Segmentation fault") -#: src/host/launch.c:327 +#: src/host/launch.c:432 #, c-format msgid "The emulator crashed (%s). Game saves up to a few seconds ago are kept." msgstr "" -#: src/host/launch.c:343 +#: src/host/launch.c:448 msgid "Could not start the game" msgstr "" -#: src/host/launch.c:356 +#: src/host/launch.c:461 msgid "The emulator stopped responding. Game saves up to a few seconds ago are kept." msgstr "" -#: src/host/launch.c:360 +#: src/host/launch.c:465 msgid "Cannot start the game process" msgstr "" -#: src/host/launch.c:365 +#: src/host/launch.c:470 #, c-format msgid "The emulator failed (code %d)" msgstr "" #. TRANSLATORS: in-game menu > Scaling values (right-aligned on a 640 px row) -#: src/host/menu.c:110 src/ui/screens.c:2776 src/ui/screens.c:2780 +#: src/host/menu.c:110 src/ui/screens.c:2783 src/ui/screens.c:2787 msgctxt "scaling" msgid "Integer" msgstr "" -#: src/host/menu.c:111 src/ui/screens.c:2781 +#: src/host/menu.c:111 src/ui/screens.c:2788 msgctxt "scaling" msgid "Stretch" msgstr "" -#: src/host/menu.c:111 src/ui/screens.c:2777 src/ui/screens.c:2779 +#: src/host/menu.c:111 src/ui/screens.c:2784 src/ui/screens.c:2786 msgctxt "scaling" msgid "Aspect" msgstr "" #. TRANSLATORS: in-game menu > CPU profile values: the emulator's own #. setting / full speed / slower and cooler, saves battery -#: src/host/menu.c:126 src/ui/screens.c:2789 +#: src/host/menu.c:126 src/ui/screens.c:2796 msgctxt "cpu profile" msgid "Performance" msgstr "" -#: src/host/menu.c:127 src/ui/screens.c:2790 +#: src/host/menu.c:127 src/ui/screens.c:2797 msgctxt "cpu profile" msgid "Battery saver" msgstr "" -#: src/host/menu.c:127 src/ui/screens.c:2788 +#: src/host/menu.c:127 src/ui/screens.c:2795 msgctxt "cpu profile" msgid "Automatic" msgstr "" @@ -415,13 +415,13 @@ msgid "Core options" msgstr "" #. TRANSLATORS: game options: how the picture fills the screen, for this game -#: src/host/menu.c:218 src/ui/screens.c:2786 +#: src/host/menu.c:218 src/ui/screens.c:2793 msgid "Scaling" msgstr "" #. TRANSLATORS: in-game menu: how fast the processor runs for this game #. TRANSLATORS: game options: how fast the processor runs for this game -#: src/host/menu.c:222 src/ui/screens.c:2796 +#: src/host/menu.c:222 src/ui/screens.c:2803 msgid "CPU profile" msgstr "" @@ -437,7 +437,7 @@ msgstr "" msgid "Benchmark this game" msgstr "" -#: src/host/menu.c:231 src/host/menu.c:389 src/ui/screens.c:754 src/ui/screens.c:1887 +#: src/host/menu.c:231 src/host/menu.c:389 src/ui/screens.c:754 src/ui/screens.c:1894 msgid "Controls" msgstr "" @@ -746,68 +746,79 @@ msgctxt "strftime" msgid "%Y-%m-%d %H:%M" msgstr "" -#: src/main.c:1195 +#: src/main.c:1198 msgid "The screen refused this refresh rate" msgstr "" -#: src/main.c:2327 -msgid "No emulator is installed for this game." -msgstr "" - -#: src/main.c:2334 -msgid "This game cannot start (missing BIOS?)." +#: src/main.c:2388 +msgid "This game cannot start: its launch script is missing." msgstr "" #. TRANSLATORS: %s is a system error message (in English) -#: src/main.c:2514 +#: src/main.c:2450 src/main.c:2679 #, c-format msgid "Cannot start the game process (%s)." msgstr "" +#. TRANSLATORS: a native game (no emulator) ended with an +#. error; %d is its exit code +#: src/main.c:2460 +#, c-format +msgid "The game ended with an error (%d). Its messages are in rsos/logs on the SD card." +msgstr "" + +#: src/main.c:2492 +msgid "No emulator is installed for this game." +msgstr "" + +#: src/main.c:2499 +msgid "This game cannot start (missing BIOS?)." +msgstr "" + #. TRANSLATORS: the automatic power-off (no input for a while) #. was stopped because the game's state could not be saved -#: src/main.c:2529 +#: src/main.c:2694 msgid "Automatic power-off cancelled: the game could not be saved." msgstr "" -#: src/main.c:2541 src/ui/ui.c:586 +#: src/main.c:2706 src/ui/ui.c:586 msgid "The game could not be started." msgstr "" #. TRANSLATORS: boot message: the SD card's data partition (games, saves) -#: src/main.c:2626 +#: src/main.c:2791 msgid "The data partition could not be restored. Your games and saves may need to be copied again." msgstr "" -#: src/main.c:2630 +#: src/main.c:2795 msgid "The data partition was unreadable and was reformatted." msgstr "" -#: src/main.c:2633 +#: src/main.c:2798 msgid "The data partition has errors: saving may fail. Back up your saves." msgstr "" #. TRANSLATORS: boot toast: the SD card's data partition is mounted read-only #. (its first-boot setup finishes at the next start) -#: src/main.c:2641 +#: src/main.c:2806 msgid "Storage is read-only until the console restarts" msgstr "" -#: src/main.c:2646 +#: src/main.c:2811 msgid "The last update didn't start; the previous version was restored." msgstr "" -#: src/main.c:2652 +#: src/main.c:2817 msgid "The SD card was checked after an unexpected power loss" msgstr "" #. TRANSLATORS: boot screen under the logo (first start, or after an update) -#: src/main.c:2924 +#: src/main.c:3089 msgid "Preparing the SD card, please wait..." msgstr "" #. TRANSLATORS: boot screen under the logo, after the power was cut -#: src/main.c:2926 +#: src/main.c:3091 msgid "Checking the SD card, please wait..." msgstr "" @@ -835,7 +846,7 @@ msgstr "" msgid "connecting..." msgstr "" -#: src/ui/bt_ui.c:142 src/ui/bt_ui.c:201 src/ui/screens.c:819 +#: src/ui/bt_ui.c:142 src/ui/bt_ui.c:201 src/ui/screens.c:826 msgid "please wait..." msgstr "" @@ -848,7 +859,7 @@ msgstr "" msgid "Searching..." msgstr "" -#: src/ui/bt_ui.c:205 src/ui/screens.c:846 src/ui/update_ui.c:930 +#: src/ui/bt_ui.c:205 src/ui/screens.c:853 src/ui/update_ui.c:930 #, c-format msgid "Cannot run %s" msgstr "" @@ -875,7 +886,7 @@ msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short (CANCEL is selected) #. TRANSLATORS: dialog button, uppercase, short -#: src/ui/bt_ui.c:246 src/ui/screens.c:2476 src/ui/screens.c:2991 src/ui/storage.c:1037 +#: src/ui/bt_ui.c:246 src/ui/screens.c:2483 src/ui/screens.c:2998 src/ui/storage.c:1037 #: src/ui/storage.c:1067 src/ui/storage.c:1075 src/ui/storage.c:1122 src/ui/storage.c:1179 #: src/ui/storage.c:1192 src/ui/storage.c:1204 src/ui/storage.c:1338 src/ui/update_ui.c:535 #: src/ui/update_ui.c:973 @@ -885,7 +896,7 @@ msgstr "" #. TRANSLATORS: Settings > Network: the Bluetooth pairing screen #. TRANSLATORS: Settings > Network, under the Bluetooth switch: pair #. and connect Bluetooth controllers -#: src/ui/bt_ui.c:263 src/ui/screens.c:1008 +#: src/ui/bt_ui.c:263 src/ui/screens.c:1015 msgid "Bluetooth devices" msgstr "" @@ -909,7 +920,7 @@ msgstr "" msgid "%s forgotten" msgstr "" -#: src/ui/bt_ui.c:317 src/ui/bt_ui.c:321 src/ui/screens.c:1072 src/ui/transfer_ui.c:1908 +#: src/ui/bt_ui.c:317 src/ui/bt_ui.c:321 src/ui/screens.c:1079 src/ui/transfer_ui.c:1908 msgid "failed" msgstr "" @@ -934,8 +945,8 @@ msgstr "" #. TRANSLATORS: help bar at the bottom of the screen: one short word each #. TRANSLATORS: first-boot language picker: help bar, one short word each #. TRANSLATORS: help bar labels (uppercase, short: ~10 characters) -#: src/ui/menu.c:99 src/ui/menu.c:471 src/ui/menu.c:474 src/ui/screens.c:1988 -#: src/ui/screens.c:2868 src/ui/storage.c:560 src/ui/view_gamelist.c:1005 +#: src/ui/menu.c:99 src/ui/menu.c:471 src/ui/menu.c:474 src/ui/screens.c:1995 +#: src/ui/screens.c:2875 src/ui/storage.c:560 src/ui/view_gamelist.c:1005 #: src/ui/view_system.c:115 src/ui/view_system.c:119 msgid "choose" msgstr "" @@ -944,7 +955,7 @@ msgstr "" msgid "change" msgstr "" -#: src/ui/menu.c:99 src/ui/menu.c:471 src/ui/menu.c:474 src/ui/screens.c:2868 +#: src/ui/menu.c:99 src/ui/menu.c:471 src/ui/menu.c:474 src/ui/screens.c:2875 #: src/ui/storage.c:560 src/ui/view_system.c:115 src/ui/view_system.c:119 msgid "select" msgstr "" @@ -956,7 +967,7 @@ msgstr "" #. TRANSLATORS: dialog button, uppercase, short #. TRANSLATORS: storage task page button -#: src/ui/menu.c:633 src/ui/screens.c:1988 src/ui/storage.c:525 src/ui/transfer_ui.c:279 +#: src/ui/menu.c:633 src/ui/screens.c:1995 src/ui/storage.c:525 src/ui/transfer_ui.c:279 msgid "OK" msgstr "" @@ -1027,7 +1038,7 @@ msgstr "" #. TRANSLATORS: game list style: automatic, a plain list, list + picture and details #. TRANSLATORS: how the battery level is measured: automatic, from the voltage, #. or the power chip's (AXP) fuel gauge -#: src/ui/screens.c:66 src/ui/screens.c:1564 +#: src/ui/screens.c:66 src/ui/screens.c:1571 msgid "Automatic" msgstr "" @@ -1109,7 +1120,7 @@ msgid "" "Keep this setting? Reverting in %d s" msgstr "" -#: src/ui/screens.c:279 src/ui/screens.c:1531 src/ui/screens.c:1886 +#: src/ui/screens.c:279 src/ui/screens.c:1538 src/ui/screens.c:1893 msgid "Display" msgstr "" @@ -1309,11 +1320,11 @@ msgid "Players" msgstr "" #. TRANSLATORS: "stop when idle for": durations, and never -#: src/ui/screens.c:784 src/ui/screens.c:1556 src/ui/screens.c:1559 +#: src/ui/screens.c:784 src/ui/screens.c:1563 src/ui/screens.c:1566 msgid "10 min" msgstr "" -#: src/ui/screens.c:784 src/ui/screens.c:1560 +#: src/ui/screens.c:784 src/ui/screens.c:1567 msgid "30 min" msgstr "" @@ -1322,407 +1333,407 @@ msgid "1 hour" msgstr "" #. TRANSLATORS: dim / turn off the screen after: never, or minutes -#: src/ui/screens.c:784 src/ui/screens.c:1202 src/ui/screens.c:1554 src/ui/screens.c:1556 -#: src/ui/screens.c:1559 +#: src/ui/screens.c:784 src/ui/screens.c:1209 src/ui/screens.c:1561 src/ui/screens.c:1563 +#: src/ui/screens.c:1566 msgid "Never" msgstr "" -#: src/ui/screens.c:824 +#: src/ui/screens.c:831 msgid "not set" msgstr "" #. TRANSLATORS: the network transfer is on -#: src/ui/screens.c:830 +#: src/ui/screens.c:837 msgid "running" msgstr "" -#: src/ui/screens.c:839 +#: src/ui/screens.c:846 msgid "WiFi: turning off, please wait..." msgstr "" -#: src/ui/screens.c:839 +#: src/ui/screens.c:846 msgid "Ethernet: turning off, please wait..." msgstr "" -#: src/ui/screens.c:840 +#: src/ui/screens.c:847 msgid "Bluetooth: turning off, please wait..." msgstr "" #. TRANSLATORS: toast: WiFi is being turned on for the update check -#: src/ui/screens.c:841 src/ui/update_ui.c:936 +#: src/ui/screens.c:848 src/ui/update_ui.c:936 msgid "WiFi: turning on, please wait..." msgstr "" -#: src/ui/screens.c:841 +#: src/ui/screens.c:848 msgid "Ethernet: turning on, please wait..." msgstr "" -#: src/ui/screens.c:842 +#: src/ui/screens.c:849 msgid "Bluetooth: turning on, please wait..." msgstr "" -#: src/ui/screens.c:881 src/ui/screens.c:903 +#: src/ui/screens.c:888 src/ui/screens.c:910 msgid "Could not save the WiFi settings" msgstr "" -#: src/ui/screens.c:894 +#: src/ui/screens.c:901 msgid "A WiFi password has at least 8 characters." msgstr "" -#: src/ui/screens.c:899 +#: src/ui/screens.c:906 msgid "Set the network name first." msgstr "" -#: src/ui/screens.c:917 +#: src/ui/screens.c:924 msgid "WiFi password saved" msgstr "" -#: src/ui/screens.c:937 +#: src/ui/screens.c:944 msgid "WiFi network name" msgstr "" -#: src/ui/screens.c:942 src/ui/screens.c:1002 +#: src/ui/screens.c:949 src/ui/screens.c:1009 msgid "WiFi password" msgstr "" -#: src/ui/screens.c:950 src/ui/screens.c:1014 +#: src/ui/screens.c:957 src/ui/screens.c:1021 msgid "Console name" msgstr "" -#: src/ui/screens.c:989 +#: src/ui/screens.c:996 msgid "Use letters, digits and dashes." msgstr "" -#: src/ui/screens.c:993 +#: src/ui/screens.c:1000 #, c-format msgid "Console name: %s (next transfer)" msgstr "" #. TRANSLATORS: the wireless network ("Wi-Fi" in French, "WLAN" in German) -#: src/ui/screens.c:1000 +#: src/ui/screens.c:1007 msgid "WiFi" msgstr "" -#: src/ui/screens.c:1001 +#: src/ui/screens.c:1008 msgid "WiFi network" msgstr "" -#: src/ui/screens.c:1012 +#: src/ui/screens.c:1019 msgid "Transfer games from a computer or phone" msgstr "" #. TRANSLATORS: screen title (the Settings > Network row of the same name) -#: src/ui/screens.c:1013 src/ui/transfer_ui.c:2053 +#: src/ui/screens.c:1020 src/ui/transfer_ui.c:2053 msgid "Transfer over network" msgstr "" -#: src/ui/screens.c:1015 +#: src/ui/screens.c:1022 msgid "Stop when idle for" msgstr "" -#: src/ui/screens.c:1018 +#: src/ui/screens.c:1025 msgid "Keep receiving in the background" msgstr "" #. TRANSLATORS: Settings > Network: also share the SD card as a #. Windows network folder (\\RETROSTONE) while "Transfer over #. network" runs -#: src/ui/screens.c:1027 +#: src/ui/screens.c:1034 msgid "Windows file share" msgstr "" -#: src/ui/screens.c:1035 src/ui/screens.c:1888 +#: src/ui/screens.c:1042 src/ui/screens.c:1895 msgid "Network" msgstr "" -#: src/ui/screens.c:1062 +#: src/ui/screens.c:1069 msgid "WiFi off" msgstr "" -#: src/ui/screens.c:1062 +#: src/ui/screens.c:1069 msgid "Ethernet off" msgstr "" -#: src/ui/screens.c:1062 +#: src/ui/screens.c:1069 msgid "Bluetooth off" msgstr "" -#: src/ui/screens.c:1063 +#: src/ui/screens.c:1070 msgid "WiFi on" msgstr "" -#: src/ui/screens.c:1063 +#: src/ui/screens.c:1070 msgid "Ethernet on" msgstr "" -#: src/ui/screens.c:1063 +#: src/ui/screens.c:1070 msgid "Bluetooth on" msgstr "" -#: src/ui/screens.c:1102 src/ui/screens.c:1889 +#: src/ui/screens.c:1109 src/ui/screens.c:1896 msgid "Theme" msgstr "" -#: src/ui/screens.c:1113 +#: src/ui/screens.c:1120 msgid "No theme found" msgstr "" #. TRANSLATORS: Settings > Game lists > Sort games by: alphabetical, the most #. played first (play time), the most recently played first -#: src/ui/screens.c:1123 +#: src/ui/screens.c:1130 msgid "Name" msgstr "" -#: src/ui/screens.c:1123 +#: src/ui/screens.c:1130 msgid "Most played" msgstr "" -#: src/ui/screens.c:1123 +#: src/ui/screens.c:1130 msgid "Recently played" msgstr "" -#: src/ui/screens.c:1148 +#: src/ui/screens.c:1155 msgid "Game lists refreshed" msgstr "" -#: src/ui/screens.c:1164 src/ui/screens.c:1890 +#: src/ui/screens.c:1171 src/ui/screens.c:1897 msgid "Game lists" msgstr "" -#: src/ui/screens.c:1167 +#: src/ui/screens.c:1174 msgid "Favorites first" msgstr "" -#: src/ui/screens.c:1169 +#: src/ui/screens.c:1176 msgid "Favorites & last played pages" msgstr "" #. TRANSLATORS: Settings > Game lists: the order of the games in a list -#: src/ui/screens.c:1172 +#: src/ui/screens.c:1179 msgid "Sort games by" msgstr "" #. TRANSLATORS: Settings > Game lists: also list the games hidden with #. "Hide this game" (or in gamelist.xml) -#: src/ui/screens.c:1176 +#: src/ui/screens.c:1183 msgid "Show hidden games" msgstr "" -#: src/ui/screens.c:1178 +#: src/ui/screens.c:1185 msgid "Refresh game lists" msgstr "" #. TRANSLATORS: Settings > Game lists: box pictures from the libretro thumbnail server -#: src/ui/screens.c:1180 +#: src/ui/screens.c:1187 msgid "Download game covers" msgstr "" #. TRANSLATORS: Settings > Game lists: one entry per multi-disc game (.m3u playlists) -#: src/ui/screens.c:1182 +#: src/ui/screens.c:1189 msgid "Group multi-disc games" msgstr "" #. TRANSLATORS: what launching a game that has an automatic save does -#: src/ui/screens.c:1195 src/ui/screens.c:1202 +#: src/ui/screens.c:1202 src/ui/screens.c:1209 msgid "Ask" msgstr "" -#: src/ui/screens.c:1195 +#: src/ui/screens.c:1202 msgid "Always resume" msgstr "" -#: src/ui/screens.c:1195 +#: src/ui/screens.c:1202 msgid "Always start fresh" msgstr "" #. TRANSLATORS: Settings > Games > Resume on boot: relaunch the game without #. asking / ask / never (the menu comes up) -#: src/ui/screens.c:1202 +#: src/ui/screens.c:1209 msgid "Always" msgstr "" #. TRANSLATORS: toggle: copy the games -#: src/ui/screens.c:1229 src/ui/screens.c:1532 src/ui/screens.c:1891 src/ui/transfer_ui.c:701 +#: src/ui/screens.c:1236 src/ui/screens.c:1539 src/ui/screens.c:1898 src/ui/transfer_ui.c:701 msgid "Games" msgstr "" -#: src/ui/screens.c:1232 +#: src/ui/screens.c:1239 msgid "Resume games" msgstr "" -#: src/ui/screens.c:1233 +#: src/ui/screens.c:1240 msgid "Auto-save on exit" msgstr "" -#: src/ui/screens.c:1235 +#: src/ui/screens.c:1242 msgid "On launch" msgstr "" #. TRANSLATORS: Settings > Games: what happens at start-up after the #. console was turned off during a game -#: src/ui/screens.c:1240 +#: src/ui/screens.c:1247 msgid "Resume on boot" msgstr "" -#: src/ui/screens.c:1243 +#: src/ui/screens.c:1250 msgid "Powering off in a game always saves it" msgstr "" #. TRANSLATORS: Settings > Games: section of the in-game shortcuts -#: src/ui/screens.c:1245 +#: src/ui/screens.c:1252 msgid "In game" msgstr "" #. TRANSLATORS: Settings > Games: how fast SELECT + R2 runs the game -#: src/ui/screens.c:1247 +#: src/ui/screens.c:1254 msgid "Fast-forward speed" msgstr "" -#: src/ui/screens.c:1292 +#: src/ui/screens.c:1299 msgid "Could not update /boot/rsos.env." msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short -#: src/ui/screens.c:1296 +#: src/ui/screens.c:1303 msgid "REBOOT NOW" msgstr "" -#: src/ui/screens.c:1297 src/ui/screens.c:1619 src/ui/storage.c:1137 src/ui/storage.c:1152 +#: src/ui/screens.c:1304 src/ui/screens.c:1626 src/ui/storage.c:1137 src/ui/storage.c:1152 #: src/ui/ui.c:1173 src/ui/update_ui.c:898 msgid "LATER" msgstr "" -#: src/ui/screens.c:1299 +#: src/ui/screens.c:1306 msgid "Reboot required: the change applies at the next start." msgstr "" #. TRANSLATORS: storage: "12.5 GB free of 29.1 GB" -#: src/ui/screens.c:1329 src/ui/screens.c:1451 src/ui/storage.c:998 +#: src/ui/screens.c:1336 src/ui/screens.c:1458 src/ui/storage.c:998 #, c-format msgid "%s free of %s" msgstr "" -#: src/ui/screens.c:1354 +#: src/ui/screens.c:1361 msgid "Game storage" msgstr "" #. TRANSLATORS: a USB stick with no volume label and no vendor name -#: src/ui/screens.c:1356 src/ui/transfer_ui.c:184 src/ui/transfer_ui.c:805 +#: src/ui/screens.c:1363 src/ui/transfer_ui.c:184 src/ui/transfer_ui.c:805 #: src/ui/transfer_ui.c:1101 msgid "USB drive" msgstr "" -#: src/ui/screens.c:1357 +#: src/ui/screens.c:1364 msgid "Import games" msgstr "" -#: src/ui/screens.c:1358 +#: src/ui/screens.c:1365 msgid "Export games" msgstr "" -#: src/ui/screens.c:1359 +#: src/ui/screens.c:1366 msgid "Back up saves" msgstr "" -#: src/ui/screens.c:1360 +#: src/ui/screens.c:1367 msgid "Eject USB drive" msgstr "" -#: src/ui/screens.c:1361 +#: src/ui/screens.c:1368 msgid "Ask when a USB drive is plugged in" msgstr "" -#: src/ui/screens.c:1372 +#: src/ui/screens.c:1379 msgid "Advanced: off for a fast boot, restart needed" msgstr "" #. TRANSLATORS: toggle: make the eMMC visible (needs a restart) -#: src/ui/screens.c:1374 src/ui/storage.c:902 +#: src/ui/screens.c:1381 src/ui/storage.c:902 msgid "Internal eMMC" msgstr "" #. TRANSLATORS: toggle: make the M.2 slot work (needs a restart) -#: src/ui/screens.c:1378 src/ui/storage.c:962 +#: src/ui/screens.c:1385 src/ui/storage.c:962 msgid "M.2 SATA drive" msgstr "" -#: src/ui/screens.c:1385 src/ui/screens.c:1530 src/ui/screens.c:1892 +#: src/ui/screens.c:1392 src/ui/screens.c:1537 src/ui/screens.c:1899 msgid "Storage" msgstr "" -#: src/ui/screens.c:1410 +#: src/ui/screens.c:1417 msgid "no battery" msgstr "" #. TRANSLATORS: battery: percent, then the time until full: hours, minutes -#: src/ui/screens.c:1413 +#: src/ui/screens.c:1420 #, c-format msgid "%d %%, full in %d h %02d" msgstr "" #. TRANSLATORS: battery: percent, then about how long it lasts: hours, minutes -#: src/ui/screens.c:1417 +#: src/ui/screens.c:1424 #, c-format msgid "%d %%, about %d h %02d" msgstr "" -#: src/ui/screens.c:1420 +#: src/ui/screens.c:1427 #, c-format msgid "%d %%, charging" msgstr "" -#: src/ui/screens.c:1422 +#: src/ui/screens.c:1429 #, c-format msgid "%d %%, full" msgstr "" #. TRANSLATORS: battery percent ("80 %") -#: src/ui/screens.c:1425 +#: src/ui/screens.c:1432 #, c-format msgid "%d %%" msgstr "" #. TRANSLATORS: power source and battery voltage ("charger, 4.12 V") -#: src/ui/screens.c:1433 +#: src/ui/screens.c:1440 #, c-format msgid "charger, %s V" msgstr "" -#: src/ui/screens.c:1434 +#: src/ui/screens.c:1441 #, c-format msgid "battery, %s V" msgstr "" -#: src/ui/screens.c:1436 +#: src/ui/screens.c:1443 msgid "charger connected" msgstr "" -#: src/ui/screens.c:1436 +#: src/ui/screens.c:1443 msgid "battery" msgstr "" -#: src/ui/screens.c:1442 +#: src/ui/screens.c:1449 #, c-format msgid "%d °C (hot)" msgstr "" -#: src/ui/screens.c:1442 +#: src/ui/screens.c:1449 #, c-format msgid "%d °C" msgstr "" -#: src/ui/screens.c:1453 +#: src/ui/screens.c:1460 msgid "not mounted" msgstr "" #. TRANSLATORS: display: HDMI or LCD, then the menu's size in pixels -#: src/ui/screens.c:1458 +#: src/ui/screens.c:1465 #, c-format msgid "%s, UI %dx%d" msgstr "" #. TRANSLATORS: part of "3 systems, 120 games, 1.5 GB" -#: src/ui/screens.c:1469 src/ui/transfer_ui.c:572 +#: src/ui/screens.c:1476 src/ui/transfer_ui.c:572 #, c-format msgid "%d system" msgid_plural "%d systems" @@ -1730,7 +1741,7 @@ msgstr[0] "" msgstr[1] "" #. TRANSLATORS: "25 games in 3 systems": %s is the "3 systems" part -#: src/ui/screens.c:1471 +#: src/ui/screens.c:1478 #, c-format msgid "%d game in %s" msgid_plural "%d games in %s" @@ -1738,282 +1749,282 @@ msgstr[0] "" msgstr[1] "" #. TRANSLATORS: System information: the people who made RetroStoneOS -#: src/ui/screens.c:1484 src/ui/screens.c:1536 +#: src/ui/screens.c:1491 src/ui/screens.c:1543 msgid "Credits" msgstr "" #. TRANSLATORS: System information > Credits, when the list is missing -#: src/ui/screens.c:1502 +#: src/ui/screens.c:1509 msgid "No credits list on this system" msgstr "" -#: src/ui/screens.c:1515 src/ui/screens.c:1893 +#: src/ui/screens.c:1522 src/ui/screens.c:1900 msgid "System information" msgstr "" -#: src/ui/screens.c:1520 +#: src/ui/screens.c:1527 msgid "Version" msgstr "" #. TRANSLATORS: System information: RetroStone2 or RetroStone2 Pro #. TRANSLATORS: Settings > Storage: RetroStone2 or RetroStone2 Pro (found at the first start) -#: src/ui/screens.c:1524 src/ui/storage.c:881 +#: src/ui/screens.c:1531 src/ui/storage.c:881 msgid "Model" msgstr "" -#: src/ui/screens.c:1527 +#: src/ui/screens.c:1534 msgid "Battery" msgstr "" #. TRANSLATORS: where the power comes from (charger or battery) -#: src/ui/screens.c:1529 +#: src/ui/screens.c:1536 msgctxt "power source" msgid "Power" msgstr "" -#: src/ui/screens.c:1533 +#: src/ui/screens.c:1540 msgid "Input devices" msgstr "" -#: src/ui/screens.c:1534 +#: src/ui/screens.c:1541 msgid "CPU temperature" msgstr "" -#: src/ui/screens.c:1554 +#: src/ui/screens.c:1561 msgid "1 min" msgstr "" -#: src/ui/screens.c:1554 src/ui/screens.c:1556 +#: src/ui/screens.c:1561 src/ui/screens.c:1563 msgid "2 min" msgstr "" -#: src/ui/screens.c:1554 src/ui/screens.c:1556 src/ui/screens.c:1559 +#: src/ui/screens.c:1561 src/ui/screens.c:1563 src/ui/screens.c:1566 msgid "5 min" msgstr "" -#: src/ui/screens.c:1559 +#: src/ui/screens.c:1566 msgid "15 min" msgstr "" -#: src/ui/screens.c:1560 +#: src/ui/screens.c:1567 msgid "60 min" msgstr "" -#: src/ui/screens.c:1564 +#: src/ui/screens.c:1571 msgid "Voltage" msgstr "" -#: src/ui/screens.c:1564 +#: src/ui/screens.c:1571 msgid "AXP fuel gauge" msgstr "" #. TRANSLATORS: Settings > Power > Memory speed: the DRAM clock (normal 384 MHz, #. +12 % = 432 MHz, +25 % = 480 MHz) -#: src/ui/screens.c:1568 +#: src/ui/screens.c:1575 msgid "Normal" msgstr "" -#: src/ui/screens.c:1568 +#: src/ui/screens.c:1575 msgid "Faster (+12 %)" msgstr "" -#: src/ui/screens.c:1568 +#: src/ui/screens.c:1575 msgid "Fastest (+25 %)" msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short -#: src/ui/screens.c:1591 src/ui/screens.c:2430 +#: src/ui/screens.c:1598 src/ui/screens.c:2437 msgid "YES" msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short (NO is selected) -#: src/ui/screens.c:1592 src/ui/screens.c:2429 src/ui/screens.c:2600 +#: src/ui/screens.c:1599 src/ui/screens.c:2436 src/ui/screens.c:2607 msgid "NO" msgstr "" -#: src/ui/screens.c:1597 +#: src/ui/screens.c:1604 msgid "Restart the console?" msgstr "" -#: src/ui/screens.c:1602 +#: src/ui/screens.c:1609 msgid "Power off the console?" msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short -#: src/ui/screens.c:1619 src/ui/storage.c:1137 src/ui/storage.c:1152 src/ui/update_ui.c:898 +#: src/ui/screens.c:1626 src/ui/storage.c:1137 src/ui/storage.c:1152 src/ui/update_ui.c:898 msgid "RESTART NOW" msgstr "" #. TRANSLATORS: Settings > Storage: the switch could not be saved -#: src/ui/screens.c:1622 src/ui/storage.c:795 src/ui/storage.c:1140 src/ui/storage.c:1156 +#: src/ui/screens.c:1629 src/ui/storage.c:795 src/ui/storage.c:1140 src/ui/storage.c:1156 msgid "The setting could not be saved." msgstr "" #. TRANSLATORS: Settings > Power > Memory speed, a faster level chosen -#: src/ui/screens.c:1627 +#: src/ui/screens.c:1634 msgid "The next start tries the faster memory speed. After 30 seconds without a problem, a quick memory test confirms it. If the console freezes, hold the power button to turn it off: it then starts at the normal speed by itself." msgstr "" #. TRANSLATORS: Settings > Power > Memory speed, back to normal -#: src/ui/screens.c:1631 src/ui/storage.c:1160 +#: src/ui/screens.c:1638 src/ui/storage.c:1160 msgid "Restart required: the change applies at the next start." msgstr "" #. TRANSLATORS: Settings section: restart, power off, battery saving -#: src/ui/screens.c:1640 src/ui/screens.c:1898 +#: src/ui/screens.c:1647 src/ui/screens.c:1905 msgctxt "settings" msgid "Power" msgstr "" -#: src/ui/screens.c:1643 +#: src/ui/screens.c:1650 msgid "Restart" msgstr "" -#: src/ui/screens.c:1644 +#: src/ui/screens.c:1651 msgid "Power off" msgstr "" -#: src/ui/screens.c:1645 +#: src/ui/screens.c:1652 msgid "Battery saving" msgstr "" -#: src/ui/screens.c:1646 +#: src/ui/screens.c:1653 msgid "Dim the screen after" msgstr "" -#: src/ui/screens.c:1649 +#: src/ui/screens.c:1656 msgid "Screen off after" msgstr "" #. TRANSLATORS: Settings > Power: turn the console off by itself after #. this long without a button pressed (never while copying or updating) -#: src/ui/screens.c:1654 +#: src/ui/screens.c:1661 msgid "Power off after" msgstr "" -#: src/ui/screens.c:1657 +#: src/ui/screens.c:1664 msgid "Battery gauge" msgstr "" #. TRANSLATORS: Settings > Power section header -#: src/ui/screens.c:1666 +#: src/ui/screens.c:1673 msgid "Performance" msgstr "" #. TRANSLATORS: Settings > Power: the faster memory did not work on this unit -#: src/ui/screens.c:1670 +#: src/ui/screens.c:1677 msgid "Last faster try failed" msgstr "" #. TRANSLATORS: value: the console went back to the normal memory speed -#: src/ui/screens.c:1672 +#: src/ui/screens.c:1679 msgid "normal speed" msgstr "" -#: src/ui/screens.c:1675 +#: src/ui/screens.c:1682 msgid "Memory clock now" msgstr "" #. TRANSLATORS: Settings > Power: the DRAM clock -#: src/ui/screens.c:1680 +#: src/ui/screens.c:1687 msgid "Memory speed" msgstr "" -#: src/ui/screens.c:1716 +#: src/ui/screens.c:1723 #, c-format msgid "Time zone: %s" msgstr "" -#: src/ui/screens.c:1722 src/ui/screens.c:1802 +#: src/ui/screens.c:1729 src/ui/screens.c:1809 msgid "Time zone" msgstr "" -#: src/ui/screens.c:1776 +#: src/ui/screens.c:1783 msgid "That date is not valid." msgstr "" -#: src/ui/screens.c:1781 +#: src/ui/screens.c:1788 msgid "The clock could not be set." msgstr "" -#: src/ui/screens.c:1783 +#: src/ui/screens.c:1790 msgid "Date and time set" msgstr "" -#: src/ui/screens.c:1785 +#: src/ui/screens.c:1792 msgid "Setting the clock needs the power module" msgstr "" -#: src/ui/screens.c:1794 src/ui/screens.c:1897 +#: src/ui/screens.c:1801 src/ui/screens.c:1904 msgid "Date & time" msgstr "" #. TRANSLATORS: the date and time now -#: src/ui/screens.c:1801 +#: src/ui/screens.c:1808 msgid "Now" msgstr "" -#: src/ui/screens.c:1803 +#: src/ui/screens.c:1810 msgid "Set the clock" msgstr "" -#: src/ui/screens.c:1804 +#: src/ui/screens.c:1811 msgid "Year" msgstr "" -#: src/ui/screens.c:1808 +#: src/ui/screens.c:1815 msgid "Month" msgstr "" -#: src/ui/screens.c:1812 +#: src/ui/screens.c:1819 msgid "Day" msgstr "" -#: src/ui/screens.c:1816 +#: src/ui/screens.c:1823 msgid "Hour" msgstr "" -#: src/ui/screens.c:1820 +#: src/ui/screens.c:1827 msgid "Minute" msgstr "" -#: src/ui/screens.c:1824 +#: src/ui/screens.c:1831 msgid "Set date and time" msgstr "" #. TRANSLATORS: Settings item that opens the list of languages -#: src/ui/screens.c:1873 +#: src/ui/screens.c:1880 msgid "Language" msgstr "" -#: src/ui/screens.c:1883 +#: src/ui/screens.c:1890 msgid "Settings" msgstr "" #. TRANSLATORS: Settings item: check for and install a new RetroStoneOS version -#: src/ui/screens.c:1896 src/ui/update_ui.c:1265 +#: src/ui/screens.c:1903 src/ui/update_ui.c:1265 msgid "System update" msgstr "" #. TRANSLATORS: Settings item: find a game by its name in every system -#: src/ui/screens.c:1900 src/ui/screens.c:2279 +#: src/ui/screens.c:1907 src/ui/screens.c:2286 msgid "Search all games" msgstr "" -#: src/ui/screens.c:1939 +#: src/ui/screens.c:1946 #, c-format msgid "%s is not installed" msgstr "" #. TRANSLATORS: first-boot language picker title (shown in each language as #. the cursor moves: keep it short and simple, children read it) -#: src/ui/screens.c:1999 src/ui/screens.c:2061 +#: src/ui/screens.c:2006 src/ui/screens.c:2068 msgid "Choose your language" msgstr "" #. TRANSLATORS: title of the search keyboard, with the number of games #. that match what is typed so far -#: src/ui/screens.c:2210 +#: src/ui/screens.c:2217 #, c-format msgid "Search: %s game" msgid_plural "Search: %s games" @@ -2023,254 +2034,254 @@ msgstr[1] "" #. TRANSLATORS: title of the search keyboard (nothing typed yet) #. TRANSLATORS: game options: find games of this list by name (the #. keyboard opens; the list shows the matches as you type) -#: src/ui/screens.c:2213 src/ui/screens.c:2247 src/ui/screens.c:2718 +#: src/ui/screens.c:2220 src/ui/screens.c:2254 src/ui/screens.c:2725 msgid "Search this list" msgstr "" #. TRANSLATORS: toast: "Search all games" found nothing; %s is the text searched -#: src/ui/screens.c:2268 +#: src/ui/screens.c:2275 #, c-format msgid "No game found for \"%s\"" msgstr "" #. TRANSLATORS: toast after "Hide this game"; the menu path must match #. your translations of Settings, Game lists and Show hidden games -#: src/ui/screens.c:2300 +#: src/ui/screens.c:2307 msgid "Game hidden: Settings > Game lists > Show hidden games shows it again" msgstr "" #. TRANSLATORS: after "Delete this game": the saves question is not asked, because #. another game file has the same name (the saves belong to both) -#: src/ui/screens.c:2372 src/ui/screens.c:2421 +#: src/ui/screens.c:2379 src/ui/screens.c:2428 msgid "Game deleted. Its saves were kept: another game uses the same save files." msgstr "" #. TRANSLATORS: toast after deleting a game and its saves -#: src/ui/screens.c:2377 +#: src/ui/screens.c:2384 msgid "Game and saves deleted" msgstr "" #. TRANSLATORS: toast after deleting a game (its saves are kept) -#: src/ui/screens.c:2380 +#: src/ui/screens.c:2387 msgid "Game deleted" msgstr "" #. TRANSLATORS: the game file could not be deleted; %s is the system's reason -#: src/ui/screens.c:2403 +#: src/ui/screens.c:2410 #, c-format msgid "The game could not be deleted (%s)." msgstr "" #. TRANSLATORS: second question after deleting a game -#: src/ui/screens.c:2433 +#: src/ui/screens.c:2440 msgid "Also delete its saves and save states?" msgstr "" #. TRANSLATORS: delete confirmation; %s is the game's file name (French #. quotes: « %s ») -#: src/ui/screens.c:2466 +#: src/ui/screens.c:2473 #, c-format msgid "Delete \"%s\" from the SD card?" msgstr "" #. TRANSLATORS: added to the delete confirmation of a multi-disc playlist -#: src/ui/screens.c:2471 +#: src/ui/screens.c:2478 msgid "Only this playlist is deleted: the disc files it lists stay on the SD card." msgstr "" #. TRANSLATORS: added to the delete confirmation of a CD image's .cue file -#: src/ui/screens.c:2473 +#: src/ui/screens.c:2480 msgid "Only this .cue file is deleted: the track files it lists stay on the SD card." msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short (CANCEL is selected) -#: src/ui/screens.c:2475 +#: src/ui/screens.c:2482 msgid "DELETE" msgstr "" #. TRANSLATORS: Rename this game: the characters a file name cannot have on the card -#: src/ui/screens.c:2554 +#: src/ui/screens.c:2561 msgid "A game name cannot start with a dot or contain / \\ : * ? \" < > |" msgstr "" #. TRANSLATORS: Rename this game: the new name is taken -#: src/ui/screens.c:2561 +#: src/ui/screens.c:2568 msgid "Another game already has this name." msgstr "" #. TRANSLATORS: Rename this game failed; %s is the system's reason -#: src/ui/screens.c:2568 +#: src/ui/screens.c:2575 #, c-format msgid "The game could not be renamed (%s)." msgstr "" #. TRANSLATORS: toast after Rename this game -#: src/ui/screens.c:2595 +#: src/ui/screens.c:2602 msgid "Game renamed" msgstr "" #. TRANSLATORS: dialog buttons, uppercase, short -#: src/ui/screens.c:2600 +#: src/ui/screens.c:2607 msgid "SEARCH" msgstr "" #. TRANSLATORS: after Rename this game -#: src/ui/screens.c:2603 +#: src/ui/screens.c:2610 msgid "Look for a cover with the new name now?" msgstr "" #. TRANSLATORS: on-screen keyboard title: the game's new file name -#: src/ui/screens.c:2627 +#: src/ui/screens.c:2634 msgid "Rename game" msgstr "" -#: src/ui/screens.c:2638 +#: src/ui/screens.c:2645 msgid "This game is not in the list any more" msgstr "" #. TRANSLATORS: the game is one of the favorites (on/off) -#: src/ui/screens.c:2710 +#: src/ui/screens.c:2717 msgid "Favorite" msgstr "" #. TRANSLATORS: core for this game: the one chosen for the whole system -#: src/ui/screens.c:2725 +#: src/ui/screens.c:2732 msgid "System default" msgstr "" #. TRANSLATORS: a core (emulator) name, marked as not finished -#: src/ui/screens.c:2729 +#: src/ui/screens.c:2736 #, c-format msgid "%s (experimental)" msgstr "" #. TRANSLATORS: the default core of a system, with its name -#: src/ui/screens.c:2734 src/ui/screens.c:2775 +#: src/ui/screens.c:2741 src/ui/screens.c:2782 #, c-format msgid "Default (%s)" msgstr "" #. TRANSLATORS: no core #. TRANSLATORS: value: no card in the slot -#: src/ui/screens.c:2736 src/ui/storage.c:934 +#: src/ui/screens.c:2743 src/ui/storage.c:934 msgid "none" msgstr "" #. TRANSLATORS: "core" = the emulator used to run the game -#: src/ui/screens.c:2750 +#: src/ui/screens.c:2757 msgid "Core for this game" msgstr "" #. TRANSLATORS: %s is a system name ("Core for all Game Boy games") -#: src/ui/screens.c:2761 +#: src/ui/screens.c:2768 #, c-format msgid "Core for all %s games" msgstr "" -#: src/ui/screens.c:2766 +#: src/ui/screens.c:2773 msgid "Core" msgstr "" -#: src/ui/screens.c:2767 +#: src/ui/screens.c:2774 msgid "none installed" msgstr "" #. TRANSLATORS: start the game -#: src/ui/screens.c:2800 +#: src/ui/screens.c:2807 msgid "Launch" msgstr "" #. TRANSLATORS: game options: take the game out of the lists (Settings > #. Game lists > Show hidden games brings it back) -#: src/ui/screens.c:2803 +#: src/ui/screens.c:2810 msgid "Hide this game" msgstr "" #. TRANSLATORS: game options: delete the game's file from the SD card (asks first) -#: src/ui/screens.c:2809 +#: src/ui/screens.c:2816 msgid "Delete this game" msgstr "" #. TRANSLATORS: game options: change the game's file name (covers are found by it) -#: src/ui/screens.c:2812 +#: src/ui/screens.c:2819 msgid "Rename this game" msgstr "" #. TRANSLATORS: game options: the time spent playing this game -#: src/ui/screens.c:2814 +#: src/ui/screens.c:2821 msgid "Play time" msgstr "" #. TRANSLATORS: the game's file name -#: src/ui/screens.c:2817 +#: src/ui/screens.c:2824 msgid "File" msgstr "" #. TRANSLATORS: storage problem screen (the data partition of the SD card could not be read) -#: src/ui/screens.c:2847 +#: src/ui/screens.c:2854 msgid "The SD card reports read errors: it may be failing." msgstr "" #. TRANSLATORS: storage problem screen; %s is a file system name ("ntfs", "ext4") -#: src/ui/screens.c:2850 +#: src/ui/screens.c:2857 #, c-format msgid "The data partition uses a format this console does not use (%s)." msgstr "" #. TRANSLATORS: storage problem screen; %s is a file system name ("exfat") -#: src/ui/screens.c:2853 +#: src/ui/screens.c:2860 #, c-format msgid "The data partition (%s) could not be opened: it may be damaged." msgstr "" #. TRANSLATORS: storage problem screen -#: src/ui/screens.c:2856 +#: src/ui/screens.c:2863 msgid "The data partition could not be opened: it may be damaged." msgstr "" #. TRANSLATORS: title of the screen shown instead of the menu when the storage #. (the SD card's data partition: games, saves) cannot be read -#: src/ui/screens.c:2875 +#: src/ui/screens.c:2882 msgid "The storage could not be read" msgstr "" #. TRANSLATORS: storage problem screen: formatted, the console restarts -#: src/ui/screens.c:2880 +#: src/ui/screens.c:2887 msgid "Done. Restarting..." msgstr "" #. TRANSLATORS: storage problem screen, while the format runs -#: src/ui/screens.c:2882 +#: src/ui/screens.c:2889 msgid "Formatting the storage, please wait. Do not turn the console off." msgstr "" #. TRANSLATORS: storage problem screen, after what happened -#: src/ui/screens.c:2887 +#: src/ui/screens.c:2894 msgid "Nothing was erased: your games and saves may still be recoverable. Turn the console off and read the SD card on a PC to copy them." msgstr "" #. TRANSLATORS: storage problem screen: the format did not work; %d is its exit code -#: src/ui/screens.c:2893 +#: src/ui/screens.c:2900 #, c-format msgid "The storage could not be formatted (error %d)." msgstr "" #. TRANSLATORS: storage problem screen: power off, to read the SD card on a computer -#: src/ui/screens.c:2912 +#: src/ui/screens.c:2919 msgid "Turn off (to back up on a PC)" msgstr "" #. TRANSLATORS: storage problem screen: erase the data partition and start again -#: src/ui/screens.c:2914 +#: src/ui/screens.c:2921 msgid "Format the storage (erases everything)" msgstr "" -#: src/ui/screens.c:2991 +#: src/ui/screens.c:2998 msgid "FORMAT" msgstr "" #. TRANSLATORS: the second question before formatting the storage -#: src/ui/screens.c:2995 +#: src/ui/screens.c:3002 msgid "Erase everything on the storage? All games, saves and settings on it are lost. This cannot be undone." msgstr "" @@ -2994,6 +3005,11 @@ msgctxt "system" msgid "Cave Story" msgstr "" +#: src/ui/systems.c:72 +msgctxt "system" +msgid "Native games" +msgstr "" + #: src/ui/transfer_ui.c:35 msgctxt "system" msgid "NES / Famicom" diff --git a/frontend/src/host/host.h b/frontend/src/host/host.h index b50ebe9..9f83af0 100644 --- a/frontend/src/host/host.h +++ b/frontend/src/host/host.h @@ -207,6 +207,18 @@ struct host_launch_result { int host_launch(const char *core_path, const char *rom_path, const char *system, const struct host_launch_opts *opts, struct host_launch_result *res); +/* + * A native game (system "native"): "/bin/sh script" in its own process + * group (pgid = pid: signal it with kill(-pid, ...), which reaches what the + * script started), with cwd as the working directory and env (NAME=value, + * NULL-terminated) added to this process's environment. Output appended to + * log_path. idle() runs every ~100 ms; true kills the group (SIGKILL). + * No status pipe, no save states: *exit_code = the exit status, or 128 + + * the signal. Returns 0, or -errno if fork/exec failed. + */ +int host_launch_native(const char *script, const char *cwd, const char *const *env, + const char *log_path, bool (*idle)(void *), void *user, int *exit_code); + /* The running game process (0 if none): forward SIGTERM to it. */ pid_t host_child_pid(void); diff --git a/frontend/src/host/launch.c b/frontend/src/host/launch.c index 8893ffb..d232c3d 100644 --- a/frontend/src/host/launch.c +++ b/frontend/src/host/launch.c @@ -28,6 +28,111 @@ pid_t host_child_pid(void) return child_pid; } +extern char **environ; + +int host_launch_native(const char *script, const char *cwd, const char *const *env, + const char *log_path, bool (*idle)(void *), void *user, int *exit_code) +{ + const char *argv[] = { "/bin/sh", script, NULL }; + const char **envp; + char hdr[1200]; + struct timespec ts; + int n = 0, ne = 0, wstatus = 0, hdr_len, log_flags = O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC; + struct stat lst; + pid_t pid; + + *exit_code = -1; + /* the environment is made before fork(): the child only calls + * async-signal-safe functions (the menu has threads) */ + while (environ && environ[n]) + n++; + while (env && env[ne]) + ne++; + envp = calloc((size_t)(n + ne + 1), sizeof(*envp)); + if (!envp) + return -ENOMEM; + n = 0; + for (int i = 0; env && env[i]; i++) + envp[n++] = env[i]; + for (int i = 0; environ && environ[i]; i++) { + const char *eq = strchr(environ[i], '='); + bool replaced = false; + + for (int j = 0; eq && env && env[j]; j++) + if (!strncmp(env[j], environ[i], (size_t)(eq - environ[i]) + 1)) + replaced = true; + if (!replaced) + envp[n++] = environ[i]; + } + envp[n] = NULL; + + clock_gettime(CLOCK_MONOTONIC, &ts); + hdr_len = snprintf(hdr, sizeof(hdr), "\n=== [%5lld.%03ld] launch native: %s\n", + (long long)ts.tv_sec, ts.tv_nsec / 1000000, script); + if (hdr_len < 0) + hdr_len = 0; + if (hdr_len > (int)sizeof(hdr) - 1) + hdr_len = (int)sizeof(hdr) - 1; + if (stat(log_path, &lst) == 0 && lst.st_size > (1 << 20)) + log_flags |= O_TRUNC; /* keep /run small */ + + pid = fork(); + if (pid < 0) { + int e = errno; + + free(envp); + return -e; + } + if (pid == 0) { + sigset_t none; + int fd; + + sigemptyset(&none); + sigprocmask(SIG_SETMASK, &none, NULL); + signal(SIGPIPE, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + setsid(); /* its own process group: kill(-pid) */ + fd = open(log_path, log_flags, 0644); + if (fd >= 0) { + if (hdr_len > 0 && write(fd, hdr, (size_t)hdr_len) < 0) { + /* the game runs without its header */ + } + dup2(fd, 1); + dup2(fd, 2); + } + if (chdir(cwd) < 0) + _exit(126); + execve(argv[0], (char *const *)argv, (char *const *)envp); + _exit(127); + } + free(envp); + child_pid = pid; + hlog(HLOG_INFO, "native game %d started: %s", (int)pid, script); + + for (;;) { + pid_t w = waitpid(pid, &wstatus, WNOHANG); + + if (w == pid) + break; + if (w < 0 && errno != EINTR) + break; + if (idle && idle(user)) { + hlog(HLOG_WARN, "killing native game %d on request", (int)pid); + kill(-pid, SIGKILL); + } + poll(NULL, 0, 100); + } + /* what the script started and left behind goes too */ + kill(-pid, SIGKILL); + child_pid = 0; + if (WIFEXITED(wstatus)) + *exit_code = WEXITSTATUS(wstatus); + else if (WIFSIGNALED(wstatus)) + *exit_code = 128 + WTERMSIG(wstatus); + hlog(HLOG_INFO, "native game %d ended: %d", (int)pid, *exit_code); + return 0; +} + static void parse_line(char *line, const struct host_launch_opts *opts, struct host_launch_result *res) { char *arg = strchr(line, ' '); diff --git a/frontend/src/main.c b/frontend/src/main.c index dfffaa4..bd5a39f 100644 --- a/frontend/src/main.c +++ b/frontend/src/main.c @@ -66,6 +66,7 @@ #include "i18n/i18n.h" #include "splash.h" #include "ui/settings.h" +#include "ui/systems.h" #include "ui/ui.h" #include "ui/util.h" #include "board_apply.h" /* after ui/ui.h: board_apply_ui() */ @@ -77,6 +78,7 @@ #define MENU_UP_PATH "/run/rsos/menu-up" /* written once the menu is on screen */ #define CHILD_GRACE_MS 8000 /* game child: time to save after SIGTERM/SIGUSR2 */ #define HANG_KILL_MS 5000 /* Select+Start held this long: kill the game */ +#define NATIVE_QUIT_MS 1000 /* native game: Select+Start held this long asks it to quit */ #define SHUTDOWN_MSG_MS 0 /* no hold: the "Powering off..." frame is drawn at the next loop pass, then the state is flushed and init signalled right away (docs/power.md, shutdown) */ @@ -174,6 +176,7 @@ static struct { /* game */ bool in_game; + bool native; /* the game is a native one (host_launch_native: a process group) */ int64_t combo_since; int64_t term_sent_at; int64_t poweroff_sent_at; @@ -1271,12 +1274,50 @@ static void pw_critical(const struct power_status *st, void *user) scr_draw(); } +/* A native game's sleep: every backlight's bl_power (0 = on, 4 = off). */ +static void native_backlight(bool on) +{ + const char *dir = P("/sys/class/backlight"); + DIR *d = opendir(dir); + struct dirent *de; + + if (!d) + return; + while ((de = readdir(d))) { + char p[PATH_MAX]; + int fd; + + if (de->d_name[0] == '.') + continue; + snprintf(p, sizeof(p), "%s/%s/bl_power", dir, de->d_name); + fd = open(p, O_WRONLY | O_CLOEXEC); + if (fd < 0) + continue; + if (write(fd, on ? "0\n" : "4\n", 2) < 0) + mlog("%s: %s", p, strerror(errno)); + close(fd); + } + closedir(d); +} + static void pw_sleep(bool enter, void *user) { pid_t c = host_child_pid(); (void)user; mlog("%s", enter ? "sleep" : "wake"); + if (M.in_game && M.native && c > 0) { + /* a native game knows nothing of RetroStoneOS: frozen (sound and + * picture stop), the backlight off by hand (it owns the display) */ + if (enter) { + kill(-c, SIGSTOP); + native_backlight(false); + } else { + native_backlight(true); + kill(-c, SIGCONT); + } + return; + } if (M.in_game && c > 0) { /* the game pauses, closes ALSA and turns its own display off */ kill(c, enter ? RSOS_SIG_SLEEP : RSOS_SIG_WAKE); @@ -1349,6 +1390,14 @@ static void pw_shutdown(enum power_reason why, void *user) * gamedb, caches */ if (M.ui) ui_power_event(M.ui, why == POWER_REASON_REBOOT ? UI_PWR_REBOOT : UI_PWR_SHUTDOWN); + if (M.in_game && M.native && c > 0) { + /* no save state: the game saves on its own (sm64: at each star); + * SIGCONT in case it was asleep */ + kill(-c, SIGTERM); + kill(-c, SIGCONT); + M.poweroff_sent_at = now_ms(); + return; + } if (M.in_game && c > 0) { /* SRAM flush + .state.auto, exit 3; launch_idle() waits for it * (then SIGKILL after CHILD_GRACE_MS) and the launch callback @@ -1386,6 +1435,8 @@ static void pw_idle(enum power_idle_event ev, int seconds, void *user) (void)user; mlog("idle power-off: %s", ev == POWER_IDLE_WARN ? "notice (10 s)" : "cancelled"); (void)seconds; + if (M.in_game && M.native) + return; /* held off (game_nostate): it cannot come */ if (M.in_game && c > 0) { kill(c, ev == POWER_IDLE_WARN ? RSOS_SIG_IDLE_WARN : RSOS_SIG_IDLE_CANCEL); return; @@ -1773,7 +1824,7 @@ static bool launch_idle(void *user) if (M.quit && c > 0) { if (!M.term_sent_at) { mlog("signal %d: asking the game to quit", (int)M.quit); - kill(c, SIGTERM); + kill(M.native ? -c : c, SIGTERM); M.term_sent_at = t; } else if (t - M.term_sent_at > CHILD_GRACE_MS) { return true; @@ -1795,6 +1846,13 @@ static bool launch_idle(void *user) if (awake && (input_any_buttons(M.in) & combo) == combo) { if (!M.combo_since) { M.combo_since = t; + } else if (M.native && !M.term_sent_at && c > 0 && t - M.combo_since >= NATIVE_QUIT_MS) { + /* a native game has no menu of ours: Select+Start + * held quits it (SIGKILL after HANG_KILL_MS) */ + mlog("Select+Start held: asking the native game to quit"); + kill(-c, SIGTERM); + kill(-c, SIGCONT); + M.term_sent_at = t; } else if (t - M.combo_since >= HANG_KILL_MS) { mlog("Select+Start held %d s: killing the game", HANG_KILL_MS / 1000); M.combo_since = 0; @@ -1838,6 +1896,8 @@ static bool has_resume(const struct ui_launch *req, void *user) const char *core_path = req->core_path; (void)user; + if (systems_is_native(req->system)) + return false; if (!core_path || !*core_path || access(core_path, R_OK) != 0) { if (host_pick_core(req->system, req->rom_path, core, sizeof(core)) < 0) return false; @@ -2306,6 +2366,109 @@ static int game_saves(const struct ui_launch *req, bool remove, void *user) return n; } +/* + * A native game (system "native", docs/native-games.md): its launch script + * runs with /bin/sh in the script's folder, in its own process group, with + * the display, sound and pads handed over as for a libretro game. It has + * no save states, no in-game menu and no RetroStoneOS signals: Select+Start + * held quits it (SIGTERM, SIGKILL later), sleep freezes it, a power-off + * stops it, and the idle power-off waits until it has ended. + */ +static int launch_native(const struct ui_launch *req) +{ + char dir[PATH_MAX], home[PATH_MAX], e_home[PATH_MAX + 8], e_cfg[PATH_MAX + 32]; + char e_data[PATH_MAX + 32], e_dir[PATH_MAX + 16], e_lang[64]; + const char *env[12]; + const char *slash; + bool suspended; + int r, exit_code = -1, ne = 0, ret = 0; + + slash = strrchr(req->rom_path, '/'); + if (!slash || access(req->rom_path, R_OK) != 0) { + set_msg(req, "%s", _("This game cannot start: its launch script is missing.")); + mlog("launch native %s: %s", req->rom_path, strerror(errno)); + return -1; + } + snprintf(dir, sizeof(dir), "%.*s", (int)(slash - req->rom_path), req->rom_path); + /* the root file system is read-only: settings a game keeps in its + * HOME go to the data partition */ + snprintf(home, sizeof(home), "%s", P("/data/rsos/native-home")); + if (mkdir(home, 0755) < 0 && errno != EEXIST) + mlog("%s: %s", home, strerror(errno)); + snprintf(e_home, sizeof(e_home), "HOME=%s", home); + snprintf(e_cfg, sizeof(e_cfg), "XDG_CONFIG_HOME=%s/.config", home); + snprintf(e_data, sizeof(e_data), "XDG_DATA_HOME=%s/.local/share", home); + snprintf(e_dir, sizeof(e_dir), "RSOS_GAME_DIR=%s", dir); + snprintf(e_lang, sizeof(e_lang), "RSOS_LANG=%s", i18n_language()); + env[ne++] = e_home; + env[ne++] = e_cfg; + env[ne++] = e_data; + env[ne++] = e_dir; + env[ne++] = e_lang; + /* SDL games: straight to KMS and ALSA, no window system here */ + env[ne++] = "SDL_VIDEODRIVER=kmsdrm"; + env[ne++] = "SDL_AUDIODRIVER=alsa"; + env[ne] = NULL; + + if (M.in) { + input_set_mode(M.in, INPUT_MODE_GAME); + input_set_docked(M.in, true); + } + if (M.power_ok) + power_set_game_running(true); + suspended = scr_suspend() == 0; + if (!suspended) { + mlog("display_suspend failed: closing the display for the game"); + scr_shutdown(); + } + M.in_game = M.native = true; + M.combo_since = M.term_sent_at = M.poweroff_sent_at = 0; + M.game_busy = M.resume_recorded = false; + M.game_nostate = true; /* no resume state: the idle power-off waits */ + idle_busy_check(true); + M.game_log_checked_at = 0; + mlog("launch native %s (%s)", req->game_name, req->rom_path); + + r = host_launch_native(req->rom_path, dir, env, P(GAME_LOG_PATH), launch_idle, (void *)req, &exit_code); + + native_backlight(true); /* asleep when it was stopped */ + M.in_game = M.native = false; + M.game_buttons = 0; + M.game_nostate = false; + idle_busy_check(true); + /* the menu comes back, also to show "Powering off..." */ + restore_display_after_game(suspended); + if (M.in) { + input_set_docked(M.in, M.out.type == DISPLAY_OUTPUT_HDMI); + input_drain(); + input_set_mode(M.in, INPUT_MODE_UI); + } + if (M.power_ok) + power_set_game_running(false); + + if (r < 0) { + set_msg(req, _("Cannot start the game process (%s)."), strerror(-r)); + mlog("host_launch_native: %s", strerror(-r)); + ret = -1; + } else { + mlog("native game ended: %d", exit_code); + /* 143 = SIGTERM, 137 = SIGKILL: Select+Start or a power-off */ + if (exit_code != 0 && exit_code != 128 + SIGTERM && exit_code != 128 + SIGKILL && + !M.term_sent_at && !M.poweroff_sent_at && !M.quit && !M.shutdown_req) { + /* TRANSLATORS: a native game (no emulator) ended with an + * error; %d is its exit code */ + set_msg(req, _("The game ended with an error (%d). Its messages are in rsos/logs on the SD card."), + exit_code); + ret = -1; + } + } + if (M.shutdown_req && !M.shutdown_at) + M.shutdown_at = now_ms() + SHUTDOWN_MSG_MS; + M.poweroff_sent_at = 0; + M.need_redraw = true; + return ret; +} + static int launch_game(const struct ui_launch *req, void *user) { char core[PATH_MAX], msg[256] = "", game[256], p1dev[320] = ""; @@ -2319,6 +2482,8 @@ static int launch_game(const struct ui_launch *req, void *user) int na = 0, r, ret = 0; (void)user; + if (systems_is_native(req->system)) + return launch_native(req); /* The UI picked the core (game > system > default); host_pick_core() * only when it has no library path. */ if (!core_path || !*core_path || access(core_path, R_OK) != 0) { diff --git a/frontend/src/transfer/sysmap.c b/frontend/src/transfer/sysmap.c index 71ed902..e898478 100644 --- a/frontend/src/transfer/sysmap.c +++ b/frontend/src/transfer/sysmap.c @@ -76,6 +76,8 @@ static const struct sysmap g_sys[] = { { "pico8", "PICO-8", { "p8", "fake08", NULL } }, { "doom", "Doom", { "prboom", "gzdoom", NULL } }, { "cavestory", "Cave Story", { "nxengine", "doukutsu", NULL } }, + /* not "ports": RetroPie's port scripts start its own emulators */ + { "native", "Native games", { "nativegames", NULL } }, }; #define NSYS ((int)(sizeof(g_sys) / sizeof(g_sys[0]))) diff --git a/frontend/src/ui/systems.c b/frontend/src/ui/systems.c index 903ed76..43338f6 100644 --- a/frontend/src/ui/systems.c +++ b/frontend/src/ui/systems.c @@ -67,6 +67,9 @@ static const struct sysdef g_systems[] = { { "pico8", NC_("system", "PICO-8"), "Fantasy console", { "pico8", "pico-8", NULL }, { "pico-8", NULL }, "fake08", "p8,png", 2015 }, { "doom", NC_("system", "Doom"), "Ports", { "doom", "ports", NULL }, { "prboom", NULL }, "prboom", "wad,iwad,pwad", 1993 }, { "cavestory", NC_("system", "Cave Story"), "Ports", { "cavestory", "ports", NULL }, { "nxengine", NULL }, "nxengine", "exe", 2004 }, + /* native games: programs built for this console, no emulator (one + * launch script per game, docs/native-games.md) */ + { "native", NC_("system", "Native games"), "Ports", { "native", "ports", NULL }, { NULL }, "", "sh", 0 }, }; /* Used only when no core .ini is installed (development host). */ @@ -247,6 +250,11 @@ const struct sysdef *systems_find(const char *name) return NULL; } +bool systems_is_native(const char *system) +{ + return system && !strcmp(system, "native"); +} + static bool any_core_runs(const char *system) { for (int i = 0; i < g_ncores; i++) @@ -260,7 +268,7 @@ static void build_list(void) g_list = xrealloc(g_list, sizeof(*g_list) * (ARRAY_SIZE(g_systems) + 64)); g_nlist = 0; for (size_t i = 0; i < ARRAY_SIZE(g_systems); i++) { - if (any_core_runs(g_systems[i].name)) + if (any_core_runs(g_systems[i].name) || systems_is_native(g_systems[i].name)) g_list[g_nlist++] = &g_systems[i]; else LOGD("systems: no core for %s", g_systems[i].name); @@ -353,7 +361,11 @@ const struct core_info *systems_core(const char *id) bool systems_ext_ok(const char *system, const char *ext) { const struct core_info *cs[MAX_CORES_PER_SYSTEM]; - int n = systems_cores_for(system, cs, MAX_CORES_PER_SYSTEM); + int n; + + if (systems_is_native(system)) + return !strcasecmp(ext, "sh"); + n = systems_cores_for(system, cs, MAX_CORES_PER_SYSTEM); for (int i = 0; i < n; i++) if (list_has(cs[i]->exts, ext)) diff --git a/frontend/src/ui/systems.h b/frontend/src/ui/systems.h index e58946c..d3feff1 100644 --- a/frontend/src/ui/systems.h +++ b/frontend/src/ui/systems.h @@ -62,5 +62,8 @@ int systems_cores_for_file(const char *system, const char *path, const struct core_info *systems_core(const char *id); /* True if ext (no dot, any case) is a ROM extension for the system. */ bool systems_ext_ok(const char *system, const char *ext); +/* "native": games built for the console, started by their own launch + * script (no core) */ +bool systems_is_native(const char *system); #endif diff --git a/frontend/src/ui/ui.c b/frontend/src/ui/ui.c index 701e001..de23f8b 100644 --- a/frontend/src/ui/ui.c +++ b/frontend/src/ui/ui.c @@ -832,7 +832,7 @@ static void switch_launch(struct ui *ui) req.core = ui->switch_req.core; req.core_path = ci ? ci->library : ""; } - if (!req.core[0]) { + if (!req.core[0] && !systems_is_native(req.system)) { message_open(ui, _("No emulator is installed for this system.")); return; } @@ -935,7 +935,7 @@ void ui_launch_game(struct ui *ui, struct sysent *se, struct game *g) buttons[1] = _("START FRESH"); buttons[2] = NULL; fill_launch(ui, g, &req); - if (!req.core[0]) { + if (!req.core[0] && !systems_is_native(req.system)) { message_open(ui, _("No emulator is installed for this system.")); return; } diff --git a/ports/2ship/.gitattributes b/ports/2ship/.gitattributes new file mode 100644 index 0000000..ec1ffa8 --- /dev/null +++ b/ports/2ship/.gitattributes @@ -0,0 +1 @@ +*.patch -text diff --git a/ports/2ship/2ship.patch b/ports/2ship/2ship.patch new file mode 100644 index 0000000..11fe45c --- /dev/null +++ b/ports/2ship/2ship.patch @@ -0,0 +1,206 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index a9378e8..b31788d 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -123,6 +123,9 @@ elseif(MSVC) + string(REGEX REPLACE "/RTC(su|[1su])" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") + set(CMAKE_C_FLAGS_RELEASE "/O2 /DNDEBUG") + set(CMAKE_CXX_FLAGS_RELEASE "/O2 /DNDEBUG") ++elseif(USE_GLES2) ++ set(CMAKE_C_FLAGS_RELEASE "-O3 -fno-math-errno -funsafe-math-optimizations -DNDEBUG") ++ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -fno-math-errno -funsafe-math-optimizations -DNDEBUG") + else() + set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG") + set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") +diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp +index 653de66..eee4d60 100644 +--- a/mm/2s2h/BenPort.cpp ++++ b/mm/2s2h/BenPort.cpp +@@ -836,15 +836,20 @@ void OTRAudio_Thread() { + int samples_left = AudioPlayer_Buffered(); + u32 num_audio_samples = samples_left < AudioPlayer_GetDesiredBuffered() ? SAMPLES_HIGH : SAMPLES_LOW; + +- // 3 is the maximum authentic frame divisor. +- s16 audio_buffer[SAMPLES_HIGH * NUM_AUDIO_CHANNELS * 3]; +- for (int i = 0; i < AUDIO_FRAMES_PER_UPDATE; i++) { ++ // 3 is the maximum authentic frame divisor, plus up to 3 frames to catch up (below) ++ int audioFrames = AUDIO_FRAMES_PER_UPDATE; ++ if (samples_left < AudioPlayer_GetDesiredBuffered()) { ++ int extra = (AudioPlayer_GetDesiredBuffered() - samples_left + SAMPLES_HIGH - 1) / SAMPLES_HIGH; ++ audioFrames += extra > 3 ? 3 : extra; ++ } ++ s16 audio_buffer[SAMPLES_HIGH * NUM_AUDIO_CHANNELS * 6]; ++ for (int i = 0; i < audioFrames; i++) { + AudioMgr_CreateNextAudioBuffer(audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS), + num_audio_samples); + } + + AudioPlayer_Play((u8*)audio_buffer, +- num_audio_samples * (sizeof(int16_t) * NUM_AUDIO_CHANNELS * AUDIO_FRAMES_PER_UPDATE)); ++ num_audio_samples * (sizeof(int16_t) * NUM_AUDIO_CHANNELS * audioFrames)); + + audio.processing = false; + audio.cv_from_thread.notify_one(); +diff --git a/mm/2s2h/mixer.c b/mm/2s2h/mixer.c +index 6364fa5..6cccdb4 100644 +--- a/mm/2s2h/mixer.c ++++ b/mm/2s2h/mixer.c +@@ -18,6 +18,8 @@ + + //#define DMEM_BUF_SIZE (0x1000 - 0x0330 - 0x10 - 0x40) + #define DMEM_BUF_SIZE 0xC80 ++// past the end of DMEM: the largest 16-bit address plus the largest command (mixing: count << 4) ++#define DMEM_GUARD_SIZE 0x110000 + #define BUF_U8(a) (rspa.buf.as_u8 + ((a)-0x0330)) + #define BUF_S16(a) (rspa.buf.as_s16 + ((a)-0x0330) / sizeof(int16_t)) + +@@ -39,8 +41,8 @@ static struct { + int16_t filter[8]; + + union { +- int16_t as_s16[DMEM_BUF_SIZE / sizeof(int16_t)]; +- uint8_t as_u8[DMEM_BUF_SIZE]; ++ int16_t as_s16[(DMEM_BUF_SIZE + DMEM_GUARD_SIZE) / sizeof(int16_t)]; ++ uint8_t as_u8[DMEM_BUF_SIZE + DMEM_GUARD_SIZE]; + } buf; + } rspa; + +@@ -90,8 +92,17 @@ static inline int32_t clamp32(int64_t v) { + return (int32_t)v; + } + ++// a length that ends inside the buffer and its guard (none if negative) ++static int DmemFit(uint16_t addr, int nbytes) { ++ long room = (long)sizeof(rspa.buf) - ((long)addr - 0x0330); ++ if (nbytes <= 0 || room <= 0) { ++ return 0; ++ } ++ return nbytes > room ? (int)room : nbytes; ++} ++ + void aClearBufferImpl(uint16_t addr, int nbytes) { +- nbytes = ROUND_UP_16(nbytes); ++ nbytes = DmemFit(addr, ROUND_UP_16(nbytes)); + memset(BUF_U8(addr), 0, nbytes); + } + +@@ -173,7 +184,7 @@ void aInterleaveImpl(uint16_t dest, uint16_t left, uint16_t right, uint16_t c) { + } + + void aDMEMMoveImpl(uint16_t in_addr, uint16_t out_addr, int nbytes) { +- nbytes = ROUND_UP_16(nbytes); ++ nbytes = DmemFit(in_addr, DmemFit(out_addr, ROUND_UP_16(nbytes))); + memmove(BUF_U8(out_addr), BUF_U8(in_addr), nbytes); + } + +@@ -721,11 +732,11 @@ void aMixImplNEON(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_a + int16x8_t outLoVec2 = vcombine_s16(outLoVec, outLoVec); + int16x4_t inLoVec = vget_low_s16(inVec); + int16x8_t inLoVec2 = vcombine_s16(inLoVec, inLoVec); +- int32x4_t outX7fffHiVec = vmull_high_n_s16(outVec, 0x7FFF); +- int32x4_t outX7fffLoVec = vmull_high_n_s16(outLoVec2, 0x7FFF); ++ int32x4_t outX7fffHiVec = vmull_n_s16(vget_high_s16(outVec), 0x7FFF); ++ int32x4_t outX7fffLoVec = vmull_n_s16(vget_high_s16(outLoVec2), 0x7FFF); + +- int32x4_t inGainLoVec = vmull_high_s16(inLoVec2, gainVec); +- int32x4_t inGainHiVec = vmull_high_s16(inVec, gainVec); ++ int32x4_t inGainLoVec = vmull_s16(vget_high_s16(inLoVec2), vget_high_s16(gainVec)); ++ int32x4_t inGainHiVec = vmull_s16(vget_high_s16(inVec), vget_high_s16(gainVec)); + int32x4_t addVecLo = vaddq_s32(outX7fffLoVec, inGainLoVec); + int32x4_t addVecHi = vaddq_s32(outX7fffHiVec, inGainHiVec); + addVecHi = vaddq_s32(addVecHi, x4000Vec); +diff --git a/mm/2s2h/resource/importer/AudioSampleFactory.cpp b/mm/2s2h/resource/importer/AudioSampleFactory.cpp +index 8948a67..a18b98e 100644 +--- a/mm/2s2h/resource/importer/AudioSampleFactory.cpp ++++ b/mm/2s2h/resource/importer/AudioSampleFactory.cpp +@@ -190,6 +190,26 @@ static void OggDecoderWorker(std::shared_ptr audioSample, std: + } + } + ++// the sample's length in samples, for AdpcmLoop.sampleEnd (0x0C) ++static void SetSampleEnd(SOH::AdpcmLoop& loop, uint32_t codec, uint32_t size) { ++ uint32_t samples; ++ switch (codec) { ++ case 0: // CODEC_ADPCM: 16 samples in 9 bytes ++ samples = size / 9 * 16; ++ break; ++ case 3: // CODEC_SMALL_ADPCM: 16 samples in 5 bytes ++ samples = size / 5 * 16; ++ break; ++ case 1: // CODEC_S8 ++ samples = size; ++ break; ++ default: // 16-bit samples ++ samples = size / 2; ++ break; ++ } ++ memcpy(loop.unk_0C, &samples, sizeof(samples)); ++} ++ + namespace SOH { + std::shared_ptr + ResourceFactoryBinaryAudioSampleV2::ReadResource(std::shared_ptr file, +@@ -225,6 +245,7 @@ ResourceFactoryBinaryAudioSampleV2::ReadResource(std::shared_ptr fil + audioSample->loop.state[i] = reader->ReadInt16(); + } + audioSample->sample.loop = &audioSample->loop; ++ SetSampleEnd(audioSample->loop, audioSample->sample.codec, audioSample->sample.size); + + audioSample->book.order = reader->ReadInt32(); + audioSample->book.npredictors = reader->ReadInt32(); +@@ -288,6 +309,7 @@ ResourceFactoryXMLAudioSampleV0::ReadResource(std::shared_ptr file, + audioSample->sample.loop = &audioSample->loop; + size_t size = child->Int64Attribute("Size"); + audioSample->sample.size = size; ++ SetSampleEnd(audioSample->loop, audioSample->sample.codec, audioSample->sample.size); + + const char* path = child->Attribute("Path"); + +diff --git a/mm/assets/custom/shaders/opengl/default.shader.glsl b/mm/assets/custom/shaders/opengl/default.shader.glsl +index 5309260..c204478 100644 +--- a/mm/assets/custom/shaders/opengl/default.shader.glsl ++++ b/mm/assets/custom/shaders/opengl/default.shader.glsl +@@ -134,14 +134,18 @@ + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++ @if(gles2) ++ return fract(sin(random) * 43758.5453); ++ @else + return fract(sin(random) * 143758.5453); ++ @end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -187,7 +191,9 @@ + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/mm/src/audio/lib/playback.c b/mm/src/audio/lib/playback.c +index a349540..c7526d0 100644 +--- a/mm/src/audio/lib/playback.c ++++ b/mm/src/audio/lib/playback.c +@@ -196,7 +196,7 @@ void AudioPlayback_ProcessNotes(void) { + sampleState = &gAudioCtx.sampleStateList[gAudioCtx.sampleStateOffset + i]; + playbackState = ¬e->playbackState; + if (playbackState->parentLayer != NO_LAYER) { +-#ifndef __WIIU__ ++#if !defined(__WIIU__) && UINTPTR_MAX > 0xFFFFFFFFu // 32-bit pointers are all below 0x7FFFFFFF + // OTRTODO: This skips playback if the pointer is below where memory on the N64 normally would be. + // This does not translate well to modern platforms and how they map memory. + // Considering that this check is not present in OoT/SoH, we may be able to remove this altogether. diff --git a/ports/2ship/Majora's Mask.sh b/ports/2ship/Majora's Mask.sh new file mode 100644 index 0000000..3cd5f3b --- /dev/null +++ b/ports/2ship/Majora's Mask.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# The Legend of Zelda: Majora's Mask (2Ship2Harkinian), built from your own ROM by +# ports/2ship/build-2ship.sh. The game data (mm.o2r, 2ship.o2r), settings and saves stay in 2ship/. +cd 2ship || exit 1 +export SHIP_HOME="$PWD" +# Opus and OpusFile, copied next to the game for a RetroStoneOS without them +export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +# The built-in controls as one standard gamepad (rsos-gamepad); SDL then +# ignores the raw button devices. It runs in the +# game's process group: the menu stops it with the game. +if [ -x /usr/bin/rsos-gamepad ]; then + SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" + export SDL_GAMECONTROLLERCONFIG + export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 + /usr/bin/rsos-gamepad & + usleep 300000 +fi +# Not through /lib/ld-linux-armhf.so.3 as the other ports can: 2Ship looks for its .o2r files +# next to the program it was started as, which would then be /lib +exec ./2s2h.elf diff --git a/ports/2ship/README.md b/ports/2ship/README.md new file mode 100644 index 0000000..9b10c1e --- /dev/null +++ b/ports/2ship/README.md @@ -0,0 +1,101 @@ +# The Legend of Zelda: Majora's Mask (2Ship2Harkinian) on the RetroStone2 + +[2Ship2Harkinian](https://github.com/HarbourMasters/2ship2harkinian) is the PC port of the Majora's Mask +decompilation, built on HarbourMasters' [libultraship](https://github.com/Kenix3/libultraship), like Ocarina of +Time ([../soh](../soh/README.md)). This folder adds the same OpenGL ES 2.0 mode to 2Ship's libultraship, fixes +2Ship for 32-bit ARM and builds the game for the console as a native game (docs/native-games.md). + +**Nothing from the game is in this repository, the OS images or the updates.** The game's data comes from **your +own ROM** on your PC and stays yours. Tested with the N64 US ROM (SHA-1 `d6133ace5afaa0882cf214cf88daba39e266c078` +in `.z64` order); 2Ship's extractor also takes the decompressed N64 US ROM and the GameCube US one. `.z64`, `.v64` +and `.n64` byte orders are all fine. + +## Build + +2Ship's extractor (ZAPD) runs on the PC and is built with libultraship, so the PC needs libultraship's libraries +too, once: + +```sh +sudo apt install libsdl2-dev libsdl2-net-dev libpng-dev libzip-dev zipcmp zipmerge ziptool libtinyxml2-dev \ + nlohmann-json3-dev libspdlog-dev libgles-dev libegl-dev libogg-dev libvorbis-dev libopusfile-dev libbz2-dev +``` + +Then: + +```sh +ports/2ship/build-2ship.sh ~/"Majora's Mask (U).z64" ~/my-ports +``` + +It fetches 2Ship2Harkinian at a fixed commit into `~/rsos/2ship-work`, applies `2ship.patch` and +`libultraship-gles2.patch`, builds ZAPD for the PC and makes `mm.o2r` from the ROM and `2ship.o2r` (which carries +the patched shaders), builds a static libpng and the game for the console (`USE_GLES2`, the RetroStoneOS cross +compiler), then writes: + +``` +~/my-ports/roms/native/Majora's Mask.sh +~/my-ports/roms/native/2ship/ 2s2h.elf, mm.o2r, 2ship.o2r, gamecontrollerdb.txt, lib/, assets/ +``` + +Copy the `roms` folder onto the SD card's **RETROSTONE** partition; in the menu: **Native games > Majora's +Mask**. Settings and saves stay in `roms/native/2ship/` (`SHIP_HOME`). Hold **Select + Start** for a second to go +back to the menu. + +The console needs a RetroStoneOS with Native games and `rsos-gamepad`. Opus and OpusFile are in the RetroStone2 +configs too; `build-2ship.sh` also copies them into `2ship/lib/` for a RetroStoneOS that doesn't have them. + +## Controls + +`rsos-gamepad` makes the built-in controls one SDL gamepad, by position. By the labels on the console: + +| RetroStone2 | N64 | +|---|---| +| B (bottom) / A (right) | A / B | +| X (top) / Y (left) / R1 | C-down / C-left / C-right | +| L2 / R2 | Z / R | +| L1 | L | +| Start | Start | +| Analog stick (Pro) | the stick | +| D-pad | the D-pad | + +As in Ocarina of Time, the C buttons go on X, Y and R1 (the default with `USE_GLES2`); C-up is free to set in +2Ship's own controller settings. + +## What the patches change + +* **`libultraship-gles2.patch`**: Ship of Harkinian's GLES2 patch ([../soh](../soh/README.md): the OpenGL ES 2.0 + mode, the 32-bit ARM and Mali-400 fixes, the speed work), which applies to 2Ship's libultraship (`7cb1022`) as + it is, plus: + * backported from newer libultraship (`766a6bad`): SIGTERM no longer calls `exit()` inside the signal handler, + whose destructors deadlocked, so quitting from the menu is immediate (as for Mario Kart 64); + * loading a texture that is already bound no longer ends the draw call (as for Mario Kart 64); + * first start: linear texture filtering (one sample instead of three-point's three); + * the crash handler also catches SIGBUS and, on 32-bit ARM, logs the registers and the program's load address. +* **`2ship.patch`**, 2Ship's own code: + * **notes on 32-bit** (an upstream bug, the cause of the broken music): `AudioPlayback_ProcessNotes` skips a + note whose layer pointer is below `0x7FFFFFFF`, a check from the N64, where RAM starts at `0x80000000`. On a + 64-bit PC no heap pointer is that low; on 32-bit ARM every one is, so no playing note got its volume, pitch or + start, and instruments were cut off or missing. The check is now for 64-bit builds only (2Ship already left + it out on the Wii U, its other 32-bit platform); + * **the mixer's DMEM buffer** (an upstream bug, found with AddressSanitizer on the console): some audio commands + clear or move more bytes than the 0xC80-byte buffer holds, which overwrote the memory after it (crashes and a + freeze in the opening cutscene). The buffer has a guard area and those lengths are clamped to it; + * **samples' `sampleEnd`** (an upstream bug): the sample importers never set it, so the synthesizer read + leftover memory for where a sample ends. It is now the sample's length; + * when the game runs below 20 fps, the audio thread makes up to three extra audio frames to refill the buffer, + instead of gaps; + * the GLES2 changes in 2Ship's own copy of the shaders (packed into `2ship.o2r`); + * `mixer.c`'s NEON code uses AArch64-only intrinsics: their ARMv7 equivalents; + * Release builds with `USE_GLES2` use `-O3 -funsafe-math-optimizations` instead of 2Ship's `-O2` on Linux: + GCC uses NEON for float math on 32-bit ARM only with unsafe math. + +The launcher starts `2s2h.elf` directly, never through `/lib/ld-linux-armhf.so.3` as the other ports can: 2Ship +looks for its `.o2r` files next to the program it was started as, which would be `/lib`. + +## Status + +**Works on a RetroStone2 Pro**: title screen, the opening, 3D, music and sound, at the game's own speed (20 fps +in most places). The game's messages are in `rsos/logs/boot/game.log` on the SD card, and 2Ship's own log in +`roms/native/2ship/logs/`. + +`mm.o2r` is not the same byte for byte from one build to the next: ZAPD writes uninitialized memory into fields the +game doesn't use (the vertex count of limbs without skin, the tuning of drums without a sample). diff --git a/ports/2ship/build-2ship.sh b/ports/2ship/build-2ship.sh new file mode 100755 index 0000000..cf70cf7 --- /dev/null +++ b/ports/2ship/build-2ship.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Builds The Legend of Zelda: Majora's Mask (2Ship2Harkinian) for RetroStoneOS from +# YOUR OWN ROM, on the PC (Linux/WSL), as a native game for the SD card +# (docs/native-games.md). +# +# ports/2ship/build-2ship.sh [output folder] +# +# Needs a built RetroStoneOS tree for the RetroStone2 with SDL2 and the port +# libraries (the RetroStone2 configs): ~/rsos/output, or RSOS_OUTPUT=; cmake, +# ninja, python3, a C++ compiler for the PC and the PC libraries of 2Ship's extractor +# (ZAPD, which runs here and is built with libultraship): see README.md. +# 2Ship2Harkinian is fetched at a fixed commit into ~/rsos/2ship-work +# (RSOS_2SHIP_WORK) and patched (2ship.patch, libultraship-gles2.patch). +# RSOS_HOST_CMAKE_ARGS: extra cmake arguments for the PC build (libraries outside /usr). +# +# The result contains the game's data, taken from your ROM: keep it for +# yourself. Never put it in a release, an update or a repository. +# +# Output (default ./2ship-retrostone2): roms/native/Majora's Mask.sh and +# roms/native/2ship/. Copy the "roms" folder onto the SD card's RETROSTONE +# partition (it merges with the roms folder there). +set -euo pipefail + +REPO=https://github.com/HarbourMasters/2ship2harkinian.git +COMMIT=8a24047fbce8915993804e7819f4df4fa591551f # 5.0.1 + fixes, libultraship 7cb1022 +LIBPNG_REPO=https://github.com/pnggroup/libpng.git +LIBPNG_COMMIT=95ab3fdca83ea294efd3b092e9a53c5a39886444 # v1.6.57 + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/2ship-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_2SHIP_WORK:-$HOME/rsos/2ship-work} + +die() { echo "build-2ship: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +# libopus and libopusfile are also copied next to the game, for consoles whose +# RetroStoneOS does not have them yet +LIBS="libSDL2.so libzip.so libtinyxml2.so libspdlog.so libvorbisfile.so libopus.so.0 libopusfile.so.0" +for lib in $LIBS; do + [ -e "$O/staging/usr/lib/$lib" ] || die "$lib is not in $O/staging (build RetroStoneOS with the RetroStone2 configs)" +done +TOOLCHAIN=$O/host/share/buildroot/toolchainfile.cmake + +# only the fixed commit (the whole history is large) +if [ ! -d "$W/.git" ]; then + git init -q "$W" + git -C "$W" remote add origin "$REPO" +fi +cd "$W" +git fetch -q --depth 1 origin "$COMMIT" +# a clean tree at the fixed commit, then our changes (the build folders and the +# ROM are ignored by git and stay) +git checkout -q -f "$COMMIT" +git submodule update -q --init --recursive --force --depth 1 +git -C libultraship checkout -q -f . +git apply "$here/2ship.patch" +git -C libultraship apply "$here/libultraship-gles2.patch" + +# the ROM, in .z64 byte order, in OTRExporter/ (where the extractor looks for it) +rm -f OTRExporter/*.z64 +python3 - "$rom" OTRExporter/baserom.z64 << 'EOF' || die "$rom is not an N64 ROM" +import sys +data = bytearray(open(sys.argv[1], 'rb').read()) +head = bytes(data[:4]) +if head == b'\x37\x80\x40\x12': # .v64: 16-bit words swapped + data[0::2], data[1::2] = data[1::2], data[0::2] +elif head == b'\x40\x12\x37\x80': # .n64: 32-bit words reversed + for i in range(0, len(data) - 3, 4): + data[i:i + 4] = data[i:i + 4][::-1] +elif head != b'\x80\x37\x12\x40': + sys.exit(1) +open(sys.argv[2], 'wb').write(data) +EOF + +# ZAPD for this PC: mm.o2r from the ROM, and 2ship.o2r, which carries the (patched) shaders. +# 2Ship has no tools-only build: this configures the whole game for the PC, and builds ZAPD only. +rm -f mm/mm.o2r mm/2ship.o2r mm.o2r 2ship.o2r +# shellcheck disable=SC2086 +cmake -S . -B build-tools -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_OPENGLES=ON -DUSE_GLES2=ON \ + -DNON_PORTABLE=OFF ${RSOS_HOST_CMAKE_ARGS:-} +cmake --build build-tools --target ExtractAssets || + die "the extraction failed: is $rom Majora's Mask (US), N64 or GameCube?" +[ -f build-tools/mm/mm.o2r ] && [ -f build-tools/mm/2ship.o2r ] || + die "no mm.o2r: $rom is not a Majora's Mask ROM that 2Ship2Harkinian supports" + +# The game links ZAPD's library (its own extractor), which needs libpng. RetroStoneOS has +# none: a static one, built with the console's compiler from libpng's repository. +P=$W-libpng +if [ ! -f "$P/arm/lib/libpng16.a" ]; then + rm -rf "$P" + git init -q "$P/src" + git -C "$P/src" fetch -q --depth 1 "$LIBPNG_REPO" "$LIBPNG_COMMIT" + git -C "$P/src" checkout -q FETCH_HEAD + cmake -S "$P/src" -B "$P/build" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \ + -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DPNG_SHARED=OFF -DPNG_STATIC=ON -DPNG_TESTS=OFF -DPNG_TOOLS=OFF -DPNG_FRAMEWORK=OFF \ + -DCMAKE_INSTALL_PREFIX="$P/arm" + cmake --build "$P/build" --target install +fi + +# the game for the console. CMAKE_SYSTEM_NAME Linux: Buildroot's toolchain file names +# it "Buildroot", which skips the Linux parts of 2Ship and libultraship. The release +# flags (-O3, unsafe math for NEON) are in 2ship.patch: 2Ship sets its own on Linux. +cmake -S . -B build-gles2 -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_OPENGLES=ON -DUSE_GLES2=ON \ + -DNON_PORTABLE=OFF -DPNG_LIBRARY="$P/arm/lib/libpng16.a" -DPNG_PNG_INCLUDE_DIR="$P/arm/include" \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DCMAKE_SYSTEM_NAME=Linux +cmake --build build-gles2 --target 2ship + +file build-gles2/mm/2s2h.elf | grep -q 'ARM' || die "build-gles2/mm/2s2h.elf is not an ARM program" +S=$out/roms/native/2ship +mkdir -p "$S/lib" "$S/assets" +"$O/host/bin/arm-linux-strip" -o "$S/2s2h.elf" build-gles2/mm/2s2h.elf +cp build-tools/mm/mm.o2r build-tools/mm/2ship.o2r "$S/" +[ -f build-gles2/gamecontrollerdb.txt ] && cp build-gles2/gamecontrollerdb.txt "$S/" +for lib in libopus.so.0 libopusfile.so.0; do + cp -L "$O/staging/usr/lib/$lib" "$S/lib/" + "$O/host/bin/arm-linux-strip" "$S/lib/$lib" +done +# 2Ship quits at start without an assets/ folder next to the program; it reads it only to +# make mm.o2r on the console, which this script does on the PC instead +printf '2Ship2Harkinian checks that this folder exists. mm.o2r is made on the PC\n(ports/2ship/build-2ship.sh), so the extraction files are not needed here.\n' \ + > "$S/assets/README.txt" +install -m 0644 "$here/Majora's Mask.sh" "$out/roms/native/Majora's Mask.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > Majora's Mask. Hold Select + Start to quit." diff --git a/ports/2ship/libultraship-gles2.patch b/ports/2ship/libultraship-gles2.patch new file mode 100644 index 0000000..cf432c2 --- /dev/null +++ b/ports/2ship/libultraship-gles2.patch @@ -0,0 +1,892 @@ +diff --git a/cmake/dependencies/linux.cmake b/cmake/dependencies/linux.cmake +index 02301f1..8e231e1 100644 +--- a/cmake/dependencies/linux.cmake ++++ b/cmake/dependencies/linux.cmake +@@ -2,7 +2,10 @@ + find_package(SDL2 REQUIRED) + target_link_libraries(ImGui PUBLIC SDL2::SDL2) + +-if (USE_OPENGLES) ++if (USE_GLES2) ++ target_link_libraries(ImGui PUBLIC GLESv2) ++ add_compile_definitions(IMGUI_IMPL_OPENGL_ES2) ++elseif (USE_OPENGLES) + target_link_libraries(ImGui PUBLIC ${OPENGL_GLESv2_LIBRARY}) + add_compile_definitions(IMGUI_IMPL_OPENGL_ES3) + else() +diff --git a/include/fast/backends/gfx_opengl.h b/include/fast/backends/gfx_opengl.h +index af24c70..efdf5c5 100644 +--- a/include/fast/backends/gfx_opengl.h ++++ b/include/fast/backends/gfx_opengl.h +@@ -16,6 +16,10 @@ + #elif __APPLE__ + #include + #include ++#elif defined(USE_GLES2) ++#include ++#include ++#include + #elif USE_OPENGLES + #include + #include +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 45989b6..0209da4 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -11,6 +11,11 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) + + set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include) + option(USE_OPENGLES "Enable GLES3" OFF) ++# OpenGL ES 2.0 (Mali-400 and other GLES2-only GPUs); implies USE_OPENGLES ++option(USE_GLES2 "Enable GLES2" OFF) ++if (USE_GLES2) ++ set(USE_OPENGLES ON) ++endif() + option(GFX_DEBUG_DISASSEMBLER "Enable libgfxd" OFF) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +@@ -208,6 +213,7 @@ else() + $<$>:NDEBUG> + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + $<$:USE_OPENGLES> ++ $<$:USE_GLES2> + $<$:GFX_DEBUG_DISASSEMBLER> + ) + endif() +diff --git a/src/fast/Fast3dGui.cpp b/src/fast/Fast3dGui.cpp +index 237a502..8c939c0 100644 +--- a/src/fast/Fast3dGui.cpp ++++ b/src/fast/Fast3dGui.cpp +@@ -146,6 +146,8 @@ void Fast3dGui::ImGuiBackendInit() { + case WindowBackend::FAST3D_SDL_OPENGL: + #ifdef __APPLE__ + ImGui_ImplOpenGL3_Init("#version 410 core"); ++#elif defined(USE_GLES2) ++ ImGui_ImplOpenGL3_Init("#version 100"); + #elif USE_OPENGLES + ImGui_ImplOpenGL3_Init("#version 300 es"); + #else +diff --git a/src/fast/Fast3dWindow.cpp b/src/fast/Fast3dWindow.cpp +index 2b77a5f..6b0380f 100644 +--- a/src/fast/Fast3dWindow.cpp ++++ b/src/fast/Fast3dWindow.cpp +@@ -109,6 +109,12 @@ void Fast3dWindow::Init() { + mWindowManagerApi->SetKeyboardCallbacks(KeyDown, KeyUp, AllKeysUp); + mWindowManagerApi->SetMouseCallbacks(MouseButtonDown, MouseButtonUp); + ++#ifdef USE_GLES2 ++ if (Ship::Context::GetRawInstance()->GetConsoleVariables()->Get(CVAR_TEXTURE_FILTER) == nullptr) { ++ Ship::Context::GetRawInstance()->GetConsoleVariables()->SetInteger(CVAR_TEXTURE_FILTER, FILTER_LINEAR); ++ Ship::Context::GetRawInstance()->GetConsoleVariables()->Save(); ++ } ++#endif + SetTextureFilter((FilteringMode)Ship::Context::GetRawInstance()->GetConsoleVariables()->GetInteger( + CVAR_TEXTURE_FILTER, FILTER_THREE_POINT)); + } +diff --git a/src/fast/backends/gfx_opengl.cpp b/src/fast/backends/gfx_opengl.cpp +index ad9b1dd..2f6e080 100644 +--- a/src/fast/backends/gfx_opengl.cpp ++++ b/src/fast/backends/gfx_opengl.cpp +@@ -27,6 +27,52 @@ + #include "fast/interpreter.h" + #include "ship/config/ConsoleVariable.h" + ++// a texture was bound outside SelectTexture(): its cache no longer says what is bound ++#define FORGET_TEXTURE_BINDINGS() do { mLastActiveTexture = -1; for (GLuint& bound : mLastBoundTextures) bound = (GLuint)-1; } while (0) ++ ++#include ++ ++#ifdef USE_GLES2 ++/* ++ * OpenGL ES 2.0: no sized texture formats, no multisampling, no blits and no ++ * depth reads. Multisampling stays off (mMaxMsaaLevel 1), so the multisample ++ * calls below are never reached; CopyFramebuffer and GetPixelDepth have their ++ * own GLES2 versions. Depth: packed depth+stencil when the driver has ++ * GL_OES_packed_depth_stencil, else a 16-bit depth buffer. ++ */ ++#define GL_RGB8 GL_RGB ++#define GL_RGBA8 GL_RGBA ++#define GL_DEPTH24_STENCIL8 GL_DEPTH24_STENCIL8_OES ++#define GL_DEPTH_STENCIL GL_DEPTH_STENCIL_OES ++#define GL_UNSIGNED_INT_24_8 GL_UNSIGNED_INT_24_8_OES ++#define GL_READ_FRAMEBUFFER GL_FRAMEBUFFER ++#define GL_DRAW_FRAMEBUFFER GL_FRAMEBUFFER ++#define glBlitFramebuffer(...) ((void)0) ++#define glReadBuffer(b) ((void)0) ++#define glRenderbufferStorageMultisample(target, samples, format, w, h) glRenderbufferStorage(target, format, w, h) ++static bool sPackedDepthStencil = true; ++#endif ++ ++namespace Fast { ++static void RenderbufferDepthStorage(GLsizei width, GLsizei height) { ++#ifdef USE_GLES2 ++ glRenderbufferStorage(GL_RENDERBUFFER, sPackedDepthStencil ? GL_DEPTH24_STENCIL8_OES : GL_DEPTH_COMPONENT16, ++ width, height); ++#else ++ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++#endif ++} ++ ++static void FramebufferDepthAttach(GLuint rb) { ++#ifdef USE_GLES2 ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb); ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, sPackedDepthStencil ? rb : 0); ++#else ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb); ++#endif ++} ++} // namespace Fast ++ + namespace Fast { + int GfxRenderingAPIOGL::GetMaxTextureSize() { + GLint max_texture_size; +@@ -42,7 +88,17 @@ GfxClipParameters GfxRenderingAPIOGL::GetClipParameters() { + return { false, mFrameBuffers[mCurrentFrameBuffer].invertY }; + } + +-static void VertexArraySetAttribs(ShaderProgram* prg) { ++#ifdef USE_GLES2 ++static ShaderProgram* sClientArrayProgram; ++static const float* sClientArrayBase; ++#endif ++ ++static void VertexArraySetAttribs(ShaderProgram* prg, const float* clientArray = nullptr) { ++#ifdef USE_GLES2 ++ if (clientArray == nullptr) { ++ sClientArrayProgram = nullptr; // set for a buffer object: the next draw sets them again ++ } ++#endif + size_t numFloats = prg->numFloats; + size_t pos = 0; + +@@ -50,7 +106,9 @@ static void VertexArraySetAttribs(ShaderProgram* prg) { + if (prg->attribLocations[i] >= 0) { + glEnableVertexAttribArray(prg->attribLocations[i]); + glVertexAttribPointer(prg->attribLocations[i], prg->attribSizes[i], GL_FLOAT, GL_FALSE, +- numFloats * sizeof(float), (void*)(pos * sizeof(float))); ++ numFloats * sizeof(float), ++ clientArray != nullptr ? (const void*)(clientArray + pos) ++ : (const void*)(pos * sizeof(float))); + } + pos += prg->attribSizes[i]; + } +@@ -285,13 +343,23 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "opengles", false }, + { "core_opengl", true }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100\nprecision mediump float;" }, ++ { "attr", "varying" }, ++ { "opengles", false }, ++ { "core_opengl", false }, ++ { "gles2", true }, ++ { "texture", "texture2D" }, ++ { "vOutColor", "gl_FragColor" }, + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es\nprecision mediump float;" }, + { "attr", "in" }, + { "opengles", true }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, + #else +@@ -299,6 +367,7 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "varying" }, + { "opengles", false }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture2D" }, + { "vOutColor", "gl_FragColor" }, + #endif +@@ -356,6 +425,11 @@ static std::string BuildVsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "out", "out" }, + { "opengles", false } ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100" }, ++ { "attr", "attribute" }, ++ { "out", "varying" }, ++ { "opengles", false } + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es" }, + { "attr", "in" }, +@@ -420,9 +494,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- // fprintf(stderr, "Vertex shader compilation failed\n"); +- glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]); +- // fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(vertex_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Vertex shader compilation failed:\n%s\n%s\n", error_log, vs_buf.c_str()); + abort(); + } + +@@ -434,9 +507,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- fprintf(stderr, "Fragment shader compilation failed\n"); +- glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]); +- fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(fragment_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Fragment shader compilation failed:\n%s\n%s\n", error_log, fs_buf.c_str()); + abort(); + } + +@@ -559,6 +631,7 @@ GLuint GfxRenderingAPIOGL::NewTexture() { + + void GfxRenderingAPIOGL::DeleteTexture(uint32_t texID) { + glDeleteTextures(1, &texID); ++ FORGET_TEXTURE_BINDINGS(); // deleting unbinds it, and the name comes back with the next texture + } + + void GfxRenderingAPIOGL::SelectTexture(int tile, GLuint texture_id) { +@@ -583,7 +656,9 @@ void GfxRenderingAPIOGL::UploadTexture(const uint8_t* rgba32_buf, uint32_t width + textures[mCurrentTextureIds[mCurrentTile]].height = height; + } + +-#ifdef USE_OPENGLES ++#ifdef USE_GLES2 ++#define GL_MIRROR_CLAMP_TO_EDGE GL_MIRRORED_REPEAT ++#elif defined(USE_OPENGLES) + #define GL_MIRROR_CLAMP_TO_EDGE 0x8743 + #endif + +@@ -703,7 +778,17 @@ void GfxRenderingAPIOGL::DrawTriangles(float buf_vbo[], size_t buf_vbo_len, size + SetPerDrawUniforms(); + + // printf("flushing %d tris\n", buf_vbo_num_tris); ++#ifdef USE_GLES2 ++ // the pointers stay valid until the program changes or ImGui draws (after the frame) ++ if (mCurrentShaderProgram != sClientArrayProgram || buf_vbo != sClientArrayBase) { ++ glBindBuffer(GL_ARRAY_BUFFER, 0); ++ VertexArraySetAttribs(mCurrentShaderProgram, buf_vbo); ++ sClientArrayProgram = mCurrentShaderProgram; ++ sClientArrayBase = buf_vbo; ++ } ++#else + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); ++#endif + glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris); + } + +@@ -715,7 +800,16 @@ void GfxRenderingAPIOGL::Init() { + glGenBuffers(1, &mOpenglVbo); + glBindBuffer(GL_ARRAY_BUFFER, mOpenglVbo); + +-#if defined(__APPLE__) || defined(USE_OPENGLES) ++#ifdef USE_GLES2 ++ { ++ const char* ext = (const char*)glGetString(GL_EXTENSIONS); ++ ++ sPackedDepthStencil = ext != nullptr && strstr(ext, "GL_OES_packed_depth_stencil") != nullptr; ++ SPDLOG_INFO("OpenGL ES 2.0: {} ({}), depth buffer {}", (const char*)glGetString(GL_RENDERER), ++ (const char*)glGetString(GL_VERSION), sPackedDepthStencil ? "24-bit + stencil" : "16-bit"); ++ } ++#endif ++#if defined(__APPLE__) || (defined(USE_OPENGLES) && !defined(USE_GLES2)) + glGenVertexArrays(1, &mOpenglVao); + glBindVertexArray(mOpenglVao); + #endif +@@ -730,17 +824,21 @@ void GfxRenderingAPIOGL::Init() { + + glGenRenderbuffers(1, &mPixelDepthRb); + glBindRenderbuffer(GL_RENDERBUFFER, mPixelDepthRb); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + glGenFramebuffers(1, &mPixelDepthFb); + glBindFramebuffer(GL_FRAMEBUFFER, mPixelDepthFb); +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, mPixelDepthRb); ++ FramebufferDepthAttach(mPixelDepthRb); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + mPixelDepthRbSize = 1; + ++#ifdef USE_GLES2 ++ mMaxMsaaLevel = 1; ++#else + glGetIntegerv(GL_MAX_SAMPLES, &mMaxMsaaLevel); ++#endif + } + + void GfxRenderingAPIOGL::OnResize() { +@@ -748,6 +846,9 @@ void GfxRenderingAPIOGL::OnResize() { + + void GfxRenderingAPIOGL::StartFrame() { + mFrameCount++; ++#ifdef USE_GLES2 ++ sClientArrayProgram = nullptr; // ImGui drew with its own arrays ++#endif + } + + void GfxRenderingAPIOGL::EndFrame() { +@@ -764,7 +865,12 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ++#ifdef USE_GLES2 ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ++#endif + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + + GLuint clrbufMsaa; + glGenRenderbuffers(1, &clrbufMsaa); +@@ -772,7 +878,7 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + GLuint rbo; + glGenRenderbuffers(1, &rbo); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + GLuint fbo; +@@ -806,6 +912,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + glBindTexture(GL_TEXTURE_2D, fb.clrbuf); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.clrbuf, 0); + } else { + glBindRenderbuffer(GL_RENDERBUFFER, fb.clrbufMsaa); +@@ -819,7 +926,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + (fb.width != width || fb.height != height || fb.msaa_level != msaa_level || !fb.has_depth_buffer)) { + glBindRenderbuffer(GL_RENDERBUFFER, fb.rbo); + if (msaa_level <= 1) { +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++ RenderbufferDepthStorage(width, height); + } else { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_DEPTH24_STENCIL8, width, height); + } +@@ -827,9 +934,9 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + } + + if (!fb.has_depth_buffer && has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb.rbo); ++ FramebufferDepthAttach(fb.rbo); + } else if (fb.has_depth_buffer && !has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); ++ FramebufferDepthAttach(0); + } + } + +@@ -932,6 +1039,35 @@ void GfxRenderingAPIOGL::CopyFramebuffer(int fb_dst_id, int fb_src_id, int srcX0 + FramebufferOGL src = mFrameBuffers[fb_src_id]; + const FramebufferOGL& dst = mFrameBuffers[fb_dst_id]; + ++#ifdef USE_GLES2 ++ // No blits: copy into the destination's colour texture, unscaled and in the ++ // same orientation only (G_COPYFB effects; Starship does not use them). ++ if (fb_dst_id == 0 || srcX1 - srcX0 != dstX1 - dstX0 || srcY1 - srcY0 != dstY1 - dstY0 || ++ src.invertY != dst.invertY) { ++ static bool warned; ++ if (!warned) { ++ SPDLOG_WARN("GLES2: scaled, flipped or on-screen framebuffer copies are not supported"); ++ warned = true; ++ } ++ return; ++ } ++ // OpenGL's origin is the bottom left, as in the blit path below ++ if (!src.invertY) { ++ int h = srcY1 - srcY0; ++ srcY1 = src.height - srcY0; ++ srcY0 = srcY1 - h; ++ } ++ glDisable(GL_SCISSOR_TEST); ++ glBindFramebuffer(GL_FRAMEBUFFER, src.fbo); ++ glActiveTexture(GL_TEXTURE0); ++ glBindTexture(GL_TEXTURE_2D, dst.clrbuf); ++ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, dstX0, dstY0, srcX0, srcY0, srcX1 - srcX0, srcY1 - srcY0); ++ glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[mCurrentFrameBuffer].fbo); ++ glEnable(GL_SCISSOR_TEST); ++ FORGET_TEXTURE_BINDINGS(); ++ return; ++#endif ++ + // Adjust y values for non-inverted source frame buffers because opengl uses bottom left for origin + if (!src.invertY) { + int temp = srcY1 - srcY0; +@@ -1024,6 +1160,14 @@ std::unordered_map, uint16_t, hash_pair_ff> + GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set>& coordinates) { + std::unordered_map, uint16_t, hash_pair_ff> res; + ++#ifdef USE_GLES2 ++ // GLES2 cannot read the depth buffer: every point is reported at the far plane ++ for (const auto& coord : coordinates) { ++ res.emplace(coord, 0xFFFC); ++ } ++ return res; ++#endif ++ + FramebufferOGL& fb = mFrameBuffers[fb_id]; + + // When looking up one value and the framebuffer is single-sampled, we can read pixels directly +@@ -1045,8 +1189,8 @@ GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set + #ifndef _WIN32 + #include ++#include ++#include + #endif + + #include +@@ -134,6 +136,46 @@ void GfxSetInstance(std::shared_ptr gfx) { + // N64 prim_depth is 15-bit (0 near, 0x7FFF far). + static constexpr float N64_PRIM_DEPTH_MAX = 32767.0f; + ++// Alternate ("alt/") assets are unloaded and loaded again as new objects; with any in the ++// archives, nothing is kept (decided once: archives are added at start only) ++static bool NoAltAssetFiles() { ++ static const bool none = ++ Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->ListFiles("alt/*")->empty(); ++ return none; ++} ++ ++static std::unordered_map> sHashResourceCache; ++ ++// texture paths named by pointer: kept per pointer, checked against a copy of the path ++struct ResolvedPath { ++ std::string path; ++ std::shared_ptr res; ++}; ++static std::unordered_map sPathResourceCache; ++ ++static std::shared_ptr CachedResourceByHash(uint64_t hash) { ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ } ++ auto it = sHashResourceCache.find(hash); ++ if (it != sHashResourceCache.end()) { ++ return it->second; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ if (res != nullptr) { ++ if (sHashResourceCache.size() >= 4096) { ++ sHashResourceCache.clear(); ++ } ++ sHashResourceCache.emplace(hash, res); ++ } ++ return res; ++} ++ ++static void* CachedResourceRawPointer(uint64_t hash) { ++ auto res = CachedResourceByHash(hash); ++ return res != nullptr ? res->GetRawPointer() : nullptr; ++} ++ + void Interpreter::Flush() { + if (mBufVboLen > 0) { + mRapi->SetCurrentPrimDepth((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX); +@@ -458,7 +500,21 @@ std::shared_ptr Interpreter::ResolveResourceCached(const char* + return nullptr; + } + if (!mResolvedResourceCacheEnabled) { +- return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ } ++ auto found = sPathResourceCache.find(path); ++ if (found != sPathResourceCache.end() && found->second.path == path) { ++ return found->second.res; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ if (res != nullptr) { ++ if (sPathResourceCache.size() >= 4096) { ++ sPathResourceCache.clear(); ++ } ++ sPathResourceCache[path] = { path, res }; ++ } ++ return res; + } + auto it = mResolvedResourceCache.find(path); + if (it != mResolvedResourceCache.end()) { +@@ -481,6 +537,8 @@ void Interpreter::TextureCacheClear() { + mTextureCache.map.clear(); + mTextureCache.lru.clear(); + mResolvedResourceCache.clear(); ++ sHashResourceCache.clear(); ++ sPathResourceCache.clear(); + // Pre-allocate buckets so the map never rehashes during normal operation. + // Rehashing invalidates all iterators, including those stored in LRU entries. + mTextureCache.map.reserve(TEXTURE_CACHE_MAX_SIZE); +@@ -1275,6 +1333,7 @@ void Interpreter::ImportTexture(int i, int tile, bool importReplacement) { + if (fbIt != mFbTextures.end()) { + Flush(); + mRapi->SelectTextureFb(fbIt->second); ++ mRenderingState.mTextures[i] = nullptr; + mRdp->textures_changed[i] = false; + return; + } +@@ -1318,6 +1377,15 @@ void Interpreter::ImportTexture(int i, int tile, bool importReplacement) { + key = { origAddr, {}, fmt, siz, paletteIndex, origSizeBytes }; + } + ++ // The texture already bound to this slot: the triangles so far can keep going (selected ++ // again in case the backend lost its bindings) ++ if (!importReplacement && mRenderingState.mTextures[i] != nullptr && ++ mRenderingState.mTextures[i]->first == key) { ++ mRapi->SelectTexture(i, mRenderingState.mTextures[i]->second.texture_id); ++ return; ++ } ++ Flush(); ++ + if (TextureCacheLookup(i, key)) { + return; + } +@@ -1919,9 +1987,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + if (use_grayscale) { + cc_options |= SHADER_OPT(GRAYSCALE); + } ++#ifndef USE_GLES2 + if (use_prim_depth) { + cc_options |= SHADER_OPT(PRIM_DEPTH); + } ++#endif + + if (!mShaderStack.empty()) { + cc_options |= (mShaderStack.top() << SHADER_ID_SHIFT); +@@ -1949,8 +2019,9 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + ColorCombiner* comb = LookupOrCreateColorCombiner(key); + + uint32_t tm = 0; +- uint32_t tex_width[2], tex_height[2], tex_width2[2], tex_height2[2]; +- uint32_t effective_tile[2]; ++ uint32_t tex_width[2] = { 1, 1 }, tex_height[2] = { 1, 1 }, tex_width2[2] = { 1, 1 }, ++ tex_height2[2] = { 1, 1 }; ++ uint32_t effective_tile[2] = { mRdp->first_tile_index, mRdp->first_tile_index }; + + for (int i = 0; i < 2; i++) { + uint32_t tile = mRdp->first_tile_index + i; +@@ -1963,12 +2034,13 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + + if (comb->usedTextures[i]) { + if (mRdp->textures_changed[i]) { +- Flush(); +- ImportTexture(i, tile, false); ++ ImportTexture(i, tile, false); // flushes if the texture changes + if (mRdp->loaded_texture[i].masked) { ++ Flush(); + ImportTextureMask(SHADER_FIRST_MASK_TEXTURE + i, tile); + } + if (mRdp->loaded_texture[i].blended) { ++ Flush(); + ImportTexture(SHADER_FIRST_REPLACEMENT_TEXTURE + i, tile, true); + } + mRdp->textures_changed[i] = false; +@@ -2077,6 +2149,15 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + } + } + ++ for (int i = 0; i < 2; i++) { ++ if (!comb->usedTextures[i] && comb->usedTextures[i ^ 1]) { ++ tex_width[i] = tex_width[i ^ 1]; ++ tex_height[i] = tex_height[i ^ 1]; ++ tex_width2[i] = tex_width2[i ^ 1]; ++ tex_height2[i] = tex_height2[i ^ 1]; ++ } ++ } ++ + struct ShaderProgram* prg = comb->prg[tm]; + if (prg == NULL) { + comb->prg[tm] = prg = +@@ -2102,6 +2183,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + + for (int i = 0; i < 3; i++) { + float z = v_arr[i]->z, w = v_arr[i]->w; ++#ifdef USE_GLES2 ++ if (use_prim_depth) { ++ z = ((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX * 2.0f - 1.0f) * w; ++ } ++#endif + if (clip_parameters.z_is_from_0_to_1) { + z = (z + w) / 2.0f; + } +@@ -3418,7 +3504,7 @@ bool gfx_mtx_otr_handler_custom_f3dex2(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + + if (mtx != NULL) { + Interpreter* gfx = mInstance.lock().get(); +@@ -3437,7 +3523,7 @@ bool gfx_mtx_otr_handler_custom_f3d(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + if (mtx != nullptr) { + cmd--; + gfx->GfxSpMatrix(C0(16, 8), mtx); +@@ -3504,10 +3590,10 @@ bool gfx_movemem_handler_otr(F3DGfx** cmd0) { + + if (ucode_handler_index == ucode_f3dex2) { + gfx->GfxSpMovememF3dex2(index, offset, +- Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash)); ++ CachedResourceRawPointer(hash)); + } else { + auto light = +- (Fast::LightEntry*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (Fast::LightEntry*)CachedResourceRawPointer(hash); + uintptr_t data = (uintptr_t)&light->Ambient; + gfx->GfxSpMovememF3d(index, offset, (void*)(data + (hasOffset == 1 ? 0x8 : 0))); + } +@@ -3646,7 +3732,7 @@ bool gfx_vtx_hash_handler_custom(F3DGfx** cmd0) { + gfx->GfxSpVertex(C0(12, 8), C0(1, 7) - C0(12, 8), (F3DVtx*)offset); + (*cmd0)++; + } else { +- F3DVtx* vtx = (F3DVtx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DVtx* vtx = (F3DVtx*)CachedResourceRawPointer(hash); + + if (vtx != NULL) { + vtx = (F3DVtx*)((char*)vtx + offset); +@@ -3738,7 +3824,7 @@ bool gfx_dl_otr_hash_handler_custom(F3DGfx** cmd0) { + + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + g_exec_stack.call(cmd, gfx); +@@ -3796,7 +3882,7 @@ bool gfx_branch_z_otr_handler_f3dex2(F3DGfx** cmd0) { + (gfx->mRsp->extra_geometry_mode & G_EX_ALWAYS_EXECUTE_BRANCH) != 0) { + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + (*cmd0) = gfx; +@@ -3986,9 +4072,17 @@ static bool IsValidResolvedAddress(uintptr_t addr) { + reinterpret_cast(addr), &module) != 0; + #else + // For non-Windows platforms, check whether the address belongs to a loaded object. ++#if UINTPTR_MAX <= 0xFFFFFFFFu ++ // 32-bit: the heap and mappings live below 0x10000000 as well; a mapped page is a real address. ++ // mincore is one cheap system call (dladdr searched symbol tables: 3% of the CPU time on an A20). ++ static const uintptr_t pageSize = (uintptr_t)sysconf(_SC_PAGESIZE); ++ unsigned char resident; ++ return mincore(reinterpret_cast(addr & ~(pageSize - 1)), 1, &resident) == 0; ++#else + Dl_info info; + return dladdr(reinterpret_cast(addr), &info) != 0; + #endif ++#endif + } + + bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { +@@ -4003,6 +4097,7 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + // from the resource, but raw textures would leave them at 0. + rawTexMetdata.h_byte_scale = 1; + rawTexMetdata.v_pixel_scale = 1; ++ bool fromResource = false; + + if ((i & 1) != 1) { + if (gfx_check_image_signature(imgData) == 1) { +@@ -4022,10 +4117,11 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + rawTexMetdata.v_pixel_scale = tex->VPixelScale; + rawTexMetdata.type = tex->Type; + rawTexMetdata.resource = tex; ++ fromResource = true; + } + } + +- if (!IsValidResolvedAddress(i)) { ++ if (!fromResource && !IsValidResolvedAddress(i)) { + return false; + } + +@@ -4050,8 +4146,7 @@ bool gfx_set_timg_otr_hash_handler_custom(F3DGfx** cmd0) { + } + + std::shared_ptr texture = std::static_pointer_cast( +- Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess( +- Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->HashToCString(hash))); ++ CachedResourceByHash(hash)); + if (texture != nullptr) { + texFlags = texture->Flags; + rawTexMetadata.width = texture->Width; +diff --git a/src/fast/shaders/opengl/default.shader.glsl b/src/fast/shaders/opengl/default.shader.glsl +index 1fe86aa..ff45672 100644 +--- a/src/fast/shaders/opengl/default.shader.glsl ++++ b/src/fast/shaders/opengl/default.shader.glsl +@@ -134,14 +134,18 @@ + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++ @if(gles2) ++ return fract(sin(random) * 43758.5453); ++ @else + return fract(sin(random) * 143758.5453); ++ @end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -187,7 +191,9 @@ + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +index c431c34..35f66b2 100644 +--- a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp ++++ b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +@@ -80,6 +80,11 @@ void ControllerDefaultMappings::SetDefaultSDLButtonToButtonMappings( + { BTN_DDOWN, { SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, + { BTN_DLEFT, { SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, + { BTN_DRIGHT, { SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, ++#ifdef USE_GLES2 ++ { BTN_CDOWN, { SDL_CONTROLLER_BUTTON_Y } }, ++ { BTN_CLEFT, { SDL_CONTROLLER_BUTTON_X } }, ++ { BTN_CRIGHT, { SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, ++#endif + }); + } + +diff --git a/src/ship/audio/Audio.cpp b/src/ship/audio/Audio.cpp +index 8cc5b12..604a30c 100644 +--- a/src/ship/audio/Audio.cpp ++++ b/src/ship/audio/Audio.cpp +@@ -36,8 +36,11 @@ void Audio::InitAudioPlayer() { + + if (mAudioPlayer && !mAudioPlayer->Init()) { + // Failed to initialize system audio player. +- // Fallback to Null if the native system player does not work. +- SetCurrentAudioBackend(AudioBackend::NUL); ++ // Fallback to Null if the native system player does not work: for this run only, ++ // the saved backend stays and the next start tries it again ++ mAudioBackend = AudioBackend::NUL; ++ mAudioPlayer = std::make_shared(this->mAudioSettings); ++ mAudioPlayer->Init(); + } + } + +diff --git a/src/ship/debug/CrashHandler.cpp b/src/ship/debug/CrashHandler.cpp +index 39d76be..311b80d 100644 +--- a/src/ship/debug/CrashHandler.cpp ++++ b/src/ship/debug/CrashHandler.cpp +@@ -2,6 +2,9 @@ + #include "ship/utils/StringHelper.h" + #include "ship/debug/CrashHandler.h" + #include "ship/Context.h" ++#ifndef _WIN32 ++#include // _exit ++#endif + + #ifdef _WIN32 + #include +@@ -134,6 +137,23 @@ void CrashHandler::PrintRegisters(ucontext_t* ctx) { + AppendLine(regbuffer); + snprintf(regbuffer, std::size(regbuffer), "EFL: 0x%08lX", ctx->uc_mcontext.gregs[REG_EFL]); + AppendLine(regbuffer); ++#elif defined(__arm__) ++ const struct { ++ const char* name; ++ unsigned long value; ++ } regs[] = { { "PC", ctx->uc_mcontext.arm_pc }, { "LR", ctx->uc_mcontext.arm_lr }, ++ { "SP", ctx->uc_mcontext.arm_sp }, { "R0", ctx->uc_mcontext.arm_r0 }, ++ { "R1", ctx->uc_mcontext.arm_r1 }, { "R2", ctx->uc_mcontext.arm_r2 }, ++ { "R3", ctx->uc_mcontext.arm_r3 }, { "FAULT", ctx->uc_mcontext.fault_address } }; ++ for (const auto& reg : regs) { ++ snprintf(regbuffer, std::size(regbuffer), "%s: 0x%08lX", reg.name, reg.value); ++ AppendLine(regbuffer); ++ } ++ Dl_info module; ++ if (dladdr((void*)ctx->uc_mcontext.arm_pc, &module) != 0) { ++ snprintf(regbuffer, std::size(regbuffer), "BASE: 0x%08lX", (unsigned long)module.dli_fbase); ++ AppendLine(regbuffer); ++ } + #endif + } + +@@ -163,6 +183,9 @@ static void ErrorHandler(int sig, siginfo_t* sigInfo, void* data) { + case SIGSEGV: + crashHandler->AppendLine("INVALID ACCESS TO STORAGE"); + break; ++ case SIGBUS: ++ crashHandler->AppendLine("BUS ERROR (UNALIGNED OR INVALID ADDRESS)"); ++ break; + } + + crashHandler->PrintRegisters(ctx); +@@ -199,11 +222,13 @@ static void ErrorHandler(int sig, siginfo_t* sigInfo, void* data) { + + Context::GetRawInstance()->GetLogger()->flush(); + spdlog::shutdown(); +- exit(1); ++ _exit(128 + sig); + } + + static void ShutdownHandler(int sig, siginfo_t* sigInfo, void* data) { +- exit(1); ++ SDL_Event event; ++ event.type = SDL_QUIT; ++ SDL_PushEvent(&event); + } + + #elif _WIN32 +@@ -426,6 +451,7 @@ CrashHandler::CrashHandler() : mOutBuffer(std::make_unique(gMaxBufferSiz + sigaction(SIGABRT, &action, nullptr); + sigaction(SIGFPE, &action, nullptr); + sigaction(SIGSEGV, &action, nullptr); ++ sigaction(SIGBUS, &action, nullptr); + + shutdownAction.sa_flags = SA_SIGINFO; + shutdownAction.sa_sigaction = ShutdownHandler; diff --git a/ports/README.md b/ports/README.md new file mode 100644 index 0000000..ccd6f79 --- /dev/null +++ b/ports/README.md @@ -0,0 +1,71 @@ +# Native ports for the RetroStone2 + +PC ports of N64 games (and other open-source engines) can run on the RetroStone2 **as native games**: ARM programs +that draw with the console's GPU, with no emulator in between. They are much faster and look much better than +emulating the N64: Super Mario 64 runs at full speed on the RetroStone2. + +**RetroStoneOS never ships any game.** This folder holds only build scripts and code changes. Each port is built +**on your PC, from your own ROM**, and the result goes onto your own SD card. Keep it to yourself: never upload it, +share it or put it in a release. + +## How to get a native port (step by step) + +1. **Update the console** to a RetroStoneOS with **Native games** in the menu (Settings > System update). The + libultraship ports (Star Fox 64, Ocarina of Time, Majora's Mask, Mario Kart 64) also need `rsos-gamepad` and the port libraries in the + RetroStone2 configs (Ogg/Vorbis, Opus, libzip, tinyxml2, spdlog, SDL2_net). +2. **Set up the build on a PC** with Linux or Windows + WSL (Ubuntu): + * the usual tools: `sudo apt install build-essential git python3 cmake ninja-build`; + * RetroStoneOS, built once for the RetroStone2: it provides the compiler for the console and its libraries. + Follow [docs/build.md](../docs/build.md) with `retrostone2_release_defconfig` (the output in + `~/rsos/output`). A port built once usually keeps working after the console's System updates. +3. **Build the port** with its script, giving it your ROM (each port's README says which ROM version): + ```sh + ports/sm64/build-sm64.sh ~/baserom.us.z64 ~/my-ports + ``` +4. **Copy** the `roms` folder from the output (`~/my-ports/roms`) onto the SD card's **RETROSTONE** partition + (it merges with the `roms` folder there). From WSL, Windows sees it at `\\wsl$\Ubuntu\home\\my-ports`. +5. In the menu: **Native games** > the game. **Select + Start** goes back to the menu (Super Mario 64 and Star Fox + 64 quit at once, as the emulators do; any other native game when both are held for a second). + +## Available ports + +| Game | Folder | Port | Status on the RetroStone2 | +|---|---|---|---| +| Super Mario 64 | [sm64](sm64/README.md) | [sm64-port](https://github.com/sm64-port/sm64-port) | **Works**: full speed, built-in controls, analog stick (Pro) or D-pad | +| Star Fox 64 | [starship](starship/README.md) | [Starship](https://github.com/HarbourMasters/Starship) + a GLES2 libultraship | **Works**: built-in controls, cheaper first-start settings; some slowdown in busy scenes | +| Mario Kart 64 | [spaghettikart](spaghettikart/README.md) | [SpaghettiKart](https://github.com/HarbourMasters/SpaghettiKart) + a GLES2 libultraship | **Works**: races, sound, saves, built-in controls; mostly 30 fps, some slowdown in busy places | +| The Legend of Zelda: Ocarina of Time | [soh](soh/README.md) | [Ship of Harkinian](https://github.com/HarbourMasters/Shipwright) 9.3.1 + a GLES2 libultraship | **Works**: menus, 3D, sound, saves; a little slowdown in busy places | +| GoldenEye 007 | [goldeneye](goldeneye/README.md) | [Project Midas](https://github.com/murkantor/Project-Midas) + a GLES2 fast3d renderer | **Works**: intro, menus, levels, sound; 30 fps in light areas, 10-15 in busy views | +| The Legend of Zelda: Majora's Mask | [2ship](2ship/README.md) | [2Ship2Harkinian](https://github.com/HarbourMasters/2ship2harkinian) + a GLES2 libultraship | **Works**: 3D, music and sound, at the game's own 20 fps in most places | + +## Other N64 PC ports: can they run here? + +The RetroStone2's GPU is a **Mali-400 MP2: OpenGL ES 2.0 only** (Mesa's lima driver), with 1 GB of RAM and two +Cortex-A7 cores at 960-1056 MHz. That decides which ports can work: + +| Port family | Games | GPU it needs | Here | +|---|---|---|---| +| sm64-port's renderer (Fast3D, GLSL 1.10 / ES 1.00) | Super Mario 64 (sm64-port, sm64ex) | OpenGL 2.1 / **ES 2.0** | **Yes** (Super Mario 64 works) | +| [libultraship](https://github.com/Kenix3/libultraship) (HarbourMasters) | Star Fox 64 ([Starship](https://github.com/HarbourMasters/Starship)), Ocarina of Time ([Ship of Harkinian](https://github.com/HarbourMasters/Shipwright)), Majora's Mask ([2Ship2Harkinian](https://github.com/HarbourMasters/2ship2harkinian)), Mario Kart 64 ([SpaghettiKart](https://github.com/HarbourMasters/SpaghettiKart)), [Ghostship](https://github.com/HarbourMasters/Ghostship), Banjo-Kazooie ([Lighthouse](https://github.com/HarbourMasters/Lighthouse)), Paper Mario ([PaperBoat](https://github.com/HarbourMasters/PaperBoat)) | OpenGL 3.0 (GLSL 1.30) or **ES 3.0** | **Yes with the GLES2 mode** in these patches (Star Fox 64, Ocarina of Time, Majora's Mask and Mario Kart 64 work); ports on the newest libultraship also need SDL3 | +| The [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark)'s fast3d (also in [Project Midas](https://github.com/murkantor/Project-Midas)) | Perfect Dark, GoldenEye 007 | OpenGL 3.0 / ES 3.0 | **Yes with the GLES2 mode** in [goldeneye](goldeneye/README.md) (GoldenEye works); Perfect Dark would need the same changes | +| N64 Recompiled (RT64) | Zelda 64 Recomp, Banjo: Recompiled, DK64, Star Fox 64 Recomp, ... | Vulkan / Direct3D 12 | **No**: the Mali-400 has neither | + +### libultraship on OpenGL ES 2.0 + +libultraship's OpenGL renderer has shader variants for desktop GL and **GLES 3.0**; the ports here add a **GLES +2.0** mode (`USE_GLES2`): GLSL ES 1.00 shaders, GLES2 framebuffers and depth buffers, no multisampling. Each port +pins its own libultraship, so each has its own copy of the change: [starship/libultraship-gles2.patch](starship/libultraship-gles2.patch) +(libultraship `eaaf9d0`), [soh/libultraship-gles2.patch](soh/libultraship-gles2.patch) (libultraship `da6daf1`, +with fixes for 32-bit ARM and speed work measured on the console) and +[spaghettikart/libultraship-gles2.patch](spaghettikart/libultraship-gles2.patch) (libultraship `60c9bb7`: Ship of +Harkinian's, plus fewer draw calls) and [2ship/libultraship-gles2.patch](2ship/libultraship-gles2.patch) +(libultraship `7cb1022`: the same). These libultraship versions still use SDL2. + +## Making a new port + +A native game is any ARM program with a launch script in `roms/native/` (docs/native-games.md). For an SDL2 port: +build it with the RetroStoneOS cross compiler (`~/rsos/output/host/bin/arm-linux-`) against the staging +libraries (`~/rsos/output/staging`), use GLES2 for 3D, and either start `rsos-gamepad` for the built-in controls +as one SDL gamepad (`starship/Star Fox 64.sh`) or read them from `/dev/input` yourself (three devices: +"RetroStone2 Buttons", "RetroStone2 Shoulder Buttons" and "analog-stick"; `sm64/controller_rsos.c` is an +example). Put the game's code changes in a patch next to a build script, never the game's data. diff --git a/ports/goldeneye/.gitattributes b/ports/goldeneye/.gitattributes new file mode 100644 index 0000000..ec1ffa8 --- /dev/null +++ b/ports/goldeneye/.gitattributes @@ -0,0 +1 @@ +*.patch -text diff --git a/ports/goldeneye/GoldenEye 007.sh b/ports/goldeneye/GoldenEye 007.sh new file mode 100644 index 0000000..06a3caf --- /dev/null +++ b/ports/goldeneye/GoldenEye 007.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# GoldenEye 007 (Project Midas), built from your own ROM by ports/goldeneye/build-goldeneye.sh. +# The program, your ROM (Data/), settings (midas.ini) and saves (Data/Saves/) stay in goldeneye/. +cd goldeneye || exit 1 +# first start: settings for the console (the game fills in the rest and keeps them): one game +# tick per frame, so maxfps is also the game's speed +if [ ! -f midas.ini ]; then + printf '[display]\nfullscreen=fullscreen\nmaxfps=30\nfiltering=bilinear\nshow_fps=1\n' > midas.ini +fi +# The built-in controls as one standard gamepad (rsos-gamepad); SDL then +# ignores the raw button devices. It runs in the +# game's process group: the menu stops it with the game. +if [ -x /usr/bin/rsos-gamepad ]; then + SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" + export SDL_GAMECONTROLLERCONFIG + export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 + /usr/bin/rsos-gamepad & + usleep 300000 +fi +exec ./ProjectMidas diff --git a/ports/goldeneye/README.md b/ports/goldeneye/README.md new file mode 100644 index 0000000..372d1ae --- /dev/null +++ b/ports/goldeneye/README.md @@ -0,0 +1,90 @@ +# GoldenEye 007 (Project Midas) on the RetroStone2 + +[Project Midas](https://github.com/murkantor/Project-Midas) is a PC port of GoldenEye 007 built from the +[N64 decompilation](https://github.com/n64decomp/007). Its renderer is fast3d (the Perfect Dark port's, the same +family as Super Mario 64's) and its PC builds are deliberately **32-bit**: GoldenEye's code keeps N64 pointers in +32-bit fields, so Midas runs it on the ILP32 model. The RetroStone2's ARMv7 is that same 32-bit, little-endian +model, so the game itself needs almost no change; the work is the renderer, which needs OpenGL 3.0. This folder +adds an OpenGL ES 2.0 mode to it and builds the game for the console as a native game (docs/native-games.md). + +**Nothing from the game is in this repository, the OS images or the updates.** The game's data comes from **your +own ROM** on your PC and stays yours. Project Midas takes the **GoldenEye 007 (US)** ROM, SHA-1 +`abe01e4aeb033b6c0836819f549c791b26cfde83` in `.z64` order (`.v64` and `.n64` dumps are converted). The game +also reads the ROM itself at every start, so a copy goes into `goldeneye/Data/` on the card. + +## Build + +```sh +ports/goldeneye/build-goldeneye.sh ~/"GoldenEye 007 (U).z64" ~/my-ports +``` + +It fetches Project Midas at a fixed commit (1.0.2b) into `~/rsos/goldeneye-work`, applies `goldeneye.patch`, +extracts the assets from the ROM with Midas's own tools (built for the PC: `make`, a C compiler and `python3` +are enough), builds the game for the console (`GE_GLES2`, the RetroStoneOS cross compiler), then writes: + +``` +~/my-ports/roms/native/GoldenEye 007.sh +~/my-ports/roms/native/goldeneye/ ProjectMidas, gamecontrollerdb.txt, gamecontrollerdefault.txt, + midas_ini_help.txt, Data/ (your ROM, shaders_base.txt) +``` + +Copy the `roms` folder onto the SD card's **RETROSTONE** partition; in the menu: **Native games > GoldenEye +007**. Settings (`midas.ini`) and saves (`Data/Saves/`) stay in `roms/native/goldeneye/`. Hold **Select + Start** +for a second to go back to the menu. The console needs a RetroStoneOS with Native games and `rsos-gamepad`. + +On the first start the launcher writes a `midas.ini` for the console: 30 fps (Midas runs one game tick per +frame, so this is also the game's pace), bilinear filtering and the frame counter in the top-left corner +(`show_fps=0` hides it). `midas_ini_help.txt` explains every setting. + +## Controls + +The original N64 control styles (1.1 Honey by default, 1.1-2.4 in the game's options), through `rsos-gamepad`. By +the labels on the console: + +| RetroStone2 | N64 | +|---|---| +| Analog stick (Pro) | the stick (move, turn) | +| D-pad | C buttons (look up/down, strafe) | +| R2 or Y (left) | Z (fire) | +| R1 or X (top) | R (aim) | +| L1 or L2 | L | +| B (bottom) / A (right) | A (change weapon) / B (use, reload) | +| Start | Start | + +The layout is the "RetroStone2" line at the top of `goldeneye/gamecontrollerdefault.txt`. + +## What the patch changes + +**OpenGL ES 2.0** (`GE_GLES2`, for the Mali-400): an ES 2.0 context and glad's GLES2 loader, GLSL ES 1.00 shaders, +texture sizes as uniforms (GLSL ES 1.00 has no `textureSize()`), no vertex array objects, unsized texture +formats, edge clamping instead of border clamping, no mipmaps on non-power-of-two textures without +`OES_texture_npot`, linear instead of three-point filtering, no framebuffer effects (they need blits and +multisampling: no MSAA, N64 edge AA, Low Res or Classic Mode), a window that is visible from the start (SDL's +KMSDRM backend), and streamed vertices as client-side arrays (lima made a new buffer object for every upload). + +**The Mali-400's 16-bit fragment shaders**, found on the console: +* GoldenEye's far clip discards fragments past the far plane by comparing clip z with w. In 16-bit floats those + two large, nearly equal numbers rounded to the wrong side, and the whole intro, the menus and many effects + (muzzle flashes, impacts, the Dam's gate switch) were discarded. The vertex shader (32-bit) now passes w - z. +* The noise (alpha dither) multiplied by 143758.5453, which is infinity in 16 bits: a hash that stays small. + +**32-bit ARM**: the x86 flags give way to ARM's (single-precision VFP rounds like the N64 already; +`-ffp-contract=off` keeps multiply-adds unfused, as on the N64 and SSE), the program is linked at `0x10000000` +(the address the Windows build uses: the display-list code takes any value below it for an N64 segment address), +`-fpermissive` for GCC 14, and crash reports with the ARM registers and the return addresses on the stack. +Upstream bug fixed: `stanCheckLinkedSpecialTile` declared the float arguments of the tile-walk callback as `s32`; +on the hard-float ARM ABI the floats travel in other registers, so its last argument came from the stack and +walking onto some tiles crashed the game. + +**Speed** (CPU profiles on the console): drawing on its own thread (the A20 has two cores), `-O3` and NEON math +for the renderer, `-O3` for the audio mixer. Midas's room cache (geometry kept on the GPU) stays off: its vertex +shader exceeds the Mali-400 vertex processor's 512 instructions. Its setting now applies at start-up (it only +did from an ini line). + +**The controls**: the "RetroStone2" layout above, on the N64 control styles. + +## Status + +**Works on a RetroStone2 Pro** (played: the intro, the menus and the Dam): picture, effects and sound. 30 fps in light areas, around 10-15 in the +busiest views (the N64 itself often ran GoldenEye at 15-20); the time goes to GL draw calls in the Mesa lima +driver, a few hundred a frame. The game's messages are in `rsos/logs/boot/game.log` on the SD card. diff --git a/ports/goldeneye/build-goldeneye.sh b/ports/goldeneye/build-goldeneye.sh new file mode 100755 index 0000000..db9364d --- /dev/null +++ b/ports/goldeneye/build-goldeneye.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Builds GoldenEye 007 (Project Midas) for RetroStoneOS from YOUR OWN ROM, on the PC +# (Linux/WSL), as a native game for the SD card (docs/native-games.md). +# +# ports/goldeneye/build-goldeneye.sh [output folder] +# +# Needs a built RetroStoneOS tree for the RetroStone2 with SDL2 (the RetroStone2 +# configs): ~/rsos/output, or RSOS_OUTPUT=; cmake, ninja, make, python3 and a C +# compiler for the PC (the asset extraction runs here). Project Midas is fetched at a +# fixed commit into ~/rsos/goldeneye-work (RSOS_GOLDENEYE_WORK) and patched +# (goldeneye.patch). +# +# The result contains the game's data, taken from your ROM (the game also reads the +# ROM itself at every start, from goldeneye/Data/): keep it for yourself. Never put +# it in a release, an update or a repository. +# +# Output (default ./goldeneye-retrostone2): roms/native/GoldenEye 007.sh and +# roms/native/goldeneye/. Copy the "roms" folder onto the SD card's RETROSTONE +# partition (it merges with the roms folder there). +set -euo pipefail + +REPO=https://github.com/murkantor/Project-Midas.git +COMMIT=9062f9f120b7f64d39e5db59500ed50e8e7a6686 # Project Midas 1.0.2b +ROM_SHA1=abe01e4aeb033b6c0836819f549c791b26cfde83 # GoldenEye 007 (US), .z64 order + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/goldeneye-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_GOLDENEYE_WORK:-$HOME/rsos/goldeneye-work} + +die() { echo "build-goldeneye: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +[ -e "$O/staging/usr/lib/libSDL2.so" ] || die "libSDL2.so is not in $O/staging (build RetroStoneOS with the RetroStone2 configs)" +TOOLCHAIN=$O/host/share/buildroot/toolchainfile.cmake + +# only the fixed commit (the whole history is large) +if [ ! -d "$W/.git" ]; then + git init -q "$W" + git -C "$W" remote add origin "$REPO" +fi +cd "$W" +git fetch -q --depth 1 origin "$COMMIT" +# a clean tree at the fixed commit, then our changes (the build folder, the ROM and +# the extracted assets are ignored by git and stay) +git checkout -q -f "$COMMIT" +git apply "$here/goldeneye.patch" + +# the ROM, in .z64 byte order, as baserom.u.z64 (where the extraction reads it) +python3 - "$rom" baserom.u.z64 << 'EOF' || die "$rom is not an N64 ROM" +import sys +data = bytearray(open(sys.argv[1], 'rb').read()) +head = bytes(data[:4]) +if head == b'\x37\x80\x40\x12': # .v64: 16-bit words swapped + data[0::2], data[1::2] = data[1::2], data[0::2] +elif head == b'\x40\x12\x37\x80': # .n64: 32-bit words reversed + for i in range(0, len(data) - 3, 4): + data[i:i + 4] = data[i:i + 4][::-1] +elif head != b'\x80\x37\x12\x40': + sys.exit(1) +open(sys.argv[2], 'wb').write(data) +EOF +sum=$(sha1sum baserom.u.z64 | cut -d' ' -f1) +[ "$sum" = "$ROM_SHA1" ] || + die "$rom is not the GoldenEye 007 (US) ROM Project Midas takes (SHA-1 $sum in .z64 order, needs $ROM_SHA1)" + +# the assets the build compiles in or includes (images, music, demos), with tools built for this PC +TERM=${TERM:-dumb} ./scripts/extract_baserom.u.sh +make convert_props convert_chrs convert_guns combine_images + +# the game for the console: the fast3d renderer on OpenGL ES 2.0 (GE_GLES2). CMAKE_SYSTEM_NAME +# Linux: Buildroot's toolchain file names it "Buildroot". 32-bit ARM is the same ILP32 +# little-endian model Midas is written for (its PC builds are 32-bit x86). +cmake -S . -B build-gles2 -G Ninja -DCMAKE_BUILD_TYPE=Release -DGE_VIDEO=ON -DGE_GLES2=ON \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DCMAKE_SYSTEM_NAME=Linux +cmake --build build-gles2 + +file build-gles2/ProjectMidas | grep -q 'ARM' || die "build-gles2/ProjectMidas is not an ARM program" +G=$out/roms/native/goldeneye +mkdir -p "$G/Data" +"$O/host/bin/arm-linux-strip" -o "$G/ProjectMidas" build-gles2/ProjectMidas +cp tools/gamecontrollerdb.txt tools/gamecontrollerdefault.txt tools/midas_ini_help.txt "$G/" +cp tools/shaders_base.txt "$G/Data/" +cp tools/Data_README.txt "$G/Data/README.txt" +# the game reads its levels, models and sounds from the ROM at every start +cp baserom.u.z64 "$G/Data/ge007.u.z64" +install -m 0644 "$here/GoldenEye 007.sh" "$out/roms/native/GoldenEye 007.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > GoldenEye 007. Hold Select + Start to quit." diff --git a/ports/goldeneye/goldeneye.patch b/ports/goldeneye/goldeneye.patch new file mode 100644 index 0000000..1b9c029 --- /dev/null +++ b/ports/goldeneye/goldeneye.patch @@ -0,0 +1,733 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 2df2e82..f9e4c61 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -7,13 +7,18 @@ project(ge007 C) + + set(CMAKE_C_STANDARD 11) + ++if(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm") ++ set(GE_ARM ON) ++ add_compile_options(-ffp-contract=off) ++else() ++ set(GE_ARM OFF) ++ add_compile_options(-m32 -msse2 -mfpmath=sse) ++endif() ++ + add_compile_options( +- -m32 + # Use SSE scalar floats so every operation rounds to single precision like + # the N64 FPU; x87's 80-bit excess precision flips borderline comparisons + # (it broke the stan.c tile-walk edge test and room visibility in Egypt). +- -msse2 +- -mfpmath=sse + -fno-strict-aliasing + -fwrapv + -funsigned-char +@@ -50,7 +55,11 @@ if(WIN32) + -Wl,--stack,16777216) + link_libraries(dbghelp) + else() +- add_link_options(-m32 -no-pie) ++ if(GE_ARM) ++ add_link_options(-no-pie -Wl,-Ttext-segment=0x10000000) ++ else() ++ add_link_options(-m32 -no-pie) ++ endif() + endif() + + add_compile_definitions( +@@ -175,7 +184,6 @@ set(GU_SRC + # oddtextures texel data are big-endian blobs. + set(ASSET_C_FLAGS + -std=gnu11 +- -m32 -malign-data=abi + -fms-extensions -ffreestanding -fno-strict-aliasing -fwrapv -funsigned-char + -fno-common -fno-zero-initialized-in-bss -fno-toplevel-reorder -fno-pic + -Og -w +@@ -192,6 +200,15 @@ set(ASSET_C_FLAGS + -I${CMAKE_SOURCE_DIR}/port/include + -I${CMAKE_SOURCE_DIR}/src/libultra) + ++if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 14) ++ list(APPEND ASSET_C_FLAGS -fpermissive) ++ add_compile_options($<$:-fpermissive>) ++endif() ++ ++if(NOT GE_ARM) ++ list(APPEND ASSET_C_FLAGS -m32 -malign-data=abi) ++endif() ++ + if(NOT CMAKE_OBJCOPY) + set(CMAKE_OBJCOPY objcopy) + endif() +@@ -359,6 +376,10 @@ list(APPEND PORT_SRC src/libultrare/io/vitbl.c) + # GE microcode opcode work lands (docs/port/ge-microcode.md); the null + # renderer in pcsched.c stays the headless default. + option(GE_VIDEO "Build the fast3d renderer (SDL2 + OpenGL)" OFF) ++option(GE_GLES2 "fast3d on OpenGL ES 2.0 (no framebuffer effects)" OFF) ++if(GE_GLES2) ++ add_compile_definitions(USE_GLES2) ++endif() + + # Testing convenience: every solo mission, bonus stage and difficulty + # (incl. 007 mode) selectable regardless of save state. Progression logic +@@ -388,6 +409,11 @@ if(GE_VIDEO) + port/fast3d/glad/glad.c + ) + add_compile_definitions(GE_VIDEO=1) ++ if(GE_ARM) ++ set_source_files_properties(${FAST3D_SRC} PROPERTIES ++ COMPILE_OPTIONS "-O3;-fno-math-errno;-funsafe-math-optimizations") ++ set_source_files_properties(port/src/audio_pc.c PROPERTIES COMPILE_OPTIONS "-O3") ++ endif() + else() + set(FAST3D_SRC) + # video.c/input_pc.c/raphnet.c are GE_VIDEO-only; exclude them from the port glob. +@@ -448,7 +474,10 @@ if(GE_VIDEO) + # 32-bit SDL2: needs libsdl2-dev:i386 (+ libgl-dev:i386), or set + # SDL2_LINUX_ROOT to a self-built prefix (e.g. CI's cached, user-writable + # install, avoiding a system-wide /usr install). +- if(SDL2_LINUX_ROOT) ++ if(GE_GLES2) ++ find_package(SDL2 REQUIRED) ++ target_link_libraries(ge SDL2::SDL2 dl pthread) ++ elseif(SDL2_LINUX_ROOT) + target_include_directories(ge PRIVATE ${SDL2_LINUX_ROOT}/include/SDL2) + target_link_libraries(ge ${SDL2_LINUX_ROOT}/lib/i386-linux-gnu/libSDL2.so GL dl pthread) + set_target_properties(ge PROPERTIES BUILD_RPATH ${SDL2_LINUX_ROOT}/lib/i386-linux-gnu) +diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp +index e1efb53..269376f 100644 +--- a/port/fast3d/gfx_opengl.cpp ++++ b/port/fast3d/gfx_opengl.cpp +@@ -67,6 +67,8 @@ struct ShaderProgram { + GLint frame_count_location; + GLint noise_scale_location; + GLint three_point_filter_locations[2]; ++ GLint tex_size_locations[2]; /* GLSL < 1.30: uTexSize0/1 */ ++ float tex_size_pushed[2][2]; + /* GE retained geometry (below): the fragment shader object outlives the + * link so the twin program can share it, plus the ids its features come from */ + GLuint fragment_shader; +@@ -91,6 +93,11 @@ static GLuint opengl_vao; + static bool current_depth_mask; + + static uint32_t frame_count; ++#ifdef USE_GLES2 ++#define GL_FRAME_COUNT ((GLint)(frame_count & 63)) ++#else ++#define GL_FRAME_COUNT ((GLint)frame_count) ++#endif + + static std::vector framebuffers; + static size_t current_framebuffer; +@@ -120,6 +127,24 @@ static void gl_set_has_mips(GLuint id, uint8_t v) { + } + gl_tex_has_mips[id] = v; + } ++/* each texture object's size (GLSL < 1.30 shaders get it as a uniform) */ ++static std::vector> gl_tex_size; ++static void gl_set_tex_size(GLuint id, uint32_t w, uint32_t h) { ++ if (id >= gl_tex_size.size()) { ++ gl_tex_size.resize((size_t)id + 64, std::make_pair((uint16_t)1, (uint16_t)1)); ++ } ++ gl_tex_size[id] = std::make_pair((uint16_t)w, (uint16_t)h); ++} ++static void gl_tex_size_of(GLuint id, float out[2]) { ++ if (id < gl_tex_size.size()) { ++ out[0] = gl_tex_size[id].first; ++ out[1] = gl_tex_size[id].second; ++ } else { ++ out[0] = out[1] = 1.0f; ++ } ++} ++/* push the bound textures' sizes into a program whose cache says otherwise */ ++static void gl_sync_tex_size(const GLint loc[2], float pushed[2][2]); + static bool gl_has_mips(GLuint id) { + return id < gl_tex_has_mips.size() && gl_tex_has_mips[id] != 0; + } +@@ -129,6 +154,7 @@ static char gl_glsl_version_str[16] = "130"; + static GLenum gl_mirror_clamp = GL_MIRROR_CLAMP_TO_EDGE; + static bool gl_es = false; + static bool gl_core_profile = false; ++static bool gl_npot = true; /* GLES2 without OES_texture_npot: no mipmaps on NPOT textures */ + + static void gl_ret_restore(void); /* GE retained geometry: back to the interpreter's VBO / program (defined below) */ + static struct ShaderProgram* gl_current_prg_set(struct ShaderProgram* prg); +@@ -163,7 +189,7 @@ static void gfx_opengl_vertex_array_set_attribs(struct ShaderProgram* prg) { + + static void gfx_opengl_set_uniforms(struct ShaderProgram* prg) { + if (prg->frame_count_location >= 0) { +- glUniform1i(prg->frame_count_location, frame_count); ++ glUniform1i(prg->frame_count_location, GL_FRAME_COUNT); + } + if (prg->noise_scale_location >= 0) { + glUniform1f(prg->noise_scale_location, current_noise_scale); +@@ -387,6 +413,8 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + * to the fragment stage; both interpolate perspective-correct, so + * z/w is the fragment's true NDC depth. No extra attribute. */ + append_line(vs_buf, &vs_len, "OUTPUT vec2 vGeZw;"); ++ if (gl_glsl_version < 130) ++ append_line(vs_buf, &vs_len, "OUTPUT float vGeFar;"); + } + if (cc_features.opt_ge_pixfog) { + /* fog register curve (mul, offset) rides as a constant attribute */ +@@ -445,6 +473,8 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + + if (cc_features.opt_ge_farclip || cc_features.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, " vGeZw = aVtxPos.zw;"); ++ if (gl_glsl_version < 130) ++ append_line(vs_buf, &vs_len, " vGeFar = aVtxPos.w - aVtxPos.z;"); + } + if (cc_features.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, " vGeFogP = aGeFogP;"); +@@ -455,7 +485,7 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + append_line(vs_buf, &vs_len, " gl_Position = aVtxPos;"); + if (!GLAD_GL_ARB_depth_clamp) { + // HACK: workaround for no GL_DEPTH_CLAMP +- append_line(vs_buf, &vs_len, " gl_Position.z *= 0.3f;"); ++ append_line(vs_buf, &vs_len, " gl_Position.z *= 0.3;"); + } + append_line(vs_buf, &vs_len, "}"); + +@@ -499,6 +529,8 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + } + if (cc_features.opt_ge_farclip || cc_features.opt_ge_pixfog) { + append_line(fs_buf, &fs_len, "INPUT vec2 vGeZw;"); ++ if (gl_glsl_version < 130) ++ append_line(fs_buf, &fs_len, "INPUT float vGeFar;"); + } + if (cc_features.opt_ge_pixfog) { + append_line(fs_buf, &fs_len, "INPUT vec2 vGeFogP;"); +@@ -515,11 +547,15 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + + if (cc_features.used_textures[0]) { + append_line(fs_buf, &fs_len, "uniform sampler2D uTex0;"); ++ if (gl_glsl_version < 130) ++ append_line(fs_buf, &fs_len, "uniform vec2 uTexSize0;"); + if (current_filter_mode == FILTER_THREE_POINT) + append_line(fs_buf, &fs_len, "uniform int three_point_filter0;"); + } + if (cc_features.used_textures[1]) { + append_line(fs_buf, &fs_len, "uniform sampler2D uTex1;"); ++ if (gl_glsl_version < 130) ++ append_line(fs_buf, &fs_len, "uniform vec2 uTexSize1;"); + if (current_filter_mode == FILTER_THREE_POINT) + append_line(fs_buf, &fs_len, "uniform int three_point_filter1;"); + } +@@ -528,8 +564,13 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + append_line(fs_buf, &fs_len, "uniform float noise_scale;"); + + append_line(fs_buf, &fs_len, "float random(in vec3 value) {"); +- append_line(fs_buf, &fs_len, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));"); +- append_line(fs_buf, &fs_len, " return fract(sin(random) * 143758.5453);"); ++ if (gl_glsl_version < 130) { ++ append_line(fs_buf, &fs_len, " float n = fract(dot(value.xy, vec2(0.06711056, 0.00583715)) + value.z * 0.618034);"); ++ append_line(fs_buf, &fs_len, " return fract(52.9829189 * n);"); ++ } else { ++ append_line(fs_buf, &fs_len, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));"); ++ append_line(fs_buf, &fs_len, " return fract(sin(random) * 143758.5453);"); ++ } + append_line(fs_buf, &fs_len, "}"); + + if (current_filter_mode == FILTER_THREE_POINT) { +@@ -555,7 +596,7 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + + append_line(fs_buf, &fs_len, " lowp vec4 cw = vec4(0.0);"); + append_line(fs_buf, &fs_len, " for (int i = 0; i < 16; ++i) {"); +- append_line(fs_buf, &fs_len, " vec2 xy = vec2(float(i & 3), float(i >> 2));"); ++ append_line(fs_buf, &fs_len, " vec2 xy = vec2(mod(float(i), 4.0), floor(float(i) / 4.0));"); + append_line(fs_buf, &fs_len, " lowp float w = 0.009947 - length(xy) * 0.001;"); + append_line(fs_buf, &fs_len, " vec2 scaled_uv = uv + (vec2(-1.5) + xy) / texSize;"); + +@@ -589,14 +630,20 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + if (cc_features.opt_ge_farclip) { + /* GE far clip: fragment past the projection far plane - the console + * clips fogless geometry there. */ +- append_line(fs_buf, &fs_len, " if (vGeZw.x > vGeZw.y) discard;"); ++ if (gl_glsl_version < 130) ++ append_line(fs_buf, &fs_len, " if (vGeFar < 0.0) discard;"); ++ else ++ append_line(fs_buf, &fs_len, " if (vGeZw.x > vGeZw.y) discard;"); + } + + for (int i = 0; i < 2; i++) { + if (cc_features.used_textures[i]) { + bool s = cc_features.clamp[i][0], t = cc_features.clamp[i][1]; + +- fs_len += sprintf(fs_buf + fs_len, " vec2 texSize%d = vec2(textureSize(uTex%d, 0));\n", i, i); ++ if (gl_glsl_version < 130) ++ fs_len += sprintf(fs_buf + fs_len, " vec2 texSize%d = uTexSize%d;\n", i, i); ++ else ++ fs_len += sprintf(fs_buf + fs_len, " vec2 texSize%d = vec2(textureSize(uTex%d, 0));\n", i, i); + + /* GE sky/water per-pixel divide: S/W and T/W by 1/W at this pixel, + * as the RDP does (the old CPU grid only approximated it). q is +@@ -858,6 +905,9 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad + prg->noise_scale_location = glGetUniformLocation(shader_program, "noise_scale"); + prg->three_point_filter_locations[0] = glGetUniformLocation(shader_program, "three_point_filter0"); + prg->three_point_filter_locations[1] = glGetUniformLocation(shader_program, "three_point_filter1"); ++ prg->tex_size_locations[0] = glGetUniformLocation(shader_program, "uTexSize0"); ++ prg->tex_size_locations[1] = glGetUniformLocation(shader_program, "uTexSize1"); ++ prg->tex_size_pushed[0][0] = prg->tex_size_pushed[1][0] = -1.0f; + + gfx_opengl_load_shader(prg); + +@@ -1001,7 +1051,12 @@ static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width, + } + g_GfxUpDrainUs = portProfNow() - up_t; + up_t += g_GfxUpDrainUs; ++#ifdef USE_GLES2 ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba32_buf); ++#else + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba32_buf); ++#endif ++ gl_set_tex_size(current_texture_ids[last_selected_tile], width, height); + g_GfxUpTexUs = portProfNow() - up_t; + up_t += g_GfxUpTexUs; + /* a fresh level 0 invalidates any older chain on this object; the N64 +@@ -1053,7 +1108,11 @@ static uint32_t gfx_cm_to_opengl(uint32_t val) { + switch (val) { + case 0x10: /* GE_TX_BORDER (gfx_pc.cpp): a grown font glyph - outside the + * texture is GL's default border colour, transparent black */ ++#ifdef USE_GLES2 ++ return GL_CLAMP_TO_EDGE; ++#else + return GL_CLAMP_TO_BORDER; ++#endif + case G_TX_NOMIRROR | G_TX_CLAMP: + return GL_CLAMP_TO_EDGE; + case G_TX_MIRROR | G_TX_WRAP: +@@ -1069,6 +1128,16 @@ static uint32_t gfx_cm_to_opengl(uint32_t val) { + extern "C" int ge_sky_tri_active; /* gfx_pc: inside an RDP-triangle (sky/water) draw */ + extern "C" int ge_mip_active; /* gfx_pc: this sampler serves a G_TL_LOD draw - minify through mips */ + ++static bool gl_can_mip(GLuint id) { ++ if (gl_npot) { ++ return true; ++ } ++ float sz[2]; ++ gl_tex_size_of(id, sz); ++ const uint32_t w = (uint32_t)sz[0], h = (uint32_t)sz[1]; ++ return (w & (w - 1)) == 0 && (h & (h - 1)) == 0; ++} ++ + static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint32_t cms, uint32_t cmt) { + const GLint filter = linear_filter && (current_filter_mode == FILTER_LINEAR) ? GL_LINEAR : GL_NEAREST; + /* THIS is the value the draw actually wants - gfx_pc computes it per draw +@@ -1103,7 +1172,7 @@ static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, gfx_cm_to_opengl(cmt)); + return; + } +- if (ge_mip_active && !ge_sky_tri_active) { ++ if (ge_mip_active && !ge_sky_tri_active && gl_can_mip(current_texture_ids[tile])) { + /* GE N64 mipmaps: the RDP walked a mip chain for this draw, so give GL + * one (once per upload, box-filtered like GE's own builder) and minify + * through it to avoid moire. Bilerp becomes trilinear and point / +@@ -1138,7 +1207,7 @@ static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint + const char* e = getenv("GE_SKY_NOMIP"); + skynomip = (e != NULL) && (e[0] != '0'); + } +- if (skynomip) { ++ if (skynomip || !gl_can_mip(current_texture_ids[tile])) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + } else { + glGenerateMipmap(GL_TEXTURE_2D); +@@ -1283,6 +1352,24 @@ static void gfx_opengl_set_use_alpha(bool use_alpha, bool modulate) { + * room decals keep the one-sided test. GE_DEC_CLIP=0 = old behaviour, + * GE_DEC_TOL= (default 8). */ + extern float gfx_ge_dec_b, gfx_ge_dec_d; ++static void gl_stream_vertices(const float* data, size_t len) { ++#ifdef USE_GLES2 ++ struct ShaderProgram* prg = gl_current_prg; ++ glBindBuffer(GL_ARRAY_BUFFER, 0); ++ size_t pos = 0; ++ for (int i = 0; i < prg->num_attribs; i++) { ++ if (prg->attrib_locations[i] >= 0) { ++ glVertexAttribPointer(prg->attrib_locations[i], prg->attrib_sizes[i], GL_FLOAT, GL_FALSE, ++ prg->num_floats * sizeof(float), data + pos); ++ } ++ pos += prg->attrib_sizes[i]; ++ } ++ (void)len; ++#else ++ glBufferData(GL_ARRAY_BUFFER, sizeof(float) * len, data, GL_STREAM_DRAW); ++#endif ++} ++ + static void gfx_opengl_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_t buf_vbo_num_tris) { + // printf("flushing %d tris\n", buf_vbo_num_tris); + static int clip = -1; +@@ -1294,8 +1381,11 @@ static void gfx_opengl_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_ + if (e != NULL && atof(e) > 0.0) tol = (float)atof(e); + } + gl_ret_restore(); /* after a retained draw: the stream VBO + this program's arrays again */ ++ if (gl_current_prg != nullptr) { ++ gl_sync_tex_size(gl_current_prg->tex_size_locations, gl_current_prg->tex_size_pushed); ++ } + if (!clip || !ge_dec_draw || gfx_ge_dec_b >= 0.0f || buf_vbo_num_tris == 0) { +- glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); ++ gl_stream_vertices(buf_vbo, buf_vbo_len); + glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris); + return; + } +@@ -1326,14 +1416,14 @@ static void gfx_opengl_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_ + glDisable(GL_POLYGON_OFFSET_FILL); + glStencilFunc(GL_ALWAYS, 1, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); +- glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, pushed.data(), GL_STREAM_DRAW); ++ gl_stream_vertices(pushed.data(), buf_vbo_len); + glDrawArrays(GL_TRIANGLES, 0, nv); + /* 2. draw */ + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + gfx_opengl_set_depth_mode(ge_dm_args[0], ge_dm_args[1], ge_dm_args[2], ge_dm_args[3], ge_dm_zmode); + glStencilFunc(GL_EQUAL, 1, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); +- glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); ++ gl_stream_vertices(buf_vbo, buf_vbo_len); + glDrawArrays(GL_TRIANGLES, 0, nv); + /* 3. clear */ + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); +@@ -1490,7 +1580,11 @@ static void gfx_opengl_init_extensions(void) { + } + + static void gfx_opengl_init(void) { ++#ifdef USE_GLES2 ++ if (!gladLoadGLES2Loader(gl_load_proc) || glGetString == NULL || glEnable == NULL) { ++#else + if (!gladLoadGLLoader(gl_load_proc) || glGetString == NULL || glEnable == NULL) { ++#endif + sysFatalError("Could not load OpenGL.\nReported SDL error: %s", SDL_GetError()); + } + +@@ -1509,13 +1603,22 @@ static void gfx_opengl_init(void) { + // name the GPU (iGPU vs dGPU is the first question on any fps report) + gfx_opengl_log_info(); + +- if (GLVersion.major < 2 || (GLVersion.major == 2 && GLVersion.minor < 1)) { ++ if (!gl_es && (GLVersion.major < 2 || (GLVersion.major == 2 && GLVersion.minor < 1))) { + const char *ver = (const char *)glGetString(GL_VERSION); + sysFatalError("Could not load OpenGL 2.1.\nReported version: %d.%d (%s)", + GLVersion.major, GLVersion.minor, ver ? ver : "unknown"); + } + +- if (!gfx_opengl_supports_shaders()) { ++#ifdef USE_GLES2 ++ // framebuffer effects need blits and multisampling: everything draws to the screen ++ gfx_framebuffers_enabled = false; ++ { ++ const char *ext = (const char *)glGetString(GL_EXTENSIONS); ++ gl_npot = ext && strstr(ext, "GL_OES_texture_npot"); ++ sysLogPrintf(LOG_NOTE, "GL: OES_texture_npot: %s", gl_npot ? "yes" : "no"); ++ } ++#endif ++ if (!gl_es && !gfx_opengl_supports_shaders()) { + sysLogPrintf(LOG_WARNING, "GL: GLSL 1.30 may be unsupported"); + // maybe replace this with sysFatalError, though the GLSL compiler will cause that later anyway + } +@@ -1552,12 +1655,16 @@ static void gfx_opengl_init(void) { + } + snprintf(gl_glsl_version_str, sizeof(gl_glsl_version_str), "%d core", gl_glsl_version); + } ++#ifdef USE_GLES2 ++ gl_glsl_version = 100; ++ snprintf(gl_glsl_version_str, sizeof(gl_glsl_version_str), "100"); ++#endif + sysLogPrintf(LOG_NOTE, "GL: using GLSL version %s", gl_glsl_version_str); + + glGenBuffers(1, &opengl_vbo); + glBindBuffer(GL_ARRAY_BUFFER, opengl_vbo); + +- if (gl_core_profile || gl_es) { ++ if (gl_core_profile || (gl_es && glGenVertexArrays != NULL)) { + // warn user that funny things can happen + sysLogPrintf(LOG_WARNING, "GL: using core profile or ES, watch out for errors"); + // core/es will explode if we don't use a VAO for our VBO +@@ -1747,6 +1854,11 @@ static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, + } + + bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) { ++#ifdef USE_GLES2 ++ if (!gfx_framebuffers_enabled && noise_scale != 0.0f) { ++ current_noise_scale = 1.0f / noise_scale; ++ } ++#endif + if (gfx_framebuffers_enabled && fb_id < (int)framebuffers.size()) { + Framebuffer& fb = framebuffers[fb_id]; + if (noise_scale != 0.0f) { +@@ -1962,6 +2074,11 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool + } + + void gfx_opengl_set_texture_filter(FilteringMode mode) { ++#ifdef USE_GLES2 ++ if (mode == FILTER_THREE_POINT) { ++ mode = FILTER_LINEAR; ++ } ++#endif + current_filter_mode = mode; + } + +@@ -2033,6 +2150,8 @@ struct GlRetainedProgram { + int num_attribs = 0; + GLint u_mats, u_mats1, u_fixup, u_fogp, u_fogsel, u_fogrgb, u_decal, u_light, u_shade, u_kind, u_prim, u_env, u_lod; + GLint frame_count_location, noise_scale_location, three_point_filter_locations[2]; ++ GLint tex_size_locations[2] = { -1, -1 }; ++ float tex_size_pushed[2][2] = { { -1.0f, -1.0f }, { -1.0f, -1.0f } }; + /* uniform value cache: a program object keeps its uniforms between binds, + * so a retained draw uploads only what changed since this program's last + * one (the per-draw GL call count is the PC's cost, not the triangles) */ +@@ -2132,6 +2251,8 @@ static GlRetainedProgram* gl_ret_program_get(struct ShaderProgram* prg) { + } + if (cc.opt_ge_farclip || cc.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, "OUTPUT vec2 vGeZw;"); ++ if (gl_glsl_version < 130) ++ append_line(vs_buf, &vs_len, "OUTPUT float vGeFar;"); + } + if (cc.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, "INPUT vec2 aGeFogP;"); +@@ -2210,13 +2331,15 @@ static GlRetainedProgram* gl_ret_program_get(struct ShaderProgram* prg) { + } + if (cc.opt_ge_farclip || cc.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, " vGeZw = clip.zw;"); ++ if (gl_glsl_version < 130) ++ append_line(vs_buf, &vs_len, " vGeFar = clip.w - clip.z;"); + } + if (cc.opt_ge_pixfog) { + append_line(vs_buf, &vs_len, " vGeFogP = uFogP.xy * 255.0;"); /* the frame's registers, not the captured pair */ + } + append_line(vs_buf, &vs_len, " gl_Position = clip;"); + if (!GLAD_GL_ARB_depth_clamp) { +- append_line(vs_buf, &vs_len, " gl_Position.z *= 0.3f;"); ++ append_line(vs_buf, &vs_len, " gl_Position.z *= 0.3;"); + } + append_line(vs_buf, &vs_len, "}"); + vs_buf[vs_len] = '\0'; +@@ -2314,6 +2437,8 @@ static GlRetainedProgram* gl_ret_program_get(struct ShaderProgram* prg) { + rp.noise_scale_location = glGetUniformLocation(program, "noise_scale"); + rp.three_point_filter_locations[0] = glGetUniformLocation(program, "three_point_filter0"); + rp.three_point_filter_locations[1] = glGetUniformLocation(program, "three_point_filter1"); ++ rp.tex_size_locations[0] = glGetUniformLocation(program, "uTexSize0"); ++ rp.tex_size_locations[1] = glGetUniformLocation(program, "uTexSize1"); + /* back to the interpreter's program: the caller expects it current */ + if (gl_current_prg != nullptr) { + glUseProgram(gl_current_prg->opengl_program_id); +@@ -2593,9 +2718,9 @@ static void gl_ret_draw(int desc, const uint16_t* idx, uint32_t nidx, const floa + glUniform1iv(rp->u_kind, d.prg->num_inputs, kinds); + } + } +- if (rp->frame_count_location >= 0 && rp->c_frame_count != (GLint)frame_count) { +- rp->c_frame_count = (GLint)frame_count; +- glUniform1i(rp->frame_count_location, frame_count); ++ if (rp->frame_count_location >= 0 && rp->c_frame_count != GL_FRAME_COUNT) { ++ rp->c_frame_count = GL_FRAME_COUNT; ++ glUniform1i(rp->frame_count_location, GL_FRAME_COUNT); + } + if (rp->noise_scale_location >= 0 && rp->c_noise != current_noise_scale) { + rp->c_noise = current_noise_scale; +@@ -2607,6 +2732,7 @@ static void gl_ret_draw(int desc, const uint16_t* idx, uint32_t nidx, const floa + glUniform1i(rp->three_point_filter_locations[t], current_textures_linear_filter[t]); + } + } ++ gl_sync_tex_size(rp->tex_size_locations, rp->tex_size_pushed); + rp->c_valid = true; + /* Backface cull on the GPU: front = COUNTER-clockwise in GL's y-up window + * space, verified against a cull-off reference. The clockwise reading of +@@ -2676,6 +2802,20 @@ extern "C" void gfx_opengl_retained_stats(unsigned* draws, unsigned* tris, unsig + gl_ret_dbg_draws = gl_ret_dbg_tris = 0; + } + ++static void gl_sync_tex_size(const GLint loc[2], float pushed[2][2]) { ++ for (int t = 0; t < 2; t++) { ++ if (loc[t] >= 0) { ++ float sz[2]; ++ gl_tex_size_of(current_texture_ids[t], sz); ++ if (pushed[t][0] != sz[0] || pushed[t][1] != sz[1]) { ++ pushed[t][0] = sz[0]; ++ pushed[t][1] = sz[1]; ++ glUniform2f(loc[t], sz[0], sz[1]); ++ } ++ } ++ } ++} ++ + static struct ShaderProgram* gl_current_prg_set(struct ShaderProgram* prg) { + gl_current_prg = prg; + return prg; +diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp +index 52a8146..841b240 100644 +--- a/port/fast3d/gfx_sdl2.cpp ++++ b/port/fast3d/gfx_sdl2.cpp +@@ -325,7 +325,11 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { + } + + // we will unhide the window once the GL context is successfully created ++#ifdef USE_GLES2 ++ Uint32 flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL; ++#else + Uint32 flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL; ++#endif + + // if fullscreen was requested, start the window in fullscreen right away + if (set->fullscreen) { +@@ -383,6 +387,12 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { + + // ideally we need 3.0 compat + // if that doesn't work, try 3.2 core in case we're on mac, 2.1 compat as a last resort ++#ifdef USE_GLES2 ++ static u32 glver[][3] = { ++ { 0, 0, 0 }, // for command line override ++ { 2, 0, SDL_GL_CONTEXT_PROFILE_ES }, ++ }; ++#else + static u32 glver[][3] = { + { 0, 0, 0 }, // for command line override + { 3, 0, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 3.0: default, has all the features required +@@ -391,6 +401,7 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { + { 3, 0, SDL_GL_CONTEXT_PROFILE_ES }, // es3: don't really support ES properly, but we can try + { 2, 1, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 2.1: absolute last resort, will still require GLSL130 as an extension + }; ++#endif + + u32 verstart = 1; + const u32 verend = sizeof(glver) / sizeof(*glver); +@@ -794,7 +805,21 @@ static void gfx_sdl_maybe_screenshot(void) { + uint8_t *px = (uint8_t *)SDL_calloc(1, padded * h); + if (!px) return; + glPixelStorei(GL_PACK_ALIGNMENT, 1); ++#ifdef USE_GLES2 ++ { ++ uint8_t *rgba = (uint8_t *)SDL_malloc((size_t)w * h * 4); ++ if (!rgba) { SDL_free(px); return; } ++ glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, rgba); ++ for (size_t i = 0; i < (size_t)w * h; i++) { ++ px[i * 3 + 0] = rgba[i * 4 + 0]; ++ px[i * 3 + 1] = rgba[i * 4 + 1]; ++ px[i * 3 + 2] = rgba[i * 4 + 2]; ++ } ++ SDL_free(rgba); ++ } ++#else + glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, px); ++#endif + /* repack in place bottom-up rows into padded stride, RGB->BGR */ + for (int y = h - 1; y >= 0; y--) { + uint8_t *src = px + (size_t)y * stride; +diff --git a/port/src/main.c b/port/src/main.c +index 71ff41d..145b875 100644 +--- a/port/src/main.c ++++ b/port/src/main.c +@@ -514,6 +514,36 @@ static void portArmWatchpoints(void) + } + #else + #include ++#if defined(__arm__) ++#include ++#include ++#include ++#include ++extern char __executable_start[], etext[]; ++static void portCrashHandlerArm(int sig, siginfo_t *si, void *ucv) ++{ ++ const ucontext_t *uc = (const ucontext_t *) ucv; ++ const struct sigcontext *m = &uc->uc_mcontext; ++ const unsigned long *sp = (const unsigned long *) m->arm_sp; ++ int i, k = 0; ++ ++ fprintf(stderr, "\nge: fatal signal %d (thread %ld) at pc 0x%08lx lr 0x%08lx sp 0x%08lx, fault address %p\n", ++ sig, (long) syscall(SYS_gettid), m->arm_pc, m->arm_lr, m->arm_sp, si->si_addr); ++ fprintf(stderr, "ge: r0 %08lx r1 %08lx r2 %08lx r3 %08lx r4 %08lx r5 %08lx r6 %08lx r7 %08lx\n", ++ m->arm_r0, m->arm_r1, m->arm_r2, m->arm_r3, m->arm_r4, m->arm_r5, m->arm_r6, m->arm_r7); ++ fprintf(stderr, "ge: r8 %08lx r9 %08lx r10 %08lx fp %08lx ip %08lx\nge: return addresses on the stack:", ++ m->arm_r8, m->arm_r9, m->arm_r10, m->arm_fp, m->arm_ip); ++ for (i = 0; i < 512 && k < 24; i++) { ++ if (sp[i] >= (unsigned long) __executable_start && sp[i] < (unsigned long) etext) { ++ fprintf(stderr, " %08lx", sp[i]); ++ k++; ++ } ++ } ++ fprintf(stderr, "\n"); ++ fflush(NULL); ++ _exit(128 + sig); ++} ++#endif + + /* Bring-up crash handler: print a raw backtrace (addresses decode with + * addr2line -e build_pc/ge) so crashes are diagnosable without gdb. */ +@@ -793,6 +823,18 @@ int main(int argc, char **argv) + #endif + signal(SIGFPE, portCrashHandler); + signal(SIGILL, portCrashHandler); ++#if defined(__arm__) && !defined(_WIN32) ++ { ++ struct sigaction sa; ++ memset(&sa, 0, sizeof(sa)); ++ sa.sa_sigaction = portCrashHandlerArm; ++ sa.sa_flags = SA_SIGINFO; ++ sigaction(SIGSEGV, &sa, NULL); ++ sigaction(SIGBUS, &sa, NULL); ++ sigaction(SIGILL, &sa, NULL); ++ sigaction(SIGFPE, &sa, NULL); ++ } ++#endif + #ifdef _WIN32 + /* GE_STALLTEST=1: park the main thread here forever so the stall + * watchdog's boot-grace path + loader-lock-free module naming can be +diff --git a/port/src/video.c b/port/src/video.c +index b883b42..8dcb2fa 100644 +--- a/port/src/video.c ++++ b/port/src/video.c +@@ -71,7 +71,11 @@ static int s_CfgWidescreen = 1; /* 0 Stretch, 1 Wide (widen FOV to the win + /* [display] render_thread: -1 auto (on when tickhz != 60), 0 off, 1 on; GE_RENDERTHREAD + * overrides. Re-applied at the next frame boundary when it or the tick changes + * (see portRenderThreadFollowTick). */ ++#ifdef USE_GLES2 ++static int s_CfgRenderThread = 1; ++#else + static int s_CfgRenderThread = -1; ++#endif + static int s_CfgAdzy = 0; /* [display] adzy: Siberian Special Forces + snow cuff */ + static int s_CfgUnlockAll = 0; /* [display] unlock_all: every mission/difficulty/cheat available */ + static int s_CfgPausePillars = 0; /* [display] pause_pillars: animated 4:3 pillarbox while the watch is up */ +@@ -4904,6 +4908,7 @@ s32 videoInit(void) + s_CfgTriHoist = (e[0] != '0'); + gfx_tri_hoist = s_CfgTriHoist ? 1u : 0u; + } ++ portRoomCacheSetMode(s_CfgRoomCache); + portConLogf("ge: video: room_cache=%d tri_hoist=%d\n", s_CfgRoomCache, s_CfgTriHoist); + } + { /* [display] hw_mipmaps: N64 mip chains on G_TL_LOD draws (reduces Low Res +diff --git a/src/game/stan.c b/src/game/stan.c +index df375ec..b323780 100644 +--- a/src/game/stan.c ++++ b/src/game/stan.c +@@ -163,7 +163,7 @@ const char aStanlinelog[] = "-stanlinelog"; + // forward declarations + + s32 stanIsSpecialBit1Set(StandTile *arg0, struct StandTileLocusCallbackRecord* arg1); +-s32 stanCheckLinkedSpecialTile(StandTile *tile, s32 pointIdx, s32 arg2, s32 arg3, s32 arg4, s32 *outFlags); ++s32 stanCheckLinkedSpecialTile(StandTile *tile, s32 pointIdx, f32 arg2, f32 arg3, f32 arg4, s32 *outFlags); + s32 sub_GAME_7F0B21B0(StandTile **tileStack, f32 target_x, f32 target_z, f32 radius, s32 *rooms, s32 *count_rtn, s32 bufMax); + f32 getShortest2dDispToInfTripleEdge(StandTile *tile, s32 start3index, f32 p_x, f32 p_z); + StanCollisionResult sub_GAME_7F0B1DDC(struct StandTile**, f32, f32, f32, standTileLocusCallback_A_t, standTileLocusCallback_B_t, standTileLocusCallback_C_t, struct StandTileLocusCallbackRecord*); +@@ -2716,7 +2716,7 @@ s32 stanIsSpecialBit1Set(StandTile *arg0, struct StandTileLocusCallbackRecord *a + /** + * Address: 7F0B2274 + */ +-s32 stanCheckLinkedSpecialTile(StandTile *tile, s32 pointIdx, s32 arg2, s32 arg3, s32 arg4, s32 *outFlags) ++s32 stanCheckLinkedSpecialTile(StandTile *tile, s32 pointIdx, f32 arg2, f32 arg3, f32 arg4, s32 *outFlags) + { + u16 link; + StandTile *target; +diff --git a/tools/gamecontrollerdefault.txt b/tools/gamecontrollerdefault.txt +index a7d777e..8f5ee5a 100644 +--- a/tools/gamecontrollerdefault.txt ++++ b/tools/gamecontrollerdefault.txt +@@ -79,6 +79,8 @@ + # The N64-adapter layouts below are not paired: an N64 pad has only N64 + # controls, so its Standard and its Pro would be the same line. + ++RetroStone2 | name=RetroStoneOS Gamepad, n64 | stick_x=leftx+; stick_y=lefty-; a=a; b=b; z=righttrigger,x; r=rightshoulder,y; l=leftshoulder,lefttrigger; start=start; c_up=dpup; c_down=dpdown; c_left=dpleft; c_right=dpright ++ + NSO N64 (BT) | name=Nintendo N64 Controller, n64 | stick_x=leftx+; stick_y=lefty-; a=a; b=b; z=lefttrigger; start=start; l=leftshoulder; r=rightshoulder; c_up=y; c_down=righttrigger; c_left=x; c_right=back; dpad_up=dpup; dpad_down=dpdown; dpad_left=dpleft; dpad_right=dpright + + 8BitDo 64 (DInput) | name=8BitDo 64, n64 | stick_x=leftx+; stick_y=lefty-; a=a; b=b; z=lefttrigger,righttrigger; start=start; l=leftshoulder; r=rightshoulder; c_up=b15; c_down=b16; c_left=b18; c_right=b19; dpad_up=dpup; dpad_down=dpdown; dpad_left=dpleft; dpad_right=dpright diff --git a/ports/sm64/README.md b/ports/sm64/README.md new file mode 100644 index 0000000..56427ff --- /dev/null +++ b/ports/sm64/README.md @@ -0,0 +1,83 @@ +# Super Mario 64 on the RetroStone2 (native, built from your own ROM) + +This folder builds [sm64-port](https://github.com/sm64-port/sm64-port) (the PC port of the Super Mario 64 +decompilation) for RetroStoneOS, where it runs as a **native game**: ARM code with the Mali-400 GPU (GLES2), no N64 +emulator (docs/native-games.md). + +**Nothing from the game is in this repository, in the OS images or in the System updates.** The build takes the +game's data from **your own ROM**, on your PC, and the result is yours alone: copy it to your own SD card, never +publish it (not in a release, an update, a fork or a download link). + +## What you need + +* A PC with Linux or WSL (Ubuntu), the usual build tools (`build-essential`, `git`, `python3`) and a built + RetroStoneOS tree for the RetroStone2 with SDL2 (`retrostone2_release_defconfig`; `~/rsos/output` or + `RSOS_OUTPUT=`). +* Your Super Mario 64 ROM, **US** version, `.z64` byte order (SHA-1 `9bef1128717f958171a4afac3ed78ee2bb4e86ce`). +* A RetroStoneOS with "Native games" on the console (it brings SDL2). + +## Build + +```sh +ports/sm64/build-sm64.sh ~/baserom.us.z64 ~/sm64-retrostone2 +``` + +It clones sm64-port at a fixed commit into `~/rsos/sm64-work`, applies `retrostone2.patch` and adds +`controller_rsos.c`, builds the asset tools for the PC and the game for the console (`TARGET_LINUX_KMSDRM=1`, +the RetroStoneOS cross compiler), and writes: + +``` +~/sm64-retrostone2/roms/native/Super Mario 64.sh +~/sm64-retrostone2/roms/native/sm64/sm64.us +``` + +## Install + +Put the console's SD card in the PC and copy the `roms` folder onto the **RETROSTONE** partition (it merges with +the `roms` folder already there), or use the menu's USB import. In the menu: **Native games > Super Mario 64**. + +## Controls + +The buttons are **the same as the N64 emulator** (parallel_n64), so Mario plays the same natively and emulated, +and the N64 button settings are global: Mario follows the remap you make for N64 in the emulator's in-game menu +(`rsos/remaps/n64.ini` on the card; `n64-cz.ini` when "C/Z buttons fitted" is on) and the N64 core option +**Independent C-button Controls**. + +| RetroStone2 | N64 (default) | N64 (Independent C-button Controls on) | +|---|---|---| +| B (bottom) | A | A | +| Y (left) | B | B | +| L2 | Z | Z | +| L1 / R1 | L / R | C-left / C-right | +| A (right) / X (top) | (with R2: C-right / C-up) | C-down / C-up | +| R2 | hold: B, Y, A, X become C-down, C-left, C-right, C-up | R | +| Select | | L | +| Start | Start | Start | +| Analog stick (Pro), or the D-pad | the stick | the stick | + +For Super Mario 64, **Independent C-button Controls** is the comfortable one (L1/R1 turn the camera, R2 is R): +in any N64 game, open the in-game menu, Core options, turn it on and choose **Save for all games**; both the +emulator and native Mario use it from then on. ("Save for this game" keeps it to that one ROM only.) Mario reads +these settings when it starts. + +**Select + Start** goes back to the menu at once, as in the emulators (the save file is written whenever the game +saves). A USB pad works too (SDL: bottom = A, left = B, the +shoulders = Z/R, the right stick = C buttons). + +Saves (`sm64_save_file.bin`) and settings (`sm64config.txt`) stay in `roms/native/sm64/`. The game saves on its +own (each star); quitting or the power key never loses a saved star. + +## What the patch changes in sm64-port + +* `TARGET_LINUX_KMSDRM=1`: SDL2 on KMS/DRM with a GLES2 context (`#version 100` shaders with + `precision mediump float`), ALSA, no X11/PulseAudio/Wii U adapter, no `-march=native`; `CROSS_PC` (the + compiler of the game only, the asset tools keep the PC's) and `SDL2_CONFIG`. +* `controller_rsos.c`: the console's three input devices (buttons, shoulder buttons, analog stick) read + together from `/dev/input`; `controller_sdl` leaves them alone and keeps USB pads. +* `tools/armips.cpp`: `` for newer host compilers. + +## Status + +**Works on a RetroStone2 Pro**: full speed, it looks great. The analog stick moves Mario on the Pro; the D-pad +moves him whenever the stick is centred or not fitted. If something goes wrong, the game's messages are in +`rsos/logs/boot/game.log` on the SD card. diff --git a/ports/sm64/Super Mario 64.sh b/ports/sm64/Super Mario 64.sh new file mode 100644 index 0000000..96068ce --- /dev/null +++ b/ports/sm64/Super Mario 64.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Super Mario 64 (sm64-port, built from your own ROM by ports/sm64/build-sm64.sh). +# Saves (sm64_save_file.bin) and settings (sm64config.txt) stay in sm64/. +cd sm64 || exit 1 +if [ -x ./sm64.us ]; then + exec ./sm64.us +fi +exec /lib/ld-linux-armhf.so.3 ./sm64.us diff --git a/ports/sm64/build-sm64.sh b/ports/sm64/build-sm64.sh new file mode 100755 index 0000000..64deba0 --- /dev/null +++ b/ports/sm64/build-sm64.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Builds Super Mario 64 (sm64-port) for RetroStoneOS from YOUR OWN ROM, on the +# PC (Linux/WSL), as a native game for the SD card (docs/native-games.md). +# +# ports/sm64/build-sm64.sh [output folder] +# +# Needs a built RetroStoneOS tree with SDL2 (the cross compiler and the +# staging libraries): ~/rsos/output, or RSOS_OUTPUT=. The sm64-port +# source is cloned at a fixed commit into ~/rsos/sm64-work (RSOS_SM64_WORK) +# and patched (retrostone2.patch, controller_rsos.c). +# +# The result contains the game's data, taken from your ROM: keep it for +# yourself. Never put it in a release, an update or a repository. +# +# Output (default ./sm64-retrostone2): roms/native/Super Mario 64.sh and +# roms/native/sm64/sm64.us. Copy the "roms" folder onto the SD card's +# RETROSTONE partition (it merges with the roms folder there). +set -euo pipefail + +SM64_REPO=https://github.com/sm64-port/sm64-port.git +SM64_COMMIT=2b17d08 # "Fix GCC 14 build on MinGW" +US_SHA1=9bef1128717f958171a4afac3ed78ee2bb4e86ce + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/sm64-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_SM64_WORK:-$HOME/rsos/sm64-work} + +die() { echo "build-sm64: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +sum=$(sha1sum "$rom" | cut -d' ' -f1) +[ "$sum" = "$US_SHA1" ] || die "$rom is not the US Super Mario 64 ROM in .z64 (big-endian) order (SHA-1 $sum)" +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +SDL2_CONFIG=$O/staging/usr/bin/sdl2-config +[ -x "$SDL2_CONFIG" ] || die "no SDL2 in $O/staging (build RetroStoneOS with BR2_PACKAGE_SDL2)" + +if [ ! -d "$W/.git" ]; then + git clone -q "$SM64_REPO" "$W" +fi +cd "$W" +git fetch -q origin 2> /dev/null || true +# a clean tree at the fixed commit, then our changes (the extracted assets +# and the build folder stay: they are ignored by git) +git checkout -q -f "$SM64_COMMIT" +git clean -q -f -- src tools Makefile +git apply "$here/retrostone2.patch" +cp "$here/controller_rsos.c" "$here/controller_rsos.h" src/pc/controller/ +cp "$rom" baserom.us.z64 + +# the asset tools for this PC (with its own compiler), then the game for the +# console (CROSS_PC: the game's compiler only) +make -C tools -j"$(nproc)" all-except-recomp +make -j"$(nproc)" VERSION=us TARGET_LINUX_KMSDRM=1 CROSS_PC="$O/host/bin/arm-linux-" SDL2_CONFIG="$SDL2_CONFIG" + +file build/us_pc/sm64.us | grep -q 'ARM' || die "build/us_pc/sm64.us is not an ARM program" +mkdir -p "$out/roms/native/sm64" +"$O/host/bin/arm-linux-strip" -o "$out/roms/native/sm64/sm64.us" build/us_pc/sm64.us +install -m 0644 "$here/Super Mario 64.sh" "$out/roms/native/Super Mario 64.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > Super Mario 64. Select + Start quits." diff --git a/ports/sm64/controller_rsos.c b/ports/sm64/controller_rsos.c new file mode 100644 index 0000000..d65c038 --- /dev/null +++ b/ports/sm64/controller_rsos.c @@ -0,0 +1,411 @@ +/* + * controller_rsos.c - the RetroStone2's built-in controls for sm64-port + * (RetroStoneOS native games, ports/sm64). + * + * The console's controls are three evdev devices: "RetroStone2 Buttons" + * (face buttons, D-pad, Start/Select), "RetroStone2 Shoulder Buttons" (L1, + * R1, L2, R2) and the optional analog stick ("analog-stick"). They are read + * here together, straight from /dev/input; controller_sdl leaves them + * alone (it would see three separate pads) and keeps USB pads. + * + * The buttons follow the N64 emulator (parallel_n64), so Mario plays the + * same natively and emulated, and the N64 button settings are global: + * 1. physical button -> RetroPad, as the menu's input layer does + * (bottom = B, right = A, top = X, left = Y, C/Z = L3/R3); + * 2. the N64 remap made in the emulator's in-game menu, in the same files + * and order (/data/rsos/remaps/n64[-cz].ini, then the shipped one in + * /usr/share/rsos/remaps; -cz first when "C/Z buttons fitted" is on); + * 3. RetroPad -> N64 as parallel_n64 maps it, following its core option + * "Independent C-button Controls" (parallel-n64-alt-map, from + * /data/rsos/coreopts/parallel_n64.ini or the shipped defaults): + * off (default): B = A, Y = B, L2 = Z, L = L, R = R, Start; + * R2 held: A = C-right, Y = C-left, B = C-down, + * X = C-up (the core's "C buttons mode") + * on: B = A, Y = B, L2 = Z, R2 = R, Select = L, Start, + * R = C-right, L = C-left, A = C-down, X = C-up + * The analog stick (Pro) moves Mario; the D-pad does too while the stick is + * centred or not fitted (Super Mario 64 has no use for the N64 D-pad). + * Select+Start stays the menu's: held, it quits the game. + */ +#if defined(__linux__) && defined(TARGET_LINUX_KMSDRM) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "controller_api.h" +#include "controller_rsos.h" + +#define MAX_DEVS 8 +#define STICK_DEADZONE 0.15f /* of the half range */ +#define DATA "/data/rsos" +#define SHIP "/usr/share/rsos" + +/* RetroPad ids, in the menu's order (input.h enum input_btn) */ +enum { RP_B, RP_Y, RP_SELECT, RP_START, RP_UP, RP_DOWN, RP_LEFT, RP_RIGHT, + RP_A, RP_X, RP_L, RP_R, RP_L2, RP_R2, RP_L3, RP_R3, RP_NUM }; + +static const char *const rp_names[RP_NUM] = { + "b", "y", "select", "start", "up", "down", "left", "right", + "a", "x", "l", "r", "l2", "r2", "l3", "r3", +}; + +/* the built-in buttons, as the menu's input layer maps them */ +static const struct { int code, rp; } builtin_keys[] = { + { BTN_SOUTH, RP_B }, { BTN_EAST, RP_A }, { BTN_NORTH, RP_X }, { BTN_WEST, RP_Y }, + { BTN_TL, RP_L }, { BTN_TR, RP_R }, { BTN_TL2, RP_L2 }, { BTN_TR2, RP_R2 }, + { BTN_SELECT, RP_SELECT }, { BTN_START, RP_START }, { BTN_C, RP_L3 }, { BTN_Z, RP_R3 }, + { BTN_DPAD_UP, RP_UP }, { BTN_DPAD_DOWN, RP_DOWN }, { BTN_DPAD_LEFT, RP_LEFT }, + { BTN_DPAD_RIGHT, RP_RIGHT }, +}; + +struct rsos_dev { + int fd; + bool has_stick; + struct input_absinfo ax, ay; +}; + +static struct rsos_dev devs[MAX_DEVS]; +static int ndevs; +static bool keys[KEY_MAX + 1]; +static int abs_x, abs_y; +static unsigned char remap[RP_NUM]; /* physical RetroPad id -> RetroPad id, 0xff = none */ +static bool alt_map; + +/* ------------------------------------------------------------- ini files */ + +static char *trim(char *s) { + char *e; + + while (isspace((unsigned char)*s)) { + s++; + } + e = s + strlen(s); + while (e > s && isspace((unsigned char)e[-1])) { + *--e = 0; + } + return s; +} + +/* calls fn(section, key, value, user) for every key = value line; false if + * the file cannot be read */ +static bool ini_each(const char *path, void (*fn)(const char *, const char *, const char *, void *), + void *user) { + FILE *f = fopen(path, "r"); + char line[512], section[64] = ""; + + if (f == NULL) { + return false; + } + while (fgets(line, sizeof(line), f) != NULL) { + char *c, *k, *v, *eq; + + if ((c = strchr(line, ';')) != NULL) *c = 0; + if ((c = strchr(line, '#')) != NULL) *c = 0; + k = trim(line); + if (k[0] == '[') { + snprintf(section, sizeof(section), "%s", k); + continue; + } + if ((eq = strchr(k, '=')) == NULL) { + continue; + } + *eq = 0; + v = trim(eq + 1); + k = trim(k); + if (v[0] == '"' && strlen(v) >= 2 && v[strlen(v) - 1] == '"') { + v[strlen(v) - 1] = 0; + v++; + } + fn(section, k, v, user); + } + fclose(f); + return true; +} + +static int rp_id(const char *name) { + for (int i = 0; i < RP_NUM; i++) { + if (!strcasecmp(name, rp_names[i])) { + return i; + } + } + if (!strcasecmp(name, "l1")) return RP_L; + if (!strcasecmp(name, "r1")) return RP_R; + return -1; +} + +static void remap_line(const char *section, const char *k, const char *v, void *user) { + int from = rp_id(k), to = rp_id(v); + + (void)user; + if (strcmp(section, "[remap]") != 0 && section[0] != 0) { + return; /* [options]: dpad_to_analog is always on here */ + } + if (from >= 0 && (to >= 0 || !strcasecmp(v, "none"))) { + remap[from] = to >= 0 ? (unsigned char)to : 0xff; + } +} + +static void flag_line(const char *section, const char *k, const char *v, void *user) { + const char **want = user; + + (void)section; + if (!strcmp(k, want[0])) { + want[1] = (!strcmp(v, "1") || !strcasecmp(v, "true") || !strcasecmp(v, "yes") || + !strcasecmp(v, "on") || !strcasecmp(v, "enabled")) ? "1" : "0"; + } +} + +/* "1"/"0" for key in the first file that has it, NULL if none does */ +static const char *ini_flag(const char *key, const char *const *paths, int n) { + static const char *want[2]; + + for (int i = 0; i < n; i++) { + want[0] = key; + want[1] = NULL; + if (ini_each(paths[i], flag_line, want) && want[1] != NULL) { + return want[1]; + } + } + return NULL; +} + +/* the emulator's N64 button settings (see the top of this file) */ +static void load_n64_settings(void) { + static const char *const settings[] = { DATA "/settings.ini" }; + static const char *const coreopts[] = { DATA "/coreopts/parallel_n64.ini", SHIP "/coreopts/parallel_n64.ini" }; + const char *cz = ini_flag("cz_buttons", settings, 1); + const char *am = ini_flag("parallel-n64-alt-map", coreopts, 2); + char cand[4][128]; + int n = 0; + + for (int i = 0; i < RP_NUM; i++) { + remap[i] = (unsigned char)i; + } + if (cz != NULL && cz[0] == '1') { + snprintf(cand[n++], sizeof(cand[0]), DATA "/remaps/n64-cz.ini"); + } + snprintf(cand[n++], sizeof(cand[0]), DATA "/remaps/n64.ini"); + if (cz != NULL && cz[0] == '1') { + snprintf(cand[n++], sizeof(cand[0]), SHIP "/remaps/n64-cz.ini"); + } + snprintf(cand[n++], sizeof(cand[0]), SHIP "/remaps/n64.ini"); + for (int i = 0; i < n; i++) { + if (ini_each(cand[i], remap_line, NULL)) { + fprintf(stderr, "controller_rsos: N64 remap %s\n", cand[i]); + break; + } + } + alt_map = am != NULL && am[0] == '1'; + fprintf(stderr, "controller_rsos: independent C buttons %s\n", alt_map ? "on" : "off"); +} + +/* ------------------------------------------------------------- devices */ + +static void controller_rsos_init(void) { + DIR *d = opendir("/dev/input"); + struct dirent *de; + + load_n64_settings(); + if (d == NULL) { + return; + } + while ((de = readdir(d)) != NULL && ndevs < MAX_DEVS) { + char path[300], name[128] = ""; + unsigned long absbits[(ABS_MAX + 8 * sizeof(long)) / (8 * sizeof(long))] = { 0 }; + int fd; + + if (strncmp(de->d_name, "event", 5) != 0) { + continue; + } + snprintf(path, sizeof(path), "/dev/input/%s", de->d_name); + fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) { + continue; + } + /* the buttons are "RetroStone2 ...", the stick (adc-joystick) is + * named after its device tree node, as in the menu's board.ini */ + if (ioctl(fd, EVIOCGNAME(sizeof(name)), name) < 0 || + (strcmp(name, "analog-stick") != 0 && + (strncmp(name, "RetroStone2", 11) != 0 || strstr(name, "Brightness") != NULL))) { + close(fd); + continue; + } + devs[ndevs].fd = fd; + ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absbits)), absbits); + if ((absbits[ABS_X / (8 * sizeof(long))] >> (ABS_X % (8 * sizeof(long)))) & 1) { + devs[ndevs].has_stick = ioctl(fd, EVIOCGABS(ABS_X), &devs[ndevs].ax) == 0 && + ioctl(fd, EVIOCGABS(ABS_Y), &devs[ndevs].ay) == 0 && + devs[ndevs].ax.maximum != devs[ndevs].ax.minimum && + devs[ndevs].ay.maximum != devs[ndevs].ay.minimum; + if (devs[ndevs].has_stick) { + abs_x = devs[ndevs].ax.value; + abs_y = devs[ndevs].ay.value; + } + } + fprintf(stderr, "controller_rsos: %s (%s)%s\n", path, name, + devs[ndevs].has_stick ? ", analog stick" : ""); + ndevs++; + } + closedir(d); +} + +static bool axis_in_range(int v, const struct input_absinfo *a, bool near_centre) { + int lo = a->minimum < a->maximum ? a->minimum : a->maximum; + int hi = a->minimum < a->maximum ? a->maximum : a->minimum; + + if (near_centre) { + return abs(v - (lo + (hi - lo) / 2)) <= (hi - lo) / 4; + } + return v >= lo && v <= hi; +} + +/* + * Fitted or not, as the menu decides (input.c): a reading out of range means + * no stick (the ADC inputs float above it); fitted again only once both axes + * read near the centre (floating inputs can wander into the range near an + * edge and would hold a direction). + */ +static bool stick_fitted(const struct rsos_dev *sd) { + static bool fitted; + + if (!axis_in_range(abs_x, &sd->ax, false) || !axis_in_range(abs_y, &sd->ay, false)) { + if (fitted) { + fprintf(stderr, "controller_rsos: analog stick reads %d, %d: not fitted\n", abs_x, abs_y); + } + fitted = false; + } else if (!fitted && axis_in_range(abs_x, &sd->ax, true) && axis_in_range(abs_y, &sd->ay, true)) { + fprintf(stderr, "controller_rsos: analog stick fitted (%d, %d)\n", abs_x, abs_y); + fitted = true; + } + return fitted; +} + +/* -1..1 from an axis, 0 in the dead zone */ +static float stick_axis(int v, const struct input_absinfo *a) { + int lo = a->minimum < a->maximum ? a->minimum : a->maximum; + int hi = a->minimum < a->maximum ? a->maximum : a->minimum; + float half = (hi - lo) / 2.0f, f; + + if (half <= 0) { + return 0; + } + f = (v - (lo + half)) / half; + if (f > -STICK_DEADZONE && f < STICK_DEADZONE) { + return 0; + } + return f; +} + +static void controller_rsos_read(OSContPad *pad) { + const struct rsos_dev *sd = NULL; + struct input_event ev; + bool rp[RP_NUM] = { false }; + int dx, dy; + + for (int i = 0; i < ndevs; i++) { + while (read(devs[i].fd, &ev, sizeof(ev)) == sizeof(ev)) { + if (ev.type == EV_KEY && ev.code <= KEY_MAX) { + keys[ev.code] = ev.value != 0; + } else if (ev.type == EV_ABS && ev.code == ABS_X) { + abs_x = ev.value; + } else if (ev.type == EV_ABS && ev.code == ABS_Y) { + abs_y = ev.value; + } + } + if (devs[i].has_stick) { + sd = &devs[i]; + } + } + + /* Select + Start quits at once, as in the console's emulators (the + * physical buttons, whatever the remap). The save file is written as + * soon as the game saves, so nothing is lost. */ + { + static bool start_was; + bool start = keys[BTN_START]; + + if (keys[BTN_SELECT] && start && !start_was) { + fprintf(stderr, "controller_rsos: Select+Start: quitting\n"); + _exit(0); + } + start_was = start; + } + + /* physical -> RetroPad -> the N64 remap */ + for (size_t i = 0; i < sizeof(builtin_keys) / sizeof(builtin_keys[0]); i++) { + unsigned char to = remap[builtin_keys[i].rp]; + + if (keys[builtin_keys[i].code] && to != 0xff) { + rp[to] = true; + } + } + + /* RetroPad -> N64, as parallel_n64 */ + if (rp[RP_START]) pad->button |= START_BUTTON; + if (rp[RP_L2]) pad->button |= Z_TRIG; + if (alt_map) { + if (rp[RP_B]) pad->button |= A_BUTTON; + if (rp[RP_Y]) pad->button |= B_BUTTON; + if (rp[RP_R]) pad->button |= R_CBUTTONS; + if (rp[RP_L]) pad->button |= L_CBUTTONS; + if (rp[RP_A]) pad->button |= D_CBUTTONS; + if (rp[RP_X]) pad->button |= U_CBUTTONS; + if (rp[RP_R2]) pad->button |= R_TRIG; + if (rp[RP_SELECT]) pad->button |= L_TRIG; + } else { + if (rp[RP_R]) pad->button |= R_TRIG; + if (rp[RP_L]) pad->button |= L_TRIG; + if (rp[RP_R2]) { + if (rp[RP_A]) pad->button |= R_CBUTTONS; + if (rp[RP_Y]) pad->button |= L_CBUTTONS; + if (rp[RP_B]) pad->button |= D_CBUTTONS; + if (rp[RP_X]) pad->button |= U_CBUTTONS; + } else { + if (rp[RP_B]) pad->button |= A_BUTTON; + if (rp[RP_Y]) pad->button |= B_BUTTON; + } + } + + /* the analog stick, when fitted and moved */ + if (sd != NULL && stick_fitted(sd)) { + float fx = stick_axis(abs_x, &sd->ax), fy = stick_axis(abs_y, &sd->ay); + + if (fx != 0 || fy != 0) { + int sx = (int)(fx * 80), sy = (int)(-fy * 80); + + if (abs(sx) > abs(pad->stick_x) || abs(sy) > abs(pad->stick_y)) { + pad->stick_x = sx; + pad->stick_y = sy; + } + return; + } + } + /* else the D-pad steers (the remapped one, like the emulator's "D-pad + * to analog"): full tilt, 56 on a diagonal (80 / sqrt 2) */ + dx = (rp[RP_RIGHT] ? 1 : 0) - (rp[RP_LEFT] ? 1 : 0); + dy = (rp[RP_UP] ? 1 : 0) - (rp[RP_DOWN] ? 1 : 0); + if (dx != 0 || dy != 0) { + int m = dx != 0 && dy != 0 ? 56 : 80; + + pad->stick_x = dx * m; + pad->stick_y = dy * m; + } +} + +struct ControllerAPI controller_rsos = { + controller_rsos_init, + controller_rsos_read +}; + +#endif diff --git a/ports/sm64/controller_rsos.h b/ports/sm64/controller_rsos.h new file mode 100644 index 0000000..7d33b6d --- /dev/null +++ b/ports/sm64/controller_rsos.h @@ -0,0 +1,8 @@ +#ifndef CONTROLLER_RSOS_H +#define CONTROLLER_RSOS_H + +#include "controller_api.h" + +extern struct ControllerAPI controller_rsos; + +#endif diff --git a/ports/sm64/retrostone2.patch b/ports/sm64/retrostone2.patch new file mode 100644 index 0000000..5a0944f --- /dev/null +++ b/ports/sm64/retrostone2.patch @@ -0,0 +1,254 @@ +diff --git a/Makefile b/Makefile +index cfac1e8..529d7e1 100644 +--- a/Makefile ++++ b/Makefile +@@ -19,6 +19,11 @@ DEFINES := + TARGET_N64 ?= 0 + # Build for Emscripten/WebGL + TARGET_WEB ?= 0 ++# Build for RetroStoneOS (SDL2 KMS/DRM + GLES2 + ALSA, cross-compiled: ++# CROSS_PC=arm-linux- SDL2_CONFIG=) ++TARGET_LINUX_KMSDRM ?= 0 ++CROSS_PC ?= ++SDL2_CONFIG ?= sdl2-config + # Compiler to use (ido or gcc) + + +@@ -314,6 +319,14 @@ include Makefile.split + LEVEL_C_FILES := $(wildcard levels/*/leveldata.c) $(wildcard levels/*/script.c) $(wildcard levels/*/geo.c) + C_FILES := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.c)) $(LEVEL_C_FILES) + CXX_FILES := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.cpp)) ++ifeq ($(TARGET_LINUX_KMSDRM),1) ++ # RetroStoneOS (RetroStone2): SDL2 on KMS/DRM with GLES2, ALSA; no X11, ++ # PulseAudio or Wii U adapter ++ C_FILES := $(filter-out src/pc/audio/audio_pulse.c src/pc/controller/controller_wup.c \ ++ src/pc/controller/wup.c src/pc/gfx/gfx_glx.c,$(C_FILES)) ++ CXX_FILES := $(filter-out src/pc/gfx/gfx_direct3d11.cpp src/pc/gfx/gfx_direct3d12.cpp \ ++ src/pc/gfx/gfx_direct3d_common.cpp src/pc/gfx/gfx_dxgi.cpp src/pc/audio/audio_wasapi.cpp,$(CXX_FILES)) ++endif + S_FILES := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.s)) + ULTRA_C_FILES := $(foreach dir,$(ULTRA_SRC_DIRS),$(wildcard $(dir)/*.c)) + GODDARD_C_FILES := $(foreach dir,$(GODDARD_SRC_DIRS),$(wildcard $(dir)/*.c)) +@@ -458,8 +471,8 @@ else # TARGET_N64 + + AS := as + ifneq ($(TARGET_WEB),1) +- CC := gcc +- CXX := g++ ++ CC := $(CROSS_PC)gcc ++ CXX := $(CROSS_PC)g++ + else + CC := emcc + endif +@@ -477,7 +490,10 @@ ifeq ($(TARGET_WINDOWS),1) + PLATFORM_CFLAGS := -DTARGET_WINDOWS + PLATFORM_LDFLAGS := -lm -lxinput9_1_0 -lole32 -no-pie -mwindows + endif +-ifeq ($(TARGET_LINUX),1) ++ifeq ($(TARGET_LINUX_KMSDRM),1) ++ PLATFORM_CFLAGS := -DTARGET_LINUX -DTARGET_LINUX_KMSDRM ++ PLATFORM_LDFLAGS := -lm -lpthread -lasound ++else ifeq ($(TARGET_LINUX),1) + PLATFORM_CFLAGS := -DTARGET_LINUX `pkg-config --cflags libusb-1.0` + PLATFORM_LDFLAGS := -lm -lpthread `pkg-config --libs libusb-1.0` -lasound -lpulse -no-pie + endif +@@ -496,7 +512,10 @@ ifeq ($(ENABLE_OPENGL),1) + GFX_CFLAGS += $(shell sdl2-config --cflags) -DGLEW_STATIC + GFX_LDFLAGS += $(shell sdl2-config --libs) -lglew32 -lopengl32 -lwinmm -limm32 -lversion -loleaut32 -lsetupapi + endif +- ifeq ($(TARGET_LINUX),1) ++ ifeq ($(TARGET_LINUX_KMSDRM),1) ++ GFX_CFLAGS += $(shell $(SDL2_CONFIG) --cflags) ++ GFX_LDFLAGS += $(shell $(SDL2_CONFIG) --libs) -lEGL -lGLESv2 ++ else ifeq ($(TARGET_LINUX),1) + GFX_CFLAGS += $(shell sdl2-config --cflags) + GFX_LDFLAGS += -lGL $(shell sdl2-config --libs) -lX11 -lXrandr + endif +@@ -517,7 +536,12 @@ endif + GFX_CFLAGS += -DWIDESCREEN + + CC_CHECK := $(CC) -fsyntax-only -fsigned-char -Wall -Wextra -Wno-format-security -D_LANGUAGE_C $(DEF_INC_CFLAGS) $(PLATFORM_CFLAGS) $(GFX_CFLAGS) ++ifeq ($(TARGET_LINUX_KMSDRM),1) ++# the cross compiler's own CPU flags (Cortex-A7), not the build machine's ++CFLAGS := $(OPT_FLAGS) -D_LANGUAGE_C $(DEF_INC_CFLAGS) $(PLATFORM_CFLAGS) $(GFX_CFLAGS) -fno-strict-aliasing -fwrapv ++else + CFLAGS := $(OPT_FLAGS) -D_LANGUAGE_C $(DEF_INC_CFLAGS) $(PLATFORM_CFLAGS) $(GFX_CFLAGS) -fno-strict-aliasing -fwrapv -march=native ++endif + + ASFLAGS := -I include -I $(BUILD_DIR) $(foreach d,$(DEFINES),--defsym $(d)) + +diff --git a/src/game/memory.c b/src/game/memory.c +index 36ceff3..9b91338 100644 +--- a/src/game/memory.c ++++ b/src/game/memory.c +@@ -518,7 +518,9 @@ void alloc_only_pool_clear(struct AllocOnlyPool *pool) { + + void *alloc_only_pool_alloc(struct AllocOnlyPool *pool, s32 size) { + u8 *addr; +- u32 s = size; ++ /* every block 8-byte aligned, like the block header above: on 32-bit ++ * ARM a float store to a misaligned address is a fatal SIGBUS */ ++ u32 s = ALIGN8(size); + if (pool->lastBlockSize - pool->lastBlockNextPos < s) { + struct AllocOnlyPoolBlock *block; + u32 nextSize = pool->lastBlockSize * 2; +diff --git a/src/pc/audio/audio_pulse.h b/src/pc/audio/audio_pulse.h +index c4d7e4b..7fbae7a 100644 +--- a/src/pc/audio/audio_pulse.h ++++ b/src/pc/audio/audio_pulse.h +@@ -3,7 +3,7 @@ + + #include "../compat.h" + +-#if defined(__linux__) || defined(__BSD__) ++#if (defined(__linux__) || defined(__BSD__)) && !defined(TARGET_LINUX_KMSDRM) + extern struct AudioAPI audio_pulse; + #define HAVE_PULSE_AUDIO 1 + #else +diff --git a/src/pc/controller/controller_entry_point.c b/src/pc/controller/controller_entry_point.c +index f86cf0d..7de45c1 100644 +--- a/src/pc/controller/controller_entry_point.c ++++ b/src/pc/controller/controller_entry_point.c +@@ -12,9 +12,12 @@ + #include "controller_sdl.h" + #endif + +-#ifdef __linux__ ++#if defined(__linux__) && !defined(TARGET_LINUX_KMSDRM) + #include "controller_wup.h" + #endif ++#ifdef TARGET_LINUX_KMSDRM ++#include "controller_rsos.h" ++#endif + + static struct ControllerAPI *controller_implementations[] = { + &controller_recorded_tas, +@@ -23,8 +26,11 @@ static struct ControllerAPI *controller_implementations[] = { + #else + &controller_sdl, + #endif +-#ifdef __linux__ ++#if defined(__linux__) && !defined(TARGET_LINUX_KMSDRM) + &controller_wup, ++#endif ++#ifdef TARGET_LINUX_KMSDRM ++ &controller_rsos, + #endif + &controller_keyboard, + }; +diff --git a/src/pc/controller/controller_sdl.c b/src/pc/controller/controller_sdl.c +index f6880e7..1674531 100644 +--- a/src/pc/controller/controller_sdl.c ++++ b/src/pc/controller/controller_sdl.c +@@ -4,6 +4,7 @@ + #include + #include + #include ++#include + + #include + +@@ -38,6 +39,13 @@ static void controller_sdl_read(OSContPad *pad) { + } + if (sdl_cntrl == NULL) { + for (int i = 0; i < SDL_NumJoysticks(); i++) { ++#ifdef TARGET_LINUX_KMSDRM ++ /* the built-in controls: controller_rsos reads them together */ ++ if (SDL_JoystickNameForIndex(i) != NULL && ++ strncmp(SDL_JoystickNameForIndex(i), "RetroStone2", 11) == 0) { ++ continue; ++ } ++#endif + if (SDL_IsGameController(i)) { + sdl_cntrl = SDL_GameControllerOpen(i); + if (sdl_cntrl != NULL) { +diff --git a/src/pc/gfx/gfx_glx.c b/src/pc/gfx/gfx_glx.c +index 9d2743e..182b682 100644 +--- a/src/pc/gfx/gfx_glx.c ++++ b/src/pc/gfx/gfx_glx.c +@@ -1,6 +1,6 @@ + #include "../compat.h" + +-#if defined(__linux__) || defined(__BSD__) ++#if (defined(__linux__) && !defined(TARGET_LINUX_KMSDRM)) || defined(__BSD__) + #include + #include + #include +diff --git a/src/pc/gfx/gfx_opengl.c b/src/pc/gfx/gfx_opengl.c +index 7bc9745..8d40dfb 100644 +--- a/src/pc/gfx/gfx_opengl.c ++++ b/src/pc/gfx/gfx_opengl.c +@@ -175,7 +175,11 @@ static struct ShaderProgram *gfx_opengl_create_and_load_new_shader(uint32_t shad + size_t num_floats = 4; + + // Vertex shader ++#ifdef TARGET_LINUX_KMSDRM ++ append_line(vs_buf, &vs_len, "#version 100"); ++#else + append_line(vs_buf, &vs_len, "#version 110"); ++#endif + append_line(vs_buf, &vs_len, "attribute vec4 aVtxPos;"); + if (cc_features.used_textures[0] || cc_features.used_textures[1]) { + append_line(vs_buf, &vs_len, "attribute vec2 aTexCoord;"); +@@ -206,8 +210,12 @@ static struct ShaderProgram *gfx_opengl_create_and_load_new_shader(uint32_t shad + append_line(vs_buf, &vs_len, "}"); + + // Fragment shader ++#ifdef TARGET_LINUX_KMSDRM ++ append_line(fs_buf, &fs_len, "#version 100"); ++ append_line(fs_buf, &fs_len, "precision mediump float;"); ++#else + append_line(fs_buf, &fs_len, "#version 110"); +- //append_line(fs_buf, &fs_len, "precision mediump float;"); ++#endif + if (cc_features.used_textures[0] || cc_features.used_textures[1]) { + append_line(fs_buf, &fs_len, "varying vec2 vTexCoord;"); + } +diff --git a/src/pc/gfx/gfx_sdl2.c b/src/pc/gfx/gfx_sdl2.c +index 4fd5c43..9b2a1a2 100644 +--- a/src/pc/gfx/gfx_sdl2.c ++++ b/src/pc/gfx/gfx_sdl2.c +@@ -1,6 +1,6 @@ + #include "../compat.h" + +-#if !defined(__linux__) && !defined(__BSD__) && defined(ENABLE_OPENGL) ++#if ((!defined(__linux__) && !defined(__BSD__)) || defined(TARGET_LINUX_KMSDRM)) && defined(ENABLE_OPENGL) + + #ifdef __MINGW32__ + #define FOR_WINDOWS 1 +@@ -155,6 +155,13 @@ int test_vsync(void) { + static void gfx_sdl_init(const char *game_name, bool start_in_fullscreen) { + SDL_Init(SDL_INIT_VIDEO); + ++#ifdef TARGET_LINUX_KMSDRM ++ /* GLES2 on the Mali-400 (Mesa lima), the screen's own mode, no cursor */ ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); ++ SDL_ShowCursor(SDL_DISABLE); ++#endif + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + +diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c +index 89aa1fa..380f27c 100644 +--- a/src/pc/pc_main.c ++++ b/src/pc/pc_main.c +@@ -166,7 +166,7 @@ void main_func(void) { + wm_api = &gfx_dxgi_api; + #elif defined(ENABLE_OPENGL) + rendering_api = &gfx_opengl_api; +- #if defined(__linux__) || defined(__BSD__) ++ #if (defined(__linux__) && !defined(TARGET_LINUX_KMSDRM)) || defined(__BSD__) + wm_api = &gfx_glx; + #else + wm_api = &gfx_sdl; +diff --git a/tools/armips.cpp b/tools/armips.cpp +index 2e26cf9..ef434a8 100644 +--- a/tools/armips.cpp ++++ b/tools/armips.cpp +@@ -1,3 +1,4 @@ ++#include + // armips assembler v0.11 + // https://github.com/Kingcom/armips/ + // To simplify compilation, all files have been concatenated into one. diff --git a/ports/soh/.gitattributes b/ports/soh/.gitattributes new file mode 100644 index 0000000..589b54d --- /dev/null +++ b/ports/soh/.gitattributes @@ -0,0 +1,2 @@ +# the patches carry CRLF lines of their targets (libultraship shaders): byte for byte +*.patch -text diff --git a/ports/soh/Ocarina of Time.sh b/ports/soh/Ocarina of Time.sh new file mode 100644 index 0000000..753b79e --- /dev/null +++ b/ports/soh/Ocarina of Time.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# The Legend of Zelda: Ocarina of Time (Ship of Harkinian), built from your own ROM by +# ports/soh/build-soh.sh. The game data (oot.o2r, soh.o2r), settings and saves stay in soh/. +cd soh || exit 1 +export SHIP_HOME="$PWD" +# Opus, OpusFile and SDL2_net, copied next to the game for a RetroStoneOS without them +export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +# The built-in controls as one standard gamepad (rsos-gamepad); SDL then +# ignores the raw button devices. It runs in the +# game's process group: the menu stops it with the game. +if [ -x /usr/bin/rsos-gamepad ]; then + SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" + export SDL_GAMECONTROLLERCONFIG + export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 + /usr/bin/rsos-gamepad & + usleep 300000 +fi +if [ -x ./soh.elf ]; then + exec ./soh.elf +fi +exec /lib/ld-linux-armhf.so.3 ./soh.elf diff --git a/ports/soh/README.md b/ports/soh/README.md new file mode 100644 index 0000000..60b70a9 --- /dev/null +++ b/ports/soh/README.md @@ -0,0 +1,83 @@ +# The Legend of Zelda: Ocarina of Time (Ship of Harkinian) on the RetroStone2 + +[Ship of Harkinian](https://github.com/HarbourMasters/Shipwright) is the PC port of the Ocarina of Time +decompilation, built on HarbourMasters' [libultraship](https://github.com/Kenix3/libultraship), like Star Fox 64 +([../starship](../starship/README.md)). This folder adds the same OpenGL ES 2.0 mode to Ship of Harkinian 9.3.1's +libultraship and builds the game for the console as a native game (docs/native-games.md). + +**Nothing from the game is in this repository, the OS images or the updates.** The game's data comes from **your +own ROM** on your PC and stays yours. Tested with the N64 NTSC 1.2 ROM; the other versions Ship of Harkinian 9.3.1 +supports should work the same. `.z64`, `.v64` and `.n64` byte orders are all fine. + +## Build + +```sh +ports/soh/build-soh.sh ~/"Ocarina of Time (U) (V1.2).z64" ~/my-ports +``` + +It clones Ship of Harkinian at 9.3.1 into `~/rsos/soh-work`, applies `soh.patch` and `libultraship-gles2.patch`, +builds the asset tools (Torch) for the PC and makes `oot.o2r` from the ROM and `soh.o2r` (which carries the +patched shaders), builds the game for the console (`USE_GLES2`, the RetroStoneOS cross compiler), then writes: + +``` +~/my-ports/roms/native/Ocarina of Time.sh +~/my-ports/roms/native/soh/ soh.elf, oot.o2r, soh.o2r, gamecontrollerdb.txt, lib/, assets/ +``` + +Copy the `roms` folder onto the SD card's **RETROSTONE** partition; in the menu: **Native games > Ocarina of +Time**. Settings and saves stay in `roms/native/soh/` (`SHIP_HOME`). Hold **Select + Start** for a second to go +back to the menu. + +The console needs a RetroStoneOS with Native games and `rsos-gamepad`. Opus, OpusFile and SDL2_net are in the +RetroStone2 configs too; `build-soh.sh` also copies them into `soh/lib/` for a RetroStoneOS that doesn't have them. + +## Controls + +`rsos-gamepad` makes the built-in controls one SDL gamepad, by position. By the labels on the console: + +| RetroStone2 | N64 | +|---|---| +| B (bottom) / A (right) | A / B | +| X (top) / Y (left) / R1 | C-down / C-left / C-right | +| L2 / R2 | Z / R | +| L1 | L | +| Start | Start | +| Analog stick (Pro) | the stick | +| D-pad | the D-pad | + +The console has no right stick, so the C buttons go on X, Y and R1 (the default with `USE_GLES2`). C-up is free: +set it in Ship of Harkinian's own controller settings if you want it. Without an analog stick, map the stick's +directions to the D-pad there too (only tried on a Pro, which has the stick). + +## What the patches change + +**The GLES2 mode** (as for Starship): an OpenGL ES 2.0 context, GLSL ES 1.00 shaders, unsized texture formats, +packed depth+stencil or 16-bit depth, no multisampling or vertex array objects, framebuffer copies through +`glCopyTexSubImage2D`, depth reads reported as "far", and shader errors printed before aborting. + +**32-bit ARM and the Mali-400**, found on the console: +* Textures whose data lives below `0x10000000` were taken for unresolved N64 segment addresses and dropped. That + is always the heap on 32-bit ARM, so menus, titles and the HUD showed leftover pictures. Textures from the + game's archives are always valid now, and on 32-bit any mapped address is. +* In 2-cycle mode the shader samples a texture slot the combiner leaves unsized: the slot gets the used one's size. +* Draws with a primitive depth (the pause screen, parts of the HUD) wrote `gl_FragDepth`, which GLSL ES 1.00 + doesn't have: the depth goes into the vertices instead. +* Mesa hands a deleted texture's name out again at once; the texture binding cache forgets it. +* `mixer.c`'s NEON code uses AArch64-only intrinsics: their ARMv7 equivalents. +* `audio_load.c` cleared 8 bytes of a 4-byte array (glibc's `_FORTIFY_SOURCE` stopped the game). +* Torch's command line reads the ROM into memory first (9.3.1's `Extract()` signature). + +**Speed** (measured with a CPU profile on the console, where the game's main thread is the limit): vertices go to +the driver as client-side arrays instead of a new buffer object per draw (lima made one each time); assets named +by hash or by a path string are kept once resolved instead of going through the resource manager on every draw +(not when the archives hold alternate assets, which are reloaded as new objects); the texture address check uses +`mincore` instead of `dladdr`. + +**Other**: a failed sound start no longer saves "null" as the audio backend for every later start; the crash +handler ends the game with `_exit()`, so a crash returns to the menu at once. + +## Status + +**Works on a RetroStone2 Pro**: title screen, menus, 3D, sound, saves and the pause screen, with a little slowdown +in busy places. The game's messages are in `rsos/logs/boot/game.log` on the SD card, and Ship of Harkinian's +own log in `roms/native/soh/logs/`. diff --git a/ports/soh/build-soh.sh b/ports/soh/build-soh.sh new file mode 100755 index 0000000..f578195 --- /dev/null +++ b/ports/soh/build-soh.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Builds The Legend of Zelda: Ocarina of Time (Ship of Harkinian) for RetroStoneOS +# from YOUR OWN ROM, on the PC (Linux/WSL), as a native game for the SD card +# (docs/native-games.md). +# +# ports/soh/build-soh.sh [output folder] +# +# Needs a built RetroStoneOS tree for the RetroStone2 with SDL2 and the port +# libraries (the RetroStone2 configs): ~/rsos/output, or RSOS_OUTPUT=; +# cmake, ninja, python3 and a C++ compiler for the PC (Torch, the asset +# extractor, runs here). Ship of Harkinian is cloned at a fixed release into +# ~/rsos/soh-work (RSOS_SOH_WORK) and patched (soh.patch, +# libultraship-gles2.patch). +# +# The result contains the game's data, taken from your ROM: keep it for +# yourself. Never put it in a release, an update or a repository. +# +# Output (default ./soh-retrostone2): roms/native/Ocarina of Time.sh and +# roms/native/soh/. Copy the "roms" folder onto the SD card's RETROSTONE +# partition (it merges with the roms folder there). +set -euo pipefail + +REPO=https://github.com/HarbourMasters/Shipwright.git +COMMIT=f0599f96f8d442f26d0fa1c5cedafddacd56633f # 9.3.1, libultraship da6daf1 + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/soh-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_SOH_WORK:-$HOME/rsos/soh-work} + +die() { echo "build-soh: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +# libopus, libopusfile and libSDL2_net are also copied next to the game, for consoles +# whose RetroStoneOS does not have them yet +LIBS="libSDL2.so libzip.so libtinyxml2.so libspdlog.so libvorbisfile.so libopus.so.0 libopusfile.so.0 libSDL2_net-2.0.so.0" +for lib in $LIBS; do + [ -e "$O/staging/usr/lib/$lib" ] || die "$lib is not in $O/staging (build RetroStoneOS with the RetroStone2 configs)" +done +TOOLCHAIN=$O/host/share/buildroot/toolchainfile.cmake + +# only the fixed commit (the whole history is large) +if [ ! -d "$W/.git" ]; then + git init -q "$W" + git -C "$W" remote add origin "$REPO" +fi +cd "$W" +git fetch -q --depth 1 origin "$COMMIT" +# a clean tree at the fixed commit, then our changes (the build folders and the +# ROM are ignored by git and stay) +git checkout -q -f "$COMMIT" +git submodule update -q --init --recursive --force --depth 1 +git -C libultraship checkout -q -f . +git apply "$here/soh.patch" +git -C libultraship apply "$here/libultraship-gles2.patch" + +# the ROM, in .z64 byte order (Torch reads that one only) +mkdir -p rsos-rom +rm -f rsos-rom/* +python3 - "$rom" rsos-rom/oot.z64 << 'EOF' || die "$rom is not an N64 ROM" +import sys +data = bytearray(open(sys.argv[1], 'rb').read()) +head = bytes(data[:4]) +if head == b'\x37\x80\x40\x12': # .v64: 16-bit words swapped + data[0::2], data[1::2] = data[1::2], data[0::2] +elif head == b'\x40\x12\x37\x80': # .n64: 32-bit words reversed + for i in range(0, len(data) - 3, 4): + data[i:i + 4] = data[i:i + 4][::-1] +elif head != b'\x80\x37\x12\x40': + sys.exit(1) +open(sys.argv[2], 'wb').write(data) +EOF + +# Torch for this PC: oot.o2r (or oot-mq.o2r) from the ROM, and soh.o2r, which carries +# libultraship's (patched) shaders +cmake -S . -B build-tools -G Ninja -DSOH_TOOLS_ONLY=ON -DCMAKE_BUILD_TYPE=Release \ + -DSOH_ROM_PATH="$W/rsos-rom" +cmake --build build-tools --target ExtractAssets +cmake --build build-tools --target GenerateSohOtr +ls build-tools/soh/oot*.o2r > /dev/null 2>&1 || + die "no oot.o2r: $rom is not an Ocarina of Time ROM that Ship of Harkinian supports" + +# the game for the console. CMAKE_SYSTEM_NAME Linux: Buildroot's toolchain file names +# it "Buildroot", which skips the Linux parts of SoH and libultraship. SDL2_net's CMake +# file (made by autotools) points at the PC's /usr: a copy pointing at the staging folder. +C=$W/build-gles2-cmake/SDL2_net +mkdir -p "$C" +sed -e "s#^set(exec_prefix \"/usr\")#set(exec_prefix \"$O/staging/usr\")#" \ + -e "s#^get_filename_component(prefix .*#set(prefix \"$O/staging/usr\")#" \ + "$O/staging/usr/lib/cmake/SDL2_net/sdl2_net-config.cmake" > "$C/sdl2_net-config.cmake" +cp "$O/staging/usr/lib/cmake/SDL2_net/sdl2_net-config-version.cmake" "$C/" +cmake -S . -B build-gles2 -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_OPENGLES=ON -DUSE_GLES2=ON \ + -DNON_PORTABLE=OFF -DSDL2_net_DIR="$C" -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DCMAKE_SYSTEM_NAME=Linux +cmake --build build-gles2 --target soh + +file build-gles2/soh/soh.elf | grep -q 'ARM' || die "build-gles2/soh/soh.elf is not an ARM program" +S=$out/roms/native/soh +mkdir -p "$S/lib" "$S/assets" +"$O/host/bin/arm-linux-strip" -o "$S/soh.elf" build-gles2/soh/soh.elf +cp build-tools/soh/oot*.o2r build-tools/soh/soh.o2r "$S/" +[ -f build-gles2/gamecontrollerdb.txt ] && cp build-gles2/gamecontrollerdb.txt "$S/" +for lib in libopus.so.0 libopusfile.so.0 libSDL2_net-2.0.so.0; do + cp -L "$O/staging/usr/lib/$lib" "$S/lib/" + "$O/host/bin/arm-linux-strip" "$S/lib/$lib" +done +# SoH quits at start without an assets/ folder next to the program; it reads it only to +# make oot.o2r on the console, which this script does on the PC instead +printf 'Ship of Harkinian checks that this folder exists. oot.o2r is made on the PC\n(ports/soh/build-soh.sh), so the extraction files are not needed here.\n' \ + > "$S/assets/README.txt" +install -m 0644 "$here/Ocarina of Time.sh" "$out/roms/native/Ocarina of Time.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > Ocarina of Time. Hold Select + Start to quit." diff --git a/ports/soh/libultraship-gles2.patch b/ports/soh/libultraship-gles2.patch new file mode 100644 index 0000000..bb83d9e --- /dev/null +++ b/ports/soh/libultraship-gles2.patch @@ -0,0 +1,786 @@ +diff --git a/cmake/dependencies/linux.cmake b/cmake/dependencies/linux.cmake +index 02301f1..8e231e1 100644 +--- a/cmake/dependencies/linux.cmake ++++ b/cmake/dependencies/linux.cmake +@@ -2,7 +2,10 @@ + find_package(SDL2 REQUIRED) + target_link_libraries(ImGui PUBLIC SDL2::SDL2) + +-if (USE_OPENGLES) ++if (USE_GLES2) ++ target_link_libraries(ImGui PUBLIC GLESv2) ++ add_compile_definitions(IMGUI_IMPL_OPENGL_ES2) ++elseif (USE_OPENGLES) + target_link_libraries(ImGui PUBLIC ${OPENGL_GLESv2_LIBRARY}) + add_compile_definitions(IMGUI_IMPL_OPENGL_ES3) + else() +diff --git a/include/fast/backends/gfx_opengl.h b/include/fast/backends/gfx_opengl.h +index af24c70..efdf5c5 100644 +--- a/include/fast/backends/gfx_opengl.h ++++ b/include/fast/backends/gfx_opengl.h +@@ -16,6 +16,10 @@ + #elif __APPLE__ + #include + #include ++#elif defined(USE_GLES2) ++#include ++#include ++#include + #elif USE_OPENGLES + #include + #include +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 45989b6..0209da4 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -11,6 +11,11 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) + + set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include) + option(USE_OPENGLES "Enable GLES3" OFF) ++# OpenGL ES 2.0 (Mali-400 and other GLES2-only GPUs); implies USE_OPENGLES ++option(USE_GLES2 "Enable GLES2" OFF) ++if (USE_GLES2) ++ set(USE_OPENGLES ON) ++endif() + option(GFX_DEBUG_DISASSEMBLER "Enable libgfxd" OFF) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +@@ -208,6 +213,7 @@ else() + $<$>:NDEBUG> + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + $<$:USE_OPENGLES> ++ $<$:USE_GLES2> + $<$:GFX_DEBUG_DISASSEMBLER> + ) + endif() +diff --git a/src/fast/Fast3dGui.cpp b/src/fast/Fast3dGui.cpp +index 237a502..8c939c0 100644 +--- a/src/fast/Fast3dGui.cpp ++++ b/src/fast/Fast3dGui.cpp +@@ -146,6 +146,8 @@ void Fast3dGui::ImGuiBackendInit() { + case WindowBackend::FAST3D_SDL_OPENGL: + #ifdef __APPLE__ + ImGui_ImplOpenGL3_Init("#version 410 core"); ++#elif defined(USE_GLES2) ++ ImGui_ImplOpenGL3_Init("#version 100"); + #elif USE_OPENGLES + ImGui_ImplOpenGL3_Init("#version 300 es"); + #else +diff --git a/src/fast/backends/gfx_opengl.cpp b/src/fast/backends/gfx_opengl.cpp +index ad9b1dd..2f6e080 100644 +--- a/src/fast/backends/gfx_opengl.cpp ++++ b/src/fast/backends/gfx_opengl.cpp +@@ -27,6 +27,52 @@ + #include "fast/interpreter.h" + #include "ship/config/ConsoleVariable.h" + ++// a texture was bound outside SelectTexture(): its cache no longer says what is bound ++#define FORGET_TEXTURE_BINDINGS() do { mLastActiveTexture = -1; for (GLuint& bound : mLastBoundTextures) bound = (GLuint)-1; } while (0) ++ ++#include ++ ++#ifdef USE_GLES2 ++/* ++ * OpenGL ES 2.0: no sized texture formats, no multisampling, no blits and no ++ * depth reads. Multisampling stays off (mMaxMsaaLevel 1), so the multisample ++ * calls below are never reached; CopyFramebuffer and GetPixelDepth have their ++ * own GLES2 versions. Depth: packed depth+stencil when the driver has ++ * GL_OES_packed_depth_stencil, else a 16-bit depth buffer. ++ */ ++#define GL_RGB8 GL_RGB ++#define GL_RGBA8 GL_RGBA ++#define GL_DEPTH24_STENCIL8 GL_DEPTH24_STENCIL8_OES ++#define GL_DEPTH_STENCIL GL_DEPTH_STENCIL_OES ++#define GL_UNSIGNED_INT_24_8 GL_UNSIGNED_INT_24_8_OES ++#define GL_READ_FRAMEBUFFER GL_FRAMEBUFFER ++#define GL_DRAW_FRAMEBUFFER GL_FRAMEBUFFER ++#define glBlitFramebuffer(...) ((void)0) ++#define glReadBuffer(b) ((void)0) ++#define glRenderbufferStorageMultisample(target, samples, format, w, h) glRenderbufferStorage(target, format, w, h) ++static bool sPackedDepthStencil = true; ++#endif ++ ++namespace Fast { ++static void RenderbufferDepthStorage(GLsizei width, GLsizei height) { ++#ifdef USE_GLES2 ++ glRenderbufferStorage(GL_RENDERBUFFER, sPackedDepthStencil ? GL_DEPTH24_STENCIL8_OES : GL_DEPTH_COMPONENT16, ++ width, height); ++#else ++ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++#endif ++} ++ ++static void FramebufferDepthAttach(GLuint rb) { ++#ifdef USE_GLES2 ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb); ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, sPackedDepthStencil ? rb : 0); ++#else ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb); ++#endif ++} ++} // namespace Fast ++ + namespace Fast { + int GfxRenderingAPIOGL::GetMaxTextureSize() { + GLint max_texture_size; +@@ -42,7 +88,17 @@ GfxClipParameters GfxRenderingAPIOGL::GetClipParameters() { + return { false, mFrameBuffers[mCurrentFrameBuffer].invertY }; + } + +-static void VertexArraySetAttribs(ShaderProgram* prg) { ++#ifdef USE_GLES2 ++static ShaderProgram* sClientArrayProgram; ++static const float* sClientArrayBase; ++#endif ++ ++static void VertexArraySetAttribs(ShaderProgram* prg, const float* clientArray = nullptr) { ++#ifdef USE_GLES2 ++ if (clientArray == nullptr) { ++ sClientArrayProgram = nullptr; // set for a buffer object: the next draw sets them again ++ } ++#endif + size_t numFloats = prg->numFloats; + size_t pos = 0; + +@@ -50,7 +106,9 @@ static void VertexArraySetAttribs(ShaderProgram* prg) { + if (prg->attribLocations[i] >= 0) { + glEnableVertexAttribArray(prg->attribLocations[i]); + glVertexAttribPointer(prg->attribLocations[i], prg->attribSizes[i], GL_FLOAT, GL_FALSE, +- numFloats * sizeof(float), (void*)(pos * sizeof(float))); ++ numFloats * sizeof(float), ++ clientArray != nullptr ? (const void*)(clientArray + pos) ++ : (const void*)(pos * sizeof(float))); + } + pos += prg->attribSizes[i]; + } +@@ -285,13 +343,23 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "opengles", false }, + { "core_opengl", true }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100\nprecision mediump float;" }, ++ { "attr", "varying" }, ++ { "opengles", false }, ++ { "core_opengl", false }, ++ { "gles2", true }, ++ { "texture", "texture2D" }, ++ { "vOutColor", "gl_FragColor" }, + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es\nprecision mediump float;" }, + { "attr", "in" }, + { "opengles", true }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, + #else +@@ -299,6 +367,7 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "varying" }, + { "opengles", false }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture2D" }, + { "vOutColor", "gl_FragColor" }, + #endif +@@ -356,6 +425,11 @@ static std::string BuildVsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "out", "out" }, + { "opengles", false } ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100" }, ++ { "attr", "attribute" }, ++ { "out", "varying" }, ++ { "opengles", false } + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es" }, + { "attr", "in" }, +@@ -420,9 +494,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- // fprintf(stderr, "Vertex shader compilation failed\n"); +- glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]); +- // fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(vertex_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Vertex shader compilation failed:\n%s\n%s\n", error_log, vs_buf.c_str()); + abort(); + } + +@@ -434,9 +507,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- fprintf(stderr, "Fragment shader compilation failed\n"); +- glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]); +- fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(fragment_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Fragment shader compilation failed:\n%s\n%s\n", error_log, fs_buf.c_str()); + abort(); + } + +@@ -559,6 +631,7 @@ GLuint GfxRenderingAPIOGL::NewTexture() { + + void GfxRenderingAPIOGL::DeleteTexture(uint32_t texID) { + glDeleteTextures(1, &texID); ++ FORGET_TEXTURE_BINDINGS(); // deleting unbinds it, and the name comes back with the next texture + } + + void GfxRenderingAPIOGL::SelectTexture(int tile, GLuint texture_id) { +@@ -583,7 +656,9 @@ void GfxRenderingAPIOGL::UploadTexture(const uint8_t* rgba32_buf, uint32_t width + textures[mCurrentTextureIds[mCurrentTile]].height = height; + } + +-#ifdef USE_OPENGLES ++#ifdef USE_GLES2 ++#define GL_MIRROR_CLAMP_TO_EDGE GL_MIRRORED_REPEAT ++#elif defined(USE_OPENGLES) + #define GL_MIRROR_CLAMP_TO_EDGE 0x8743 + #endif + +@@ -703,7 +778,17 @@ void GfxRenderingAPIOGL::DrawTriangles(float buf_vbo[], size_t buf_vbo_len, size + SetPerDrawUniforms(); + + // printf("flushing %d tris\n", buf_vbo_num_tris); ++#ifdef USE_GLES2 ++ // the pointers stay valid until the program changes or ImGui draws (after the frame) ++ if (mCurrentShaderProgram != sClientArrayProgram || buf_vbo != sClientArrayBase) { ++ glBindBuffer(GL_ARRAY_BUFFER, 0); ++ VertexArraySetAttribs(mCurrentShaderProgram, buf_vbo); ++ sClientArrayProgram = mCurrentShaderProgram; ++ sClientArrayBase = buf_vbo; ++ } ++#else + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); ++#endif + glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris); + } + +@@ -715,7 +800,16 @@ void GfxRenderingAPIOGL::Init() { + glGenBuffers(1, &mOpenglVbo); + glBindBuffer(GL_ARRAY_BUFFER, mOpenglVbo); + +-#if defined(__APPLE__) || defined(USE_OPENGLES) ++#ifdef USE_GLES2 ++ { ++ const char* ext = (const char*)glGetString(GL_EXTENSIONS); ++ ++ sPackedDepthStencil = ext != nullptr && strstr(ext, "GL_OES_packed_depth_stencil") != nullptr; ++ SPDLOG_INFO("OpenGL ES 2.0: {} ({}), depth buffer {}", (const char*)glGetString(GL_RENDERER), ++ (const char*)glGetString(GL_VERSION), sPackedDepthStencil ? "24-bit + stencil" : "16-bit"); ++ } ++#endif ++#if defined(__APPLE__) || (defined(USE_OPENGLES) && !defined(USE_GLES2)) + glGenVertexArrays(1, &mOpenglVao); + glBindVertexArray(mOpenglVao); + #endif +@@ -730,17 +824,21 @@ void GfxRenderingAPIOGL::Init() { + + glGenRenderbuffers(1, &mPixelDepthRb); + glBindRenderbuffer(GL_RENDERBUFFER, mPixelDepthRb); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + glGenFramebuffers(1, &mPixelDepthFb); + glBindFramebuffer(GL_FRAMEBUFFER, mPixelDepthFb); +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, mPixelDepthRb); ++ FramebufferDepthAttach(mPixelDepthRb); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + mPixelDepthRbSize = 1; + ++#ifdef USE_GLES2 ++ mMaxMsaaLevel = 1; ++#else + glGetIntegerv(GL_MAX_SAMPLES, &mMaxMsaaLevel); ++#endif + } + + void GfxRenderingAPIOGL::OnResize() { +@@ -748,6 +846,9 @@ void GfxRenderingAPIOGL::OnResize() { + + void GfxRenderingAPIOGL::StartFrame() { + mFrameCount++; ++#ifdef USE_GLES2 ++ sClientArrayProgram = nullptr; // ImGui drew with its own arrays ++#endif + } + + void GfxRenderingAPIOGL::EndFrame() { +@@ -764,7 +865,12 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ++#ifdef USE_GLES2 ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ++#endif + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + + GLuint clrbufMsaa; + glGenRenderbuffers(1, &clrbufMsaa); +@@ -772,7 +878,7 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + GLuint rbo; + glGenRenderbuffers(1, &rbo); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + GLuint fbo; +@@ -806,6 +912,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + glBindTexture(GL_TEXTURE_2D, fb.clrbuf); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.clrbuf, 0); + } else { + glBindRenderbuffer(GL_RENDERBUFFER, fb.clrbufMsaa); +@@ -819,7 +926,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + (fb.width != width || fb.height != height || fb.msaa_level != msaa_level || !fb.has_depth_buffer)) { + glBindRenderbuffer(GL_RENDERBUFFER, fb.rbo); + if (msaa_level <= 1) { +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++ RenderbufferDepthStorage(width, height); + } else { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_DEPTH24_STENCIL8, width, height); + } +@@ -827,9 +934,9 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + } + + if (!fb.has_depth_buffer && has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb.rbo); ++ FramebufferDepthAttach(fb.rbo); + } else if (fb.has_depth_buffer && !has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); ++ FramebufferDepthAttach(0); + } + } + +@@ -932,6 +1039,35 @@ void GfxRenderingAPIOGL::CopyFramebuffer(int fb_dst_id, int fb_src_id, int srcX0 + FramebufferOGL src = mFrameBuffers[fb_src_id]; + const FramebufferOGL& dst = mFrameBuffers[fb_dst_id]; + ++#ifdef USE_GLES2 ++ // No blits: copy into the destination's colour texture, unscaled and in the ++ // same orientation only (G_COPYFB effects; Starship does not use them). ++ if (fb_dst_id == 0 || srcX1 - srcX0 != dstX1 - dstX0 || srcY1 - srcY0 != dstY1 - dstY0 || ++ src.invertY != dst.invertY) { ++ static bool warned; ++ if (!warned) { ++ SPDLOG_WARN("GLES2: scaled, flipped or on-screen framebuffer copies are not supported"); ++ warned = true; ++ } ++ return; ++ } ++ // OpenGL's origin is the bottom left, as in the blit path below ++ if (!src.invertY) { ++ int h = srcY1 - srcY0; ++ srcY1 = src.height - srcY0; ++ srcY0 = srcY1 - h; ++ } ++ glDisable(GL_SCISSOR_TEST); ++ glBindFramebuffer(GL_FRAMEBUFFER, src.fbo); ++ glActiveTexture(GL_TEXTURE0); ++ glBindTexture(GL_TEXTURE_2D, dst.clrbuf); ++ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, dstX0, dstY0, srcX0, srcY0, srcX1 - srcX0, srcY1 - srcY0); ++ glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[mCurrentFrameBuffer].fbo); ++ glEnable(GL_SCISSOR_TEST); ++ FORGET_TEXTURE_BINDINGS(); ++ return; ++#endif ++ + // Adjust y values for non-inverted source frame buffers because opengl uses bottom left for origin + if (!src.invertY) { + int temp = srcY1 - srcY0; +@@ -1024,6 +1160,14 @@ std::unordered_map, uint16_t, hash_pair_ff> + GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set>& coordinates) { + std::unordered_map, uint16_t, hash_pair_ff> res; + ++#ifdef USE_GLES2 ++ // GLES2 cannot read the depth buffer: every point is reported at the far plane ++ for (const auto& coord : coordinates) { ++ res.emplace(coord, 0xFFFC); ++ } ++ return res; ++#endif ++ + FramebufferOGL& fb = mFrameBuffers[fb_id]; + + // When looking up one value and the framebuffer is single-sampled, we can read pixels directly +@@ -1045,8 +1189,8 @@ GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set + #ifndef _WIN32 + #include ++#include ++#include + #endif + + #include +@@ -134,6 +136,46 @@ void GfxSetInstance(std::shared_ptr gfx) { + // N64 prim_depth is 15-bit (0 near, 0x7FFF far). + static constexpr float N64_PRIM_DEPTH_MAX = 32767.0f; + ++// Alternate ("alt/") assets are unloaded and loaded again as new objects; with any in the ++// archives, nothing is kept (decided once: archives are added at start only) ++static bool NoAltAssetFiles() { ++ static const bool none = ++ Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->ListFiles("alt/*")->empty(); ++ return none; ++} ++ ++static std::unordered_map> sHashResourceCache; ++ ++// texture paths named by pointer: kept per pointer, checked against a copy of the path ++struct ResolvedPath { ++ std::string path; ++ std::shared_ptr res; ++}; ++static std::unordered_map sPathResourceCache; ++ ++static std::shared_ptr CachedResourceByHash(uint64_t hash) { ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ } ++ auto it = sHashResourceCache.find(hash); ++ if (it != sHashResourceCache.end()) { ++ return it->second; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ if (res != nullptr) { ++ if (sHashResourceCache.size() >= 4096) { ++ sHashResourceCache.clear(); ++ } ++ sHashResourceCache.emplace(hash, res); ++ } ++ return res; ++} ++ ++static void* CachedResourceRawPointer(uint64_t hash) { ++ auto res = CachedResourceByHash(hash); ++ return res != nullptr ? res->GetRawPointer() : nullptr; ++} ++ + void Interpreter::Flush() { + if (mBufVboLen > 0) { + mRapi->SetCurrentPrimDepth((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX); +@@ -458,7 +500,21 @@ std::shared_ptr Interpreter::ResolveResourceCached(const char* + return nullptr; + } + if (!mResolvedResourceCacheEnabled) { +- return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ } ++ auto found = sPathResourceCache.find(path); ++ if (found != sPathResourceCache.end() && found->second.path == path) { ++ return found->second.res; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ if (res != nullptr) { ++ if (sPathResourceCache.size() >= 4096) { ++ sPathResourceCache.clear(); ++ } ++ sPathResourceCache[path] = { path, res }; ++ } ++ return res; + } + auto it = mResolvedResourceCache.find(path); + if (it != mResolvedResourceCache.end()) { +@@ -481,6 +537,8 @@ void Interpreter::TextureCacheClear() { + mTextureCache.map.clear(); + mTextureCache.lru.clear(); + mResolvedResourceCache.clear(); ++ sHashResourceCache.clear(); ++ sPathResourceCache.clear(); + // Pre-allocate buckets so the map never rehashes during normal operation. + // Rehashing invalidates all iterators, including those stored in LRU entries. + mTextureCache.map.reserve(TEXTURE_CACHE_MAX_SIZE); +@@ -1912,9 +1970,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + if (use_grayscale) { + cc_options |= SHADER_OPT(GRAYSCALE); + } ++#ifndef USE_GLES2 + if (use_prim_depth) { + cc_options |= SHADER_OPT(PRIM_DEPTH); + } ++#endif + + if (!mShaderStack.empty()) { + cc_options |= (mShaderStack.top() << SHADER_ID_SHIFT); +@@ -1942,8 +2002,9 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + ColorCombiner* comb = LookupOrCreateColorCombiner(key); + + uint32_t tm = 0; +- uint32_t tex_width[2], tex_height[2], tex_width2[2], tex_height2[2]; +- uint32_t effective_tile[2]; ++ uint32_t tex_width[2] = { 1, 1 }, tex_height[2] = { 1, 1 }, tex_width2[2] = { 1, 1 }, ++ tex_height2[2] = { 1, 1 }; ++ uint32_t effective_tile[2] = { mRdp->first_tile_index, mRdp->first_tile_index }; + + for (int i = 0; i < 2; i++) { + uint32_t tile = mRdp->first_tile_index + i; +@@ -2075,6 +2136,15 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + } + } + ++ for (int i = 0; i < 2; i++) { ++ if (!comb->usedTextures[i] && comb->usedTextures[i ^ 1]) { ++ tex_width[i] = tex_width[i ^ 1]; ++ tex_height[i] = tex_height[i ^ 1]; ++ tex_width2[i] = tex_width2[i ^ 1]; ++ tex_height2[i] = tex_height2[i ^ 1]; ++ } ++ } ++ + struct ShaderProgram* prg = comb->prg[tm]; + if (prg == NULL) { + comb->prg[tm] = prg = +@@ -2100,6 +2170,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + + for (int i = 0; i < 3; i++) { + float z = v_arr[i]->z, w = v_arr[i]->w; ++#ifdef USE_GLES2 ++ if (use_prim_depth) { ++ z = ((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX * 2.0f - 1.0f) * w; ++ } ++#endif + if (clip_parameters.z_is_from_0_to_1) { + z = (z + w) / 2.0f; + } +@@ -3419,7 +3494,7 @@ bool gfx_mtx_otr_handler_custom_f3dex2(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + + if (mtx != NULL) { + Interpreter* gfx = mInstance.lock().get(); +@@ -3438,7 +3513,7 @@ bool gfx_mtx_otr_handler_custom_f3d(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + if (mtx != nullptr) { + cmd--; + gfx->GfxSpMatrix(C0(16, 8), mtx); +@@ -3505,10 +3580,10 @@ bool gfx_movemem_handler_otr(F3DGfx** cmd0) { + + if (ucode_handler_index == ucode_f3dex2) { + gfx->GfxSpMovememF3dex2(index, offset, +- Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash)); ++ CachedResourceRawPointer(hash)); + } else { + auto light = +- (Fast::LightEntry*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (Fast::LightEntry*)CachedResourceRawPointer(hash); + uintptr_t data = (uintptr_t)&light->Ambient; + gfx->GfxSpMovememF3d(index, offset, (void*)(data + (hasOffset == 1 ? 0x8 : 0))); + } +@@ -3649,7 +3724,7 @@ bool gfx_vtx_hash_handler_custom(F3DGfx** cmd0) { + gfx->GfxSpVertex(C0(12, 8), C0(1, 7) - C0(12, 8), (F3DVtx*)offset); + (*cmd0)++; + } else { +- F3DVtx* vtx = (F3DVtx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DVtx* vtx = (F3DVtx*)CachedResourceRawPointer(hash); + + if (vtx != NULL) { + vtx = (F3DVtx*)((char*)vtx + offset); +@@ -3741,7 +3816,7 @@ bool gfx_dl_otr_hash_handler_custom(F3DGfx** cmd0) { + + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + g_exec_stack.call(cmd, gfx); +@@ -3799,7 +3874,7 @@ bool gfx_branch_z_otr_handler_f3dex2(F3DGfx** cmd0) { + (gfx->mRsp->extra_geometry_mode & G_EX_ALWAYS_EXECUTE_BRANCH) != 0) { + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + (*cmd0) = gfx; +@@ -3989,9 +4064,17 @@ static bool IsValidResolvedAddress(uintptr_t addr) { + reinterpret_cast(addr), &module) != 0; + #else + // For non-Windows platforms, check whether the address belongs to a loaded object. ++#if UINTPTR_MAX <= 0xFFFFFFFFu ++ // 32-bit: the heap and mappings live below 0x10000000 as well; a mapped page is a real address. ++ // mincore is one cheap system call (dladdr searched symbol tables: 3% of the CPU time on an A20). ++ static const uintptr_t pageSize = (uintptr_t)sysconf(_SC_PAGESIZE); ++ unsigned char resident; ++ return mincore(reinterpret_cast(addr & ~(pageSize - 1)), 1, &resident) == 0; ++#else + Dl_info info; + return dladdr(reinterpret_cast(addr), &info) != 0; + #endif ++#endif + } + + bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { +@@ -4006,6 +4089,7 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + // from the resource, but raw textures would leave them at 0. + rawTexMetdata.h_byte_scale = 1; + rawTexMetdata.v_pixel_scale = 1; ++ bool fromResource = false; + + if ((i & 1) != 1) { + if (gfx_check_image_signature(imgData) == 1) { +@@ -4025,10 +4109,11 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + rawTexMetdata.v_pixel_scale = tex->VPixelScale; + rawTexMetdata.type = tex->Type; + rawTexMetdata.resource = tex; ++ fromResource = true; + } + } + +- if (!IsValidResolvedAddress(i)) { ++ if (!fromResource && !IsValidResolvedAddress(i)) { + return false; + } + +@@ -4053,8 +4138,7 @@ bool gfx_set_timg_otr_hash_handler_custom(F3DGfx** cmd0) { + } + + std::shared_ptr texture = std::static_pointer_cast( +- Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess( +- Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->HashToCString(hash))); ++ CachedResourceByHash(hash)); + if (texture != nullptr) { + texFlags = texture->Flags; + rawTexMetadata.width = texture->Width; +diff --git a/src/fast/shaders/opengl/default.shader.glsl b/src/fast/shaders/opengl/default.shader.glsl +index 1fe86aa..ff45672 100644 +--- a/src/fast/shaders/opengl/default.shader.glsl ++++ b/src/fast/shaders/opengl/default.shader.glsl +@@ -134,14 +134,18 @@ + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++ @if(gles2) ++ return fract(sin(random) * 43758.5453); ++ @else + return fract(sin(random) * 143758.5453); ++ @end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -187,7 +191,9 @@ + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +index c431c34..35f66b2 100644 +--- a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp ++++ b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +@@ -80,6 +80,11 @@ void ControllerDefaultMappings::SetDefaultSDLButtonToButtonMappings( + { BTN_DDOWN, { SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, + { BTN_DLEFT, { SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, + { BTN_DRIGHT, { SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, ++#ifdef USE_GLES2 ++ { BTN_CDOWN, { SDL_CONTROLLER_BUTTON_Y } }, ++ { BTN_CLEFT, { SDL_CONTROLLER_BUTTON_X } }, ++ { BTN_CRIGHT, { SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, ++#endif + }); + } + +diff --git a/src/ship/audio/Audio.cpp b/src/ship/audio/Audio.cpp +index 8cc5b12..604a30c 100644 +--- a/src/ship/audio/Audio.cpp ++++ b/src/ship/audio/Audio.cpp +@@ -36,8 +36,11 @@ void Audio::InitAudioPlayer() { + + if (mAudioPlayer && !mAudioPlayer->Init()) { + // Failed to initialize system audio player. +- // Fallback to Null if the native system player does not work. +- SetCurrentAudioBackend(AudioBackend::NUL); ++ // Fallback to Null if the native system player does not work: for this run only, ++ // the saved backend stays and the next start tries it again ++ mAudioBackend = AudioBackend::NUL; ++ mAudioPlayer = std::make_shared(this->mAudioSettings); ++ mAudioPlayer->Init(); + } + } + +diff --git a/src/ship/debug/CrashHandler.cpp b/src/ship/debug/CrashHandler.cpp +index 56abfcb..b15b228 100644 +--- a/src/ship/debug/CrashHandler.cpp ++++ b/src/ship/debug/CrashHandler.cpp +@@ -2,6 +2,9 @@ + #include "ship/utils/StringHelper.h" + #include "ship/debug/CrashHandler.h" + #include "ship/Context.h" ++#ifndef _WIN32 ++#include // _exit ++#endif + + #ifdef _WIN32 + #include +@@ -201,7 +204,7 @@ static void ErrorHandler(int sig, siginfo_t* sigInfo, void* data) { + + Context::GetRawInstance()->GetLogger()->flush(); + spdlog::shutdown(); +- exit(1); ++ _exit(128 + sig); + } + + static void ShutdownHandler(int sig, siginfo_t* sigInfo, void* data) { diff --git a/ports/soh/soh.patch b/ports/soh/soh.patch new file mode 100644 index 0000000..7f99145 --- /dev/null +++ b/ports/soh/soh.patch @@ -0,0 +1,126 @@ +diff --git a/soh/assets/tools/torch-cli/main.cpp b/soh/assets/tools/torch-cli/main.cpp +index bc2a3d14..44fb4cea 100644 +--- a/soh/assets/tools/torch-cli/main.cpp ++++ b/soh/assets/tools/torch-cli/main.cpp +@@ -1,3 +1,6 @@ ++#include ++#include ++#include + // Build-time ROM extraction, for the ExtractAssets target and anything else that needs an + // archive without launching the game. + // +@@ -98,7 +101,9 @@ int main(int argc, char** argv) { + + for (const auto& rom : roms) { + // A fresh extraction per ROM; torch names the archive from config.yml. +- const std::string archive = SohTorch::Extract(rom, src, dest, version, nullptr); ++ std::ifstream romFile(rom, std::ios::binary); ++ std::vector romData((std::istreambuf_iterator(romFile)), std::istreambuf_iterator()); ++ const std::string archive = SohTorch::Extract(std::move(romData), src, dest, version, nullptr); + if (archive.empty()) { + fprintf(stderr, "failed to extract %s\n", rom.c_str()); + return 1; +diff --git a/soh/soh/ResourceManagerHelpers.cpp b/soh/soh/ResourceManagerHelpers.cpp +index 69a482f6..12b24bb6 100644 +--- a/soh/soh/ResourceManagerHelpers.cpp ++++ b/soh/soh/ResourceManagerHelpers.cpp +@@ -9,6 +9,23 @@ + + #include "ResourceManagerHelpers.h" + #include "OTRGlobals.h" ++#include ++#include ++#include ++ ++static bool NoAltAssetFiles() { ++ static const bool none = ++ Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->ListFiles("alt/*")->empty(); ++ return none; ++} ++ ++struct ResolvedByName { ++ std::string path; ++ bool mq; ++ std::shared_ptr res; ++}; ++static std::unordered_map sResolvedByName; ++static std::mutex sResolvedByNameMutex; + #include "cvar_prefixes.h" + #include "Enhancements/enhancementTypes.h" + #include "Enhancements/randomizer/dungeon.h" +@@ -267,20 +284,37 @@ extern "C" bool ResourceMgr_IsAltAssetsEnabled() { + // Unloads a resource if an alternate version exists when alt assets are enabled + // The resource is only removed from the internal cache to prevent it from used in the next resource lookup + extern "C" void ResourceMgr_UnloadOriginalWhenAltExists(const char* resName) { +- if (ResourceMgr_IsAltAssetsEnabled() && ResourceMgr_FileAltExists((char*)resName)) { ++ if (!NoAltAssetFiles() && ResourceMgr_IsAltAssetsEnabled() && ResourceMgr_FileAltExists((char*)resName)) { + ResourceMgr_UnloadResource((char*)resName); + } + } + + std::shared_ptr ResourceMgr_GetResourceByNameHandlingMQ(const char* path) { ++ const bool mq = ResourceMgr_IsGameMasterQuest(); ++ const bool keep = path != nullptr && NoAltAssetFiles(); ++ if (keep) { ++ std::lock_guard lock(sResolvedByNameMutex); ++ auto it = sResolvedByName.find(path); ++ if (it != sResolvedByName.end() && it->second.mq == mq && it->second.path == path) { ++ return it->second.res; ++ } ++ } + std::string Path = path; +- if (ResourceMgr_IsGameMasterQuest()) { ++ if (mq) { + size_t pos = 0; + if ((pos = Path.find("/nonmq/", 0)) != std::string::npos) { + Path.replace(pos, 7, "/mq/"); + } + } +- return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(Path.c_str()); ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(Path.c_str()); ++ if (keep && res != nullptr) { ++ std::lock_guard lock(sResolvedByNameMutex); ++ if (sResolvedByName.size() >= 4096) { ++ sResolvedByName.clear(); ++ } ++ sResolvedByName[path] = { path, mq, res }; ++ } ++ return res; + } + + extern "C" char* ResourceMgr_GetResourceDataByNameHandlingMQ(const char* path) { +diff --git a/soh/soh/mixer.c b/soh/soh/mixer.c +index 303a977d..aebbe568 100644 +--- a/soh/soh/mixer.c ++++ b/soh/soh/mixer.c +@@ -748,11 +748,11 @@ void aMixImplNEON(uint16_t count, int16_t gain, uint16_t in_addr, uint16_t out_a + int16x8_t outLoVec2 = vcombine_s16(outLoVec, outLoVec); + int16x4_t inLoVec = vget_low_s16(inVec); + int16x8_t inLoVec2 = vcombine_s16(inLoVec, inLoVec); +- int32x4_t outX7fffHiVec = vmull_high_n_s16(outVec, 0x7FFF); +- int32x4_t outX7fffLoVec = vmull_high_n_s16(outLoVec2, 0x7FFF); ++ int32x4_t outX7fffHiVec = vmull_n_s16(vget_high_s16(outVec), 0x7FFF); ++ int32x4_t outX7fffLoVec = vmull_n_s16(vget_high_s16(outLoVec2), 0x7FFF); + +- int32x4_t inGainLoVec = vmull_high_s16(inLoVec2, gainVec); +- int32x4_t inGainHiVec = vmull_high_s16(inVec, gainVec); ++ int32x4_t inGainLoVec = vmull_s16(vget_high_s16(inLoVec2), vget_high_s16(gainVec)); ++ int32x4_t inGainHiVec = vmull_s16(vget_high_s16(inVec), vget_high_s16(gainVec)); + int32x4_t addVecLo = vaddq_s32(outX7fffLoVec, inGainLoVec); + int32x4_t addVecHi = vaddq_s32(outX7fffHiVec, inGainHiVec); + addVecHi = vaddq_s32(addVecHi, x4000Vec); +diff --git a/soh/src/code/audio_load.c b/soh/src/code/audio_load.c +index d7de0389..00814510 100644 +--- a/soh/src/code/audio_load.c ++++ b/soh/src/code/audio_load.c +@@ -1252,8 +1252,8 @@ void AudioLoad_Init(void* heap, size_t heapSize) { + gAudioContext.resetTimer = 0; + + memset(&gAudioContext, 0, sizeof(gAudioContext)); +- memset(gAudioContext.seqToPlay, 0, 8); +- memset(gAudioContext.seqReplaced, 0, 8); ++ memset(gAudioContext.seqToPlay, 0, sizeof(gAudioContext.seqToPlay)); ++ memset(gAudioContext.seqReplaced, 0, sizeof(gAudioContext.seqReplaced)); + + switch (osTvType) { + case OS_TV_PAL: diff --git a/ports/spaghettikart/.gitattributes b/ports/spaghettikart/.gitattributes new file mode 100644 index 0000000..ec1ffa8 --- /dev/null +++ b/ports/spaghettikart/.gitattributes @@ -0,0 +1 @@ +*.patch -text diff --git a/ports/spaghettikart/Mario Kart 64.sh b/ports/spaghettikart/Mario Kart 64.sh new file mode 100644 index 0000000..c5341ba --- /dev/null +++ b/ports/spaghettikart/Mario Kart 64.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Mario Kart 64 (SpaghettiKart), built from your own ROM by ports/spaghettikart/build-spaghettikart.sh. +# The game data (mk64.o2r, spaghetti.o2r), settings and saves stay in spaghettikart/. +cd spaghettikart || exit 1 +export SHIP_HOME="$PWD" +# The built-in controls as one standard gamepad (rsos-gamepad); SDL then +# ignores the raw button devices. It runs in the +# game's process group: the menu stops it with the game. +if [ -x /usr/bin/rsos-gamepad ]; then + SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" + export SDL_GAMECONTROLLERCONFIG + export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 + /usr/bin/rsos-gamepad & + usleep 300000 +fi +if [ -x ./Spaghettify ]; then + exec ./Spaghettify +fi +exec /lib/ld-linux-armhf.so.3 ./Spaghettify diff --git a/ports/spaghettikart/README.md b/ports/spaghettikart/README.md new file mode 100644 index 0000000..bcf84cf --- /dev/null +++ b/ports/spaghettikart/README.md @@ -0,0 +1,81 @@ +# Mario Kart 64 (SpaghettiKart) on the RetroStone2 + +[SpaghettiKart](https://github.com/HarbourMasters/SpaghettiKart) is the PC port of the Mario Kart 64 +decompilation, built on HarbourMasters' [libultraship](https://github.com/Kenix3/libultraship), like Star Fox 64 +and Ocarina of Time ([../starship](../starship/README.md), [../soh](../soh/README.md)). This folder adds the same +OpenGL ES 2.0 mode to SpaghettiKart's libultraship and builds the game for the console as a native game +(docs/native-games.md). + +**Nothing from the game is in this repository, the OS images or the updates.** The game's data comes from **your +own ROM** on your PC and stays yours. SpaghettiKart takes one ROM only: **Mario Kart 64 (US)**, SHA-1 +`579c48e211ae952530ffc8738709f078d5dd215e` in `.z64` order (`.v64` and `.n64` dumps are converted). A modified +dump (`[h]`, `[t]`, `[f]` in its name) has another checksum and is refused. + +## Build + +```sh +ports/spaghettikart/build-spaghettikart.sh ~/"Mario Kart 64 (U).z64" ~/my-ports +``` + +It fetches SpaghettiKart at a fixed commit into `~/rsos/spaghettikart-work`, applies `spaghettikart.patch` and +`libultraship-gles2.patch`, builds the game for the console (`USE_GLES2`, the RetroStoneOS cross compiler) and +Torch for the PC, which makes `mk64.o2r` from the ROM and `spaghetti.o2r` (with the patched shaders), then +writes: + +``` +~/my-ports/roms/native/Mario Kart 64.sh +~/my-ports/roms/native/spaghettikart/ Spaghettify, mk64.o2r, spaghetti.o2r, config.yml, yamls/, meta/ +``` + +Copy the `roms` folder onto the SD card's **RETROSTONE** partition; in the menu: **Native games > Mario Kart +64**. Settings and saves stay in `roms/native/spaghettikart/` (`SHIP_HOME`). Hold **Select + Start** for a +second to go back to the menu. The console needs a RetroStoneOS with Native games, the port libraries and +`rsos-gamepad`. + +## Controls + +Through `rsos-gamepad` (by position), by the labels on the console. They are the defaults of a GLES2 build, set +on the first start and changeable in the game's menu afterwards: + +| RetroStone2 | N64 | +|---|---| +| B (bottom) | A (accelerate) | +| A (right) | B (brake) | +| Y (left) or L2 | Z (item) | +| R1 or R2 | R (hop, drift) | +| L1 | L | +| X (top) | C-left | +| Start | Start | +| Analog stick (Pro), D-pad | steering, the D-pad | + +## What the patches change + +* **`libultraship-gles2.patch`**: Ship of Harkinian's GLES2 patch ([../soh](../soh/README.md): the OpenGL ES 2.0 + mode, the 32-bit ARM and Mali-400 fixes, the speed work), carried over to SpaghettiKart's libultraship + (`60c9bb7`, 25 commits before Ship of Harkinian's). This older libultraship dropped every texture address + below `0x10000000` on Linux, which on 32-bit ARM is the heap: the same fix applies. Also backported from newer + libultraship (`766a6bad`): SIGTERM no longer calls `exit()` inside the signal handler, whose destructors + deadlocked. The menu quits a native game with SIGTERM, so every quit hung until the menu killed the game; it + now quits at once. One speed change measured on the console: loading a texture that is already bound no longer + ends the draw call (Mario Kart reloads its textures between every few triangles, and on the Mali-400 each draw + call costs ~0.1 ms of CPU). +* **`spaghettikart.patch`**, SpaghettiKart's own code, for GLES2 builds (`USE_GLES2`, now passed to the game too): + * the GLES2 changes in its own copy of the shaders (packed into `spaghetti.o2r`); + * no screen readback every race frame: it copied the screen to a 320x240 framebuffer and read it back to the + CPU (`glReadPixels`, the CPU waits for the GPU) on every track, for the jumbotrons of Luigi Raceway and Wario + Stadium. GLES2 cannot make that copy (the screen is flipped), so their screens are black, made and uploaded + once instead of six texture uploads a frame; + * first start: linear texture filtering (one sample instead of three-point's three); + * the controls above; + * Toad's Turnpike (an upstream bug): the finish line was placed with `xOrientation`, which is only set when the + race is set up, after the track is made. On the first race after starting the game it was 0 and a pole stood + in the road; it now uses the mirror mode chosen in the menu, as Wario Stadium does. +* **`build-spaghettikart.sh`** builds with `-O3 -funsafe-math-optimizations` instead of Buildroot's `-O2`: GCC uses + NEON for float math on 32-bit ARM only with unsafe math, and libultraship's vertex and triangle work is the + busiest code. + +## Status + +**Works** on the RetroStone2: races, menus, sound, saves, quitting to the menu. Mostly 30 fps; busy places are +CPU-bound by the number of draw calls (measured 190-260 a frame before the draw-call change). Building `mk64.o2r` +logs one "Failed to process" per track for its last packed display list: an empty end marker that nothing uses. diff --git a/ports/spaghettikart/build-spaghettikart.sh b/ports/spaghettikart/build-spaghettikart.sh new file mode 100755 index 0000000..d02e853 --- /dev/null +++ b/ports/spaghettikart/build-spaghettikart.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Builds Mario Kart 64 (SpaghettiKart) for RetroStoneOS from YOUR OWN ROM, on the PC +# (Linux/WSL), as a native game for the SD card (docs/native-games.md). +# +# ports/spaghettikart/build-spaghettikart.sh [output folder] +# +# Needs a built RetroStoneOS tree for the RetroStone2 with SDL2 and the port +# libraries (the RetroStone2 configs): ~/rsos/output, or RSOS_OUTPUT=; cmake, +# ninja, python3 and a C++ compiler for the PC (Torch, the asset extractor, runs here). +# SpaghettiKart is fetched at a fixed commit into ~/rsos/spaghettikart-work +# (RSOS_SPAGHETTIKART_WORK) and patched (spaghettikart.patch, libultraship-gles2.patch). +# +# The result contains the game's data, taken from your ROM: keep it for +# yourself. Never put it in a release, an update or a repository. +# +# Output (default ./spaghettikart-retrostone2): roms/native/Mario Kart 64.sh and +# roms/native/spaghettikart/. Copy the "roms" folder onto the SD card's RETROSTONE +# partition (it merges with the roms folder there). +set -euo pipefail + +REPO=https://github.com/HarbourMasters/SpaghettiKart.git +COMMIT=ce0f9358f0505bbdeb76b1aff7a25556cda3fdef # libultraship 60c9bb7, Torch 2d474dd +ROM_SHA1=579c48e211ae952530ffc8738709f078d5dd215e # Mario Kart 64 (US), .z64 order + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/spaghettikart-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_SPAGHETTIKART_WORK:-$HOME/rsos/spaghettikart-work} + +die() { echo "build-spaghettikart: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +for lib in libSDL2.so libzip.so libtinyxml2.so libspdlog.so libvorbisfile.so; do + [ -e "$O/staging/usr/lib/$lib" ] || die "$lib is not in $O/staging (build RetroStoneOS with the RetroStone2 configs)" +done +TOOLCHAIN=$O/host/share/buildroot/toolchainfile.cmake + +# only the fixed commit (the whole history is large) +if [ ! -d "$W/.git" ]; then + git init -q "$W" + git -C "$W" remote add origin "$REPO" +fi +cd "$W" +git fetch -q --depth 1 origin "$COMMIT" +# a clean tree at the fixed commit, then our changes (the build folder, the +# ROM and the .o2r files are ignored by git and stay) +git checkout -q -f "$COMMIT" +git submodule update -q --init --force --depth 1 libultraship torch +git -C libultraship checkout -q -f . +git apply "$here/spaghettikart.patch" +git -C libultraship apply "$here/libultraship-gles2.patch" + +# the ROM, in .z64 byte order, as baserom.us.z64 (the only one SpaghettiKart takes) +python3 - "$rom" baserom.us.z64 << 'EOF' || die "$rom is not an N64 ROM" +import sys +data = bytearray(open(sys.argv[1], 'rb').read()) +head = bytes(data[:4]) +if head == b'\x37\x80\x40\x12': # .v64: 16-bit words swapped + data[0::2], data[1::2] = data[1::2], data[0::2] +elif head == b'\x40\x12\x37\x80': # .n64: 32-bit words reversed + for i in range(0, len(data) - 3, 4): + data[i:i + 4] = data[i:i + 4][::-1] +elif head != b'\x80\x37\x12\x40': + sys.exit(1) +open(sys.argv[2], 'wb').write(data) +EOF +sum=$(sha1sum baserom.us.z64 | cut -d' ' -f1) +[ "$sum" = "$ROM_SHA1" ] || + die "$rom is not the Mario Kart 64 (US) ROM SpaghettiKart takes (SHA-1 $sum in .z64 order, needs $ROM_SHA1; a modified dump such as [h] or [t] does not work)" + +# the game for the console. CMAKE_SYSTEM_NAME Linux: Buildroot's toolchain file names it +# "Buildroot", which skips libultraship's Linux parts. Torch is built for this PC (its own +# sub-build) and makes mk64.o2r from the ROM and spaghetti.o2r (with the patched shaders). +# Release flags: Buildroot's toolchain file builds with -O2, and GCC uses NEON for float math on +# 32-bit ARM only with unsafe math; libultraship's vertex and triangle work is the busiest code. +fast="-O3 -DNDEBUG -fno-math-errno -funsafe-math-optimizations" +cmake -S . -B build-gles2 -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_OPENGLES=ON -DUSE_GLES2=ON \ + -DCMAKE_C_FLAGS_RELEASE="$fast" -DCMAKE_CXX_FLAGS_RELEASE="$fast" \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DCMAKE_SYSTEM_NAME=Linux +cmake --build build-gles2 --target Spaghettify +cmake --build build-gles2 --target ExtractAssets + +file build-gles2/Spaghettify | grep -q 'ARM' || die "build-gles2/Spaghettify is not an ARM program" +S=$out/roms/native/spaghettikart +mkdir -p "$S" +"$O/host/bin/arm-linux-strip" -o "$S/Spaghettify" build-gles2/Spaghettify +cp build-gles2/mk64.o2r build-gles2/spaghetti.o2r "$S/" +cp -r build-gles2/config.yml build-gles2/yamls build-gles2/meta "$S/" +[ -f build-gles2/gamecontrollerdb.txt ] && cp build-gles2/gamecontrollerdb.txt "$S/" +install -m 0644 "$here/Mario Kart 64.sh" "$out/roms/native/Mario Kart 64.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > Mario Kart 64. Hold Select + Start to quit." diff --git a/ports/spaghettikart/libultraship-gles2.patch b/ports/spaghettikart/libultraship-gles2.patch new file mode 100644 index 0000000..bc7521f --- /dev/null +++ b/ports/spaghettikart/libultraship-gles2.patch @@ -0,0 +1,872 @@ +diff --git a/cmake/dependencies/linux.cmake b/cmake/dependencies/linux.cmake +index 02301f1..8e231e1 100644 +--- a/cmake/dependencies/linux.cmake ++++ b/cmake/dependencies/linux.cmake +@@ -2,7 +2,10 @@ + find_package(SDL2 REQUIRED) + target_link_libraries(ImGui PUBLIC SDL2::SDL2) + +-if (USE_OPENGLES) ++if (USE_GLES2) ++ target_link_libraries(ImGui PUBLIC GLESv2) ++ add_compile_definitions(IMGUI_IMPL_OPENGL_ES2) ++elseif (USE_OPENGLES) + target_link_libraries(ImGui PUBLIC ${OPENGL_GLESv2_LIBRARY}) + add_compile_definitions(IMGUI_IMPL_OPENGL_ES3) + else() +diff --git a/include/fast/backends/gfx_opengl.h b/include/fast/backends/gfx_opengl.h +index af24c70..efdf5c5 100644 +--- a/include/fast/backends/gfx_opengl.h ++++ b/include/fast/backends/gfx_opengl.h +@@ -16,6 +16,10 @@ + #elif __APPLE__ + #include + #include ++#elif defined(USE_GLES2) ++#include ++#include ++#include + #elif USE_OPENGLES + #include + #include +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 45989b6..0209da4 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -11,6 +11,11 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) + + set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include) + option(USE_OPENGLES "Enable GLES3" OFF) ++# OpenGL ES 2.0 (Mali-400 and other GLES2-only GPUs); implies USE_OPENGLES ++option(USE_GLES2 "Enable GLES2" OFF) ++if (USE_GLES2) ++ set(USE_OPENGLES ON) ++endif() + option(GFX_DEBUG_DISASSEMBLER "Enable libgfxd" OFF) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +@@ -208,6 +213,7 @@ else() + $<$>:NDEBUG> + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + $<$:USE_OPENGLES> ++ $<$:USE_GLES2> + $<$:GFX_DEBUG_DISASSEMBLER> + ) + endif() +diff --git a/src/fast/Fast3dGui.cpp b/src/fast/Fast3dGui.cpp +index 237a502..8c939c0 100644 +--- a/src/fast/Fast3dGui.cpp ++++ b/src/fast/Fast3dGui.cpp +@@ -146,6 +146,8 @@ void Fast3dGui::ImGuiBackendInit() { + case WindowBackend::FAST3D_SDL_OPENGL: + #ifdef __APPLE__ + ImGui_ImplOpenGL3_Init("#version 410 core"); ++#elif defined(USE_GLES2) ++ ImGui_ImplOpenGL3_Init("#version 100"); + #elif USE_OPENGLES + ImGui_ImplOpenGL3_Init("#version 300 es"); + #else +diff --git a/src/fast/backends/gfx_opengl.cpp b/src/fast/backends/gfx_opengl.cpp +index ad9b1dd..2f6e080 100644 +--- a/src/fast/backends/gfx_opengl.cpp ++++ b/src/fast/backends/gfx_opengl.cpp +@@ -27,6 +27,52 @@ + #include "fast/interpreter.h" + #include "ship/config/ConsoleVariable.h" + ++// a texture was bound outside SelectTexture(): its cache no longer says what is bound ++#define FORGET_TEXTURE_BINDINGS() do { mLastActiveTexture = -1; for (GLuint& bound : mLastBoundTextures) bound = (GLuint)-1; } while (0) ++ ++#include ++ ++#ifdef USE_GLES2 ++/* ++ * OpenGL ES 2.0: no sized texture formats, no multisampling, no blits and no ++ * depth reads. Multisampling stays off (mMaxMsaaLevel 1), so the multisample ++ * calls below are never reached; CopyFramebuffer and GetPixelDepth have their ++ * own GLES2 versions. Depth: packed depth+stencil when the driver has ++ * GL_OES_packed_depth_stencil, else a 16-bit depth buffer. ++ */ ++#define GL_RGB8 GL_RGB ++#define GL_RGBA8 GL_RGBA ++#define GL_DEPTH24_STENCIL8 GL_DEPTH24_STENCIL8_OES ++#define GL_DEPTH_STENCIL GL_DEPTH_STENCIL_OES ++#define GL_UNSIGNED_INT_24_8 GL_UNSIGNED_INT_24_8_OES ++#define GL_READ_FRAMEBUFFER GL_FRAMEBUFFER ++#define GL_DRAW_FRAMEBUFFER GL_FRAMEBUFFER ++#define glBlitFramebuffer(...) ((void)0) ++#define glReadBuffer(b) ((void)0) ++#define glRenderbufferStorageMultisample(target, samples, format, w, h) glRenderbufferStorage(target, format, w, h) ++static bool sPackedDepthStencil = true; ++#endif ++ ++namespace Fast { ++static void RenderbufferDepthStorage(GLsizei width, GLsizei height) { ++#ifdef USE_GLES2 ++ glRenderbufferStorage(GL_RENDERBUFFER, sPackedDepthStencil ? GL_DEPTH24_STENCIL8_OES : GL_DEPTH_COMPONENT16, ++ width, height); ++#else ++ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++#endif ++} ++ ++static void FramebufferDepthAttach(GLuint rb) { ++#ifdef USE_GLES2 ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb); ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, sPackedDepthStencil ? rb : 0); ++#else ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb); ++#endif ++} ++} // namespace Fast ++ + namespace Fast { + int GfxRenderingAPIOGL::GetMaxTextureSize() { + GLint max_texture_size; +@@ -42,7 +88,17 @@ GfxClipParameters GfxRenderingAPIOGL::GetClipParameters() { + return { false, mFrameBuffers[mCurrentFrameBuffer].invertY }; + } + +-static void VertexArraySetAttribs(ShaderProgram* prg) { ++#ifdef USE_GLES2 ++static ShaderProgram* sClientArrayProgram; ++static const float* sClientArrayBase; ++#endif ++ ++static void VertexArraySetAttribs(ShaderProgram* prg, const float* clientArray = nullptr) { ++#ifdef USE_GLES2 ++ if (clientArray == nullptr) { ++ sClientArrayProgram = nullptr; // set for a buffer object: the next draw sets them again ++ } ++#endif + size_t numFloats = prg->numFloats; + size_t pos = 0; + +@@ -50,7 +106,9 @@ static void VertexArraySetAttribs(ShaderProgram* prg) { + if (prg->attribLocations[i] >= 0) { + glEnableVertexAttribArray(prg->attribLocations[i]); + glVertexAttribPointer(prg->attribLocations[i], prg->attribSizes[i], GL_FLOAT, GL_FALSE, +- numFloats * sizeof(float), (void*)(pos * sizeof(float))); ++ numFloats * sizeof(float), ++ clientArray != nullptr ? (const void*)(clientArray + pos) ++ : (const void*)(pos * sizeof(float))); + } + pos += prg->attribSizes[i]; + } +@@ -285,13 +343,23 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "opengles", false }, + { "core_opengl", true }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100\nprecision mediump float;" }, ++ { "attr", "varying" }, ++ { "opengles", false }, ++ { "core_opengl", false }, ++ { "gles2", true }, ++ { "texture", "texture2D" }, ++ { "vOutColor", "gl_FragColor" }, + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es\nprecision mediump float;" }, + { "attr", "in" }, + { "opengles", true }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, + #else +@@ -299,6 +367,7 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "varying" }, + { "opengles", false }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture2D" }, + { "vOutColor", "gl_FragColor" }, + #endif +@@ -356,6 +425,11 @@ static std::string BuildVsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "out", "out" }, + { "opengles", false } ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100" }, ++ { "attr", "attribute" }, ++ { "out", "varying" }, ++ { "opengles", false } + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es" }, + { "attr", "in" }, +@@ -420,9 +494,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- // fprintf(stderr, "Vertex shader compilation failed\n"); +- glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]); +- // fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(vertex_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Vertex shader compilation failed:\n%s\n%s\n", error_log, vs_buf.c_str()); + abort(); + } + +@@ -434,9 +507,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- fprintf(stderr, "Fragment shader compilation failed\n"); +- glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]); +- fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(fragment_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Fragment shader compilation failed:\n%s\n%s\n", error_log, fs_buf.c_str()); + abort(); + } + +@@ -559,6 +631,7 @@ GLuint GfxRenderingAPIOGL::NewTexture() { + + void GfxRenderingAPIOGL::DeleteTexture(uint32_t texID) { + glDeleteTextures(1, &texID); ++ FORGET_TEXTURE_BINDINGS(); // deleting unbinds it, and the name comes back with the next texture + } + + void GfxRenderingAPIOGL::SelectTexture(int tile, GLuint texture_id) { +@@ -583,7 +656,9 @@ void GfxRenderingAPIOGL::UploadTexture(const uint8_t* rgba32_buf, uint32_t width + textures[mCurrentTextureIds[mCurrentTile]].height = height; + } + +-#ifdef USE_OPENGLES ++#ifdef USE_GLES2 ++#define GL_MIRROR_CLAMP_TO_EDGE GL_MIRRORED_REPEAT ++#elif defined(USE_OPENGLES) + #define GL_MIRROR_CLAMP_TO_EDGE 0x8743 + #endif + +@@ -703,7 +778,17 @@ void GfxRenderingAPIOGL::DrawTriangles(float buf_vbo[], size_t buf_vbo_len, size + SetPerDrawUniforms(); + + // printf("flushing %d tris\n", buf_vbo_num_tris); ++#ifdef USE_GLES2 ++ // the pointers stay valid until the program changes or ImGui draws (after the frame) ++ if (mCurrentShaderProgram != sClientArrayProgram || buf_vbo != sClientArrayBase) { ++ glBindBuffer(GL_ARRAY_BUFFER, 0); ++ VertexArraySetAttribs(mCurrentShaderProgram, buf_vbo); ++ sClientArrayProgram = mCurrentShaderProgram; ++ sClientArrayBase = buf_vbo; ++ } ++#else + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); ++#endif + glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris); + } + +@@ -715,7 +800,16 @@ void GfxRenderingAPIOGL::Init() { + glGenBuffers(1, &mOpenglVbo); + glBindBuffer(GL_ARRAY_BUFFER, mOpenglVbo); + +-#if defined(__APPLE__) || defined(USE_OPENGLES) ++#ifdef USE_GLES2 ++ { ++ const char* ext = (const char*)glGetString(GL_EXTENSIONS); ++ ++ sPackedDepthStencil = ext != nullptr && strstr(ext, "GL_OES_packed_depth_stencil") != nullptr; ++ SPDLOG_INFO("OpenGL ES 2.0: {} ({}), depth buffer {}", (const char*)glGetString(GL_RENDERER), ++ (const char*)glGetString(GL_VERSION), sPackedDepthStencil ? "24-bit + stencil" : "16-bit"); ++ } ++#endif ++#if defined(__APPLE__) || (defined(USE_OPENGLES) && !defined(USE_GLES2)) + glGenVertexArrays(1, &mOpenglVao); + glBindVertexArray(mOpenglVao); + #endif +@@ -730,17 +824,21 @@ void GfxRenderingAPIOGL::Init() { + + glGenRenderbuffers(1, &mPixelDepthRb); + glBindRenderbuffer(GL_RENDERBUFFER, mPixelDepthRb); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + glGenFramebuffers(1, &mPixelDepthFb); + glBindFramebuffer(GL_FRAMEBUFFER, mPixelDepthFb); +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, mPixelDepthRb); ++ FramebufferDepthAttach(mPixelDepthRb); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + mPixelDepthRbSize = 1; + ++#ifdef USE_GLES2 ++ mMaxMsaaLevel = 1; ++#else + glGetIntegerv(GL_MAX_SAMPLES, &mMaxMsaaLevel); ++#endif + } + + void GfxRenderingAPIOGL::OnResize() { +@@ -748,6 +846,9 @@ void GfxRenderingAPIOGL::OnResize() { + + void GfxRenderingAPIOGL::StartFrame() { + mFrameCount++; ++#ifdef USE_GLES2 ++ sClientArrayProgram = nullptr; // ImGui drew with its own arrays ++#endif + } + + void GfxRenderingAPIOGL::EndFrame() { +@@ -764,7 +865,12 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ++#ifdef USE_GLES2 ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ++#endif + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + + GLuint clrbufMsaa; + glGenRenderbuffers(1, &clrbufMsaa); +@@ -772,7 +878,7 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + GLuint rbo; + glGenRenderbuffers(1, &rbo); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + GLuint fbo; +@@ -806,6 +912,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + glBindTexture(GL_TEXTURE_2D, fb.clrbuf); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); ++ FORGET_TEXTURE_BINDINGS(); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.clrbuf, 0); + } else { + glBindRenderbuffer(GL_RENDERBUFFER, fb.clrbufMsaa); +@@ -819,7 +926,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + (fb.width != width || fb.height != height || fb.msaa_level != msaa_level || !fb.has_depth_buffer)) { + glBindRenderbuffer(GL_RENDERBUFFER, fb.rbo); + if (msaa_level <= 1) { +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++ RenderbufferDepthStorage(width, height); + } else { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_DEPTH24_STENCIL8, width, height); + } +@@ -827,9 +934,9 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + } + + if (!fb.has_depth_buffer && has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb.rbo); ++ FramebufferDepthAttach(fb.rbo); + } else if (fb.has_depth_buffer && !has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); ++ FramebufferDepthAttach(0); + } + } + +@@ -932,6 +1039,35 @@ void GfxRenderingAPIOGL::CopyFramebuffer(int fb_dst_id, int fb_src_id, int srcX0 + FramebufferOGL src = mFrameBuffers[fb_src_id]; + const FramebufferOGL& dst = mFrameBuffers[fb_dst_id]; + ++#ifdef USE_GLES2 ++ // No blits: copy into the destination's colour texture, unscaled and in the ++ // same orientation only (G_COPYFB effects; Starship does not use them). ++ if (fb_dst_id == 0 || srcX1 - srcX0 != dstX1 - dstX0 || srcY1 - srcY0 != dstY1 - dstY0 || ++ src.invertY != dst.invertY) { ++ static bool warned; ++ if (!warned) { ++ SPDLOG_WARN("GLES2: scaled, flipped or on-screen framebuffer copies are not supported"); ++ warned = true; ++ } ++ return; ++ } ++ // OpenGL's origin is the bottom left, as in the blit path below ++ if (!src.invertY) { ++ int h = srcY1 - srcY0; ++ srcY1 = src.height - srcY0; ++ srcY0 = srcY1 - h; ++ } ++ glDisable(GL_SCISSOR_TEST); ++ glBindFramebuffer(GL_FRAMEBUFFER, src.fbo); ++ glActiveTexture(GL_TEXTURE0); ++ glBindTexture(GL_TEXTURE_2D, dst.clrbuf); ++ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, dstX0, dstY0, srcX0, srcY0, srcX1 - srcX0, srcY1 - srcY0); ++ glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[mCurrentFrameBuffer].fbo); ++ glEnable(GL_SCISSOR_TEST); ++ FORGET_TEXTURE_BINDINGS(); ++ return; ++#endif ++ + // Adjust y values for non-inverted source frame buffers because opengl uses bottom left for origin + if (!src.invertY) { + int temp = srcY1 - srcY0; +@@ -1024,6 +1160,14 @@ std::unordered_map, uint16_t, hash_pair_ff> + GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set>& coordinates) { + std::unordered_map, uint16_t, hash_pair_ff> res; + ++#ifdef USE_GLES2 ++ // GLES2 cannot read the depth buffer: every point is reported at the far plane ++ for (const auto& coord : coordinates) { ++ res.emplace(coord, 0xFFFC); ++ } ++ return res; ++#endif ++ + FramebufferOGL& fb = mFrameBuffers[fb_id]; + + // When looking up one value and the framebuffer is single-sampled, we can read pixels directly +@@ -1045,8 +1189,8 @@ GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set + #include + #include ++#ifndef _WIN32 ++#include ++#include ++#include ++#endif + + #include + #include +@@ -131,6 +136,64 @@ void GfxSetInstance(std::shared_ptr gfx) { + // N64 prim_depth is 15-bit (0 near, 0x7FFF far). + static constexpr float N64_PRIM_DEPTH_MAX = 32767.0f; + ++// Alternate ("alt/") assets are unloaded and loaded again as new objects; with any in the ++// archives, nothing is kept (decided once: archives are added at start only) ++static bool NoAltAssetFiles() { ++ static const bool none = ++ Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->ListFiles("alt/*")->empty(); ++ return none; ++} ++ ++static std::unordered_map> sHashResourceCache; ++ ++// texture paths named by pointer: kept per pointer, checked against a copy of the path ++struct ResolvedPath { ++ std::string path; ++ std::shared_ptr res; ++}; ++static std::unordered_map sPathResourceCache; ++ ++static std::shared_ptr CachedResourceByHash(uint64_t hash) { ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ } ++ auto it = sHashResourceCache.find(hash); ++ if (it != sHashResourceCache.end()) { ++ return it->second; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResource(hash); ++ if (res != nullptr) { ++ if (sHashResourceCache.size() >= 4096) { ++ sHashResourceCache.clear(); ++ } ++ sHashResourceCache.emplace(hash, res); ++ } ++ return res; ++} ++ ++static std::shared_ptr CachedResourceByPath(const char* path) { ++ if (!NoAltAssetFiles()) { ++ return Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ } ++ auto found = sPathResourceCache.find(path); ++ if (found != sPathResourceCache.end() && found->second.path == path) { ++ return found->second.res; ++ } ++ auto res = Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path); ++ if (res != nullptr) { ++ if (sPathResourceCache.size() >= 4096) { ++ sPathResourceCache.clear(); ++ } ++ sPathResourceCache[path] = { path, res }; ++ } ++ return res; ++} ++ ++static void* CachedResourceRawPointer(uint64_t hash) { ++ auto res = CachedResourceByHash(hash); ++ return res != nullptr ? res->GetRawPointer() : nullptr; ++} ++ + void Interpreter::Flush() { + if (mBufVboLen > 0) { + mRapi->SetCurrentPrimDepth((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX); +@@ -447,6 +510,8 @@ void Interpreter::TextureCacheClear() { + } + mTextureCache.map.clear(); + mTextureCache.lru.clear(); ++ sHashResourceCache.clear(); ++ sPathResourceCache.clear(); + // Pre-allocate buckets so the map never rehashes during normal operation. + // Rehashing invalidates all iterators, including those stored in LRU entries. + mTextureCache.map.reserve(TEXTURE_CACHE_MAX_SIZE); +@@ -1242,6 +1307,7 @@ void Interpreter::ImportTexture(int i, int tile, bool importReplacement) { + if (fbIt != mFbTextures.end()) { + Flush(); + mRapi->SelectTextureFb(fbIt->second); ++ mRenderingState.mTextures[i] = nullptr; + mRdp->textures_changed[i] = false; + return; + } +@@ -1285,6 +1351,15 @@ void Interpreter::ImportTexture(int i, int tile, bool importReplacement) { + key = { origAddr, {}, fmt, siz, paletteIndex, origSizeBytes }; + } + ++ // The texture already bound to this slot: the triangles so far can keep going (selected ++ // again in case the backend lost its bindings) ++ if (!importReplacement && mRenderingState.mTextures[i] != nullptr && ++ mRenderingState.mTextures[i]->first == key) { ++ mRapi->SelectTexture(i, mRenderingState.mTextures[i]->second.texture_id); ++ return; ++ } ++ Flush(); ++ + if (TextureCacheLookup(i, key)) { + return; + } +@@ -1886,9 +1961,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + if (use_grayscale) { + cc_options |= SHADER_OPT(GRAYSCALE); + } ++#ifndef USE_GLES2 + if (use_prim_depth) { + cc_options |= SHADER_OPT(PRIM_DEPTH); + } ++#endif + + if (!mShaderStack.empty()) { + cc_options |= (mShaderStack.top() << SHADER_ID_SHIFT); +@@ -1916,8 +1993,9 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + ColorCombiner* comb = LookupOrCreateColorCombiner(key); + + uint32_t tm = 0; +- uint32_t tex_width[2], tex_height[2], tex_width2[2], tex_height2[2]; +- uint32_t effective_tile[2]; ++ uint32_t tex_width[2] = { 1, 1 }, tex_height[2] = { 1, 1 }, tex_width2[2] = { 1, 1 }, ++ tex_height2[2] = { 1, 1 }; ++ uint32_t effective_tile[2] = { mRdp->first_tile_index, mRdp->first_tile_index }; + + for (int i = 0; i < 2; i++) { + uint32_t tile = mRdp->first_tile_index + i; +@@ -1930,12 +2008,13 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + + if (comb->usedTextures[i]) { + if (mRdp->textures_changed[i]) { +- Flush(); +- ImportTexture(i, tile, false); ++ ImportTexture(i, tile, false); // flushes if the texture changes + if (mRdp->loaded_texture[i].masked) { ++ Flush(); + ImportTextureMask(SHADER_FIRST_MASK_TEXTURE + i, tile); + } + if (mRdp->loaded_texture[i].blended) { ++ Flush(); + ImportTexture(SHADER_FIRST_REPLACEMENT_TEXTURE + i, tile, true); + } + mRdp->textures_changed[i] = false; +@@ -2044,6 +2123,15 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + } + } + ++ for (int i = 0; i < 2; i++) { ++ if (!comb->usedTextures[i] && comb->usedTextures[i ^ 1]) { ++ tex_width[i] = tex_width[i ^ 1]; ++ tex_height[i] = tex_height[i ^ 1]; ++ tex_width2[i] = tex_width2[i ^ 1]; ++ tex_height2[i] = tex_height2[i ^ 1]; ++ } ++ } ++ + struct ShaderProgram* prg = comb->prg[tm]; + if (prg == NULL) { + comb->prg[tm] = prg = +@@ -2069,6 +2157,11 @@ void Interpreter::GfxSpTri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx + + for (int i = 0; i < 3; i++) { + float z = v_arr[i]->z, w = v_arr[i]->w; ++#ifdef USE_GLES2 ++ if (use_prim_depth) { ++ z = ((float)mRdp->prim_depth / N64_PRIM_DEPTH_MAX * 2.0f - 1.0f) * w; ++ } ++#endif + if (clip_parameters.z_is_from_0_to_1) { + z = (z + w) / 2.0f; + } +@@ -3382,7 +3475,7 @@ bool gfx_mtx_otr_handler_custom_f3dex2(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + + if (mtx != NULL) { + Interpreter* gfx = mInstance.lock().get(); +@@ -3401,7 +3494,7 @@ bool gfx_mtx_otr_handler_custom_f3d(F3DGfx** cmd0) { + + const uint64_t hash = ((uint64_t)cmd->words.w0 << 32) + cmd->words.w1; + const int32_t* mtx = +- (const int32_t*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (const int32_t*)CachedResourceRawPointer(hash); + if (mtx != nullptr) { + cmd--; + gfx->GfxSpMatrix(C0(16, 8), mtx); +@@ -3468,10 +3561,10 @@ bool gfx_movemem_handler_otr(F3DGfx** cmd0) { + + if (ucode_handler_index == ucode_f3dex2) { + gfx->GfxSpMovememF3dex2(index, offset, +- Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash)); ++ CachedResourceRawPointer(hash)); + } else { + auto light = +- (Fast::LightEntry*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ (Fast::LightEntry*)CachedResourceRawPointer(hash); + uintptr_t data = (uintptr_t)&light->Ambient; + gfx->GfxSpMovememF3d(index, offset, (void*)(data + (hasOffset == 1 ? 0x8 : 0))); + } +@@ -3610,7 +3703,7 @@ bool gfx_vtx_hash_handler_custom(F3DGfx** cmd0) { + gfx->GfxSpVertex(C0(12, 8), C0(1, 7) - C0(12, 8), (F3DVtx*)offset); + (*cmd0)++; + } else { +- F3DVtx* vtx = (F3DVtx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DVtx* vtx = (F3DVtx*)CachedResourceRawPointer(hash); + + if (vtx != NULL) { + vtx = (F3DVtx*)((char*)vtx + offset); +@@ -3702,7 +3795,7 @@ bool gfx_dl_otr_hash_handler_custom(F3DGfx** cmd0) { + + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + g_exec_stack.call(cmd, gfx); +@@ -3760,7 +3853,7 @@ bool gfx_branch_z_otr_handler_f3dex2(F3DGfx** cmd0) { + (gfx->mRsp->extra_geometry_mode & G_EX_ALWAYS_EXECUTE_BRANCH) != 0) { + uint64_t hash = ((uint64_t)(*cmd0)->words.w0 << 32) + (*cmd0)->words.w1; + +- F3DGfx* gfx = (F3DGfx*)Ship::Context::GetRawInstance()->GetResourceManager()->GetResourceRawPointer(hash); ++ F3DGfx* gfx = (F3DGfx*)CachedResourceRawPointer(hash); + + if (gfx != 0) { + (*cmd0) = gfx; +@@ -3934,6 +4027,35 @@ bool gfx_othermode_h_handler_f3d(F3DGfx** cmd0) { + return false; + } + ++// A resolved address still in the N64 segmented range (<= 0x0FFFFFFF) usually means SegAddr ++// failed to resolve it (segment not set up). Keep it only if it belongs to a loaded module, ++// where it's a real low pointer (e.g. a static TLUT) rather than an unresolved segment addr. ++static bool IsValidResolvedAddress(uintptr_t addr) { ++ if (addr > 0x0FFFFFFF) { ++ return true; ++ } ++ ++ // Still in the N64 segmented range, but might be a false positive (a real low pointer). ++#ifdef _WIN32 ++ // For Windows, check whether the address belongs to a dll. ++ HMODULE module = nullptr; ++ return GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, ++ reinterpret_cast(addr), &module) != 0; ++#else ++ // For non-Windows platforms, check whether the address belongs to a loaded object. ++#if UINTPTR_MAX <= 0xFFFFFFFFu ++ // 32-bit: the heap and mappings live below 0x10000000 as well; a mapped page is a real address. ++ // mincore is one cheap system call (dladdr searched symbol tables: 3% of the CPU time on an A20). ++ static const uintptr_t pageSize = (uintptr_t)sysconf(_SC_PAGESIZE); ++ unsigned char resident; ++ return mincore(reinterpret_cast(addr & ~(pageSize - 1)), 1, &resident) == 0; ++#else ++ Dl_info info; ++ return dladdr(reinterpret_cast(addr), &info) != 0; ++#endif ++#endif ++} ++ + bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + Interpreter* gfx = mInstance.lock().get(); + F3DGfx* cmd = *cmd0; +@@ -3946,11 +4068,12 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + // from the resource, but raw textures would leave them at 0. + rawTexMetdata.h_byte_scale = 1; + rawTexMetdata.v_pixel_scale = 1; ++ bool fromResource = false; + + if ((i & 1) != 1) { + if (gfx_check_image_signature(imgData) == 1) { +- std::shared_ptr tex = std::static_pointer_cast( +- Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(imgData)); ++ std::shared_ptr tex = ++ std::static_pointer_cast(CachedResourceByPath(imgData)); + + if (tex == nullptr) { + (*cmd0)++; +@@ -3965,26 +4088,13 @@ bool gfx_set_timg_handler_rdp(F3DGfx** cmd0) { + rawTexMetdata.v_pixel_scale = tex->VPixelScale; + rawTexMetdata.type = tex->Type; + rawTexMetdata.resource = tex; ++ fromResource = true; + } + } + +- // If the resolved address is still in the N64 segmented range, SegAddr +- // failed to resolve it (segment not set up). Skip to avoid dereferencing +- // invalid memory. +- // For Windows, also check if the address is not from a dll because this validation returns a false positive caused +- // by how the virtual memory is allocated. +-#ifdef _WIN32 +- HMODULE module = nullptr; +- if (i <= 0x0FFFFFFF && +- !(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, +- reinterpret_cast(i), &module))) { +- return false; +- } +-#else +- if (i <= 0x0FFFFFFF) { ++ if (!fromResource && !IsValidResolvedAddress(i)) { + return false; + } +-#endif + + gfx->GfxDpSetTextureImage(C0(21, 3), C0(19, 2), C0(0, 12) + 1, imgData, texFlags, rawTexMetdata, (void*)i); + +@@ -4007,8 +4117,7 @@ bool gfx_set_timg_otr_hash_handler_custom(F3DGfx** cmd0) { + } + + std::shared_ptr texture = std::static_pointer_cast( +- Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess( +- Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager()->HashToCString(hash))); ++ CachedResourceByHash(hash)); + if (texture != nullptr) { + texFlags = texture->Flags; + rawTexMetadata.width = texture->Width; +diff --git a/src/fast/shaders/opengl/default.shader.glsl b/src/fast/shaders/opengl/default.shader.glsl +index 1fe86aa..ff45672 100644 +--- a/src/fast/shaders/opengl/default.shader.glsl ++++ b/src/fast/shaders/opengl/default.shader.glsl +@@ -134,14 +134,18 @@ + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++ @if(gles2) ++ return fract(sin(random) * 43758.5453); ++ @else + return fract(sin(random) * 143758.5453); ++ @end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -187,7 +191,9 @@ + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +index c431c34..35f66b2 100644 +--- a/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp ++++ b/src/libultraship/controller/controldevice/controller/mapping/ControllerDefaultMappings.cpp +@@ -80,6 +80,11 @@ void ControllerDefaultMappings::SetDefaultSDLButtonToButtonMappings( + { BTN_DDOWN, { SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, + { BTN_DLEFT, { SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, + { BTN_DRIGHT, { SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, ++#ifdef USE_GLES2 ++ { BTN_CDOWN, { SDL_CONTROLLER_BUTTON_Y } }, ++ { BTN_CLEFT, { SDL_CONTROLLER_BUTTON_X } }, ++ { BTN_CRIGHT, { SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, ++#endif + }); + } + +diff --git a/src/ship/audio/Audio.cpp b/src/ship/audio/Audio.cpp +index 8cc5b12..604a30c 100644 +--- a/src/ship/audio/Audio.cpp ++++ b/src/ship/audio/Audio.cpp +@@ -36,8 +36,11 @@ void Audio::InitAudioPlayer() { + + if (mAudioPlayer && !mAudioPlayer->Init()) { + // Failed to initialize system audio player. +- // Fallback to Null if the native system player does not work. +- SetCurrentAudioBackend(AudioBackend::NUL); ++ // Fallback to Null if the native system player does not work: for this run only, ++ // the saved backend stays and the next start tries it again ++ mAudioBackend = AudioBackend::NUL; ++ mAudioPlayer = std::make_shared(this->mAudioSettings); ++ mAudioPlayer->Init(); + } + } + +diff --git a/src/ship/debug/CrashHandler.cpp b/src/ship/debug/CrashHandler.cpp +index 39d76be..c4cec84 100644 +--- a/src/ship/debug/CrashHandler.cpp ++++ b/src/ship/debug/CrashHandler.cpp +@@ -2,6 +2,9 @@ + #include "ship/utils/StringHelper.h" + #include "ship/debug/CrashHandler.h" + #include "ship/Context.h" ++#ifndef _WIN32 ++#include // _exit ++#endif + + #ifdef _WIN32 + #include +@@ -199,11 +202,13 @@ static void ErrorHandler(int sig, siginfo_t* sigInfo, void* data) { + + Context::GetRawInstance()->GetLogger()->flush(); + spdlog::shutdown(); +- exit(1); ++ _exit(128 + sig); + } + + static void ShutdownHandler(int sig, siginfo_t* sigInfo, void* data) { +- exit(1); ++ SDL_Event event; ++ event.type = SDL_QUIT; ++ SDL_PushEvent(&event); + } + + #elif _WIN32 diff --git a/ports/spaghettikart/spaghettikart.patch b/ports/spaghettikart/spaghettikart.patch new file mode 100644 index 0000000..d0dfdbf --- /dev/null +++ b/ports/spaghettikart/spaghettikart.patch @@ -0,0 +1,215 @@ +diff --git a/assets/shaders/opengl/default.shader.glsl b/assets/shaders/opengl/default.shader.glsl +index 5e0f93f..046207d 100644 +--- a/assets/shaders/opengl/default.shader.glsl ++++ b/assets/shaders/opengl/default.shader.glsl +@@ -130,14 +130,18 @@ + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++ @if(gles2) ++ return fract(sin(random) * 43758.5453); ++ @else + return fract(sin(random) * 143758.5453); ++ @end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -183,7 +187,9 @@ + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/cmake/SetFlags.cmake b/cmake/SetFlags.cmake +index 44bfce2..eeef3d2 100644 +--- a/cmake/SetFlags.cmake ++++ b/cmake/SetFlags.cmake +@@ -116,6 +116,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE + NDEBUG + VERSION_US=1 + "$<$:USE_OPENGLES>" ++ "$<$:USE_GLES2>" + ENABLE_RUMBLE=1 + F3DEX_GBI=1 + _LANGUAGE_C +diff --git a/src/engine/tracks/LuigiRaceway.cpp b/src/engine/tracks/LuigiRaceway.cpp +index 70127e1..de5cf79 100644 +--- a/src/engine/tracks/LuigiRaceway.cpp ++++ b/src/engine/tracks/LuigiRaceway.cpp +@@ -256,12 +256,32 @@ void LuigiRaceway::Draw(ScreenContext* arg0) { + // Invalidate Jumbotron textures so they update each frame + // This could be more efficient if we exposed the non-opcode based invalidation to be called + // inside copy_framebuffers_port ++#ifdef USE_GLES2 ++ static const void* sJumbotronBlankedFor; ++ if (sJumbotronBlankedFor != this) { ++ for (const char* tex : { gTexture68272C, gTexture682928, gTexture682B24, gTexture682D20, gTexture682F1C, gTexture683118 }) { ++ u8* px = (u8*) LOAD_ASSET_RAW(tex); ++ for (int k = 0; k < 64 * 32; k++) { // RGBA16, big-endian: opaque black ++ px[2 * k] = 0x00; ++ px[2 * k + 1] = 0x01; ++ } ++ } ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture68272C); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682928); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682B24); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682D20); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682F1C); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture683118); ++ sJumbotronBlankedFor = this; ++ } ++#else + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture68272C); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682928); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682B24); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682D20); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682F1C); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture683118); ++#endif + + if (func_80290C20(arg0->camera) == 1) { + gDPSetCombineMode(gDisplayListHead++, G_CC_SHADE, G_CC_SHADE); +@@ -304,8 +324,10 @@ void LuigiRaceway::Draw(ScreenContext* arg0) { + * The jumbo television screen used to be split into six sections to fit into the n64's texture size + * restrictions It isn't split into six sections anymore + */ ++#ifndef USE_GLES2 + CopyJumbotron(D_800DC5DC, D_800DC5E0, currentScreenSection, + (u16*) gPortFramebuffers[prevFrame]); ++#endif + } + } + +diff --git a/src/engine/tracks/ToadsTurnpike.cpp b/src/engine/tracks/ToadsTurnpike.cpp +index 3952927..d26b726 100644 +--- a/src/engine/tracks/ToadsTurnpike.cpp ++++ b/src/engine/tracks/ToadsTurnpike.cpp +@@ -105,7 +105,7 @@ ToadsTurnpike::ToadsTurnpike() { + mCloudType = CloudType::STARS; + + FVector finish; +- finish.x = -38.0f * xOrientation; ++ finish.x = (gIsMirrorMode != 0) ? 38.0f : -38.0f; + finish.y = (f32) (0 - 15); + finish.z = 16; + +diff --git a/src/engine/tracks/WarioStadium.cpp b/src/engine/tracks/WarioStadium.cpp +index 99e672e..af040fc 100644 +--- a/src/engine/tracks/WarioStadium.cpp ++++ b/src/engine/tracks/WarioStadium.cpp +@@ -236,12 +236,32 @@ void WarioStadium::Draw(ScreenContext* arg0) { + // Invalidate Jumbotron textures so they update each frame + // This could be more efficient if we exposed the non-opcode based invalidation to be called + // inside copy_framebuffers_port ++#ifdef USE_GLES2 ++ static const void* sJumbotronBlankedFor; ++ if (sJumbotronBlankedFor != this) { ++ for (const char* tex : { gTexture68272C, gTexture682928, gTexture682B24, gTexture682D20, gTexture682F1C, gTexture683118 }) { ++ u8* px = (u8*) LOAD_ASSET_RAW(tex); ++ for (int k = 0; k < 64 * 32; k++) { // RGBA16, big-endian: opaque black ++ px[2 * k] = 0x00; ++ px[2 * k + 1] = 0x01; ++ } ++ } ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture68272C); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682928); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682B24); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682D20); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682F1C); ++ gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture683118); ++ sJumbotronBlankedFor = this; ++ } ++#else + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture68272C); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682928); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682B24); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682D20); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture682F1C); + gSPInvalidateTexCache(gDisplayListHead++, (uintptr_t) gTexture683118); ++#endif + + if (func_80290C20(arg0->camera) == 1) { + gDPSetCombineMode(gDisplayListHead++, G_CC_SHADE, G_CC_SHADE); +@@ -281,7 +301,9 @@ void WarioStadium::Draw(ScreenContext* arg0) { + * The jumbo television screen used to be split into six sections to fit into the n64's texture size + * restrictions It isn't split into six sections anymore + */ ++#ifndef USE_GLES2 + CopyJumbotron(D_800DC5DC, D_800DC5E0, currentScreenSection, (u16*) gPortFramebuffers[prevFrame]); ++#endif + } + } + +diff --git a/src/main.c b/src/main.c +index 1bdbee6..12896a5 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -804,7 +804,9 @@ void race_logic_loop(void) { + // Copies after all the main rendering is complete. This is accurate to hardware. + // This means things like jumbotron will have the pause menu rendered in it. + // To avoid that, this line can be moved above func_800591B4. ++#ifndef USE_GLES2 + FB_WriteFramebufferSliceToCPU(&gDisplayListHead, gPortFramebuffers[sRenderingFramebuffer], true); ++#endif + gDPFullSync(gDisplayListHead++); + gSPEndDisplayList(gDisplayListHead++); + +diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp +index 23036ad..d03a40e 100644 +--- a/src/port/Engine.cpp ++++ b/src/port/Engine.cpp +@@ -96,6 +96,13 @@ GameEngine::GameEngine() { + this->context->InitConfiguration(); // without this line InitConsoleVariables fails at Config::Reload() + this->context->InitConsoleVariables(); // without this line the controldeck constructor failes in + // ShipDeviceIndexMappingManager::UpdateControllerNamesFromConfig() ++#ifdef USE_GLES2 ++ if (!CVarGetInteger("gGles2DefaultsApplied", 0)) { ++ CVarSetInteger("gTextureFilter", 1); // FILTER_LINEAR ++ CVarSetInteger("gGles2DefaultsApplied", 1); ++ CVarSave(); ++ } ++#endif + + auto defaultMappings = std::make_shared( + // KeyboardKeyToButtonMappings +@@ -127,10 +134,17 @@ GameEngine::GameEngine() { + // SDLButtonToButtonMappings + std::unordered_map>{ + { BTN_A, { SDL_CONTROLLER_BUTTON_A } }, ++#ifdef USE_GLES2 ++ { BTN_B, { SDL_CONTROLLER_BUTTON_B } }, ++ { BTN_Z, { SDL_CONTROLLER_BUTTON_X } }, ++ { BTN_START, { SDL_CONTROLLER_BUTTON_START } }, ++ { BTN_CLEFT, { SDL_CONTROLLER_BUTTON_Y } }, ++#else + { BTN_B, { SDL_CONTROLLER_BUTTON_X } }, + { BTN_START, { SDL_CONTROLLER_BUTTON_START } }, + { BTN_CLEFT, { SDL_CONTROLLER_BUTTON_Y } }, + { BTN_CDOWN, { SDL_CONTROLLER_BUTTON_B } }, ++#endif + { BTN_DUP, { SDL_CONTROLLER_BUTTON_DPAD_UP } }, + { BTN_DDOWN, { SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, + { BTN_DLEFT, { SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, +@@ -145,6 +159,9 @@ GameEngine::GameEngine() { + { BTN_R, { { SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 1 } } }, + { BTN_Z, { { SDL_CONTROLLER_AXIS_TRIGGERLEFT, 1 } } }, + { BTN_CUP, { { SDL_CONTROLLER_AXIS_RIGHTY, -1 } } }, ++#ifdef USE_GLES2 ++ { BTN_CDOWN, { { SDL_CONTROLLER_AXIS_RIGHTY, 1 } } }, ++#endif + { BTN_CRIGHT, { { SDL_CONTROLLER_AXIS_RIGHTX, 1 } } } + }, + // SDLAxisDirectionToAxisDirectionMappings - use built-in LUS defaults diff --git a/ports/starship/README.md b/ports/starship/README.md new file mode 100644 index 0000000..ef3bc5b --- /dev/null +++ b/ports/starship/README.md @@ -0,0 +1,71 @@ +# Star Fox 64 (Starship) on the RetroStone2 + +[Starship](https://github.com/HarbourMasters/Starship) is the PC port of the Star Fox 64 decompilation, built on +HarbourMasters' [libultraship](https://github.com/Kenix3/libultraship). libultraship draws with OpenGL 3.0 or +OpenGL ES 3.0; the RetroStone2's Mali-400 only has **OpenGL ES 2.0**. This folder adds an OpenGL ES 2.0 mode to +libultraship (`USE_GLES2`) and builds Starship for the console as a native game (docs/native-games.md). + +**Nothing from the game is in this repository, the OS images or the updates.** As for Super Mario 64, the game's +data comes from **your own ROM** on your PC (Star Fox 64 US 1.1, `.z64`, SHA-1 +`09f0d105f476b00efa5303a3ebc42e60a7753b7a`) and stays yours. + +## Status + +| Step | | +|---|---| +| GLES2 renderer (`libultraship-gles2.patch`) | written | +| Cross-build for the RetroStone2 (32-bit ARM) | **builds** (Starship `3322643f`, libultraship `eaaf9d0`, Torch `cd92cc0`) | +| Game data from the ROM (`sf64.o2r`) and the port archive (`starship.o2r`) | **built** on a PC | +| Libraries in the OS (Ogg/Vorbis, libzip, tinyxml2, spdlog, fmt) | in the RetroStone2 configs | +| The built-in controls | `rsos-gamepad`: the three devices as one standard SDL gamepad, started by the launch script | +| Build script for the SD card folder | `build-starship.sh` | +| Run on a RetroStone2 | **runs** (RetroStone2 Pro); some slowdown in busy scenes | + +Starship's libultraship still uses **SDL2** (only libultraship's newest code moved to SDL3), so no SDL3 is needed. + +## What the GLES2 mode changes + +* **Context**: asks SDL for an OpenGL ES 2.0 context (Mesa's lima would otherwise give desktop OpenGL 2.1). +* **Shaders**: GLSL ES 1.00 (`attribute`/`varying`, `texture2D`, `gl_FragColor`, `precision mediump float`); + `textureSize()` replaced by the texture's size for masks; `mix(..., bvec3)` written with `vec3()`; the noise + constant kept below 65504 (Mali-400 fragment shaders have 16-bit floats only). Both copies of the shader + templates: libultraship's and Starship's own `port/shaders` (packed into `starship.o2r`). +* **Framebuffers**: unsized texture formats, packed depth+stencil when the driver has + `GL_OES_packed_depth_stencil` (else 16-bit depth), clamp-to-edge on the screen-sized colour textures (GLES2 has + no repeat on non-power-of-two textures), no multisampling, no vertex array object. +* **What GLES2 cannot do**: framebuffer blits (copies go through `glCopyTexSubImage2D`, unscaled only) and depth + reads (reported as "far"). Starship uses neither; only libultraship's debug window does. +* A shader that fails to compile now prints the compiler's message and the shader before aborting. + +## Other fixes in the patches + +* **First start** (GLES2 builds): the cheap settings once, still changeable in the menu: the game's own frame + rate (no interpolated frames), 320x240 rendering, bilinear filtering, no MSAA, no "press F1" hint. +* **Crash with frame interpolation off** ("Original", 20 FPS): the game turned recording back on in the middle + of a frame and crashed in `FrameInterpolation_RecordMatrixPop`; recording now only resumes in a frame that + started it. +* **Handheld controls**: Select + Start quits at once; Start skips any level intro (`gSkipAnyIntro`). +* **Crash handler**: `_exit()` instead of `exit()` in the signal handlers, so a crash or a quit ends the game at + once (exit status 128 + signal) instead of hanging on a frozen picture. + +## Build + +```sh +ports/starship/build-starship.sh ~/"Star Fox 64 (U) (V1.1).z64" ~/my-ports +``` + +It clones Starship at the fixed commit into `~/rsos/starship-work`, applies `starship.patch` and +`libultraship-gles2.patch`, checks that the ROM is one Starship supports, builds the game for the console +(`USE_GLES2`, the RetroStoneOS cross compiler), builds Torch for the PC and makes `sf64.o2r` from the ROM and +`starship.o2r`, then writes: + +``` +~/my-ports/roms/native/Star Fox 64.sh +~/my-ports/roms/native/starship/ Starship, sf64.o2r, starship.o2r, gamecontrollerdb.txt +``` + +Copy the `roms` folder onto the SD card's **RETROSTONE** partition; in the menu: **Native games > Star Fox 64**. +Settings and saves stay in `roms/native/starship/` (`SHIP_HOME`). **Select + Start** quits at once, as in the +emulators. **Start** skips a level's opening scene, also on a planet not cleared yet (the N64 game allows it only +after a clear; the `gSkipAnyIntro` setting at 0 in `starship.cfg.json` brings that back). +The console needs a RetroStoneOS with Native games, the port libraries and `rsos-gamepad`. diff --git a/ports/starship/Star Fox 64.sh b/ports/starship/Star Fox 64.sh new file mode 100644 index 0000000..ba9ecf6 --- /dev/null +++ b/ports/starship/Star Fox 64.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Star Fox 64 (Starship), built from your own ROM by ports/starship/build-starship.sh. +# The game data (sf64.o2r), settings and saves stay in starship/. +cd starship || exit 1 +export SHIP_HOME="$PWD" +# The built-in controls as one standard gamepad (rsos-gamepad); SDL then +# ignores the raw button devices. It runs in the +# game's process group: the menu stops it with the game. +if [ -x /usr/bin/rsos-gamepad ]; then + SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" + export SDL_GAMECONTROLLERCONFIG + export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 + /usr/bin/rsos-gamepad & + usleep 300000 +fi +if [ -x ./Starship ]; then + exec ./Starship +fi +exec /lib/ld-linux-armhf.so.3 ./Starship diff --git a/ports/starship/build-starship.sh b/ports/starship/build-starship.sh new file mode 100755 index 0000000..662eaa8 --- /dev/null +++ b/ports/starship/build-starship.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Builds Star Fox 64 (Starship) for RetroStoneOS from YOUR OWN ROM, on the PC +# (Linux/WSL), as a native game for the SD card (docs/native-games.md). +# +# ports/starship/build-starship.sh [output folder] +# +# Needs a built RetroStoneOS tree for the RetroStone2 with SDL2 and the port +# libraries (the RetroStone2 configs): ~/rsos/output, or RSOS_OUTPUT=; cmake, +# ninja and a C++ compiler for the PC (Torch, the asset extractor, runs here). +# Starship is cloned at a fixed commit into ~/rsos/starship-work +# (RSOS_STARSHIP_WORK) and patched (starship.patch, libultraship-gles2.patch). +# +# The result contains the game's data, taken from your ROM: keep it for +# yourself. Never put it in a release, an update or a repository. +# +# Output (default ./starship-retrostone2): roms/native/Star Fox 64.sh and +# roms/native/starship/. Copy the "roms" folder onto the SD card's +# RETROSTONE partition (it merges with the roms folder there). +set -euo pipefail + +REPO=https://github.com/HarbourMasters/Starship.git +COMMIT=3322643f0bbf80d2f8f7e1c0d2c4c578515f6c90 # libultraship eaaf9d0, Torch cd92cc0 + +here=$(cd "$(dirname "$0")" && pwd) +rom=${1:-} +out=${2:-$PWD/starship-retrostone2} +O=${RSOS_OUTPUT:-$HOME/rsos/output} +W=${RSOS_STARSHIP_WORK:-$HOME/rsos/starship-work} + +die() { echo "build-starship: $*" >&2; exit 1; } + +[ -n "$rom" ] || die "usage: $0 [output folder]" +[ -f "$rom" ] || die "$rom: no such file" +rom=$(cd "$(dirname "$rom")" && pwd)/$(basename "$rom") +case "$out" in /*) ;; *) out=$PWD/$out ;; esac +[ -x "$O/host/bin/arm-linux-gcc" ] || die "no cross compiler in $O/host/bin (build RetroStoneOS first)" +for lib in libSDL2.so libzip.so libtinyxml2.so libspdlog.so libvorbisfile.so; do + [ -e "$O/staging/usr/lib/$lib" ] || die "$lib is not in $O/staging (build RetroStoneOS with the RetroStone2 configs)" +done +TOOLCHAIN=$O/host/share/buildroot/toolchainfile.cmake + +if [ ! -d "$W/.git" ]; then + git clone -q "$REPO" "$W" +fi +cd "$W" +git fetch -q origin 2> /dev/null || true +# a clean tree at the fixed commit, then our changes (the build folder, the +# ROM and the .o2r files are ignored by git and stay) +git checkout -q -f "$COMMIT" +git submodule update -q --init --recursive --force +git -C libultraship checkout -q -f . +git apply "$here/starship.patch" +git -C libultraship apply "$here/libultraship-gles2.patch" + +# the ROM: a supported Star Fox 64 (its SHA-1 is a key of config.yml) +sum=$(sha1sum "$rom" | cut -d' ' -f1) +grep -q "^$sum:" config.yml || die "$rom is not a Star Fox 64 ROM that Starship supports, in .z64 order (SHA-1 $sum)" +cp "$rom" baserom.z64 + +# the game for the console, then Torch for this PC and the two archives +cmake -S . -B build-gles2 -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_OPENGLES=ON -DUSE_GLES2=ON \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" +cmake --build build-gles2 --target Starship +cmake --build build-gles2 --target ExtractAssets +cmake --build build-gles2 --target GeneratePortO2R + +file build-gles2/Starship | grep -q 'ARM' || die "build-gles2/Starship is not an ARM program" +mkdir -p "$out/roms/native/starship" +"$O/host/bin/arm-linux-strip" -o "$out/roms/native/starship/Starship" build-gles2/Starship +cp build-gles2/sf64.o2r build-gles2/starship.o2r "$out/roms/native/starship/" +[ -f build-gles2/gamecontrollerdb.txt ] && cp build-gles2/gamecontrollerdb.txt "$out/roms/native/starship/" +install -m 0644 "$here/Star Fox 64.sh" "$out/roms/native/Star Fox 64.sh" +echo +echo "Done: $out/roms" +echo "Copy that \"roms\" folder onto the SD card's RETROSTONE partition, then in the menu:" +echo "Native games > Star Fox 64. Select + Start quits." diff --git a/ports/starship/libultraship-gles2.patch b/ports/starship/libultraship-gles2.patch new file mode 100644 index 0000000..9af49eb --- /dev/null +++ b/ports/starship/libultraship-gles2.patch @@ -0,0 +1,426 @@ +diff --git a/cmake/dependencies/linux.cmake b/cmake/dependencies/linux.cmake +index 02301f13..8e231e1b 100644 +--- a/cmake/dependencies/linux.cmake ++++ b/cmake/dependencies/linux.cmake +@@ -2,7 +2,10 @@ + find_package(SDL2 REQUIRED) + target_link_libraries(ImGui PUBLIC SDL2::SDL2) + +-if (USE_OPENGLES) ++if (USE_GLES2) ++ target_link_libraries(ImGui PUBLIC GLESv2) ++ add_compile_definitions(IMGUI_IMPL_OPENGL_ES2) ++elseif (USE_OPENGLES) + target_link_libraries(ImGui PUBLIC ${OPENGL_GLESv2_LIBRARY}) + add_compile_definitions(IMGUI_IMPL_OPENGL_ES3) + else() +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index db92a29a..1675a88e 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -8,6 +8,11 @@ set_property(TARGET libultraship PROPERTY C_STANDARD 11) + + set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../include) + option(USE_OPENGLES "Enable GLES3" OFF) ++# OpenGL ES 2.0 (Mali-400 and other GLES2-only GPUs); implies USE_OPENGLES ++option(USE_GLES2 "Enable GLES2" OFF) ++if (USE_GLES2) ++ set(USE_OPENGLES ON) ++endif() + option(GFX_DEBUG_DISASSEMBLER "Enable libgfxd" OFF) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +@@ -286,6 +291,7 @@ else() + $<$:SPDLOG_ACTIVE_LEVEL=0> + $<$>:SPDLOG_ACTIVE_LEVEL=1> + $<$:USE_OPENGLES> ++ $<$:USE_GLES2> + $<$:GFX_DEBUG_DISASSEMBLER> + ) + endif() +diff --git a/src/debug/CrashHandler.cpp b/src/debug/CrashHandler.cpp +index 5f2256ac..df5179a7 100644 +--- a/src/debug/CrashHandler.cpp ++++ b/src/debug/CrashHandler.cpp +@@ -2,6 +2,9 @@ + #include "utils/StringHelper.h" + #include "CrashHandler.h" + #include "Context.h" ++#ifndef _WIN32 ++#include // _exit ++#endif + + #ifdef _WIN32 + #include +@@ -199,11 +202,11 @@ static void ErrorHandler(int sig, siginfo_t* sigInfo, void* data) { + + Context::GetInstance()->GetLogger()->flush(); + spdlog::shutdown(); +- exit(1); ++ _exit(128 + sig); + } + + static void ShutdownHandler(int sig, siginfo_t* sigInfo, void* data) { +- exit(1); ++ _exit(128 + sig); + } + + #elif _WIN32 +diff --git a/src/graphic/Fast3D/backends/gfx_opengl.cpp b/src/graphic/Fast3D/backends/gfx_opengl.cpp +index 097e2e86..4074598d 100644 +--- a/src/graphic/Fast3D/backends/gfx_opengl.cpp ++++ b/src/graphic/Fast3D/backends/gfx_opengl.cpp +@@ -27,6 +27,49 @@ + #include "../interpreter.h" + #include + ++#include ++ ++#ifdef USE_GLES2 ++/* ++ * OpenGL ES 2.0: no sized texture formats, no multisampling, no blits and no ++ * depth reads. Multisampling stays off (mMaxMsaaLevel 1), so the multisample ++ * calls below are never reached; CopyFramebuffer and GetPixelDepth have their ++ * own GLES2 versions. Depth: packed depth+stencil when the driver has ++ * GL_OES_packed_depth_stencil, else a 16-bit depth buffer. ++ */ ++#define GL_RGB8 GL_RGB ++#define GL_RGBA8 GL_RGBA ++#define GL_DEPTH24_STENCIL8 GL_DEPTH24_STENCIL8_OES ++#define GL_DEPTH_STENCIL GL_DEPTH_STENCIL_OES ++#define GL_UNSIGNED_INT_24_8 GL_UNSIGNED_INT_24_8_OES ++#define GL_READ_FRAMEBUFFER GL_FRAMEBUFFER ++#define GL_DRAW_FRAMEBUFFER GL_FRAMEBUFFER ++#define glBlitFramebuffer(...) ((void)0) ++#define glReadBuffer(b) ((void)0) ++#define glRenderbufferStorageMultisample(target, samples, format, w, h) glRenderbufferStorage(target, format, w, h) ++static bool sPackedDepthStencil = true; ++#endif ++ ++namespace Fast { ++static void RenderbufferDepthStorage(GLsizei width, GLsizei height) { ++#ifdef USE_GLES2 ++ glRenderbufferStorage(GL_RENDERBUFFER, sPackedDepthStencil ? GL_DEPTH24_STENCIL8_OES : GL_DEPTH_COMPONENT16, ++ width, height); ++#else ++ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++#endif ++} ++ ++static void FramebufferDepthAttach(GLuint rb) { ++#ifdef USE_GLES2 ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb); ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, sPackedDepthStencil ? rb : 0); ++#else ++ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb); ++#endif ++} ++} // namespace Fast ++ + namespace Fast { + int GfxRenderingAPIOGL::GetMaxTextureSize() { + GLint max_texture_size; +@@ -273,13 +316,23 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "opengles", false }, + { "core_opengl", true }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100\nprecision mediump float;" }, ++ { "attr", "varying" }, ++ { "opengles", false }, ++ { "core_opengl", false }, ++ { "gles2", true }, ++ { "texture", "texture2D" }, ++ { "vOutColor", "gl_FragColor" }, + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es\nprecision mediump float;" }, + { "attr", "in" }, + { "opengles", true }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture" }, + { "vOutColor", "vOutColor" }, + #else +@@ -287,6 +340,7 @@ std::string GfxRenderingAPIOGL::BuildFsShader(const CCFeatures& cc_features) { + { "attr", "varying" }, + { "opengles", false }, + { "core_opengl", false }, ++ { "gles2", false }, + { "texture", "texture2D" }, + { "vOutColor", "gl_FragColor" }, + #endif +@@ -336,6 +390,11 @@ static std::string BuildVsShader(const CCFeatures& cc_features) { + { "attr", "in" }, + { "out", "out" }, + { "opengles", false } ++#elif defined(USE_GLES2) ++ { "GLSL_VERSION", "#version 100" }, ++ { "attr", "attribute" }, ++ { "out", "varying" }, ++ { "opengles", false } + #elif defined(USE_OPENGLES) + { "GLSL_VERSION", "#version 300 es" }, + { "attr", "in" }, +@@ -389,9 +448,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- // fprintf(stderr, "Vertex shader compilation failed\n"); +- glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]); +- // fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(vertex_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Vertex shader compilation failed:\n%s\n%s\n", error_log, vs_buf.c_str()); + abort(); + } + +@@ -403,9 +461,8 @@ ShaderProgram* GfxRenderingAPIOGL::CreateAndLoadNewShader(uint64_t shader_id0, u + GLint max_length = 0; + glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &max_length); + char error_log[1024]; +- fprintf(stderr, "Fragment shader compilation failed\n"); +- glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]); +- fprintf(stderr, "%s\n", &error_log[0]); ++ glGetShaderInfoLog(fragment_shader, sizeof(error_log), nullptr, error_log); ++ fprintf(stderr, "Fragment shader compilation failed:\n%s\n%s\n", error_log, fs_buf.c_str()); + abort(); + } + +@@ -541,7 +598,9 @@ void GfxRenderingAPIOGL::UploadTexture(const uint8_t* rgba32_buf, uint32_t width + textures[mCurrentTextureIds[mCurrentTile]].height = height; + } + +-#ifdef USE_OPENGLES ++#ifdef USE_GLES2 ++#define GL_MIRROR_CLAMP_TO_EDGE GL_MIRRORED_REPEAT ++#elif defined(USE_OPENGLES) + #define GL_MIRROR_CLAMP_TO_EDGE 0x8743 + #endif + +@@ -659,7 +718,16 @@ void GfxRenderingAPIOGL::Init() { + glGenBuffers(1, &mOpenglVbo); + glBindBuffer(GL_ARRAY_BUFFER, mOpenglVbo); + +-#if defined(__APPLE__) || defined(USE_OPENGLES) ++#ifdef USE_GLES2 ++ { ++ const char* ext = (const char*)glGetString(GL_EXTENSIONS); ++ ++ sPackedDepthStencil = ext != nullptr && strstr(ext, "GL_OES_packed_depth_stencil") != nullptr; ++ SPDLOG_INFO("OpenGL ES 2.0: {} ({}), depth buffer {}", (const char*)glGetString(GL_RENDERER), ++ (const char*)glGetString(GL_VERSION), sPackedDepthStencil ? "24-bit + stencil" : "16-bit"); ++ } ++#endif ++#if defined(__APPLE__) || (defined(USE_OPENGLES) && !defined(USE_GLES2)) + glGenVertexArrays(1, &mOpenglVao); + glBindVertexArray(mOpenglVao); + #endif +@@ -674,17 +742,21 @@ void GfxRenderingAPIOGL::Init() { + + glGenRenderbuffers(1, &mPixelDepthRb); + glBindRenderbuffer(GL_RENDERBUFFER, mPixelDepthRb); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + glGenFramebuffers(1, &mPixelDepthFb); + glBindFramebuffer(GL_FRAMEBUFFER, mPixelDepthFb); +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, mPixelDepthRb); ++ FramebufferDepthAttach(mPixelDepthRb); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + mPixelDepthRbSize = 1; + ++#ifdef USE_GLES2 ++ mMaxMsaaLevel = 1; ++#else + glGetIntegerv(GL_MAX_SAMPLES, &mMaxMsaaLevel); ++#endif + } + + void GfxRenderingAPIOGL::OnResize() { +@@ -708,6 +780,10 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ++#ifdef USE_GLES2 ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ++#endif + glBindTexture(GL_TEXTURE_2D, 0); + + GLuint clrbufMsaa; +@@ -716,7 +792,7 @@ int GfxRenderingAPIOGL::CreateFramebuffer() { + GLuint rbo; + glGenRenderbuffers(1, &rbo); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1); ++ RenderbufferDepthStorage(1, 1); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + GLuint fbo; +@@ -763,7 +839,7 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + (fb.width != width || fb.height != height || fb.msaa_level != msaa_level || !fb.has_depth_buffer)) { + glBindRenderbuffer(GL_RENDERBUFFER, fb.rbo); + if (msaa_level <= 1) { +- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); ++ RenderbufferDepthStorage(width, height); + } else { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_DEPTH24_STENCIL8, width, height); + } +@@ -771,9 +847,9 @@ void GfxRenderingAPIOGL::UpdateFramebufferParameters(int fb_id, uint32_t width, + } + + if (!fb.has_depth_buffer && has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb.rbo); ++ FramebufferDepthAttach(fb.rbo); + } else if (fb.has_depth_buffer && !has_depth_buffer) { +- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0); ++ FramebufferDepthAttach(0); + } + } + +@@ -838,6 +914,34 @@ void GfxRenderingAPIOGL::CopyFramebuffer(int fb_dst_id, int fb_src_id, int srcX0 + FramebufferOGL src = mFrameBuffers[fb_src_id]; + const FramebufferOGL& dst = mFrameBuffers[fb_dst_id]; + ++#ifdef USE_GLES2 ++ // No blits: copy into the destination's colour texture, unscaled and in the ++ // same orientation only (G_COPYFB effects; Starship does not use them). ++ if (fb_dst_id == 0 || srcX1 - srcX0 != dstX1 - dstX0 || srcY1 - srcY0 != dstY1 - dstY0 || ++ src.invertY != dst.invertY) { ++ static bool warned; ++ if (!warned) { ++ SPDLOG_WARN("GLES2: scaled, flipped or on-screen framebuffer copies are not supported"); ++ warned = true; ++ } ++ return; ++ } ++ // OpenGL's origin is the bottom left, as in the blit path below ++ if (!src.invertY) { ++ int h = srcY1 - srcY0; ++ srcY1 = src.height - srcY0; ++ srcY0 = srcY1 - h; ++ } ++ glDisable(GL_SCISSOR_TEST); ++ glBindFramebuffer(GL_FRAMEBUFFER, src.fbo); ++ glActiveTexture(GL_TEXTURE0); ++ glBindTexture(GL_TEXTURE_2D, dst.clrbuf); ++ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, dstX0, dstY0, srcX0, srcY0, srcX1 - srcX0, srcY1 - srcY0); ++ glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[mCurrentFrameBuffer].fbo); ++ glEnable(GL_SCISSOR_TEST); ++ return; ++#endif ++ + // Adjust y values for non-inverted source frame buffers because opengl uses bottom left for origin + if (!src.invertY) { + int temp = srcY1 - srcY0; +@@ -909,6 +1013,14 @@ std::unordered_map, uint16_t, hash_pair_ff> + GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set>& coordinates) { + std::unordered_map, uint16_t, hash_pair_ff> res; + ++#ifdef USE_GLES2 ++ // GLES2 cannot read the depth buffer: every point is reported at the far plane ++ for (const auto& coord : coordinates) { ++ res.emplace(coord, 0xFFFC); ++ } ++ return res; ++#endif ++ + FramebufferOGL& fb = mFrameBuffers[fb_id]; + + // When looking up one value and the framebuffer is single-sampled, we can read pixels directly +@@ -930,8 +1042,8 @@ GfxRenderingAPIOGL::GetPixelDepth(int fb_id, const std::set + #include ++#elif defined(USE_GLES2) ++#include ++#include ++#include + #elif USE_OPENGLES + #include + #include +diff --git a/src/graphic/Fast3D/backends/gfx_sdl2.cpp b/src/graphic/Fast3D/backends/gfx_sdl2.cpp +index 1387e280..6a62bb8b 100644 +--- a/src/graphic/Fast3D/backends/gfx_sdl2.cpp ++++ b/src/graphic/Fast3D/backends/gfx_sdl2.cpp +@@ -343,6 +343,13 @@ void GfxWindowBackendSDL2::Init(const char* gameName, const char* gfxApiName, bo + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); + #endif ++#if defined(USE_GLES2) ++ // an OpenGL ES 2.0 context (without it, Mesa drivers such as lima give ++ // desktop OpenGL 2.1, whose GLSL 1.20 compiles none of our shaders) ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); ++ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); ++#endif + + #ifdef _WIN32 + // Use high-resolution mTimer by default on Windows 10 (so that NtSetTimerResolution (...) hacks are not needed) +diff --git a/src/graphic/Fast3D/shaders/opengl/default.shader.fs b/src/graphic/Fast3D/shaders/opengl/default.shader.fs +index 404caae6..f9b5dd43 100644 +--- a/src/graphic/Fast3D/shaders/opengl/default.shader.fs ++++ b/src/graphic/Fast3D/shaders/opengl/default.shader.fs +@@ -53,14 +53,18 @@ uniform int texture_filtering[2]; + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++@if(gles2) ++ return fract(sin(random) * 43758.5453); ++@else + return fract(sin(random) * 143758.5453); ++@end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -106,7 +110,9 @@ void main() { + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/src/window/gui/Gui.cpp b/src/window/gui/Gui.cpp +index 953d4514..9df16003 100644 +--- a/src/window/gui/Gui.cpp ++++ b/src/window/gui/Gui.cpp +@@ -220,6 +220,8 @@ void Gui::ImGuiBackendInit() { + case WindowBackend::FAST3D_SDL_OPENGL: + #ifdef __APPLE__ + ImGui_ImplOpenGL3_Init("#version 410 core"); ++#elif defined(USE_GLES2) ++ ImGui_ImplOpenGL3_Init("#version 100"); + #elif USE_OPENGLES + ImGui_ImplOpenGL3_Init("#version 300 es"); + #else diff --git a/ports/starship/starship.patch b/ports/starship/starship.patch new file mode 100644 index 0000000..1029220 --- /dev/null +++ b/ports/starship/starship.patch @@ -0,0 +1,187 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 2b872a44..22a361f1 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -114,9 +114,13 @@ add_compile_definitions( + ) + + # Find necessary libraries +-if (UNIX AND NOT APPLE) ++if (UNIX AND NOT APPLE AND NOT USE_GLES2) + find_package(OpenGL REQUIRED) + endif() ++# the game knows it is a GLES2 build too (its first-start settings, Engine.cpp) ++if (USE_GLES2) ++add_compile_definitions(USE_GLES2) ++endif() + + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/properties.h.in ${CMAKE_CURRENT_SOURCE_DIR}/properties.h @ONLY) + +diff --git a/port/shaders/opengl/default.shader.fs b/port/shaders/opengl/default.shader.fs +index 404caae6..f9b5dd43 100644 +--- a/port/shaders/opengl/default.shader.fs ++++ b/port/shaders/opengl/default.shader.fs +@@ -53,14 +53,18 @@ uniform int texture_filtering[2]; + + float random(in vec3 value) { + float random = dot(sin(value), vec3(12.9898, 78.233, 37.719)); ++@if(gles2) ++ return fract(sin(random) * 43758.5453); ++@else + return fract(sin(random) * 143758.5453); ++@end + } + + vec4 fromLinear(vec4 linearRGB){ + bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linearRGB.rgb * vec3(12.92); +- return vec4(mix(higher, lower, cutoff), linearRGB.a); ++ return vec4(mix(higher, lower, vec3(cutoff)), linearRGB.a); + } + + vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) { +@@ -106,7 +110,9 @@ void main() { + vec4 texVal@{i} = hookTexture2D(@{i}, uTex@{i}, vTexCoordAdj@{i}, texSize@{i}); + + @if(o_masks[i]) +- @if(opengles) ++ @if(gles2) ++ vec2 maskSize@{i} = texSize@{i}; ++ @elseif(opengles) + vec2 maskSize@{i} = vec2(textureSize(uTexMask@{i}, 0)); + @else + vec2 maskSize@{i} = textureSize(uTexMask@{i}, 0); +diff --git a/src/engine/fox_play.c b/src/engine/fox_play.c +index c11c9dd6..d0e192a5 100644 +--- a/src/engine/fox_play.c ++++ b/src/engine/fox_play.c +@@ -7172,7 +7172,8 @@ void Play_Main(void) { + + if ((gControllerPress[gMainController].button & START_BUTTON) && + (gPlayer[0].state == PLAYERSTATE_LEVEL_INTRO) && +- gSaveFile.save.data.planet[sSaveSlotFromLevel[gCurrentLevel]].normalClear) { ++ (gSaveFile.save.data.planet[sSaveSlotFromLevel[gCurrentLevel]].normalClear || ++ CVarGetInteger("gSkipAnyIntro", 1))) { + Audio_ClearVoice(); + Audio_SetEnvSfxReverb(0); + Play_ClearObjectData(); +diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp +index bc0ccb1f..a2323027 100644 +--- a/src/port/Engine.cpp ++++ b/src/port/Engine.cpp +@@ -1,4 +1,7 @@ + #include "Engine.h" ++#ifdef USE_GLES2 ++#include // _exit ++#endif + #include "ui/ImguiUI.h" + #include "StringHelper.h" + +@@ -128,6 +131,17 @@ GameEngine::GameEngine() { + this->context->InitConfiguration(); // without this line InitConsoleVariables fails at Config::Reload() + this->context->InitConsoleVariables(); // without this line the controldeck constructor failes in + // ShipDeviceIndexMappingManager::UpdateControllerNamesFromConfig() ++#ifdef USE_GLES2 ++ if (!CVarGetInteger("gGles2DefaultsApplied", 0)) { ++ CVarSetInteger("gInterpolationFPS", 20); // the game's own frame rate: no extra frames ++ CVarSetInteger("gLowResMode", 1); // render at 320x240 (N64 mode), scaled to the screen ++ CVarSetInteger("gTextureFilter", 1); // FILTER_LINEAR: one hardware sample, not three-point ++ CVarSetInteger("gMSAAValue", 1); ++ CVarSetInteger("gHideMenuHint", 1); // no F1 key on a handheld ++ CVarSetInteger("gGles2DefaultsApplied", 1); ++ CVarSave(); ++ } ++#endif + + auto defaultMappings = std::make_shared( + // KeyboardKeyToButtonMappings - use built-in LUS defaults +@@ -318,6 +332,27 @@ void GameEngine::Destroy() { + + void GameEngine::StartFrame() const { + using Ship::KbScancode; ++#ifdef USE_GLES2 ++ { ++ static bool startWasDown; ++ bool back = false, start = false; ++ ++ for (int i = 0; i < SDL_NumJoysticks(); i++) { ++ SDL_GameController* pad = SDL_GameControllerFromInstanceID(SDL_JoystickGetDeviceInstanceID(i)); ++ ++ if (pad != nullptr) { ++ back |= SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_BACK) != 0; ++ start |= SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_START) != 0; ++ } ++ } ++ if (back && start && !startWasDown) { ++ SPDLOG_INFO("Select+Start: quitting"); ++ this->context->GetLogger()->flush(); ++ _exit(0); ++ } ++ startWasDown = start; ++ } ++#endif + const int32_t dwScancode = this->context->GetWindow()->GetLastScancode(); + this->context->GetWindow()->SetLastScancode(-1); + +diff --git a/src/port/interpolation/FrameInterpolation.cpp b/src/port/interpolation/FrameInterpolation.cpp +index 8d6c31ce..e0d55170 100644 +--- a/src/port/interpolation/FrameInterpolation.cpp ++++ b/src/port/interpolation/FrameInterpolation.cpp +@@ -164,6 +164,7 @@ struct Recording { + }; + + bool is_recording; ++bool frame_recording; // StartRecord turned recording on for this frame + vector current_path; + uint32_t camera_epoch; + uint32_t previous_camera_epoch; +@@ -416,7 +417,7 @@ bool camera_interpolation = true; + + void FrameInterpolation_ShouldInterpolateFrame(bool shouldInterpolate) { + // camera_interpolation = shouldInterpolate; +- is_recording = shouldInterpolate; ++ is_recording = shouldInterpolate && frame_recording; + } + + void FrameInterpolation_StartRecord(void) { +@@ -424,6 +425,7 @@ void FrameInterpolation_StartRecord(void) { + current_recording = {}; + current_path.clear(); + current_path.push_back(¤t_recording.root_path); ++ frame_recording = false; + if (!camera_interpolation) { + // default to interpolating + camera_interpolation = true; +@@ -432,6 +434,7 @@ void FrameInterpolation_StartRecord(void) { + } + if (GameEngine::GetInterpolationFPS() != 20) { + is_recording = true; ++ frame_recording = true; + } + } + +@@ -456,7 +459,9 @@ void FrameInterpolation_RecordCloseChild(void) { + if (has_inv_actor_mtx && current_path.size() == inv_actor_mtx_path_index) { + has_inv_actor_mtx = false; + } +- current_path.pop_back(); ++ if (current_path.size() > 1) { ++ current_path.pop_back(); ++ } + } + + void FrameInterpolation_DontInterpolateCamera(void) { +diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp +index 5874ac2a..c4af2d96 100644 +--- a/src/port/ui/ImguiUI.cpp ++++ b/src/port/ui/ImguiUI.cpp +@@ -45,7 +45,7 @@ void SetupGuiElements() { + mGameMenuBar = std::make_shared("gOpenMenuBar", CVarGetInteger("gOpenMenuBar", 0)); + gui->SetMenuBar(mGameMenuBar); + +- if (gui->GetMenuBar() && !gui->GetMenuBar()->IsVisible()) { ++ if (gui->GetMenuBar() && !gui->GetMenuBar()->IsVisible() && !CVarGetInteger("gHideMenuHint", 0)) { + #if defined(__SWITCH__) || defined(__WIIU__) + Notification::Emit({ .message = "Press - to access enhancements menu", .remainingTime = 10.0f }); + #else