From 702f1bb9ce791cad6cf03831c9657dc3821df36a Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:14:02 -0400 Subject: [PATCH 1/7] Native games: programs built for the console, started without an emulator The system "native" (Native games, roms/native): one launch script per game (.sh), run by /bin/sh in its own folder and its own process group (host_launch_native), with the display, sound and pads handed over as for a libretro game. HOME on /data (the root is read-only), SDL_VIDEODRIVER kmsdrm, SDL_AUDIODRIVER alsa, RSOS_GAME_DIR, RSOS_LANG; output to game.log. Select+Start held 1 s quits it (SIGTERM to the group, SIGKILL after HANG_KILL_MS); sleep stops the group and turns the backlight off; a power-off sends SIGTERM; the idle power-off waits while it runs (no save state); an error exit is shown. SDL2 (KMS/DRM, GLES2, ALSA) in the RetroStone2 configs. docs/native-games.md; check-frontend 2a-2c (the launch and environment, an error exit, the menu stopping: the group goes too). Co-Authored-By: Claude Opus 5.5 --- .../configs/retrostone2_defconfig | 4 + .../configs/retrostone2_release_defconfig | 4 + docs/native-games.md | 55 ++ frontend/Makefile | 32 ++ frontend/po/rsos.pot | 510 +++++++++--------- frontend/src/host/host.h | 12 + frontend/src/host/launch.c | 105 ++++ frontend/src/main.c | 167 +++++- frontend/src/ui/systems.c | 16 +- frontend/src/ui/systems.h | 3 + frontend/src/ui/ui.c | 4 +- 11 files changed, 660 insertions(+), 252 deletions(-) create mode 100644 docs/native-games.md diff --git a/buildroot-external/configs/retrostone2_defconfig b/buildroot-external/configs/retrostone2_defconfig index 3db2d23..f894ea3 100644 --- a/buildroot-external/configs/retrostone2_defconfig +++ b/buildroot-external/configs/retrostone2_defconfig @@ -77,6 +77,10 @@ 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 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..db5a945 100644 --- a/buildroot-external/configs/retrostone2_release_defconfig +++ b/buildroot-external/configs/retrostone2_release_defconfig @@ -77,6 +77,10 @@ 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 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..3a92583 --- /dev/null +++ b/docs/native-games.md @@ -0,0 +1,55 @@ +# 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**. + +## 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 sees only one of them. Read every `/dev/input/event*` device +whose name starts with "RetroStone2", or merge them. + +## 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/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; } From 32b163d70e4ca6933dfe2654dea43589958775b7 Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:12:21 -0400 Subject: [PATCH 2/7] Native games: USB import and network upload know the native folder check-transfer: sysmap.c lists every system of systems.c. Its alias is "nativegames", not "ports": RetroPie's port scripts start RetroPie's own emulators. Co-Authored-By: Claude Opus 5.5 --- frontend/src/transfer/sysmap.c | 2 ++ 1 file changed, 2 insertions(+) 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]))) From 4bd2608401656edc7384391a24f6c1f504f52084 Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:35:04 -0400 Subject: [PATCH 3/7] Native games: rsos-gamepad, the built-in controls as one SDL gamepad The RetroStone2's buttons, shoulder buttons and optional analog stick are three input devices, and many SDL games read one game controller only (SDL does not take the stick for a joystick at all). rsos-gamepad merges them into one standard uinput gamepad, "RetroStoneOS Gamepad": positional buttons, the D-pad as hat 0, the stick as the left stick (centred while not fitted). A game's launch script starts it in the game's process group and gives SDL its mapping (--print-mapping). The kernel gets CONFIG_INPUT_UINPUT. Co-Authored-By: Claude Opus 5.5 --- .../board/retrostone2/linux.fragment | 2 + .../board/retrostone2/post-build.sh | 4 + .../board/retrostone2/tools/rsos-gamepad.c | 272 ++++++++++++++++++ docs/native-games.md | 18 +- 4 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 buildroot-external/board/retrostone2/tools/rsos-gamepad.c 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/docs/native-games.md b/docs/native-games.md index 3a92583..18c3118 100644 --- a/docs/native-games.md +++ b/docs/native-games.md @@ -38,8 +38,22 @@ A program that the exFAT partition does not mark executable can be started throu * 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 sees only one of them. Read every `/dev/input/event*` device -whose name starts with "RetroStone2", or merge them. +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: + +```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` (for example to follow an emulator's button settings). ## What the menu does around it From 47954d7f1a1c47d07bc62330189d9162d1c32f04 Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:56:18 -0400 Subject: [PATCH 4/7] Native games: ports of Super Mario 64, Star Fox 64 and Ocarina of Time Build scripts and code changes for three N64 PC ports, each built on a PC from the user's own ROM into a roms/native folder for the SD card. No game or game data is in the repository. - ports/sm64: sm64-port with a KMS/DRM GLES2 target and the built-in controls read from /dev/input with the N64 emulator's button settings. - ports/starship: Starship (Star Fox 64) with an OpenGL ES 2.0 mode for libultraship (USE_GLES2: GLSL ES 1.00, GLES2 framebuffers, no MSAA). - ports/soh: Ship of Harkinian 9.3.1 (Ocarina of Time) with the same GLES2 mode for its libultraship, fixes for 32-bit ARM and Mesa's lima (textures in the heap were dropped, unsized texture slots, primitive depth without gl_FragDepth) and speed work measured on the console. - The RetroStone2 configs get the libraries of the libultraship ports: Ogg/Vorbis, Opus/OpusFile, libzip, tinyxml2, spdlog, nlohmann-json, SDL2_net. Co-Authored-By: Claude Opus 5.5 --- .../configs/retrostone2_defconfig | 10 + .../configs/retrostone2_release_defconfig | 10 + docs/native-games.md | 7 +- ports/README.md | 65 ++ ports/sm64/README.md | 83 ++ ports/sm64/Super Mario 64.sh | 8 + ports/sm64/build-sm64.sh | 67 ++ ports/sm64/controller_rsos.c | 411 +++++++++ ports/sm64/controller_rsos.h | 8 + ports/sm64/retrostone2.patch | 254 ++++++ ports/soh/.gitattributes | 2 + ports/soh/Ocarina of Time.sh | 21 + ports/soh/README.md | 83 ++ ports/soh/build-soh.sh | 119 +++ ports/soh/libultraship-gles2.patch | 786 ++++++++++++++++++ ports/soh/soh.patch | 126 +++ ports/starship/README.md | 71 ++ ports/starship/Star Fox 64.sh | 19 + ports/starship/build-starship.sh | 76 ++ ports/starship/libultraship-gles2.patch | 426 ++++++++++ ports/starship/starship.patch | 187 +++++ 21 files changed, 2837 insertions(+), 2 deletions(-) create mode 100644 ports/README.md create mode 100644 ports/sm64/README.md create mode 100644 ports/sm64/Super Mario 64.sh create mode 100755 ports/sm64/build-sm64.sh create mode 100644 ports/sm64/controller_rsos.c create mode 100644 ports/sm64/controller_rsos.h create mode 100644 ports/sm64/retrostone2.patch create mode 100644 ports/soh/.gitattributes create mode 100644 ports/soh/Ocarina of Time.sh create mode 100644 ports/soh/README.md create mode 100755 ports/soh/build-soh.sh create mode 100644 ports/soh/libultraship-gles2.patch create mode 100644 ports/soh/soh.patch create mode 100644 ports/starship/README.md create mode 100644 ports/starship/Star Fox 64.sh create mode 100755 ports/starship/build-starship.sh create mode 100644 ports/starship/libultraship-gles2.patch create mode 100644 ports/starship/starship.patch diff --git a/buildroot-external/configs/retrostone2_defconfig b/buildroot-external/configs/retrostone2_defconfig index f894ea3..744ae23 100644 --- a/buildroot-external/configs/retrostone2_defconfig +++ b/buildroot-external/configs/retrostone2_defconfig @@ -81,6 +81,16 @@ BR2_PACKAGE_MESA3D_OPENGL_ES=y 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 db5a945..550e564 100644 --- a/buildroot-external/configs/retrostone2_release_defconfig +++ b/buildroot-external/configs/retrostone2_release_defconfig @@ -81,6 +81,16 @@ BR2_PACKAGE_MESA3D_OPENGL_ES=y 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 index 18c3118..f430a27 100644 --- a/docs/native-games.md +++ b/docs/native-games.md @@ -3,6 +3,9 @@ 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: @@ -43,7 +46,7 @@ joystick at all. **`rsos-gamepad`** (RetroStone2) merges them into one standard 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: +mapping (ports/starship/Star Fox 64.sh): ```sh SDL_GAMECONTROLLERCONFIG="$(/usr/bin/rsos-gamepad --print-mapping)" @@ -53,7 +56,7 @@ export SDL_GAMECONTROLLER_IGNORE_DEVICES=0x0001/0x0001 # the raw button device ``` 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` (for example to follow an emulator's button settings). +itself from `/dev/input` (ports/sm64/controller_rsos.c does, to follow the N64 emulator's button settings). ## What the menu does around it diff --git a/ports/README.md b/ports/README.md new file mode 100644 index 0000000..e980011 --- /dev/null +++ b/ports/README.md @@ -0,0 +1,65 @@ +# 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) 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 | +| 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 | + +## 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 and Ocarina of Time work); ports on the newest libultraship also need SDL3 | +| [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) | Perfect Dark | OpenGL 3.0 / ES 3.0 | Not as it is (the same GLES2 work) | +| 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`) and [soh/libultraship-gles2.patch](soh/libultraship-gles2.patch) (libultraship `da6daf1`, +with fixes for 32-bit ARM and speed work measured on the console). Both 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/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/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 From 5a24d4d2d782b378b1fb5f9f21b53412ab8bc19d Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:21:10 -0400 Subject: [PATCH 5/7] Native games: port of Mario Kart 64 (SpaghettiKart) ports/spaghettikart builds HarbourMasters' SpaghettiKart for the console from the user's own ROM, with the same GLES2 libultraship mode as Ship of Harkinian, carried over to SpaghettiKart's older libultraship (60c9bb7). Nothing from the game is in the repository. Also, measured on the console: - libultraship: loading the texture already bound no longer ends the draw call (each costs ~0.1 ms of CPU on the Mali-400) - SIGTERM no longer calls exit() in the signal handler (backport of libultraship 766a6bad): every quit from the menu used to hang - no screen readback to the CPU every race frame; the jumbotrons, which GLES2 cannot fill, are black and uploaded once - -O3 and unsafe math (NEON for float math) instead of Buildroot's -O2 - first start: linear texture filtering; controls for the built-in buttons - Toad's Turnpike: the finish line was placed before xOrientation was set, so on the first race a pole stood in the road (upstream bug) Co-Authored-By: Claude Opus 5.5 --- ports/README.md | 9 +- ports/spaghettikart/.gitattributes | 1 + ports/spaghettikart/Mario Kart 64.sh | 19 + ports/spaghettikart/README.md | 81 ++ ports/spaghettikart/build-spaghettikart.sh | 99 +++ ports/spaghettikart/libultraship-gles2.patch | 872 +++++++++++++++++++ ports/spaghettikart/spaghettikart.patch | 215 +++++ 7 files changed, 1293 insertions(+), 3 deletions(-) create mode 100644 ports/spaghettikart/.gitattributes create mode 100644 ports/spaghettikart/Mario Kart 64.sh create mode 100644 ports/spaghettikart/README.md create mode 100755 ports/spaghettikart/build-spaghettikart.sh create mode 100644 ports/spaghettikart/libultraship-gles2.patch create mode 100644 ports/spaghettikart/spaghettikart.patch diff --git a/ports/README.md b/ports/README.md index e980011..2d95edc 100644 --- a/ports/README.md +++ b/ports/README.md @@ -33,6 +33,7 @@ share it or put it in a release. |---|---|---|---| | 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 | ## Other N64 PC ports: can they run here? @@ -43,7 +44,7 @@ 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 and Ocarina of Time work); ports on the newest libultraship also need SDL3 | +| [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 and Mario Kart 64 work); ports on the newest libultraship also need SDL3 | | [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) | Perfect Dark | OpenGL 3.0 / ES 3.0 | Not as it is (the same GLES2 work) | | N64 Recompiled (RT64) | Zelda 64 Recomp, Banjo: Recompiled, DK64, Star Fox 64 Recomp, ... | Vulkan / Direct3D 12 | **No**: the Mali-400 has neither | @@ -52,8 +53,10 @@ Cortex-A7 cores at 960-1056 MHz. That decides which ports can work: 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`) and [soh/libultraship-gles2.patch](soh/libultraship-gles2.patch) (libultraship `da6daf1`, -with fixes for 32-bit ARM and speed work measured on the console). Both libultraship versions still use SDL2. +(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). These libultraship versions still use SDL2. ## Making a new port 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 From c4612181d8e137c429b319965754e4bb79e3567c Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:56:25 -0400 Subject: [PATCH 6/7] Native games: port of Majora's Mask (2Ship2Harkinian) ports/2ship/build-2ship.sh builds 2Ship2Harkinian 8a24047 (libultraship 7cb1022) from the user's own ROM: ZAPD on the PC for mm.o2r and 2ship.o2r, a static libpng and the game for the console. libultraship-gles2.patch is Ship of Harkinian's GLES2 patch plus the SIGTERM quit backport (766a6bad), no flush for an already-bound texture, linear filtering on first start and SIGBUS/ARM registers in crash reports. 2ship.patch fixes 2Ship on 32-bit ARM: - notes were skipped by an N64 pointer check that every 32-bit heap pointer fails - the mixer's DMEM buffer overflowed (found with ASan) - samples' sampleEnd was never set - audio catches up when the game is slow - GLES2 shaders, ARMv7 NEON, -O3/unsafe math for GLES2 release builds Co-Authored-By: Claude Opus 5.5 --- ports/2ship/.gitattributes | 1 + ports/2ship/2ship.patch | 206 +++++++ ports/2ship/Majora's Mask.sh | 20 + ports/2ship/README.md | 101 +++ ports/2ship/build-2ship.sh | 133 ++++ ports/2ship/libultraship-gles2.patch | 892 +++++++++++++++++++++++++++ ports/README.md | 8 +- 7 files changed, 1358 insertions(+), 3 deletions(-) create mode 100644 ports/2ship/.gitattributes create mode 100644 ports/2ship/2ship.patch create mode 100644 ports/2ship/Majora's Mask.sh create mode 100644 ports/2ship/README.md create mode 100755 ports/2ship/build-2ship.sh create mode 100644 ports/2ship/libultraship-gles2.patch 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 index 2d95edc..8dc15df 100644 --- a/ports/README.md +++ b/ports/README.md @@ -11,7 +11,7 @@ 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) also need `rsos-gamepad` and the port libraries in 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`; @@ -35,6 +35,7 @@ share it or put it in a release. | 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 | +| 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? @@ -44,7 +45,7 @@ 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 and Mario Kart 64 work); ports on the newest libultraship also need SDL3 | +| [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 | | [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) | Perfect Dark | OpenGL 3.0 / ES 3.0 | Not as it is (the same GLES2 work) | | N64 Recompiled (RT64) | Zelda 64 Recomp, Banjo: Recompiled, DK64, Star Fox 64 Recomp, ... | Vulkan / Direct3D 12 | **No**: the Mali-400 has neither | @@ -56,7 +57,8 @@ pins its own libultraship, so each has its own copy of the change: [starship/lib (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). These libultraship versions still use SDL2. +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 From 4b5932e249234aef7856fd73cc06786f60e41355 Mon Sep 17 00:00:00 2001 From: KurdtNervanna <136639095+KurdtNervanna@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:24:58 -0400 Subject: [PATCH 7/7] Native games: port of GoldenEye 007 (Project Midas) build-goldeneye.sh builds Project Midas 1.0.2b (9062f9f) from the user's own US ROM: Midas's asset extraction on the PC, the game for the console, and a copy of the ROM in goldeneye/Data/ (the game reads it at every start). goldeneye.patch: - OpenGL ES 2.0 mode for the fast3d renderer (GE_GLES2): GLES2 loader, GLSL ES 1.00, texture sizes as uniforms, no VAOs or framebuffer effects, client-side vertex arrays, a window visible from the start (KMSDRM) - Mali-400 16-bit fragment precision: the far clip compares w - z from the vertex shader (the intro, menus and effects were discarded), noise hash - 32-bit ARM: ARM flags, link at 0x10000000, -fpermissive for GCC 14, crash reports with registers and a stack scan - upstream bug: stanCheckLinkedSpecialTile took the tile-walk callback's float arguments as s32, which crashed on the hard-float ARM ABI - render thread on, -O3/NEON for the renderer, room_cache applied at start - a RetroStone2 controller layout on the N64 control styles Co-Authored-By: Claude Opus 5.5 --- ports/README.md | 3 +- ports/goldeneye/.gitattributes | 1 + ports/goldeneye/GoldenEye 007.sh | 20 + ports/goldeneye/README.md | 90 ++++ ports/goldeneye/build-goldeneye.sh | 96 ++++ ports/goldeneye/goldeneye.patch | 733 +++++++++++++++++++++++++++++ 6 files changed, 942 insertions(+), 1 deletion(-) create mode 100644 ports/goldeneye/.gitattributes create mode 100644 ports/goldeneye/GoldenEye 007.sh create mode 100644 ports/goldeneye/README.md create mode 100755 ports/goldeneye/build-goldeneye.sh create mode 100644 ports/goldeneye/goldeneye.patch diff --git a/ports/README.md b/ports/README.md index 8dc15df..ccd6f79 100644 --- a/ports/README.md +++ b/ports/README.md @@ -35,6 +35,7 @@ share it or put it in a release. | 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? @@ -46,7 +47,7 @@ Cortex-A7 cores at 960-1056 MHz. That decides which ports can work: |---|---|---|---| | 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 | -| [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) | Perfect Dark | OpenGL 3.0 / ES 3.0 | Not as it is (the same GLES2 work) | +| 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 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