mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-26 18:02:17 +01:00
Compare commits
26
Commits
00.007.000
..
0.7.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bca99ff6e | ||
|
|
129d055f99 | ||
|
|
89786fd668 | ||
|
|
f97346904d | ||
|
|
0ec5920b37 | ||
|
|
0ca7ea5721 | ||
|
|
240baec05e | ||
|
|
7b2a21208e | ||
|
|
9e3b6c1596 | ||
|
|
bca88bf154 | ||
|
|
a2147c5dbe | ||
|
|
7f4949f830 | ||
|
|
ab6a937908 | ||
|
|
6c09976c62 | ||
|
|
fbda6fd21e | ||
|
|
96a850b3b4 | ||
|
|
5b98886d51 | ||
|
|
5715de56a5 | ||
|
|
cfd4bbfe95 | ||
|
|
dc42d378c5 | ||
|
|
caafe1f412 | ||
|
|
bd40c5092a | ||
|
|
f00c397657 | ||
|
|
ee516620b0 | ||
|
|
5d7ccaae19 | ||
|
|
7257726071 |
No files matched your search
@@ -7,6 +7,7 @@
|
||||
*.vcxproj* text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.bin binary
|
||||
*.dds binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
|
||||
@@ -46,6 +46,12 @@ newer releases. Install 0.6.3 or newer by hand once: delete `/data/homebrew/PPSA
|
||||
|
||||
For a crash or a bug, please [open an issue](../../issues) and attach `/data/mupen64plus/logs/boot.log`.
|
||||
|
||||
## Discussion and support
|
||||
|
||||
- **[Mupen64Plus PS5 thread](https://raaproductions.ca/forum/showthread.php?tid=20)** on the RAAI Productions
|
||||
forum: questions, help and news.
|
||||
- **[RAA! Forum](https://raaproductions.ca/forum/showthread.php?tid=20)**: my website.
|
||||
|
||||
## Documentation
|
||||
|
||||
[ps5/README.md](ps5/README.md): controls, settings, folders, how the port works, and building.
|
||||
@@ -66,7 +72,8 @@ The GPU renderer needs RADV built first (mihawk-99/PS5_Vulkan, in WSL `/root/gpu
|
||||
Releasing:
|
||||
|
||||
1. Raise `VERSION` in `ps5/Makefile`, and set the matching `contentVersion` in `ps5/app/sce_sys/param.json`
|
||||
(0.7.0 → `00.007.000`; the build stops if they differ). That number is also the release tag.
|
||||
(0.7.0 → `00.007.000`; the build stops if they differ). The release tag is the version as people write it
|
||||
(`0.7.4`; up to 0.7.3 it was the `contentVersion`, `00.007.003`). The app reads both as numbers.
|
||||
2. Write `release-notes/<contentVersion>.md`: a few short lines, shown on the console in the update question.
|
||||
3. Commit and push, then run `release.bat`. It builds, creates the release with `Mupen64PlusPS5.zip`, and checks
|
||||
GitHub's digest against the local zip.
|
||||
@@ -75,7 +82,7 @@ Never reuse or lower a version.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `mupen64plus-core/`: the original core source, **unmodified**.
|
||||
- `mupen64plus-core/`: the original core source, with one change: ROMs up to 252 MB (ROM hacks bigger than a 64 MB cartridge): `src/device/memory/memory.h` and `src/device/cart/cart_rom.{c,h}`, marked `Mupen64Plus PS5`.
|
||||
- `ps5/`: the port (plugins, PS5 layer, frontend, build).
|
||||
- `release-notes/`: one file per release.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Mupen64Plus PS5 as a whole (the `eboot.bin` it builds) is distributed under the
|
||||
|
||||
| Part | Where | License |
|
||||
| --- | --- | --- |
|
||||
| mupen64plus-core 2.6.0 (unmodified) | `mupen64plus-core/` | GPL-2.0-or-later (`mupen64plus-core/LICENSES`) |
|
||||
| mupen64plus-core 2.6.0 (changed for ROMs bigger than 64 MB: `memory.h`, `cart_rom.c/.h`, marked `Mupen64Plus PS5`) | `mupen64plus-core/` | GPL-2.0-or-later (`mupen64plus-core/LICENSES`) |
|
||||
| The PS5 port: plugins glue, PS5 layer, frontend, build | `ps5/` | MIT (per-file SPDX headers) |
|
||||
| ps5-native-app-boilerplate tooling (startup code, eboot builder and signer, libc.prx builder) | `ps5/proto/native/` | GPL-3.0-or-later |
|
||||
| mupen64plus-rsp-cxd4 | `ps5/third_party/rsp-cxd4/` | CC0-1.0 (`COPYING`) |
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
#define __STDC_FORMAT_MACROS
|
||||
#include <inttypes.h>
|
||||
|
||||
#define CART_ROM_ADDR_MASK UINT32_C(0x03ffffff);
|
||||
#define CART_ROM_ADDR_MASK UINT32_C(0x03ffffff)
|
||||
/* Mupen64Plus PS5: for ROM hacks bigger than 64 MiB (see CART_ROM_MAX_SIZE) */
|
||||
#define CART_ROM_ADDR_MASK_BIG UINT32_C(0x0fffffff)
|
||||
|
||||
|
||||
void init_cart_rom(struct cart_rom* cart_rom,
|
||||
@@ -42,6 +44,7 @@ void init_cart_rom(struct cart_rom* cart_rom,
|
||||
{
|
||||
cart_rom->rom = rom;
|
||||
cart_rom->rom_size = rom_size;
|
||||
cart_rom->addr_mask = (rom_size > CART_ROM_ADDR_MASK + 1) ? CART_ROM_ADDR_MASK_BIG : CART_ROM_ADDR_MASK;
|
||||
|
||||
cart_rom->r4300 = r4300;
|
||||
cart_rom->pi = pi;
|
||||
@@ -56,7 +59,7 @@ void poweron_cart_rom(struct cart_rom* cart_rom)
|
||||
void read_cart_rom(void* opaque, uint32_t address, uint32_t* value)
|
||||
{
|
||||
struct cart_rom* cart_rom = (struct cart_rom*)opaque;
|
||||
uint32_t addr = rom_address(address);
|
||||
uint32_t addr = rom_address(cart_rom, address);
|
||||
|
||||
if (cart_rom->pi->regs[PI_STATUS_REG] & PI_STATUS_IO_BUSY)
|
||||
{
|
||||
@@ -84,7 +87,8 @@ void write_cart_rom(void* opaque, uint32_t address, uint32_t value, uint32_t mas
|
||||
|
||||
unsigned int cart_rom_dma_read(void* opaque, const uint8_t* dram, uint32_t dram_addr, uint32_t cart_addr, uint32_t length)
|
||||
{
|
||||
cart_addr &= CART_ROM_ADDR_MASK;
|
||||
struct cart_rom* cart_rom = (struct cart_rom*)opaque;
|
||||
cart_addr &= cart_rom->addr_mask;
|
||||
|
||||
DebugMessage(M64MSG_WARNING, "DMA Writing to CART_ROM: 0x%" PRIX32 " -> 0x%" PRIX32 " (0x%" PRIX32 ")", dram_addr, cart_addr, length);
|
||||
|
||||
@@ -97,7 +101,7 @@ unsigned int cart_rom_dma_write(void* opaque, uint8_t* dram, uint32_t dram_addr,
|
||||
struct cart_rom* cart_rom = (struct cart_rom*)opaque;
|
||||
const uint8_t* mem = cart_rom->rom;
|
||||
|
||||
cart_addr &= CART_ROM_ADDR_MASK;
|
||||
cart_addr &= cart_rom->addr_mask;
|
||||
|
||||
if (cart_addr + length < cart_rom->rom_size)
|
||||
{
|
||||
|
||||
@@ -34,6 +34,8 @@ struct cart_rom
|
||||
{
|
||||
uint8_t* rom;
|
||||
size_t rom_size;
|
||||
/* Mupen64Plus PS5: cart addresses wrap at 64 MiB, as before, unless the ROM is bigger */
|
||||
uint32_t addr_mask;
|
||||
|
||||
uint32_t last_write;
|
||||
|
||||
@@ -41,9 +43,9 @@ struct cart_rom
|
||||
struct pi_controller* pi;
|
||||
};
|
||||
|
||||
static osal_inline uint32_t rom_address(uint32_t address)
|
||||
static osal_inline uint32_t rom_address(const struct cart_rom* cart_rom, uint32_t address)
|
||||
{
|
||||
return (address & 0x03fffffc);
|
||||
return (address & cart_rom->addr_mask & ~UINT32_C(3));
|
||||
}
|
||||
|
||||
void init_cart_rom(struct cart_rom* cart_rom,
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
#include "osal/preproc.h"
|
||||
|
||||
enum { RDRAM_MAX_SIZE = 0x800000 };
|
||||
enum { CART_ROM_MAX_SIZE = 0x4000000 };
|
||||
/* Mupen64Plus PS5: ROM hacks can be bigger than the 64 MiB of the biggest cartridges.
|
||||
* Up to 252 MiB, all of dom1 addr2 (0x10000000 - 0x1fbfffff); upstream lifted the limit after 2.6.0 too. */
|
||||
enum { CART_ROM_MAX_SIZE = 0xfc00000 };
|
||||
enum { DD_ROM_MAX_SIZE = 0x400000 };
|
||||
|
||||
typedef void (*read32fn)(void*,uint32_t,uint32_t*);
|
||||
|
||||
+50
-18
@@ -20,8 +20,9 @@
|
||||
# 3. build/ps5/Mupen64PS5.elf installer + helper, with the app folder from step 2 built in
|
||||
#
|
||||
# What goes into eboot.bin:
|
||||
# - mupen64plus-core 2.6.0 (../mupen64plus-core, unmodified), with its x86-64 dynarec, minus the parts that need
|
||||
# SDL/OpenGL/libpng/dlopen, which n64/core/*.c replace; n64/sdl is the bit of SDL it still uses;
|
||||
# - mupen64plus-core 2.6.0 (../mupen64plus-core, changed only for ROMs up to 252 MiB), with its x86-64 dynarec,
|
||||
# minus the parts that need SDL/OpenGL/libpng/dlopen, which n64/core/*.c replace; n64/sdl is the bit of SDL it
|
||||
# still uses;
|
||||
# - its plugins, linked in (n64/plugins): mupen64plus-rsp-cxd4 (LLE RSP) behind mupen64plus-rsp-hle (HLE
|
||||
# audio), angrylion-rdp-plus (software RDP/VI), AudioOut and DualSense input;
|
||||
# - the frontend (shelf, menus, covers) and the PS5 layer (coreorbis).
|
||||
@@ -32,7 +33,7 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
PS5_HOST ?= ps5
|
||||
PS5_PORT ?= 9021
|
||||
VERSION ?= 0.7.0
|
||||
VERSION ?= 0.7.5
|
||||
# param.json's contentVersion, NN.NNN.NNN (0.6.1 -> 00.006.001): the release tag, as in the Homebrew Browser; it
|
||||
# must also be written in app/sce_sys/param.json (checked below)
|
||||
CONTENT_VERSION := $(shell echo $(VERSION) | awk -F. '{printf "%02d.%03d.%03d", $$1, $$2, $$3}')
|
||||
@@ -62,6 +63,8 @@ APP_OUT := $(APP_BUILD)/$(TITLE_ID)
|
||||
|
||||
M64 := ../mupen64plus-core
|
||||
M64SRC := $(M64)/src
|
||||
# the core's headers: a change to one rebuilds the core (and the port's C files, which include some of them)
|
||||
M64_HDRS := $(shell find $(M64SRC) -name '*.h')
|
||||
ZLIB := third_party/zlib
|
||||
CXD4 := third_party/rsp-cxd4
|
||||
RSPHLE := third_party/rsp-hle/src
|
||||
@@ -93,7 +96,7 @@ CORE_SRC := \
|
||||
CORE_SUB_SRC := md5/md5.c minizip/ioapi.c minizip/unzip.c minizip/zip.c
|
||||
# the port's side of the core (C)
|
||||
N64_SRC := n64/sdl/sdl_shim.c n64/core/perf.c n64/core/jit_ps5.c n64/core/vidext_ps5.c n64/core/eventloop_ps5.c n64/core/screenshot_ps5.c \
|
||||
n64/core/osal_dynlib_static.c
|
||||
n64/core/osal_dynlib_static.c n64/core/memory_ps5.c
|
||||
# the plugins
|
||||
PLUGIN_C_SRC := n64/plugins/rsp_cxd4_ps5.c n64/plugins/rsp_hle_ps5.c n64/plugins/gfx_ps5.c
|
||||
PLUGIN_CXX_SRC := n64/plugins/audio_ps5.cpp n64/plugins/input_ps5.cpp
|
||||
@@ -118,9 +121,11 @@ EMU_SRC := coreorbis/main-boot.cpp \
|
||||
coreorbis/orbis-shims/ProsperoVideo.cpp coreorbis/orbis-shims/ProsperoAudio.cpp \
|
||||
coreorbis/orbis-shims/ProsperoInput.cpp coreorbis/orbis-shims/ProsperoJailbreak.cpp \
|
||||
coreorbis/orbis-shims/ProsperoCrash.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp \
|
||||
coreorbis/orbis-shims/ProsperoKeyboard.cpp \
|
||||
frontend/fe_emu.cpp frontend/fe_menu.cpp frontend/fe_settings.cpp frontend/fe_text.cpp \
|
||||
frontend/fe_games.cpp frontend/fe_http.cpp frontend/fe_covers.cpp frontend/fe_shelf.cpp \
|
||||
frontend/fe_prefetch.cpp frontend/fe_update.cpp
|
||||
frontend/fe_prefetch.cpp frontend/fe_update.cpp frontend/fe_cheats.cpp frontend/fe_draw.cpp \
|
||||
frontend/fe_search.cpp
|
||||
# the payloads: the helper, and the installer (= helper + install)
|
||||
HELPER_SRC := coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.cpp \
|
||||
coreorbis/orbis-shims/ProsperoHelper.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp
|
||||
@@ -143,13 +148,22 @@ endif
|
||||
GEN := build/gen
|
||||
GAMEDB := $(GEN)/n64-gamedb.tsv
|
||||
M64_INI := $(M64)/data/mupen64plus.ini
|
||||
$(GAMEDB): tools/make_n64db.py $(M64_INI)
|
||||
# libretro-thumbnails' box art names, so the table's names match its files (make_n64db.py --fetch-boxarts)
|
||||
BOXARTS := tools/libretro-n64-boxarts.txt
|
||||
$(GAMEDB): tools/make_n64db.py $(M64_INI) $(BOXARTS)
|
||||
@mkdir -p $(dir $@)
|
||||
python3 tools/make_n64db.py $(M64_INI) $@
|
||||
python3 tools/make_n64db.py $(M64_INI) $@ $(BOXARTS)
|
||||
|
||||
%/port/frontend/fe_games.o: EXTRA_FLAGS = -DGAMEDB_TSV='"$(abspath $(GAMEDB))"'
|
||||
# the ROM catalog itself goes into fe_emu.o (written to /data/mupen64plus/data for the core)
|
||||
%/port/frontend/fe_emu.o: EXTRA_FLAGS = -DM64P_INI_PATH='"$(abspath $(M64_INI))"'
|
||||
# the core's cheat database (1.2 MB of text), compressed into fe_cheats.o: 4 bytes of size, then zlib
|
||||
CHEATS_Z := build/gen/mupencheat.z
|
||||
$(CHEATS_Z): $(M64)/data/mupencheat.txt
|
||||
@mkdir -p $(dir $@)
|
||||
python3 -c "import struct, sys, zlib; d = open(sys.argv[1], 'rb').read(); \
|
||||
open(sys.argv[2], 'wb').write(struct.pack('<I', len(d)) + zlib.compress(d, 9))" $< $@
|
||||
%/port/frontend/fe_cheats.o: EXTRA_FLAGS = -DCHEATS_Z='"$(abspath $(CHEATS_Z))"'
|
||||
# stb_image in fe_covers.o has warnings of its own
|
||||
%/port/frontend/fe_covers.o: EXTRA_FLAGS = -Wno-error -Wno-sign-compare -Wno-unused-but-set-variable -Wno-implicit-fallthrough
|
||||
# the UI fonts (PS5SX2's: PCSX2's Roboto Regular and PromptFont) built into fe_text.o
|
||||
@@ -176,8 +190,9 @@ PS5_OUT := build/ps5-cpu
|
||||
endif
|
||||
# The PS5's CPU is Zen 2 (PS5SX2 builds with the same -march).
|
||||
PS5_OPT := -O3 -fPIC -march=znver2 -fno-strict-aliasing
|
||||
# the payloads: notifications, and the kernel access the helper needs (libkernel_sys: sceKernelGetAppInfo)
|
||||
PS5_PAYLOAD_LIBS := -lkernel_sys
|
||||
# the payloads: notifications, the kernel access the helper needs (libkernel_sys: sceKernelGetAppInfo), and
|
||||
# starting the app again (libSceSystemService, libSceUserService: ProsperoUpdateJob.h, RelaunchIfPending)
|
||||
PS5_PAYLOAD_LIBS := -lkernel_sys -lSceSystemService -lSceUserService
|
||||
|
||||
PS5_CORE_OBJ := $(patsubst %.c,$(PS5_OUT)/m64/%.o,$(CORE_SRC)) $(patsubst %.c,$(PS5_OUT)/m64sub/%.o,$(CORE_SUB_SRC)) \
|
||||
$(PS5_OUT)/n64/n64/core/linkage_x64.o \
|
||||
@@ -193,16 +208,16 @@ PS5_ZLIB_OBJ := $(patsubst $(ZLIB)/%.c,$(PS5_OUT)/zlib/%.o,$(ZLIB_SRC))
|
||||
ps5: $(PS5_OUT)/Mupen64PS5.elf
|
||||
|
||||
# mupen64plus-core: third-party C, its own warnings are not ours
|
||||
$(PS5_OUT)/m64/%.o: $(M64SRC)/%.c
|
||||
$(PS5_OUT)/m64/%.o: $(M64SRC)/%.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/m64/device/r4300/new_dynarec/new_dynarec.o: $(M64SRC)/device/r4300/new_dynarec/new_dynarec.c
|
||||
$(PS5_OUT)/m64/device/r4300/new_dynarec/new_dynarec.o: $(M64SRC)/device/r4300/new_dynarec/new_dynarec.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CORE_DEFS) $(CORE_INCS) $(DYNAREC_FLAGS) -w -c -o $@ $<
|
||||
|
||||
# the struct offsets the dynarec's assembly needs (the core's tools/gen_asm_script.sh, on our compiler's object)
|
||||
$(PS5_OUT)/gen/asm_defines.o: $(M64SRC)/asm_defines/asm_defines.c
|
||||
$(PS5_OUT)/gen/asm_defines.o: $(M64SRC)/asm_defines/asm_defines.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
$(PS5_OUT)/gen/asm_defines_gas.h: $(PS5_OUT)/gen/asm_defines.o
|
||||
@@ -224,7 +239,7 @@ $(PS5_OUT)/n64/n64/plugins/gfx_ps5.o: n64/plugins/gfx_ps5.c $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(GFX_INCS) -Wall -Wextra -Werror -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/n64/%.o: %.c $(PORT_HDRS)
|
||||
$(PS5_OUT)/n64/%.o: %.c $(PORT_HDRS) $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CORE_DEFS) $(CORE_INCS) -In64 -In64/core -Wall -Wextra -Werror -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
@@ -275,9 +290,13 @@ $(PS5_OUT)/helper_data.o: coreorbis/orbis-shims/helper_data.cpp coreorbis/orbis-
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) $(PORT_WARN) -DHELPER_ELF='"$(abspath $(PS5_OUT)/Mupen64PS5-helper.elf)"' -c -o $@ $<
|
||||
|
||||
# 3. the installer + helper, with the app folder built in
|
||||
# the GPU renderer's pipeline cache shipped with the app (a console's cache/parallel-rdp-pipelines.bin), when there
|
||||
# is one: assets/ in the app folder (the installer payload doesn't carry it)
|
||||
PIPELINE_SEED := $(if $(filter 1,$(VULKAN)),$(wildcard n64/plugins/assets/parallel-rdp-pipelines.bin))
|
||||
APP_FILES = $(APP_OUT)/eboot.bin $(APP_OUT)/sce_sys/param.json $(APP_OUT)/sce_sys/icon0.png $(APP_OUT)/sce_sys/pic0.dds \
|
||||
$(APP_OUT)/sce_sys/pic1.dds \
|
||||
$(APP_OUT)/sce_module/libc.prx
|
||||
$(APP_OUT)/sce_module/libc.prx \
|
||||
$(if $(PIPELINE_SEED),$(APP_OUT)/assets/parallel-rdp-pipelines.bin)
|
||||
APP_DATA_DEFS = -DAPP_EBOOT='"$(abspath $(APP_OUT)/eboot.bin)"' -DAPP_PARAM='"$(abspath $(APP_OUT)/sce_sys/param.json)"' \
|
||||
-DAPP_ICON0='"$(abspath $(APP_OUT)/sce_sys/icon0.png)"' -DAPP_PIC0='"$(abspath $(APP_OUT)/sce_sys/pic0.dds)"' \
|
||||
-DAPP_PIC1='"$(abspath $(APP_OUT)/sce_sys/pic1.dds)"' \
|
||||
@@ -426,6 +445,10 @@ endif
|
||||
|
||||
EBOOT_OBJS = $(APP_BUILD)/app_crt.o $(APP_BUILD)/app_cpp_runtime.o $(PS5_CORE_OBJ) $(PS5_EMU_OBJ) $(EBOOT_ZLIB) \
|
||||
$(VULKAN_OBJ) $(PS5_OUT)/helper_data.o
|
||||
# boot.log's "built <date> <time>" (__DATE__/__TIME__ in main-boot.cpp): recompiled whenever anything else in
|
||||
# eboot.bin changes, so the date always says which build is running
|
||||
MAIN_BOOT_O := $(PS5_OUT)/port/coreorbis/main-boot.o
|
||||
$(MAIN_BOOT_O): $(filter-out $(MAIN_BOOT_O),$(PS5_CORE_OBJ) $(PS5_EMU_OBJ) $(VULKAN_OBJ) $(PS5_OUT)/helper_data.o)
|
||||
$(APP_BUILD)/mupen64plus-pie.elf: $(EBOOT_OBJS) $(NATIVE)/tooling/native/ps5-pie.ld $(NATIVE)/tooling/native/ehframe.ld \
|
||||
$(NATIVE)/tooling/native/app-symbols.map $(NATIVE)/tooling/native/localize.map $(VULKAN_STUBS)
|
||||
$(LLD) -m elf_x86_64 -pie -z max-page-size=0x4000 --hash-style=gnu \
|
||||
@@ -440,10 +463,18 @@ $(APP_BUILD)/eboot.elf: $(APP_BUILD)/mupen64plus-pie.elf $(NATIVE_TOOL)
|
||||
--companion-sdk 0x08050001 --file-name eboot.elf
|
||||
python3 tools/prepare_console_elf.py --input $@ --output $(APP_BUILD)/eboot-console-ready.elf
|
||||
|
||||
# 0.6.3 to 0.7.2 refuse an update with a file over 32 MiB (fe_update.cpp kMaxFile): eboot.bin must stay under it
|
||||
EBOOT_MAX := 33554432
|
||||
$(APP_OUT)/eboot.bin: $(APP_BUILD)/eboot.elf $(NATIVE_TOOL)
|
||||
@mkdir -p $(dir $@)
|
||||
$(NATIVE_TOOL) self --sign --in $< --out $@ --magic 0x1D3D154F
|
||||
$(NATIVE_TOOL) self --inspect --file $@
|
||||
@size=$$(stat -c %s $@); if [ $$size -gt $(EBOOT_MAX) ]; then rm -f $@; \
|
||||
echo "eboot.bin is $$size bytes: apps 0.6.3-0.7.2 can't update to an eboot.bin over 32 MiB" >&2; exit 1; fi
|
||||
|
||||
$(APP_OUT)/assets/parallel-rdp-pipelines.bin: $(PIPELINE_SEED)
|
||||
@mkdir -p $(dir $@)
|
||||
cp $< $@
|
||||
|
||||
# app/sce_sys/param.json is committed with the contentVersion the release ships: the homebrew.page catalog reads
|
||||
# it from the repository at the release tag. It must match VERSION; the build stops when it doesn't.
|
||||
@@ -485,15 +516,15 @@ HOST_ZLIB_OBJ := $(patsubst $(ZLIB)/%.c,$(HOST_OUT)/zlib/%.o,$(ZLIB_SRC))
|
||||
|
||||
host: $(HOST_OUT)/mupen64plus-ps5-host $(HOST_OUT)/mupen64plus-ps5-installer $(HOST_OUT)/mupen64plus-ps5-helper
|
||||
|
||||
$(HOST_OUT)/m64/%.o: $(M64SRC)/%.c
|
||||
$(HOST_OUT)/m64/%.o: $(M64SRC)/%.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/m64/device/r4300/new_dynarec/new_dynarec.o: $(M64SRC)/device/r4300/new_dynarec/new_dynarec.c
|
||||
$(HOST_OUT)/m64/device/r4300/new_dynarec/new_dynarec.o: $(M64SRC)/device/r4300/new_dynarec/new_dynarec.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CORE_DEFS) $(CORE_INCS) $(DYNAREC_FLAGS) -w -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/gen/asm_defines.o: $(M64SRC)/asm_defines/asm_defines.c
|
||||
$(HOST_OUT)/gen/asm_defines.o: $(M64SRC)/asm_defines/asm_defines.c $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
$(HOST_OUT)/gen/asm_defines_gas.h: $(HOST_OUT)/gen/asm_defines.o
|
||||
@@ -515,7 +546,7 @@ $(HOST_OUT)/n64/n64/plugins/gfx_ps5.o: n64/plugins/gfx_ps5.c $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(GFX_INCS) -Wall -Wextra -Werror -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/n64/%.o: %.c $(PORT_HDRS)
|
||||
$(HOST_OUT)/n64/%.o: %.c $(PORT_HDRS) $(M64_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CORE_DEFS) $(CORE_INCS) -In64 -In64/core -Wall -Wextra -Werror -Wno-unused-parameter -c -o $@ $<
|
||||
|
||||
@@ -575,6 +606,7 @@ $(HOST_OUT)/mupen64plus-ps5-host: $(HOST_CORE_OBJ) $(HOST_EMU_OBJ) $(HOST_ZLIB_O
|
||||
# the files built into fe_games.o / fe_emu.o
|
||||
$(PS5_OUT)/port/frontend/fe_games.o $(HOST_OUT)/port/frontend/fe_games.o: $(GAMEDB)
|
||||
$(PS5_OUT)/port/frontend/fe_emu.o $(HOST_OUT)/port/frontend/fe_emu.o: $(M64_INI)
|
||||
$(PS5_OUT)/port/frontend/fe_cheats.o $(HOST_OUT)/port/frontend/fe_cheats.o: $(CHEATS_Z)
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
+71
-21
@@ -4,7 +4,7 @@ mupen64plus-core 2.6.0, the Nintendo 64 emulator, as a **native home-screen app*
|
||||
shelf with covers, DualSense support and save states. The release is the app folder itself (`eboot.bin` and
|
||||
its files): no payload to send.
|
||||
|
||||
- `../mupen64plus-core` is the original core source, **unmodified**.
|
||||
- `../mupen64plus-core` is the original core source, with one change: ROMs up to 252 MB (ROM hacks bigger than a 64 MB cartridge): `src/device/memory/memory.h` and `src/device/cart/cart_rom.{c,h}`, marked `Mupen64Plus PS5`.
|
||||
- Everything that makes it a PS5 app is in this folder:
|
||||
- `n64/`: the core's PS5 side and its built-in plugins;
|
||||
- `coreorbis/`: the PS5 layer;
|
||||
@@ -75,6 +75,18 @@ picture is read back and shown like angrylion's. Its compiled shaders are kept i
|
||||
`/data/mupen64plus/cache/parallel-rdp-pipelines.bin`. If Vulkan doesn't work, the app falls back to angrylion,
|
||||
which renders on the CPU at the N64's own resolution.
|
||||
|
||||
**Shaders shipped with the app.** Every PS5 has the same GPU and runs the same RADV, built into `eboot.bin`, so
|
||||
one console's compiled shaders suit the others. `n64/plugins/assets/parallel-rdp-pipelines.bin`, when present,
|
||||
is a copy of a console's `cache/parallel-rdp-pipelines.bin` and goes into the app folder as
|
||||
`assets/parallel-rdp-pipelines.bin`. At the first start of a new version, the app merges it into the player's own
|
||||
cache (`cache/parallel-rdp-pipelines.seed` remembers it), so nobody waits for those compiles. A cache made by
|
||||
another driver (its UUID differs) is left out. To refresh it, copy the cache from a console that has played
|
||||
many games, after a build that changed RADV or paraLLEl-RDP.
|
||||
|
||||
**`eboot.bin` stays under 32 MiB.** The in-app updater of 0.6.3 to 0.7.2 refuses a file over 32 MiB, so those
|
||||
versions could never update to a bigger `eboot.bin`; the build stops if it gets there. Large data goes into the
|
||||
app folder as files of its own.
|
||||
|
||||
**What the core needs from SDL, libpng and dlopen is replaced:**
|
||||
|
||||
- `n64/sdl/` provides threads, mutexes and the clock on pthreads.
|
||||
@@ -131,7 +143,8 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
|
||||
## Controls
|
||||
|
||||
**In a game** (buttons by position, as on the N64 pad)
|
||||
**In a game** (the default layout: buttons by position, as on the N64 pad; Settings → Controller layout changes
|
||||
it, for every game or, from the pause menu, for one game)
|
||||
|
||||
| PS5 | N64 |
|
||||
|---|---|
|
||||
@@ -143,14 +156,37 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| right stick | C buttons |
|
||||
| Triangle / Circle | C-up / C-down |
|
||||
| OPTIONS | Start |
|
||||
| **touchpad click** or **L3 + R3** | pause menu |
|
||||
| **touchpad click** | pause menu |
|
||||
| **R3** (press the right stick) | save the state in the current slot |
|
||||
| **L3** (press the left stick) | load the state of the current slot |
|
||||
|
||||
**The pause menu** has:
|
||||
**The pause menu** (the touchpad) is a panel over the paused game, drawn on the CPU with the anti-aliased shapes
|
||||
of `frontend/fe_draw.cpp`; Settings on the game list and the Controller Layout and Cheats pages share its look
|
||||
(`fe_menu.cpp`, namespace `pm`). Its footer's buttons work anywhere in it: touchpad resume, Square reset the
|
||||
game (after a question), Triangle back to the game list, Cross confirm, Circle back (resume). Its rows:
|
||||
|
||||
- resume, save state, load state, and the state slot (0–9);
|
||||
- save state and load state (Cross), and the state slot (0–9);
|
||||
- **cheats**: the GameShark codes of the core's `data/mupencheat.txt` (about 560 game versions, found by the ROM
|
||||
header's CRCs and country; built into `eboot.bin` compressed, `frontend/fe_cheats.cpp`). Each one is off or on,
|
||||
or off or one of its choices, with the database's note beside it; the ones left on are on again the next time
|
||||
(`cheats/<ROM file>.txt`). Codes that need the GameShark's button are left out. Above the list:
|
||||
- **Search for a Cheat** (`frontend/fe_search.cpp`): finds where the game keeps a value in its RAM. It starts
|
||||
from every 8, 16 or 32-bit value, either equal to a number or unknown, and each step keeps the addresses whose
|
||||
value is (not) the number, went up or down (by the number), changed or didn't, since the step before; Undo
|
||||
goes one step back. A result becomes a cheat (GameShark 80 / 81 codes keeping the value) or is set once
|
||||
(`n64/core/memory_ps5.c` reads and writes the RDRAM);
|
||||
- **Type In Codes**: GameShark codes typed on the PS5's keyboard.
|
||||
|
||||
The player's own cheats come first in the list (Square renames one, changes its codes or deletes it; on one of
|
||||
the database's, Square makes an own copy to change) and are kept in `cheats/<ROM file>.custom.txt` in the
|
||||
database's format, so codes can be put there from a PC too. Names and codes are typed on the system's on-screen
|
||||
keyboard (`libSceImeDialog`, `coreorbis/orbis-shims/ProsperoKeyboard.cpp`). Its system module is loaded at
|
||||
start, before the app leaves the sandbox: afterwards the system looks for modules inside the sandbox and doesn't
|
||||
find them (`80020063`). Where it doesn't load the app still runs, only without typing;
|
||||
- speed (normal or fast forward);
|
||||
- every setting below;
|
||||
- reset, back to the game list, and quit.
|
||||
- every setting below but the game list's (Continue where you left off, Game list view, Download covers, Check
|
||||
for updates: Settings on the game list has them);
|
||||
- quit (Cross).
|
||||
|
||||
**Settings** (marked `*` when they apply from the next game start):
|
||||
|
||||
@@ -164,18 +200,27 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| Internal resolution * | GPU renderer: 1x (native), 2x, 4x (default), 8x |
|
||||
| GPU sync * | GPU renderer: Accurate (default: waits for the GPU wherever the game waits for the RDP) or Fast (may glitch) |
|
||||
| CPU core * | Dynarec (default, when the console allows it) or Interpreter (slower; for a game the dynarec gets wrong) |
|
||||
| CPU overclock * | Off (default), 2x, 4x: the N64's CPU gets 2 or 4 times the cycles per frame (the core's `CountPerOpDenomPot`), so games that slowed down on the console (GoldenEye, Perfect Dark, Conker) run smoother. A few games misbehave with it |
|
||||
| Audio processing * | Fast (HLE, default): rsp-hle does the RSP's audio work; Accurate (LLE): cxd4 runs the game's audio microcode (slower; for a game whose sound HLE gets wrong) |
|
||||
| Render threads * | CPU renderer (angrylion) workers: 1–12, default 6 |
|
||||
| Controller pak * | Controller Pak (default), Rumble Pak, none |
|
||||
| Stick dead zone | 0–30% |
|
||||
| Invert left stick | turns the analog stick's X (left/right), Y (up/down) or both round; Off by default |
|
||||
| Invert right stick | the same for the right stick's C buttons (Triangle / Circle stay C-up / C-down); Off by default |
|
||||
| Controller layout | what each DualSense control presses on the N64 pad: Cross, Circle, Square, Triangle, L1, R1, L2, R2 (separately), OPTIONS, the D-pad and the right stick's four directions, each set to an N64 button or nothing (the left stick stays the analog stick). From the game list it is every game's layout (in `mupen64plus-ps5.ini`); from the pause menu, the running game's own (`layouts/<ROM file>.txt`, gone again when it matches every game's) |
|
||||
| Continue where you left off | Ask (default): leaving a game from the pause menu saves where you were (`states/<ROM file>.resume`), and starting it again asks "Continue where you left off?" (✕ continue, □ start over, ○ back). Off: neither |
|
||||
| Game list view | Shelf (3D, default), Grid or List; the touchpad on the game list switches too |
|
||||
| Show FPS | the speed (`VI/s`, 100% = full speed) and, below it, where the emulation thread's time goes: `cpu` (VR4300 interpreter), `rsp` (rsp-hle + cxd4), `rdp` (the renderer: angrylion's rasteriser incl. waiting for its threads, or handing commands to the GPU), `vi` (the VI filter, or reading the GPU's picture back), `idle` (waiting for the TV: spare time). Also logged every 10 s in `boot.log` |
|
||||
| Volume | the game's sound, 0–100% (default 100%) |
|
||||
| Sound, Download covers | |
|
||||
| Check for updates | ask at start-up when a newer GitHub release exists (see [Updates](#updates)) |
|
||||
|
||||
**On the shelf:**
|
||||
**On the game list** (three views: the 3D shelf, a grid of covers, a list of titles with the cover beside it):
|
||||
|
||||
- Left/Right: choose a game
|
||||
- Cross: play
|
||||
- D-pad: choose a game (shelf: Left/Right; grid: all four directions; list: Up/Down)
|
||||
- L1/R1: 10 games back or on (shelf), a page up or down (grid, list)
|
||||
- Cross: play (a game you left from the pause menu asks whether to continue there)
|
||||
- Touchpad: the next view (remembered; also Settings → Game list view)
|
||||
- Triangle: settings
|
||||
- Square: fetch the cover again
|
||||
- OPTIONS: quit
|
||||
@@ -206,11 +251,14 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
|
||||
- **Names:** a ROM's name comes from its file name when that is already a No-Intro name ("F-Zero X
|
||||
(USA).z64"). Otherwise it comes from the CRC in its cartridge header, looked up in a table built from the
|
||||
core's `mupen64plus.ini` (1891 games, `tools/make_n64db.py`).
|
||||
- **Covers:** they come from libretro-thumbnails (Nintendo 64, `Named_Boxarts`). The source works exactly as
|
||||
in Snes9x PS5 (a prefetch at start-up, then a restart for new covers).
|
||||
- **When a cover doesn't download:** the catalog's names are GoodN64 names converted to No-Intro style, and
|
||||
some differ from libretro's in capitalisation, which the download URL is sensitive to. Name the ROM file the
|
||||
core's `mupen64plus.ini` (1891 games, `tools/make_n64db.py`). Its GoodN64 names are matched against
|
||||
libretro-thumbnails' file list (`tools/libretro-n64-boxarts.txt`) and take their spelling there, language
|
||||
tags included ("Diddy Kong Racing (USA) (En,Fr) (Rev 1)").
|
||||
- **Covers:** they come from libretro-thumbnails (Nintendo 64, `Named_Boxarts`), downloaded at start-up, before
|
||||
the app asks for `/data` (HTTPS works only then). When the game list has new games without covers, the app
|
||||
closes and its helper starts it again (`sceLncUtilLaunchApp`), so the next start fetches them; if it can't, the
|
||||
covers come the next time you open the app.
|
||||
- **When a cover doesn't download:** the game isn't on libretro-thumbnails under that name. Name the ROM file the
|
||||
No-Intro way, or put your own `.png` in `covers/`.
|
||||
|
||||
## Known limitations
|
||||
@@ -221,7 +269,7 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
Interpreter.
|
||||
- **The RSP is the next bottleneck.** cxd4 interprets every graphics microcode instruction (GoldenEye:
|
||||
~30% of the time); audio goes through rsp-hle (Audio processing: Fast).
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, a cheats menu, netplay.
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, netplay.
|
||||
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
|
||||
|
||||
## Building
|
||||
@@ -276,16 +324,18 @@ ROM, on Linux:
|
||||
N64PS5_PS5_ROOT=/tmp/n64 N64PS5_HOST_DUMP=150 N64PS5_HOST_MAX_FLIPS=200 build/host/mupen64plus-ps5-host test.z64
|
||||
```
|
||||
|
||||
The run writes flip 150 to a `.ppm` file. `N64PS5_HOST_PAD="frame:buttons;..."` scripts the pad (see
|
||||
`host/sce_host.cpp`).
|
||||
The run writes flip 150 to a `.ppm` file. `N64PS5_HOST_PAD="frame:buttons;..."` scripts the pad, and
|
||||
`N64PS5_HOST_IME="text|text..."` what is typed on the keyboard each time it opens (see `host/sce_host.cpp`).
|
||||
|
||||
**Dynarec.** `new_dynarec.c` is built with `-Dmprotect=n64ps5_jit_mprotect` (the core stays unmodified),
|
||||
**Dynarec.** `new_dynarec.c` is built with `-Dmprotect=n64ps5_jit_mprotect` (no change to the core's source),
|
||||
its NASM stubs are translated to GNU syntax in `n64/core/linkage_x64.S`, and the struct offsets they need are
|
||||
generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`. On the host,
|
||||
`N64PS5_JIT=direct` exercises the PS5's direct-memory way (an anonymous RWX mapping stands in for it).
|
||||
|
||||
**Art.** `tools/make_art.py app/sce_sys` redraws `icon0.png`, `background-source.png` and the BC7
|
||||
`pic0.dds`/`pic1.dds`.
|
||||
**Art.** The home screen's icon and background are pictures in `app/art/` (`icon.png`, `background.png`).
|
||||
`tools/make_art.py app/sce_sys --images app/art/icon.png app/art/background.png` fits them to `icon0.png`
|
||||
(512x512) and `background-source.png` (3840x2160) and writes the BC7 `pic0.dds`/`pic1.dds`; without
|
||||
`--images` it draws the old art itself.
|
||||
|
||||
## Source layout
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 460 KiB After Width: | Height: | Size: 7.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 314 KiB |
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99064",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
|
||||
"contentVersion": "00.007.000",
|
||||
"contentVersion": "00.007.005",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -30,6 +30,10 @@ std::string OrbisDir(const char* sub);
|
||||
bool OrbisPathsInit();
|
||||
// Folders that hold ROMs and exist right now (internal first, then USB drives).
|
||||
std::vector<std::string> OrbisRomRoots();
|
||||
// Every copy of the app folder (<base>/<TITLE_ID> with sce_sys/param.json and eboot.bin) in ShadowMountPlus's scan
|
||||
// paths: /data/homebrew, then USB and extended storage, at their root or in homebrew/ (N64PS5_PS5_HOMEBREW
|
||||
// replaces them all for the host tests).
|
||||
std::vector<std::string> OrbisAppFolders();
|
||||
|
||||
bool OrbisIsDir(const std::string& path);
|
||||
bool OrbisIsFile(const std::string& path);
|
||||
|
||||
@@ -42,6 +42,21 @@ int sceSystemServiceHideSplashScreen(void);
|
||||
// "exit" ends the app the way the system wants (PS5SX2's OrbisExitApp: exit()/_exit() end in SIGSYS)
|
||||
int sceSystemServiceLoadExec(const char* path, const char* argv[]);
|
||||
|
||||
// Starting a title from a payload (the helper: ProsperoUpdateJob.h, RelaunchIfPending), with the parameter
|
||||
// etaHEN's launcher passes.
|
||||
typedef struct
|
||||
{
|
||||
uint32_t size;
|
||||
int32_t user_id;
|
||||
uint32_t app_opt;
|
||||
uint64_t crash_report;
|
||||
uint64_t check_flag;
|
||||
} SceLncAppParam;
|
||||
int sceLncUtilLaunchApp(const char* title_id, const char* argv[], SceLncAppParam* param);
|
||||
int sceSystemServiceLaunchApp(const char* title_id, const char* argv[], SceLncAppParam* param);
|
||||
uint32_t sceLncUtilGetAppIdOfRunningBigApp(void);
|
||||
int sceLncUtilGetAppTitleId(uint32_t app_id, char* title_id);
|
||||
|
||||
// ---- libSceVideoOut ----------------------------------------------------------------------------------------
|
||||
typedef struct SceVideoOutBuffers
|
||||
{
|
||||
@@ -162,6 +177,57 @@ int sceUserServiceInitialize(void* params);
|
||||
int sceUserServiceGetForegroundUser(int32_t* user);
|
||||
int sceUserServiceGetLoginUserIdList(int32_t users[4]);
|
||||
|
||||
// ---- libSceSysmodule: a system module the app loads while it runs ---------------------------------------
|
||||
// Its functions are imported like the others, but they work only once the module is loaded (ps5-generals'
|
||||
// SDL_ps5kbm.c does the same for the keyboard 0x0106 and the mouse 0x00a9, the PS4's numbers).
|
||||
#define SCE_SYSMODULE_IME_DIALOG 0x0096
|
||||
int sceSysmoduleLoadModule(uint16_t id);
|
||||
|
||||
// ---- libSceImeDialog (the on-screen keyboard; PS4's layout, as shadPS4's ime_dialog.h has it) -------------
|
||||
// Called only after sceSysmoduleLoadModule(SCE_SYSMODULE_IME_DIALOG) succeeded (ProsperoKeyboard.cpp). Loading
|
||||
// the .sprx by its path doesn't work from the app (sceKernelLoadStartModule: 80020063).
|
||||
#define SCE_IME_TYPE_DEFAULT 0
|
||||
#define SCE_IME_TYPE_BASIC_LATIN 1
|
||||
#define SCE_IME_TYPE_NUMBER 4
|
||||
#define SCE_IME_HALIGN_CENTER 1
|
||||
#define SCE_IME_VALIGN_CENTER 1
|
||||
#define SCE_IME_DIALOG_MAX_TEXT_LENGTH 2048
|
||||
#define SCE_IME_DIALOG_STATUS_NONE 0
|
||||
#define SCE_IME_DIALOG_STATUS_RUNNING 1
|
||||
#define SCE_IME_DIALOG_STATUS_FINISHED 2
|
||||
#define SCE_IME_DIALOG_END_STATUS_OK 0
|
||||
#define SCE_IME_DIALOG_END_STATUS_USER_CANCELED 1
|
||||
#define SCE_IME_DIALOG_END_STATUS_ABORTED 2
|
||||
|
||||
typedef struct SceImeDialogParam
|
||||
{
|
||||
int32_t userId;
|
||||
int32_t type; // SCE_IME_TYPE_*
|
||||
uint64_t supportedLanguages; // 0: the system's
|
||||
int32_t enterLabel; // 0: Done
|
||||
int32_t inputMethod; // 0
|
||||
void* filter;
|
||||
uint32_t option;
|
||||
uint32_t maxTextLength; // characters, the buffer holds one more
|
||||
uint16_t* inputTextBuffer; // UTF-16: the text to start with, and the result
|
||||
float posx, posy; // the text field, 1920 x 1080
|
||||
int32_t horizontalAlignment, verticalAlignment;
|
||||
const uint16_t* placeholder;
|
||||
const uint16_t* title;
|
||||
int8_t reserved[16];
|
||||
} SceImeDialogParam;
|
||||
|
||||
typedef struct SceImeDialogResult
|
||||
{
|
||||
int32_t endstatus; // SCE_IME_DIALOG_END_STATUS_*
|
||||
int32_t reserved[12];
|
||||
} SceImeDialogResult;
|
||||
|
||||
int sceImeDialogInit(const SceImeDialogParam* param, const void* extended);
|
||||
int sceImeDialogGetStatus(void); // SCE_IME_DIALOG_STATUS_*
|
||||
int sceImeDialogGetResult(SceImeDialogResult* result);
|
||||
int sceImeDialogTerm(void);
|
||||
|
||||
// ---- libSceNet / libSceNetCtl / libSceSsl / libSceHttp2 (as the SDK's http2_get sample and PS5SX2) ----
|
||||
int sceNetInit(void);
|
||||
int sceNetPoolCreate(const char* name, int size, int flags);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "ProsperoCrash.h"
|
||||
#include "ProsperoInput.h"
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoKeyboard.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoSce.h"
|
||||
#include "ProsperoVideo.h"
|
||||
@@ -56,10 +57,10 @@ void WritePid()
|
||||
}
|
||||
|
||||
// One game, until the player leaves it. Returns false when the player chose "quit Mupen64Plus".
|
||||
bool PlayGame(const std::string& rom)
|
||||
bool PlayGame(const std::string& rom, bool resume)
|
||||
{
|
||||
std::string error;
|
||||
switch (emu::RunGame(rom, &error))
|
||||
switch (emu::RunGame(rom, &error, resume))
|
||||
{
|
||||
case emu::ExitReason::BackToList: return true;
|
||||
case emu::ExitReason::Quit: return false;
|
||||
@@ -106,6 +107,8 @@ int Run(int argc, char** argv)
|
||||
// A newer release on GitHub, downloaded now for the same reason (fe_update.h); offered further down.
|
||||
N64_STAGE(Boot, "update check");
|
||||
fe::UpdateOffer update = fe::CheckForUpdate();
|
||||
// the on-screen keyboard's system module (cheat names and codes): out of the sandbox it can't be loaded
|
||||
ps5keyboard::Preload();
|
||||
|
||||
// out of the sandbox, like PS5SX2: before it the app sees no /data and no USB drives
|
||||
N64_STAGE(Boot, "jailbreak");
|
||||
@@ -177,11 +180,12 @@ int Run(int argc, char** argv)
|
||||
std::string rom = (argc > 1 && argv[1] && OrbisIsFile(argv[1])) ? argv[1] : "";
|
||||
for (;;)
|
||||
{
|
||||
bool resume = false;
|
||||
if (rom.empty())
|
||||
rom = fe::Shelf();
|
||||
rom = fe::Shelf(&resume);
|
||||
if (rom.empty())
|
||||
break; // OPTIONS on the shelf
|
||||
const bool again = PlayGame(rom);
|
||||
const bool again = PlayGame(rom, resume);
|
||||
rom.clear();
|
||||
if (!again)
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Mupen64Plus PS5: the system's on-screen keyboard (ProsperoKeyboard.h).
|
||||
//
|
||||
// libSceImeDialog's functions are imported like the other system calls, but its module is one the app loads
|
||||
// itself, with sceSysmoduleLoadModule (ps5-generals' SDL port does the same for the keyboard and mouse
|
||||
// modules). That has to happen while the app is still in its sandbox: once it left it, a module asked for is
|
||||
// looked for at the sandbox's path and not found (80020063 on the console, by path or by sysmodule; ps5-native's
|
||||
// docs/CURL.md, "Elevated apps"), so main-boot.cpp calls Preload() before the jailbreak. Until the module loaded
|
||||
// its functions aren't called, so where it doesn't load the app still runs, only without typing. The dialog is the
|
||||
// system's: it draws itself over the app's picture and takes the controller while it is up. sceImeDialogInit
|
||||
// opens it, sceImeDialogGetStatus says when the player closed it, sceImeDialogGetResult how (Done or canceled),
|
||||
// and sceImeDialogTerm ends it.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "ProsperoKeyboard.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoSce.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
static_assert(sizeof(SceImeDialogParam) == 0x60, "SceImeDialogParam is 0x60 bytes");
|
||||
|
||||
namespace ps5keyboard
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool s_tried = false;
|
||||
int s_load = -1; // sceSysmoduleLoadModule's answer: 0 (or more) loaded
|
||||
|
||||
bool Load()
|
||||
{
|
||||
if (!s_tried)
|
||||
{
|
||||
s_tried = true;
|
||||
s_load = sceSysmoduleLoadModule(SCE_SYSMODULE_IME_DIALOG);
|
||||
OrbisLog("[keyboard] sceSysmoduleLoadModule(IME dialog %#x) -> %x", SCE_SYSMODULE_IME_DIALOG, unsigned(s_load));
|
||||
}
|
||||
return s_load >= 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Preload()
|
||||
{
|
||||
Load();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// UTF-8 -> UTF-16 (at most max units), and back
|
||||
std::vector<uint16_t> ToUtf16(const std::string& s, size_t max)
|
||||
{
|
||||
std::vector<uint16_t> out;
|
||||
for (size_t i = 0; i < s.size();)
|
||||
{
|
||||
const unsigned char c = (unsigned char)s[i];
|
||||
uint32_t cp = c;
|
||||
int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
|
||||
if (extra)
|
||||
cp = c & (0x3F >> extra);
|
||||
i++;
|
||||
for (; extra > 0 && i < s.size(); extra--, i++)
|
||||
cp = cp << 6 | ((unsigned char)s[i] & 0x3F);
|
||||
const size_t need = cp >= 0x10000 ? 2 : 1;
|
||||
if (out.size() + need > max)
|
||||
break;
|
||||
if (cp >= 0x10000)
|
||||
{
|
||||
cp -= 0x10000;
|
||||
out.push_back(uint16_t(0xD800 | (cp >> 10)));
|
||||
out.push_back(uint16_t(0xDC00 | (cp & 0x3FF)));
|
||||
}
|
||||
else
|
||||
out.push_back(uint16_t(cp));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ToUtf8(const uint16_t* s)
|
||||
{
|
||||
std::string out;
|
||||
for (size_t i = 0; s[i]; i++)
|
||||
{
|
||||
uint32_t cp = s[i];
|
||||
if (cp >= 0xD800 && cp < 0xDC00 && s[i + 1] >= 0xDC00 && s[i + 1] < 0xE000)
|
||||
{
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (s[i + 1] - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
if (cp < 0x80)
|
||||
out += char(cp);
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
out += char(0xC0 | (cp >> 6));
|
||||
out += char(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
out += char(0xE0 | (cp >> 12));
|
||||
out += char(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += char(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
out += char(0xF0 | (cp >> 18));
|
||||
out += char(0x80 | ((cp >> 12) & 0x3F));
|
||||
out += char(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += char(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Fail(std::string* error, const char* what, int code)
|
||||
{
|
||||
char text[96];
|
||||
snprintf(text, sizeof(text), "The keyboard did not open (%s %08X)", what, unsigned(code));
|
||||
OrbisLog("[keyboard] %s", text);
|
||||
if (error)
|
||||
*error = text;
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Edit(const std::string& title, std::string& text, int max_chars, Type type, const std::function<void()>& frame,
|
||||
std::string* error)
|
||||
{
|
||||
if (!Load())
|
||||
return Fail(error, "module", s_load);
|
||||
const size_t max = size_t(max_chars < 1 ? 1 : max_chars > SCE_IME_DIALOG_MAX_TEXT_LENGTH ? SCE_IME_DIALOG_MAX_TEXT_LENGTH : max_chars);
|
||||
std::vector<uint16_t> buffer = ToUtf16(text, max);
|
||||
buffer.resize(max + 1, 0);
|
||||
std::vector<uint16_t> title16 = ToUtf16(title, 128);
|
||||
title16.push_back(0);
|
||||
|
||||
SceImeDialogParam param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
int32_t user = -1;
|
||||
sceUserServiceGetForegroundUser(&user);
|
||||
param.userId = user;
|
||||
param.type = type == Type::Number ? SCE_IME_TYPE_NUMBER : type == Type::Latin ? SCE_IME_TYPE_BASIC_LATIN : SCE_IME_TYPE_DEFAULT;
|
||||
param.maxTextLength = uint32_t(max);
|
||||
param.inputTextBuffer = buffer.data();
|
||||
param.posx = 960.0f;
|
||||
param.posy = 540.0f;
|
||||
param.horizontalAlignment = SCE_IME_HALIGN_CENTER;
|
||||
param.verticalAlignment = SCE_IME_VALIGN_CENTER;
|
||||
param.title = title16.data();
|
||||
|
||||
int r = sceImeDialogInit(¶m, nullptr);
|
||||
if (r != 0)
|
||||
{
|
||||
// one left open (a crash of the menu around it): end that one and try again
|
||||
sceImeDialogTerm();
|
||||
r = sceImeDialogInit(¶m, nullptr);
|
||||
}
|
||||
OrbisLog("[keyboard] sceImeDialogInit(user %x, type %d, max %zu) -> %x", unsigned(user), param.type, max, unsigned(r));
|
||||
if (r != 0)
|
||||
return Fail(error, "init", r);
|
||||
|
||||
// running until the player closes it (the first frames may still say "none" while it comes up)
|
||||
int status = SCE_IME_DIALOG_STATUS_RUNNING;
|
||||
for (int n = 0;; n++)
|
||||
{
|
||||
status = sceImeDialogGetStatus();
|
||||
if (status != SCE_IME_DIALOG_STATUS_RUNNING && !(status == SCE_IME_DIALOG_STATUS_NONE && n < 30))
|
||||
break;
|
||||
frame();
|
||||
}
|
||||
SceImeDialogResult result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
result.endstatus = SCE_IME_DIALOG_END_STATUS_ABORTED;
|
||||
const int rr = status == SCE_IME_DIALOG_STATUS_FINISHED ? sceImeDialogGetResult(&result) : status;
|
||||
sceImeDialogTerm();
|
||||
OrbisLog("[keyboard] closed: status %x, result %x, end %d", unsigned(status), unsigned(rr), int(result.endstatus));
|
||||
if (status != SCE_IME_DIALOG_STATUS_FINISHED || rr != 0)
|
||||
return Fail(error, "status", status != SCE_IME_DIALOG_STATUS_FINISHED ? status : rr);
|
||||
if (result.endstatus != SCE_IME_DIALOG_END_STATUS_OK)
|
||||
return false;
|
||||
buffer[max] = 0;
|
||||
text = ToUtf8(buffer.data());
|
||||
return true;
|
||||
}
|
||||
} // namespace ps5keyboard
|
||||
@@ -0,0 +1,27 @@
|
||||
// Mupen64Plus PS5: the system's on-screen keyboard (libSceImeDialog), for naming cheats and typing codes.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace ps5keyboard
|
||||
{
|
||||
// Loads the keyboard's system module. Before the app leaves the sandbox (main-boot.cpp): afterwards the system
|
||||
// looks for modules at a path inside the sandbox and doesn't find them (80020063; ps5-native's docs/CURL.md,
|
||||
// "Elevated apps"). The module stays loaded.
|
||||
void Preload();
|
||||
|
||||
enum class Type
|
||||
{
|
||||
Text, // any text: a name
|
||||
Number, // digits
|
||||
Latin, // letters and digits: GameShark codes
|
||||
};
|
||||
|
||||
// Opens the keyboard over the picture with `text` in its field, titled `title`, and calls `frame` (show one
|
||||
// frame) until the player closes it. True with the new text (UTF-8) when the player pressed Done; false when they
|
||||
// canceled it, or when the keyboard could not open -- then `error` says why ("the keyboard did not open (0x...)").
|
||||
bool Edit(const std::string& title, std::string& text, int max_chars, Type type, const std::function<void()>& frame,
|
||||
std::string* error = nullptr);
|
||||
} // namespace ps5keyboard
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoSce.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
@@ -182,6 +183,159 @@ std::string JobPath()
|
||||
return OrbisDir("update") + "/job.txt";
|
||||
}
|
||||
|
||||
std::string RelaunchPath()
|
||||
{
|
||||
return OrbisDir("update") + "/relaunch.txt";
|
||||
}
|
||||
|
||||
bool WriteRelaunch(int pid, std::string& why)
|
||||
{
|
||||
OrbisMkdirs(OrbisDir("update"));
|
||||
const std::string path = RelaunchPath(), tmp = path + ".part";
|
||||
FILE* f = fopen(tmp.c_str(), "w");
|
||||
if (!f)
|
||||
{
|
||||
why = "can't write " + tmp;
|
||||
return false;
|
||||
}
|
||||
fprintf(f, "pid=%d\ntime=%lld\n", pid, (long long)time(nullptr));
|
||||
const bool ok = fclose(f) == 0;
|
||||
if (!ok || rename(tmp.c_str(), path.c_str()) != 0)
|
||||
{
|
||||
unlink(tmp.c_str());
|
||||
why = "can't write " + path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kNoApp = 0xffffffffu;
|
||||
constexpr uint32_t kAlreadyRunning = 0x8094000cu;
|
||||
|
||||
// The title in the foreground ("" when none), as the system reports it.
|
||||
std::string RunningTitle()
|
||||
{
|
||||
const uint32_t id = sceLncUtilGetAppIdOfRunningBigApp();
|
||||
if (id == kNoApp)
|
||||
return "";
|
||||
char tid[32] = {};
|
||||
if (sceLncUtilGetAppTitleId(id, tid) != 0)
|
||||
return "?";
|
||||
return tid;
|
||||
}
|
||||
|
||||
// Starts the title. Which call and which user a payload's launch is accepted with isn't worth guessing
|
||||
// (PS5_Vulkan's ps5vkctl, confirmed on the console: etaHEN launches through sceLncUtilLaunchApp as the foreground
|
||||
// user, AirPSX through sceSystemServiceLaunchApp as a logged-in one), so the ways are tried in turn, and
|
||||
// after each the system is asked which title is up rather than trusting the return code.
|
||||
bool LaunchTitle(std::string& how)
|
||||
{
|
||||
sceUserServiceInitialize(nullptr); // the user getters answer only once it is initialized
|
||||
int32_t fg = -1;
|
||||
if (sceUserServiceGetForegroundUser(&fg) != 0)
|
||||
fg = -1;
|
||||
int32_t users[4] = {-1, -1, -1, -1};
|
||||
int32_t login = -1;
|
||||
if (sceUserServiceGetLoginUserIdList(users) == 0)
|
||||
for (int32_t u : users)
|
||||
if (u >= 0)
|
||||
{
|
||||
login = u;
|
||||
break;
|
||||
}
|
||||
struct Attempt
|
||||
{
|
||||
const char* name;
|
||||
int (*call)(const char*, const char*[], SceLncAppParam*);
|
||||
int32_t user;
|
||||
uint64_t check_flag;
|
||||
};
|
||||
const Attempt attempts[] = {
|
||||
{"sceLncUtilLaunchApp, foreground user", sceLncUtilLaunchApp, fg, 0},
|
||||
{"sceLncUtilLaunchApp, login user", sceLncUtilLaunchApp, login, 0},
|
||||
{"sceSystemServiceLaunchApp, login user", sceSystemServiceLaunchApp, login, 0},
|
||||
{"sceLncUtilLaunchApp, login user, skip check", sceLncUtilLaunchApp, login, 1},
|
||||
};
|
||||
for (const Attempt& a : attempts)
|
||||
{
|
||||
if (a.user < 0)
|
||||
continue;
|
||||
SceLncAppParam param;
|
||||
memset(¶m, 0, sizeof(param));
|
||||
param.size = uint32_t(sizeof(param));
|
||||
param.user_id = a.user;
|
||||
param.check_flag = a.check_flag;
|
||||
const uint32_t rc = uint32_t(a.call(N64PS5_TITLE_ID, nullptr, ¶m));
|
||||
// a call that reports an error may still have started it: look for 2 s before the next way
|
||||
bool up = false;
|
||||
for (int i = 0; i < 8 && !up && rc != 0 && rc != kAlreadyRunning; i++)
|
||||
{
|
||||
up = RunningTitle() == N64PS5_TITLE_ID;
|
||||
if (!up)
|
||||
usleep(250 * 1000);
|
||||
}
|
||||
OrbisLog("[relaunch] %s (user %d) -> 0x%08x%s", a.name, int(a.user), rc, up ? ", the app is up" : "");
|
||||
if (rc == 0 || rc == kAlreadyRunning || up)
|
||||
{
|
||||
how = a.name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
OrbisLog("[relaunch] no launch call worked (foreground user %d, login user %d)", int(fg), int(login));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RelaunchIfPending()
|
||||
{
|
||||
FILE* f = fopen(RelaunchPath().c_str(), "r");
|
||||
if (!f)
|
||||
return;
|
||||
int pid = 0;
|
||||
long long when = 0;
|
||||
char line[128];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
if (strncmp(line, "pid=", 4) == 0)
|
||||
pid = atoi(line + 4);
|
||||
else if (strncmp(line, "time=", 5) == 0)
|
||||
when = atoll(line + 5);
|
||||
}
|
||||
fclose(f);
|
||||
// take the job first: a second helper started meanwhile must not run it too
|
||||
const std::string taken = RelaunchPath() + ".running";
|
||||
if (rename(RelaunchPath().c_str(), taken.c_str()) != 0)
|
||||
return;
|
||||
const long long age = (long long)time(nullptr) - when;
|
||||
if (pid <= 0 || when <= 0 || age < 0 || age > 120)
|
||||
{
|
||||
OrbisLog("[relaunch] dropping an old job (pid %d, %lld s old)", pid, age);
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
OrbisLog("[relaunch] starting " N64PS5_TITLE_ID " again once pid %d has closed", pid);
|
||||
for (int i = 0; i < 300 && ProcessAlive(pid); i++) // 30 s
|
||||
usleep(100 * 1000);
|
||||
if (ProcessAlive(pid))
|
||||
{
|
||||
OrbisLog("[relaunch] pid %d still running after 30 s: not starting it again", pid);
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
// the system still lists the title for a moment after its process ends
|
||||
for (int i = 0; i < 40 && RunningTitle() == N64PS5_TITLE_ID; i++)
|
||||
usleep(250 * 1000);
|
||||
usleep(500 * 1000);
|
||||
std::string how;
|
||||
if (LaunchTitle(how))
|
||||
OrbisLog("[relaunch] " N64PS5_TITLE_ID " started again (%s)", how.c_str());
|
||||
else
|
||||
ProsperoNotify("Mupen64Plus PS5: open the app again to download the covers of your new games.");
|
||||
unlink(taken.c_str());
|
||||
}
|
||||
|
||||
bool Write(const Job& job, std::string& why)
|
||||
{
|
||||
OrbisMkdirs(OrbisDir("update"));
|
||||
|
||||
@@ -57,4 +57,14 @@ std::string EbootMode(const std::string& app_dir);
|
||||
|
||||
// The helper payload, at start: performs a pending job (and deletes it). Returns at once when there is none.
|
||||
void RunIfPending();
|
||||
|
||||
// ---- starting the app again (covers for new games, frontend/fe_prefetch.h) ---------------------------------
|
||||
// The app can only download before it asks for /data, so to fetch covers for new games it closes and its
|
||||
// helper starts it again: the app writes this job (its pid), starts a fresh helper through the ELF loader and
|
||||
// closes; the helper (RelaunchIfPending) waits for it to exit and launches the title the way the home screen
|
||||
// does (sceLncUtilLaunchApp, as PS5_Vulkan's ps5vkctl and etaHEN launch titles). If no launch call works, a
|
||||
// notification says to open the app again.
|
||||
std::string RelaunchPath(); // /data/mupen64plus/update/relaunch.txt
|
||||
bool WriteRelaunch(int pid, std::string& why);
|
||||
void RelaunchIfPending();
|
||||
} // namespace updatejob
|
||||
@@ -102,6 +102,39 @@ std::vector<std::string> OrbisRomRoots()
|
||||
return roots;
|
||||
}
|
||||
|
||||
std::vector<std::string> OrbisAppFolders()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
std::vector<std::string> bases;
|
||||
const char* hb = getenv("N64PS5_PS5_HOMEBREW"); // host tests
|
||||
bases.push_back(hb && *hb ? hb : "/data/homebrew");
|
||||
if (!(hb && *hb))
|
||||
{
|
||||
char buf[64];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "/mnt/usb%d/homebrew", i);
|
||||
bases.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "/mnt/usb%d", i);
|
||||
bases.push_back(buf);
|
||||
}
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "/mnt/ext%d/homebrew", i);
|
||||
bases.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "/mnt/ext%d", i);
|
||||
bases.push_back(buf);
|
||||
}
|
||||
}
|
||||
for (const std::string& b : bases)
|
||||
{
|
||||
const std::string dir = b + "/" N64PS5_TITLE_ID;
|
||||
if (OrbisIsFile(dir + "/sce_sys/param.json") && OrbisIsFile(dir + "/eboot.bin"))
|
||||
out.push_back(dir);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Lines logged before the log file is open (the native app logs its jailbreak before /data is visible).
|
||||
static std::vector<std::string>& EarlyLines()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
// Mupen64Plus PS5 frontend: the cheat database (fe_cheats.h).
|
||||
//
|
||||
// mupencheat.txt, as the core ships it (converted from Project64's database by Gent):
|
||||
//
|
||||
// crc 635A2BFF-8B022326-C:45 the ROM header's CRC1, CRC2 and country code
|
||||
// gn Super Mario 64 (U)
|
||||
// cn Infinite Lives a cheat (a '\' puts it in a group)
|
||||
// cd Some explanation optional
|
||||
// 8033B21D 0064 its codes: address, value
|
||||
// 8013CD0E ???? 0001:"1 Lap",... a value to choose from a list
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fe_cheats.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
|
||||
#include "zlib.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifndef CHEATS_Z
|
||||
#error "CHEATS_Z: the compressed mupencheat.txt (the Makefile builds it)"
|
||||
#endif
|
||||
|
||||
#define FE_INCBIN(sym, path) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".balign 16\n" \
|
||||
".global " #sym "_begin\n" #sym "_begin:\n" \
|
||||
".incbin \"" path "\"\n" \
|
||||
".global " #sym "_end\n" #sym "_end:\n" \
|
||||
".previous\n"); \
|
||||
extern "C" const unsigned char sym##_begin[]; \
|
||||
extern "C" const unsigned char sym##_end[];
|
||||
|
||||
// 4 bytes: the text's size (little endian), then the text, zlib-compressed
|
||||
FE_INCBIN(n64ps5_cheats_z, CHEATS_Z)
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace cheats
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string Inflate()
|
||||
{
|
||||
const unsigned char* z = n64ps5_cheats_z_begin;
|
||||
const size_t zsize = size_t(n64ps5_cheats_z_end - n64ps5_cheats_z_begin);
|
||||
if (zsize < 4)
|
||||
return "";
|
||||
uLongf size = uLongf(z[0]) | uLongf(z[1]) << 8 | uLongf(z[2]) << 16 | uLongf(z[3]) << 24;
|
||||
std::string text(size, '\0');
|
||||
if (uncompress(reinterpret_cast<Bytef*>(&text[0]), &size, z + 4, uLong(zsize - 4)) != Z_OK)
|
||||
{
|
||||
OrbisLog("[cheats] the built-in cheat list is damaged");
|
||||
return "";
|
||||
}
|
||||
text.resize(size);
|
||||
return text;
|
||||
}
|
||||
|
||||
// a code line: "8033B21D 0064" or "8013CD0E ???? 0001:"1 Lap",0002:"2 Laps""
|
||||
bool ParseCode(const std::string& line, Cheat& c)
|
||||
{
|
||||
char* end = nullptr;
|
||||
const unsigned long address = strtoul(line.c_str(), &end, 16);
|
||||
if (end == line.c_str() || *end != ' ')
|
||||
return false;
|
||||
const std::string rest = end + 1;
|
||||
if (rest.compare(0, 4, "????") == 0)
|
||||
{
|
||||
c.addresses.push_back(uint32_t(address));
|
||||
c.values.push_back(-1);
|
||||
// the choices: hex value, ':', quoted name, separated by commas. Every "????" of a cheat takes the same
|
||||
// choice; the list is on one of its lines (28 cheats leave the first one's empty).
|
||||
std::vector<std::pair<std::string, int>> options;
|
||||
size_t at = 4;
|
||||
while (at < rest.size())
|
||||
{
|
||||
while (at < rest.size() && (rest[at] == ' ' || rest[at] == ','))
|
||||
at++;
|
||||
const size_t colon = rest.find(':', at);
|
||||
const size_t q1 = colon == std::string::npos ? colon : rest.find('"', colon);
|
||||
const size_t q2 = q1 == std::string::npos ? q1 : rest.find('"', q1 + 1);
|
||||
if (q2 == std::string::npos)
|
||||
break;
|
||||
const int value = int(strtol(rest.substr(at, colon - at).c_str(), nullptr, 16));
|
||||
options.emplace_back(rest.substr(q1 + 1, q2 - q1 - 1), value);
|
||||
at = q2 + 1;
|
||||
}
|
||||
if (c.options.empty())
|
||||
c.options = std::move(options);
|
||||
return true;
|
||||
}
|
||||
c.addresses.push_back(uint32_t(address));
|
||||
c.values.push_back(int(strtol(rest.c_str(), nullptr, 16)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// codes the core applies only while the GameShark's button is held
|
||||
bool NeedsGameSharkButton(const Cheat& c)
|
||||
{
|
||||
for (uint32_t a : c.addresses)
|
||||
{
|
||||
const uint32_t type = a & 0xFF000000u;
|
||||
if (type == 0x88000000u || type == 0x89000000u || type == 0xA8000000u || type == 0xA9000000u ||
|
||||
(type >= 0xD8000000u && type <= 0xDB000000u))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// a cheat the menu can offer: it has codes, needs no GameShark button, and a choice has its list
|
||||
bool Usable(const Cheat& c)
|
||||
{
|
||||
if (c.addresses.empty() || NeedsGameSharkButton(c))
|
||||
return false;
|
||||
for (int v : c.values)
|
||||
if (v < 0 && c.options.empty())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game)
|
||||
{
|
||||
std::vector<Cheat> out;
|
||||
const std::string text = Inflate();
|
||||
char want[48];
|
||||
snprintf(want, sizeof(want), "crc %08X-%08X-C:%02X", unsigned(crc1), unsigned(crc2), unsigned(country));
|
||||
size_t at = text.find(want);
|
||||
if (at == std::string::npos)
|
||||
return out;
|
||||
at = text.find('\n', at);
|
||||
int skipped = 0;
|
||||
Cheat* cur = nullptr;
|
||||
while (at != std::string::npos && at + 1 < text.size())
|
||||
{
|
||||
const size_t start = at + 1;
|
||||
at = text.find('\n', start);
|
||||
std::string line = text.substr(start, at == std::string::npos ? std::string::npos : at - start);
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
if (line.compare(0, 4, "crc ") == 0)
|
||||
break; // the next game
|
||||
if (line.compare(0, 3, "gn ") == 0)
|
||||
{
|
||||
if (game)
|
||||
*game = line.substr(3);
|
||||
}
|
||||
else if (line.compare(0, 4, " cn ") == 0)
|
||||
{
|
||||
if (cur && !Usable(*cur))
|
||||
{
|
||||
skipped++;
|
||||
out.pop_back();
|
||||
}
|
||||
out.emplace_back();
|
||||
cur = &out.back();
|
||||
cur->name = line.substr(4);
|
||||
}
|
||||
else if (cur && line.compare(0, 5, " cd ") == 0)
|
||||
cur->note = line.substr(5);
|
||||
else if (cur && line.size() > 2 && line[0] == ' ' && line[1] == ' ')
|
||||
ParseCode(line.substr(2), *cur);
|
||||
}
|
||||
if (cur && !Usable(*cur))
|
||||
{
|
||||
skipped++;
|
||||
out.pop_back();
|
||||
}
|
||||
if (skipped)
|
||||
OrbisLog("[cheats] %d cheat(s) left out (the GameShark's button, or no codes)", skipped);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<Cheat> Parse(const std::string& text)
|
||||
{
|
||||
std::vector<Cheat> out;
|
||||
size_t at = 0;
|
||||
while (at < text.size())
|
||||
{
|
||||
size_t end = text.find('\n', at);
|
||||
if (end == std::string::npos)
|
||||
end = text.size();
|
||||
std::string line = text.substr(at, end - at);
|
||||
at = end + 1;
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||
line.pop_back();
|
||||
size_t lead = 0;
|
||||
while (lead < line.size() && (line[lead] == ' ' || line[lead] == '\t'))
|
||||
lead++;
|
||||
line.erase(0, lead);
|
||||
if (line.empty() || line[0] == '#' || line.compare(0, 4, "crc ") == 0 || line.compare(0, 3, "gn ") == 0)
|
||||
continue;
|
||||
if (line.compare(0, 3, "cn ") == 0)
|
||||
{
|
||||
out.emplace_back();
|
||||
out.back().name = line.substr(3);
|
||||
out.back().mine = true;
|
||||
}
|
||||
else if (!out.empty() && line.compare(0, 3, "cd ") == 0)
|
||||
out.back().note = line.substr(3);
|
||||
else if (!out.empty())
|
||||
ParseCode(line, out.back());
|
||||
}
|
||||
std::vector<Cheat> usable;
|
||||
for (Cheat& c : out)
|
||||
if (!c.name.empty() && Usable(c))
|
||||
usable.push_back(std::move(c));
|
||||
return usable;
|
||||
}
|
||||
|
||||
std::string Format(const std::vector<Cheat>& list)
|
||||
{
|
||||
std::string out = "# Mupen64Plus PS5: your own cheats for this game, in the format of mupen64plus's cheat list.\n"
|
||||
"# A cheat is a \" cn name\" line, then its codes: an address and a value, \"8033B21D 0064\".\n";
|
||||
for (const Cheat& c : list)
|
||||
{
|
||||
out += " cn " + c.name + "\n";
|
||||
if (!c.note.empty())
|
||||
out += " cd " + c.note + "\n";
|
||||
char code[24];
|
||||
bool listed = false; // a cheat with choices lists them on its first "????" line
|
||||
for (size_t i = 0; i < c.addresses.size(); i++)
|
||||
{
|
||||
if (c.values[i] >= 0)
|
||||
snprintf(code, sizeof(code), " %08X %04X", unsigned(c.addresses[i]), unsigned(c.values[i] & 0xFFFF));
|
||||
else
|
||||
snprintf(code, sizeof(code), " %08X ????", unsigned(c.addresses[i]));
|
||||
out += code;
|
||||
if (c.values[i] < 0 && !listed)
|
||||
{
|
||||
listed = true;
|
||||
for (size_t k = 0; k < c.options.size(); k++)
|
||||
{
|
||||
snprintf(code, sizeof(code), "%s%04X:", k ? "," : " ", unsigned(c.options[k].second & 0xFFFF));
|
||||
out += code + ("\"" + c.options[k].first + "\"");
|
||||
}
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ParseCodes(const std::string& text, Cheat& c, std::string* error)
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
std::string cur;
|
||||
for (const char ch : text + " ")
|
||||
{
|
||||
if (ch == ' ' || ch == ',' || ch == '\n' || ch == '\r' || ch == '\t' || ch == ';' || ch == ':' || ch == '+')
|
||||
{
|
||||
if (!cur.empty())
|
||||
tokens.push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
else
|
||||
cur += ch;
|
||||
}
|
||||
auto hex = [](const std::string& t, size_t max) {
|
||||
if (t.empty() || t.size() > max)
|
||||
return false;
|
||||
for (const char ch : t)
|
||||
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
Cheat out;
|
||||
for (size_t i = 0, step = 2; i < tokens.size(); i += step)
|
||||
{
|
||||
// "8033B21D0064" as one token is a code too
|
||||
std::string address = tokens[i], value = i + 1 < tokens.size() ? tokens[i + 1] : "";
|
||||
step = 2;
|
||||
if (address.size() == 12 && hex(address, 12))
|
||||
{
|
||||
value = address.substr(8);
|
||||
address.resize(8);
|
||||
step = 1;
|
||||
}
|
||||
if (address.size() != 8 || !hex(address, 8) || !hex(value, 4))
|
||||
{
|
||||
if (error)
|
||||
*error = "\"" + tokens[i] + (value.empty() ? "" : " " + value) +
|
||||
"\" is not a code: a code is 8 hex digits and 4 more, like 8033B21D 0064";
|
||||
return false;
|
||||
}
|
||||
out.addresses.push_back(uint32_t(strtoul(address.c_str(), nullptr, 16)));
|
||||
out.values.push_back(int(strtol(value.c_str(), nullptr, 16)));
|
||||
}
|
||||
if (out.addresses.empty())
|
||||
{
|
||||
if (error)
|
||||
*error = "No codes: type them like 8033B21D 0064";
|
||||
return false;
|
||||
}
|
||||
if (NeedsGameSharkButton(out))
|
||||
{
|
||||
if (error)
|
||||
*error = "Codes starting 88, 89, A8, A9 or D8 to DB need the GameShark's button, which a DualSense hasn't got";
|
||||
return false;
|
||||
}
|
||||
c.addresses = std::move(out.addresses);
|
||||
c.values = std::move(out.values);
|
||||
c.options.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CodesText(const Cheat& c)
|
||||
{
|
||||
std::string out;
|
||||
char code[24];
|
||||
for (size_t i = 0; i < c.addresses.size(); i++)
|
||||
{
|
||||
int v = c.values[i];
|
||||
if (v < 0)
|
||||
v = c.options.empty() ? 0 : c.options[size_t(c.state > 0 ? c.state - 1 : 0)].second;
|
||||
snprintf(code, sizeof(code), "%s%08X %04X", out.empty() ? "" : ", ", unsigned(c.addresses[i]), unsigned(v & 0xFFFF));
|
||||
out += code;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string DisplayName(const std::string& name)
|
||||
{
|
||||
std::string s = name;
|
||||
for (size_t i = s.find('\\'); i != std::string::npos; i = s.find('\\', i + 2))
|
||||
s.replace(i, 1, ": ");
|
||||
return s;
|
||||
}
|
||||
} // namespace cheats
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,48 @@
|
||||
// Mupen64Plus PS5 frontend: cheats -- the GameShark codes of mupen64plus-core's data/mupencheat.txt (built into
|
||||
// eboot.bin, compressed) and the player's own (found with the cheat search or typed in), switched on and off in
|
||||
// the pause menu and remembered per game (fe_emu.cpp).
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace cheats
|
||||
{
|
||||
struct Cheat
|
||||
{
|
||||
std::string name; // as in the file: "Play As\Player 1" is "Player 1" in the group "Play As"
|
||||
std::string note; // what to know before using it (its "cd" line), often empty
|
||||
std::vector<uint32_t> addresses;
|
||||
std::vector<int> values; // -1: the chosen option's value
|
||||
std::vector<std::pair<std::string, int>> options; // a cheat with choices ("????"), else empty
|
||||
int state = 0; // 0 off; then 1 on, or with options 1 + the chosen option's index
|
||||
bool mine = false; // the player's own (found with the cheat search or typed in), not the database's
|
||||
int uid = 0; // its name in the core while the game runs (fe_emu.cpp)
|
||||
int States() const { return options.empty() ? 2 : int(options.size()) + 1; }
|
||||
};
|
||||
|
||||
// The cheats of the game whose ROM header has these CRCs and country code (the database's "crc" lines), in
|
||||
// the database's order; `game` gets its name there. Cheats that need the GameShark's button are left out:
|
||||
// the DualSense has none.
|
||||
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game);
|
||||
|
||||
// The player's own cheats: a file in the database's format (" cn name" lines, each followed by its codes,
|
||||
// "8033B21D 0064"), read leniently so one written on a PC works too; and back.
|
||||
std::vector<Cheat> Parse(const std::string& text);
|
||||
std::string Format(const std::vector<Cheat>& list);
|
||||
|
||||
// Codes typed in: pairs of an address (8 hex digits) and a value (up to 4), separated by spaces, commas or new
|
||||
// lines. False with `error` set when they aren't (or need the GameShark's button).
|
||||
bool ParseCodes(const std::string& text, Cheat& c, std::string* error);
|
||||
// "8033B21D 0064, 8033B21E 0001" (the chosen option's value, or the first, where the code has "????")
|
||||
std::string CodesText(const Cheat& c);
|
||||
|
||||
// "Play As\Player 1" -> "Play As: Player 1"
|
||||
std::string DisplayName(const std::string& name);
|
||||
} // namespace cheats
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,311 @@
|
||||
// Mupen64Plus PS5 frontend: anti-aliased shapes (fe_draw.h). Rounded rectangles and circles use their signed
|
||||
// distance (coverage = 0.5 - distance), the triangles 4 x 4 samples per pixel: menus only, a few per frame.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fe_draw.h"
|
||||
|
||||
#include "ProsperoVideo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace gfx
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr int W = ps5video::kWidth;
|
||||
constexpr int H = ps5video::kHeight;
|
||||
|
||||
inline float Clamp01(float v)
|
||||
{
|
||||
return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
|
||||
}
|
||||
|
||||
inline void Put(int x, int y, uint32_t color, float coverage, int alpha)
|
||||
{
|
||||
if (coverage <= 0.0f || x < 0 || y < 0 || x >= W || y >= H)
|
||||
return;
|
||||
uint32_t& p = ps5video::Surface()[size_t(y) * W + x];
|
||||
p = Blend(p, color, int(coverage * alpha + 0.5f));
|
||||
}
|
||||
|
||||
struct RoundRect
|
||||
{
|
||||
float cx, cy, hw, hh, r;
|
||||
RoundRect(int x, int y, int w, int h, int radius)
|
||||
{
|
||||
hw = w * 0.5f;
|
||||
hh = h * 0.5f;
|
||||
cx = x + hw;
|
||||
cy = y + hh;
|
||||
r = std::max(0.0f, std::min(float(radius), std::min(hw, hh)));
|
||||
}
|
||||
// signed distance of a pixel's centre to the outline (negative inside)
|
||||
float Distance(int x, int y) const
|
||||
{
|
||||
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
|
||||
const float qx = ax - (hw - r), qy = ay - (hh - r);
|
||||
if (qx <= 0.0f || qy <= 0.0f)
|
||||
return std::max(ax - hw, ay - hh);
|
||||
return std::sqrt(qx * qx + qy * qy) - r;
|
||||
}
|
||||
// deep inside, away from the edges and corners by more than `band`
|
||||
bool Inside(int x, int y, float band) const
|
||||
{
|
||||
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
|
||||
return (ax < hw - r && ay < hh - band) || (ay < hh - r && ax < hw - band);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
uint32_t Blend(uint32_t dst, uint32_t color, int alpha)
|
||||
{
|
||||
if (alpha <= 0)
|
||||
return dst;
|
||||
const uint32_t a = uint32_t(std::min(alpha, 255));
|
||||
const uint32_t w = a + (a >> 7);
|
||||
const uint32_t iw = 256 - w;
|
||||
const uint32_t rb = (((dst & 0xff00ffu) * iw + (color & 0xff00ffu) * w) >> 8) & 0xff00ffu;
|
||||
const uint32_t g = (((dst & 0x00ff00u) * iw + (color & 0x00ff00u) * w) >> 8) & 0x00ff00u;
|
||||
return 0xff000000u | rb | g;
|
||||
}
|
||||
|
||||
uint32_t Mix(uint32_t a, uint32_t b, float t)
|
||||
{
|
||||
return Blend(a, b, int(Clamp01(t) * 255.0f + 0.5f));
|
||||
}
|
||||
|
||||
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
Put(xx, yy, color, rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
|
||||
}
|
||||
|
||||
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
{
|
||||
const float t = horizontal ? float(xx - x) / std::max(1, w - 1) : float(yy - y) / std::max(1, h - 1);
|
||||
Put(xx, yy, Mix(c0, c1, t), rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
{
|
||||
if (rr.Inside(xx, yy, thick + 1.0f))
|
||||
continue;
|
||||
const float d = rr.Distance(xx, yy);
|
||||
Put(xx, yy, color, Clamp01(0.5f - d) - Clamp01(0.5f - d - thick), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y - size); yy < std::min(H, y + h + size); yy++)
|
||||
for (int xx = std::max(0, x - size); xx < std::min(W, x + w + size); xx++)
|
||||
{
|
||||
if (rr.Inside(xx, yy, 0.0f))
|
||||
continue;
|
||||
const float d = rr.Distance(xx, yy);
|
||||
if (d <= 0.0f || d >= size)
|
||||
continue;
|
||||
const float f = 1.0f - d / size;
|
||||
Put(xx, yy, color, f * f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha)
|
||||
{
|
||||
for (int yy = std::max(0, int(cy - r - 1)); yy <= std::min(H - 1, int(cy + r + 1)); yy++)
|
||||
for (int xx = std::max(0, int(cx - r - 1)); xx <= std::min(W - 1, int(cx + r + 1)); xx++)
|
||||
{
|
||||
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
|
||||
Put(xx, yy, color, Clamp01(0.5f - (std::sqrt(dx * dx + dy * dy) - r)), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha)
|
||||
{
|
||||
StrokeArc(cx, cy, r - thick * 0.5f, thick, 0.0f, 360.0f, color, alpha);
|
||||
}
|
||||
|
||||
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha)
|
||||
{
|
||||
const float area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0);
|
||||
if (std::fabs(area) < 1e-3f)
|
||||
return;
|
||||
const float s = area > 0 ? 1.0f : -1.0f;
|
||||
auto edge = [s](float ax, float ay, float bx, float by, float px, float py) {
|
||||
return s * ((bx - ax) * (py - ay) - (by - ay) * (px - ax));
|
||||
};
|
||||
const int minx = std::max(0, int(std::floor(std::min({x0, x1, x2})))), maxx = std::min(W - 1, int(std::ceil(std::max({x0, x1, x2}))));
|
||||
const int miny = std::max(0, int(std::floor(std::min({y0, y1, y2})))), maxy = std::min(H - 1, int(std::ceil(std::max({y0, y1, y2}))));
|
||||
for (int yy = miny; yy <= maxy; yy++)
|
||||
for (int xx = minx; xx <= maxx; xx++)
|
||||
{
|
||||
int in = 0;
|
||||
for (int sy = 0; sy < 4; sy++)
|
||||
for (int sx = 0; sx < 4; sx++)
|
||||
{
|
||||
const float px = xx + (sx + 0.5f) / 4.0f, py = yy + (sy + 0.5f) / 4.0f;
|
||||
if (edge(x0, y0, x1, y1, px, py) >= 0 && edge(x1, y1, x2, y2, px, py) >= 0 && edge(x2, y2, x0, y0, px, py) >= 0)
|
||||
in++;
|
||||
}
|
||||
Put(xx, yy, color, in / 16.0f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha)
|
||||
{
|
||||
const float outer = r + thick * 0.5f + 1.0f;
|
||||
const bool full = a1 - a0 >= 360.0f;
|
||||
for (int yy = std::max(0, int(cy - outer)); yy <= std::min(H - 1, int(cy + outer)); yy++)
|
||||
for (int xx = std::max(0, int(cx - outer)); xx <= std::min(W - 1, int(cx + outer)); xx++)
|
||||
{
|
||||
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
|
||||
const float cov = Clamp01(thick * 0.5f + 0.5f - std::fabs(std::sqrt(dx * dx + dy * dy) - r));
|
||||
if (cov <= 0.0f)
|
||||
continue;
|
||||
if (!full)
|
||||
{
|
||||
float a = std::atan2(dy, dx) * 57.29578f;
|
||||
if (a < 0)
|
||||
a += 360.0f;
|
||||
float lo = std::fmod(a0, 360.0f), span = a1 - a0;
|
||||
if (lo < 0)
|
||||
lo += 360.0f;
|
||||
float rel = a - lo;
|
||||
if (rel < 0)
|
||||
rel += 360.0f;
|
||||
if (rel > span)
|
||||
continue;
|
||||
}
|
||||
Put(xx, yy, color, cov, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void HLine(int x, int y, int w, uint32_t color, int alpha)
|
||||
{
|
||||
for (int xx = x; xx < x + w; xx++)
|
||||
Put(xx, y, color, 1.0f, alpha);
|
||||
}
|
||||
|
||||
void VLine(int x, int y, int h, uint32_t color, int alpha)
|
||||
{
|
||||
for (int yy = y; yy < y + h; yy++)
|
||||
Put(x, yy, color, 1.0f, alpha);
|
||||
}
|
||||
|
||||
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color)
|
||||
{
|
||||
const float s = float(size);
|
||||
const float cx = x + s * 0.5f, cy = y + s * 0.5f;
|
||||
switch (icon)
|
||||
{
|
||||
case Icon::Pause:
|
||||
{
|
||||
const int bw = int(s * 0.2f), bh = int(s * 0.62f), gap = int(s * 0.16f);
|
||||
const int top = int(cy - bh * 0.5f), left = int(cx - gap * 0.5f) - bw;
|
||||
FillRoundRect(left, top, bw, bh, bw / 3, color);
|
||||
FillRoundRect(int(cx + gap * 0.5f), top, bw, bh, bw / 3, color);
|
||||
break;
|
||||
}
|
||||
case Icon::Play:
|
||||
FillTriangle(x + s * 0.22f, y + s * 0.14f, x + s * 0.22f, y + s * 0.86f, x + s * 0.86f, cy, color);
|
||||
break;
|
||||
case Icon::Restart:
|
||||
{
|
||||
// most of a circle, open at the top, with an arrowhead at its upper left end pointing on round
|
||||
const float r = s * 0.32f, t = s * 0.12f;
|
||||
StrokeArc(cx, cy, r, t, 300.0f, 600.0f, color);
|
||||
const float a = 240.0f / 57.29578f;
|
||||
const float px = cx + r * std::cos(a), py = cy + r * std::sin(a);
|
||||
const float tx = -std::sin(a), ty = std::cos(a); // the way round (clockwise)
|
||||
const float nx = std::cos(a), ny = std::sin(a);
|
||||
const float hl = s * 0.22f, hw = s * 0.17f;
|
||||
FillTriangle(px + tx * hl, py + ty * hl, px + nx * hw, py + ny * hw, px - nx * hw, py - ny * hw, color);
|
||||
break;
|
||||
}
|
||||
case Icon::FastForward:
|
||||
FillTriangle(x + s * 0.08f, y + s * 0.2f, x + s * 0.08f, y + s * 0.8f, x + s * 0.5f, cy, color);
|
||||
FillTriangle(x + s * 0.5f, y + s * 0.2f, x + s * 0.5f, y + s * 0.8f, x + s * 0.92f, cy, color);
|
||||
break;
|
||||
case Icon::Grid:
|
||||
for (int j = 0; j < 3; j++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
FillRoundRect(x + int(s * (0.1f + 0.29f * i)), y + int(s * (0.1f + 0.29f * j)), int(s * 0.22f),
|
||||
int(s * 0.22f), 2, color);
|
||||
break;
|
||||
case Icon::Tiles:
|
||||
for (int j = 0; j < 2; j++)
|
||||
for (int i = 0; i < 2; i++)
|
||||
FillRoundRect(x + int(s * (0.1f + 0.43f * i)), y + int(s * (0.1f + 0.43f * j)), int(s * 0.36f),
|
||||
int(s * 0.36f), int(s * 0.08f), color);
|
||||
break;
|
||||
case Icon::Frame:
|
||||
{
|
||||
// a dashed square: four dashes a side
|
||||
const int a = int(s * 0.12f), b = int(s * 0.88f), t = std::max(2, int(s * 0.09f)), d = (b - a) / 7;
|
||||
for (int k = 0; k < 7; k += 2)
|
||||
{
|
||||
FillRoundRect(x + a + k * d, y + a, d, t, 1, color);
|
||||
FillRoundRect(x + a + k * d, y + b - t, d, t, 1, color);
|
||||
FillRoundRect(x + a, y + a + k * d, t, d, 1, color);
|
||||
FillRoundRect(x + b - t, y + a + k * d, t, d, 1, color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Icon::Bars:
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
const float hgt = s * (0.3f + 0.25f * i);
|
||||
FillRoundRect(x + int(s * (0.12f + 0.28f * i)), y + int(s * 0.88f - hgt), int(s * 0.2f), int(hgt), 2, color);
|
||||
}
|
||||
break;
|
||||
case Icon::Screen:
|
||||
StrokeRoundRect(x + int(s * 0.06f), y + int(s * 0.2f), int(s * 0.88f), int(s * 0.6f), int(s * 0.08f),
|
||||
std::max(2.0f, s * 0.09f), color);
|
||||
break;
|
||||
case Icon::Gauge:
|
||||
{
|
||||
const float r = s * 0.38f;
|
||||
StrokeArc(cx, cy + s * 0.08f, r, s * 0.1f, 150.0f, 390.0f, color);
|
||||
const float a = 300.0f / 57.29578f;
|
||||
const float ex = cx + r * 0.8f * std::cos(a), ey = cy + s * 0.08f + r * 0.8f * std::sin(a);
|
||||
const float nx = -std::sin(a) * s * 0.05f, ny = std::cos(a) * s * 0.05f;
|
||||
FillTriangle(cx + nx, cy + s * 0.08f + ny, cx - nx, cy + s * 0.08f - ny, ex, ey, color);
|
||||
FillCircle(cx, cy + s * 0.08f, s * 0.08f, color);
|
||||
break;
|
||||
}
|
||||
case Icon::Download:
|
||||
FillRoundRect(int(cx - s * 0.07f), y + int(s * 0.1f), int(s * 0.14f), int(s * 0.4f), 1, color);
|
||||
FillTriangle(cx - s * 0.25f, y + s * 0.45f, cx + s * 0.25f, y + s * 0.45f, cx, y + s * 0.72f, color);
|
||||
FillRoundRect(x + int(s * 0.12f), y + int(s * 0.8f), int(s * 0.76f), std::max(2, int(s * 0.09f)), 1, color);
|
||||
break;
|
||||
case Icon::Search:
|
||||
{
|
||||
// a magnifying glass: a ring at the upper left, its handle down to the lower right
|
||||
const float r = s * 0.25f, rx = x + s * 0.42f, ry = y + s * 0.42f;
|
||||
StrokeArc(rx, ry, r, s * 0.1f, 0.0f, 360.0f, color);
|
||||
const float d = 0.70710678f, hw = s * 0.07f;
|
||||
const float ax = rx + (r + s * 0.03f) * d, ay = ry + (r + s * 0.03f) * d;
|
||||
const float bx = x + s * 0.9f, by = y + s * 0.9f;
|
||||
FillTriangle(ax - hw * d, ay + hw * d, ax + hw * d, ay - hw * d, bx + hw * d, by - hw * d, color);
|
||||
FillTriangle(ax - hw * d, ay + hw * d, bx + hw * d, by - hw * d, bx - hw * d, by + hw * d, color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace gfx
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,49 @@
|
||||
// Mupen64Plus PS5 frontend: anti-aliased shapes on the CPU-drawn screen (ps5video::Surface()), for the pause menu's
|
||||
// look: rounded panels and buttons, glows, gradients, switches, sliders and a few drawn icons.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace gfx
|
||||
{
|
||||
// color over the pixel at `alpha` (0..255)
|
||||
uint32_t Blend(uint32_t dst, uint32_t color, int alpha);
|
||||
uint32_t Mix(uint32_t a, uint32_t b, float t); // a -> b
|
||||
|
||||
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha = 255);
|
||||
// from c0 to c1, left to right (horizontal) or top to bottom
|
||||
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha = 255);
|
||||
// a border `thick` pixels wide, inside the rectangle
|
||||
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha = 255);
|
||||
// light around the rectangle, fading out over `size` pixels
|
||||
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha);
|
||||
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha = 255);
|
||||
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha = 255);
|
||||
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha = 255);
|
||||
// part of a ring, angles in degrees, clockwise from 3 o'clock (screen y grows downwards)
|
||||
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha = 255);
|
||||
void HLine(int x, int y, int w, uint32_t color, int alpha = 255);
|
||||
void VLine(int x, int y, int h, uint32_t color, int alpha = 255);
|
||||
|
||||
// small icons drawn with the shapes above, in a `size` x `size` box
|
||||
enum class Icon
|
||||
{
|
||||
Pause,
|
||||
Play,
|
||||
Restart, // a circular arrow
|
||||
FastForward,
|
||||
Grid, // 3 x 3 squares
|
||||
Tiles, // 2 x 2 squares
|
||||
Frame, // a dashed square
|
||||
Bars, // a bar chart
|
||||
Screen, // a rectangle
|
||||
Gauge,
|
||||
Download,
|
||||
Search, // a magnifying glass
|
||||
};
|
||||
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color);
|
||||
} // namespace gfx
|
||||
} // namespace fe
|
||||
+424
-21
@@ -17,6 +17,8 @@
|
||||
|
||||
#include "fe_emu.h"
|
||||
|
||||
#include "fe_cheats.h"
|
||||
|
||||
#include "fe_games.h"
|
||||
#include "fe_menu.h"
|
||||
#include "fe_settings.h"
|
||||
@@ -49,6 +51,7 @@
|
||||
#include <cstdarg>
|
||||
#include <dirent.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
@@ -73,7 +76,18 @@ FE_INCBIN(n64ps5_rom_catalog, M64P_INI_PATH)
|
||||
namespace
|
||||
{
|
||||
constexpr int kFrontendApiVersion = 0x020106;
|
||||
constexpr size_t kMaxRomSize = 64u << 20; // the biggest N64 cartridges are 64 MiB
|
||||
// the biggest N64 cartridges are 64 MiB, ROM hacks can fill the whole cartridge space (the core's CART_ROM_MAX_SIZE)
|
||||
constexpr size_t kMaxRomSize = 252u << 20;
|
||||
|
||||
std::string RomSizeError(long size)
|
||||
{
|
||||
if (size <= 0)
|
||||
return "Not an N64 ROM (the file is empty)";
|
||||
char text[96];
|
||||
snprintf(text, sizeof(text), "Too big for an N64 ROM (%.1f MB, the most is %zu MB)", double(size) / (1 << 20),
|
||||
kMaxRomSize >> 20);
|
||||
return text;
|
||||
}
|
||||
|
||||
double Now()
|
||||
{
|
||||
@@ -210,6 +224,22 @@ struct State
|
||||
uint32_t prev_buttons = 0;
|
||||
bool block_until_release = false;
|
||||
int pending_save_slot = -1;
|
||||
// the controller layout while a game is loaded (from RunGame on): its own or Settings'
|
||||
bool game_layout = false;
|
||||
bool own_layout = false;
|
||||
fe::Layout layout;
|
||||
// cheats: the player's own, then the database's for this ROM, with what the player chose
|
||||
std::vector<fe::cheats::Cheat> cheats;
|
||||
std::string cheats_game;
|
||||
int next_uid = 0;
|
||||
fe::search::Search search; // the cheat search, while the game runs
|
||||
// "continue where you left off": the state loaded at the first VI, and the one saved on the way out
|
||||
bool resume_load = false;
|
||||
bool resume_loading = false;
|
||||
bool leaving = false; // the pause menu chose to leave: the state is being saved, then the game stops
|
||||
bool leave_saved = false;
|
||||
uint32_t leave_vi = 0;
|
||||
emu::ExitReason leave_reason = emu::ExitReason::BackToList;
|
||||
// FPS counter
|
||||
double fps_t0 = 0;
|
||||
uint64_t fps_tsc0 = 0;
|
||||
@@ -272,7 +302,14 @@ void CoreState(void*, m64p_core_param param, int value)
|
||||
OrbisLog("[emu] state %s %s", save ? "save" : "load", value ? "done" : "failed");
|
||||
if (save)
|
||||
g.pending_save_slot = -1;
|
||||
if (!value)
|
||||
if (save && g.leaving)
|
||||
g.leave_saved = true;
|
||||
else if (!save && g.resume_loading)
|
||||
{
|
||||
g.resume_loading = false;
|
||||
emu::Osd(value ? "Continuing where you left off" : "Could not continue where you left off: the game starts over");
|
||||
}
|
||||
else if (!value)
|
||||
emu::Osd(save ? "Could not save the state" : "Could not load the state");
|
||||
}
|
||||
}
|
||||
@@ -334,10 +371,11 @@ bool ReadWholeFile(const std::string& path, std::vector<uint8_t>& out, std::stri
|
||||
fseek(f, 0, SEEK_END);
|
||||
const long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
OrbisLog("[emu] ROM file: %ld bytes", size);
|
||||
if (size <= 0 || size_t(size) > kMaxRomSize)
|
||||
{
|
||||
fclose(f);
|
||||
*error = "Not an N64 ROM (wrong size)";
|
||||
*error = RomSizeError(size);
|
||||
return false;
|
||||
}
|
||||
out.resize(size_t(size));
|
||||
@@ -371,8 +409,11 @@ bool ReadZippedRom(const std::string& path, std::vector<uint8_t>& out, std::stri
|
||||
}
|
||||
}
|
||||
bool ok = false;
|
||||
if (best == 0 || best > kMaxRomSize)
|
||||
OrbisLog("[emu] ROM in the zip: %s, %lu bytes", best_name, best);
|
||||
if (best == 0)
|
||||
*error = "No N64 ROM in the zip file";
|
||||
else if (best > kMaxRomSize)
|
||||
*error = RomSizeError(long(best));
|
||||
else if (unzLocateFile(z, best_name, 0) != UNZ_OK || unzOpenCurrentFile(z) != UNZ_OK)
|
||||
*error = "Can't read the zip file";
|
||||
else
|
||||
@@ -444,6 +485,128 @@ std::string StatePath(int slot)
|
||||
return OrbisDir("states") + "/" + g.file_base + ".st" + std::to_string(slot);
|
||||
}
|
||||
|
||||
// The ROM's file name without folder and extension: its states are named after it.
|
||||
std::string FileBase(const std::string& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of('/');
|
||||
std::string base = path.substr(slash == std::string::npos ? 0 : slash + 1);
|
||||
if (const size_t d = base.find_last_of('.'); d != std::string::npos && d > 0)
|
||||
base.resize(d);
|
||||
return base;
|
||||
}
|
||||
|
||||
// The cheats the player left on, by name: "<state>\t<name>" lines (fe_cheats.h's state).
|
||||
std::string CheatsPath()
|
||||
{
|
||||
return OrbisDir("cheats") + "/" + g.file_base + ".txt";
|
||||
}
|
||||
|
||||
void LoadCheatStates()
|
||||
{
|
||||
FILE* f = fopen(CheatsPath().c_str(), "r");
|
||||
if (!f)
|
||||
return;
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
char* tab = strchr(line, '\t');
|
||||
if (!tab)
|
||||
continue;
|
||||
*tab = 0;
|
||||
const int state = atoi(line);
|
||||
for (fe::cheats::Cheat& c : g.cheats)
|
||||
if (c.name == tab + 1 && state > 0 && state < c.States())
|
||||
c.state = state;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void SaveCheatStates()
|
||||
{
|
||||
std::string text;
|
||||
for (const fe::cheats::Cheat& c : g.cheats)
|
||||
if (c.state)
|
||||
text += std::to_string(c.state) + "\t" + c.name + "\n";
|
||||
if (text.empty())
|
||||
{
|
||||
unlink(CheatsPath().c_str());
|
||||
return;
|
||||
}
|
||||
OrbisMkdirs(OrbisDir("cheats"));
|
||||
if (FILE* f = fopen(CheatsPath().c_str(), "w"))
|
||||
{
|
||||
fputs(text.c_str(), f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
// The player's own cheats: cheats/<ROM file>.custom.txt, in the database's format (fe::cheats::Parse).
|
||||
std::string CustomCheatsPath()
|
||||
{
|
||||
return OrbisDir("cheats") + "/" + g.file_base + ".custom.txt";
|
||||
}
|
||||
|
||||
std::vector<fe::cheats::Cheat> LoadCustomCheats()
|
||||
{
|
||||
std::string text;
|
||||
if (FILE* f = fopen(CustomCheatsPath().c_str(), "r"))
|
||||
{
|
||||
char buf[4096];
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
|
||||
text.append(buf, n);
|
||||
fclose(f);
|
||||
}
|
||||
return fe::cheats::Parse(text);
|
||||
}
|
||||
|
||||
void SaveCustomCheats()
|
||||
{
|
||||
std::vector<fe::cheats::Cheat> mine;
|
||||
for (const fe::cheats::Cheat& c : g.cheats)
|
||||
if (c.mine)
|
||||
mine.push_back(c);
|
||||
if (mine.empty())
|
||||
{
|
||||
unlink(CustomCheatsPath().c_str());
|
||||
return;
|
||||
}
|
||||
OrbisMkdirs(OrbisDir("cheats"));
|
||||
if (FILE* f = fopen(CustomCheatsPath().c_str(), "w"))
|
||||
{
|
||||
fputs(fe::cheats::Format(mine).c_str(), f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Hands cheat `index` to the core as it is now: on (with its codes, the chosen option's value in place of
|
||||
// "????") or off (the core puts the old values back). Its name in the core is "#<uid>", which stays the same
|
||||
// while the game runs whatever the list or the cheat's name does.
|
||||
void ApplyCheat(int index)
|
||||
{
|
||||
const fe::cheats::Cheat& c = g.cheats[size_t(index)];
|
||||
const std::string id = "#" + std::to_string(c.uid);
|
||||
if (c.state == 0)
|
||||
{
|
||||
CoreCheatEnabled(id.c_str(), 0);
|
||||
return;
|
||||
}
|
||||
const int choice = c.options.empty() ? 0 : c.options[size_t(c.state - 1)].second;
|
||||
std::vector<m64p_cheat_code> codes;
|
||||
for (size_t i = 0; i < c.addresses.size(); i++)
|
||||
codes.push_back({c.addresses[i], c.values[i] < 0 ? choice : c.values[i]});
|
||||
const m64p_error r = CoreAddCheat(id.c_str(), codes.data(), int(codes.size()));
|
||||
if (r != M64ERR_SUCCESS)
|
||||
OrbisLog("[cheats] the core refused \"%s\" (%d)", c.name.c_str(), int(r));
|
||||
}
|
||||
|
||||
// "Continue where you left off": saved when the player leaves the game from the pause menu.
|
||||
std::string ResumePath(const std::string& file_base)
|
||||
{
|
||||
return OrbisDir("states") + "/" + file_base + ".resume";
|
||||
}
|
||||
|
||||
void SetLimiter(bool on)
|
||||
{
|
||||
if (g.limiter_set && g.limiter_on == on)
|
||||
@@ -463,6 +626,31 @@ void StopGame(emu::ExitReason why)
|
||||
CoreDoCommand(M64CMD_STOP, 0, nullptr);
|
||||
}
|
||||
|
||||
// Leaving the game from the pause menu: with "Continue where you left off" on, its state is saved first (at
|
||||
// the next VI; n64ps5_on_vi stops the game once the core says it is written), else it stops now.
|
||||
void LeaveGame(emu::ExitReason why)
|
||||
{
|
||||
if (g.leaving || g.stopping)
|
||||
return;
|
||||
if (fe::Config().resume)
|
||||
{
|
||||
const std::string path = ResumePath(g.file_base);
|
||||
if (CoreDoCommand(M64CMD_STATE_SAVE, 1, (void*)path.c_str()) == M64ERR_SUCCESS)
|
||||
{
|
||||
OrbisLog("[emu] leaving: saving where the player is -> %s", path.c_str());
|
||||
g.leaving = true;
|
||||
g.leave_saved = false;
|
||||
g.leave_vi = g.vi;
|
||||
g.leave_reason = why;
|
||||
n64ps5_input_block(true);
|
||||
n64ps5_audio_set_mute(true);
|
||||
return;
|
||||
}
|
||||
OrbisLog("[emu] leaving: the core refused to save the state");
|
||||
}
|
||||
StopGame(why);
|
||||
}
|
||||
|
||||
void OpenPauseMenu()
|
||||
{
|
||||
N64_STAGE(Emu, "pause menu");
|
||||
@@ -480,8 +668,8 @@ void OpenPauseMenu()
|
||||
switch (action)
|
||||
{
|
||||
case fe::PauseAction::Resume: break;
|
||||
case fe::PauseAction::BackToList: StopGame(emu::ExitReason::BackToList); break;
|
||||
case fe::PauseAction::Quit: StopGame(emu::ExitReason::Quit); break;
|
||||
case fe::PauseAction::BackToList: LeaveGame(emu::ExitReason::BackToList); break;
|
||||
case fe::PauseAction::Quit: LeaveGame(emu::ExitReason::Quit); break;
|
||||
}
|
||||
N64_STAGE(Emu, "running");
|
||||
}
|
||||
@@ -529,6 +717,20 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
g.vi++;
|
||||
if (g.vi == 1)
|
||||
N64_STAGE(Emu, "running");
|
||||
if (g.vi == 1 && g.resume_load)
|
||||
{
|
||||
g.resume_load = false;
|
||||
const std::string path = ResumePath(g.file_base);
|
||||
g.resume_loading = CoreDoCommand(M64CMD_STATE_LOAD, 0, (void*)path.c_str()) == M64ERR_SUCCESS;
|
||||
OrbisLog("[emu] continuing where the player left off <- %s: %s", path.c_str(), g.resume_loading ? "queued" : "refused");
|
||||
}
|
||||
// leaving: the state is written (or the core never said so in 5 s): now the game stops
|
||||
if (g.leaving && !g.stopping && (g.leave_saved || g.vi - g.leave_vi > 300))
|
||||
{
|
||||
if (!g.leave_saved)
|
||||
OrbisLog("[emu] leaving: no word from the core about the state, stopping anyway");
|
||||
StopGame(g.leave_reason);
|
||||
}
|
||||
const bool ff = g.fast_forward.load();
|
||||
SetLimiter(g.pal && !ff);
|
||||
|
||||
@@ -539,15 +741,23 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
if (g.block_until_release && raw == 0)
|
||||
{
|
||||
g.block_until_release = false;
|
||||
n64ps5_input_block(false);
|
||||
if (!g.leaving)
|
||||
n64ps5_input_block(false);
|
||||
}
|
||||
|
||||
const uint32_t combo = SCE_PAD_BUTTON_L3 | SCE_PAD_BUTTON_R3;
|
||||
const bool menu = (pressed & SCE_PAD_BUTTON_TOUCH_PAD) || ((raw & combo) == combo && (pressed & combo));
|
||||
if (menu && !g.stopping)
|
||||
// the touchpad opens the pause menu; R3 (pressed) saves to the state slot, L3 loads it
|
||||
if (!g.stopping && !g.leaving)
|
||||
{
|
||||
OpenPauseMenu();
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
const int slot = fe::Config().state_slot;
|
||||
if (pressed & SCE_PAD_BUTTON_TOUCH_PAD)
|
||||
{
|
||||
OpenPauseMenu();
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
}
|
||||
else if (pressed & SCE_PAD_BUTTON_R3)
|
||||
emu::Osd(emu::SaveState(slot) ? "Saved in slot %d" : "Could not save in slot %d", slot);
|
||||
else if (pressed & SCE_PAD_BUTTON_L3)
|
||||
emu::Osd(emu::StateExists(slot) && emu::LoadState(slot) ? "Loaded slot %d" : "Slot %d is empty", slot);
|
||||
}
|
||||
|
||||
UpdateFps();
|
||||
@@ -556,7 +766,7 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
ps5audio::Queued(), n64ps5_audio_target(),
|
||||
(unsigned long long)ps5audio::Underruns());
|
||||
|
||||
if (frame && !g.stopping && (!ff || (g.vi % 4) == 0))
|
||||
if (frame && !g.stopping && !g.leaving && (!ff || (g.vi % 4) == 0))
|
||||
g_presenter.Submit(frame->pixels, frame->width, frame->height, frame->pitch, !ff && !g.pal);
|
||||
|
||||
// No picture: the game blanked the screen (VI off, e.g. Perfect Dark while it loads). A real N64 shows
|
||||
@@ -570,7 +780,7 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
g.blank_vis / (g.pal ? 50.0 : 60.0));
|
||||
g.blank_vis = 0;
|
||||
}
|
||||
else if (++g.blank_vis == kBlankVis && !g.stopping)
|
||||
else if (++g.blank_vis == kBlankVis && !g.stopping && !g.leaving)
|
||||
{
|
||||
static const uint32_t kBlack[320 * 240] = {};
|
||||
g_presenter.Submit(kBlack, 320, 240, 320, !ff && !g.pal);
|
||||
@@ -650,8 +860,9 @@ void DeinitCore()
|
||||
g.core_ok = false;
|
||||
}
|
||||
|
||||
ExitReason RunGame(const std::string& path, std::string* error)
|
||||
ExitReason RunGame(const std::string& path, std::string* error, bool resume)
|
||||
{
|
||||
g.game_layout = false; // Settings' until this game's is known
|
||||
std::string dummy;
|
||||
if (!error)
|
||||
error = &dummy;
|
||||
@@ -687,10 +898,11 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
CoreDoCommand(M64CMD_ROM_GET_SETTINGS, sizeof(settings), &settings);
|
||||
|
||||
g.rom_path = path;
|
||||
const size_t slash = path.find_last_of('/');
|
||||
g.file_base = path.substr(slash == std::string::npos ? 0 : slash + 1);
|
||||
if (const size_t d = g.file_base.find_last_of('.'); d != std::string::npos && d > 0)
|
||||
g.file_base.resize(d);
|
||||
g.file_base = FileBase(path);
|
||||
g.own_layout = fe::LoadGameLayout(g.file_base, g.layout);
|
||||
if (!g.own_layout)
|
||||
g.layout = fe::Config().layout;
|
||||
g.game_layout = true;
|
||||
g.pal = IsPalCountry(header.Country_code);
|
||||
// the title: as on the shelf (No-Intro name), else the catalog's GoodName, else the file name
|
||||
uint32_t crc = 0;
|
||||
@@ -708,12 +920,45 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
// the CPU core: the dynarec when the setting asks for it and the console allows executable memory
|
||||
const bool use_dynarec = cfg.dynarec && n64ps5_jit_probe() != 0;
|
||||
m64p_handle core_cfg = nullptr;
|
||||
// overclock: the core counts 2^n times fewer cycles per instruction, so the game gets more of them per frame
|
||||
const int overclock = std::clamp(cfg.overclock, 0, 2);
|
||||
if (ConfigOpenSection("Core", &core_cfg) == M64ERR_SUCCESS && core_cfg)
|
||||
{
|
||||
SetCoreInt(core_cfg, "R4300Emulator", use_dynarec ? 2 : 1);
|
||||
OrbisLog("[emu] CPU: %s", use_dynarec ? "dynamic recompiler" : "cached interpreter");
|
||||
SetCoreInt(core_cfg, "CountPerOpDenomPot", overclock);
|
||||
}
|
||||
OrbisLog("[emu] CPU: %s, overclock %s", use_dynarec ? "dynamic recompiler" : "cached interpreter",
|
||||
overclock == 0 ? "off" : overclock == 1 ? "2x" : "4x");
|
||||
n64ps5_rsp_set_hle(cfg.hle_audio);
|
||||
static const char* const kPaks[] = {"none", "Controller Pak", "Rumble Pak"};
|
||||
OrbisLog("[emu] controller pak: %s", kPaks[cfg.pak < 0 || cfg.pak > 2 ? 1 : cfg.pak]);
|
||||
// cheats: the player's own, then the database's for this ROM (by the header's CRCs and country); the ones
|
||||
// left on are on again
|
||||
g.cheats_game.clear();
|
||||
g.cheats = LoadCustomCheats();
|
||||
const size_t mine = g.cheats.size();
|
||||
for (fe::cheats::Cheat& c : fe::cheats::ForGame(__builtin_bswap32(header.CRC1), __builtin_bswap32(header.CRC2),
|
||||
header.Country_code, &g.cheats_game))
|
||||
g.cheats.push_back(std::move(c));
|
||||
g.next_uid = 0;
|
||||
for (fe::cheats::Cheat& c : g.cheats)
|
||||
c.uid = ++g.next_uid;
|
||||
if (mine)
|
||||
OrbisLog("[cheats] %zu of your own", mine);
|
||||
g.search.Clear();
|
||||
LoadCheatStates();
|
||||
std::string cheats_on;
|
||||
for (size_t i = 0; i < g.cheats.size(); i++)
|
||||
if (g.cheats[i].state)
|
||||
{
|
||||
ApplyCheat(int(i));
|
||||
cheats_on += (cheats_on.empty() ? ": " : ", ") + fe::cheats::DisplayName(g.cheats[i].name);
|
||||
}
|
||||
if (!g.cheats.empty())
|
||||
OrbisLog("[cheats] %zu for \"%s\"%s", g.cheats.size(), g.cheats_game.c_str(),
|
||||
cheats_on.empty() ? ", none on" : cheats_on.c_str());
|
||||
OrbisLog("[emu] controller layout: %s%s", g.own_layout ? "this game's own " : "",
|
||||
g.layout == fe::Layout() ? "(the N64 pad's)" : fe::LayoutToText(g.layout).c_str());
|
||||
LogSaves("at start", settings.goodname, settings.MD5);
|
||||
const n64ps5_gfx_options gfx = {cfg.vi_filter ? 0 : 1, cfg.hide_overscan, cfg.render_threads, cfg.dp_compat,
|
||||
cfg.upscale, cfg.gpu_sync};
|
||||
@@ -762,6 +1007,10 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
g.fast_forward.store(false);
|
||||
g.fps_t0 = 0;
|
||||
g.pending_save_slot = -1;
|
||||
g.resume_load = resume && OrbisIsFile(ResumePath(g.file_base));
|
||||
g.resume_loading = false;
|
||||
g.leaving = false;
|
||||
g.leave_saved = false;
|
||||
g.fps_text[0] = 0;
|
||||
ps5video::FillRect(0, 0, ps5video::kWidth, ps5video::kHeight, ps5video::Rgb(0, 0, 0));
|
||||
ps5video::InvalidateN64();
|
||||
@@ -782,7 +1031,12 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
for (int i = 0; i < ps5input::kMaxPads; i++)
|
||||
ps5input::SetRumble(i, 0, 0);
|
||||
n64ps5_input_block(false);
|
||||
if (g.leaving)
|
||||
n64ps5_audio_set_mute(!fe::Config().audio);
|
||||
g.leaving = false;
|
||||
g.running = false;
|
||||
g.game_layout = false;
|
||||
g.search.Clear(); // its 10-20 MB
|
||||
N64_STAGE(Emu, "idle");
|
||||
if (r != M64ERR_SUCCESS && g.vi == 0)
|
||||
{
|
||||
@@ -792,11 +1046,154 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
return g.exit;
|
||||
}
|
||||
|
||||
bool ResumeStateExists(const std::string& rom, std::string* saved_at)
|
||||
{
|
||||
struct stat st = {};
|
||||
if (stat(ResumePath(FileBase(rom)).c_str(), &st) != 0 || !S_ISREG(st.st_mode))
|
||||
return false;
|
||||
if (saved_at)
|
||||
{
|
||||
char t[48];
|
||||
const time_t mt = st.st_mtime;
|
||||
strftime(t, sizeof(t), "%b %d, %H:%M", localtime(&mt));
|
||||
*saved_at = t;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GameLoaded()
|
||||
{
|
||||
return g.running;
|
||||
}
|
||||
|
||||
std::vector<fe::cheats::Cheat>& Cheats()
|
||||
{
|
||||
return g.cheats;
|
||||
}
|
||||
|
||||
std::string CheatsGameName()
|
||||
{
|
||||
return g.cheats_game;
|
||||
}
|
||||
|
||||
void SetCheat(int index, int state)
|
||||
{
|
||||
if (!g.running || index < 0 || index >= int(g.cheats.size()))
|
||||
return;
|
||||
fe::cheats::Cheat& c = g.cheats[size_t(index)];
|
||||
c.state = state < 0 || state >= c.States() ? 0 : state;
|
||||
ApplyCheat(index);
|
||||
SaveCheatStates();
|
||||
OrbisLog("[cheats] %s: %s", fe::cheats::DisplayName(c.name).c_str(),
|
||||
c.state == 0 ? "off" : c.options.empty() ? "on" : c.options[size_t(c.state - 1)].first.c_str());
|
||||
}
|
||||
|
||||
std::string UniqueCheatName(const std::string& name, int except)
|
||||
{
|
||||
std::string base = name.empty() ? "My cheat" : name, out = base;
|
||||
for (int n = 2;; n++)
|
||||
{
|
||||
bool taken = false;
|
||||
for (size_t i = 0; i < g.cheats.size(); i++)
|
||||
taken |= int(i) != except && g.cheats[i].name == out;
|
||||
if (!taken)
|
||||
return out;
|
||||
out = base + " (" + std::to_string(n) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
int AddMyCheat(const fe::cheats::Cheat& cheat)
|
||||
{
|
||||
if (!g.running)
|
||||
return -1;
|
||||
fe::cheats::Cheat c = cheat;
|
||||
c.mine = true;
|
||||
c.uid = ++g.next_uid;
|
||||
c.name = UniqueCheatName(c.name, -1);
|
||||
c.state = 1;
|
||||
// after the player's other own cheats, before the database's
|
||||
size_t at = 0;
|
||||
while (at < g.cheats.size() && g.cheats[at].mine)
|
||||
at++;
|
||||
g.cheats.insert(g.cheats.begin() + long(at), c);
|
||||
ApplyCheat(int(at));
|
||||
SaveCustomCheats();
|
||||
SaveCheatStates();
|
||||
OrbisLog("[cheats] yours: \"%s\" (%s), on", c.name.c_str(), fe::cheats::CodesText(c).c_str());
|
||||
return int(at);
|
||||
}
|
||||
|
||||
void UpdateMyCheat(int index, const fe::cheats::Cheat& cheat)
|
||||
{
|
||||
if (!g.running || index < 0 || index >= int(g.cheats.size()) || !g.cheats[size_t(index)].mine)
|
||||
return;
|
||||
fe::cheats::Cheat& c = g.cheats[size_t(index)];
|
||||
c.name = UniqueCheatName(cheat.name, index);
|
||||
c.note = cheat.note;
|
||||
c.addresses = cheat.addresses;
|
||||
c.values = cheat.values;
|
||||
c.options = cheat.options;
|
||||
if (c.state >= c.States())
|
||||
c.state = 1;
|
||||
if (c.state)
|
||||
ApplyCheat(index); // the new codes
|
||||
SaveCustomCheats();
|
||||
SaveCheatStates();
|
||||
OrbisLog("[cheats] yours: \"%s\" changed (%s)", c.name.c_str(), fe::cheats::CodesText(c).c_str());
|
||||
}
|
||||
|
||||
void DeleteMyCheat(int index)
|
||||
{
|
||||
if (!g.running || index < 0 || index >= int(g.cheats.size()) || !g.cheats[size_t(index)].mine)
|
||||
return;
|
||||
g.cheats[size_t(index)].state = 0;
|
||||
ApplyCheat(index); // off: the core puts the old values back
|
||||
OrbisLog("[cheats] yours: \"%s\" deleted", g.cheats[size_t(index)].name.c_str());
|
||||
g.cheats.erase(g.cheats.begin() + index);
|
||||
SaveCustomCheats();
|
||||
SaveCheatStates();
|
||||
}
|
||||
|
||||
fe::search::Search& CheatSearch()
|
||||
{
|
||||
return g.search;
|
||||
}
|
||||
|
||||
const uint32_t* Ram(uint32_t* size)
|
||||
{
|
||||
*size = 0;
|
||||
return g.running ? n64ps5_ram(size) : nullptr;
|
||||
}
|
||||
|
||||
void PokeRam(uint32_t address, uint32_t value, int bytes)
|
||||
{
|
||||
if (g.running)
|
||||
{
|
||||
n64ps5_ram_write(address, value, bytes);
|
||||
OrbisLog("[cheats] RAM %08X = %u (%d-bit)", unsigned(0x80000000u | address), unsigned(value), bytes * 8);
|
||||
}
|
||||
}
|
||||
|
||||
fe::Layout GameLayout()
|
||||
{
|
||||
return g.game_layout ? g.layout : fe::Config().layout;
|
||||
}
|
||||
|
||||
bool GameHasOwnLayout()
|
||||
{
|
||||
return g.game_layout && g.own_layout;
|
||||
}
|
||||
|
||||
void SetGameLayout(const fe::Layout& layout)
|
||||
{
|
||||
if (!g.game_layout)
|
||||
return;
|
||||
g.layout = layout;
|
||||
g.own_layout = layout != fe::Config().layout;
|
||||
fe::SaveGameLayout(g.file_base, g.own_layout ? &layout : nullptr);
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
std::string GameName()
|
||||
{
|
||||
return g.title;
|
||||
@@ -806,8 +1203,14 @@ void ApplySettings()
|
||||
{
|
||||
const fe::Settings& cfg = fe::Config();
|
||||
n64ps5_audio_set_mute(!cfg.audio);
|
||||
n64ps5_audio_set_volume(cfg.volume);
|
||||
n64ps5_audio_set_fast_forward(g.fast_forward.load());
|
||||
const n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak};
|
||||
n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick, {}};
|
||||
const fe::Layout& layout = g.game_layout ? g.layout : cfg.layout;
|
||||
memcpy(in.layout, layout.to, sizeof(in.layout));
|
||||
// L3 and R3 load and save states (n64ps5_on_vi): the game doesn't get them
|
||||
in.layout[N64PS5_PAD_L3] = N64PS5_N64_NONE;
|
||||
in.layout[N64PS5_PAD_R3] = N64PS5_N64_NONE;
|
||||
n64ps5_input_set_options(&in);
|
||||
ps5video::InvalidateN64();
|
||||
}
|
||||
|
||||
+32
-2
@@ -2,6 +2,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "fe_cheats.h"
|
||||
#include "fe_search.h"
|
||||
#include "fe_settings.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace emu
|
||||
@@ -16,10 +20,36 @@ enum class ExitReason
|
||||
Quit, // "quit Mupen64Plus"
|
||||
Error, // the game could not be started (see `error`)
|
||||
};
|
||||
// Opens the ROM and runs it until the player leaves it (blocks; the pause menu runs inside).
|
||||
ExitReason RunGame(const std::string& path, std::string* error);
|
||||
// Opens the ROM and runs it until the player leaves it (blocks; the pause menu runs inside). With `resume`, the
|
||||
// game continues where the player left it (see ResumeStateExists).
|
||||
ExitReason RunGame(const std::string& path, std::string* error, bool resume = false);
|
||||
// "Continue where you left off" (Settings): leaving a game from the pause menu saves its state in
|
||||
// states/<rom file>.resume. True if `rom` has one; `saved_at` gets when it was written.
|
||||
bool ResumeStateExists(const std::string& rom, std::string* saved_at = nullptr);
|
||||
|
||||
bool GameLoaded();
|
||||
// The running game's cheats (fe_cheats.h): the player's own first, then the database's (none when it has none
|
||||
// for the game). Switching one (pause menu) applies it at once; what is on is remembered for the game in
|
||||
// cheats/<ROM file>.txt.
|
||||
std::vector<fe::cheats::Cheat>& Cheats();
|
||||
std::string CheatsGameName();
|
||||
void SetCheat(int index, int state);
|
||||
// The player's own cheats, kept in cheats/<ROM file>.custom.txt: a new one (turned on; its index is returned),
|
||||
// changed (its name, note and codes) or deleted. Names are made unique: "Lives" -> "Lives (2)".
|
||||
int AddMyCheat(const fe::cheats::Cheat& cheat);
|
||||
void UpdateMyCheat(int index, const fe::cheats::Cheat& cheat);
|
||||
void DeleteMyCheat(int index);
|
||||
std::string UniqueCheatName(const std::string& name, int except = -1);
|
||||
// The cheat search (fe_search.h) of the running game, over its RAM (nullptr when no game runs). PokeRam writes a
|
||||
// value once (the cheat search's "Set it now").
|
||||
fe::search::Search& CheatSearch();
|
||||
const uint32_t* Ram(uint32_t* size);
|
||||
void PokeRam(uint32_t address, uint32_t value, int bytes);
|
||||
// The running game's controller layout: its own (fe::LoadGameLayout) or else the one of Settings. Setting it
|
||||
// (pause menu) gives the game its own layout file, unless it is the same as Settings' again.
|
||||
fe::Layout GameLayout();
|
||||
bool GameHasOwnLayout();
|
||||
void SetGameLayout(const fe::Layout& layout);
|
||||
std::string GameName(); // the title shown on the shelf (No-Intro name, ROM header name or file name)
|
||||
|
||||
// Settings from fe::Config() that apply while a game runs (aspect, smoothing, FPS, sound, dead zone).
|
||||
|
||||
+1564
-140
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,7 @@
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoSce.h"
|
||||
#include "ProsperoUpdateJob.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
@@ -52,6 +53,7 @@ bool WriteAtomic(const std::string& path, const std::vector<uint8_t>& data)
|
||||
}
|
||||
PrefetchResult g_last;
|
||||
bool g_have_last = false;
|
||||
int g_next_start = 0; // new games' covers this start couldn't fetch (CoversForNextStart)
|
||||
} // namespace
|
||||
|
||||
void RememberPrefetch(const PrefetchResult& result)
|
||||
@@ -73,16 +75,19 @@ void CoversRestartIfNeeded(const std::vector<GameInfo>& games, bool downloads_on
|
||||
int fresh = 0;
|
||||
for (const WantedCover& w : wanted)
|
||||
fresh += g_last.attempted.count(w.file) ? 0 : 1;
|
||||
g_next_start = 0;
|
||||
if (wanted.empty() || (!force && fresh == 0))
|
||||
return;
|
||||
if (!g_have_last || !g_last.helper_ok)
|
||||
{
|
||||
g_next_start = fresh;
|
||||
OrbisLog("[covers] %zu cover(s) wanted, but no helper answered at start: not restarting", wanted.size());
|
||||
return;
|
||||
}
|
||||
if (!NetConnected())
|
||||
{
|
||||
OrbisLog("[covers] %zu cover(s) wanted; the console is offline: next start", wanted.size());
|
||||
g_next_start = fresh;
|
||||
return;
|
||||
}
|
||||
// A guard against a loop: no second restart within two minutes (one for Square: ten seconds).
|
||||
@@ -96,6 +101,7 @@ void CoversRestartIfNeeded(const std::vector<GameInfo>& games, bool downloads_on
|
||||
if (n == 1 && now - time_t(t) >= 0 && now - time_t(t) < (force ? 10 : 120))
|
||||
{
|
||||
OrbisLog("[covers] restarted %lld s ago: not again", (long long)(now - time_t(t)));
|
||||
g_next_start = fresh;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -104,21 +110,32 @@ void CoversRestartIfNeeded(const std::vector<GameInfo>& games, bool downloads_on
|
||||
fprintf(f, "%lld\n", (long long)now);
|
||||
fclose(f);
|
||||
}
|
||||
const char* path = "/data/homebrew/" N64PS5_TITLE_ID "/eboot.bin";
|
||||
if (access(path, F_OK) != 0)
|
||||
path = "/app0/eboot.bin";
|
||||
OrbisLog("[covers] %d new cover(s) to fetch (%zu wanted): restarting %s so the prefetch gets them", fresh,
|
||||
wanted.size(), path);
|
||||
// Closing and starting again: sceSystemServiceLoadExec(own eboot.bin) only closed the app on the console,
|
||||
// so the helper starts it again once it has exited (ProsperoUpdateJob.h, RelaunchIfPending).
|
||||
std::string why;
|
||||
if (!updatejob::WriteRelaunch(int(getpid()), why) || !jailbreak::StartHelper())
|
||||
{
|
||||
unlink(updatejob::RelaunchPath().c_str());
|
||||
OrbisLog("[covers] %d new cover(s) wanted, but the helper can't start the app again (%s): next start", fresh,
|
||||
why.empty() ? "the ELF loader didn't answer" : why.c_str());
|
||||
g_next_start = fresh;
|
||||
return;
|
||||
}
|
||||
OrbisLog("[covers] %d new cover(s) to fetch (%zu wanted): closing, the helper starts the app again", fresh,
|
||||
wanted.size());
|
||||
if (show)
|
||||
show("Downloading covers...");
|
||||
show("Getting covers for your new games: the app restarts by itself...");
|
||||
ProsperoNotifyFlush();
|
||||
OrbisLogClose();
|
||||
const int rc = sceSystemServiceLoadExec(path, nullptr);
|
||||
if (rc == 0)
|
||||
if (sceSystemServiceLoadExec("exit", nullptr) == 0)
|
||||
for (int i = 0; i < 100; i++)
|
||||
usleep(100 * 1000);
|
||||
OrbisLogOpen("boot-after-restart");
|
||||
OrbisLog("[covers] sceSystemServiceLoadExec(%s) -> %x: carrying on without the new covers", path, unsigned(rc));
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int CoversForNextStart()
|
||||
{
|
||||
return g_next_start;
|
||||
}
|
||||
|
||||
bool NetConnected()
|
||||
|
||||
@@ -53,8 +53,12 @@ void RememberPrefetch(const PrefetchResult& result);
|
||||
|
||||
// The shelf, after a scan (console only): writes covers/wanted.txt for `games`. When it holds covers this
|
||||
// start didn't try (or `force`: Square on a game), the network is up and a helper answered, shows `text` with
|
||||
// `show` and starts the app again (sceSystemServiceLoadExec, as PS5SX2 re-executes its eboot), so the
|
||||
// prefetch fetches them. Returns only when it didn't restart.
|
||||
// `show` and closes; the helper starts the app again once it has exited (ProsperoUpdateJob.h,
|
||||
// RelaunchIfPending), so the prefetch fetches them. Returns only when it didn't close: then the covers wait
|
||||
// for the next start (CoversForNextStart), and the app never closes without coming back.
|
||||
void CoversRestartIfNeeded(const std::vector<GameInfo>& games, bool downloads_on, bool force,
|
||||
void (*show)(const char* text));
|
||||
|
||||
// New games whose covers come at the next start (the last CoversRestartIfNeeded didn't close), for the shelf.
|
||||
int CoversForNextStart();
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,153 @@
|
||||
// Mupen64Plus PS5 frontend: the cheat search (fe_search.h).
|
||||
//
|
||||
// 8 MiB of RAM is 8,388,608 8-bit values: a step over all of them takes a few milliseconds. The search keeps one
|
||||
// bit per value still in it (1 MiB at most) and a copy of the RAM from the last step, and the same once more for
|
||||
// Undo.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fe_search.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace search
|
||||
{
|
||||
uint32_t Read(const uint32_t* ram, uint32_t address, int bytes)
|
||||
{
|
||||
// the core keeps the RAM as host-order 32-bit words: byte n of the N64's memory is byte n ^ 3 of the array
|
||||
const uint8_t* b = reinterpret_cast<const uint8_t*>(ram);
|
||||
switch (bytes)
|
||||
{
|
||||
case 1: return b[address ^ 3];
|
||||
case 2:
|
||||
{
|
||||
uint16_t v;
|
||||
memcpy(&v, b + (address ^ 2), 2);
|
||||
return v;
|
||||
}
|
||||
default: return ram[address >> 2];
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t MaxValue(int bytes)
|
||||
{
|
||||
return bytes == 1 ? 0xFFu : bytes == 2 ? 0xFFFFu : 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
std::string Thousands(uint64_t n)
|
||||
{
|
||||
std::string digits = std::to_string(n), out;
|
||||
for (size_t i = 0; i < digits.size(); i++)
|
||||
{
|
||||
if (i && (digits.size() - i) % 3 == 0)
|
||||
out += ',';
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void Search::Start(const uint32_t* ram, uint32_t ram_size, int bytes)
|
||||
{
|
||||
Clear();
|
||||
m_bytes = bytes == 2 || bytes == 4 ? bytes : 1;
|
||||
m_size = ram_size & ~3u;
|
||||
const uint64_t n = m_size / uint32_t(m_bytes);
|
||||
m_bits.assign(size_t((n + 63) / 64), ~0ull);
|
||||
if (n % 64)
|
||||
m_bits.back() = (1ull << (n % 64)) - 1;
|
||||
m_count = n;
|
||||
m_before.assign(ram, ram + m_size / 4);
|
||||
}
|
||||
|
||||
void Search::Filter(const uint32_t* ram, Test test, uint32_t number)
|
||||
{
|
||||
if (!Active())
|
||||
return;
|
||||
m_undo_bits = m_bits;
|
||||
m_undo_before = m_before;
|
||||
m_undo_count = m_count;
|
||||
const uint32_t mask = MaxValue(m_bytes);
|
||||
number &= mask;
|
||||
const uint32_t* before = m_before.data();
|
||||
uint64_t count = 0;
|
||||
for (size_t w = 0; w < m_bits.size(); w++)
|
||||
{
|
||||
uint64_t bits = m_bits[w];
|
||||
while (bits)
|
||||
{
|
||||
const int i = __builtin_ctzll(bits);
|
||||
bits &= bits - 1;
|
||||
const uint32_t address = uint32_t((w * 64 + size_t(i)) * size_t(m_bytes));
|
||||
const uint32_t now = Read(ram, address, m_bytes), was = Read(before, address, m_bytes);
|
||||
bool keep;
|
||||
switch (test)
|
||||
{
|
||||
case Test::Equal: keep = now == number; break;
|
||||
case Test::NotEqual: keep = now != number; break;
|
||||
case Test::Greater: keep = now > number; break;
|
||||
case Test::Less: keep = now < number; break;
|
||||
case Test::Changed: keep = now != was; break;
|
||||
case Test::Unchanged: keep = now == was; break;
|
||||
case Test::Increased: keep = now > was; break;
|
||||
case Test::Decreased: keep = now < was; break;
|
||||
case Test::IncreasedBy: keep = ((now - was) & mask) == number && now != was; break;
|
||||
case Test::DecreasedBy: keep = ((was - now) & mask) == number && now != was; break;
|
||||
default: keep = true; break;
|
||||
}
|
||||
if (keep)
|
||||
count++;
|
||||
else
|
||||
m_bits[w] &= ~(1ull << i);
|
||||
}
|
||||
}
|
||||
m_count = count;
|
||||
m_before.assign(ram, ram + m_size / 4);
|
||||
m_steps++;
|
||||
}
|
||||
|
||||
bool Search::Undo()
|
||||
{
|
||||
if (!CanUndo())
|
||||
return false;
|
||||
m_bits.swap(m_undo_bits);
|
||||
m_before.swap(m_undo_before);
|
||||
m_count = m_undo_count;
|
||||
m_undo_bits.clear();
|
||||
m_undo_before.clear();
|
||||
m_steps = m_steps > 0 ? m_steps - 1 : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Search::Clear()
|
||||
{
|
||||
m_bytes = 0;
|
||||
m_size = 0;
|
||||
m_count = 0;
|
||||
m_steps = 0;
|
||||
std::vector<uint64_t>().swap(m_bits);
|
||||
std::vector<uint32_t>().swap(m_before);
|
||||
std::vector<uint64_t>().swap(m_undo_bits);
|
||||
std::vector<uint32_t>().swap(m_undo_before);
|
||||
m_undo_count = 0;
|
||||
}
|
||||
|
||||
std::vector<Search::Result> Search::Results(size_t max) const
|
||||
{
|
||||
std::vector<Result> out;
|
||||
for (size_t w = 0; w < m_bits.size() && out.size() < max; w++)
|
||||
{
|
||||
uint64_t bits = m_bits[w];
|
||||
while (bits && out.size() < max)
|
||||
{
|
||||
const int i = __builtin_ctzll(bits);
|
||||
bits &= bits - 1;
|
||||
const uint32_t address = uint32_t((w * 64 + size_t(i)) * size_t(m_bytes));
|
||||
out.push_back({address, Read(m_before.data(), address, m_bytes)});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace search
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,76 @@
|
||||
// Mupen64Plus PS5 frontend: the cheat search -- finding where a game keeps a value (lives, health, money) in the
|
||||
// N64's RAM, by narrowing every address down to the ones that changed the way the value did.
|
||||
//
|
||||
// A search looks at every 1, 2 or 4-byte value of the RAM (8, 16 or 32-bit; at addresses of that alignment).
|
||||
// Start() remembers the RAM; each Filter() keeps the addresses whose value passes the test -- against a number,
|
||||
// or against what it was the last time -- and remembers the RAM again. Undo() goes one step back.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace search
|
||||
{
|
||||
enum class Test
|
||||
{
|
||||
Equal, // == the number
|
||||
NotEqual,
|
||||
Greater, // > the number
|
||||
Less,
|
||||
Changed, // than the last time
|
||||
Unchanged,
|
||||
Increased,
|
||||
Decreased,
|
||||
IncreasedBy, // by the number
|
||||
DecreasedBy,
|
||||
};
|
||||
|
||||
// the value at N64 RAM address `address` (bytes 1, 2 or 4), from RAM in the core's layout (n64_bridge.h)
|
||||
uint32_t Read(const uint32_t* ram, uint32_t address, int bytes);
|
||||
// the biggest value of that size
|
||||
uint32_t MaxValue(int bytes);
|
||||
// 1234567 -> "1,234,567"
|
||||
std::string Thousands(uint64_t n);
|
||||
|
||||
class Search
|
||||
{
|
||||
public:
|
||||
bool Active() const { return m_bytes != 0; }
|
||||
int Bytes() const { return m_bytes; }
|
||||
uint64_t Count() const { return m_count; }
|
||||
int Steps() const { return m_steps; } // filters since Start
|
||||
bool CanUndo() const { return !m_undo_bits.empty(); }
|
||||
|
||||
// a new search over the RAM (ram_size bytes) for values of `bytes` bytes: every address is in it
|
||||
void Start(const uint32_t* ram, uint32_t ram_size, int bytes);
|
||||
// keeps the addresses that pass `test` (with `number` where the test has one); the RAM is remembered again
|
||||
void Filter(const uint32_t* ram, Test test, uint32_t number);
|
||||
bool Undo();
|
||||
void Clear();
|
||||
|
||||
struct Result
|
||||
{
|
||||
uint32_t address; // in the RAM, 0..size-1
|
||||
uint32_t before; // the value the last step saw
|
||||
};
|
||||
// the first `max` addresses still in the search, lowest first
|
||||
std::vector<Result> Results(size_t max) const;
|
||||
|
||||
private:
|
||||
int m_bytes = 0;
|
||||
uint32_t m_size = 0;
|
||||
uint64_t m_count = 0;
|
||||
int m_steps = 0;
|
||||
std::vector<uint64_t> m_bits; // one bit per address / bytes
|
||||
std::vector<uint32_t> m_before; // the RAM at the last step
|
||||
std::vector<uint64_t> m_undo_bits;
|
||||
std::vector<uint32_t> m_undo_before;
|
||||
uint64_t m_undo_count = 0;
|
||||
};
|
||||
} // namespace search
|
||||
} // namespace fe
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
@@ -22,8 +23,97 @@ int Clamp(int v, int lo, int hi)
|
||||
{
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// the layout's names in the settings file and the per-game files
|
||||
const char* const kPadKeys[N64PS5_PAD_COUNT] = {"cross", "circle", "square", "triangle", "l1", "r1", "l2", "r2",
|
||||
"l3", "r3", "options", "up", "down", "left", "right", "rs_up", "rs_down", "rs_left", "rs_right"};
|
||||
const char* const kN64Keys[N64PS5_N64_COUNT] = {"none", "a", "b", "z", "l", "r", "start", "c_up", "c_down",
|
||||
"c_left", "c_right", "d_up", "d_down", "d_left", "d_right"};
|
||||
|
||||
std::string GameLayoutPath(const std::string& file_base)
|
||||
{
|
||||
return OrbisDir("layouts") + "/" + file_base + ".txt";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Layout::Layout()
|
||||
{
|
||||
n64ps5_input_default_layout(to);
|
||||
}
|
||||
|
||||
bool Layout::operator==(const Layout& o) const
|
||||
{
|
||||
return memcmp(to, o.to, sizeof(to)) == 0;
|
||||
}
|
||||
|
||||
std::string LayoutToText(const Layout& layout)
|
||||
{
|
||||
std::string s;
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
{
|
||||
if (i)
|
||||
s += ',';
|
||||
s += std::string(kPadKeys[i]) + '=' + kN64Keys[layout.to[i] < N64PS5_N64_COUNT ? layout.to[i] : 0];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Layout LayoutFromText(const std::string& text)
|
||||
{
|
||||
Layout layout;
|
||||
size_t at = 0;
|
||||
while (at < text.size())
|
||||
{
|
||||
size_t end = text.find(',', at);
|
||||
if (end == std::string::npos)
|
||||
end = text.size();
|
||||
const std::string item = text.substr(at, end - at);
|
||||
at = end + 1;
|
||||
const size_t eq = item.find('=');
|
||||
if (eq == std::string::npos)
|
||||
continue;
|
||||
const std::string pad = item.substr(0, eq), n64 = item.substr(eq + 1);
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
if (pad == kPadKeys[i])
|
||||
for (int t = 0; t < N64PS5_N64_COUNT; t++)
|
||||
if (n64 == kN64Keys[t])
|
||||
layout.to[i] = (unsigned char)t;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
bool LoadGameLayout(const std::string& file_base, Layout& out)
|
||||
{
|
||||
FILE* f = fopen(GameLayoutPath(file_base).c_str(), "r");
|
||||
if (!f)
|
||||
return false;
|
||||
char line[1024] = "";
|
||||
const bool ok = fgets(line, sizeof(line), f) != nullptr;
|
||||
fclose(f);
|
||||
if (!ok)
|
||||
return false;
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
out = LayoutFromText(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SaveGameLayout(const std::string& file_base, const Layout* layout)
|
||||
{
|
||||
const std::string path = GameLayoutPath(file_base);
|
||||
if (!layout)
|
||||
{
|
||||
unlink(path.c_str());
|
||||
return;
|
||||
}
|
||||
OrbisMkdirs(OrbisDir("layouts"));
|
||||
if (FILE* f = fopen(path.c_str(), "w"))
|
||||
{
|
||||
fprintf(f, "%s\n", LayoutToText(*layout).c_str());
|
||||
fclose(f);
|
||||
OrbisLog("[settings] %s: own controller layout", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
Settings& Config()
|
||||
{
|
||||
static Settings s;
|
||||
@@ -61,6 +151,8 @@ void Settings::Load()
|
||||
dynarec = atoi(val) != 0;
|
||||
else if (key == "hle_audio")
|
||||
hle_audio = atoi(val) != 0;
|
||||
else if (key == "overclock")
|
||||
overclock = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "gpu")
|
||||
gpu = atoi(val) != 0;
|
||||
else if (key == "upscale")
|
||||
@@ -74,12 +166,24 @@ void Settings::Load()
|
||||
dp_compat = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "audio")
|
||||
audio = atoi(val) != 0;
|
||||
else if (key == "volume")
|
||||
volume = Clamp(atoi(val), 0, 100);
|
||||
else if (key == "pak")
|
||||
pak = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "deadzone")
|
||||
deadzone = Clamp(atoi(val), 0, 30);
|
||||
else if (key == "invert_stick")
|
||||
invert_stick = Clamp(atoi(val), 0, 3);
|
||||
else if (key == "invert_c_stick")
|
||||
invert_c_stick = Clamp(atoi(val), 0, 3);
|
||||
else if (key == "layout")
|
||||
layout = LayoutFromText(val);
|
||||
else if (key == "game_view")
|
||||
game_view = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "state_slot")
|
||||
state_slot = Clamp(atoi(val), 0, 9);
|
||||
else if (key == "resume")
|
||||
resume = atoi(val) != 0;
|
||||
else if (key == "covers_download")
|
||||
covers_download = atoi(val) != 0;
|
||||
else if (key == "updates")
|
||||
@@ -111,14 +215,21 @@ void Settings::Save() const
|
||||
fprintf(f, "render_threads=%d\n", render_threads);
|
||||
fprintf(f, "dynarec=%d\n", dynarec ? 1 : 0);
|
||||
fprintf(f, "hle_audio=%d\n", hle_audio ? 1 : 0);
|
||||
fprintf(f, "overclock=%d\n", overclock);
|
||||
fprintf(f, "gpu=%d\n", gpu ? 1 : 0);
|
||||
fprintf(f, "upscale=%d\n", upscale);
|
||||
fprintf(f, "gpu_sync=%d\n", gpu_sync ? 1 : 0);
|
||||
fprintf(f, "dp_compat=%d\n", dp_compat);
|
||||
fprintf(f, "audio=%d\n", audio ? 1 : 0);
|
||||
fprintf(f, "volume=%d\n", volume);
|
||||
fprintf(f, "pak=%d\n", pak);
|
||||
fprintf(f, "deadzone=%d\n", deadzone);
|
||||
fprintf(f, "invert_stick=%d\n", invert_stick);
|
||||
fprintf(f, "invert_c_stick=%d\n", invert_c_stick);
|
||||
fprintf(f, "layout=%s\n", LayoutToText(layout).c_str());
|
||||
fprintf(f, "game_view=%d\n", game_view);
|
||||
fprintf(f, "state_slot=%d\n", state_slot);
|
||||
fprintf(f, "resume=%d\n", resume ? 1 : 0);
|
||||
fprintf(f, "covers_download=%d\n", covers_download ? 1 : 0);
|
||||
fprintf(f, "updates=%d\n", updates ? 1 : 0);
|
||||
fprintf(f, "last_dir=%s\n", last_dir.c_str());
|
||||
|
||||
@@ -2,10 +2,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "n64_bridge.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
// The controller layout: for each DualSense control (N64PS5_PAD_*), the N64 button it presses (N64PS5_N64_*).
|
||||
// The default one is the N64 pad's layout by position (n64ps5_input_default_layout).
|
||||
struct Layout
|
||||
{
|
||||
unsigned char to[N64PS5_PAD_COUNT];
|
||||
Layout();
|
||||
bool operator==(const Layout& o) const;
|
||||
bool operator!=(const Layout& o) const { return !(*this == o); }
|
||||
};
|
||||
// "cross=a,circle=c_down,..."; names it doesn't know keep their default
|
||||
std::string LayoutToText(const Layout& layout);
|
||||
Layout LayoutFromText(const std::string& text);
|
||||
// A game's own layout (pause menu): <root>/layouts/<ROM file name>.txt, by the ROM's file name like its states.
|
||||
bool LoadGameLayout(const std::string& file_base, Layout& out);
|
||||
// With nullptr the file goes and the game follows the layout of Settings.
|
||||
void SaveGameLayout(const std::string& file_base, const Layout* layout);
|
||||
|
||||
struct Settings
|
||||
{
|
||||
// picture
|
||||
@@ -17,17 +36,24 @@ struct Settings
|
||||
// emulation
|
||||
bool dynarec = true; // the x86-64 dynarec when the console gives us executable memory, else the interpreter
|
||||
bool hle_audio = true; // audio tasks done by rsp-hle (fast), else by cxd4 running the game's microcode
|
||||
int overclock = 0; // the N64 CPU's speed: 0 as on the console, 1 twice, 2 four times (CountPerOpDenomPot)
|
||||
bool gpu = true; // the GPU renderer, paraLLEl-RDP (builds with Vulkan only), else angrylion on the CPU
|
||||
int upscale = 4; // the GPU renderer's internal resolution: 1, 2, 4 or 8 times the N64's
|
||||
bool gpu_sync = true; // wait for the GPU wherever the game waits for the RDP (else faster, may glitch)
|
||||
int render_threads = 6; // angrylion workers (1 = single-threaded)
|
||||
int dp_compat = 1; // angrylion sync points between its workers: 0 fewest (fastest), 1, 2 most (safest)
|
||||
bool audio = true;
|
||||
int volume = 100; // the game's sound, 0..100 %
|
||||
// controller
|
||||
int pak = 1; // 0 none, 1 Controller Pak, 2 Rumble Pak
|
||||
int deadzone = 12; // % of the stick's travel
|
||||
int invert_stick = 0; // the left (analog) stick's axes turned round: 0 off, 1 X, 2 Y, 3 X and Y
|
||||
int invert_c_stick = 0; // the right stick (C buttons), the same way
|
||||
Layout layout; // every game's, unless it has its own (LoadGameLayout)
|
||||
// misc
|
||||
int state_slot = 0; // 0..9
|
||||
bool resume = true; // leaving a game saves where the player was; starting it again offers to continue there
|
||||
int game_view = 0; // the game list: 0 shelf (3D cover flow), 1 grid, 2 list (the touchpad cycles them)
|
||||
bool covers_download = true; // fetch box art from libretro-thumbnails
|
||||
bool updates = true; // offer new GitHub releases at start-up (fe_update.h)
|
||||
std::string last_dir; // the browser reopens here
|
||||
|
||||
+450
-58
@@ -4,6 +4,7 @@
|
||||
#include "fe_shelf.h"
|
||||
|
||||
#include "fe_covers.h"
|
||||
#include "fe_emu.h"
|
||||
#include "fe_games.h"
|
||||
#include "fe_menu.h"
|
||||
#include "fe_prefetch.h"
|
||||
@@ -44,8 +45,9 @@ namespace
|
||||
using ps5video::Rgb;
|
||||
constexpr int W = ps5video::kWidth;
|
||||
constexpr int H = ps5video::kHeight;
|
||||
// Under the wordmark: what runs the games (the port's credits are in ps5/README.md).
|
||||
constexpr const char* kTagline = "Nintendo 64 - mupen64plus, angrylion RDP, cxd4 RSP";
|
||||
// Under the wordmark: what runs the games (the port's credits are in ps5/README.md). Short enough to end
|
||||
// before the middle cover (x ~580): the renderer names are in Settings.
|
||||
constexpr const char* kTagline = "Nintendo 64 - mupen64plus";
|
||||
|
||||
// ---- camera and layout (world units: the middle cover is kCoverH tall at distance kCamDist) ----------
|
||||
constexpr float kFocal = 1000.0f;
|
||||
@@ -173,12 +175,14 @@ struct Backdrop
|
||||
{
|
||||
std::vector<uint32_t> px; // W*H
|
||||
const CoverTex* from = nullptr;
|
||||
bool floor = true; // the shelf's floor and horizon (the grid and the list have none)
|
||||
};
|
||||
|
||||
void BuildBackdrop(Pool& pool, Backdrop& bd, const CoverTex* tex)
|
||||
void BuildBackdrop(Pool& pool, Backdrop& bd, const CoverTex* tex, bool floor)
|
||||
{
|
||||
bd.px.resize(size_t(W) * H);
|
||||
bd.from = tex;
|
||||
bd.floor = floor;
|
||||
// 1. a 64x36 thumbnail: the cover's smallest level, scaled to fill (cropped), or a plain colour
|
||||
constexpr int TW = 64, TH = 36;
|
||||
float small[TH][TW][3];
|
||||
@@ -248,7 +252,7 @@ void BuildBackdrop(Pool& pool, Backdrop& bd, const CoverTex* tex)
|
||||
const float wy = std::clamp(fy - y0, 0.0f, 1.0f);
|
||||
const float vy = (y - H * 0.45f) / (H * 0.75f);
|
||||
float floor_k = 1.0f;
|
||||
if (y > floor_y)
|
||||
if (floor && y > floor_y)
|
||||
floor_k = 0.78f - 0.25f * std::min(1.0f, (y - floor_y) / (H - floor_y));
|
||||
uint32_t* row = &bd.px[size_t(y) * W];
|
||||
for (int x = x0; x < x1; x++)
|
||||
@@ -271,7 +275,7 @@ void BuildBackdrop(Pool& pool, Backdrop& bd, const CoverTex* tex)
|
||||
});
|
||||
// a faint horizon line on the floor
|
||||
const int fy = int(floor_y);
|
||||
for (int x = 0; x < W; x++)
|
||||
for (int x = 0; x < W && floor; x++)
|
||||
{
|
||||
const float k = 1.0f - std::abs(x - W * 0.5f) / (W * 0.5f);
|
||||
uint32_t& p = bd.px[size_t(fy) * W + x];
|
||||
@@ -422,18 +426,15 @@ void DrawCover(const Placed& p, uint32_t* surf, int x0, int x1)
|
||||
}
|
||||
}
|
||||
|
||||
// The glow behind the middle cover, in the cover's average colour.
|
||||
void DrawGlow(const Placed& p, uint32_t* surf, int x0, int x1, float strength)
|
||||
// A glow of radius R around the rectangle l..r, top..bot, in colour col (reaching below * R under it);
|
||||
// columns [x0, x1), rows [y0, y1).
|
||||
void DrawGlowRect(float l, float r, float top, float bot, uint32_t col, float strength, float R, float below,
|
||||
uint32_t* surf, int x0, int x1, int y0, int y1)
|
||||
{
|
||||
if (strength <= 0)
|
||||
return;
|
||||
const float top = kHorizon - kFocal * (p.wh - kCamY) / p.dist;
|
||||
const float bot = kHorizon + kFocal * kCamY / p.dist;
|
||||
const float l = float(p.sx0), r = float(p.sx1);
|
||||
const float R = 70.0f;
|
||||
const uint32_t col = p.tex->average;
|
||||
const int gx0 = std::max(x0, int(l - R)), gx1 = std::min(x1, int(r + R));
|
||||
const int gy0 = std::max(0, int(top - R)), gy1 = std::min(H, int(bot + R * 0.6f));
|
||||
const int gy0 = std::max(y0, int(top - R)), gy1 = std::min(y1, int(bot + R * below));
|
||||
for (int y = gy0; y < gy1; y++)
|
||||
{
|
||||
const float dy = std::max({top - y, 0.0f, y - bot});
|
||||
@@ -457,6 +458,94 @@ void DrawGlow(const Placed& p, uint32_t* surf, int x0, int x1, float strength)
|
||||
}
|
||||
}
|
||||
|
||||
// The glow behind the shelf's middle cover, in the cover's average colour.
|
||||
void DrawGlow(const Placed& p, uint32_t* surf, int x0, int x1, float strength)
|
||||
{
|
||||
const float top = kHorizon - kFocal * (p.wh - kCamY) / p.dist;
|
||||
const float bot = kHorizon + kFocal * kCamY / p.dist;
|
||||
DrawGlowRect(float(p.sx0), float(p.sx1), top, bot, p.tex->average, strength, 70.0f, 0.6f, surf, x0, x1, 0, H);
|
||||
}
|
||||
|
||||
// ---- flat covers: the grid and the list ------------------------------------------------------------------
|
||||
struct FlatRect
|
||||
{
|
||||
float x = 0, y = 0, w = 0, h = 0;
|
||||
};
|
||||
|
||||
// The cover fitted into the box (bx, by, bw, bh), keeping its shape, centred.
|
||||
FlatRect FitCover(const CoverTex& t, float bx, float by, float bw, float bh)
|
||||
{
|
||||
const float aspect = float(t.w[0]) / float(t.h[0]);
|
||||
float w = bw, h = bw / aspect;
|
||||
if (h > bh)
|
||||
{
|
||||
h = bh;
|
||||
w = bh * aspect;
|
||||
}
|
||||
return {bx + (bw - w) * 0.5f, by + (bh - h) * 0.5f, w, h};
|
||||
}
|
||||
|
||||
// Draws a cover into r, straight on (bilinear from the nearest mip level, anti-aliased edges): columns
|
||||
// [x0, x1), rows [y0, y1).
|
||||
void DrawFlat(const CoverTex& t, const FlatRect& r, uint32_t bright, uint32_t* surf, int x0, int x1, int y0, int y1)
|
||||
{
|
||||
const float scale = r.h / float(t.h[0]);
|
||||
const int l = scale < 0.38f ? 2 : scale < 0.75f ? 1 : 0;
|
||||
const int tw = t.w[l], th = t.h[l];
|
||||
const uint32_t* px = t.px[l].data();
|
||||
const int cx0 = std::max(x0, int(std::floor(r.x))), cx1 = std::min(x1, int(std::ceil(r.x + r.w)));
|
||||
const int ry0 = std::max(std::max(y0, 0), int(std::floor(r.y)));
|
||||
const int ry1 = std::min(std::min(y1, H), int(std::ceil(r.y + r.h)));
|
||||
if (cx0 >= cx1 || ry0 >= ry1 || th < 2)
|
||||
return;
|
||||
const float du = tw / r.w, dv = th / r.h;
|
||||
const int32_t dv16 = int32_t(dv * 65536.0f);
|
||||
const int32_t vmax = (th - 1) << 16;
|
||||
for (int x = cx0; x < cx1; x++)
|
||||
{
|
||||
const float cov_x = std::min(x + 1.0f, r.x + r.w) - std::max(float(x), r.x);
|
||||
if (cov_x <= 0)
|
||||
continue;
|
||||
const float u = (x + 0.5f - r.x) * du - 0.5f;
|
||||
const int u0 = std::clamp(int(std::floor(u)), 0, tw - 1), u1 = std::min(u0 + 1, tw - 1);
|
||||
const uint32_t fu = uint32_t(std::clamp(u - std::floor(u), 0.0f, 1.0f) * 256);
|
||||
const uint32_t* c0 = px + size_t(u0) * th;
|
||||
const uint32_t* c1 = px + size_t(u1) * th;
|
||||
int32_t v = int32_t(((ry0 + 0.5f - r.y) * dv - 0.5f) * 65536.0f);
|
||||
uint32_t* d = surf + size_t(ry0) * W + x;
|
||||
for (int y = ry0; y < ry1; y++, v += dv16, d += W)
|
||||
{
|
||||
float cov = cov_x;
|
||||
if (y < r.y || y + 1 > r.y + r.h)
|
||||
cov *= std::min(y + 1.0f, r.y + r.h) - std::max(float(y), r.y);
|
||||
if (cov <= 0)
|
||||
continue;
|
||||
const int32_t vc = std::clamp(v, 0, vmax - 1);
|
||||
const int ty = vc >> 16;
|
||||
const uint32_t fv = uint32_t(vc >> 8) & 0xff;
|
||||
const uint32_t s = Lerp32(Lerp32(c0[ty], c0[ty + 1], fv), Lerp32(c1[ty], c1[ty + 1], fv), fu);
|
||||
const uint32_t c = bright >= 256 ? s : Scale32(s, bright);
|
||||
const uint32_t a = uint32_t(std::min(cov, 1.0f) * 256);
|
||||
*d = a >= 256 ? c : Lerp32(*d, c, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A frame of thickness t around r, in col: columns [x0, x1), rows [y0, y1).
|
||||
void DrawFrame(const FlatRect& r, int t, uint32_t col, uint32_t* surf, int x0, int x1, int y0, int y1)
|
||||
{
|
||||
const int l = int(std::floor(r.x)) - t, rr = int(std::ceil(r.x + r.w)) + t;
|
||||
const int top = int(std::floor(r.y)) - t, bot = int(std::ceil(r.y + r.h)) + t;
|
||||
for (int y = std::max({top, y0, 0}); y < std::min({bot, y1, H}); y++)
|
||||
{
|
||||
const bool edge_row = y < top + t || y >= bot - t;
|
||||
uint32_t* row = surf + size_t(y) * W;
|
||||
for (int x = std::max(l, x0); x < std::min(rr, x1); x++)
|
||||
if (edge_row || x < l + t || x >= rr - t)
|
||||
row[x] = col;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- input: left/right with key repeat that speeds up -------------------------------------------------
|
||||
struct Repeat
|
||||
{
|
||||
@@ -520,6 +609,41 @@ bool Confirm(const char* question)
|
||||
}
|
||||
}
|
||||
|
||||
// A game with a state saved when the player last left it: 1 continue there, 0 start over, -1 back to the list.
|
||||
int AskResume(const std::string& saved_at)
|
||||
{
|
||||
std::vector<uint32_t> saved(ps5video::Surface(), ps5video::Surface() + size_t(W) * H);
|
||||
ps5input::Poll();
|
||||
uint32_t prev = ps5input::Pad(0).buttons;
|
||||
ps5video::DarkenRect(0, 0, W, H);
|
||||
const int bw = 1100, bh = 300, bx = (W - bw) / 2, by = (H - bh) / 2;
|
||||
ps5video::FillRect(bx - 3, by - 3, bw + 6, bh + 6, Rgb(150, 125, 255));
|
||||
ps5video::FillRect(bx, by, bw, bh, Rgb(26, 24, 48));
|
||||
DrawText(bx + 50, by + 50, "Continue where you left off?", 5, Rgb(240, 240, 250));
|
||||
DrawText(bx + 50, by + 135, ("Saved " + saved_at).c_str(), 3, Rgb(170, 160, 210));
|
||||
DrawText(bx + 50, by + 215,
|
||||
(std::string(icon::Cross) + " Continue " + icon::Square + " Start over " + icon::Circle + " Back").c_str(), 3,
|
||||
Rgb(205, 200, 228));
|
||||
ps5video::Present(0, 0, 0, 0, true);
|
||||
for (;;)
|
||||
{
|
||||
ps5input::Poll();
|
||||
const uint32_t cur = ps5input::Pad(0).buttons;
|
||||
const uint32_t down = cur & ~prev;
|
||||
prev = cur;
|
||||
if (down & SCE_PAD_BUTTON_CROSS)
|
||||
return 1;
|
||||
if (down & SCE_PAD_BUTTON_SQUARE)
|
||||
return 0;
|
||||
if (down & (SCE_PAD_BUTTON_CIRCLE | SCE_PAD_BUTTON_OPTIONS))
|
||||
{
|
||||
memcpy(ps5video::Surface(), saved.data(), saved.size() * 4);
|
||||
return -1;
|
||||
}
|
||||
ps5video::Present(0, 0, 1, 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- state kept between visits to the shelf (the game list, its covers) -----------------------------------
|
||||
struct ShelfState
|
||||
{
|
||||
@@ -570,6 +694,47 @@ void Loading(const char* text)
|
||||
CenterText(H / 2 - 20, text, 4, Rgb(220, 220, 235));
|
||||
ps5video::Present(0, 0, 0, 0, true);
|
||||
}
|
||||
// ---- the views of the game list: the 3D shelf, a grid, a list (setting "Game list view"; the touchpad
|
||||
// cycles them) ---------------------------------------------------------------------------------------------
|
||||
enum View
|
||||
{
|
||||
kViewShelf = 0,
|
||||
kViewGrid = 1,
|
||||
kViewList = 2,
|
||||
kViewCount = 3
|
||||
};
|
||||
|
||||
// grid: kGridCols x kGridRows cells, each with the box art fitted in, scrolling by rows
|
||||
constexpr int kGridCols = 6, kGridRows = 3;
|
||||
constexpr float kGridX = 150, kGridY = 215, kCellW = 270, kCellH = 205, kCellPad = 14;
|
||||
// covers are drawn between these rows only (the next row doesn't peek in); the selection's glow a bit further
|
||||
constexpr int kGridTop = 205, kGridBottom = 836;
|
||||
constexpr int kGlowTop = 190, kGlowBottom = 852;
|
||||
|
||||
// list: kListRows titles with a small cover, the selected game's box art large on the right
|
||||
constexpr int kListRows = 13;
|
||||
constexpr int kListX = 100, kListY = 212, kListW = 1000, kListRowH = 56;
|
||||
constexpr float kListArtX = 1170, kListArtY = 222, kListArtW = 650, kListArtH = 470;
|
||||
|
||||
const uint32_t kAccent = Rgb(150, 125, 255);
|
||||
const uint32_t kSel = Rgb(78, 62, 170);
|
||||
|
||||
// "USA Z64 USB", as under the selection
|
||||
std::string SubLine(const GameInfo& g, const CoverTex* tex)
|
||||
{
|
||||
std::string sub;
|
||||
if (!g.region.empty())
|
||||
sub = g.region + " ";
|
||||
std::string ext = g.ext.size() > 1 ? g.ext.substr(1) : g.ext;
|
||||
for (char& c : ext)
|
||||
c = char(toupper(uint8_t(c)));
|
||||
sub += ext;
|
||||
if (g.on_usb)
|
||||
sub += " USB";
|
||||
if (tex && !tex->real)
|
||||
sub += " no cover";
|
||||
return sub;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ShelfShutdown()
|
||||
@@ -577,9 +742,11 @@ void ShelfShutdown()
|
||||
S().covers.Stop();
|
||||
}
|
||||
|
||||
std::string Shelf()
|
||||
std::string Shelf(bool* resume)
|
||||
{
|
||||
Settings& cfg = Config();
|
||||
if (resume)
|
||||
*resume = false;
|
||||
ShelfState& st = S();
|
||||
N64_STAGE(Shelf, "pool");
|
||||
Pool pool;
|
||||
@@ -611,18 +778,22 @@ std::string Shelf()
|
||||
for (int i = 0; i < n; i++)
|
||||
if (st.games[size_t(i)].path == cfg.last_rom)
|
||||
sel = i;
|
||||
float pos = float(sel);
|
||||
st.covers.SetFocus(sel);
|
||||
float pos = float(sel); // shelf: the cover in the middle (gliding)
|
||||
int grid_top = 0; // grid: the first row shown
|
||||
float grid_pos = 0; // ... gliding
|
||||
int list_top = 0; // list: the first title shown
|
||||
int focus = -1; // where the cover service loads around
|
||||
int last_view = -1;
|
||||
|
||||
Backdrop bd_cur, bd_old;
|
||||
float fade = 1.0f; // 0..1: old -> cur
|
||||
N64_STAGE(Shelf, "build backdrop");
|
||||
BuildBackdrop(pool, bd_cur, nullptr);
|
||||
BuildBackdrop(pool, bd_cur, nullptr, std::clamp(cfg.game_view, 0, kViewCount - 1) == kViewShelf);
|
||||
N64_STAGE(Shelf, "first frame");
|
||||
const CoverTex* bd_wanted = nullptr;
|
||||
double sel_since = Now();
|
||||
|
||||
Repeat rep_l, rep_r, rep_l1, rep_r1;
|
||||
Repeat rep_l, rep_r, rep_u, rep_d, rep_l1, rep_r1;
|
||||
ps5input::Poll();
|
||||
uint32_t prev = ps5input::Pad(0).buttons;
|
||||
double last = Now();
|
||||
@@ -635,6 +806,7 @@ std::string Shelf()
|
||||
const double now = Now();
|
||||
const float dt = float(std::min(0.05, now - last));
|
||||
last = now;
|
||||
const int view = std::clamp(cfg.game_view, 0, kViewCount - 1);
|
||||
|
||||
// -- input
|
||||
ps5input::Poll();
|
||||
@@ -642,28 +814,81 @@ std::string Shelf()
|
||||
const uint32_t down = cur & ~prev;
|
||||
prev = cur;
|
||||
const int old_sel = sel;
|
||||
if (n > 0)
|
||||
{
|
||||
if (rep_l.Fire(cur & SCE_PAD_BUTTON_LEFT, now))
|
||||
sel = std::max(0, sel - 1);
|
||||
if (rep_r.Fire(cur & SCE_PAD_BUTTON_RIGHT, now))
|
||||
sel = std::min(n - 1, sel + 1);
|
||||
if (rep_l1.Fire(cur & SCE_PAD_BUTTON_L1, now))
|
||||
sel = std::max(0, sel - 10);
|
||||
if (rep_r1.Fire(cur & SCE_PAD_BUTTON_R1, now))
|
||||
sel = std::min(n - 1, sel + 10);
|
||||
const bool left = rep_l.Fire(cur & SCE_PAD_BUTTON_LEFT, now);
|
||||
const bool right = rep_r.Fire(cur & SCE_PAD_BUTTON_RIGHT, now);
|
||||
const bool up = rep_u.Fire(cur & SCE_PAD_BUTTON_UP, now);
|
||||
const bool dn = rep_d.Fire(cur & SCE_PAD_BUTTON_DOWN, now);
|
||||
const bool l1 = rep_l1.Fire(cur & SCE_PAD_BUTTON_L1, now);
|
||||
const bool r1 = rep_r1.Fire(cur & SCE_PAD_BUTTON_R1, now);
|
||||
if (n > 0 && view == kViewShelf)
|
||||
{
|
||||
if (left)
|
||||
sel = std::max(0, sel - 1);
|
||||
if (right)
|
||||
sel = std::min(n - 1, sel + 1);
|
||||
if (l1)
|
||||
sel = std::max(0, sel - 10);
|
||||
if (r1)
|
||||
sel = std::min(n - 1, sel + 10);
|
||||
}
|
||||
else if (n > 0 && view == kViewGrid)
|
||||
{
|
||||
const int page = kGridCols * kGridRows;
|
||||
const int last_row = (n - 1) / kGridCols;
|
||||
if (left)
|
||||
sel = std::max(0, sel - 1);
|
||||
if (right)
|
||||
sel = std::min(n - 1, sel + 1);
|
||||
if (up && sel >= kGridCols)
|
||||
sel -= kGridCols;
|
||||
if (dn && sel / kGridCols < last_row)
|
||||
sel = std::min(n - 1, sel + kGridCols);
|
||||
// a page up or down, in the same column where it can
|
||||
if (l1)
|
||||
sel = sel >= page ? sel - page : sel % kGridCols;
|
||||
if (r1)
|
||||
sel = sel + page <= n - 1 ? sel + page : std::max(sel, std::min(n - 1, last_row * kGridCols + sel % kGridCols));
|
||||
}
|
||||
else if (n > 0)
|
||||
{
|
||||
if (up)
|
||||
sel = std::max(0, sel - 1);
|
||||
if (dn)
|
||||
sel = std::min(n - 1, sel + 1);
|
||||
if (l1)
|
||||
sel = std::max(0, sel - (kListRows - 1));
|
||||
if (r1)
|
||||
sel = std::min(n - 1, sel + (kListRows - 1));
|
||||
}
|
||||
}
|
||||
if (sel != old_sel)
|
||||
{
|
||||
st.covers.SetFocus(sel);
|
||||
sel_since = now;
|
||||
dirty = true;
|
||||
}
|
||||
if ((down & SCE_PAD_BUTTON_CROSS) && n > 0)
|
||||
{
|
||||
cfg.last_rom = st.games[size_t(sel)].path;
|
||||
const std::string path = st.games[size_t(sel)].path;
|
||||
std::string saved_at;
|
||||
const int choice = cfg.resume && emu::ResumeStateExists(path, &saved_at) ? AskResume(saved_at) : 0;
|
||||
if (choice >= 0)
|
||||
{
|
||||
cfg.last_rom = path;
|
||||
cfg.Save();
|
||||
if (resume)
|
||||
*resume = choice == 1;
|
||||
return path;
|
||||
}
|
||||
ps5input::Poll();
|
||||
prev = ps5input::Pad(0).buttons;
|
||||
dirty = true;
|
||||
}
|
||||
if (down & SCE_PAD_BUTTON_TOUCH_PAD)
|
||||
{
|
||||
cfg.game_view = (view + 1) % kViewCount;
|
||||
cfg.Save();
|
||||
return st.games[size_t(sel)].path;
|
||||
dirty = true;
|
||||
}
|
||||
if (down & SCE_PAD_BUTTON_TRIANGLE)
|
||||
{
|
||||
@@ -699,7 +924,52 @@ std::string Shelf()
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// -- covers that arrived
|
||||
// -- scrolling: the grid's rows and the list's titles keep the selection in view
|
||||
if (n > 0)
|
||||
{
|
||||
const int row = sel / kGridCols;
|
||||
const int rows = (n + kGridCols - 1) / kGridCols;
|
||||
if (row < grid_top)
|
||||
grid_top = row;
|
||||
if (row > grid_top + kGridRows - 1)
|
||||
grid_top = row - kGridRows + 1;
|
||||
grid_top = std::clamp(grid_top, 0, std::max(0, rows - kGridRows));
|
||||
if (sel < list_top + 2)
|
||||
list_top = sel - 2;
|
||||
if (sel > list_top + kListRows - 3)
|
||||
list_top = sel - kListRows + 3;
|
||||
list_top = std::clamp(list_top, 0, std::max(0, n - kListRows));
|
||||
}
|
||||
const int new_view = std::clamp(cfg.game_view, 0, kViewCount - 1);
|
||||
if (new_view != last_view)
|
||||
{
|
||||
// a new view: no glide into it, and the shelf's floor only behind the shelf
|
||||
last_view = new_view;
|
||||
pos = float(sel);
|
||||
grid_pos = float(grid_top);
|
||||
if (bd_cur.floor != (new_view == kViewShelf))
|
||||
{
|
||||
BuildBackdrop(pool, bd_cur, bd_cur.from, new_view == kViewShelf);
|
||||
bd_old.px.clear();
|
||||
fade = 1.0f;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// -- covers: loaded around the selection (shelf) or the middle of what's on screen (grid, list)
|
||||
if (n > 0)
|
||||
{
|
||||
int want = sel;
|
||||
if (new_view == kViewGrid)
|
||||
want = std::min(n - 1, grid_top * kGridCols + kGridCols * kGridRows / 2);
|
||||
else if (new_view == kViewList)
|
||||
want = std::min(n - 1, list_top + kListRows / 2);
|
||||
if (want != focus)
|
||||
{
|
||||
focus = want;
|
||||
st.covers.SetFocus(focus);
|
||||
}
|
||||
}
|
||||
const size_t before = std::count_if(st.slots.begin(), st.slots.end(), [](const CoverPtr& c) { return c != nullptr; });
|
||||
st.covers.Collect(st.slots, kKeepRadius);
|
||||
const size_t after = std::count_if(st.slots.begin(), st.slots.end(), [](const CoverPtr& c) { return c != nullptr; });
|
||||
@@ -715,6 +985,13 @@ std::string Shelf()
|
||||
pos = target;
|
||||
dirty = true;
|
||||
}
|
||||
if (std::abs(float(grid_top) - grid_pos) > 0.0005f)
|
||||
{
|
||||
grid_pos += (float(grid_top) - grid_pos) * (1.0f - std::exp(-dt * 14.0f));
|
||||
if (std::abs(float(grid_top) - grid_pos) < 0.0005f)
|
||||
grid_pos = float(grid_top);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// -- backdrop follows the selection once it rests for a moment
|
||||
const CoverTex* sel_tex = (n > 0 && st.slots[size_t(sel)]) ? st.slots[size_t(sel)].get() : nullptr;
|
||||
@@ -722,7 +999,7 @@ std::string Shelf()
|
||||
{
|
||||
bd_wanted = sel_tex;
|
||||
std::swap(bd_old, bd_cur);
|
||||
BuildBackdrop(pool, bd_cur, sel_tex);
|
||||
BuildBackdrop(pool, bd_cur, sel_tex, new_view == kViewShelf);
|
||||
fade = bd_old.px.empty() ? 1.0f : 0.0f;
|
||||
dirty = true;
|
||||
}
|
||||
@@ -747,21 +1024,71 @@ std::string Shelf()
|
||||
}
|
||||
dirty = false;
|
||||
|
||||
// -- place the covers, far ones first
|
||||
std::vector<Placed> placed;
|
||||
if (n > 0)
|
||||
auto tex_of = [&](int i) -> const CoverTex* {
|
||||
return st.slots[size_t(i)] ? st.slots[size_t(i)].get() : LoadingCard().get();
|
||||
};
|
||||
|
||||
// -- lay out what this view shows
|
||||
std::vector<Placed> placed; // shelf: far ones first
|
||||
struct Flat
|
||||
{
|
||||
const CoverTex* tex;
|
||||
FlatRect r;
|
||||
bool selected;
|
||||
};
|
||||
std::vector<Flat> flats; // grid and list
|
||||
if (n > 0 && new_view == kViewShelf)
|
||||
{
|
||||
const int lo = std::max(0, int(std::floor(pos - kVisible)));
|
||||
const int hi = std::min(n - 1, int(std::ceil(pos + kVisible)));
|
||||
for (int i = lo; i <= hi; i++)
|
||||
{
|
||||
const CoverTex* tex = st.slots[size_t(i)] ? st.slots[size_t(i)].get() : LoadingCard().get();
|
||||
Placed p;
|
||||
if (Place(p, tex, float(i) - pos))
|
||||
if (Place(p, tex_of(i), float(i) - pos))
|
||||
placed.push_back(p);
|
||||
}
|
||||
std::sort(placed.begin(), placed.end(), [](const Placed& a, const Placed& b) { return std::abs(a.d) > std::abs(b.d); });
|
||||
}
|
||||
else if (n > 0 && new_view == kViewGrid)
|
||||
{
|
||||
const int r0 = std::max(0, int(std::floor(grid_pos)) - 1);
|
||||
const int r1 = int(std::ceil(grid_pos)) + kGridRows;
|
||||
for (int row = r0; row <= r1; row++)
|
||||
for (int c = 0; c < kGridCols; c++)
|
||||
{
|
||||
const int i = row * kGridCols + c;
|
||||
if (i >= n)
|
||||
break;
|
||||
const float cy = kGridY + (float(row) - grid_pos) * kCellH;
|
||||
if (cy + kCellH < kGridTop || cy > kGridBottom)
|
||||
continue;
|
||||
float bx = kGridX + c * kCellW + kCellPad, by = cy + kCellPad;
|
||||
float bw = kCellW - 2 * kCellPad, bh = kCellH - 2 * kCellPad;
|
||||
if (i == sel) // the selection stands out a little
|
||||
{
|
||||
bx -= bw * 0.04f;
|
||||
by -= bh * 0.04f;
|
||||
bw *= 1.08f;
|
||||
bh *= 1.08f;
|
||||
}
|
||||
const CoverTex* tex = tex_of(i);
|
||||
flats.push_back({tex, FitCover(*tex, bx, by, bw, bh), i == sel});
|
||||
}
|
||||
// the selection last, over its neighbours
|
||||
std::stable_partition(flats.begin(), flats.end(), [](const Flat& f) { return !f.selected; });
|
||||
}
|
||||
else if (n > 0 && new_view == kViewList)
|
||||
{
|
||||
for (int k = 0; k < kListRows && list_top + k < n; k++)
|
||||
{
|
||||
const CoverTex* tex = tex_of(list_top + k);
|
||||
const float ry = float(kListY + k * kListRowH);
|
||||
flats.push_back({tex, FitCover(*tex, kListX + 14.0f, ry + 6, 80, kListRowH - 12.0f), false});
|
||||
}
|
||||
const CoverTex* tex = tex_of(sel);
|
||||
flats.push_back({tex, FitCover(*tex, kListArtX, kListArtY, kListArtW, kListArtH), true});
|
||||
}
|
||||
|
||||
uint32_t* surf = ps5video::Surface();
|
||||
const uint32_t fade_w = uint32_t(fade * 256);
|
||||
const double t_render = Now();
|
||||
@@ -780,12 +1107,47 @@ std::string Shelf()
|
||||
out[x] = Lerp32(o[x], a[x], fade_w);
|
||||
}
|
||||
}
|
||||
// covers; the glow goes right before the middle one
|
||||
for (const Placed& p : placed)
|
||||
if (new_view == kViewShelf)
|
||||
{
|
||||
if (std::abs(p.d) < 0.5f)
|
||||
DrawGlow(p, surf, x0, x1, 1.0f - std::abs(p.d) * 2.0f);
|
||||
DrawCover(p, surf, x0, x1);
|
||||
// covers; the glow goes right before the middle one
|
||||
for (const Placed& p : placed)
|
||||
{
|
||||
if (std::abs(p.d) < 0.5f)
|
||||
DrawGlow(p, surf, x0, x1, 1.0f - std::abs(p.d) * 2.0f);
|
||||
DrawCover(p, surf, x0, x1);
|
||||
}
|
||||
}
|
||||
else if (new_view == kViewGrid)
|
||||
{
|
||||
for (const Flat& f : flats)
|
||||
{
|
||||
if (f.selected)
|
||||
DrawGlowRect(f.r.x, f.r.x + f.r.w, f.r.y, f.r.y + f.r.h, f.tex->average, 1.0f, 46.0f, 1.0f, surf, x0, x1,
|
||||
kGlowTop, kGlowBottom);
|
||||
DrawFlat(*f.tex, f.r, f.selected ? 256 : 200, surf, x0, x1, kGridTop, kGridBottom);
|
||||
if (f.selected)
|
||||
DrawFrame(f.r, 4, kAccent, surf, x0, x1, kGridTop, kGridBottom);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// a darker panel behind the titles, and the selected row in the menus' colour
|
||||
const int py0 = kListY - 8, py1 = kListY + kListRows * kListRowH + 8;
|
||||
const int px0 = std::max(x0, kListX), px1 = std::min(x1, kListX + kListW);
|
||||
const int srow = sel - list_top;
|
||||
for (int y = py0; y < py1 && px0 < px1; y++)
|
||||
{
|
||||
uint32_t* row = surf + size_t(y) * W;
|
||||
const bool in_sel = n > 0 && y >= kListY + srow * kListRowH && y < kListY + (srow + 1) * kListRowH;
|
||||
for (int x = px0; x < px1; x++)
|
||||
row[x] = in_sel ? Lerp32(row[x], kSel, 215) : Scale32(row[x], 120);
|
||||
}
|
||||
for (const Flat& f : flats)
|
||||
{
|
||||
if (f.selected)
|
||||
DrawGlowRect(f.r.x, f.r.x + f.r.w, f.r.y, f.r.y + f.r.h, f.tex->average, 0.9f, 60.0f, 1.0f, surf, x0, x1, 0, H);
|
||||
DrawFlat(*f.tex, f.r, 256, surf, x0, x1, 0, H);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -795,7 +1157,8 @@ std::string Shelf()
|
||||
sum += Now() - t_render;
|
||||
if (++frames == 60)
|
||||
{
|
||||
OrbisLog("[shelf] 3D pass: %.2f ms a frame (average of 60)", sum / frames * 1000.0);
|
||||
OrbisLog("[shelf] %s pass: %.2f ms a frame (average of 60)", new_view == kViewShelf ? "3D" : "flat",
|
||||
sum / frames * 1000.0);
|
||||
sum = 0;
|
||||
frames = 0;
|
||||
}
|
||||
@@ -812,19 +1175,41 @@ std::string Shelf()
|
||||
snprintf(count, sizeof(count), "%d / %d", sel + 1, n);
|
||||
DrawText(W - 80 - TextWidth(count, 4), 50, count, 4, Rgb(220, 220, 235));
|
||||
const GameInfo& g = st.games[size_t(sel)];
|
||||
CenterText(800, g.title, 6, Rgb(245, 245, 252));
|
||||
std::string sub;
|
||||
if (!g.region.empty())
|
||||
sub = g.region + " ";
|
||||
std::string ext = g.ext.size() > 1 ? g.ext.substr(1) : g.ext;
|
||||
for (char& c : ext)
|
||||
c = char(toupper(uint8_t(c)));
|
||||
sub += ext;
|
||||
if (g.on_usb)
|
||||
sub += " USB";
|
||||
if (sel_tex && !sel_tex->real)
|
||||
sub += " no cover";
|
||||
CenterText(880, sub, 3, Rgb(175, 170, 205));
|
||||
if (new_view == kViewShelf)
|
||||
{
|
||||
CenterText(800, g.title, 6, Rgb(245, 245, 252));
|
||||
CenterText(880, SubLine(g, sel_tex), 3, Rgb(175, 170, 205));
|
||||
}
|
||||
else if (new_view == kViewGrid)
|
||||
{
|
||||
CenterText(872, g.title, 5, Rgb(245, 245, 252));
|
||||
CenterText(940, SubLine(g, sel_tex), 3, Rgb(175, 170, 205));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int k = 0; k < kListRows && list_top + k < n; k++)
|
||||
{
|
||||
const int i = list_top + k;
|
||||
const std::string t = FitText(st.games[size_t(i)].title, 3, kListW - 150);
|
||||
DrawText(kListX + 112, kListY + k * kListRowH + 13, t.c_str(), 3,
|
||||
i == sel ? Rgb(250, 250, 255) : Rgb(200, 196, 224));
|
||||
}
|
||||
// a scroll bar when the list is longer than the panel
|
||||
if (n > kListRows)
|
||||
{
|
||||
const int track = kListRows * kListRowH;
|
||||
const int thumb = std::max(30, track * kListRows / n);
|
||||
const int ty = kListY + (track - thumb) * list_top / (n - kListRows);
|
||||
ps5video::FillRect(kListX + kListW - 14, kListY, 6, track, Rgb(40, 36, 70));
|
||||
ps5video::FillRect(kListX + kListW - 14, ty, 6, thumb, kAccent);
|
||||
}
|
||||
const int tx = int(kListArtX);
|
||||
const std::string title = FitText(g.title, 4, int(kListArtW));
|
||||
DrawText(tx, int(kListArtY + kListArtH) + 28, title.c_str(), 4, Rgb(245, 245, 252));
|
||||
DrawText(tx, int(kListArtY + kListArtH) + 82, SubLine(g, sel_tex).c_str(), 3, Rgb(175, 170, 205));
|
||||
const std::string file = FitText(g.file_base + g.ext, 2, int(kListArtW));
|
||||
DrawText(tx, int(kListArtY + kListArtH) + 126, file.c_str(), 2, Rgb(140, 136, 170));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -845,12 +1230,19 @@ std::string Shelf()
|
||||
const char* s = "Offline: covers next time";
|
||||
DrawText(W - 80 - TextWidth(s, 3), 112, s, 3, Rgb(170, 160, 210));
|
||||
}
|
||||
else if (CoversForNextStart() > 0 && cfg.covers_download)
|
||||
{
|
||||
const char* s = "New games: covers next time you open the app";
|
||||
DrawText(W - 80 - TextWidth(s, 3), 112, s, 3, Rgb(170, 160, 210));
|
||||
}
|
||||
// hints
|
||||
ps5video::DarkenRect(0, H - 78, W, 78);
|
||||
const std::string sp = " ";
|
||||
const std::string browse = new_view == kViewShelf ? icon::DpadLeftRight : new_view == kViewGrid ? icon::Dpad : icon::DpadUpDown;
|
||||
const std::string skip = new_view == kViewShelf ? " Skip 10" : " Page";
|
||||
const std::string hint = n > 0
|
||||
? std::string(icon::Cross) + " Play" + sp + icon::DpadLeftRight + " Browse" + sp + icon::L1 + " " + icon::R1 +
|
||||
" Skip 10" + sp + icon::Triangle + " Settings" + sp + icon::Square + " Get cover" + sp +
|
||||
? std::string(icon::Cross) + " Play" + sp + browse + " Browse" + sp + icon::L1 + " " + icon::R1 + skip + sp +
|
||||
icon::Triangle + " Settings" + sp + icon::Square + " Get cover" + sp + icon::Touchpad + " View" + sp +
|
||||
icon::Options + " Quit"
|
||||
: std::string(icon::Triangle) + " Settings" + sp + icon::Options + " Quit";
|
||||
DrawText((W - TextWidth(hint.c_str(), 3)) / 2, H - 60, hint.c_str(), 3, Rgb(205, 200, 228));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Mupen64Plus PS5 frontend: the game shelf -- a 3D cover flow of the library, like PS5SX2's.
|
||||
// Mupen64Plus PS5 frontend: the game shelf -- a 3D cover flow of the library, like PS5SX2's. Setting "Game
|
||||
// list view" (the touchpad cycles it) shows the library as a grid of covers or a list of titles instead.
|
||||
//
|
||||
// The selected game's box art stands in the middle, lit by a glow in its own colour, over a blurred
|
||||
// copy of the same art; the rest of the library stands at an angle on both sides, in perspective, and every
|
||||
@@ -16,8 +17,9 @@
|
||||
|
||||
namespace fe
|
||||
{
|
||||
// Returns the game to play, or "" when the player quits.
|
||||
std::string Shelf();
|
||||
// Returns the game to play, or "" when the player quits. `resume`: the player chose to continue where they
|
||||
// left the game (emu::ResumeStateExists).
|
||||
std::string Shelf(bool* resume = nullptr);
|
||||
// Stops the cover downloads before the payload exits.
|
||||
void ShelfShutdown();
|
||||
} // namespace fe
|
||||
+27
-10
@@ -191,19 +191,19 @@ struct Target
|
||||
int w, h, pitch;
|
||||
};
|
||||
|
||||
void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t color, bool shadow)
|
||||
// bold: the text again 1..bold pixels to the right (one weight of Roboto is built in)
|
||||
void Draw(const Target& t, int x, int y, const char* utf8, int px, uint32_t color, bool shadow, int bold = 0)
|
||||
{
|
||||
Fonts& fonts = F();
|
||||
if (!fonts.ok || !utf8)
|
||||
return;
|
||||
const int px = EmPx(scale);
|
||||
const int baseline = y + int(std::lround(px * 0.80f));
|
||||
const int sh = std::max(1, scale / 2); // shadow offset
|
||||
for (int pass = shadow ? 0 : 1; pass < 2; pass++)
|
||||
const int sh = std::max(1, px / 20); // shadow offset
|
||||
for (int pass = shadow ? 0 : 1; pass < 2 + bold; pass++)
|
||||
{
|
||||
const uint32_t col = pass == 0 ? 0xff000000u : color;
|
||||
const uint32_t strength = pass == 0 ? 150 : 255;
|
||||
const int ox = pass == 0 ? sh : 0, oy = pass == 0 ? sh : 0;
|
||||
const int ox = pass == 0 ? sh : pass - 1, oy = pass == 0 ? sh : 0;
|
||||
float pen = float(x);
|
||||
uint32_t prev = 0;
|
||||
const char* s = utf8;
|
||||
@@ -238,11 +238,15 @@ void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t c
|
||||
} // namespace
|
||||
|
||||
int TextWidth(const char* utf8, int scale)
|
||||
{
|
||||
return TextWidthPx(utf8, EmPx(scale));
|
||||
}
|
||||
|
||||
int TextWidthPx(const char* utf8, int px)
|
||||
{
|
||||
Fonts& fonts = F();
|
||||
if (!fonts.ok || !utf8)
|
||||
return 0;
|
||||
const int px = EmPx(scale);
|
||||
float pen = 0;
|
||||
uint32_t prev = 0;
|
||||
const char* s = utf8;
|
||||
@@ -263,17 +267,30 @@ void DrawText(int x, int y, const char* utf8, int scale, uint32_t color, bool ou
|
||||
uint32_t* surf = ps5video::Surface();
|
||||
if (!surf)
|
||||
return;
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, scale, color, outline);
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, EmPx(scale), color, outline);
|
||||
}
|
||||
|
||||
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline, int bold)
|
||||
{
|
||||
uint32_t* surf = ps5video::Surface();
|
||||
if (!surf)
|
||||
return;
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, px, color, outline, bold);
|
||||
}
|
||||
|
||||
void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale, uint32_t color, bool outline)
|
||||
{
|
||||
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, scale, color, outline);
|
||||
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, EmPx(scale), color, outline);
|
||||
}
|
||||
|
||||
std::string FitText(const std::string& utf8, int scale, int max_px)
|
||||
{
|
||||
if (TextWidth(utf8.c_str(), scale) <= max_px)
|
||||
return FitTextPx(utf8, EmPx(scale), max_px);
|
||||
}
|
||||
|
||||
std::string FitTextPx(const std::string& utf8, int px, int max_px)
|
||||
{
|
||||
if (TextWidthPx(utf8.c_str(), px) <= max_px)
|
||||
return utf8;
|
||||
std::string s = utf8;
|
||||
while (!s.empty())
|
||||
@@ -283,7 +300,7 @@ std::string FitText(const std::string& utf8, int scale, int max_px)
|
||||
n--;
|
||||
s.resize(n);
|
||||
const std::string t = s + "\xE2\x80\xA6"; // the ellipsis character, as PS5SX2
|
||||
if (TextWidth(t.c_str(), scale) <= max_px)
|
||||
if (TextWidthPx(t.c_str(), px) <= max_px)
|
||||
return t;
|
||||
}
|
||||
return "\xE2\x80\xA6";
|
||||
|
||||
@@ -30,6 +30,12 @@ void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale,
|
||||
// Cuts the string so it fits max_px (adds "...").
|
||||
std::string FitText(const std::string& utf8, int scale, int max_px);
|
||||
|
||||
// The same with the line height in pixels (the em; the pause menu's sizes), and a bold made of the text drawn
|
||||
// again `bold` pixels further right.
|
||||
int TextWidthPx(const char* utf8, int px);
|
||||
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline = false, int bold = 0);
|
||||
std::string FitTextPx(const std::string& utf8, int px, int max_px);
|
||||
|
||||
// PromptFont's controller symbols (the same code points as PS5SX2's fe_text.h).
|
||||
namespace icon
|
||||
{
|
||||
@@ -45,6 +51,8 @@ constexpr const char* L3 = "\xE2\x86\xBA";
|
||||
constexpr const char* R3 = "\xE2\x86\xBB";
|
||||
constexpr const char* DpadLeftRight = "\xE2\x86\xA2";
|
||||
constexpr const char* DpadUpDown = "\xE2\x86\xA3";
|
||||
constexpr const char* Dpad = "\xE2\x87\x8E"; // all four directions
|
||||
constexpr const char* Touchpad = "\xE2\x87\xA7";
|
||||
constexpr const char* Options = "\xE2\x88\x88";
|
||||
constexpr const char* GitHub = "\xEF\x82\x9B"; // U+F09B, Font Awesome Brands
|
||||
} // namespace icon
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace
|
||||
{
|
||||
constexpr size_t kMaxReleaseJson = 1u << 20;
|
||||
constexpr size_t kMaxZip = 64u << 20;
|
||||
constexpr size_t kMaxFile = 32u << 20;
|
||||
// 0.6.3 to 0.7.2 refuse a file over 32 MiB: the Makefile keeps eboot.bin under that, so they can still update
|
||||
constexpr size_t kMaxFile = 64u << 20;
|
||||
const char* const kAsset = N64PS5_RELEASE_ZIP; // not PPSA99064.zip: see RELEASE_ZIP in the Makefile
|
||||
|
||||
// ---- a small JSON reader (the release object) --------------------------------------------------------------
|
||||
@@ -454,39 +455,6 @@ bool ParseRelease(const std::string& body, Release& r, std::string& why)
|
||||
}
|
||||
|
||||
// ---- installing ------------------------------------------------------------------------------------------
|
||||
// Every copy of the app folder the console can see (ShadowMountPlus's scan paths).
|
||||
std::vector<std::string> AppFolders()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
std::vector<std::string> bases;
|
||||
const char* hb = getenv("N64PS5_PS5_HOMEBREW"); // host tests
|
||||
bases.push_back(hb && *hb ? hb : "/data/homebrew");
|
||||
if (!(hb && *hb))
|
||||
{
|
||||
char buf[64];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "/mnt/usb%d/homebrew", i);
|
||||
bases.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "/mnt/usb%d", i);
|
||||
bases.push_back(buf);
|
||||
}
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "/mnt/ext%d/homebrew", i);
|
||||
bases.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "/mnt/ext%d", i);
|
||||
bases.push_back(buf);
|
||||
}
|
||||
}
|
||||
for (const std::string& b : bases)
|
||||
{
|
||||
const std::string dir = b + "/" N64PS5_TITLE_ID;
|
||||
if (OrbisIsFile(dir + "/sce_sys/param.json") && OrbisIsFile(dir + "/eboot.bin"))
|
||||
out.push_back(dir);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// A path inside the zip we accept: under PPSA99064/, no "..", no absolute path or backslash.
|
||||
bool SafeEntry(const std::string& name, std::string& rel)
|
||||
@@ -747,7 +715,7 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
std::vector<std::string> folders;
|
||||
if (ok)
|
||||
{
|
||||
folders = AppFolders();
|
||||
folders = OrbisAppFolders();
|
||||
if (folders.empty())
|
||||
{
|
||||
ok = false;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// N64PS5_HOST_DUMP_DIR=dir where (default .)
|
||||
// N64PS5_HOST_MAX_FLIPS=n abort after n flips (a hang guard)
|
||||
// N64PS5_HOST_REALTIME=1 flips wait 1/60 s
|
||||
// N64PS5_HOST_IME="text[|text...]" what is typed on the keyboard, in turn ("!" cancels)
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@@ -198,6 +199,77 @@ int sceKernelUsleep(unsigned int usec)
|
||||
{
|
||||
return usleep(usec);
|
||||
}
|
||||
// ---- libSceSysmodule (only the IME dialog loads) and libSceImeDialog, faked --------------------------------
|
||||
// N64PS5_HOST_IME="text[|text...]": what the player types in each keyboard opened, in turn ("!" cancels it; with
|
||||
// none left, it is canceled). The keyboard stays up for three status calls.
|
||||
static std::vector<std::string> g_ime_texts;
|
||||
static size_t g_ime_next = 0;
|
||||
static int g_ime_polls = -1; // -1: not open
|
||||
static bool g_ime_cancel = false;
|
||||
static SceImeDialogParam g_ime_param;
|
||||
|
||||
int sceImeDialogInit(const SceImeDialogParam* p, const void*)
|
||||
{
|
||||
if (g_ime_polls >= 0)
|
||||
return (int)0x80bc0001; // busy
|
||||
static bool parsed = false;
|
||||
if (!parsed)
|
||||
{
|
||||
parsed = true;
|
||||
if (const char* s = getenv("N64PS5_HOST_IME"))
|
||||
{
|
||||
std::string str = s;
|
||||
size_t pos = 0;
|
||||
for (;;)
|
||||
{
|
||||
const size_t end = str.find('|', pos);
|
||||
g_ime_texts.push_back(str.substr(pos, end == std::string::npos ? std::string::npos : end - pos));
|
||||
if (end == std::string::npos)
|
||||
break;
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_ime_param = *p;
|
||||
g_ime_polls = 0;
|
||||
std::string title;
|
||||
for (const uint16_t* t = p->title; t && *t; t++)
|
||||
title += *t < 0x80 ? char(*t) : '?';
|
||||
const std::string text = g_ime_next < g_ime_texts.size() ? g_ime_texts[g_ime_next++] : "!";
|
||||
g_ime_cancel = text == "!";
|
||||
if (!g_ime_cancel)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (; i < text.size() && i < p->maxTextLength; i++)
|
||||
p->inputTextBuffer[i] = uint16_t((unsigned char)text[i]);
|
||||
p->inputTextBuffer[i] = 0;
|
||||
}
|
||||
fprintf(stderr, "[host] keyboard \"%s\" (type %d, max %u): %s\n", title.c_str(), p->type, p->maxTextLength,
|
||||
g_ime_cancel ? "canceled" : text.c_str());
|
||||
return 0;
|
||||
}
|
||||
int sceImeDialogGetStatus(void)
|
||||
{
|
||||
if (g_ime_polls < 0)
|
||||
return SCE_IME_DIALOG_STATUS_NONE;
|
||||
return ++g_ime_polls > 3 ? SCE_IME_DIALOG_STATUS_FINISHED : SCE_IME_DIALOG_STATUS_RUNNING;
|
||||
}
|
||||
int sceImeDialogGetResult(SceImeDialogResult* r)
|
||||
{
|
||||
r->endstatus = g_ime_cancel ? SCE_IME_DIALOG_END_STATUS_USER_CANCELED : SCE_IME_DIALOG_END_STATUS_OK;
|
||||
return 0;
|
||||
}
|
||||
int sceImeDialogTerm(void)
|
||||
{
|
||||
g_ime_polls = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sceSysmoduleLoadModule(uint16_t id)
|
||||
{
|
||||
return id == SCE_SYSMODULE_IME_DIALOG ? 0 : (int)0x805a1000; // not found
|
||||
}
|
||||
|
||||
int sceSystemServiceHideSplashScreen(void)
|
||||
{
|
||||
fprintf(stderr, "[host] splash screen hidden\n");
|
||||
@@ -209,6 +281,34 @@ int sceSystemServiceLoadExec(const char* path, const char**)
|
||||
fprintf(stderr, "[host] sceSystemServiceLoadExec(%s)\n", path ? path : "(null)");
|
||||
return -1;
|
||||
}
|
||||
// Launching a title: logged; N64PS5_HOST_LAUNCH_FAILS=1 makes every call fail (no title ever comes up).
|
||||
static bool g_launched = false;
|
||||
static int HostLaunch(const char* what, const char* title_id, SceLncAppParam* param)
|
||||
{
|
||||
const char* fails = getenv("N64PS5_HOST_LAUNCH_FAILS");
|
||||
const int rc = fails && atoi(fails) ? int(0x80940002u) : 0;
|
||||
fprintf(stderr, "[host] %s(%s, user %d) -> 0x%08x\n", what, title_id ? title_id : "(null)",
|
||||
param ? int(param->user_id) : -1, unsigned(rc));
|
||||
g_launched = g_launched || rc == 0;
|
||||
return rc;
|
||||
}
|
||||
int sceLncUtilLaunchApp(const char* title_id, const char*[], SceLncAppParam* param)
|
||||
{
|
||||
return HostLaunch("sceLncUtilLaunchApp", title_id, param);
|
||||
}
|
||||
int sceSystemServiceLaunchApp(const char* title_id, const char*[], SceLncAppParam* param)
|
||||
{
|
||||
return HostLaunch("sceSystemServiceLaunchApp", title_id, param);
|
||||
}
|
||||
uint32_t sceLncUtilGetAppIdOfRunningBigApp(void)
|
||||
{
|
||||
return g_launched ? 0x60000100u : 0xffffffffu;
|
||||
}
|
||||
int sceLncUtilGetAppTitleId(uint32_t, char* title_id)
|
||||
{
|
||||
strcpy(title_id, "PPSA99064");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// N64PS5_HOST_VIDEO_BUSY=N: the first N opens find the screen held by another process (0x80290009)
|
||||
int sceVideoOutOpen(int, int, int, const void*)
|
||||
|
||||
@@ -86,8 +86,9 @@ int main()
|
||||
g_ready_message = msg;
|
||||
#endif
|
||||
|
||||
// an update the app staged before closing (ProsperoUpdateJob.h)
|
||||
// an update the app staged before closing, or the app to start again (ProsperoUpdateJob.h)
|
||||
updatejob::RunIfPending();
|
||||
updatejob::RelaunchIfPending();
|
||||
|
||||
// the helper: runs until the console is turned off, or returns at once when one already runs
|
||||
if (!jailbreak::ServeHelper(OnReady))
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Mupen64Plus PS5: the N64's RAM for the cheat search (n64_bridge.h).
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "n64_bridge.h"
|
||||
|
||||
#include "device/device.h"
|
||||
#include "main/main.h"
|
||||
#include "osal/preproc.h"
|
||||
|
||||
const uint32_t* n64ps5_ram(uint32_t* size)
|
||||
{
|
||||
*size = (uint32_t)g_dev.rdram.dram_size;
|
||||
return g_dev.rdram.dram;
|
||||
}
|
||||
|
||||
void n64ps5_ram_write(uint32_t address, uint32_t value, int bytes)
|
||||
{
|
||||
address &= 0x00ffffff;
|
||||
if (address + (uint32_t)bytes > g_dev.rdram.dram_size)
|
||||
return;
|
||||
uint8_t* dram = (uint8_t*)g_dev.rdram.dram;
|
||||
switch (bytes)
|
||||
{
|
||||
case 1: dram[address ^ S8] = (uint8_t)value; break;
|
||||
case 2: *(uint16_t*)(dram + (address ^ S16)) = (uint16_t)value; break;
|
||||
default: *(uint32_t*)(dram + address) = value; break;
|
||||
}
|
||||
invalidate_r4300_cached_code(&g_dev.r4300, 0x80000000u | address, (size_t)bytes);
|
||||
}
|
||||
@@ -52,24 +52,87 @@ void n64ps5_rsp_set_hle(bool on);
|
||||
|
||||
// ---- audio (plugins/audio_ps5.cpp) ---------------------------------------------------------------------
|
||||
void n64ps5_audio_set_mute(bool mute);
|
||||
// Settings "Volume": the game's sound scaled to 0..100 percent.
|
||||
void n64ps5_audio_set_volume(int percent);
|
||||
// Fast forward: the audio plugin drops what doesn't fit instead of slowing the game down.
|
||||
void n64ps5_audio_set_fast_forward(bool on);
|
||||
int n64ps5_audio_game_rate(void); // the game's current output rate in Hz (0 before it sets one)
|
||||
int n64ps5_audio_target(void); // the AudioOut ring level the rate control aims at (48 kHz frames)
|
||||
|
||||
// ---- input (plugins/input_ps5.cpp) ---------------------------------------------------------------------
|
||||
// The controller layout (Settings / pause menu "Controller layout"): for each DualSense control, the N64
|
||||
// button it presses. Several controls may press the same one (L2 and R2 are both Z by default). The left
|
||||
// stick is always the N64's analog stick.
|
||||
enum
|
||||
{
|
||||
N64PS5_PAD_CROSS,
|
||||
N64PS5_PAD_CIRCLE,
|
||||
N64PS5_PAD_SQUARE,
|
||||
N64PS5_PAD_TRIANGLE,
|
||||
N64PS5_PAD_L1,
|
||||
N64PS5_PAD_R1,
|
||||
N64PS5_PAD_L2,
|
||||
N64PS5_PAD_R2,
|
||||
N64PS5_PAD_L3,
|
||||
N64PS5_PAD_R3,
|
||||
N64PS5_PAD_OPTIONS,
|
||||
N64PS5_PAD_UP,
|
||||
N64PS5_PAD_DOWN,
|
||||
N64PS5_PAD_LEFT,
|
||||
N64PS5_PAD_RIGHT,
|
||||
N64PS5_PAD_RS_UP, // the right stick tilted (half way)
|
||||
N64PS5_PAD_RS_DOWN,
|
||||
N64PS5_PAD_RS_LEFT,
|
||||
N64PS5_PAD_RS_RIGHT,
|
||||
N64PS5_PAD_COUNT
|
||||
};
|
||||
enum
|
||||
{
|
||||
N64PS5_N64_NONE,
|
||||
N64PS5_N64_A,
|
||||
N64PS5_N64_B,
|
||||
N64PS5_N64_Z,
|
||||
N64PS5_N64_L,
|
||||
N64PS5_N64_R,
|
||||
N64PS5_N64_START,
|
||||
N64PS5_N64_C_UP,
|
||||
N64PS5_N64_C_DOWN,
|
||||
N64PS5_N64_C_LEFT,
|
||||
N64PS5_N64_C_RIGHT,
|
||||
N64PS5_N64_D_UP,
|
||||
N64PS5_N64_D_DOWN,
|
||||
N64PS5_N64_D_LEFT,
|
||||
N64PS5_N64_D_RIGHT,
|
||||
N64PS5_N64_COUNT
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int deadzone; // percent of the stick's travel ignored around the centre (0..30)
|
||||
int range; // N64 stick range at full tilt (the real stick reaches about 80)
|
||||
int pak; // 0 none, 1 Controller Pak (memory), 2 Rumble Pak
|
||||
int invert; // the analog stick's (left stick's) axes turned round: bit 0 X, bit 1 Y
|
||||
int invert_c; // the right stick turned round: bit 0 X (left/right), bit 1 Y (up/down)
|
||||
unsigned char layout[N64PS5_PAD_COUNT]; // N64PS5_N64_* for each N64PS5_PAD_*
|
||||
} n64ps5_input_options;
|
||||
|
||||
// The layout of the N64 pad by position: Cross A, Square B, L2 / R2 Z, L1 / R1 L / R, OPTIONS Start, D-pad
|
||||
// D-pad, right stick and Triangle / Circle the C buttons.
|
||||
void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT]);
|
||||
|
||||
void n64ps5_input_set_options(const n64ps5_input_options* opt);
|
||||
// While the frontend uses the pad itself (a hotkey held, the pause menu just closed), the game sees no
|
||||
// buttons.
|
||||
void n64ps5_input_block(bool block);
|
||||
|
||||
// ---- the N64's RAM (core/memory_ps5.c), for the cheat search ---------------------------------------------
|
||||
// The RDRAM as the core keeps it: 32-bit words in the host's byte order (byte n of the N64's memory is byte
|
||||
// n ^ 3 here). `size` gets its size, 4 or 8 MiB (the Expansion Pak).
|
||||
const uint32_t* n64ps5_ram(uint32_t* size);
|
||||
// Writes `value` (`bytes` 1, 2 or 4, at an address of that alignment) the way a cheat code does, so recompiled
|
||||
// code there is dropped too.
|
||||
void n64ps5_ram_write(uint32_t address, uint32_t value, int bytes);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Binary file not shown.
@@ -48,7 +48,7 @@ m64p_system_type g_system = SYSTEM_NTSC;
|
||||
std::atomic<bool> g_mute{false};
|
||||
std::atomic<bool> g_fast_forward{false};
|
||||
std::atomic<int> g_rate{0};
|
||||
int g_volume = 100;
|
||||
std::atomic<int> g_volume{100};
|
||||
|
||||
// resampler state
|
||||
double g_pos = 0.0; // position between g_prev and the next input sample
|
||||
@@ -211,7 +211,7 @@ void AiLenChanged()
|
||||
g_out.resize(size_t(max_out) * 2);
|
||||
int16_t* out = g_out.data();
|
||||
int n = 0;
|
||||
const int vol = g_volume;
|
||||
const int vol = g_volume.load(std::memory_order_relaxed);
|
||||
// Linear interpolation between g_prev (position 0) and each next sample (position 1).
|
||||
for (int i = 0; i < in_frames; i++)
|
||||
{
|
||||
@@ -262,7 +262,7 @@ void VolumeDown()
|
||||
|
||||
int VolumeGetLevel()
|
||||
{
|
||||
return g_mute.load() ? 0 : g_volume;
|
||||
return g_mute.load() ? 0 : g_volume.load();
|
||||
}
|
||||
|
||||
void VolumeSetLevel(int level)
|
||||
@@ -276,7 +276,7 @@ const char* VolumeGetString()
|
||||
if (g_mute.load())
|
||||
snprintf(text, sizeof(text), "Mute");
|
||||
else
|
||||
snprintf(text, sizeof(text), "%d%%", g_volume);
|
||||
snprintf(text, sizeof(text), "%d%%", g_volume.load());
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -309,6 +309,11 @@ extern "C" void n64ps5_audio_set_mute(bool mute)
|
||||
g_mute.store(mute);
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_audio_set_volume(int percent)
|
||||
{
|
||||
g_volume.store(percent < 0 ? 0 : percent > 100 ? 100 : percent);
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_audio_set_fast_forward(bool on)
|
||||
{
|
||||
g_fast_forward.store(on);
|
||||
|
||||
@@ -29,6 +29,7 @@ extern "C" {
|
||||
#include "perf.h"
|
||||
#include "static_dynlib.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -107,6 +108,88 @@ void LoadPipelineCache(Vulkan::Device& device)
|
||||
ok ? "" : " (not usable, starting a new one)");
|
||||
}
|
||||
|
||||
// The cache shipped with the app: assets/parallel-rdp-pipelines.bin in the app folder (n64/plugins/assets/ in the
|
||||
// source), a console's own cache file. Every PS5 has the same GPU and runs the same RADV, built into eboot.bin,
|
||||
// so its pipelines spare new players the first-time compiles (a short pause the first time a scene appears). It
|
||||
// is merged into the player's cache once per shipped file -- cache/parallel-rdp-pipelines.seed remembers which --
|
||||
// and saved with it. A console whose driver differs (another GPU) is told apart by the cache's UUID and keeps its
|
||||
// own cache only. A file next to eboot.bin, not inside it: 0.6.3 to 0.7.2 can't update to an eboot.bin over 32 MiB.
|
||||
std::string SeedMarkerPath()
|
||||
{
|
||||
return OrbisDir("cache") + "/parallel-rdp-pipelines.seed";
|
||||
}
|
||||
|
||||
std::string ShippedPipelineCachePath()
|
||||
{
|
||||
std::vector<std::string> dirs = {"/app0"}; // the running app, where the sandbox mount is still visible
|
||||
for (const std::string& d : OrbisAppFolders())
|
||||
dirs.push_back(d);
|
||||
for (const std::string& d : dirs)
|
||||
{
|
||||
const std::string path = d + "/assets/parallel-rdp-pipelines.bin";
|
||||
if (OrbisIsFile(path))
|
||||
return path;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// True when the shipped cache was merged now (the caller then saves the result).
|
||||
bool MergeShippedPipelineCache(Vulkan::Device& device)
|
||||
{
|
||||
const std::string path = ShippedPipelineCachePath();
|
||||
if (path.empty())
|
||||
{
|
||||
OrbisLog("[gpu] no pipeline cache shipped with the app (assets/parallel-rdp-pipelines.bin)");
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> data;
|
||||
if (FILE* f = fopen(path.c_str(), "rb"))
|
||||
{
|
||||
fseek(f, 0, SEEK_END);
|
||||
const long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (size > 24)
|
||||
{
|
||||
data.resize(size_t(size));
|
||||
if (fread(data.data(), 1, data.size(), f) != data.size())
|
||||
data.clear();
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
if (data.empty())
|
||||
return false;
|
||||
// which shipped cache this is: the hash Granite writes after the driver's 16-byte UUID, and the size
|
||||
uint64_t hash = 0;
|
||||
memcpy(&hash, data.data() + 16, sizeof(hash));
|
||||
char id[48];
|
||||
snprintf(id, sizeof(id), "%016llx-%zu", (unsigned long long)hash, data.size());
|
||||
char done[48] = "";
|
||||
if (FILE* f = fopen(SeedMarkerPath().c_str(), "r"))
|
||||
{
|
||||
if (!fgets(done, sizeof(done), f))
|
||||
done[0] = 0;
|
||||
fclose(f);
|
||||
}
|
||||
if (strcmp(id, done) == 0)
|
||||
{
|
||||
OrbisLog("[gpu] pipeline cache shipped with the app (%s): merged before", path.c_str());
|
||||
return false;
|
||||
}
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const bool ok = device.merge_pipeline_cache(data.data(), data.size());
|
||||
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||
OrbisLog("[gpu] pipeline cache shipped with the app (%s, %zu KiB): %s in %.0f ms", path.c_str(), data.size() >> 10,
|
||||
ok ? "merged" : "made by another driver or GPU, not used", ms);
|
||||
OrbisMkdirs(OrbisDir("cache"));
|
||||
if (FILE* f = fopen(SeedMarkerPath().c_str(), "w"))
|
||||
{
|
||||
fputs(id, f);
|
||||
fclose(f);
|
||||
chmod(SeedMarkerPath().c_str(), 0666);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SavePipelineCache()
|
||||
{
|
||||
if (!g_gpu.device)
|
||||
@@ -420,9 +503,13 @@ void UpdateScreen()
|
||||
opts.vi.dither_filter = filtered;
|
||||
opts.vi.gamma_dither = filtered;
|
||||
opts.vi.scale = true;
|
||||
opts.blend_previous_frame = true;
|
||||
opts.upscale_deinterlacing = false;
|
||||
// interlaced pictures (Indiana Jones, Resident Evil 2, hi-res modes): each field upscaled on its own with
|
||||
// the field's line offset ("bob"), as RetroArch does. Weave (upscale_deinterlacing off, which needs
|
||||
// blend_previous_frame) combed everything that moves (GitHub issue #2); paraLLEl-RDP keeps it for tests.
|
||||
opts.blend_previous_frame = false;
|
||||
opts.upscale_deinterlacing = true;
|
||||
opts.crop_overscan_pixels = l_Options.hide_overscan ? 8 : 0;
|
||||
const bool interlaced = (*l_Gfx.VI_STATUS_REG & RDP::VI_CONTROL_SERRATE_BIT) != 0;
|
||||
l_Rdp->scanout_async_buffer(l_Scanout, opts);
|
||||
|
||||
if (l_Scanout.width && l_Scanout.height && l_Scanout.fence)
|
||||
@@ -434,6 +521,12 @@ void UpdateScreen()
|
||||
g_gpu.device->map_host_buffer(*l_Scanout.buffer, Vulkan::MEMORY_ACCESS_READ_BIT));
|
||||
n64ps5_perf_add(N64PS5_PERF_VI, n64ps5_perf_now() - t0);
|
||||
const n64ps5_frame frame = {px, int(l_Scanout.width), int(l_Scanout.height), int(l_Scanout.width)};
|
||||
static int s_logged = -1; // the last picture mode logged: 0 progressive, 1 interlaced
|
||||
if (s_logged != int(interlaced))
|
||||
{
|
||||
s_logged = int(interlaced);
|
||||
OrbisLog("[gpu] picture: %s, %dx%d", interlaced ? "interlaced" : "progressive", frame.width, frame.height);
|
||||
}
|
||||
n64ps5_on_vi(&frame);
|
||||
g_gpu.device->unmap_host_buffer(*l_Scanout.buffer, Vulkan::MEMORY_ACCESS_READ_BIT);
|
||||
}
|
||||
@@ -555,8 +648,11 @@ extern "C" bool n64ps5_gpu_available(void)
|
||||
return false;
|
||||
}
|
||||
LoadPipelineCache(*device);
|
||||
const bool merged = MergeShippedPipelineCache(*device);
|
||||
g_gpu.context = std::move(context);
|
||||
g_gpu.device = std::move(device);
|
||||
if (merged)
|
||||
SavePipelineCache();
|
||||
OrbisLog("[gpu] Vulkan device ready for paraLLEl-RDP");
|
||||
return true;
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
// Mupen64Plus PS5: the input plugin, DualSense / DualShock through libScePad (ProsperoInput.h).
|
||||
//
|
||||
// The frontend polls the pads once per video interrupt (fe_emu.cpp); GetKeys turns that state into the
|
||||
// N64 controller's buttons and stick. Mapping (by position, as on the N64 pad):
|
||||
// N64 controller's buttons and stick. The left stick is the analog stick; every other control presses the
|
||||
// N64 button the controller layout gives it (n64ps5_input_options.layout, per game). The default layout, by
|
||||
// position as on the N64 pad:
|
||||
//
|
||||
// left stick -> analog stick D-pad -> D-pad
|
||||
// Cross -> A Square -> B
|
||||
// L2 or R2 -> Z L1 / R1 -> L / R
|
||||
// right stick -> C buttons Triangle -> C-up, Circle -> C-down
|
||||
// OPTIONS -> Start
|
||||
// OPTIONS -> Start L3, R3 -> nothing
|
||||
//
|
||||
// Rumble: the core sends Rumble Pak writes through ControllerCommand; they drive the controller's motors.
|
||||
//
|
||||
@@ -33,7 +35,8 @@ constexpr int kNumControllers = 4;
|
||||
|
||||
bool g_init = false;
|
||||
CONTROL* g_controls = nullptr;
|
||||
n64ps5_input_options g_opt = {12, 80, 1};
|
||||
n64ps5_input_options g_opt = {12, 80, 1, 0, 0, {}};
|
||||
bool g_opt_set = false;
|
||||
std::atomic<bool> g_block{false};
|
||||
|
||||
// One stick axis (0..255, centre 128) -> -range..range, with a dead zone and a linear ramp after it.
|
||||
@@ -95,6 +98,8 @@ int PakPlugin()
|
||||
|
||||
void InitiateControllers(CONTROL_INFO info)
|
||||
{
|
||||
if (!g_opt_set) // before the frontend's first options: the default layout
|
||||
n64ps5_input_default_layout(g_opt.layout);
|
||||
g_controls = info.Controls;
|
||||
if (!g_controls)
|
||||
return;
|
||||
@@ -122,25 +127,55 @@ void GetKeys(int control, BUTTONS* keys)
|
||||
return;
|
||||
const uint32_t b = p.raw_buttons; // the D-pad only (the stick is the analog stick here)
|
||||
|
||||
keys->R_DPAD = (b & SCE_PAD_BUTTON_RIGHT) != 0;
|
||||
keys->L_DPAD = (b & SCE_PAD_BUTTON_LEFT) != 0;
|
||||
keys->D_DPAD = (b & SCE_PAD_BUTTON_DOWN) != 0;
|
||||
keys->U_DPAD = (b & SCE_PAD_BUTTON_UP) != 0;
|
||||
keys->START_BUTTON = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
keys->Z_TRIG = (b & (SCE_PAD_BUTTON_L2 | SCE_PAD_BUTTON_R2)) != 0;
|
||||
keys->B_BUTTON = (b & SCE_PAD_BUTTON_SQUARE) != 0;
|
||||
keys->A_BUTTON = (b & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
keys->L_TRIG = (b & SCE_PAD_BUTTON_L1) != 0;
|
||||
keys->R_TRIG = (b & SCE_PAD_BUTTON_R1) != 0;
|
||||
// what each DualSense control is doing; the right stick counts at half tilt. Settings "Invert left stick"
|
||||
// and "Invert right stick" (GitHub issue #3) turn the sticks' axes round.
|
||||
bool on[N64PS5_PAD_COUNT] = {};
|
||||
on[N64PS5_PAD_CROSS] = (b & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
on[N64PS5_PAD_CIRCLE] = (b & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
on[N64PS5_PAD_SQUARE] = (b & SCE_PAD_BUTTON_SQUARE) != 0;
|
||||
on[N64PS5_PAD_TRIANGLE] = (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
on[N64PS5_PAD_L1] = (b & SCE_PAD_BUTTON_L1) != 0;
|
||||
on[N64PS5_PAD_R1] = (b & SCE_PAD_BUTTON_R1) != 0;
|
||||
on[N64PS5_PAD_L2] = (b & SCE_PAD_BUTTON_L2) != 0;
|
||||
on[N64PS5_PAD_R2] = (b & SCE_PAD_BUTTON_R2) != 0;
|
||||
on[N64PS5_PAD_L3] = (b & SCE_PAD_BUTTON_L3) != 0;
|
||||
on[N64PS5_PAD_R3] = (b & SCE_PAD_BUTTON_R3) != 0;
|
||||
on[N64PS5_PAD_OPTIONS] = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
on[N64PS5_PAD_UP] = (b & SCE_PAD_BUTTON_UP) != 0;
|
||||
on[N64PS5_PAD_DOWN] = (b & SCE_PAD_BUTTON_DOWN) != 0;
|
||||
on[N64PS5_PAD_LEFT] = (b & SCE_PAD_BUTTON_LEFT) != 0;
|
||||
on[N64PS5_PAD_RIGHT] = (b & SCE_PAD_BUTTON_RIGHT) != 0;
|
||||
const bool rs_right = p.rx > 192, rs_left = p.rx < 64, rs_down = p.ry > 192, rs_up = p.ry < 64;
|
||||
const bool rs_flip_x = (g_opt.invert_c & 1) != 0, rs_flip_y = (g_opt.invert_c & 2) != 0;
|
||||
on[N64PS5_PAD_RS_RIGHT] = rs_flip_x ? rs_left : rs_right;
|
||||
on[N64PS5_PAD_RS_LEFT] = rs_flip_x ? rs_right : rs_left;
|
||||
on[N64PS5_PAD_RS_DOWN] = rs_flip_y ? rs_up : rs_down;
|
||||
on[N64PS5_PAD_RS_UP] = rs_flip_y ? rs_down : rs_up;
|
||||
|
||||
// C buttons: the right stick (half tilt), plus Triangle / Circle
|
||||
keys->R_CBUTTON = p.rx > 192;
|
||||
keys->L_CBUTTON = p.rx < 64;
|
||||
keys->D_CBUTTON = p.ry > 192 || (b & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
keys->U_CBUTTON = p.ry < 64 || (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
// and what they press on the N64 pad
|
||||
bool n64[N64PS5_N64_COUNT] = {};
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
if (on[i] && g_opt.layout[i] < N64PS5_N64_COUNT)
|
||||
n64[g_opt.layout[i]] = true;
|
||||
keys->A_BUTTON = n64[N64PS5_N64_A];
|
||||
keys->B_BUTTON = n64[N64PS5_N64_B];
|
||||
keys->Z_TRIG = n64[N64PS5_N64_Z];
|
||||
keys->L_TRIG = n64[N64PS5_N64_L];
|
||||
keys->R_TRIG = n64[N64PS5_N64_R];
|
||||
keys->START_BUTTON = n64[N64PS5_N64_START];
|
||||
keys->U_CBUTTON = n64[N64PS5_N64_C_UP];
|
||||
keys->D_CBUTTON = n64[N64PS5_N64_C_DOWN];
|
||||
keys->L_CBUTTON = n64[N64PS5_N64_C_LEFT];
|
||||
keys->R_CBUTTON = n64[N64PS5_N64_C_RIGHT];
|
||||
keys->U_DPAD = n64[N64PS5_N64_D_UP];
|
||||
keys->D_DPAD = n64[N64PS5_N64_D_DOWN];
|
||||
keys->L_DPAD = n64[N64PS5_N64_D_LEFT];
|
||||
keys->R_DPAD = n64[N64PS5_N64_D_RIGHT];
|
||||
|
||||
keys->X_AXIS = Axis(p.lx, g_opt.deadzone, g_opt.range);
|
||||
keys->Y_AXIS = -Axis(p.ly, g_opt.deadzone, g_opt.range); // the N64's Y grows upwards
|
||||
const int x = Axis(p.lx, g_opt.deadzone, g_opt.range);
|
||||
const int y = -Axis(p.ly, g_opt.deadzone, g_opt.range); // the N64's Y grows upwards
|
||||
keys->X_AXIS = (g_opt.invert & 1) ? -x : x;
|
||||
keys->Y_AXIS = (g_opt.invert & 2) ? -y : y;
|
||||
}
|
||||
|
||||
// Raw joybus commands for this controller. Only the Rumble Pak's motor write is looked at:
|
||||
@@ -209,7 +244,37 @@ extern "C" const m64ps5_library m64ps5_input_lib = {"ps5-dualsense", kSymbols};
|
||||
extern "C" void n64ps5_input_set_options(const n64ps5_input_options* opt)
|
||||
{
|
||||
if (opt)
|
||||
{
|
||||
g_opt = *opt;
|
||||
g_opt_set = true;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT])
|
||||
{
|
||||
static const unsigned char kDefault[N64PS5_PAD_COUNT] = {
|
||||
N64PS5_N64_A, // Cross
|
||||
N64PS5_N64_C_DOWN, // Circle
|
||||
N64PS5_N64_B, // Square
|
||||
N64PS5_N64_C_UP, // Triangle
|
||||
N64PS5_N64_L, // L1
|
||||
N64PS5_N64_R, // R1
|
||||
N64PS5_N64_Z, // L2
|
||||
N64PS5_N64_Z, // R2
|
||||
N64PS5_N64_NONE, // L3
|
||||
N64PS5_N64_NONE, // R3
|
||||
N64PS5_N64_START, // OPTIONS
|
||||
N64PS5_N64_D_UP,
|
||||
N64PS5_N64_D_DOWN,
|
||||
N64PS5_N64_D_LEFT,
|
||||
N64PS5_N64_D_RIGHT,
|
||||
N64PS5_N64_C_UP, // right stick
|
||||
N64PS5_N64_C_DOWN,
|
||||
N64PS5_N64_C_LEFT,
|
||||
N64PS5_N64_C_RIGHT,
|
||||
};
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
layout[i] = kDefault[i];
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_input_block(bool block)
|
||||
|
||||
@@ -1351,8 +1351,13 @@ Vulkan::ImageHandle VideoInterface::scanout(VkImageLayout target_layout, const S
|
||||
divot_image = std::move(aa_image);
|
||||
|
||||
// Scale pass
|
||||
bool is_final_pass = !downscale_steps || scaling_factor <= 1;
|
||||
bool serrate = (regs.status & VI_CONTROL_SERRATE_BIT) != 0;
|
||||
// Mupen64Plus PS5: upscale_deinterlace (each field offset by its quarter line) used to run only after a
|
||||
// downscale pass, so without downscaling (downscale_steps 0) interlaced pictures jumped half a line every
|
||||
// frame (Indiana Jones' title screen, GitHub issue #2). It now runs whenever bob deinterlacing applies.
|
||||
bool deinterlace_pass = serrate && options.upscale_deinterlacing;
|
||||
bool downscale = downscale_steps && scaling_factor > 1;
|
||||
bool is_final_pass = !downscale && !deinterlace_pass;
|
||||
|
||||
auto scale_image = scale_stage(*cmd, divot_image.get(),
|
||||
regs, lines,
|
||||
@@ -1361,7 +1366,7 @@ Vulkan::ImageHandle VideoInterface::scanout(VkImageLayout target_layout, const S
|
||||
|
||||
auto src_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
|
||||
if (!is_final_pass && scale_image)
|
||||
if (downscale && scale_image)
|
||||
{
|
||||
cmd->image_barrier(*scale_image, src_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
layout_to_stage(src_layout), layout_to_access(src_layout),
|
||||
|
||||
+35
@@ -752,6 +752,41 @@ bool Device::init_pipeline_cache(const uint8_t *data, size_t size)
|
||||
return table->vkCreatePipelineCache(device, &info, nullptr, &pipeline_cache) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
// Mupen64Plus PS5: the same checks as init_pipeline_cache, then vkMergePipelineCaches into the current cache.
|
||||
bool Device::merge_pipeline_cache(const uint8_t *data, size_t size)
|
||||
{
|
||||
static const auto uuid_size = sizeof(gpu_props.pipelineCacheUUID);
|
||||
static const auto hash_size = sizeof(Util::Hash);
|
||||
|
||||
if (pipeline_cache == VK_NULL_HANDLE || !data || size <= uuid_size + hash_size)
|
||||
return false;
|
||||
if (memcmp(data, gpu_props.pipelineCacheUUID, uuid_size) != 0)
|
||||
{
|
||||
LOGI("Pipeline cache to merge is from another driver or GPU.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
Util::Hash reference_hash;
|
||||
memcpy(&reference_hash, data + uuid_size, sizeof(reference_hash));
|
||||
VkPipelineCacheCreateInfo info = { VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO };
|
||||
info.initialDataSize = size - uuid_size - hash_size;
|
||||
info.pInitialData = data + uuid_size + hash_size;
|
||||
Util::Hasher h;
|
||||
h.data(static_cast<const uint8_t *>(info.pInitialData), info.initialDataSize);
|
||||
if (h.get() != reference_hash)
|
||||
{
|
||||
LOGW("Pipeline cache to merge is corrupt.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
VkPipelineCache other = VK_NULL_HANDLE;
|
||||
if (table->vkCreatePipelineCache(device, &info, nullptr, &other) != VK_SUCCESS)
|
||||
return false;
|
||||
const bool ok = table->vkMergePipelineCaches(device, pipeline_cache, 1, &other) == VK_SUCCESS;
|
||||
table->vkDestroyPipelineCache(device, other, nullptr);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void Device::init_pipeline_cache()
|
||||
{
|
||||
#ifdef GRANITE_VULKAN_SYSTEM_HANDLES
|
||||
|
||||
@@ -242,6 +242,9 @@ public:
|
||||
size_t get_pipeline_cache_size();
|
||||
bool get_pipeline_cache_data(uint8_t *data, size_t size);
|
||||
bool init_pipeline_cache(const uint8_t *data, size_t size);
|
||||
// Mupen64Plus PS5: adds a saved cache (as get_pipeline_cache_data writes it) to the current one.
|
||||
// False if it was written by another driver or GPU, or is damaged.
|
||||
bool merge_pipeline_cache(const uint8_t *data, size_t size);
|
||||
|
||||
// Frame-pushing interface.
|
||||
void next_frame_context();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+13
-6
@@ -5,7 +5,8 @@
|
||||
# The BC7 encoder is a small one: every 4x4 block in mode 6 (two RGBA endpoints, 4-bit indices), endpoints
|
||||
# from the block's colour bounding box. Plenty for a smooth background.
|
||||
#
|
||||
# make_art.py <out-dir> [font.ttf bold.ttf] (needs Pillow and numpy)
|
||||
# make_art.py <out-dir> [font.ttf bold.ttf] draws its own (needs Pillow and numpy)
|
||||
# make_art.py <out-dir> --images <icon> <bg> from pictures (app/art/icon.png, app/art/background.png)
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import os
|
||||
@@ -13,7 +14,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
|
||||
|
||||
ACCENT = (255, 196, 64)
|
||||
BLUE = (64, 140, 255)
|
||||
@@ -145,11 +146,17 @@ def dds_bc7(width, height, data):
|
||||
|
||||
def main():
|
||||
out = sys.argv[1]
|
||||
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
|
||||
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
|
||||
os.makedirs(out, exist_ok=True)
|
||||
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = make_background(black, bold)
|
||||
if len(sys.argv) > 4 and sys.argv[2] == "--images":
|
||||
# pictures made elsewhere (app/art/): fitted to the icon's and the background's sizes
|
||||
ImageOps.fit(Image.open(sys.argv[3]).convert("RGB"), (512, 512), Image.LANCZOS).save(
|
||||
os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = ImageOps.fit(Image.open(sys.argv[4]).convert("RGB"), (3840, 2160), Image.LANCZOS)
|
||||
else:
|
||||
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
|
||||
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
|
||||
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = make_background(black, bold)
|
||||
bg.save(os.path.join(out, "background-source.png"), optimize=True)
|
||||
dds = dds_bc7(bg.width, bg.height, bc7_encode(bg))
|
||||
for name in ("pic0.dds", "pic1.dds"):
|
||||
|
||||
+81
-2
@@ -7,7 +7,13 @@
|
||||
# No-Intro style ones: dump tags ([!] [b1] [t1] ...) dropped, country codes spelled out ((U) -> (USA)),
|
||||
# (V1.1) -> (Rev 1). It is not a full No-Intro table; files already named the No-Intro way are used as is.
|
||||
#
|
||||
# make_n64db.py mupen64plus.ini out.tsv
|
||||
# GoodN64 names have no language tags and spell some titles differently, so a converted name often isn't
|
||||
# libretro-thumbnails' file name ("Diddy Kong Racing (USA) (Rev 1)" is "... (USA) (En,Fr) (Rev 1)" there,
|
||||
# and every such cover download was a 404). With a list of those file names (tools/libretro-n64-boxarts.txt)
|
||||
# each name is matched against it, ignoring language tags, case and punctuation, and takes its spelling.
|
||||
#
|
||||
# make_n64db.py mupen64plus.ini out.tsv [boxarts.txt]
|
||||
# make_n64db.py --fetch-boxarts boxarts.txt refresh the list (GitHub's file list of the repository)
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import re
|
||||
@@ -36,6 +42,68 @@ def convert(good):
|
||||
return re.sub(r"\s+", " ", name).strip()
|
||||
|
||||
|
||||
# ---- libretro-thumbnails' names ------------------------------------------------------------------------------
|
||||
BOXARTS_API = "https://api.github.com/repos/libretro-thumbnails/Nintendo_-_Nintendo_64/git/trees/master?recursive=1"
|
||||
# characters a thumbnail's file name has as "_" (fe_covers.cpp ThumbnailName)
|
||||
UNSAFE = '&*/:`<>?\\|"'
|
||||
LANG = re.compile(r"\s*\((?:[A-Z][a-z](?:-[A-Z][a-z]+)?)(?:,[A-Z][a-z](?:-[A-Z][a-z]+)?)*\)") # (En,Fr) (Fr,De,Nl)
|
||||
# GoodN64's own language tags: (Ita-Spa) (Eng-Spa-Fre-Ger)
|
||||
GOOD_LANG = re.compile(r"\s*\((?:Eng|Fre|Ger|Spa|Ita|Dut|Swe|Jap|Por|Dan|Nor|Fin)(?:-(?:Eng|Fre|Ger|Spa|Ita|Dut|Swe|Jap|Por|Dan|Nor|Fin))*\)")
|
||||
|
||||
|
||||
def fetch_boxarts(dst):
|
||||
import datetime
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(BOXARTS_API, headers={"User-Agent": "mupen64plus-ps5 make_n64db.py"})
|
||||
tree = json.load(urllib.request.urlopen(req, timeout=60))
|
||||
if tree.get("truncated"):
|
||||
sys.exit("GitHub's file list came back truncated")
|
||||
names = sorted(e["path"][len("Named_Boxarts/"):-4] for e in tree["tree"]
|
||||
if e["path"].startswith("Named_Boxarts/") and e["path"].endswith(".png"))
|
||||
with open(dst, "w", encoding="utf-8", newline="\n") as out:
|
||||
out.write("# Named_Boxarts of github.com/libretro-thumbnails/Nintendo_-_Nintendo_64 (master, %s), for "
|
||||
"tools/make_n64db.py\n" % datetime.date.today().isoformat())
|
||||
out.write("".join(n + "\n" for n in names))
|
||||
print("%s: %d box art names" % (dst, len(names)))
|
||||
|
||||
|
||||
def thumb(name):
|
||||
return "".join("_" if c in UNSAFE else c for c in name)
|
||||
|
||||
|
||||
def loose(name):
|
||||
# the same game whatever the language tags, case, punctuation or a (Z64) dump tag
|
||||
n = LANG.sub("", GOOD_LANG.sub("", thumb(name)))
|
||||
n = re.sub(r"\s*\((?:Z64|V64)\)", "", n)
|
||||
n = n.lower().replace("_", " ")
|
||||
n = re.sub(r"[^a-z0-9()]+", " ", n)
|
||||
return re.sub(r"\s+", " ", n).strip()
|
||||
|
||||
|
||||
class Boxarts:
|
||||
def __init__(self, path):
|
||||
names = [l.rstrip("\n") for l in open(path, encoding="utf-8") if l.strip() and not l.startswith("#")]
|
||||
self.exact = set(names)
|
||||
self.by_key = {}
|
||||
for n in sorted(names, key=len): # the plainest variant first
|
||||
self.by_key.setdefault(loose(n), n)
|
||||
|
||||
def match(self, name):
|
||||
if thumb(name) in self.exact:
|
||||
return name
|
||||
found = self.by_key.get(loose(name))
|
||||
if not found:
|
||||
return name
|
||||
# keep our readable characters where the file name has "_" ("Command & Conquer")
|
||||
ours = [c for c in name if c in UNSAFE]
|
||||
if found.count("_") == len(ours):
|
||||
it = iter(ours)
|
||||
found = "".join(next(it) if c == "_" else c for c in found)
|
||||
return found
|
||||
|
||||
|
||||
def rank(good):
|
||||
# verified dumps first, then plain names, then bad/overdumps/hacks/trainers/translations
|
||||
if "[!]" in good:
|
||||
@@ -46,7 +114,11 @@ def rank(good):
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "--fetch-boxarts":
|
||||
fetch_boxarts(sys.argv[2])
|
||||
return
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
boxarts = Boxarts(sys.argv[3]) if len(sys.argv) > 3 else None
|
||||
best = {}
|
||||
good = crc = None
|
||||
with open(src, encoding="latin-1") as f:
|
||||
@@ -63,11 +135,18 @@ def main():
|
||||
if crc not in best or r < best[crc][0]:
|
||||
best[crc] = (r, convert(good))
|
||||
good = crc = None
|
||||
renamed = 0
|
||||
if boxarts:
|
||||
for crc, (r, name) in best.items():
|
||||
m = boxarts.match(name)
|
||||
if m != name:
|
||||
best[crc] = (r, m)
|
||||
renamed += 1
|
||||
with open(dst, "w", encoding="utf-8", newline="\n") as out:
|
||||
out.write("# CRC1\tname (generated by tools/make_n64db.py from mupen64plus.ini)\n")
|
||||
for crc in sorted(best):
|
||||
out.write("%s\t%s\n" % (crc, best[crc][1]))
|
||||
print("%s: %d games" % (dst, len(best)))
|
||||
print("%s: %d games, %d named as in libretro-thumbnails" % (dst, len(best), renamed))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+16
-11
@@ -3,10 +3,11 @@
|
||||
# 1. builds the app from scratch (build-native.bat Clean): build-native\PPSA99064\ and Mupen64PlusPS5.zip. Always
|
||||
# clean: 0.6.1 was published from an incremental build that didn't start on the console;
|
||||
# 2. checks that everything is committed and pushed, that release-notes\<contentVersion>.md exists and that
|
||||
# the tag isn't taken;
|
||||
# 3. creates the GitHub release: tag = contentVersion (NN.NNN.NNN, from VERSION in ps5\Makefile), asset
|
||||
# Mupen64PlusPS5.zip, notes from release-notes\<contentVersion>.md (the app shows them when it offers the
|
||||
# update);
|
||||
# the version isn't released yet (under either tag style);
|
||||
# 3. creates the GitHub release: tag = the version as people write it (0.7.4, VERSION in ps5\Makefile; up to
|
||||
# 0.7.3 the tag was the contentVersion, 00.007.003), asset Mupen64PlusPS5.zip, notes from
|
||||
# release-notes\<contentVersion>.md (the app shows them when it offers the update). Every app since 0.6.0
|
||||
# reads both tag styles as numbers (fe_update.cpp ParseVersion), so they compare correctly;
|
||||
# 4. reads the release back and compares GitHub's SHA-256 of the asset with the local zip (the app checks
|
||||
# the download against that digest).
|
||||
#
|
||||
@@ -42,23 +43,27 @@ try {
|
||||
$ahead = git rev-list --count origin/main..HEAD
|
||||
if ([int]$ahead -gt 0) { throw "push your commits first (git push): $ahead not on GitHub" }
|
||||
|
||||
$tag = $version
|
||||
# "release not found" on stderr is the expected answer: PowerShell 5.1 would make it fatal under 'Stop'
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $gh release view $cv --repo $repo 2>&1 | Out-Null
|
||||
$exists = $LASTEXITCODE -eq 0
|
||||
$exists = $false
|
||||
foreach ($t in @($tag, $cv)) {
|
||||
& $gh release view $t --repo $repo 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $exists = $true }
|
||||
}
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($exists) { throw "release $cv already exists: raise VERSION in ps5\Makefile" }
|
||||
if ($exists) { throw "release $version already exists: raise VERSION in ps5\Makefile" }
|
||||
|
||||
Write-Host "Publishing Mupen64Plus PS5 $version (tag $cv) to $repo"
|
||||
& $gh release create $cv $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
|
||||
Write-Host "Publishing Mupen64Plus PS5 $version (tag $tag) to $repo"
|
||||
& $gh release create $tag $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
|
||||
if ($LASTEXITCODE -ne 0) { throw 'gh release create failed' }
|
||||
|
||||
$local = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
# parsed here: PowerShell 5.1 strips the quotes a --jq filter would need
|
||||
$release = (& $gh api "repos/$repo/releases/tags/$cv") -join "`n" | ConvertFrom-Json
|
||||
$release = (& $gh api "repos/$repo/releases/tags/$tag") -join "`n" | ConvertFrom-Json
|
||||
$remote = ($release.assets | Where-Object { $_.name -eq $asset }).digest
|
||||
if ($remote -ne "sha256:$local") { throw "GitHub's digest '$remote' doesn't match the local zip (sha256:$local)" }
|
||||
Write-Host "Done: https://github.com/$repo/releases/tag/$cv (sha256 $local)"
|
||||
Write-Host "Done: https://github.com/$repo/releases/tag/$tag (sha256 $local)"
|
||||
Write-Host 'Consoles on 0.6.3 or newer will offer it the next time Mupen64Plus PS5 starts.'
|
||||
}
|
||||
finally { Pop-Location }
|
||||
@@ -0,0 +1,3 @@
|
||||
## Fixes
|
||||
- GPU renderer: interlaced pictures (Indiana Jones and the Infernal Machine, high-resolution modes) no longer show comb-like lines on anything moving, and the picture no longer jitters or flickers at the top and bottom.
|
||||
- The line under the title on the game list was out of date and ran behind the covers.
|
||||
@@ -0,0 +1,7 @@
|
||||
## New
|
||||
- Game list views: besides the 3D shelf, a grid of covers or a list of titles with the cover beside it. Press the touchpad on the game list to switch (also Settings, Game list view).
|
||||
- Settings: Invert left stick and Invert right stick (X, Y, or both).
|
||||
|
||||
## Fixes
|
||||
- Covers for over 160 more games download now (Diddy Kong Racing, F-Zero X, the European versions of many games...): the app asked for them under the wrong name.
|
||||
- After you add games, the app reopens by itself once it has closed to fetch their covers. Before, it only closed.
|
||||
@@ -0,0 +1,8 @@
|
||||
## New
|
||||
- Continue where you left off: leaving a game from the pause menu saves where you were. The next time you start that game, the app asks whether to continue there (Cross), start over (Square) or go back (Circle). Settings, Continue where you left off, turns it off.
|
||||
- Settings: CPU overclock (Off, 2x, 4x). Games that slowed down on the N64, like GoldenEye, Perfect Dark and Conker, run smoother. A few games misbehave with it, so it is off by default.
|
||||
- ROM hacks bigger than an N64 cartridge (64 MB) load now, up to 252 MB, for example ECW Born to be Wired.
|
||||
- The app comes with the GPU renderer's shaders already compiled, so most scenes no longer pause briefly the first time they appear.
|
||||
|
||||
## Fixes
|
||||
- When a game can't start because its file is empty or too big, the message now says so, with the file's size.
|
||||
@@ -0,0 +1,7 @@
|
||||
## New
|
||||
- A new icon and background on the home screen.
|
||||
- A new look for the pause menu and Settings: one panel with switches, a volume slider and icons. In the pause menu, the touchpad resumes, Square resets the game (it asks first) and Triangle goes back to the game list; Save State, Load State and Quit are at the top and bottom of the list.
|
||||
- Quick save and load: in a game, press R3 (the right stick) to save in the current state slot, and L3 (the left stick) to load it. The pause menu now opens with the touchpad only.
|
||||
- Cheats: a Cheats page in the pause menu with the GameShark codes of mupen64plus's cheat list (about 560 game versions: infinite lives, unlock everything and more). Switch each one on or off, or pick one of its choices; the game remembers them.
|
||||
- Controller layout (Settings): choose what each DualSense button does on the N64 controller. Cross, Circle, Square, Triangle, L1, R1, L2 and R2 (separately), OPTIONS, the D-pad and the right stick can each press any N64 button, or nothing. From the game list it changes every game's layout; from the pause menu, only the game you are playing, which keeps it.
|
||||
- Settings: Volume (0 to 100%), also from the pause menu while you play.
|
||||
@@ -0,0 +1,6 @@
|
||||
## New
|
||||
- Cheat search: find where a game keeps a number (lives, health, money) and make your own cheat from it. In the pause menu, Cheats → Search for a Cheat: search for the value the game shows, or without a value for things like a health bar. Then play, pause, and pick what happened to it (changed, went up, went down by the value...) until only a few addresses are left. In Results, keep one at the value you want as a cheat, or set it just once. Works on 8, 16 and 32-bit values; Undo takes back a step.
|
||||
- Type in your own GameShark codes with the PS5's on-screen keyboard (Cheats → Type In Codes).
|
||||
- Your own cheats come first in the Cheats list and are kept with the game. Square renames one, changes its codes or deletes it; on a cheat of the list, Square makes your own copy to fix. They are saved in /data/mupen64plus/cheats/<game>.custom.txt, where you can also add codes from a PC.
|
||||
- Cheats is in the pause menu for every game now, also the ones the cheat list has nothing for.
|
||||
- The touchpad goes back to the game from any page of the pause menu (Cheats, the cheat search, Controller Layout).
|
||||
Reference in new issue
Block a user