mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-28 08:02:08 +01:00
Compare commits
4
Commits
00.007.002
...
00.007.003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2147c5dbe | ||
|
|
7f4949f830 | ||
|
|
ab6a937908 | ||
|
|
6c09976c62 |
No files matched your search
@@ -7,6 +7,7 @@
|
||||
*.vcxproj* text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.bin binary
|
||||
*.dds binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
|
||||
@@ -81,7 +81,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*);
|
||||
|
||||
+27
-12
@@ -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.2
|
||||
VERSION ?= 0.7.3
|
||||
# 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
|
||||
@@ -196,16 +199,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
|
||||
@@ -227,7 +230,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 $@ $<
|
||||
|
||||
@@ -278,9 +281,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)"' \
|
||||
@@ -447,10 +454,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.
|
||||
@@ -492,15 +507,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
|
||||
@@ -522,7 +537,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 $@ $<
|
||||
|
||||
|
||||
+17
-3
@@ -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.
|
||||
@@ -164,12 +176,14 @@ 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 |
|
||||
| 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` |
|
||||
| Sound, Download covers | |
|
||||
@@ -179,7 +193,7 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
|
||||
- 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
|
||||
- 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
|
||||
@@ -287,7 +301,7 @@ N64PS5_PS5_ROOT=/tmp/n64 N64PS5_HOST_DUMP=150 N64PS5_HOST_MAX_FLIPS=200 build/ho
|
||||
The run writes flip 150 to a `.ppm` file. `N64PS5_HOST_PAD="frame:buttons;..."` scripts the pad (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).
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99064",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
|
||||
"contentVersion": "00.007.002",
|
||||
"contentVersion": "00.007.003",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,10 +56,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;
|
||||
@@ -177,11 +177,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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
+126
-16
@@ -73,7 +73,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 +221,13 @@ struct State
|
||||
uint32_t prev_buttons = 0;
|
||||
bool block_until_release = false;
|
||||
int pending_save_slot = -1;
|
||||
// "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 +290,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 +359,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 +397,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 +473,22 @@ 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;
|
||||
}
|
||||
|
||||
// "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 +508,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 +550,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 +599,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,12 +623,13 @@ 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)
|
||||
if (menu && !g.stopping && !g.leaving)
|
||||
{
|
||||
OpenPauseMenu();
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
@@ -556,7 +641,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 +655,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,7 +735,7 @@ 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)
|
||||
{
|
||||
std::string dummy;
|
||||
if (!error)
|
||||
@@ -687,10 +772,7 @@ 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.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,9 +790,15 @@ 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]);
|
||||
@@ -762,6 +850,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,6 +874,9 @@ 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;
|
||||
N64_STAGE(Emu, "idle");
|
||||
if (r != M64ERR_SUCCESS && g.vi == 0)
|
||||
@@ -792,6 +887,21 @@ 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;
|
||||
|
||||
@@ -16,8 +16,12 @@ 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();
|
||||
std::string GameName(); // the title shown on the shelf (No-Intro name, ROM header name or file name)
|
||||
|
||||
@@ -265,12 +265,14 @@ enum SettingRow
|
||||
S_GPU_SYNC,
|
||||
#endif
|
||||
S_CPU,
|
||||
S_OVERCLOCK,
|
||||
S_RSP_AUDIO,
|
||||
S_THREADS,
|
||||
S_PAK,
|
||||
S_DEADZONE,
|
||||
S_INVERT,
|
||||
S_INVERT_C,
|
||||
S_RESUME,
|
||||
S_VIEW,
|
||||
S_COVERS,
|
||||
S_UPDATES,
|
||||
@@ -294,12 +296,14 @@ std::string SettingLabel(int s)
|
||||
case S_GPU_SYNC: return "GPU sync *";
|
||||
#endif
|
||||
case S_CPU: return "CPU core *";
|
||||
case S_OVERCLOCK: return "CPU overclock *";
|
||||
case S_RSP_AUDIO: return "Audio processing *";
|
||||
case S_THREADS: return "Render threads *";
|
||||
case S_PAK: return "Controller pak *";
|
||||
case S_DEADZONE: return "Stick dead zone";
|
||||
case S_INVERT: return "Invert left stick";
|
||||
case S_INVERT_C: return "Invert right stick";
|
||||
case S_RESUME: return "Continue where you left off";
|
||||
case S_VIEW: return "Game list view";
|
||||
case S_COVERS: return "Download covers";
|
||||
case S_UPDATES: return "Check for updates";
|
||||
@@ -339,6 +343,11 @@ std::string SettingValue(int s)
|
||||
case S_GPU_SYNC: return !c.gpu ? "GPU only" : c.gpu_sync ? "Accurate" : "Fast";
|
||||
#endif
|
||||
case S_CPU: return c.dynarec ? "Dynarec" : "Interpreter";
|
||||
case S_OVERCLOCK:
|
||||
{
|
||||
static const char* const kSpeeds[] = {"Off", "2x", "4x"};
|
||||
return kSpeeds[std::clamp(c.overclock, 0, 2)];
|
||||
}
|
||||
case S_RSP_AUDIO: return c.hle_audio ? "Fast (HLE)" : "Accurate (LLE)";
|
||||
case S_THREADS: snprintf(buf, sizeof(buf), "%d", c.render_threads); return buf;
|
||||
case S_PAK: return PakName(c.pak);
|
||||
@@ -349,6 +358,7 @@ std::string SettingValue(int s)
|
||||
static const char* const kInvert[] = {"Off", "X (left/right)", "Y (up/down)", "X and Y"};
|
||||
return kInvert[(s == S_INVERT ? c.invert_stick : c.invert_c_stick) & 3];
|
||||
}
|
||||
case S_RESUME: return c.resume ? "Ask" : "Off";
|
||||
case S_VIEW:
|
||||
{
|
||||
static const char* const kViews[] = {"Shelf (3D)", "Grid", "List"};
|
||||
@@ -396,6 +406,7 @@ void ChangeSetting(int s, int dir)
|
||||
break;
|
||||
#endif
|
||||
case S_CPU: c.dynarec = !c.dynarec; break;
|
||||
case S_OVERCLOCK: c.overclock = (std::clamp(c.overclock, 0, 2) + dir + 3) % 3; break;
|
||||
case S_RSP_AUDIO: c.hle_audio = !c.hle_audio; break;
|
||||
case S_THREADS:
|
||||
{
|
||||
@@ -412,6 +423,7 @@ void ChangeSetting(int s, int dir)
|
||||
}
|
||||
case S_INVERT: c.invert_stick = (c.invert_stick + dir + 4) & 3; break;
|
||||
case S_INVERT_C: c.invert_c_stick = (c.invert_c_stick + dir + 4) & 3; break;
|
||||
case S_RESUME: c.resume = !c.resume; break;
|
||||
case S_VIEW: c.game_view = (std::clamp(c.game_view, 0, 2) + dir + 3) % 3; break;
|
||||
case S_COVERS: c.covers_download = !c.covers_download; break;
|
||||
case S_UPDATES: c.updates = !c.updates; break;
|
||||
|
||||
@@ -61,6 +61,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")
|
||||
@@ -86,6 +88,8 @@ void Settings::Load()
|
||||
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")
|
||||
@@ -117,6 +121,7 @@ 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);
|
||||
@@ -128,6 +133,7 @@ void Settings::Save() const
|
||||
fprintf(f, "invert_c_stick=%d\n", invert_c_stick);
|
||||
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());
|
||||
|
||||
@@ -17,6 +17,7 @@ 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)
|
||||
@@ -30,6 +31,7 @@ struct Settings
|
||||
int invert_c_stick = 0; // the right stick (C buttons), the same way
|
||||
// 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)
|
||||
|
||||
@@ -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"
|
||||
@@ -608,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
|
||||
{
|
||||
@@ -706,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;
|
||||
@@ -831,9 +869,20 @@ std::string Shelf()
|
||||
}
|
||||
if ((down & SCE_PAD_BUTTON_CROSS) && n > 0)
|
||||
{
|
||||
cfg.last_rom = st.games[size_t(sel)].path;
|
||||
cfg.Save();
|
||||
return 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)
|
||||
{
|
||||
|
||||
@@ -17,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
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
@@ -565,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;
|
||||
}
|
||||
+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();
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user