mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-26 14:03:39 +01:00
Compare commits
33
Commits
00.006.002
...
0.7.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89786fd668 | ||
|
|
f97346904d | ||
|
|
0ec5920b37 | ||
|
|
0ca7ea5721 | ||
|
|
240baec05e | ||
|
|
7b2a21208e | ||
|
|
9e3b6c1596 | ||
|
|
bca88bf154 | ||
|
|
a2147c5dbe | ||
|
|
7f4949f830 | ||
|
|
ab6a937908 | ||
|
|
6c09976c62 | ||
|
|
fbda6fd21e | ||
|
|
96a850b3b4 | ||
|
|
5b98886d51 | ||
|
|
5715de56a5 | ||
|
|
cfd4bbfe95 | ||
|
|
dc42d378c5 | ||
|
|
caafe1f412 | ||
|
|
bd40c5092a | ||
|
|
f00c397657 | ||
|
|
ee516620b0 | ||
|
|
5d7ccaae19 | ||
|
|
7257726071 | ||
|
|
35dac4d527 | ||
|
|
a373c12b48 | ||
|
|
3c829c43c0 | ||
|
|
d9a56f158f | ||
|
|
df4f15f1c2 | ||
|
|
0445292682 | ||
|
|
8ebab24e15 | ||
|
|
7add4b5f3c | ||
|
|
b75d1130e3 |
No files matched your search
@@ -7,6 +7,7 @@
|
||||
*.vcxproj* text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.bin binary
|
||||
*.dds binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Build output
|
||||
build-native/
|
||||
build-native-vulkan/
|
||||
build-native-cpu/
|
||||
ps5/build/
|
||||
ps5/proto/native/build/
|
||||
*.o
|
||||
@@ -23,3 +25,6 @@ github-token.txt
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local test builds
|
||||
test-updater/
|
||||
@@ -3,8 +3,9 @@
|
||||
# Mupen64Plus PS5
|
||||
|
||||
**mupen64plus-core 2.6.0, the Nintendo 64 emulator, as a native PS5 home-screen app** for jailbroken consoles:
|
||||
a game shelf with box art, DualSense support with rumble, save states, and the x86-64 dynarec, so games like
|
||||
GoldenEye 007 and Donkey Kong 64 run at full speed.
|
||||
a game shelf with box art, DualSense support with rumble, save states, the x86-64 dynarec, and a GPU renderer
|
||||
(paraLLEl-RDP on Vulkan) with up to 8x internal resolution, so games like GoldenEye 007, Perfect Dark and
|
||||
Donkey Kong 64 run at full speed in HD.
|
||||
|
||||

|
||||
|
||||
@@ -15,7 +16,7 @@ Title ID `PPSA99064`. No games are included: use ROMs you dumped yourself.
|
||||
Requirements: a jailbroken PS5 with **kstuff** (or kstuff-lite), **ShadowMountPlus**, and an **ELF loader**
|
||||
running (elfldr, etaHEN or PS5 Payload Manager's: the usual setup).
|
||||
|
||||
1. Download `PPSA99064.zip` from the [latest release](../../releases/latest).
|
||||
1. Download `Mupen64PlusPS5.zip` from the [latest release](../../releases/latest).
|
||||
2. Copy the `PPSA99064` folder to `/data/homebrew/` on the console (FTP...), or to `homebrew/` on an exFAT
|
||||
USB drive. ShadowMountPlus adds **Mupen64Plus PS5** to the home screen.
|
||||
3. Put your ROMs (`.z64 .n64 .v64 .rom`, zipped or not) in `/data/mupen64plus/roms`, or in `mupen64plus/roms`
|
||||
@@ -27,9 +28,14 @@ which lets it out of its sandbox (`/data`, USB drives, executable memory for the
|
||||
## Updates
|
||||
|
||||
The app checks this repository's latest release each time it starts. When a newer one exists it downloads
|
||||
`PPSA99064.zip`, checks it against the SHA-256 GitHub publishes for it, and asks:
|
||||
**✕ Update now / ○ Later**. On ✕ it writes the new files over its own folder and restarts. Saves, states,
|
||||
covers and settings in `/data/mupen64plus` are kept. **Settings → Check for updates** turns the question off.
|
||||
`Mupen64PlusPS5.zip`, checks it against the SHA-256 GitHub publishes for it, and asks:
|
||||
**✕ Update now / ○ Later**. On ✕ it unpacks the update and closes; a background helper installs it, and a
|
||||
notification says when to open the app again. Saves, states, covers and settings in `/data/mupen64plus` are
|
||||
kept. **Settings → Check for updates** turns the question off.
|
||||
|
||||
**On 0.6.0, 0.6.1 or 0.6.2?** Their updater is broken (the updated app wouldn't start), so they don't see
|
||||
newer releases. Install 0.6.3 or newer by hand once: delete `/data/homebrew/PPSA99064`, then copy the
|
||||
`PPSA99064` folder from `Mupen64PlusPS5.zip`. Updates after that work from inside the app.
|
||||
|
||||
## Compatibility
|
||||
|
||||
@@ -40,6 +46,12 @@ covers and settings in `/data/mupen64plus` are kept. **Settings → Check for up
|
||||
|
||||
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.
|
||||
@@ -47,25 +59,30 @@ For a crash or a bug, please [open an issue](../../issues) and attach `/data/mup
|
||||
## Building and releasing (Windows + WSL Ubuntu 24.04)
|
||||
|
||||
```
|
||||
build-native.bat build-native\PPSA99064\ + PPSA99064.zip (+ .debug.elf)
|
||||
build-native.bat build-native\PPSA99064\ + Mupen64PlusPS5.zip (+ .debug.elf)
|
||||
build-native.bat Ffpfsc also a compressed .ffpfsc image
|
||||
build-native.bat Clean from scratch (nothing reused from earlier builds)
|
||||
build-native.bat Cpu without the GPU renderer, in build-native-cpu\
|
||||
release.bat clean build, then publish a GitHub release
|
||||
```
|
||||
|
||||
The GPU renderer needs RADV built first (mihawk-99/PS5_Vulkan, in WSL `/root/gpu/PS5_Vulkan`): see
|
||||
[ps5/README.md](ps5/README.md#building).
|
||||
|
||||
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 `PPSA99064.zip`, and checks
|
||||
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.
|
||||
|
||||
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,11 +5,14 @@ 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`) |
|
||||
| mupen64plus-rsp-hle 2.6.0 (its core, without `plugin.c` and the dlopen osal) | `ps5/third_party/rsp-hle/` | GPL-2.0-or-later (`LICENSES`) |
|
||||
| angrylion-rdp-plus | `ps5/third_party/angrylion-rdp-plus/` | MAME license (`MAME License.txt`) |
|
||||
| paraLLEl-RDP (standalone tree, with parts of Granite, volk and the Vulkan headers; Vulkan builds only) | `ps5/third_party/parallel-rdp/` | MIT (`LICENSE`); Vulkan headers Apache-2.0 / MIT |
|
||||
| RADV, Mesa's Vulkan driver, as built by mihawk-99/PS5_Vulkan (linked, not included here; Vulkan builds only) | outside the repository (`PS5_VULKAN`) | MIT (Mesa) / GPL-3.0-or-later (PS5_Vulkan, its SDK fork) |
|
||||
| zlib | `ps5/third_party/zlib/` | zlib license (`LICENSE`) |
|
||||
| stb_image, stb_image_resize2, stb_truetype | `ps5/frontend/third_party/` | MIT / public domain (in each file) |
|
||||
| Roboto Regular | `ps5/frontend/assets/fonts/` | Apache-2.0 (`Roboto-Regular-copyright`) |
|
||||
|
||||
@@ -3,6 +3,7 @@ rem Builds the PS5 native application (Mupen64Plus PS5, PPSA99064) through WSL (
|
||||
rem build-native.bat app folder + zip in build-native\
|
||||
rem build-native.bat Ffpfsc also a compressed .ffpfsc image
|
||||
rem build-native.bat Clean from scratch (deletes ps5\build first); release.bat always does this
|
||||
rem build-native.bat Cpu without the GPU renderer (angrylion only), in build-native-cpu\ (ps5\Makefile, VULKAN)
|
||||
rem See ps5\README.md (Building) for the WSL setup.
|
||||
setlocal
|
||||
set "N64_DIR=%~dp0"
|
||||
|
||||
@@ -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*);
|
||||
|
||||
+177
-50
@@ -4,7 +4,7 @@
|
||||
# included) and stays running as the helper that lets the app reach /data
|
||||
# make app -> build/app/$(TITLE_ID)/ the app folder on its own (eboot.bin, sce_module/, sce_sys/)
|
||||
# make host -> build/host/ Linux builds of the same code (PS5 calls faked by host/sce_host.cpp)
|
||||
# make native -> ../build-native/: $(TITLE_ID)/ (the app folder), $(TITLE_ID).zip (the release),
|
||||
# make native -> ../build-native/: $(TITLE_ID)/ (the app folder), $(RELEASE_ZIP) (the release),
|
||||
# $(TITLE_ID).debug.elf, and $(TITLE_ID).ffpfsc with FORMAT=Ffpfsc -- the layout of the Homebrew
|
||||
# Browser's build-native.bat (../build-native.bat runs this)
|
||||
# make dist -> make native + build/dist/Mupen64PS5-v$(VERSION).elf (the optional installer) and
|
||||
@@ -20,10 +20,11 @@
|
||||
# 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;
|
||||
# - its plugins, linked in (n64/plugins): mupen64plus-rsp-cxd4 (LLE RSP), angrylion-rdp-plus (software
|
||||
# RDP/VI), AudioOut and DualSense input;
|
||||
# - 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).
|
||||
#
|
||||
# Dependencies: the ps5-payload-dev SDK (v0.42 or newer, https://github.com/ps5-payload-dev/sdk) in
|
||||
@@ -32,19 +33,41 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
PS5_HOST ?= ps5
|
||||
PS5_PORT ?= 9021
|
||||
VERSION ?= 0.6.2
|
||||
VERSION ?= 0.7.4
|
||||
# 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}')
|
||||
TITLE_ID ?= PPSA99064
|
||||
# where the app looks for updates (fe_update.h) and `make release` publishes
|
||||
GITHUB_REPO ?= TheRealRetro/mupen64plus-ps5
|
||||
APP_OUT := build/app/$(TITLE_ID)
|
||||
# The release zip's name, which the app's updater looks for. Up to 0.6.2 it was $(TITLE_ID).zip, but those
|
||||
# versions' updater wrote files without execute permission and left the app unable to start: under this name
|
||||
# they never find a newer release, so they can't break themselves (their users install 0.6.3 by hand once).
|
||||
RELEASE_ZIP := Mupen64PlusPS5.zip
|
||||
# The GPU renderer (paraLLEl-RDP, setting Renderer; GitHub issue #1): the app links RADV, the Vulkan driver
|
||||
# mihawk-99/PS5_Vulkan builds for the console (in $(PS5_VULKAN): tools/setup-native-dependencies.sh, then
|
||||
# tools/build-radv.sh release), and runs a Vulkan self-test at start (coreorbis/orbis-shims/ProsperoVulkan.h).
|
||||
# VULKAN=0 builds the app without it (angrylion only, eboot.bin ~4 MB instead of ~30 MB) in folders of its own,
|
||||
# with the app in ../build-native-cpu/. The host build never has it.
|
||||
ifneq ($(filter host%,$(MAKECMDGOALS)),)
|
||||
VULKAN ?= 0
|
||||
endif
|
||||
VULKAN ?= 1
|
||||
PS5_VULKAN ?= /root/gpu/PS5_Vulkan
|
||||
ifeq ($(VULKAN),1)
|
||||
APP_BUILD := build/app
|
||||
else
|
||||
APP_BUILD := build/app-cpu
|
||||
endif
|
||||
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
|
||||
ANGRYLION := third_party/angrylion-rdp-plus/src
|
||||
|
||||
# ---- mupen64plus-core (the unix Makefile's list, without vidext/eventloop/screenshot/sdl_key_converter and the
|
||||
@@ -75,9 +98,12 @@ CORE_SUB_SRC := md5/md5.c minizip/ioapi.c minizip/unzip.c minizip/zip.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
|
||||
# the plugins
|
||||
PLUGIN_C_SRC := n64/plugins/rsp_cxd4_ps5.c n64/plugins/gfx_ps5.c
|
||||
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
|
||||
ANGRYLION_C_SRC := core/n64video.c
|
||||
# mupen64plus-rsp-hle's core (its plugin.c is replaced by n64/plugins/rsp_hle_ps5.c): HLE audio etc.
|
||||
RSPHLE_SRC := alist.c alist_audio.c alist_naudio.c alist_nead.c audio.c cicx105.c hle.c hvqm.c jpeg.c memory.c mp3.c \
|
||||
musyx.c re2.c
|
||||
ANGRYLION_CXX_SRC := core/parallel.cpp
|
||||
|
||||
# the x86-64 new dynarec (DYNAREC, NEW_DYNAREC=2): its code cache is made executable by n64/core/jit_ps5.c
|
||||
@@ -86,7 +112,7 @@ CORE_DEFS := -DNDEBUG -DNOCRYPT -DNOUNCRYPT -DM64P_PARALLEL -DDYNAREC -DNEW_DYNA
|
||||
DYNAREC_FLAGS := -Dmprotect=n64ps5_jit_mprotect
|
||||
CORE_INCS := -In64/sdl -I$(M64SRC) -I$(M64)/subprojects/md5 -I$(M64)/subprojects/xxhash -I$(M64)/subprojects/minizip -I$(ZLIB)
|
||||
CXD4_DEFS := -DM64P_PLUGIN_API -DARCH_MIN_SSE2 -DM64PS5_NO_RDRAM_PROBE -DNDEBUG
|
||||
CXD4_INCS := -I$(CXD4) -I$(M64SRC)/api -In64 -In64/core -I$(M64SRC)
|
||||
CXD4_INCS := -I$(CXD4) -I$(RSPHLE) -I$(M64SRC)/api -In64 -In64/core -I$(M64SRC)
|
||||
GFX_INCS := -I$(ANGRYLION) -I$(M64SRC) -In64 -In64/core
|
||||
|
||||
# the app (eboot.bin): the frontend and the PS5 layer
|
||||
@@ -94,13 +120,13 @@ EMU_SRC := coreorbis/main-boot.cpp \
|
||||
coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.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/ProsperoCrash.cpp coreorbis/orbis-shims/ProsperoUpdateJob.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
|
||||
# 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/ProsperoHelper.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp
|
||||
INSTALLER_SRC := installer/installer_main.cpp $(HELPER_SRC) coreorbis/orbis-shims/ProsperoInstall.cpp
|
||||
|
||||
ZLIB_SRC := $(wildcard $(ZLIB)/*.c)
|
||||
@@ -108,20 +134,34 @@ ZLIB_SRC := $(wildcard $(ZLIB)/*.c)
|
||||
INCS := -I$(M64SRC) -I$(M64)/subprojects/minizip -I$(ZLIB) \
|
||||
-Icoreorbis/include-orbis -Icoreorbis/orbis-shims -Ifrontend -In64 -In64/core
|
||||
DEFS := -DN64PS5_VERSION='"$(VERSION)"' -DN64PS5_TITLE_ID='"$(TITLE_ID)"' \
|
||||
-DN64PS5_CONTENT_VERSION='"$(CONTENT_VERSION)"' -DN64PS5_GITHUB_REPO='"$(GITHUB_REPO)"'
|
||||
-DN64PS5_CONTENT_VERSION='"$(CONTENT_VERSION)"' -DN64PS5_GITHUB_REPO='"$(GITHUB_REPO)"' \
|
||||
-DN64PS5_RELEASE_ZIP='"$(RELEASE_ZIP)"'
|
||||
PORT_WARN := -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers
|
||||
ifeq ($(VULKAN),1)
|
||||
VULKAN_SRC := coreorbis/orbis-shims/ProsperoVulkan.cpp
|
||||
DEFS += -DN64PS5_VULKAN
|
||||
endif
|
||||
|
||||
# generated: the game-name table (from the core's ROM catalog) built into fe_games.o
|
||||
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
|
||||
@@ -141,18 +181,24 @@ PORT_HDRS := $(wildcard coreorbis/include-orbis/*.h coreorbis/orbis-shims/*.h fr
|
||||
# ---------------- PS5 ----------------
|
||||
PS5_CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang
|
||||
PS5_CXX := $(PS5_PAYLOAD_SDK)/bin/prospero-clang++
|
||||
ifeq ($(VULKAN),1)
|
||||
PS5_OUT := build/ps5
|
||||
else
|
||||
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 \
|
||||
$(patsubst %.c,$(PS5_OUT)/n64/%.o,$(N64_SRC)) \
|
||||
$(patsubst %.c,$(PS5_OUT)/n64/%.o,$(PLUGIN_C_SRC)) $(patsubst %.cpp,$(PS5_OUT)/n64/%.o,$(PLUGIN_CXX_SRC)) \
|
||||
$(patsubst %.c,$(PS5_OUT)/angrylion/%.o,$(ANGRYLION_C_SRC)) $(patsubst %.cpp,$(PS5_OUT)/angrylion/%.o,$(ANGRYLION_CXX_SRC))
|
||||
PS5_EMU_OBJ := $(patsubst %.cpp,$(PS5_OUT)/port/%.o,$(EMU_SRC))
|
||||
$(patsubst %.c,$(PS5_OUT)/angrylion/%.o,$(ANGRYLION_C_SRC)) $(patsubst %.cpp,$(PS5_OUT)/angrylion/%.o,$(ANGRYLION_CXX_SRC)) \
|
||||
$(patsubst %.c,$(PS5_OUT)/rsphle/%.o,$(RSPHLE_SRC))
|
||||
PS5_EMU_OBJ := $(patsubst %.cpp,$(PS5_OUT)/port/%.o,$(EMU_SRC) $(VULKAN_SRC))
|
||||
PS5_HELPER_OBJ := $(PS5_OUT)/port/installer/helper_main.o $(patsubst %.cpp,$(PS5_OUT)/port/%.o,$(HELPER_SRC))
|
||||
PS5_INSTALLER_OBJ := $(patsubst %.cpp,$(PS5_OUT)/port/%.o,$(INSTALLER_SRC))
|
||||
PS5_ZLIB_OBJ := $(patsubst $(ZLIB)/%.c,$(PS5_OUT)/zlib/%.o,$(ZLIB_SRC))
|
||||
@@ -160,16 +206,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
|
||||
@@ -183,7 +229,7 @@ $(PS5_OUT)/m64sub/%.o: $(M64)/subprojects/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/n64/n64/plugins/rsp_cxd4_ps5.o: n64/plugins/rsp_cxd4_ps5.c $(wildcard $(CXD4)/*.c $(CXD4)/*.h $(CXD4)/vu/*) n64/core/static_dynlib.h
|
||||
$(PS5_OUT)/n64/n64/plugins/rsp_cxd4_ps5.o: n64/plugins/rsp_cxd4_ps5.c $(wildcard $(CXD4)/*.c $(CXD4)/*.h $(CXD4)/vu/*) n64/core/static_dynlib.h n64/n64_bridge.h
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(CXD4_DEFS) $(CXD4_INCS) -w -c -o $@ $<
|
||||
|
||||
@@ -191,7 +237,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 $@ $<
|
||||
|
||||
@@ -199,6 +245,10 @@ $(PS5_OUT)/n64/%.o: %.cpp $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) $(PORT_WARN) -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/rsphle/%.o: $(RSPHLE)/%.c $(wildcard $(RSPHLE)/*.h)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) -DNDEBUG -I$(RSPHLE) -w -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/angrylion/%.o: $(ANGRYLION)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) -I$(ANGRYLION) -w -c -o $@ $<
|
||||
@@ -211,6 +261,17 @@ $(PS5_OUT)/port/%.o: %.cpp $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) $(PORT_WARN) $(EXTRA_FLAGS) -c -o $@ $<
|
||||
|
||||
# the Vulkan self-test (VULKAN=1): RADV's headers, and its compute shader compiled to SPIR-V
|
||||
RADV := $(PS5_VULKAN)/.deps/native/radv-release
|
||||
$(PS5_OUT)/gen/probe_comp.h: coreorbis/orbis-shims/shaders/probe.comp
|
||||
@mkdir -p $(dir $@)
|
||||
glslangValidator -V --target-env vulkan1.1 --vn kProbeComp -o $@ $< > /dev/null
|
||||
|
||||
$(PS5_OUT)/port/coreorbis/orbis-shims/ProsperoVulkan.o: coreorbis/orbis-shims/ProsperoVulkan.cpp \
|
||||
$(PS5_OUT)/gen/probe_comp.h $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) -I$(RADV)/include -I$(PS5_OUT)/gen $(PORT_WARN) -c -o $@ $<
|
||||
|
||||
$(PS5_OUT)/port/installer/helper_main.o: installer/installer_main.cpp $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) $(PORT_WARN) $(EXTRA_FLAGS) -c -o $@ $<
|
||||
@@ -227,9 +288,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)"' \
|
||||
@@ -250,29 +315,33 @@ strip: $(PS5_OUT)/Mupen64PS5-stripped.elf
|
||||
# The release, in the Homebrew Browser's layout (its native/build-native.sh), in ../build-native/:
|
||||
# $(TITLE_ID)/ the app folder: copied to /data/homebrew/ (or a USB drive's homebrew/), ShadowMountPlus
|
||||
# lists it
|
||||
# $(TITLE_ID).zip the release artifact: just $(TITLE_ID)/ (the browser installs a release's
|
||||
# $(TITLE_ID).zip; the tag is the contentVersion, $(CONTENT_VERSION))
|
||||
# $(RELEASE_ZIP) the release artifact: just $(TITLE_ID)/ (the browser installs a release's
|
||||
# $(RELEASE_ZIP); the tag is the contentVersion, $(CONTENT_VERSION))
|
||||
# $(TITLE_ID).debug.elf eboot.bin's program with its symbols, for reading crash reports
|
||||
# $(TITLE_ID).ffpfsc with FORMAT=Ffpfsc: a compressed image of the folder (the boilerplate's mkpfs)
|
||||
# release-assets/$(CONTENT_VERSION)/$(TITLE_ID).zip a copy kept per version
|
||||
# release-assets/$(CONTENT_VERSION)/$(RELEASE_ZIP) a copy kept per version
|
||||
ifeq ($(VULKAN),1)
|
||||
NATIVE_OUT := ../build-native
|
||||
else
|
||||
NATIVE_OUT := ../build-native-cpu
|
||||
endif
|
||||
FORMAT ?= Folder
|
||||
PS5_NATIVE_BOILERPLATE ?= /root/ps5-native
|
||||
native: $(APP_FILES) build/app/mupen64plus-pie.elf
|
||||
native: $(APP_FILES) $(APP_BUILD)/mupen64plus-pie.elf
|
||||
@mkdir -p "$(abspath $(NATIVE_OUT))"
|
||||
rm -rf "$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip"
|
||||
rm -rf "$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)"
|
||||
cp -r $(APP_OUT) "$(abspath $(NATIVE_OUT))/"
|
||||
cd "$(abspath $(NATIVE_OUT))" && python3 -m zipfile -c $(TITLE_ID).zip $(TITLE_ID)
|
||||
cp build/app/mupen64plus-pie.elf "$(abspath $(NATIVE_OUT))/$(TITLE_ID).debug.elf"
|
||||
cd "$(abspath $(NATIVE_OUT))" && python3 -m zipfile -c $(RELEASE_ZIP) $(TITLE_ID)
|
||||
cp $(APP_BUILD)/mupen64plus-pie.elf "$(abspath $(NATIVE_OUT))/$(TITLE_ID).debug.elf"
|
||||
mkdir -p "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)"
|
||||
cp "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip" "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)/"
|
||||
cp "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)" "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)/"
|
||||
@if [ "$$(echo $(FORMAT) | tr A-Z a-z)" = ffpfsc ]; then \
|
||||
mkpfs=$$(bash $(PS5_NATIVE_BOILERPLATE)/tools/setup-packaging-dependencies.sh ffpfsc) && \
|
||||
rm -f "$(abspath $(NATIVE_OUT))/$(TITLE_ID).ffpfsc" && \
|
||||
"$$mkpfs" pack folder --no-adjust-output-file-extension --version PS5 --verify \
|
||||
"$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(TITLE_ID).ffpfsc"; \
|
||||
fi
|
||||
@python3 -m zipfile -l "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip"
|
||||
@python3 -m zipfile -l "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)"
|
||||
@echo "Native app: $(abspath $(NATIVE_OUT))/$(TITLE_ID) (copy this folder to /data/homebrew/), version $(CONTENT_VERSION)"
|
||||
|
||||
# the extra release files: the optional installer payload and the sources
|
||||
@@ -327,31 +396,83 @@ $(APP_OUT)/sce_module/libc.prx: build/app/libc.prx
|
||||
|
||||
app: $(APP_FILES)
|
||||
|
||||
build/app/app_crt.o build/app/app_cpp_runtime.o: build/app/%.o: $(NATIVE)/tooling/native/%.cpp
|
||||
$(APP_BUILD)/app_crt.o $(APP_BUILD)/app_cpp_runtime.o: $(APP_BUILD)/%.o: $(NATIVE)/tooling/native/%.cpp
|
||||
@mkdir -p $(dir $@)
|
||||
$(NATIVE_CLANG) -std=c++20 -O2 -Wall -Wextra -fno-exceptions -fno-rtti -ffunction-sections -fdata-sections \
|
||||
-c -o $@ $<
|
||||
|
||||
EBOOT_OBJS = build/app/app_crt.o build/app/app_cpp_runtime.o $(PS5_CORE_OBJ) $(PS5_EMU_OBJ) $(PS5_ZLIB_OBJ) \
|
||||
$(PS5_OUT)/helper_data.o
|
||||
build/app/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=1: RADV and what it needs (tools/radv-link-flags.sh prints PS5_Vulkan's recipe), and link stubs that
|
||||
# name the AGC system modules it calls (PS5_Vulkan's tools/build-radv-title.sh builds the same two)
|
||||
ifeq ($(VULKAN),1)
|
||||
ifeq ($(filter host% clean,$(MAKECMDGOALS)),)
|
||||
ifeq ($(wildcard $(RADV)/lib/libvulkan_radeon.ps5.a),)
|
||||
$(error RADV is missing ($(RADV)/lib/libvulkan_radeon.ps5.a): check out mihawk-99/PS5_Vulkan in PS5_VULKAN \
|
||||
and run its tools/setup-native-dependencies.sh and tools/build-radv.sh release, or build with VULKAN=0)
|
||||
endif
|
||||
endif
|
||||
VULKAN_STUBS := $(APP_BUILD)/stubs/libSceAgc.so $(APP_BUILD)/stubs/libSceAgcDriver.so
|
||||
VULKAN_LINK := $$(bash tools/radv-link-flags.sh $(PS5_VULKAN))
|
||||
# the RADV archive has zlib built in (its shader cache), which stands in for ours
|
||||
EBOOT_ZLIB :=
|
||||
# the GPU renderer: paraLLEl-RDP (third_party/parallel-rdp, its standalone tree; config.mk lists its files)
|
||||
# and its plugin glue (n64/plugins/gfx_parallel_ps5.cpp)
|
||||
PARALLEL_RDP_IMPLEMENTATION := third_party/parallel-rdp
|
||||
include $(PARALLEL_RDP_IMPLEMENTATION)/config.mk
|
||||
VULKAN_OBJ := $(patsubst %.cpp,$(PS5_OUT)/prdp/%.o,$(PARALLEL_RDP_SOURCES_CXX)) \
|
||||
$(patsubst %.c,$(PS5_OUT)/prdp/%.o,$(PARALLEL_RDP_SOURCES_C)) $(PS5_OUT)/n64/n64/plugins/gfx_parallel_ps5.o
|
||||
# its headers, so a change to one rebuilds what includes it
|
||||
PRDP_HDRS := $(wildcard $(PARALLEL_RDP_IMPLEMENTATION)/*/*.hpp $(PARALLEL_RDP_IMPLEMENTATION)/*/*/*.hpp)
|
||||
$(PS5_OUT)/prdp/%.o: %.cpp $(PRDP_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(PARALLEL_RDP_CXXFLAGS) $(PARALLEL_RDP_INCLUDE_DIRS) -w -c -o $@ $<
|
||||
$(PS5_OUT)/prdp/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_OPT) $(PARALLEL_RDP_CFLAGS) $(PARALLEL_RDP_INCLUDE_DIRS) -w -c -o $@ $<
|
||||
$(PS5_OUT)/n64/n64/plugins/gfx_parallel_ps5.o: n64/plugins/gfx_parallel_ps5.cpp $(PORT_HDRS) $(PRDP_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CXX) -std=c++17 $(PS5_OPT) $(DEFS) $(INCS) $(PARALLEL_RDP_INCLUDE_DIRS) $(PORT_WARN) -c -o $@ $<
|
||||
$(APP_BUILD)/stubs/libSceAgc.so: $(PS5_VULKAN)/vendor/ps5/sdk/stubs/agc_canary_link_stub.c
|
||||
$(APP_BUILD)/stubs/libSceAgcDriver.so: $(PS5_VULKAN)/vendor/ps5/sdk/stubs/agc_driver_canary_link_stub.c
|
||||
$(VULKAN_STUBS):
|
||||
@mkdir -p $(dir $@)
|
||||
$(NATIVE_CLANG) -std=c11 -O2 -fPIC -c -o $@.o $<
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-lld --shared -soname $(basename $(notdir $@)).prx -o $@ $@.o
|
||||
else
|
||||
EBOOT_ZLIB = $(PS5_ZLIB_OBJ)
|
||||
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 \
|
||||
-T $(NATIVE)/tooling/native/ps5-pie.ld -T $(NATIVE)/tooling/native/ehframe.ld --eh-frame-hdr \
|
||||
--version-script $(NATIVE)/tooling/native/app-symbols.map --version-script $(NATIVE)/tooling/native/localize.map \
|
||||
-e _start --no-dynamic-linker -o $@ $(EBOOT_OBJS) \
|
||||
-e _start --no-dynamic-linker -o $@ $(EBOOT_OBJS) $(VULKAN_STUBS) $(VULKAN_LINK) \
|
||||
--as-needed $(APP_STUBS) -L $(SDK_LIB) \
|
||||
$(SDK_LIB)/libc++.a $(SDK_LIB)/libc++abi.a $(SDK_LIB)/libunwind.a $(SDK_LIB)/libc.a -lpthread
|
||||
|
||||
build/app/eboot.elf: build/app/mupen64plus-pie.elf $(NATIVE_TOOL)
|
||||
$(NATIVE_TOOL) link --in $< --out $@ --stub-dir $(SDK_LIB) --module-sdk 0x02000009 \
|
||||
$(APP_BUILD)/eboot.elf: $(APP_BUILD)/mupen64plus-pie.elf $(NATIVE_TOOL)
|
||||
$(NATIVE_TOOL) link --in $< --out $@ --stub-dir $(SDK_LIB) $(addprefix --stub ,$(VULKAN_STUBS)) --module-sdk 0x02000009 \
|
||||
--companion-sdk 0x08050001 --file-name eboot.elf
|
||||
python3 tools/prepare_console_elf.py --input $@ --output build/app/eboot-console-ready.elf
|
||||
python3 tools/prepare_console_elf.py --input $@ --output $(APP_BUILD)/eboot-console-ready.elf
|
||||
|
||||
$(APP_OUT)/eboot.bin: build/app/eboot.elf $(NATIVE_TOOL)
|
||||
# 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.
|
||||
@@ -384,7 +505,8 @@ HOST_CORE_OBJ := $(patsubst %.c,$(HOST_OUT)/m64/%.o,$(CORE_SRC)) $(patsubst %.c,
|
||||
$(HOST_OUT)/n64/n64/core/linkage_x64.o \
|
||||
$(patsubst %.c,$(HOST_OUT)/n64/%.o,$(N64_SRC)) \
|
||||
$(patsubst %.c,$(HOST_OUT)/n64/%.o,$(PLUGIN_C_SRC)) $(patsubst %.cpp,$(HOST_OUT)/n64/%.o,$(PLUGIN_CXX_SRC)) \
|
||||
$(patsubst %.c,$(HOST_OUT)/angrylion/%.o,$(ANGRYLION_C_SRC)) $(patsubst %.cpp,$(HOST_OUT)/angrylion/%.o,$(ANGRYLION_CXX_SRC))
|
||||
$(patsubst %.c,$(HOST_OUT)/angrylion/%.o,$(ANGRYLION_C_SRC)) $(patsubst %.cpp,$(HOST_OUT)/angrylion/%.o,$(ANGRYLION_CXX_SRC)) \
|
||||
$(patsubst %.c,$(HOST_OUT)/rsphle/%.o,$(RSPHLE_SRC))
|
||||
HOST_EMU_OBJ := $(patsubst %.cpp,$(HOST_OUT)/port/%.o,$(EMU_SRC) host/sce_host.cpp)
|
||||
HOST_HELPER_OBJ := $(HOST_OUT)/port/installer/helper_main.o $(patsubst %.cpp,$(HOST_OUT)/port/%.o,$(HELPER_SRC) host/sce_host.cpp)
|
||||
HOST_INSTALLER_OBJ := $(patsubst %.cpp,$(HOST_OUT)/port/%.o,$(INSTALLER_SRC) host/sce_host.cpp)
|
||||
@@ -392,15 +514,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
|
||||
@@ -414,7 +536,7 @@ $(HOST_OUT)/m64sub/%.o: $(M64)/subprojects/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CORE_DEFS) $(CORE_INCS) -w -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/n64/n64/plugins/rsp_cxd4_ps5.o: n64/plugins/rsp_cxd4_ps5.c $(wildcard $(CXD4)/*.c $(CXD4)/*.h $(CXD4)/vu/*) n64/core/static_dynlib.h
|
||||
$(HOST_OUT)/n64/n64/plugins/rsp_cxd4_ps5.o: n64/plugins/rsp_cxd4_ps5.c $(wildcard $(CXD4)/*.c $(CXD4)/*.h $(CXD4)/vu/*) n64/core/static_dynlib.h n64/n64_bridge.h
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) $(CXD4_DEFS) $(CXD4_INCS) -w -c -o $@ $<
|
||||
|
||||
@@ -422,7 +544,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 $@ $<
|
||||
|
||||
@@ -430,6 +552,10 @@ $(HOST_OUT)/n64/%.o: %.cpp $(PORT_HDRS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CXX) -std=c++17 $(HOST_OPT) $(HOST_SAN) $(DEFS) $(INCS) $(PORT_WARN) -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/rsphle/%.o: $(RSPHLE)/%.c $(wildcard $(RSPHLE)/*.h)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) -I$(RSPHLE) -w -c -o $@ $<
|
||||
|
||||
$(HOST_OUT)/angrylion/%.o: $(ANGRYLION)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_OPT) -I$(ANGRYLION) -w -c -o $@ $<
|
||||
@@ -478,6 +604,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
|
||||
|
||||
+107
-40
@@ -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;
|
||||
@@ -15,7 +15,13 @@ its files): no payload to send.
|
||||
The PS5 layer and the build come from **Snes9x PS5** (`../../snes9xPS5-2.0/ps5`), which follows the PS5SX2
|
||||
model.
|
||||
|
||||
> **0.6:** released as the app folder (`PPSA99064.zip`): copy `PPSA99064/` to
|
||||
> **0.6.3:** in-app updates work. Up to 0.6.2 the updater wrote the new files without execute permission, so
|
||||
> the console refused to start the updated app ("Can't start the game or app", CE-107750-0). 0.6.3 unpacks the
|
||||
> update next to the app folder with the permissions an FTP copy gets, closes, and its helper swaps the folders
|
||||
> so ShadowMountPlus installs the app again. The release zip is now `Mupen64PlusPS5.zip`, so 0.6.0–0.6.2 never
|
||||
> offer an update they would break: install 0.6.3 by hand once.
|
||||
>
|
||||
> **0.6:** released as the app folder (`Mupen64PlusPS5.zip`): copy `PPSA99064/` to
|
||||
> `/data/homebrew/` or a USB drive's `homebrew/`, and ShadowMountPlus lists it. No `.elf` to send: eboot.bin
|
||||
> starts the helper it carries through the ELF loader by itself, and now prefers it over etaHEN's daemon.
|
||||
> The app updates itself from the GitHub releases ([Updates](#updates)).
|
||||
@@ -54,14 +60,32 @@ with tables of functions looked up by name.
|
||||
| Part | What runs it on the PS5 |
|
||||
|---|---|
|
||||
| CPU (VR4300) | mupen64plus-core's **x86-64 new dynarec** when the console gives executable memory (`n64/core/jit_ps5.c`), else its cached interpreter |
|
||||
| RSP | **mupen64plus-rsp-cxd4**: low level, it runs the game's own microcode (`n64/plugins/rsp_cxd4_ps5.c`) |
|
||||
| RDP + VI | **angrylion-rdp-plus**: software, pixel-accurate, 6 render threads (`n64/plugins/gfx_ps5.c`) |
|
||||
| RSP | **mupen64plus-rsp-hle** for audio, MP3 and JPEG tasks (`n64/plugins/rsp_hle_ps5.c`), **mupen64plus-rsp-cxd4** for the rest: low level, it runs the game's own microcode, graphics included (`n64/plugins/rsp_cxd4_ps5.c`) |
|
||||
| RDP + VI | **paraLLEl-RDP** on the GPU through Vulkan (RADV), at 1x-8x internal resolution (`n64/plugins/gfx_parallel_ps5.cpp`, default), or **angrylion-rdp-plus**: software, pixel-accurate, on 6 CPU threads (`n64/plugins/gfx_ps5.c`) |
|
||||
| Audio | `n64/plugins/audio_ps5.cpp`: AI samples resampled to 48 kHz, into `libSceAudioOut` |
|
||||
| Input | `n64/plugins/input_ps5.cpp`: `libScePad`, rumble through `scePadSetVibration` |
|
||||
| Picture | `ProsperoVideo`: angrylion's frame scaled (4:3 / integer / stretch, sharp or smooth) and tiled into `libSceVideoOut` |
|
||||
| Picture | `ProsperoVideo`: the renderer's frame scaled (4:3 / integer / stretch, sharp or smooth) and tiled into `libSceVideoOut` |
|
||||
|
||||
PS5 homebrew has no OpenGL or Vulkan driver, so hardware renderers (GLideN64, paraLLEl-RDP) can't be used.
|
||||
angrylion renders on the CPU, which the PS5's eight Zen 2 cores can afford.
|
||||
**The GPU renderer.** PS5 homebrew has a Vulkan driver: RADV, Mesa's driver for AMD GPUs, as
|
||||
mihawk-99/PS5_Vulkan builds it for the console; the app links it in (`eboot.bin` grows from ~4 MB to ~30 MB) and
|
||||
runs a short self-test at start (`coreorbis/orbis-shims/ProsperoVulkan.h`, results in `boot.log`).
|
||||
paraLLEl-RDP, the renderer RetroArch uses, runs the N64's RDP commands as compute shaders, reading and writing
|
||||
the emulator's RDRAM directly (`VK_EXT_external_memory_host`), and draws at up to 8x the N64's resolution. The
|
||||
picture is read back and shown like angrylion's. Its compiled shaders are kept in
|
||||
`/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:**
|
||||
|
||||
@@ -87,7 +111,7 @@ angrylion renders on the CPU, which the PS5's eight Zen 2 cores can afford.
|
||||
|
||||
## Install and play
|
||||
|
||||
1. **Copy the `PPSA99064` folder** from `PPSA99064.zip` (the folder itself, not just its
|
||||
1. **Copy the `PPSA99064` folder** from `Mupen64PlusPS5.zip` (the folder itself, not just its
|
||||
contents) to one of:
|
||||
- `/data/homebrew/PPSA99064` on the console (FTP, PS5Upload...);
|
||||
- `homebrew/PPSA99064` on an exFAT USB drive.
|
||||
@@ -114,12 +138,13 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| `/data/mupen64plus/config` | `mupen64plus.cfg`, the core's own settings (advanced) |
|
||||
| `/data/mupen64plus/data` | `mupen64plus.ini`, the core's ROM catalog (save types, per-game fixes), written by the app |
|
||||
| `/data/mupen64plus/covers` | downloaded covers and your own (`<rom file name>.png` / `.jpg`) |
|
||||
| `/data/mupen64plus/logs` | `boot.log`, `installer.log`, `helper.log` and the previous session's `.prev.log` |
|
||||
| `/data/mupen64plus/logs` | `boot.log`, `installer.log`, `helper.log` and the 4 sessions before as `.prev.log` (the last) to `.prev4.log` |
|
||||
| `/data/mupen64plus/mupen64plus-ps5.ini` | the menu settings |
|
||||
|
||||
## 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 |
|
||||
|---|---|
|
||||
@@ -131,14 +156,24 @@ 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;
|
||||
- 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):
|
||||
|
||||
@@ -148,18 +183,31 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| Smooth scaling | bilinear (on) or sharp pixels |
|
||||
| N64 video filter * | the N64's own VI filter (anti-aliasing, dither filter) |
|
||||
| Hide overscan * | crops the black border the N64 draws around the picture |
|
||||
| Renderer * | GPU (paraLLEl-RDP, default) or CPU (angrylion, pixel-accurate at the N64's resolution) |
|
||||
| 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) |
|
||||
| Render threads * | angrylion workers: 1–12, default 6 |
|
||||
| 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% |
|
||||
| Show FPS | the speed (`VI/s`, 100% = full speed) and, below it, where the emulation thread's time goes: `cpu` (VR4300 interpreter), `rsp` (cxd4), `rdp` (angrylion's rasteriser, incl. waiting for its threads), `vi` (its VI filter), `idle` (waiting for the TV: spare time). Also logged every 10 s in `boot.log` |
|
||||
| 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
|
||||
@@ -170,12 +218,17 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
`api.github.com` for the latest release of `TheRealRetro/mupen64plus-ps5` (`GITHUB_REPO` in the Makefile).
|
||||
|
||||
- **When the release's tag is newer** than the app's `contentVersion`, it downloads the release's
|
||||
`PPSA99064.zip` and checks its size and SHA-256 against the release (`frontend/fe_update.cpp`).
|
||||
`Mupen64PlusPS5.zip` and checks its size and SHA-256 against the release (`frontend/fe_update.cpp`).
|
||||
- **Once `/data` is visible,** it shows "Mupen64Plus PS5 X is available", with the release notes:
|
||||
- ✕ writes every file over each copy of the app folder it finds (`/data/homebrew/PPSA99064`, a USB drive's
|
||||
`homebrew/PPSA99064`...), each through a temporary `.new` name, and restarts the app;
|
||||
- ✕ unpacks the new version next to each copy of the app folder it finds (`/data/homebrew/PPSA99064`, a USB
|
||||
drive's `homebrew/PPSA99064`...), in `.mupen64plus-update/new/`, with the permissions an FTP copy gets
|
||||
(0777: the console doesn't start an `eboot.bin` without execute permission), writes a job file, starts its
|
||||
helper through the ELF loader and closes. The helper waits for the app to exit, moves the app folder aside,
|
||||
asks ShadowMountPlus to rescan (its API, 127.0.0.1:10101) until it has dropped the app, moves the new
|
||||
version in, rescans until it is installed again, and says so in a notification
|
||||
(`coreorbis/orbis-shims/ProsperoUpdateJob.cpp`; its steps go to `logs/helper.log`);
|
||||
- ○ carries on (asked again next start).
|
||||
- **A release that was installed but didn't take** (the restart still ran an older eboot) isn't offered
|
||||
- **A release that was installed but didn't take** (the next start still ran an older eboot) isn't offered
|
||||
again (`/data/mupen64plus/update/installed.txt`).
|
||||
- **Settings → Check for updates: No** skips the question.
|
||||
- **An app running from a `.ffpfsc` image** can't be updated this way (read-only): copy the new folder by hand.
|
||||
@@ -185,11 +238,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
|
||||
@@ -198,9 +254,9 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
first `mprotect`, then executable direct memory mapped over the code cache (what RetroArch's PS5 port uses
|
||||
for its recompilers). `boot.log` says which worked. If a game misbehaves with it, set CPU core to
|
||||
Interpreter.
|
||||
- **The RSP is the next bottleneck.** cxd4 interprets every microcode instruction, graphics and audio alike
|
||||
(GoldenEye: ~30% of the time). High-level audio emulation would take the audio part off it.
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, a cheats menu, netplay.
|
||||
- **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, netplay.
|
||||
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
|
||||
|
||||
## Building
|
||||
@@ -208,7 +264,11 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
Requirements:
|
||||
|
||||
- the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk), v0.42 or newer;
|
||||
- clang and lld 18, g++, and python3 (plus Pillow and numpy only to redraw the art).
|
||||
- clang and lld 18, g++, glslang (`glslangValidator`), and python3 (plus Pillow and numpy only to redraw the art);
|
||||
- for the GPU renderer, [mihawk-99/PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan) with its RADV archive
|
||||
built: check it out (`PS5_VULKAN`, default `/root/gpu/PS5_Vulkan`), run its
|
||||
`tools/setup-native-dependencies.sh`, then `tools/build-radv.sh release` (meson 1.12 or newer). Without it,
|
||||
build with `VULKAN=0` (`build-native.bat Cpu`): the same app with the CPU renderer only, in `build-native-cpu/`.
|
||||
|
||||
zlib is vendored.
|
||||
|
||||
@@ -217,12 +277,14 @@ From Windows, as the Homebrew Browser's build (WSL Ubuntu-24.04, the SDK of `/ro
|
||||
```
|
||||
build-native.bat app folder + zip
|
||||
build-native.bat Ffpfsc also a compressed .ffpfsc image
|
||||
build-native.bat Clean from scratch (release.bat always does this)
|
||||
build-native.bat Cpu without the GPU renderer, in build-native-cpu/
|
||||
```
|
||||
|
||||
Output in `build-native/` (next to `ps5/`):
|
||||
|
||||
- `PPSA99064/`: the app folder to copy to `/data/homebrew/`.
|
||||
- `PPSA99064.zip`: the release artifact (just the folder). Its tag is the `contentVersion`, made from
|
||||
- `Mupen64PlusPS5.zip`: the release artifact (just the folder). Its tag is the `contentVersion`, made from
|
||||
`VERSION` in the Makefile (0.6.0 -> `00.006.000`); a copy is kept in `release-assets/<contentVersion>/`.
|
||||
- `PPSA99064.debug.elf`: the program with its symbols, for reading crash reports.
|
||||
- `PPSA99064.ffpfsc`: with `Ffpfsc`, a compressed image ShadowMountPlus can mount.
|
||||
@@ -232,7 +294,7 @@ By hand:
|
||||
```sh
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
cd ps5
|
||||
make native -j$(nproc) # ../build-native/ as above (FORMAT=Ffpfsc for the image)
|
||||
make native -j$(nproc) # ../build-native/ as above (FORMAT=Ffpfsc for the image; VULKAN=0: no GPU renderer)
|
||||
make ps5 -j$(nproc) # build/ps5/Mupen64PS5.elf (installer + helper, with the app inside)
|
||||
make send PS5_HOST=192.168.0.10 # sends it to the ELF loader (port 9021)
|
||||
make dist # make native + build/dist/Mupen64PS5-v<version>.elf (optional installer)
|
||||
@@ -252,18 +314,21 @@ 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).
|
||||
|
||||
**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
|
||||
|
||||
- **`n64/plugins/`**: the four plugins.
|
||||
- `rsp_cxd4_ps5.c` builds cxd4 as one unit with its exports renamed.
|
||||
- `rsp_cxd4_ps5.c` builds cxd4 as one unit with its exports renamed; `rsp_hle_ps5.c` puts rsp-hle's
|
||||
core in front of it (what rsp-hle doesn't know, graphics above all, goes to cxd4).
|
||||
- `gfx_ps5.c` is angrylion's plugin glue without OpenGL.
|
||||
- `audio_ps5.cpp` and `input_ps5.cpp`.
|
||||
- **`n64/core/`**: the core's PS5 side.
|
||||
@@ -277,7 +342,8 @@ generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`
|
||||
and the menus. These come from Snes9x PS5.
|
||||
- **`coreorbis/`**: the PS5 layer from Snes9x PS5: VideoOut (with the N64 scaler), AudioOut, pads (with
|
||||
rumble), the sandbox request, install, crash log, notifications and paths.
|
||||
- **`third_party/`**: angrylion-rdp-plus (`9c8b9ed`) and mupen64plus-rsp-cxd4 (`00906a9`), plus zlib. Two
|
||||
- **`third_party/`**: angrylion-rdp-plus (`9c8b9ed`), mupen64plus-rsp-cxd4 (`00906a9`) and
|
||||
mupen64plus-rsp-hle (2.6.0, `src/` unmodified, minus `plugin.c` and the dlopen osal), plus zlib. Two
|
||||
changes are marked `M64PS5_*`:
|
||||
- cxd4 doesn't probe RDRAM with SIGSEGV;
|
||||
- angrylion's workers get 1 MiB stacks.
|
||||
@@ -288,6 +354,7 @@ generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`
|
||||
because of the native tooling below).
|
||||
- **angrylion-rdp-plus**: MAME license (non-commercial). `gfx_ps5.c` replaces its GPL-2.0+ mupen64plus glue.
|
||||
- **mupen64plus-rsp-cxd4**: CC0.
|
||||
- **mupen64plus-rsp-hle**: GPL-2.0-or-later (`rsp_hle_ps5.c`, which builds on it, too).
|
||||
- **New code in `ps5/n64`** and the PS5 layer: MIT.
|
||||
- **Snes9x PS5** (github.com/MisterTemaki): the PS5 layer, the frontend and the build. **PS5SX2** (Spyros): the
|
||||
model they follow.
|
||||
|
||||
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.006.002",
|
||||
"contentVersion": "00.007.004",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
// /data/mupen64plus/config mupen64plus.cfg, the core's own configuration
|
||||
// /data/mupen64plus/data mupen64plus.ini, the core's ROM catalog (written by the app)
|
||||
// /data/mupen64plus/covers downloaded covers and your own
|
||||
// /data/mupen64plus/logs boot.log (this run) and boot.prev.log (the run before); installer.log
|
||||
// /data/mupen64plus/logs boot.log (this run), boot.prev.log .. boot.prev4.log (the 4 before); installer.log
|
||||
// /data/mupen64plus/mupen64plus-ps5.ini the frontend's settings
|
||||
//
|
||||
// USB drives are searched too: /mnt/usbN/mupen64plus/roms (N = 0..7) and /mnt/extN/mupen64plus/roms.
|
||||
@@ -30,13 +30,17 @@ 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);
|
||||
bool OrbisMkdirs(const std::string& path);
|
||||
|
||||
// <root>/logs/<name>.log (the run before kept as <name>.prev.log): boot.log for the emulator, installer.log
|
||||
// for the installer/helper payload. Every line also goes to stdout. Lines logged before the file is open
|
||||
// <root>/logs/<name>.log (the 4 runs before kept as <name>.prev.log .. <name>.prev4.log): boot.log for the
|
||||
// emulator, installer.log for the installer/helper payload. Every line also goes to stdout. Lines logged before the file is open
|
||||
// (before the jailbreak shows /data to the app) are kept and written first.
|
||||
void OrbisLogOpen(const char* name = "boot");
|
||||
void OrbisLog(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoSce.h"
|
||||
#include "ProsperoVideo.h"
|
||||
#ifdef N64PS5_VULKAN
|
||||
#include "ProsperoVulkan.h"
|
||||
#endif
|
||||
|
||||
#include "fe_emu.h"
|
||||
#include "fe_covers.h"
|
||||
@@ -53,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;
|
||||
@@ -149,6 +152,14 @@ int Run(int argc, char** argv)
|
||||
return 2;
|
||||
}
|
||||
|
||||
#ifdef N64PS5_VULKAN
|
||||
// builds with Vulkan (make VULKAN=1): what the GPU offers, in boot.log (27 ms on the console); the driver's
|
||||
// own messages go to logs/vulkan.log
|
||||
N64_STAGE(Boot, "vulkan self-test");
|
||||
ps5vulkan::CaptureDriverMessages();
|
||||
ps5vulkan::Probe();
|
||||
#endif
|
||||
|
||||
N64_STAGE(Boot, "update");
|
||||
fe::OfferUpdate(update, fe::Config().updates);
|
||||
update.zip.clear();
|
||||
@@ -166,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;
|
||||
|
||||
@@ -199,6 +199,11 @@ bool FetchWantedCovers(std::string& text)
|
||||
return r == 1;
|
||||
}
|
||||
|
||||
bool StartHelper()
|
||||
{
|
||||
return SendHelper();
|
||||
}
|
||||
|
||||
bool RequestForSelf(std::string& how)
|
||||
{
|
||||
const int own = PortFromEnv("N64PS5_HELPER_PORT", kHelperPort);
|
||||
|
||||
@@ -48,6 +48,10 @@ constexpr int32_t kRetUntouched = -1337;
|
||||
// Every step goes to OrbisLog (kept in memory until boot.log can be opened).
|
||||
bool RequestForSelf(std::string& how);
|
||||
|
||||
// The app side: sends the helper built into the app to the ELF loader, even when one is running (a new copy
|
||||
// runs a pending update job, then leaves). True when every byte went out.
|
||||
bool StartHelper();
|
||||
|
||||
// The app side, before the jailbreak: the wanted-covers list from the helper (starting the helper through the
|
||||
// ELF loader if none answers). False when no helper could be reached; `text` may be empty (nothing wanted).
|
||||
bool FetchWantedCovers(std::string& text);
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// Mupen64Plus PS5: installing a downloaded update (ProsperoUpdateJob.h).
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "ProsperoUpdateJob.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoSce.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <dirent.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef N64PS5_TITLE_ID
|
||||
#define N64PS5_TITLE_ID "PPSA99064"
|
||||
#endif
|
||||
|
||||
namespace updatejob
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr const char* kUpdateDir = ".mupen64plus-update";
|
||||
|
||||
bool Exists(const std::string& path)
|
||||
{
|
||||
struct stat st;
|
||||
return lstat(path.c_str(), &st) == 0;
|
||||
}
|
||||
|
||||
// /user/app/<TITLE>/mount.lnk: ShadowMountPlus's record of where an installed folder app lives
|
||||
// (N64PS5_APP_LINK_DIR replaces /user/app in the host tests).
|
||||
std::string MountLink()
|
||||
{
|
||||
const char* dir = getenv("N64PS5_APP_LINK_DIR");
|
||||
return std::string(dir && *dir ? dir : "/user/app") + "/" N64PS5_TITLE_ID "/mount.lnk";
|
||||
}
|
||||
|
||||
// POST /api/v1/scan to ShadowMountPlus (N64PS5_SMP_PORT replaces 10101 in the host tests). The answer's
|
||||
// status line goes to the log; false when nobody listens.
|
||||
bool AskForScan()
|
||||
{
|
||||
const char* p = getenv("N64PS5_SMP_PORT");
|
||||
const int port = p && *p ? atoi(p) : 10101;
|
||||
const int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
timeval tv = {3, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
sockaddr_in sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
#ifdef __FreeBSD__
|
||||
sa.sin_len = sizeof(sa);
|
||||
#endif
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_port = htons(uint16_t(port));
|
||||
sa.sin_addr.s_addr = htonl(0x7F000001);
|
||||
if (connect(fd, reinterpret_cast<sockaddr*>(&sa), sizeof(sa)) != 0)
|
||||
{
|
||||
close(fd);
|
||||
OrbisLog("[update-job] ShadowMountPlus API (port %d): not there (errno %d)", port, errno);
|
||||
return false;
|
||||
}
|
||||
static const char req[] = "POST /api/v1/scan HTTP/1.0\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\n"
|
||||
"Content-Length: 2\r\nConnection: close\r\n\r\n{}";
|
||||
bool ok = send(fd, req, sizeof(req) - 1, 0) == ssize_t(sizeof(req) - 1);
|
||||
char buf[256] = {};
|
||||
const ssize_t n = ok ? recv(fd, buf, sizeof(buf) - 1, 0) : -1;
|
||||
close(fd);
|
||||
if (n > 0)
|
||||
{
|
||||
buf[n] = 0;
|
||||
buf[strcspn(buf, "\r\n")] = 0;
|
||||
}
|
||||
OrbisLog("[update-job] ShadowMountPlus rescan: %s", n > 0 ? buf : "no answer");
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
// Waits (asking for rescans every 15 s) until the mount link exists == want. False after `seconds`.
|
||||
bool WaitForLink(bool want, int seconds)
|
||||
{
|
||||
const std::string link = MountLink();
|
||||
for (int i = 0; i <= seconds; i++)
|
||||
{
|
||||
if (Exists(link) == want)
|
||||
{
|
||||
OrbisLog("[update-job] mount link %s after %d s", want ? "back" : "gone", i);
|
||||
return true;
|
||||
}
|
||||
if (i % 15 == 0)
|
||||
AskForScan();
|
||||
sleep(1);
|
||||
}
|
||||
OrbisLog("[update-job] mount link still %s after %d s", want ? "missing" : "there", seconds);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcessAlive(int pid)
|
||||
{
|
||||
return pid > 0 && (kill(pid, 0) == 0 || errno != ESRCH);
|
||||
}
|
||||
|
||||
bool RemoveTreeRaw(const std::string& path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path.c_str(), &st) != 0)
|
||||
return errno == ENOENT;
|
||||
if (!S_ISDIR(st.st_mode))
|
||||
return unlink(path.c_str()) == 0;
|
||||
bool ok = true;
|
||||
if (DIR* d = opendir(path.c_str()))
|
||||
{
|
||||
while (dirent* e = readdir(d))
|
||||
{
|
||||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
||||
continue;
|
||||
ok = RemoveTreeRaw(path + "/" + e->d_name) && ok;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
return rmdir(path.c_str()) == 0 && ok;
|
||||
}
|
||||
|
||||
bool Read(Job& job)
|
||||
{
|
||||
FILE* f = fopen(JobPath().c_str(), "r");
|
||||
if (!f)
|
||||
return false;
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
const std::string s = line;
|
||||
if (s.rfind("pid=", 0) == 0)
|
||||
job.pid = atoi(s.c_str() + 4);
|
||||
else if (s.rfind("tag=", 0) == 0)
|
||||
job.tag = s.substr(4);
|
||||
else if (s.rfind("version=", 0) == 0)
|
||||
job.version = s.substr(8);
|
||||
else if (s.rfind("target=", 0) == 0)
|
||||
{
|
||||
const std::string v = s.substr(7);
|
||||
const size_t a = v.find('|'), b = v.find('|', a == std::string::npos ? a : a + 1);
|
||||
if (a == std::string::npos || b == std::string::npos)
|
||||
continue;
|
||||
Target t;
|
||||
t.dir = v.substr(0, a);
|
||||
t.staging = v.substr(a + 1, b - a - 1);
|
||||
t.old = v.substr(b + 1);
|
||||
job.targets.push_back(t);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return !job.targets.empty();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Target TargetFor(const std::string& app_dir)
|
||||
{
|
||||
Target t;
|
||||
t.dir = app_dir;
|
||||
const size_t slash = app_dir.rfind('/');
|
||||
const std::string parent = slash == std::string::npos ? "." : app_dir.substr(0, slash);
|
||||
const std::string name = slash == std::string::npos ? app_dir : app_dir.substr(slash + 1);
|
||||
t.staging = parent + "/" + kUpdateDir + "/new/" + name;
|
||||
t.old = parent + "/" + kUpdateDir + "/old/" + name;
|
||||
return t;
|
||||
}
|
||||
|
||||
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"));
|
||||
const std::string path = JobPath(), tmp = path + ".part";
|
||||
FILE* f = fopen(tmp.c_str(), "w");
|
||||
if (!f)
|
||||
{
|
||||
why = "can't write " + tmp;
|
||||
return false;
|
||||
}
|
||||
fprintf(f, "pid=%d\ntag=%s\nversion=%s\n", job.pid, job.tag.c_str(), job.version.c_str());
|
||||
for (const Target& t : job.targets)
|
||||
fprintf(f, "target=%s|%s|%s\n", t.dir.c_str(), t.staging.c_str(), t.old.c_str());
|
||||
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;
|
||||
}
|
||||
|
||||
int MakeRunnable(const std::string& tree)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(tree.c_str(), &st) != 0 || S_ISLNK(st.st_mode))
|
||||
return 0;
|
||||
int changed = 0;
|
||||
if ((st.st_mode & 07777) != 0777)
|
||||
changed += chmod(tree.c_str(), 0777) == 0;
|
||||
if (S_ISDIR(st.st_mode))
|
||||
if (DIR* d = opendir(tree.c_str()))
|
||||
{
|
||||
while (dirent* e = readdir(d))
|
||||
if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0)
|
||||
changed += MakeRunnable(tree + "/" + e->d_name);
|
||||
closedir(d);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::string EbootMode(const std::string& app_dir)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat((app_dir + "/eboot.bin").c_str(), &st) != 0)
|
||||
return "missing";
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%04o", unsigned(st.st_mode & 07777));
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool RemoveUpdateTree(const std::string& path)
|
||||
{
|
||||
if (path.find(std::string("/") + kUpdateDir + "/") == std::string::npos)
|
||||
return false; // never anything else
|
||||
return RemoveTreeRaw(path);
|
||||
}
|
||||
|
||||
void RunIfPending()
|
||||
{
|
||||
Job job;
|
||||
if (!Read(job))
|
||||
return;
|
||||
// take the job first: a second helper started meanwhile must not run it too
|
||||
const std::string taken = JobPath() + ".running";
|
||||
if (rename(JobPath().c_str(), taken.c_str()) != 0)
|
||||
return;
|
||||
OrbisLog("[update-job] installing %s (%s) for pid %d, %zu folder(s)", job.version.c_str(), job.tag.c_str(),
|
||||
job.pid, job.targets.size());
|
||||
|
||||
// 1. the app closes itself right after starting this payload
|
||||
int waited = 0;
|
||||
while (ProcessAlive(job.pid) && waited < 60)
|
||||
{
|
||||
sleep(1);
|
||||
waited++;
|
||||
}
|
||||
if (ProcessAlive(job.pid))
|
||||
{
|
||||
OrbisLog("[update-job] pid %d still running after 60 s: giving up", job.pid);
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s was not installed (the app didn't close). Open the app to "
|
||||
"try again.",
|
||||
job.version.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
sleep(2); // ShadowMountPlus releases the app's runtime mounts after it exits
|
||||
ProsperoNotify("Mupen64Plus PS5: installing version %s...", job.version.c_str());
|
||||
|
||||
const bool tracked = Exists(MountLink());
|
||||
OrbisLog("[update-job] ShadowMountPlus mount link %s: %s", MountLink().c_str(),
|
||||
tracked ? "present" : "absent (folders are swapped without waiting for it)");
|
||||
|
||||
// 2. the app folders aside
|
||||
std::vector<Target> moved;
|
||||
std::string why;
|
||||
for (const Target& t : job.targets)
|
||||
{
|
||||
if (!Exists(t.staging))
|
||||
{
|
||||
why = "the new version is missing (" + t.staging + ")";
|
||||
break;
|
||||
}
|
||||
const std::string before = EbootMode(t.staging);
|
||||
const int changed = MakeRunnable(t.staging);
|
||||
OrbisLog("[update-job] %s: eboot.bin %s, installed copy %s; new version's permissions set (%d changed): "
|
||||
"eboot.bin now %s",
|
||||
t.dir.c_str(), before.c_str(), EbootMode(t.dir).c_str(), changed, EbootMode(t.staging).c_str());
|
||||
RemoveUpdateTree(t.old);
|
||||
OrbisMkdirs(t.old.substr(0, t.old.rfind('/')));
|
||||
if (rename(t.dir.c_str(), t.old.c_str()) != 0)
|
||||
{
|
||||
why = "can't move " + t.dir + " aside (errno " + std::to_string(errno) + ")";
|
||||
break;
|
||||
}
|
||||
moved.push_back(t);
|
||||
}
|
||||
auto roll_back = [&]() {
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
if (!Exists(t.dir))
|
||||
rename(t.old.c_str(), t.dir.c_str());
|
||||
}
|
||||
};
|
||||
if (!why.empty())
|
||||
{
|
||||
OrbisLog("[update-job] %s: putting everything back", why.c_str());
|
||||
roll_back();
|
||||
if (tracked)
|
||||
AskForScan();
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s failed (%s).", job.version.c_str(), why.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. ShadowMountPlus forgets the old installation
|
||||
if (tracked)
|
||||
WaitForLink(false, 90);
|
||||
|
||||
// 4. the new version in place, installed again
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
if (rename(t.staging.c_str(), t.dir.c_str()) != 0)
|
||||
{
|
||||
why = "can't move the new version into " + t.dir + " (errno " + std::to_string(errno) + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!why.empty())
|
||||
{
|
||||
OrbisLog("[update-job] %s: putting the old version back", why.c_str());
|
||||
for (const Target& t : moved)
|
||||
if (Exists(t.dir) && !Exists(t.staging))
|
||||
rename(t.dir.c_str(), t.staging.c_str()); // the new copy back out of the way
|
||||
roll_back();
|
||||
if (tracked)
|
||||
WaitForLink(true, 120);
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s failed (%s).", job.version.c_str(), why.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
const bool installed = !tracked || WaitForLink(true, 120);
|
||||
|
||||
// 5. tidy up
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
RemoveUpdateTree(t.old);
|
||||
const std::string root = t.old.substr(0, t.old.rfind("/old/"));
|
||||
rmdir((root + "/old").c_str());
|
||||
rmdir((root + "/new").c_str());
|
||||
rmdir(root.c_str());
|
||||
}
|
||||
unlink(taken.c_str());
|
||||
OrbisLog("[update-job] version %s installed%s", job.version.c_str(),
|
||||
installed ? "" : " (ShadowMountPlus hasn't confirmed it yet)");
|
||||
if (installed)
|
||||
ProsperoNotify("Mupen64Plus PS5 updated to %s. Open it from the home screen.", job.version.c_str());
|
||||
else
|
||||
ProsperoNotify("Mupen64Plus PS5 updated to %s. Wait a minute before opening it (or restart the PS5 if it "
|
||||
"doesn't start).",
|
||||
job.version.c_str());
|
||||
}
|
||||
} // namespace updatejob
|
||||
@@ -0,0 +1,70 @@
|
||||
// Mupen64Plus PS5: installing a downloaded update (frontend/fe_update.h) so the console starts it.
|
||||
//
|
||||
// Replacing the files of an installed app in place leaves it unable to start ("Can't start the game or app",
|
||||
// confirmed on the console with 0.6.0 -> 0.6.1 and 0.6.1 -> 0.6.2, whether the app or an FTP client wrote
|
||||
// them): ShadowMountPlus keeps the installation it made from the old files. A fresh copy works because
|
||||
// ShadowMountPlus sees the folder disappear ("source removed", its mount link /user/app/<TITLE>/mount.lnk goes)
|
||||
// and installs it again when it comes back ("Installed NEW!"). The update does the same:
|
||||
// 1. the app unpacks the new version into a staging folder next to the app folder
|
||||
// (<parent>/.mupen64plus-update/new/PPSA99064, too deep for ShadowMountPlus's scan), writes a job file,
|
||||
// starts its helper payload through the ELF loader, and closes;
|
||||
// 2. the helper (RunIfPending) waits for the app to exit, moves the app folder aside (.../old/PPSA99064),
|
||||
// asks ShadowMountPlus to rescan (its HTTP API, 127.0.0.1:10101) until the mount link is gone, moves the
|
||||
// new folder in, rescans until the link is back (installed again), removes the old copy and says so in
|
||||
// a notification. Anything failing puts the old folder back.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace updatejob
|
||||
{
|
||||
struct Target
|
||||
{
|
||||
std::string dir; // the app folder, e.g. /data/homebrew/PPSA99064
|
||||
std::string staging; // the new version, unpacked
|
||||
std::string old; // where the app folder goes while ShadowMountPlus forgets it
|
||||
};
|
||||
|
||||
struct Job
|
||||
{
|
||||
int pid = 0; // the app, which must have exited before the folders move
|
||||
std::string tag; // 00.006.003
|
||||
std::string version; // 0.6.3
|
||||
std::vector<Target> targets;
|
||||
};
|
||||
|
||||
// The staging and old paths for an app folder.
|
||||
Target TargetFor(const std::string& app_dir);
|
||||
|
||||
// /data/mupen64plus/update/job.txt
|
||||
std::string JobPath();
|
||||
bool Write(const Job& job, std::string& why);
|
||||
|
||||
// Deletes a folder tree, only inside a ".mupen64plus-update" folder (staging and old copies).
|
||||
bool RemoveUpdateTree(const std::string& path);
|
||||
|
||||
// Gives every file and folder in a tree the permissions an FTP copy gets (0777). Files a program writes with
|
||||
// fopen() have no execute permission (0666 at most), and the console doesn't start an eboot.bin without it:
|
||||
// that, not the files' contents, is why every in-app update so far left the app unable to start
|
||||
// ("Can't start the game or app", CE-107750-0). The number of entries changed.
|
||||
int MakeRunnable(const std::string& tree);
|
||||
|
||||
// eboot.bin's permissions in a folder, for the logs ("0777", or "missing").
|
||||
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
|
||||
@@ -508,7 +508,13 @@ Rect ComputeRect(int src_w, int src_h, Aspect aspect)
|
||||
break;
|
||||
case Aspect::Integer:
|
||||
{
|
||||
const int k = std::max(1, std::min(kWidth / src_w, kHeight / src_h));
|
||||
const int k = std::min(kWidth / src_w, kHeight / src_h);
|
||||
if (k == 0) // larger than the screen (the GPU renderer upscaled it): as 4:3
|
||||
{
|
||||
dh = kHeight;
|
||||
dw = kHeight * 4 / 3;
|
||||
break;
|
||||
}
|
||||
dw = src_w * k;
|
||||
dh = src_h * k;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
// Mupen64Plus PS5: the Vulkan self-test (ProsperoVulkan.h).
|
||||
//
|
||||
// No Vulkan loader: every command comes from the RADV archive's vk_icdGetInstanceProcAddr, as in
|
||||
// PS5_Vulkan's radv/radv_smoke.c, which this follows. A GPU fault ends the process, so each step is logged
|
||||
// before it runs and the host-memory check, the one that can fault, comes last.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "ProsperoVulkan.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
|
||||
#include "probe_comp.h" // the compute shader (shaders/probe.comp) as SPIR-V, made by glslangValidator
|
||||
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vk_icdGetInstanceProcAddr(VkInstance instance, const char* name);
|
||||
|
||||
namespace ps5vulkan
|
||||
{
|
||||
namespace
|
||||
{
|
||||
#define INSTANCE_COMMANDS(X) \
|
||||
X(DestroyInstance) \
|
||||
X(EnumeratePhysicalDevices) \
|
||||
X(GetPhysicalDeviceProperties2) \
|
||||
X(GetPhysicalDeviceFeatures2) \
|
||||
X(GetPhysicalDeviceQueueFamilyProperties) \
|
||||
X(GetPhysicalDeviceMemoryProperties) \
|
||||
X(EnumerateDeviceExtensionProperties) \
|
||||
X(CreateDevice) \
|
||||
X(GetDeviceProcAddr)
|
||||
|
||||
#define DEVICE_COMMANDS(X) \
|
||||
X(DestroyDevice) \
|
||||
X(GetDeviceQueue) \
|
||||
X(DeviceWaitIdle) \
|
||||
X(CreateBuffer) \
|
||||
X(DestroyBuffer) \
|
||||
X(GetBufferMemoryRequirements) \
|
||||
X(AllocateMemory) \
|
||||
X(FreeMemory) \
|
||||
X(BindBufferMemory) \
|
||||
X(MapMemory) \
|
||||
X(UnmapMemory) \
|
||||
X(CreateShaderModule) \
|
||||
X(DestroyShaderModule) \
|
||||
X(CreateDescriptorSetLayout) \
|
||||
X(DestroyDescriptorSetLayout) \
|
||||
X(CreatePipelineLayout) \
|
||||
X(DestroyPipelineLayout) \
|
||||
X(CreateComputePipelines) \
|
||||
X(DestroyPipeline) \
|
||||
X(CreateDescriptorPool) \
|
||||
X(DestroyDescriptorPool) \
|
||||
X(AllocateDescriptorSets) \
|
||||
X(UpdateDescriptorSets) \
|
||||
X(CreateCommandPool) \
|
||||
X(DestroyCommandPool) \
|
||||
X(AllocateCommandBuffers) \
|
||||
X(BeginCommandBuffer) \
|
||||
X(EndCommandBuffer) \
|
||||
X(ResetCommandBuffer) \
|
||||
X(CmdBindPipeline) \
|
||||
X(CmdBindDescriptorSets) \
|
||||
X(CmdPushConstants) \
|
||||
X(CmdDispatch) \
|
||||
X(CmdPipelineBarrier) \
|
||||
X(CreateFence) \
|
||||
X(DestroyFence) \
|
||||
X(ResetFences) \
|
||||
X(WaitForFences) \
|
||||
X(QueueSubmit)
|
||||
|
||||
#define DECLARE(name) PFN_vk##name vk##name;
|
||||
INSTANCE_COMMANDS(DECLARE)
|
||||
DEVICE_COMMANDS(DECLARE)
|
||||
#undef DECLARE
|
||||
PFN_vkCreateInstance vkCreateInstance;
|
||||
PFN_vkGetMemoryHostPointerPropertiesEXT vkGetMemoryHostPointerPropertiesEXT;
|
||||
|
||||
double Ms(const timespec& from)
|
||||
{
|
||||
timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (now.tv_sec - from.tv_sec) * 1e3 + (now.tv_nsec - from.tv_nsec) / 1e6;
|
||||
}
|
||||
|
||||
timespec Now()
|
||||
{
|
||||
timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return t;
|
||||
}
|
||||
|
||||
bool HasExtension(const std::vector<VkExtensionProperties>& list, const char* name)
|
||||
{
|
||||
for (const VkExtensionProperties& e : list)
|
||||
if (strcmp(e.extensionName, name) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* Yes(bool b)
|
||||
{
|
||||
return b ? "yes" : "NO";
|
||||
}
|
||||
|
||||
struct Context
|
||||
{
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice physical = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
uint32_t family = UINT32_MAX;
|
||||
VkPhysicalDeviceMemoryProperties memory = {};
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout set_layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descriptors = VK_NULL_HANDLE;
|
||||
bool host_memory_ext = false;
|
||||
VkDeviceSize host_alignment = 0;
|
||||
};
|
||||
|
||||
uint32_t MemoryType(const Context& c, uint32_t bits, VkMemoryPropertyFlags wanted)
|
||||
{
|
||||
for (uint32_t i = 0; i < c.memory.memoryTypeCount; i++)
|
||||
if ((bits & (1u << i)) && (c.memory.memoryTypes[i].propertyFlags & wanted) == wanted)
|
||||
return i;
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
bool LoadInstance(VkInstance instance)
|
||||
{
|
||||
bool ok = true;
|
||||
#define LOAD(name) \
|
||||
vk##name = (PFN_vk##name)vk_icdGetInstanceProcAddr(instance, "vk" #name); \
|
||||
if (!vk##name) \
|
||||
{ \
|
||||
OrbisLog("[vulkan] missing vk" #name); \
|
||||
ok = false; \
|
||||
}
|
||||
INSTANCE_COMMANDS(LOAD)
|
||||
#undef LOAD
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool LoadDevice(VkDevice device)
|
||||
{
|
||||
bool ok = true;
|
||||
#define LOAD(name) \
|
||||
vk##name = (PFN_vk##name)vkGetDeviceProcAddr(device, "vk" #name); \
|
||||
if (!vk##name) \
|
||||
{ \
|
||||
OrbisLog("[vulkan] missing vk" #name); \
|
||||
ok = false; \
|
||||
}
|
||||
DEVICE_COMMANDS(LOAD)
|
||||
#undef LOAD
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The instance, the GPU, a device with one graphics+compute queue; logs what ParaLLEl-RDP looks for.
|
||||
bool CreateDevice(Context& c, ProbeResult& r)
|
||||
{
|
||||
vkCreateInstance = (PFN_vkCreateInstance)vk_icdGetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateInstance");
|
||||
if (!vkCreateInstance)
|
||||
{
|
||||
OrbisLog("[vulkan] the driver has no vkCreateInstance");
|
||||
return false;
|
||||
}
|
||||
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.pApplicationName = "Mupen64Plus PS5";
|
||||
app.apiVersion = VK_API_VERSION_1_1; // what ParaLLEl-RDP asks for
|
||||
VkInstanceCreateInfo instance_info = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
instance_info.pApplicationInfo = &app;
|
||||
VkResult res = vkCreateInstance(&instance_info, nullptr, &c.instance);
|
||||
OrbisLog("[vulkan] vkCreateInstance -> %d", int(res));
|
||||
if (res != VK_SUCCESS || !LoadInstance(c.instance))
|
||||
return false;
|
||||
|
||||
uint32_t count = 1;
|
||||
res = vkEnumeratePhysicalDevices(c.instance, &count, &c.physical);
|
||||
if ((res != VK_SUCCESS && res != VK_INCOMPLETE) || count == 0)
|
||||
{
|
||||
OrbisLog("[vulkan] no GPU (vkEnumeratePhysicalDevices -> %d, %u)", int(res), count);
|
||||
return false;
|
||||
}
|
||||
|
||||
VkPhysicalDeviceExternalMemoryHostPropertiesEXT host_props = {
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceSubgroupProperties subgroup = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
subgroup.pNext = &host_props;
|
||||
VkPhysicalDeviceDriverProperties driver = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
|
||||
driver.pNext = &subgroup;
|
||||
VkPhysicalDeviceProperties2 props = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
||||
props.pNext = &driver;
|
||||
vkGetPhysicalDeviceProperties2(c.physical, &props);
|
||||
const uint32_t api = props.properties.apiVersion;
|
||||
char gpu[256];
|
||||
snprintf(gpu, sizeof(gpu), "%s (%s %s), Vulkan %u.%u.%u", props.properties.deviceName, driver.driverName,
|
||||
driver.driverInfo, VK_API_VERSION_MAJOR(api), VK_API_VERSION_MINOR(api), VK_API_VERSION_PATCH(api));
|
||||
r.gpu = gpu;
|
||||
OrbisLog("[vulkan] GPU: %s", gpu);
|
||||
OrbisLog("[vulkan] subgroups: size %u, stages 0x%x, operations 0x%x (ballot %s, arithmetic %s, shuffle %s)",
|
||||
subgroup.subgroupSize, unsigned(subgroup.supportedStages), unsigned(subgroup.supportedOperations),
|
||||
Yes(subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT),
|
||||
Yes(subgroup.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT),
|
||||
Yes(subgroup.supportedOperations & VK_SUBGROUP_FEATURE_SHUFFLE_BIT));
|
||||
OrbisLog("[vulkan] limits: max compute workgroup %u invocations, storage buffer range %u MiB, "
|
||||
"max image 2D %u", props.properties.limits.maxComputeWorkGroupInvocations,
|
||||
props.properties.limits.maxStorageBufferRange >> 20, props.properties.limits.maxImageDimension2D);
|
||||
|
||||
vkGetPhysicalDeviceMemoryProperties(c.physical, &c.memory);
|
||||
for (uint32_t i = 0; i < c.memory.memoryHeapCount; i++)
|
||||
OrbisLog("[vulkan] memory heap %u: %llu MiB, flags 0x%x", i,
|
||||
(unsigned long long)(c.memory.memoryHeaps[i].size >> 20), unsigned(c.memory.memoryHeaps[i].flags));
|
||||
for (uint32_t i = 0; i < c.memory.memoryTypeCount; i++)
|
||||
OrbisLog("[vulkan] memory type %u: heap %u, flags 0x%x", i, c.memory.memoryTypes[i].heapIndex,
|
||||
unsigned(c.memory.memoryTypes[i].propertyFlags));
|
||||
|
||||
count = 0;
|
||||
vkEnumerateDeviceExtensionProperties(c.physical, nullptr, &count, nullptr);
|
||||
std::vector<VkExtensionProperties> extensions(count);
|
||||
vkEnumerateDeviceExtensionProperties(c.physical, nullptr, &count, extensions.data());
|
||||
extensions.resize(count);
|
||||
c.host_memory_ext = HasExtension(extensions, VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
|
||||
c.host_alignment = host_props.minImportedHostPointerAlignment;
|
||||
OrbisLog("[vulkan] %u device extensions; VK_EXT_external_memory_host %s (alignment %llu), "
|
||||
"VK_KHR_8bit_storage %s, VK_KHR_16bit_storage %s, VK_KHR_timeline_semaphore %s, VK_KHR_swapchain %s",
|
||||
count, Yes(c.host_memory_ext), (unsigned long long)c.host_alignment,
|
||||
Yes(HasExtension(extensions, VK_KHR_8BIT_STORAGE_EXTENSION_NAME)),
|
||||
Yes(HasExtension(extensions, VK_KHR_16BIT_STORAGE_EXTENSION_NAME)),
|
||||
Yes(HasExtension(extensions, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME)),
|
||||
Yes(HasExtension(extensions, VK_KHR_SWAPCHAIN_EXTENSION_NAME)));
|
||||
|
||||
// The features ParaLLEl-RDP's shaders use (8/16-bit storage and arithmetic, timeline semaphores for its
|
||||
// frame pacing, storage images written without a format).
|
||||
VkPhysicalDeviceVulkan12Features f12 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
|
||||
VkPhysicalDeviceVulkan11Features f11 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES};
|
||||
f11.pNext = &f12;
|
||||
VkPhysicalDeviceFeatures2 features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
|
||||
features.pNext = &f11;
|
||||
vkGetPhysicalDeviceFeatures2(c.physical, &features);
|
||||
OrbisLog("[vulkan] features: storageBuffer8BitAccess %s, storageBuffer16BitAccess %s, shaderInt8 %s, "
|
||||
"shaderInt16 %s, shaderFloat16 %s, timelineSemaphore %s, shaderStorageImageWriteWithoutFormat %s",
|
||||
Yes(f12.storageBuffer8BitAccess), Yes(f11.storageBuffer16BitAccess), Yes(f12.shaderInt8),
|
||||
Yes(features.features.shaderInt16), Yes(f12.shaderFloat16), Yes(f12.timelineSemaphore),
|
||||
Yes(features.features.shaderStorageImageWriteWithoutFormat));
|
||||
r.features = f12.storageBuffer8BitAccess && f11.storageBuffer16BitAccess && f12.shaderInt8 &&
|
||||
features.features.shaderInt16 && f12.timelineSemaphore &&
|
||||
(subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) && c.host_memory_ext;
|
||||
|
||||
VkQueueFamilyProperties families[8];
|
||||
count = 8;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(c.physical, &count, families);
|
||||
for (uint32_t i = 0; i < count; i++)
|
||||
{
|
||||
OrbisLog("[vulkan] queue family %u: %u queues, flags 0x%x", i, families[i].queueCount,
|
||||
unsigned(families[i].queueFlags));
|
||||
const VkQueueFlags want = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT;
|
||||
if (c.family == UINT32_MAX && (families[i].queueFlags & want) == want)
|
||||
c.family = i;
|
||||
}
|
||||
if (c.family == UINT32_MAX)
|
||||
{
|
||||
OrbisLog("[vulkan] no graphics+compute queue");
|
||||
return false;
|
||||
}
|
||||
|
||||
const float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queue_info = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
queue_info.queueFamilyIndex = c.family;
|
||||
queue_info.queueCount = 1;
|
||||
queue_info.pQueuePriorities = &priority;
|
||||
const char* enable[2];
|
||||
uint32_t enabled = 0;
|
||||
if (c.host_memory_ext)
|
||||
{
|
||||
enable[enabled++] = VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME;
|
||||
enable[enabled++] = VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME;
|
||||
}
|
||||
VkDeviceCreateInfo device_info = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
device_info.queueCreateInfoCount = 1;
|
||||
device_info.pQueueCreateInfos = &queue_info;
|
||||
device_info.enabledExtensionCount = enabled;
|
||||
device_info.ppEnabledExtensionNames = enable;
|
||||
res = vkCreateDevice(c.physical, &device_info, nullptr, &c.device);
|
||||
OrbisLog("[vulkan] vkCreateDevice -> %d", int(res));
|
||||
if (res != VK_SUCCESS || !LoadDevice(c.device))
|
||||
return false;
|
||||
vkGetMemoryHostPointerPropertiesEXT =
|
||||
(PFN_vkGetMemoryHostPointerPropertiesEXT)vkGetDeviceProcAddr(c.device, "vkGetMemoryHostPointerPropertiesEXT");
|
||||
vkGetDeviceQueue(c.device, c.family, 0, &c.queue);
|
||||
|
||||
VkCommandPoolCreateInfo pool_info = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool_info.queueFamilyIndex = c.family;
|
||||
VkCommandBufferAllocateInfo cmd_info = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
cmd_info.commandBufferCount = 1;
|
||||
cmd_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
VkFenceCreateInfo fence_info = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
if (vkCreateCommandPool(c.device, &pool_info, nullptr, &c.pool) != VK_SUCCESS)
|
||||
return false;
|
||||
cmd_info.commandPool = c.pool;
|
||||
return vkAllocateCommandBuffers(c.device, &cmd_info, &c.cmd) == VK_SUCCESS &&
|
||||
vkCreateFence(c.device, &fence_info, nullptr, &c.fence) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
// The compute pipeline: one storage buffer, v[i] = i * 2654435761 + seed (shaders/probe.comp).
|
||||
bool CreatePipeline(Context& c)
|
||||
{
|
||||
VkShaderModuleCreateInfo module_info = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
module_info.codeSize = sizeof(kProbeComp);
|
||||
module_info.pCode = kProbeComp;
|
||||
VkShaderModule module = VK_NULL_HANDLE;
|
||||
if (vkCreateShaderModule(c.device, &module_info, nullptr, &module) != VK_SUCCESS)
|
||||
return false;
|
||||
|
||||
VkDescriptorSetLayoutBinding binding = {};
|
||||
binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
binding.descriptorCount = 1;
|
||||
binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
VkDescriptorSetLayoutCreateInfo set_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
set_info.bindingCount = 1;
|
||||
set_info.pBindings = &binding;
|
||||
VkPushConstantRange push = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 4};
|
||||
VkPipelineLayoutCreateInfo layout_info = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
layout_info.setLayoutCount = 1;
|
||||
layout_info.pSetLayouts = &c.set_layout;
|
||||
layout_info.pushConstantRangeCount = 1;
|
||||
layout_info.pPushConstantRanges = &push;
|
||||
VkComputePipelineCreateInfo pipeline_info = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
pipeline_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
pipeline_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
pipeline_info.stage.module = module;
|
||||
pipeline_info.stage.pName = "main";
|
||||
VkDescriptorPoolSize size = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4};
|
||||
VkDescriptorPoolCreateInfo pool_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool_info.maxSets = 4;
|
||||
pool_info.poolSizeCount = 1;
|
||||
pool_info.pPoolSizes = &size;
|
||||
|
||||
const timespec start = Now();
|
||||
bool ok = vkCreateDescriptorSetLayout(c.device, &set_info, nullptr, &c.set_layout) == VK_SUCCESS &&
|
||||
vkCreatePipelineLayout(c.device, &layout_info, nullptr, &c.layout) == VK_SUCCESS;
|
||||
pipeline_info.layout = c.layout;
|
||||
ok = ok && vkCreateComputePipelines(c.device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &c.pipeline) == VK_SUCCESS;
|
||||
OrbisLog("[vulkan] compute pipeline: %s, compiled in %.1f ms", ok ? "created" : "FAILED", Ms(start));
|
||||
vkDestroyShaderModule(c.device, module, nullptr);
|
||||
return ok && vkCreateDescriptorPool(c.device, &pool_info, nullptr, &c.descriptors) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
// Runs the shader over `words` words of `buffer`, waits for it, and returns how long the GPU part took.
|
||||
bool Dispatch(Context& c, VkBuffer buffer, uint32_t words, uint32_t seed, double* ms)
|
||||
{
|
||||
VkDescriptorSetAllocateInfo set_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
set_info.descriptorPool = c.descriptors;
|
||||
set_info.descriptorSetCount = 1;
|
||||
set_info.pSetLayouts = &c.set_layout;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
if (vkAllocateDescriptorSets(c.device, &set_info, &set) != VK_SUCCESS)
|
||||
return false;
|
||||
VkDescriptorBufferInfo buffer_info = {buffer, 0, VkDeviceSize(words) * 4};
|
||||
VkWriteDescriptorSet write = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = set;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
write.pBufferInfo = &buffer_info;
|
||||
vkUpdateDescriptorSets(c.device, 1, &write, 0, nullptr);
|
||||
|
||||
VkCommandBufferBeginInfo begin = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
vkResetCommandBuffer(c.cmd, 0);
|
||||
vkBeginCommandBuffer(c.cmd, &begin);
|
||||
vkCmdBindPipeline(c.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, c.pipeline);
|
||||
vkCmdBindDescriptorSets(c.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, c.layout, 0, 1, &set, 0, nullptr);
|
||||
vkCmdPushConstants(c.cmd, c.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, 4, &seed);
|
||||
vkCmdDispatch(c.cmd, words / 64, 1, 1);
|
||||
VkMemoryBarrier barrier = {VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(c.cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0,
|
||||
nullptr, 0, nullptr);
|
||||
vkEndCommandBuffer(c.cmd);
|
||||
|
||||
VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &c.cmd;
|
||||
vkResetFences(c.device, 1, &c.fence);
|
||||
const timespec start = Now();
|
||||
VkResult res = vkQueueSubmit(c.queue, 1, &submit, c.fence);
|
||||
if (res == VK_SUCCESS)
|
||||
res = vkWaitForFences(c.device, 1, &c.fence, VK_TRUE, 2000000000ull);
|
||||
*ms = Ms(start);
|
||||
if (res != VK_SUCCESS)
|
||||
OrbisLog("[vulkan] submit/wait -> %d after %.1f ms", int(res), *ms);
|
||||
return res == VK_SUCCESS;
|
||||
}
|
||||
|
||||
uint32_t Wrong(const volatile uint32_t* v, uint32_t words, uint32_t seed)
|
||||
{
|
||||
uint32_t wrong = 0;
|
||||
for (uint32_t i = 0; i < words; i++)
|
||||
wrong += v[i] != i * 2654435761u + seed;
|
||||
return wrong;
|
||||
}
|
||||
|
||||
// A host-visible buffer the driver allocates, written by the shader, read back by the CPU.
|
||||
bool TestCompute(Context& c)
|
||||
{
|
||||
const uint32_t words = 1u << 20; // 4 MiB, the size of the N64's RDRAM
|
||||
VkBufferCreateInfo info = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = VkDeviceSize(words) * 4;
|
||||
info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
if (vkCreateBuffer(c.device, &info, nullptr, &buffer) != VK_SUCCESS)
|
||||
return false;
|
||||
VkMemoryRequirements req;
|
||||
vkGetBufferMemoryRequirements(c.device, buffer, &req);
|
||||
VkMemoryAllocateInfo alloc = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
alloc.allocationSize = req.size;
|
||||
alloc.memoryTypeIndex = MemoryType(c, req.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
void* map = nullptr;
|
||||
bool ok = alloc.memoryTypeIndex != UINT32_MAX &&
|
||||
vkAllocateMemory(c.device, &alloc, nullptr, &memory) == VK_SUCCESS &&
|
||||
vkBindBufferMemory(c.device, buffer, memory, 0) == VK_SUCCESS &&
|
||||
vkMapMemory(c.device, memory, 0, VK_WHOLE_SIZE, 0, &map) == VK_SUCCESS;
|
||||
double ms = 0;
|
||||
uint32_t wrong = words;
|
||||
if (ok)
|
||||
{
|
||||
memset(map, 0, info.size);
|
||||
OrbisLog("[vulkan] compute: running the shader over %u words", words);
|
||||
ok = Dispatch(c, buffer, words, 0x1234u, &ms);
|
||||
if (ok)
|
||||
wrong = Wrong(static_cast<const uint32_t*>(map), words, 0x1234u);
|
||||
}
|
||||
OrbisLog("[vulkan] compute: %s (%u of %u words wrong, %.2f ms)", ok && wrong == 0 ? "passed" : "FAILED", wrong,
|
||||
words, ms);
|
||||
if (map)
|
||||
vkUnmapMemory(c.device, memory);
|
||||
vkDestroyBuffer(c.device, buffer, nullptr);
|
||||
if (memory)
|
||||
vkFreeMemory(c.device, memory, nullptr);
|
||||
return ok && wrong == 0;
|
||||
}
|
||||
|
||||
// Memory the app allocated the way mupen64plus-core allocates RDRAM (posix_memalign), imported into Vulkan
|
||||
// (VK_EXT_external_memory_host) and written by the shader: ParaLLEl-RDP works on RDRAM in place this way.
|
||||
bool TestHostMemory(Context& c)
|
||||
{
|
||||
if (!c.host_memory_ext || !vkGetMemoryHostPointerPropertiesEXT)
|
||||
{
|
||||
OrbisLog("[vulkan] host memory: VK_EXT_external_memory_host missing, skipped");
|
||||
return false;
|
||||
}
|
||||
const uint32_t words = 1u << 20;
|
||||
const size_t bytes = size_t(words) * 4;
|
||||
const size_t align = c.host_alignment > 4096 ? size_t(c.host_alignment) : 4096;
|
||||
void* host = nullptr;
|
||||
if (posix_memalign(&host, align, bytes) != 0)
|
||||
{
|
||||
OrbisLog("[vulkan] host memory: posix_memalign(%zu, %zu) failed", align, bytes);
|
||||
return false;
|
||||
}
|
||||
memset(host, 0, bytes);
|
||||
VkMemoryHostPointerPropertiesEXT host_props = {VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
|
||||
VkResult res = vkGetMemoryHostPointerPropertiesEXT(c.device,
|
||||
VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &host_props);
|
||||
OrbisLog("[vulkan] host memory: %p (%zu bytes), vkGetMemoryHostPointerPropertiesEXT -> %d, types 0x%x", host,
|
||||
bytes, int(res), unsigned(host_props.memoryTypeBits));
|
||||
|
||||
VkExternalMemoryBufferCreateInfo external = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
|
||||
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT;
|
||||
VkBufferCreateInfo info = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.pNext = &external;
|
||||
info.size = bytes;
|
||||
info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
bool ok = res == VK_SUCCESS && host_props.memoryTypeBits != 0 &&
|
||||
vkCreateBuffer(c.device, &info, nullptr, &buffer) == VK_SUCCESS;
|
||||
if (ok)
|
||||
{
|
||||
VkMemoryRequirements req;
|
||||
vkGetBufferMemoryRequirements(c.device, buffer, &req);
|
||||
VkImportMemoryHostPointerInfoEXT import = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT};
|
||||
import.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT;
|
||||
import.pHostPointer = host;
|
||||
VkMemoryAllocateInfo alloc = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
alloc.pNext = &import;
|
||||
alloc.allocationSize = bytes;
|
||||
alloc.memoryTypeIndex = MemoryType(c, req.memoryTypeBits & host_props.memoryTypeBits, 0);
|
||||
res = alloc.memoryTypeIndex == UINT32_MAX ? VK_ERROR_UNKNOWN :
|
||||
vkAllocateMemory(c.device, &alloc, nullptr, &memory);
|
||||
OrbisLog("[vulkan] host memory: import (type %u) -> %d", alloc.memoryTypeIndex, int(res));
|
||||
ok = res == VK_SUCCESS && vkBindBufferMemory(c.device, buffer, memory, 0) == VK_SUCCESS;
|
||||
}
|
||||
double ms = 0;
|
||||
uint32_t wrong = words;
|
||||
if (ok)
|
||||
{
|
||||
OrbisLog("[vulkan] host memory: the shader writes it (a GPU fault would end the app here)");
|
||||
ok = Dispatch(c, buffer, words, 0x5a5au, &ms);
|
||||
if (ok)
|
||||
wrong = Wrong(static_cast<const uint32_t*>(host), words, 0x5a5au);
|
||||
}
|
||||
OrbisLog("[vulkan] host memory: %s (%u of %u words wrong, %.2f ms)", ok && wrong == 0 ? "passed" : "FAILED",
|
||||
wrong, words, ms);
|
||||
if (buffer)
|
||||
vkDestroyBuffer(c.device, buffer, nullptr);
|
||||
if (memory)
|
||||
vkFreeMemory(c.device, memory, nullptr);
|
||||
free(host);
|
||||
return ok && wrong == 0;
|
||||
}
|
||||
|
||||
void Destroy(Context& c)
|
||||
{
|
||||
if (c.device)
|
||||
{
|
||||
vkDeviceWaitIdle(c.device);
|
||||
if (c.descriptors)
|
||||
vkDestroyDescriptorPool(c.device, c.descriptors, nullptr);
|
||||
if (c.pipeline)
|
||||
vkDestroyPipeline(c.device, c.pipeline, nullptr);
|
||||
if (c.layout)
|
||||
vkDestroyPipelineLayout(c.device, c.layout, nullptr);
|
||||
if (c.set_layout)
|
||||
vkDestroyDescriptorSetLayout(c.device, c.set_layout, nullptr);
|
||||
if (c.fence)
|
||||
vkDestroyFence(c.device, c.fence, nullptr);
|
||||
if (c.pool)
|
||||
vkDestroyCommandPool(c.device, c.pool, nullptr);
|
||||
vkDestroyDevice(c.device, nullptr);
|
||||
}
|
||||
if (c.instance)
|
||||
vkDestroyInstance(c.instance, nullptr);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void CaptureDriverMessages()
|
||||
{
|
||||
const std::string path = OrbisRoot() + "/logs/vulkan.log";
|
||||
if (freopen(path.c_str(), "w", stderr))
|
||||
{
|
||||
setvbuf(stderr, nullptr, _IOLBF, 0);
|
||||
OrbisLog("[vulkan] driver messages (standard error) go to %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ProbeResult Probe()
|
||||
{
|
||||
ProbeResult r;
|
||||
const timespec start = Now();
|
||||
OrbisLog("[vulkan] self-test starts");
|
||||
Context c;
|
||||
r.device = CreateDevice(c, r);
|
||||
OrbisLog("[vulkan] device %s in %.1f ms", r.device ? "ready" : "FAILED", Ms(start));
|
||||
if (r.device && CreatePipeline(c))
|
||||
{
|
||||
r.compute = TestCompute(c);
|
||||
if (r.compute)
|
||||
r.host_memory = TestHostMemory(c);
|
||||
}
|
||||
Destroy(c);
|
||||
OrbisLog("[vulkan] self-test ends in %.1f ms: device %s, compute %s, RDRAM sharing %s, features %s",
|
||||
Ms(start), Yes(r.device), Yes(r.compute), Yes(r.host_memory), Yes(r.features));
|
||||
|
||||
auto line = [](const char* what, bool ok) { return std::string(what) + (ok ? ": passed\n" : ": FAILED\n"); };
|
||||
r.summary = (r.gpu.empty() ? std::string("GPU: not found\n") : "GPU: " + r.gpu + "\n") +
|
||||
line("Vulkan device", r.device) + line("Compute shader", r.compute) +
|
||||
line("GPU access to N64 memory", r.host_memory) + line("Features ParaLLEl-RDP needs", r.features);
|
||||
return r;
|
||||
}
|
||||
} // namespace ps5vulkan
|
||||
@@ -0,0 +1,34 @@
|
||||
// Mupen64Plus PS5: the Vulkan self-test (GitHub issue #1, internal resolution: the first step towards a GPU
|
||||
// renderer, ParaLLEl-RDP).
|
||||
//
|
||||
// Built only with `make VULKAN=1`: the app then links RADV, Mesa's Vulkan driver for AMD GPUs, as
|
||||
// mihawk-99/PS5_Vulkan builds it for the console (tools/build-radv.sh release), and runs Probe() once the
|
||||
// video is up. The probe asks for what ParaLLEl-RDP needs and logs each answer ("[vulkan] ..." in boot.log):
|
||||
// - an instance, the GPU and a device with a compute queue;
|
||||
// - a compute shader run over a buffer and read back on the CPU;
|
||||
// - the GPU writing memory the app allocated itself (VK_EXT_external_memory_host): ParaLLEl-RDP reads and
|
||||
// writes the N64's RDRAM in place this way;
|
||||
// - the features its shaders use (8- and 16-bit storage, subgroup operations, timeline semaphores).
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps5vulkan
|
||||
{
|
||||
struct ProbeResult
|
||||
{
|
||||
bool device = false; // instance and device created
|
||||
bool compute = false; // the compute shader's output read back right
|
||||
bool host_memory = false; // the GPU wrote memory the app allocated (RDRAM in place)
|
||||
bool features = false; // every feature ParaLLEl-RDP needs is there
|
||||
std::string gpu; // "AMD ... (RADV ...), Vulkan 1.4.x"
|
||||
std::string summary; // a few lines for the screen
|
||||
};
|
||||
|
||||
ProbeResult Probe();
|
||||
|
||||
// Standard error, where RADV and paraLLEl-RDP's worker threads write their messages, to logs/vulkan.log.
|
||||
void CaptureDriverMessages();
|
||||
} // namespace ps5vulkan
|
||||
@@ -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()
|
||||
{
|
||||
@@ -117,9 +150,16 @@ void OrbisLogOpen(const char* name)
|
||||
const std::string dir = OrbisDir("logs");
|
||||
if (!OrbisIsDir(dir))
|
||||
return;
|
||||
const std::string cur = dir + "/" + name + ".log";
|
||||
const std::string prev = dir + "/" + name + ".prev.log";
|
||||
rename(cur.c_str(), prev.c_str()); // keep the run before, like PS5SX2's logs
|
||||
// keep the 4 runs before (like PS5SX2's logs): <name>.prev.log is the last one, then .prev2 .. .prev4, so
|
||||
// opening the app once or twice to fetch a log doesn't lose the session that had the problem
|
||||
const std::string base = dir + "/" + name;
|
||||
const std::string cur = base + ".log";
|
||||
auto prev = [&](int n) { return base + (n == 1 ? ".prev.log" : ".prev" + std::to_string(n) + ".log"); };
|
||||
const int kKeep = 4;
|
||||
unlink(prev(kKeep).c_str());
|
||||
for (int n = kKeep - 1; n >= 1; n--)
|
||||
rename(prev(n).c_str(), prev(n + 1).c_str());
|
||||
rename(cur.c_str(), prev(1).c_str());
|
||||
s_log = fopen(cur.c_str(), "w");
|
||||
if (s_log)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Mupen64Plus PS5: the Vulkan self-test's compute shader (ProsperoVulkan.cpp): v[i] = i * 2654435761 + seed.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#version 450
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, set = 0, binding = 0) buffer Data { uint v[]; };
|
||||
layout(push_constant) uniform Push { uint seed; };
|
||||
|
||||
void main()
|
||||
{
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
v[i] = i * 2654435761u + seed;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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::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,34 @@
|
||||
// Mupen64Plus PS5 frontend: cheats -- the GameShark codes of mupen64plus-core's data/mupencheat.txt (built into
|
||||
// eboot.bin, compressed), 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
|
||||
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);
|
||||
|
||||
// "Play As\Player 1" -> "Play As: Player 1"
|
||||
std::string DisplayName(const std::string& name);
|
||||
} // namespace cheats
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,299 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
} // namespace gfx
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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,
|
||||
};
|
||||
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color);
|
||||
} // namespace gfx
|
||||
} // namespace fe
|
||||
+322
-22
@@ -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()
|
||||
{
|
||||
@@ -206,9 +220,24 @@ struct State
|
||||
bool limiter_on = true;
|
||||
emu::ExitReason exit = emu::ExitReason::BackToList;
|
||||
uint32_t vi = 0;
|
||||
uint32_t blank_vis = 0; // VIs in a row without a picture (n64ps5_on_vi)
|
||||
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 database's for this ROM, with what the player chose
|
||||
std::vector<fe::cheats::Cheat> cheats;
|
||||
std::string cheats_game;
|
||||
// "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;
|
||||
@@ -271,7 +300,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");
|
||||
}
|
||||
}
|
||||
@@ -333,10 +369,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));
|
||||
@@ -370,8 +407,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
|
||||
@@ -443,6 +483,88 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 unique: "<index>:<name>".
|
||||
void ApplyCheat(int index)
|
||||
{
|
||||
const fe::cheats::Cheat& c = g.cheats[size_t(index)];
|
||||
const std::string id = std::to_string(index) + ":" + c.name;
|
||||
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)
|
||||
@@ -462,6 +584,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");
|
||||
@@ -479,8 +626,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");
|
||||
}
|
||||
@@ -528,6 +675,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);
|
||||
|
||||
@@ -538,15 +699,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();
|
||||
@@ -555,8 +724,25 @@ 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
|
||||
// black there, but the last picture would stay on screen and look like a hang. Short blanks (between
|
||||
// video modes) stay invisible.
|
||||
const uint32_t kBlankVis = 10;
|
||||
if (frame)
|
||||
{
|
||||
if (g.blank_vis >= kBlankVis)
|
||||
OrbisLog("[emu] screen blank for %u VIs (%.1f s), shown black", g.blank_vis,
|
||||
g.blank_vis / (g.pal ? 50.0 : 60.0));
|
||||
g.blank_vis = 0;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================================================
|
||||
@@ -632,8 +818,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;
|
||||
@@ -669,10 +856,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;
|
||||
@@ -690,20 +878,60 @@ 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 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 = fe::cheats::ForGame(__builtin_bswap32(header.CRC1), __builtin_bswap32(header.CRC2),
|
||||
header.Country_code, &g.cheats_game);
|
||||
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};
|
||||
const n64ps5_gfx_options gfx = {cfg.vi_filter ? 0 : 1, cfg.hide_overscan, cfg.render_threads, cfg.dp_compat,
|
||||
cfg.upscale, cfg.gpu_sync};
|
||||
n64ps5_gfx_set_options(&gfx);
|
||||
// the renderer: paraLLEl-RDP on the GPU when the setting asks for it and Vulkan works, else angrylion
|
||||
const m64ps5_library* gfx_lib = &m64ps5_gfx_lib;
|
||||
#ifdef N64PS5_VULKAN
|
||||
if (cfg.gpu)
|
||||
{
|
||||
if (n64ps5_gpu_available())
|
||||
{
|
||||
n64ps5_gpu_set_options(&gfx);
|
||||
gfx_lib = &m64ps5_gfx_parallel_lib;
|
||||
}
|
||||
else
|
||||
emu::Osd("The GPU renderer is not available: using the CPU renderer");
|
||||
}
|
||||
#endif
|
||||
OrbisLog("[emu] renderer: %s", gfx_lib == &m64ps5_gfx_lib ? "angrylion (CPU)" : "paraLLEl-RDP (GPU)");
|
||||
ApplySettings();
|
||||
|
||||
static const char* const kNames[] = {"video", "audio", "input", "rsp"};
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
r = CoreAttachPlugin(kPluginTypes[i], M64PS5_HANDLE(*kPlugins[i]));
|
||||
r = CoreAttachPlugin(kPluginTypes[i], M64PS5_HANDLE(i == 0 ? *gfx_lib : *kPlugins[i]));
|
||||
if (r != M64ERR_SUCCESS)
|
||||
{
|
||||
OrbisLog("[emu] CoreAttachPlugin(%s) -> %d (%s)", kNames[i], int(r), CoreErrorMessage(r));
|
||||
@@ -719,6 +947,7 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
g.stopping = false;
|
||||
g.exit = ExitReason::BackToList;
|
||||
g.vi = 0;
|
||||
g.blank_vis = 0;
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
g.block_until_release = true; // Cross from the shelf
|
||||
n64ps5_input_block(true);
|
||||
@@ -726,6 +955,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();
|
||||
@@ -746,7 +979,11 @@ 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;
|
||||
N64_STAGE(Emu, "idle");
|
||||
if (r != M64ERR_SUCCESS && g.vi == 0)
|
||||
{
|
||||
@@ -756,11 +993,68 @@ 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());
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -770,8 +1064,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();
|
||||
}
|
||||
|
||||
+19
-2
@@ -2,6 +2,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "fe_cheats.h"
|
||||
#include "fe_settings.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace emu
|
||||
@@ -16,10 +19,24 @@ 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; empty when the database has none for it), and switching one
|
||||
// (pause menu): applied at once and 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 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).
|
||||
|
||||
+917
-127
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
|
||||
@@ -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;
|
||||
@@ -59,16 +149,41 @@ void Settings::Load()
|
||||
render_threads = Clamp(atoi(val), 1, 12);
|
||||
else if (key == "dynarec")
|
||||
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")
|
||||
{
|
||||
const int u = atoi(val);
|
||||
upscale = u >= 8 ? 8 : u >= 4 ? 4 : u >= 2 ? 2 : 1;
|
||||
}
|
||||
else if (key == "gpu_sync")
|
||||
gpu_sync = atoi(val) != 0;
|
||||
else if (key == "dp_compat")
|
||||
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")
|
||||
@@ -99,11 +214,22 @@ void Settings::Save() const
|
||||
fprintf(f, "show_fps=%d\n", show_fps ? 1 : 0);
|
||||
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
|
||||
@@ -16,14 +35,25 @@ struct Settings
|
||||
bool show_fps = false;
|
||||
// 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
|
||||
|
||||
+51
-71
@@ -8,7 +8,9 @@
|
||||
#include "fe_menu.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoUpdateJob.h"
|
||||
#include "ProsperoSce.h"
|
||||
|
||||
#include "unzip.h"
|
||||
@@ -32,6 +34,9 @@
|
||||
#ifndef N64PS5_TITLE_ID
|
||||
#define N64PS5_TITLE_ID "PPSA99064"
|
||||
#endif
|
||||
#ifndef N64PS5_RELEASE_ZIP
|
||||
#define N64PS5_RELEASE_ZIP "Mupen64PlusPS5.zip"
|
||||
#endif
|
||||
#ifndef N64PS5_GITHUB_REPO
|
||||
#define N64PS5_GITHUB_REPO "TheRealRetro/mupen64plus-ps5"
|
||||
#endif
|
||||
@@ -42,8 +47,9 @@ namespace
|
||||
{
|
||||
constexpr size_t kMaxReleaseJson = 1u << 20;
|
||||
constexpr size_t kMaxZip = 64u << 20;
|
||||
constexpr size_t kMaxFile = 32u << 20;
|
||||
const char* const kAsset = N64PS5_TITLE_ID ".zip";
|
||||
// 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) --------------------------------------------------------------
|
||||
struct Json
|
||||
@@ -449,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)
|
||||
@@ -564,32 +537,24 @@ bool WriteFile(const std::string& path, const std::vector<uint8_t>& data)
|
||||
return fclose(f) == 0 && ok;
|
||||
}
|
||||
|
||||
// The new files over one app folder: all written as "<file>.new" first, then renamed into place (the
|
||||
// running eboot.bin keeps its old copy open until the restart).
|
||||
bool InstallInto(const std::string& dir, const std::map<std::string, std::vector<uint8_t>>& files, std::string& why)
|
||||
// The new version, complete, in the staging folder next to an app folder (ProsperoUpdateJob.h): the installed
|
||||
// files themselves are never written over (the console then can't start the app: 0.6.1 and 0.6.2).
|
||||
bool StageInto(const updatejob::Target& t, const std::map<std::string, std::vector<uint8_t>>& files, std::string& why)
|
||||
{
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
for (const auto& f : files)
|
||||
{
|
||||
const std::string path = dir + "/" + f.first;
|
||||
const size_t slash = path.rfind('/');
|
||||
OrbisMkdirs(path.substr(0, slash));
|
||||
if (!WriteFile(path + ".new", f.second))
|
||||
const std::string path = t.staging + "/" + f.first;
|
||||
OrbisMkdirs(path.substr(0, path.rfind('/')));
|
||||
if (!WriteFile(path, f.second))
|
||||
{
|
||||
why = "can't write " + path + ".new";
|
||||
for (const auto& g : files)
|
||||
unlink((dir + "/" + g.first + ".new").c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& f : files)
|
||||
{
|
||||
const std::string path = dir + "/" + f.first;
|
||||
if (rename((path + ".new").c_str(), path.c_str()) != 0)
|
||||
{
|
||||
why = "can't replace " + path;
|
||||
why = "can't write " + path;
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// fopen() gives no execute permission, and the console doesn't start an eboot.bin without it
|
||||
updatejob::MakeRunnable(t.staging);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -598,21 +563,19 @@ std::string StampPath()
|
||||
return OrbisDir("update") + "/installed.txt";
|
||||
}
|
||||
|
||||
void Restart(const UpdateOffer& offer)
|
||||
// The helper installs the staged version once the app has closed (ProsperoUpdateJob.h): say so, then close
|
||||
// the way main-boot.cpp's ExitApp does.
|
||||
[[noreturn]] void CloseForUpdate(const UpdateOffer& offer)
|
||||
{
|
||||
const char* path = "/data/homebrew/" N64PS5_TITLE_ID "/eboot.bin";
|
||||
if (access(path, F_OK) != 0)
|
||||
path = "/app0/eboot.bin";
|
||||
OrbisLog("[update] installed %s: restarting %s", offer.tag.c_str(), path);
|
||||
MessageBox("Installing Mupen64Plus PS5 " + offer.version,
|
||||
"The app closes now. Open it again from the home screen when the notification says the update is installed.");
|
||||
OrbisLog("[update] closing so the helper can install %s", offer.tag.c_str());
|
||||
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-update");
|
||||
OrbisLog("[update] sceSystemServiceLoadExec(%s) -> %x", path, unsigned(rc));
|
||||
MessageBox("Mupen64Plus PS5 " + offer.version + " is installed", "Close the app and open it again to use it.");
|
||||
_exit(0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -752,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;
|
||||
@@ -760,14 +723,18 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
"copy the new folder by hand)";
|
||||
}
|
||||
}
|
||||
int done = 0;
|
||||
updatejob::Job job;
|
||||
job.pid = int(getpid());
|
||||
job.tag = offer.tag;
|
||||
job.version = offer.version;
|
||||
for (const std::string& dir : folders)
|
||||
{
|
||||
const updatejob::Target t = updatejob::TargetFor(dir);
|
||||
std::string err;
|
||||
if (InstallInto(dir, files, err))
|
||||
if (StageInto(t, files, err))
|
||||
{
|
||||
done++;
|
||||
OrbisLog("[update] %s: %zu files written", dir.c_str(), files.size());
|
||||
job.targets.push_back(t);
|
||||
OrbisLog("[update] %s: %zu files staged in %s", dir.c_str(), files.size(), t.staging.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -775,9 +742,22 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
why = err;
|
||||
}
|
||||
}
|
||||
if (!ok || done == 0)
|
||||
if (ok && job.targets.empty())
|
||||
ok = false;
|
||||
if (ok && !updatejob::Write(job, why))
|
||||
ok = false;
|
||||
// the helper that installs it: a fresh copy through the ELF loader (one already running only lets apps out)
|
||||
if (ok && !jailbreak::StartHelper())
|
||||
{
|
||||
ok = false;
|
||||
why = "the ELF loader (port 9021) didn't answer, so the update can't be installed now";
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
OrbisLog("[update] %s failed: %s", offer.tag.c_str(), why.c_str());
|
||||
unlink(updatejob::JobPath().c_str());
|
||||
for (const updatejob::Target& t : job.targets)
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
MessageBox("The update failed", why);
|
||||
return;
|
||||
}
|
||||
@@ -787,6 +767,6 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
fclose(f);
|
||||
}
|
||||
offer.zip.clear();
|
||||
Restart(offer);
|
||||
CloseForUpdate(offer);
|
||||
}
|
||||
} // namespace fe
|
||||
@@ -1,15 +1,16 @@
|
||||
// Mupen64Plus PS5: updates from the project's GitHub releases (github.com/TheRealRetro/mupen64plus-ps5).
|
||||
//
|
||||
// A release is tagged with param.json's contentVersion (NN.NNN.NNN, 0.6.0 -> 00.006.000) and carries
|
||||
// PPSA99064.zip (the PPSA99064/ app folder), as the Homebrew Browser's releases do; GitHub publishes each
|
||||
// asset's SHA-256 ("digest") with the release.
|
||||
// Mupen64PlusPS5.zip (the PPSA99064/ app folder; RELEASE_ZIP in the Makefile, PPSA99064.zip up to 0.6.2);
|
||||
// GitHub publishes each asset's SHA-256 ("digest") with the release.
|
||||
//
|
||||
// Two steps, because of where HTTPS works (fe_prefetch.h: before the app asks for /data, not after):
|
||||
// 1. CheckForUpdate, at start-up before the jailbreak: asks api.github.com for the latest release; when its
|
||||
// tag is newer than this build, downloads PPSA99064.zip and checks its size and SHA-256 (in memory).
|
||||
// tag is newer than this build, downloads Mupen64PlusPS5.zip and checks its size and SHA-256 (in memory).
|
||||
// 2. OfferUpdate, once /data is visible: "Version X is available" with the release notes; on Cross it
|
||||
// writes the new files over every copy of the app folder it finds (/data/homebrew/PPSA99064, the USB
|
||||
// drives' homebrew/PPSA99064...), each file through a temporary name, and starts the app again.
|
||||
// unpacks the new version next to every copy of the app folder it finds (/data/homebrew/PPSA99064, the
|
||||
// USB drives' homebrew/PPSA99064...) and closes; its helper payload then swaps the folders so
|
||||
// ShadowMountPlus installs the app again (ProsperoUpdateJob.h), and a notification says when to reopen it.
|
||||
// The setting "Check for updates" (Off) skips the question; the download in step 1 still happens, since the
|
||||
// settings can't be read before the jailbreak.
|
||||
//
|
||||
@@ -34,8 +35,8 @@ struct UpdateOffer
|
||||
// Step 1 (before the jailbreak). Never blocks for long when the console is offline.
|
||||
UpdateOffer CheckForUpdate();
|
||||
|
||||
// Step 2 (with /data). Returns when there is nothing to do, the user said later, or the install failed;
|
||||
// after a successful install it restarts the app and doesn't return (unless the restart fails).
|
||||
// Step 2 (with /data). Returns when there is nothing to do, the user said later, or staging failed; once the
|
||||
// update is staged it closes the app and doesn't return.
|
||||
void OfferUpdate(UpdateOffer& offer, bool ask);
|
||||
|
||||
// "00.006.000" -> {0, 6, 0}; also accepts "v00.006.000" and "0.6.0". False when it isn't a version.
|
||||
|
||||
@@ -209,6 +209,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*)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoUpdateJob.h"
|
||||
#ifndef N64PS5_HELPER_ONLY
|
||||
#include "ProsperoInstall.h"
|
||||
#endif
|
||||
@@ -85,6 +86,10 @@ int main()
|
||||
g_ready_message = msg;
|
||||
#endif
|
||||
|
||||
// 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))
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ typedef struct
|
||||
extern const m64ps5_library m64ps5_core_lib; // osal_dynlib_static.c
|
||||
extern const m64ps5_library m64ps5_rsp_lib; // ps5/n64/plugins/rsp_cxd4_ps5.c
|
||||
extern const m64ps5_library m64ps5_gfx_lib; // ps5/n64/plugins/gfx_ps5.c
|
||||
extern const m64ps5_library m64ps5_gfx_parallel_lib; // ps5/n64/plugins/gfx_parallel_ps5.cpp (VULKAN=1)
|
||||
extern const m64ps5_library m64ps5_audio_lib; // ps5/n64/plugins/audio_ps5.cpp
|
||||
extern const m64ps5_library m64ps5_input_lib; // ps5/n64/plugins/input_ps5.cpp
|
||||
|
||||
|
||||
@@ -29,30 +29,97 @@ typedef struct
|
||||
bool hide_overscan; // crop the VI's blank borders
|
||||
int num_workers; // angrylion render threads (0 = one per hardware thread)
|
||||
int dp_compat; // 0 fast, 1 moderate, 2 slow (sync points between render threads)
|
||||
int upscale; // the GPU renderer's internal resolution: 1, 2, 4 or 8 times the N64's
|
||||
bool gpu_sync; // the GPU renderer waits for the GPU wherever the game waits for the RDP (accurate)
|
||||
} n64ps5_gfx_options;
|
||||
|
||||
// The frontend sets these before the game starts; RomOpen applies them.
|
||||
void n64ps5_gfx_set_options(const n64ps5_gfx_options* opt);
|
||||
|
||||
// ---- the GPU video plugin (plugins/gfx_parallel_ps5.cpp, VULKAN=1 builds only) ---------------------------
|
||||
// paraLLEl-RDP on Vulkan. n64ps5_gpu_available makes the Vulkan device the first time (false: no GPU
|
||||
// renderer, use angrylion).
|
||||
bool n64ps5_gpu_available(void);
|
||||
void n64ps5_gpu_set_options(const n64ps5_gfx_options* opt);
|
||||
|
||||
// Implemented by the frontend: called on every video interrupt. `frame` is NULL when the VI shows nothing
|
||||
// (blanked) this time. The pixels stay valid until the next call.
|
||||
void n64ps5_on_vi(const n64ps5_frame* frame);
|
||||
|
||||
// ---- RSP (plugins/rsp_cxd4_ps5.c, rsp_hle_ps5.c) -------------------------------------------------------------
|
||||
// true (default): rsp-hle does the audio (and MP3/JPEG...) tasks, cxd4 the rest; false: cxd4 runs everything.
|
||||
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.
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
// Mupen64Plus PS5: the GPU video plugin, paraLLEl-RDP (third_party/parallel-rdp, Themaister's standalone
|
||||
// tree, MIT) on Vulkan, for a higher internal resolution (GitHub issue #1). Built only with `make VULKAN=1`,
|
||||
// which links RADV (coreorbis/orbis-shims/ProsperoVulkan.h).
|
||||
//
|
||||
// paraLLEl-RDP runs the RDP's command lists as Vulkan compute shaders on the GPU, working on RDRAM in place
|
||||
// (VK_EXT_external_memory_host: the core's RDRAM is imported into Vulkan), optionally upscaled 2x/4x/8x. Its
|
||||
// VI turns the frame buffer into a picture on the GPU too. Here the picture is copied back to the CPU and
|
||||
// handed to the frontend (n64ps5_on_vi) like angrylion's, so the frontend's scaling, menus and on-screen
|
||||
// text stay as they are. The command list handling follows mupen64plus-libretro-nx's
|
||||
// mupen64plus-video-paraLLEl/rdp.cpp.
|
||||
//
|
||||
// The Vulkan device is made once (n64ps5_gpu_available) and kept; the command processor lives from RomOpen
|
||||
// to RomClosed.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "rdp_device.hpp"
|
||||
#include "context.hpp"
|
||||
#include "device.hpp"
|
||||
#include "logging.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "api/m64p_plugin.h"
|
||||
#include "api/m64p_types.h"
|
||||
}
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "n64_bridge.h"
|
||||
#include "perf.h"
|
||||
#include "static_dynlib.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vk_icdGetInstanceProcAddr(VkInstance instance, const char* name);
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kPluginVersion = 0x010000;
|
||||
constexpr int kApiVersion = 0x020200;
|
||||
constexpr uint32_t kDpStatusXbusDma = 0x1;
|
||||
constexpr uint32_t kMiIntrDp = 0x20;
|
||||
|
||||
// paraLLEl-RDP's and Granite's messages go to boot.log ("[gpu] ...") on the threads that set this.
|
||||
class Logger : public Util::LoggingInterface
|
||||
{
|
||||
public:
|
||||
bool log(const char* tag, const char* fmt, va_list va) override
|
||||
{
|
||||
char line[512];
|
||||
vsnprintf(line, sizeof(line), fmt, va);
|
||||
size_t n = strlen(line);
|
||||
while (n && (line[n - 1] == '\n' || line[n - 1] == '\r'))
|
||||
line[--n] = 0;
|
||||
OrbisLog("[gpu] %s%s", tag, line);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
Logger g_logger;
|
||||
|
||||
struct Gpu
|
||||
{
|
||||
bool tried = false;
|
||||
std::unique_ptr<Vulkan::Context> context;
|
||||
std::unique_ptr<Vulkan::Device> device;
|
||||
};
|
||||
Gpu g_gpu;
|
||||
|
||||
// paraLLEl-RDP's compiled pipelines, kept between starts in Vulkan's pipeline cache, saved when a game closes:
|
||||
// RADV's own disk cache stayed empty in this process (logs/vulkan.log showed the same compiles, up to 0.3 s
|
||||
// each, at every start).
|
||||
size_t g_saved_cache = 0;
|
||||
|
||||
std::string PipelineCachePath()
|
||||
{
|
||||
return OrbisDir("cache") + "/parallel-rdp-pipelines.bin";
|
||||
}
|
||||
|
||||
void LoadPipelineCache(Vulkan::Device& device)
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
if (FILE* f = fopen(PipelineCachePath().c_str(), "rb"))
|
||||
{
|
||||
fseek(f, 0, SEEK_END);
|
||||
const long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (size > 0)
|
||||
{
|
||||
data.resize(size_t(size));
|
||||
if (fread(data.data(), 1, data.size(), f) != data.size())
|
||||
data.clear();
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
const bool ok = device.init_pipeline_cache(data.empty() ? nullptr : data.data(), data.size());
|
||||
g_saved_cache = data.size();
|
||||
OrbisLog("[gpu] pipeline cache: %zu KiB from %s%s", data.size() >> 10, PipelineCachePath().c_str(),
|
||||
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)
|
||||
return;
|
||||
const size_t size = g_gpu.device->get_pipeline_cache_size();
|
||||
if (size == 0 || size == g_saved_cache)
|
||||
return;
|
||||
std::vector<uint8_t> data(size);
|
||||
if (!g_gpu.device->get_pipeline_cache_data(data.data(), data.size()))
|
||||
return;
|
||||
OrbisMkdirs(OrbisDir("cache"));
|
||||
const std::string path = PipelineCachePath();
|
||||
const std::string tmp = path + ".part";
|
||||
FILE* f = fopen(tmp.c_str(), "wb");
|
||||
if (!f)
|
||||
return;
|
||||
const bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
|
||||
fclose(f);
|
||||
chmod(tmp.c_str(), 0666); // reachable over FTP
|
||||
if (ok && rename(tmp.c_str(), path.c_str()) == 0)
|
||||
{
|
||||
g_saved_cache = size;
|
||||
OrbisLog("[gpu] pipeline cache saved: %zu KiB", size >> 10);
|
||||
}
|
||||
else
|
||||
unlink(tmp.c_str());
|
||||
}
|
||||
|
||||
GFX_INFO l_Gfx;
|
||||
bool l_Initialized;
|
||||
bool l_RomOpen;
|
||||
void (*l_RenderCallback)(int);
|
||||
n64ps5_gfx_options l_Options = {0, false, 6, 1, 4, true};
|
||||
std::unique_ptr<RDP::CommandProcessor> l_Rdp;
|
||||
RDP::VIScanoutBuffer l_Scanout;
|
||||
|
||||
// What the GPU costs, logged every 600 VIs (Report): the waits for it (rdtsc ticks) and the GPU's own time,
|
||||
// from paraLLEl-RDP's timestamps (PARALLEL_RDP_BENCH=2).
|
||||
struct Stats
|
||||
{
|
||||
uint64_t syncs = 0, sync_ticks = 0, scanout_ticks = 0;
|
||||
unsigned vis = 0;
|
||||
uint64_t tsc0 = 0;
|
||||
timespec t0 = {};
|
||||
};
|
||||
Stats l_Stats;
|
||||
|
||||
// the RDP command list as the RSP left it (rdp.cpp's process_commands)
|
||||
int l_CmdCur, l_CmdPtr;
|
||||
uint32_t l_CmdData[0x00040000 >> 2];
|
||||
const unsigned kCmdLength[64] = {
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 4, 6, 12, 14, 12, 14, 20, 22,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
};
|
||||
|
||||
// commands on their way to paraLLEl-RDP, framed as its ring wants them (num_words, words...): handed over
|
||||
// in one go per list (and before each SyncFull), since a lock + wake-up per command took ~9 ms per VI in
|
||||
// GoldenEye on the PS5
|
||||
constexpr size_t kBatchWords = 8192; // well under the ring's 64K words
|
||||
uint32_t l_Batch[kBatchWords];
|
||||
size_t l_BatchLen;
|
||||
|
||||
void FlushBatch()
|
||||
{
|
||||
if (l_BatchLen && l_Rdp)
|
||||
l_Rdp->enqueue_commands(l_BatchLen, l_Batch);
|
||||
l_BatchLen = 0;
|
||||
}
|
||||
|
||||
RDP::CommandProcessorFlags Flags(int upscale)
|
||||
{
|
||||
switch (upscale)
|
||||
{
|
||||
case 2: return RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_2X_BIT | RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
case 4: return RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_4X_BIT | RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
case 8: return RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT | RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the plugin API -------------------------------------------------------------------------------------
|
||||
m64p_error PluginStartup(m64p_dynlib_handle, void*, void (*)(void*, int, const char*))
|
||||
{
|
||||
if (l_Initialized)
|
||||
return M64ERR_ALREADY_INIT;
|
||||
l_Initialized = true;
|
||||
return M64ERR_SUCCESS;
|
||||
}
|
||||
|
||||
m64p_error PluginShutdown()
|
||||
{
|
||||
if (!l_Initialized)
|
||||
return M64ERR_NOT_INIT;
|
||||
l_Initialized = false;
|
||||
return M64ERR_SUCCESS;
|
||||
}
|
||||
|
||||
m64p_error PluginGetVersion(m64p_plugin_type* type, int* version, int* api, const char** name, int* caps)
|
||||
{
|
||||
if (type)
|
||||
*type = M64PLUGIN_GFX;
|
||||
if (version)
|
||||
*version = kPluginVersion;
|
||||
if (api)
|
||||
*api = kApiVersion;
|
||||
if (name)
|
||||
*name = "paraLLEl-RDP (PS5)";
|
||||
if (caps)
|
||||
*caps = 0;
|
||||
return M64ERR_SUCCESS;
|
||||
}
|
||||
|
||||
int InitiateGFX(GFX_INFO info)
|
||||
{
|
||||
l_Gfx = info;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void MoveScreen(int, int)
|
||||
{
|
||||
}
|
||||
|
||||
void ProcessDList()
|
||||
{
|
||||
}
|
||||
|
||||
void ProcessRDPList()
|
||||
{
|
||||
if (!l_RomOpen)
|
||||
return;
|
||||
const uint64_t t0 = n64ps5_perf_now();
|
||||
const uint32_t current = *l_Gfx.DPC_CURRENT_REG & 0x00FFFFF8;
|
||||
const uint32_t end = *l_Gfx.DPC_END_REG & 0x00FFFFF8;
|
||||
int length = int(end) - int(current);
|
||||
if (length <= 0)
|
||||
return;
|
||||
length = unsigned(length) >> 3;
|
||||
if ((l_CmdPtr + length) & ~(0x0003FFFF >> 3))
|
||||
return;
|
||||
|
||||
uint32_t offset = current;
|
||||
if (*l_Gfx.DPC_STATUS_REG & kDpStatusXbusDma)
|
||||
{
|
||||
do
|
||||
{
|
||||
offset &= 0xFF8;
|
||||
memcpy(&l_CmdData[2 * l_CmdPtr], l_Gfx.DMEM + offset, 8);
|
||||
offset += 8;
|
||||
l_CmdPtr++;
|
||||
} while (--length > 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (end > 0x7ffffff || current > 0x7ffffff)
|
||||
return;
|
||||
do
|
||||
{
|
||||
offset &= 0xFFFFF8;
|
||||
memcpy(&l_CmdData[2 * l_CmdPtr], l_Gfx.RDRAM + offset, 8);
|
||||
offset += 8;
|
||||
l_CmdPtr++;
|
||||
} while (--length > 0);
|
||||
}
|
||||
|
||||
while (l_CmdCur - l_CmdPtr < 0)
|
||||
{
|
||||
const uint32_t command = (l_CmdData[2 * l_CmdCur] >> 24) & 63;
|
||||
const int cmd_length = int(kCmdLength[command]);
|
||||
if (l_CmdPtr - l_CmdCur - cmd_length < 0)
|
||||
{
|
||||
*l_Gfx.DPC_START_REG = *l_Gfx.DPC_CURRENT_REG = *l_Gfx.DPC_END_REG;
|
||||
FlushBatch();
|
||||
n64ps5_perf_add(N64PS5_PERF_RDP, n64ps5_perf_now() - t0);
|
||||
return;
|
||||
}
|
||||
if (command >= 8 && l_Rdp)
|
||||
{
|
||||
const size_t words = size_t(cmd_length) * 2;
|
||||
if (l_BatchLen + 1 + words > kBatchWords)
|
||||
FlushBatch();
|
||||
l_Batch[l_BatchLen++] = uint32_t(words);
|
||||
memcpy(&l_Batch[l_BatchLen], &l_CmdData[2 * l_CmdCur], words * 4);
|
||||
l_BatchLen += words;
|
||||
}
|
||||
if (RDP::Op(command) == RDP::Op::SyncFull)
|
||||
{
|
||||
FlushBatch();
|
||||
// the game waits for the RDP here: let the GPU finish what came before (GPU sync "Accurate");
|
||||
// "Fast" lets the game go on while the GPU works, which most games don't notice
|
||||
if (l_Rdp && l_Options.gpu_sync)
|
||||
{
|
||||
const uint64_t w0 = n64ps5_perf_now();
|
||||
l_Rdp->wait_for_timeline(l_Rdp->signal_timeline());
|
||||
l_Stats.sync_ticks += n64ps5_perf_now() - w0;
|
||||
}
|
||||
l_Stats.syncs++;
|
||||
*l_Gfx.MI_INTR_REG |= kMiIntrDp;
|
||||
l_Gfx.CheckInterrupts();
|
||||
}
|
||||
l_CmdCur += cmd_length;
|
||||
}
|
||||
FlushBatch();
|
||||
l_CmdPtr = 0;
|
||||
l_CmdCur = 0;
|
||||
*l_Gfx.DPC_START_REG = *l_Gfx.DPC_CURRENT_REG = *l_Gfx.DPC_END_REG;
|
||||
n64ps5_perf_add(N64PS5_PERF_RDP, n64ps5_perf_now() - t0);
|
||||
}
|
||||
|
||||
int RomOpen()
|
||||
{
|
||||
if (!n64ps5_gpu_available())
|
||||
return 0;
|
||||
Util::set_thread_logging_interface(&g_logger);
|
||||
const size_t rdram_size = (l_Gfx.version >= 2 && l_Gfx.RDRAM_SIZE) ? *l_Gfx.RDRAM_SIZE : 8 * 1024 * 1024;
|
||||
const size_t align =
|
||||
g_gpu.device->get_device_features().host_memory_properties.minImportedHostPointerAlignment;
|
||||
if (align && (reinterpret_cast<uintptr_t>(l_Gfx.RDRAM) & (align - 1)))
|
||||
{
|
||||
OrbisLog("[gpu] RDRAM at %p is not aligned to %zu bytes", static_cast<void*>(l_Gfx.RDRAM), align);
|
||||
return 0;
|
||||
}
|
||||
const int upscale = l_Options.upscale == 2 || l_Options.upscale == 4 || l_Options.upscale == 8 ? l_Options.upscale : 1;
|
||||
l_Rdp.reset(new RDP::CommandProcessor(*g_gpu.device, l_Gfx.RDRAM, 0, rdram_size, rdram_size / 2, Flags(upscale)));
|
||||
if (!l_Rdp->device_is_supported())
|
||||
{
|
||||
OrbisLog("[gpu] paraLLEl-RDP: the device is not supported");
|
||||
l_Rdp.reset();
|
||||
return 0;
|
||||
}
|
||||
RDP::Quirks quirks;
|
||||
quirks.set_native_texture_lod(false);
|
||||
quirks.set_native_resolution_tex_rect(true);
|
||||
l_Rdp->set_quirks(quirks);
|
||||
l_CmdCur = l_CmdPtr = 0;
|
||||
l_Stats = {};
|
||||
l_RomOpen = true;
|
||||
OrbisLog("[gpu] paraLLEl-RDP ready: %ux internal resolution, GPU sync %s, RDRAM %zu KiB at %p",
|
||||
unsigned(upscale), l_Options.gpu_sync ? "accurate" : "fast", rdram_size >> 10,
|
||||
static_cast<void*>(l_Gfx.RDRAM));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RomClosed()
|
||||
{
|
||||
if (!l_RomOpen)
|
||||
return;
|
||||
l_RomOpen = false;
|
||||
if (l_Rdp)
|
||||
l_Rdp->idle();
|
||||
l_Scanout = {};
|
||||
l_Rdp.reset();
|
||||
if (g_gpu.device)
|
||||
g_gpu.device->wait_idle();
|
||||
SavePipelineCache();
|
||||
}
|
||||
|
||||
void ShowCFB()
|
||||
{
|
||||
}
|
||||
|
||||
void Report()
|
||||
{
|
||||
timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
const uint64_t tsc = n64ps5_perf_now();
|
||||
if (l_Stats.tsc0 && l_Stats.vis)
|
||||
{
|
||||
const double ms = (now.tv_sec - l_Stats.t0.tv_sec) * 1e3 + (now.tv_nsec - l_Stats.t0.tv_nsec) / 1e6;
|
||||
const double tick_ms = ms / double(tsc - l_Stats.tsc0);
|
||||
const double vis = l_Stats.vis;
|
||||
OrbisLog("[gpu] per VI: %.2f syncs, %.2f ms waiting for them, %.2f ms waiting for the picture",
|
||||
l_Stats.syncs / vis, l_Stats.sync_ticks * tick_ms / vis, l_Stats.scanout_ticks * tick_ms / vis);
|
||||
if (l_Rdp)
|
||||
{
|
||||
RDP::CommandRing& ring = l_Rdp->get_command_ring();
|
||||
const uint64_t batches = ring.batches.exchange(0), words = ring.words_in.exchange(0);
|
||||
OrbisLog("[gpu] per VI: %.0f command words in %.1f batches, %.1f wake-ups; worker busy %.2f ms, emulator "
|
||||
"waiting for room %.2f ms", words / vis, batches / vis, ring.wakeups.exchange(0) / vis,
|
||||
ring.worker_busy_ticks.exchange(0) * tick_ms / vis, ring.producer_wait_ticks.exchange(0) * tick_ms / vis);
|
||||
}
|
||||
g_gpu.device->timestamp_log([](const std::string& tag, const Vulkan::TimestampIntervalReport& r) {
|
||||
if (r.time_per_frame_context > 0.0001)
|
||||
OrbisLog("[gpu] GPU %s: %.3f ms per frame (%.1f times)", tag.c_str(), r.time_per_frame_context * 1e3,
|
||||
r.accumulations_per_frame_context);
|
||||
});
|
||||
g_gpu.device->timestamp_log_reset();
|
||||
}
|
||||
l_Stats.syncs = l_Stats.sync_ticks = l_Stats.scanout_ticks = 0;
|
||||
l_Stats.vis = 0;
|
||||
l_Stats.tsc0 = tsc;
|
||||
l_Stats.t0 = now;
|
||||
}
|
||||
|
||||
// One frame: the VI registers to paraLLEl-RDP, its scanout copied back, the picture to the frontend.
|
||||
void UpdateScreen()
|
||||
{
|
||||
if (!l_RomOpen || !l_Rdp)
|
||||
return;
|
||||
const uint64_t t0 = n64ps5_perf_now();
|
||||
unsigned int** vi = &l_Gfx.VI_STATUS_REG; // the 14 VI registers, in paraLLEl-RDP's VIRegister order
|
||||
for (int i = 0; i < int(RDP::VIRegister::Count); i++)
|
||||
l_Rdp->set_vi_register(RDP::VIRegister(i), *vi[i]);
|
||||
|
||||
RDP::ScanoutOptions opts;
|
||||
opts.persist_frame_on_invalid_input = true;
|
||||
const bool filtered = l_Options.vi_mode == 0;
|
||||
opts.vi.aa = filtered;
|
||||
opts.vi.divot_filter = filtered;
|
||||
opts.vi.dither_filter = filtered;
|
||||
opts.vi.gamma_dither = filtered;
|
||||
opts.vi.scale = true;
|
||||
// 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)
|
||||
{
|
||||
const uint64_t w0 = n64ps5_perf_now();
|
||||
l_Scanout.fence->wait();
|
||||
l_Stats.scanout_ticks += n64ps5_perf_now() - w0;
|
||||
const auto* px = static_cast<const uint32_t*>(
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
n64ps5_perf_add(N64PS5_PERF_VI, n64ps5_perf_now() - t0);
|
||||
n64ps5_on_vi(nullptr);
|
||||
}
|
||||
l_Rdp->begin_frame_context();
|
||||
if (++l_Stats.vis >= 600 || !l_Stats.tsc0)
|
||||
Report();
|
||||
if (l_RenderCallback)
|
||||
l_RenderCallback(1);
|
||||
}
|
||||
|
||||
void ViStatusChanged()
|
||||
{
|
||||
}
|
||||
|
||||
void ViWidthChanged()
|
||||
{
|
||||
}
|
||||
|
||||
void ChangeWindow()
|
||||
{
|
||||
}
|
||||
|
||||
void ReadScreen2(void*, int* width, int* height, int)
|
||||
{
|
||||
if (width)
|
||||
*width = 0;
|
||||
if (height)
|
||||
*height = 0;
|
||||
}
|
||||
|
||||
void SetRenderingCallback(void (*callback)(int))
|
||||
{
|
||||
l_RenderCallback = callback;
|
||||
}
|
||||
|
||||
void ResizeVideoOutput(int, int)
|
||||
{
|
||||
}
|
||||
|
||||
void FBRead(unsigned int)
|
||||
{
|
||||
}
|
||||
|
||||
void FBWrite(unsigned int, unsigned int)
|
||||
{
|
||||
}
|
||||
|
||||
void FBGetFrameBufferInfo(void*)
|
||||
{
|
||||
}
|
||||
|
||||
#define SYM(name) {#name, (m64p_function)name}
|
||||
const m64ps5_symbol l_Symbols[] = {
|
||||
SYM(PluginStartup),
|
||||
SYM(PluginShutdown),
|
||||
SYM(PluginGetVersion),
|
||||
SYM(ChangeWindow),
|
||||
SYM(InitiateGFX),
|
||||
SYM(MoveScreen),
|
||||
SYM(ProcessDList),
|
||||
SYM(ProcessRDPList),
|
||||
SYM(RomClosed),
|
||||
SYM(RomOpen),
|
||||
SYM(ShowCFB),
|
||||
SYM(UpdateScreen),
|
||||
SYM(ViStatusChanged),
|
||||
SYM(ViWidthChanged),
|
||||
SYM(ReadScreen2),
|
||||
SYM(SetRenderingCallback),
|
||||
SYM(ResizeVideoOutput),
|
||||
SYM(FBRead),
|
||||
SYM(FBWrite),
|
||||
SYM(FBGetFrameBufferInfo),
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
#undef SYM
|
||||
} // namespace
|
||||
|
||||
extern "C" const m64ps5_library m64ps5_gfx_parallel_lib = {"parallel-rdp", l_Symbols};
|
||||
|
||||
extern "C" void n64ps5_gpu_set_options(const n64ps5_gfx_options* opt)
|
||||
{
|
||||
if (opt)
|
||||
l_Options = *opt;
|
||||
}
|
||||
|
||||
// The Vulkan instance and device paraLLEl-RDP runs on, made the first time a game asks for the GPU renderer
|
||||
// and kept for the app's life. False when the GPU can't be used (the frontend then uses angrylion).
|
||||
extern "C" bool n64ps5_gpu_available(void)
|
||||
{
|
||||
if (g_gpu.tried)
|
||||
return g_gpu.device != nullptr;
|
||||
g_gpu.tried = true;
|
||||
Util::set_thread_logging_interface(&g_logger);
|
||||
// GPU timestamps around paraLLEl-RDP's work, for Report (test builds)
|
||||
setenv("PARALLEL_RDP_BENCH", "2", 1);
|
||||
|
||||
if (!Vulkan::Context::init_loader(vk_icdGetInstanceProcAddr))
|
||||
{
|
||||
OrbisLog("[gpu] Vulkan loader init failed");
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<Vulkan::Context> context(new Vulkan::Context);
|
||||
if (!context->init_instance_and_device(nullptr, 0, nullptr, 0, 0))
|
||||
{
|
||||
OrbisLog("[gpu] Vulkan instance/device creation failed");
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<Vulkan::Device> device(new Vulkan::Device);
|
||||
device->set_context(*context);
|
||||
if (!device->get_device_features().supports_external_memory_host)
|
||||
{
|
||||
OrbisLog("[gpu] no VK_EXT_external_memory_host: paraLLEl-RDP can't share RDRAM with the GPU");
|
||||
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,6 +1,6 @@
|
||||
// Mupen64Plus PS5: the video plugin, angrylion's RDP Plus (third_party/angrylion-rdp-plus) without OpenGL.
|
||||
//
|
||||
// The PS5 homebrew SDK has no OpenGL or Vulkan driver, so a hardware renderer (GLideN64, paraLLEl) is out.
|
||||
// It runs on the CPU at the N64's own resolution; builds with Vulkan also have the GPU one (gfx_parallel_ps5.cpp).
|
||||
// angrylion's renderer is a software, pixel-accurate RDP + VI: it rasterises the RDP command lists that
|
||||
// the low-level RSP plugin (cxd4) produces straight into RDRAM, and its VI emulation turns the frame
|
||||
// buffer into a picture. It spreads the work over several threads, which the PS5's eight Zen 2 cores have.
|
||||
@@ -35,7 +35,7 @@ static bool l_RomOpen;
|
||||
static bool l_WarnedHle;
|
||||
static GFX_INFO l_Gfx;
|
||||
static void (*l_RenderCallback)(int);
|
||||
static n64ps5_gfx_options l_Options = {0, false, 6, 1};
|
||||
static n64ps5_gfx_options l_Options = {0, false, 6, 1, 1, true};
|
||||
static struct n64video_config l_Config;
|
||||
static struct n64video_frame_buffer l_LastFb; // for ReadScreen2
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Mupen64Plus PS5: the RSP plugin, mupen64plus-rsp-cxd4 (third_party/rsp-cxd4, CC0) built into the app.
|
||||
//
|
||||
// cxd4 is a low-level RSP interpreter: it runs the game's own microcode, so graphics tasks end up as RDP
|
||||
// command lists for angrylion's renderer (gfx_ps5.c) and audio tasks write samples to RDRAM for the audio
|
||||
// plugin. Every plugin exports the same names (PluginStartup, RomOpen...), and cxd4 keeps its pointers to
|
||||
// command lists for the RDP (angrylion, gfx_ps5.c, or paraLLEl-RDP) and audio tasks write samples to RDRAM
|
||||
// for the audio plugin. With "Audio processing" on HLE (default), rsp-hle (rsp_hle_ps5.c) takes the tasks
|
||||
// it knows (audio, MP3, JPEG...) and hands the rest, graphics included, to cxd4. Every plugin exports the same names (PluginStartup, RomOpen...), and cxd4 keeps its pointers to
|
||||
// the core's config functions in globals named like the core's functions, so they are all renamed here
|
||||
// and the whole plugin is compiled as one unit (its own lto.c).
|
||||
//
|
||||
@@ -36,14 +37,37 @@
|
||||
#undef DoRspCycles
|
||||
|
||||
#include "perf.h"
|
||||
#include "rsp_hle_ps5.h"
|
||||
#include "static_dynlib.h"
|
||||
|
||||
// cxd4's time, without the RDP lists it hands angrylion while it runs
|
||||
static m64p_error rsp_PluginStartup(m64p_dynlib_handle core, void* context, void (*debug_callback)(void*, int, const char*))
|
||||
{
|
||||
n64ps5_hle_startup(context, debug_callback);
|
||||
return cxd4_PluginStartup(core, context, debug_callback);
|
||||
}
|
||||
|
||||
static void rsp_InitiateRSP(RSP_INFO info, unsigned int* cycle_count)
|
||||
{
|
||||
n64ps5_hle_init(&info);
|
||||
cxd4_InitiateRSP(info, cycle_count);
|
||||
}
|
||||
|
||||
static void rsp_RomClosed(void)
|
||||
{
|
||||
n64ps5_hle_rom_closed();
|
||||
cxd4_RomClosed();
|
||||
}
|
||||
|
||||
// the RSP's time (rsp-hle's and cxd4's), without the RDP lists it hands the RDP while it runs
|
||||
static unsigned int timed_DoRspCycles(unsigned int cycles)
|
||||
{
|
||||
const uint64_t rdp0 = n64ps5_perf_total[N64PS5_PERF_RDP];
|
||||
const uint64_t t0 = n64ps5_perf_now();
|
||||
const unsigned int r = cxd4_DoRspCycles(cycles);
|
||||
unsigned int r = cycles;
|
||||
if (n64ps5_hle_enabled())
|
||||
n64ps5_hle_execute();
|
||||
else
|
||||
r = cxd4_DoRspCycles(cycles);
|
||||
const uint64_t t1 = n64ps5_perf_now();
|
||||
const uint64_t spent = t1 - t0;
|
||||
const uint64_t rdp = n64ps5_perf_total[N64PS5_PERF_RDP] - rdp0;
|
||||
@@ -52,11 +76,11 @@ static unsigned int timed_DoRspCycles(unsigned int cycles)
|
||||
}
|
||||
|
||||
static const m64ps5_symbol l_RspSymbols[] = {
|
||||
{"PluginStartup", (m64p_function)cxd4_PluginStartup},
|
||||
{"PluginStartup", (m64p_function)rsp_PluginStartup},
|
||||
{"PluginShutdown", (m64p_function)cxd4_PluginShutdown},
|
||||
{"PluginGetVersion", (m64p_function)cxd4_PluginGetVersion},
|
||||
{"RomClosed", (m64p_function)cxd4_RomClosed},
|
||||
{"InitiateRSP", (m64p_function)cxd4_InitiateRSP},
|
||||
{"RomClosed", (m64p_function)rsp_RomClosed},
|
||||
{"InitiateRSP", (m64p_function)rsp_InitiateRSP},
|
||||
{"DoRspCycles", (m64p_function)timed_DoRspCycles},
|
||||
{NULL, NULL},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Mupen64Plus PS5: HLE audio in front of the cxd4 RSP (rsp_cxd4_ps5.c), the way mupen64plus-rsp-hle runs with
|
||||
// cxd4 as its "RspFallback".
|
||||
//
|
||||
// rsp-hle's core (third_party/rsp-hle/src, without its plugin.c) recognises the game's audio microcode and does
|
||||
// its work in C, much faster than cxd4 running the microcode instruction by instruction; the same goes for the
|
||||
// MP3 (Conker, Perfect Dark), JPEG and video tasks it knows. Everything else, graphics above all (paraLLEl-RDP
|
||||
// and angrylion are low-level RDPs: they need the RSP to run the game's own graphics microcode), is forwarded
|
||||
// to cxd4 unchanged. Setting "Audio processing" (n64ps5_rsp_set_hle) turns it off: then cxd4 runs every task.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "api/m64p_plugin.h"
|
||||
#include "api/m64p_types.h"
|
||||
#include "../../third_party/rsp-hle/src/hle.h"
|
||||
#include "../../third_party/rsp-hle/src/hle_external.h"
|
||||
|
||||
#include "n64_bridge.h"
|
||||
#include "rsp_hle_ps5.h"
|
||||
|
||||
unsigned int cxd4_DoRspCycles(unsigned int cycles);
|
||||
|
||||
static struct hle_t g_hle;
|
||||
static void (*l_CheckInterrupts)(void);
|
||||
static void (*l_DebugCallback)(void*, int, const char*);
|
||||
static void* l_DebugContext;
|
||||
static bool l_Enabled = true;
|
||||
|
||||
// which tasks went where, logged once per microcode (rsp-hle picks a handler once per microcode too)
|
||||
enum { kSeenMax = 32 };
|
||||
static struct
|
||||
{
|
||||
uint32_t type, ucode;
|
||||
} l_Seen[kSeenMax];
|
||||
static int l_SeenCount;
|
||||
static bool l_Forwarding;
|
||||
|
||||
static void Log(int level, const char* fmt, ...)
|
||||
{
|
||||
if (!l_DebugCallback)
|
||||
return;
|
||||
char msg[256];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
va_end(args);
|
||||
l_DebugCallback(l_DebugContext, level, msg);
|
||||
}
|
||||
|
||||
// the current task's type and microcode address (the OSTask in DMEM at 0xfc0), read before it runs
|
||||
static uint32_t l_Type, l_Ucode;
|
||||
|
||||
// first time a microcode is seen: say whether HLE took it or cxd4 runs it
|
||||
static void NoteTask(bool forwarded)
|
||||
{
|
||||
const uint32_t type = l_Type, ucode = l_Ucode;
|
||||
for (int i = 0; i < l_SeenCount; i++)
|
||||
if (l_Seen[i].type == type && l_Seen[i].ucode == ucode)
|
||||
return;
|
||||
if (l_SeenCount < kSeenMax)
|
||||
{
|
||||
l_Seen[l_SeenCount].type = type;
|
||||
l_Seen[l_SeenCount].ucode = ucode;
|
||||
l_SeenCount++;
|
||||
}
|
||||
const char* const how = forwarded ? "LLE (cxd4)" : "HLE";
|
||||
// no OSTask in DMEM (e.g. the CIC 6105 boot code): the "type" is whatever is there
|
||||
if (type == 0 || type > 7)
|
||||
{
|
||||
Log(M64MSG_INFO, "RSP code without a task: %s", how);
|
||||
return;
|
||||
}
|
||||
static const char* const kTypes[] = {"?", "graphics", "audio"};
|
||||
Log(M64MSG_INFO, "task type %u (%s), microcode at %06x: %s", type, type <= 2 ? kTypes[type] : "other", ucode, how);
|
||||
}
|
||||
|
||||
void n64ps5_rsp_set_hle(bool on)
|
||||
{
|
||||
l_Enabled = on;
|
||||
}
|
||||
|
||||
void n64ps5_hle_startup(void* context, void (*debug_callback)(void*, int, const char*))
|
||||
{
|
||||
l_DebugContext = context;
|
||||
l_DebugCallback = debug_callback;
|
||||
}
|
||||
|
||||
void n64ps5_hle_init(const RSP_INFO* info)
|
||||
{
|
||||
hle_init(&g_hle, info->RDRAM, info->DMEM, info->IMEM, info->MI_INTR_REG, info->SP_MEM_ADDR_REG,
|
||||
info->SP_DRAM_ADDR_REG, info->SP_RD_LEN_REG, info->SP_WR_LEN_REG, info->SP_STATUS_REG, info->SP_DMA_FULL_REG,
|
||||
info->SP_DMA_BUSY_REG, info->SP_PC_REG, info->SP_SEMAPHORE_REG, info->DPC_START_REG, info->DPC_END_REG,
|
||||
info->DPC_CURRENT_REG, info->DPC_STATUS_REG, info->DPC_CLOCK_REG, info->DPC_BUFBUSY_REG,
|
||||
info->DPC_PIPEBUSY_REG, info->DPC_TMEM_REG, NULL);
|
||||
g_hle.hle_gfx = 0; // graphics go to cxd4 (HleForwardTask)
|
||||
g_hle.hle_aud = 0; // audio is done here, not sent to the audio plugin
|
||||
l_CheckInterrupts = info->CheckInterrupts;
|
||||
Log(M64MSG_INFO, "audio processing: %s", l_Enabled ? "HLE (rsp-hle), other tasks on cxd4" : "LLE (cxd4)");
|
||||
}
|
||||
|
||||
void n64ps5_hle_rom_closed(void)
|
||||
{
|
||||
g_hle.cached_ucodes.count = 0;
|
||||
l_SeenCount = 0;
|
||||
}
|
||||
|
||||
bool n64ps5_hle_enabled(void)
|
||||
{
|
||||
return l_Enabled;
|
||||
}
|
||||
|
||||
void n64ps5_hle_execute(void)
|
||||
{
|
||||
const uint32_t* task = (const uint32_t*)(g_hle.dmem + 0xfc0);
|
||||
l_Type = task[0];
|
||||
l_Ucode = task[4] & 0x00ffffff;
|
||||
l_Forwarding = false;
|
||||
hle_execute(&g_hle);
|
||||
if (!l_Forwarding)
|
||||
NoteTask(false);
|
||||
}
|
||||
|
||||
// ---- what rsp-hle's core expects from its user (hle_external.h) -----------------------------------------
|
||||
static void HleLog(int level, const char* message, va_list args)
|
||||
{
|
||||
if (!l_DebugCallback)
|
||||
return;
|
||||
char msg[256];
|
||||
vsnprintf(msg, sizeof(msg), message, args);
|
||||
l_DebugCallback(l_DebugContext, level, msg);
|
||||
}
|
||||
|
||||
void HleVerboseMessage(void* user_defined, const char* message, ...)
|
||||
{
|
||||
(void)user_defined;
|
||||
(void)message;
|
||||
}
|
||||
|
||||
void HleInfoMessage(void* user_defined, const char* message, ...)
|
||||
{
|
||||
(void)user_defined;
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
HleLog(M64MSG_INFO, message, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void HleErrorMessage(void* user_defined, const char* message, ...)
|
||||
{
|
||||
(void)user_defined;
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
HleLog(M64MSG_ERROR, message, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void HleWarnMessage(void* user_defined, const char* message, ...)
|
||||
{
|
||||
(void)user_defined;
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
HleLog(M64MSG_WARNING, message, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void HleCheckInterrupts(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
if (l_CheckInterrupts)
|
||||
l_CheckInterrupts();
|
||||
}
|
||||
|
||||
// hle_gfx and hle_aud are off, so these are never called
|
||||
void HleProcessDlistList(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
}
|
||||
|
||||
void HleProcessAlistList(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
}
|
||||
|
||||
void HleProcessRdpList(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
}
|
||||
|
||||
void HleShowCFB(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
}
|
||||
|
||||
// a task rsp-hle doesn't know: cxd4 runs the game's microcode
|
||||
int HleForwardTask(void* user_defined)
|
||||
{
|
||||
(void)user_defined;
|
||||
l_Forwarding = true;
|
||||
NoteTask(true);
|
||||
cxd4_DoRspCycles(0xffffffffu);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Mupen64Plus PS5: rsp-hle in front of cxd4 (rsp_hle_ps5.c), for the RSP plugin (rsp_cxd4_ps5.c).
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
void n64ps5_hle_startup(void* context, void (*debug_callback)(void*, int, const char*));
|
||||
void n64ps5_hle_init(const RSP_INFO* info); // m64p_plugin.h
|
||||
void n64ps5_hle_rom_closed(void);
|
||||
bool n64ps5_hle_enabled(void);
|
||||
void n64ps5_hle_execute(void); // one task; tasks rsp-hle doesn't know go to cxd4_DoRspCycles
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
1cecd042b2619bc505c12bfdc713808386f2b54d
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2020 Themaister
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
# paraLLEl-RDP
|
||||
|
||||
This project is a revival and complete rewrite of the old, defunct paraLLEl-RDP project.
|
||||
|
||||
The goal is to implement the Nintendo 64 RDP graphics chip as accurately as possible using Vulkan compute.
|
||||
The implementation aims to be bitexact with the
|
||||
[Angrylion-Plus](https://github.com/ata4/angrylion-rdp-plus) reference renderer where possible.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
While paraLLEl-RDP uses [Angrylion-Plus](https://github.com/ata4/angrylion-rdp-plus)
|
||||
as an implementation reference, it is not a port, and not a derived codebase of said project.
|
||||
It is written from scratch by studying [Angrylion-Plus](https://github.com/ata4/angrylion-rdp-plus)
|
||||
and trying to understand what is going on.
|
||||
The test suite uses [Angrylion-Plus](https://github.com/ata4/angrylion-rdp-plus) as a reference
|
||||
to validate implementation and cross-checking behavior.
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Much** faster LLE RDP emulation of N64 compared to a CPU implementation
|
||||
as parallel graphics workloads are offloaded to the GPU.
|
||||
Emulation performance is now completely bound by CPU and LLE RSP performance.
|
||||
Early benchmarking results suggest 2000 - 5000 VI/s being achieved on mid-range desktop GPUs based on timestamp data.
|
||||
There is no way the CPU emulation can keep up with that, but that means this should
|
||||
scale down to fairly gimped GPUs as well, assuming the driver requirements are met.
|
||||
- A backend renderer for standalone engines which aim to efficiently reproduce faithful N64 graphics.
|
||||
- Hopefully, an easier to understand implementation than the reference renderer.
|
||||
- An esoteric use case of advanced Vulkan compute programming.
|
||||
|
||||
## Missing features
|
||||
|
||||
The implementation is quite complete, and compatibility is very high in the limited amount of content I've tested.
|
||||
However, not every single feature is supported at this moment.
|
||||
Ticking the last boxes depends mostly on real content making use of said features.
|
||||
|
||||
- Color combiner chroma keying
|
||||
- Various "bugs" / questionable behavior that seems meaningless to emulate
|
||||
- Certain extreme edge cases in TMEM upload. The implementation has tests for many "crazy" edge cases though.
|
||||
- ... possibly other obscure features
|
||||
|
||||
The VI is essentially complete. A fancy deinterlacer might be useful to add since we have plenty of GPU cycles to spare in the graphics queue.
|
||||
The VI filtering is always turned on if game requests it, but features can selectively be turned off for the pixel purists.
|
||||
|
||||
## Environment variables for development / testing
|
||||
|
||||
### `RDP_DEBUG` / `RDP_DEBUG_X` / `RDP_DEBUG_Y`
|
||||
|
||||
Supports printf in shaders, which is extremely useful to drill down difficult bugs.
|
||||
Only printfs from certain pixels can be filtered through to avoid spam.
|
||||
|
||||
### `VI_DEBUG` / `VI_DEBUG_X` / `VI_DEBUG_Y`
|
||||
|
||||
Same as `RDP_DEBUG` but for the VI.
|
||||
|
||||
### `PARALLEL_RDP_MEASURE_SYNC_TIME`
|
||||
|
||||
Measures time stalled in `CommandProcessor::wait_for_timeline`. Useful to measure
|
||||
CPU overhead in hard-synced emulator integrations.
|
||||
|
||||
### `PARALLEL_RDP_SMALL_TYPES=0`
|
||||
|
||||
Force-disables 8/16-bit arithmetic support. Useful when suspecting driver bugs.
|
||||
|
||||
### `PARALLEL_RDP_UBERSHADER=1`
|
||||
|
||||
Forces the use of ubershaders. Can be extremely slow depending on the shader compiler.
|
||||
|
||||
### `PARALLEL_RDP_FORCE_SYNC_SHADER=1`
|
||||
|
||||
Disabled async pipeline optimization, and blocks for every shader compiler.
|
||||
Only use if the ubershader crashes, since this adds the dreaded shader compilation stalls.
|
||||
|
||||
### `PARALLEL_RDP_BENCH=1`
|
||||
|
||||
Measures RDP rendering time spent on GPU using Vulkan timestamps.
|
||||
At end of a run, reports average time spent per render pass,
|
||||
and how many render passes are flushed per frame.
|
||||
|
||||
### `PARALLEL_RDP_SUBGROUP=0`
|
||||
|
||||
Force-disables use of Vulkan subgroup operations,
|
||||
which are used to optimize the tile binning algorithm.
|
||||
|
||||
### `PARALLEL_RDP_ALLOW_EXTERNAL_HOST=0`
|
||||
|
||||
Disables use of `VK_EXT_external_memory_host`. For testing.
|
||||
|
||||
## Vulkan driver requirements
|
||||
|
||||
paraLLEl-RDP requires up-to-date Vulkan implementations. A lot of the great improvements over the previous implementation
|
||||
comes from the idea that we can implement N64's UMA by simply importing RDRAM directly as an SSBO and perform 8 and 16-bit
|
||||
data access over the bus. With the tile based architecture in paraLLEl-RDP, this works very well and actual
|
||||
PCI-e traffic is massively reduced. The bandwidth for doing this is also trivial. On iGPU systems, this also works really well, since
|
||||
it's all the same memory anyways.
|
||||
|
||||
Thus, the requirements are as follows. All of these features are widely supported, or will soon be in drivers.
|
||||
paraLLEl-RDP does not aim for compatibility with ancient hardware and drivers.
|
||||
Just use the reference renderer for that. This is enthusiast software for a niche audience.
|
||||
|
||||
- Vulkan 1.1
|
||||
- VK_KHR_8bit_storage / VK_KHR_16bit_storage
|
||||
- Optionally VK_KHR_shader_float16_int8 which enables small integer arithmetic
|
||||
- Optionally subgroup support with VK_EXT_subgroup_size_control
|
||||
- For integration in emulators, VK_EXT_external_memory_host is currently required (may be relaxed later at some performance cost)
|
||||
|
||||
### Tested drivers
|
||||
|
||||
paraLLEl-RDP has been tested on Linux and Windows on all desktop vendors.
|
||||
|
||||
- Intel Mesa (20.0.6) - Passes conformance
|
||||
- Intel Windows - Passes conformance (**CAVEAT**. Intel Windows requires 64 KiB alignment for host memory import, make sure to add some padding around RDRAM in an emulator to make this work well.)
|
||||
- AMD RADV LLVM (20.0.6) - Passes conformance
|
||||
- AMD RADV ACO - Passes conformance with bleeding edge drivers and `PARALLEL_RDP_SMALL_TYPES=0`.
|
||||
- Linux AMDGPU-PRO - Passes conformance, with caveat that 8/16-bit arithmetic does not work correctly for some tests.
|
||||
paraLLEl-RDP automatically disables small integer arithmetic for proprietary AMD driver.
|
||||
- AMD Windows - Passes conformance with same caveat and workaround as AMDGPU-PRO.
|
||||
- NVIDIA Linux - Passes conformance (**MAJOR CAVEAT**, NVIDIA Linux does not support VK_EXT_external_memory_host as of 2020-05-12.)
|
||||
- NVIDIA Windows - Passes conformance
|
||||
|
||||
## Implementation strategy
|
||||
|
||||
This project uses Vulkan compute shaders to implement a fully programmable rasterization pipeline.
|
||||
The overall rendering architecture is reused from [RetroWarp](https://github.com/Themaister/RetroWarp)
|
||||
with some further refinements.
|
||||
|
||||
The lower level Vulkan backend comes from [Granite](https://github.com/Themaister/Granite).
|
||||
|
||||
### Asynchronous pipeline optimization
|
||||
|
||||
Toggleable paths in RDP state is expressed as specialization constants. The rendering thread will
|
||||
detect new state combinations and kick off building pipelines which only specify exact state needed to render.
|
||||
This is a massive performance optimization.
|
||||
|
||||
The same shaders are used for an "ubershader" fallback when pipelines are not ready.
|
||||
In this case, specialization constants are simply not used.
|
||||
The same SPIR-V modules are reused to great effect using this Vulkan feature.
|
||||
|
||||
### Tile-based rendering
|
||||
|
||||
See [RetroWarp](https://github.com/Themaister/RetroWarp) for more details.
|
||||
|
||||
### GPU-driven TMEM management
|
||||
|
||||
TMEM management is fully GPU-driven, but this is a very complicated implementation.
|
||||
Certain combinations of formats are not supported, but such cases would produce
|
||||
meaningless results, and it is unclear that applications can make meaningful use of these "weird" uploads.
|
||||
|
||||
### Synchronization
|
||||
|
||||
Synchronizing the GPU and CPU emulation is one of the hot button issues of N64 emulation.
|
||||
The integration code is designed around a timeline of synchronization points which can be waited on by the CPU
|
||||
when appropriate. For accurate emulation, an OpSyncFull is generally followed by a full wait,
|
||||
but most games can be more relaxed and only synchronize with the CPU N frames later.
|
||||
Implementation of this behavior is outside the scope of paraLLEl-RDP, and is left up to the integration code.
|
||||
|
||||
### Asynchronous compute
|
||||
|
||||
GPUs with a dedicated compute queue is recommended for optimal performance since
|
||||
RDP shading work can happen on the compute queue, and won't be blocked by graphics workloads happening
|
||||
in the graphics queue, which will typically be VI scanout and frontend applying shaders on top.
|
||||
|
||||
## Project structure
|
||||
|
||||
This project implements several submodules which are quite useful.
|
||||
|
||||
### rdp-replayer
|
||||
|
||||
This app replays RDP dump files, which are produced by running content through an RDP dumper.
|
||||
An implementation can be found in e.g. parallel-N64. The file format is very simple and essentially
|
||||
contains a record of RDRAM changes and RDP command streams.
|
||||
This dump is replayed and a live comparison between the reference renderer can be compared to paraLLEl-RDP
|
||||
with visual output. The UI is extremely crude, and is not user-friendly, but good enough for my use.
|
||||
|
||||
### rdp-conformance
|
||||
|
||||
I made a somewhat comprehensive test suite for the RDP, with a custom higher level RDP command stream generator.
|
||||
There are roughly ~150 fuzz tests which exercise many aspects of the RDP.
|
||||
In order to pass the test, paraLLEl-RDP must produce bit-exact results compared to Angrylion,
|
||||
so the test condition is as stringent as possible.
|
||||
|
||||
#### A note on bitexactness
|
||||
|
||||
There are a few cases where bit-exactness is a meaningless term, such as the noise feature of the RDP.
|
||||
It is not particularly meaningful to exactly reproduce noise, since it is by its very nature unpredictable.
|
||||
For that reason, this repo references a fork of the reference renderer which implements deterministic "undefined behavior"
|
||||
where appropriate. The exact formulation of the noise generator is not very interesting as long as
|
||||
correct entropy and output range is reproduced.
|
||||
|
||||
##### Intentional differences from reference renderer
|
||||
|
||||
Certain effects invoke "undefined behavior" in the RDP and requires cycle accuracy to resolve bit-accurately with real RDP.
|
||||
Reference renderer attempts to emulate these effects, but to reproduce this behavior breaks any form of multi-threading.
|
||||
To be able to validate dumps in a sensible way with buggy content, I modified the reference slightly to make certain
|
||||
"undefined behavior" deterministic. This doesn't meaningfully change the rendered output in the cases I've seen in the wild.
|
||||
Some of these effects would be possible to emulate,
|
||||
but at the cost of lots of added complexity and it wouldn't be quite correct anyways given the cycle accuracy issue.
|
||||
|
||||
- CombinedColor/Alpha in first cycle is cleared to zero. Some games read this in first cycle,
|
||||
and reference renderer will read whatever was generated last pixel.
|
||||
This causes issues in some cases, where cycle accuracy would have caused the feedback to converge to zero over time.
|
||||
- Reading LODFrac in 1 cycle mode. This is currently ignored. The results generated seem non-sensical. Never seen this in the wild.
|
||||
- Using TexLOD in copy mode. This is currently ignored. The results generated seem non-sensical. Never seen this in the wild.
|
||||
- Reading MemoryColor in first blender cycle in 2-cycle mode. Reference seems to wait until the second cycle before updating this value,
|
||||
despite memory coverage being updated right away. The sensible thing to do is to allow reading memory color in first cycle.
|
||||
- Alpha testing in 2-cycle mode reads combined alpha from next pixel in reference.
|
||||
Just doing alpha testing in first cycle on current pixel is good enough.
|
||||
If this is correct hardware behavior, I consider this a hardware bug.
|
||||
- Reading Texel1 in cycle 1 of 2-cycle mode reads the Texel0 from next pixel.
|
||||
In the few cases I've seen this, the rendered output is slightly buggy, but it's hardly visible in motion.
|
||||
The workaround is just to read Texel0 from current pixel which still renders fine.
|
||||
|
||||
### vi-conformance
|
||||
|
||||
This is a conformance suite, except for the video interface (VI) unit.
|
||||
|
||||
### rdp-validate-dump
|
||||
|
||||
This tool replays an RDP dump headless and compares outputs between reference renderer and paraLLEl-RDP.
|
||||
To pass, bitexact output must be generated.
|
||||
|
||||
## Build
|
||||
|
||||
Checkout submodules. This pulls in Angrylion-Plus as well as Granite.
|
||||
|
||||
```
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Standard CMake build.
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
cmake --build . --parallel (--config Release on MSVC)
|
||||
```
|
||||
|
||||
### Run test suite
|
||||
|
||||
You can run rdp-conformance and vi-conformance with ctest to verify if your driver is behaving correctly.
|
||||
|
||||
```
|
||||
ctest (-C Release on MSVC)
|
||||
```
|
||||
|
||||
### Embedding shaders in a C++ header
|
||||
|
||||
If embedding paraLLEl-RDP in an emulator project, it is helpful to pre-compile and bake SPIR-V shaders in a C++ header.
|
||||
Build slangmosh from Granite, and then run:
|
||||
|
||||
```
|
||||
slangmosh parallel-rdp/shaders/slangmosh.json --output slangmosh.hpp --vk11 --strip -O --namespace RDP
|
||||
```
|
||||
|
||||
### Generating a standalone code base for emulator integration
|
||||
|
||||
Run the `generate_standalone_codebase.sh $OUTDIR` script with an output directory `$OUTDIR/` as argument to generate a standalone code base which can be built without any special build system support.
|
||||
Include `$OUTDIR/config.mk` if building with Make to make your life easier.
|
||||
Note that `slangmosh` must be in your path for this script to run. It executes the command above to build `slangmosh.hpp`.
|
||||
|
||||
## License
|
||||
|
||||
paraLLEl-RDP is licensed under the permissive license MIT. See included LICENSE file.
|
||||
This implementation builds heavily on the knowledge (but not code) gained from studying the reference implementation,
|
||||
thus it felt fair to release it under a permissive license, so my work could be reused more easily.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# For use in standalone implementations.
|
||||
|
||||
PARALLEL_RDP_CFLAGS :=
|
||||
PARALLEL_RDP_CXXFLAGS :=
|
||||
|
||||
PARALLEL_RDP_SOURCES_CXX := \
|
||||
$(wildcard $(PARALLEL_RDP_IMPLEMENTATION)/parallel-rdp/*.cpp) \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/buffer.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/buffer_pool.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/command_buffer.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/command_pool.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/context.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/cookie.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/descriptor_set.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/device.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/event_manager.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/fence.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/fence_manager.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/image.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/indirect_layout.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/memory_allocator.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/pipeline_event.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/query_pool.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/render_pass.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/sampler.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/semaphore.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/semaphore_manager.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/shader.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/vulkan/texture/texture_format.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/arena_allocator.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/logging.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/thread_id.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/aligned_alloc.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/timer.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/timeline_trace_file.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/environment.cpp \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/util/thread_name.cpp
|
||||
|
||||
PARALLEL_RDP_SOURCES_C := \
|
||||
$(PARALLEL_RDP_IMPLEMENTATION)/volk/volk.c
|
||||
|
||||
PARALLEL_RDP_INCLUDE_DIRS := \
|
||||
-I$(PARALLEL_RDP_IMPLEMENTATION)/parallel-rdp \
|
||||
-I$(PARALLEL_RDP_IMPLEMENTATION)/volk \
|
||||
-I$(PARALLEL_RDP_IMPLEMENTATION)/vulkan \
|
||||
-I$(PARALLEL_RDP_IMPLEMENTATION)/vulkan-headers/include \
|
||||
-I$(PARALLEL_RDP_IMPLEMENTATION)/util
|
||||
|
||||
PARALLEL_RDP_LDFLAGS := -pthread
|
||||
ifeq (,$(findstring win,$(platform)))
|
||||
PARALLEL_RDP_LDFLAGS += -ldl
|
||||
else
|
||||
PARALLEL_RDP_CFLAGS += -DVK_USE_PLATFORM_WIN32_KHR
|
||||
PARALLEL_RDP_LDFLAGS += -lwinmm
|
||||
endif
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <chrono>
|
||||
#include "command_ring.hpp"
|
||||
#include "rdp_device.hpp"
|
||||
#include "thread_id.hpp"
|
||||
#include <assert.h>
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
void CommandRing::init(
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
Granite::Global::GlobalManagersHandle global_handles_,
|
||||
#endif
|
||||
CommandProcessor *processor_, unsigned count)
|
||||
{
|
||||
assert((count & (count - 1)) == 0);
|
||||
teardown_thread();
|
||||
processor = processor_;
|
||||
ring.resize(count);
|
||||
write_count = 0;
|
||||
read_count = 0;
|
||||
published = 0;
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
global_handles = std::move(global_handles_);
|
||||
#endif
|
||||
thr = std::thread(&CommandRing::thread_loop, this);
|
||||
}
|
||||
|
||||
void CommandRing::teardown_thread()
|
||||
{
|
||||
if (thr.joinable())
|
||||
{
|
||||
enqueue_command(0, nullptr);
|
||||
thr.join();
|
||||
}
|
||||
}
|
||||
|
||||
CommandRing::~CommandRing()
|
||||
{
|
||||
teardown_thread();
|
||||
}
|
||||
|
||||
void CommandRing::drain()
|
||||
{
|
||||
std::unique_lock<std::mutex> holder{lock};
|
||||
while (write_count != completed_count)
|
||||
{
|
||||
producer_waiting = true;
|
||||
cond_space.wait(holder);
|
||||
}
|
||||
producer_waiting = false;
|
||||
}
|
||||
|
||||
void CommandRing::wait_for_room(std::unique_lock<std::mutex> &holder, size_t count)
|
||||
{
|
||||
assert(count <= ring.size());
|
||||
if (write_count + count > read_count + ring.size())
|
||||
{
|
||||
const uint64_t t0 = __builtin_ia32_rdtsc();
|
||||
while (write_count + count > read_count + ring.size())
|
||||
{
|
||||
producer_waiting = true;
|
||||
cond_space.wait(holder);
|
||||
}
|
||||
producer_wait_ticks.fetch_add(__builtin_ia32_rdtsc() - t0, std::memory_order_relaxed);
|
||||
}
|
||||
producer_waiting = false;
|
||||
}
|
||||
|
||||
void CommandRing::publish()
|
||||
{
|
||||
published.store(write_count, std::memory_order_release);
|
||||
// Mupen64Plus PS5: wake the worker only when it sleeps, once.
|
||||
if (worker_waiting)
|
||||
{
|
||||
worker_waiting = false;
|
||||
wakeups.fetch_add(1, std::memory_order_relaxed);
|
||||
cond_data.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void CommandRing::enqueue_command(unsigned num_words, const uint32_t *words)
|
||||
{
|
||||
std::unique_lock<std::mutex> holder{lock};
|
||||
wait_for_room(holder, num_words + 1);
|
||||
|
||||
size_t mask = ring.size() - 1;
|
||||
ring[write_count++ & mask] = num_words;
|
||||
for (unsigned i = 0; i < num_words; i++)
|
||||
ring[write_count++ & mask] = words[i];
|
||||
|
||||
publish();
|
||||
}
|
||||
|
||||
void CommandRing::enqueue_commands(size_t count, const uint32_t *framed)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
std::unique_lock<std::mutex> holder{lock};
|
||||
wait_for_room(holder, count);
|
||||
|
||||
size_t mask = ring.size() - 1;
|
||||
for (size_t i = 0; i < count; i++)
|
||||
ring[write_count++ & mask] = framed[i];
|
||||
|
||||
publish();
|
||||
}
|
||||
|
||||
void CommandRing::thread_loop()
|
||||
{
|
||||
Util::register_thread_index(0);
|
||||
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
// Here to let the RDP play nice with full Granite.
|
||||
// When we move to standalone Granite, we won't need to interact with global subsystems like this.
|
||||
Granite::Global::set_thread_context(*global_handles);
|
||||
global_handles.reset();
|
||||
#endif
|
||||
|
||||
// Mupen64Plus PS5: every command queued so far is taken in one go, and processed outside the lock.
|
||||
std::vector<uint32_t> batch;
|
||||
batch.reserve(ring.size());
|
||||
size_t mask = ring.size() - 1;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
bool is_idle = false;
|
||||
uint64_t batch_end = 0;
|
||||
// Mupen64Plus PS5: spin a little (~40 us of PAUSEs on Zen 2) before sleeping, so the emulator rarely
|
||||
// has to wake this thread (a syscall each time). read_count is only written by this thread.
|
||||
if (published.load(std::memory_order_acquire) == read_count)
|
||||
for (unsigned i = 0; i < 2048 && published.load(std::memory_order_relaxed) == read_count; i++)
|
||||
__builtin_ia32_pause();
|
||||
{
|
||||
std::unique_lock<std::mutex> holder{lock};
|
||||
if (write_count == read_count)
|
||||
{
|
||||
worker_waiting = true;
|
||||
cond_data.wait_for(holder, std::chrono::microseconds(500), [this]() { return write_count > read_count; });
|
||||
worker_waiting = false;
|
||||
}
|
||||
if (write_count > read_count)
|
||||
{
|
||||
batch.resize(size_t(write_count - read_count));
|
||||
for (size_t i = 0; i < batch.size(); i++)
|
||||
batch[i] = ring[read_count++ & mask];
|
||||
batch_end = read_count;
|
||||
// room in the ring again
|
||||
if (producer_waiting)
|
||||
cond_space.notify_one();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we don't receive commands at a steady pace,
|
||||
// notify rendering thread that we should probably kick some work.
|
||||
batch.resize(1);
|
||||
batch[0] = 0;
|
||||
is_idle = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_idle)
|
||||
{
|
||||
uint32_t idle = uint32_t(Op::MetaIdle) << 24;
|
||||
processor->enqueue_command_direct(1, &idle);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t busy0 = __builtin_ia32_rdtsc();
|
||||
batches.fetch_add(1, std::memory_order_relaxed);
|
||||
words_in.fetch_add(batch.size(), std::memory_order_relaxed);
|
||||
bool quit = false;
|
||||
for (size_t i = 0; i < batch.size();)
|
||||
{
|
||||
uint32_t num_words = batch[i++];
|
||||
if (num_words == 0)
|
||||
{
|
||||
quit = true;
|
||||
break;
|
||||
}
|
||||
processor->enqueue_command_direct(num_words, batch.data() + i);
|
||||
i += num_words;
|
||||
}
|
||||
worker_busy_ticks.fetch_add(__builtin_ia32_rdtsc() - busy0, std::memory_order_relaxed);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> holder{lock};
|
||||
completed_count = batch_end;
|
||||
if (producer_waiting)
|
||||
cond_space.notify_one();
|
||||
}
|
||||
if (quit)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
#include "global_managers.hpp"
|
||||
#endif
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
class CommandProcessor;
|
||||
class CommandRing
|
||||
{
|
||||
public:
|
||||
void init(
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
Granite::Global::GlobalManagersHandle global_handles,
|
||||
#endif
|
||||
CommandProcessor *processor, unsigned count);
|
||||
~CommandRing();
|
||||
void drain();
|
||||
|
||||
void enqueue_command(unsigned num_words, const uint32_t *words);
|
||||
// Mupen64Plus PS5: several commands under one lock, framed as they sit in the ring (num_words, words...);
|
||||
// count is at most the ring's size.
|
||||
void enqueue_commands(size_t count, const uint32_t *framed);
|
||||
|
||||
// Mupen64Plus PS5: where the time goes (rdtsc ticks), read and reset by the plugin's report.
|
||||
std::atomic<uint64_t> producer_wait_ticks{0}; // enqueue_command waiting for room
|
||||
std::atomic<uint64_t> worker_busy_ticks{0}; // the worker processing commands
|
||||
std::atomic<uint64_t> batches{0}, words_in{0};
|
||||
std::atomic<uint64_t> wakeups{0}; // the producer waking the sleeping worker
|
||||
|
||||
private:
|
||||
CommandProcessor *processor = nullptr;
|
||||
std::thread thr;
|
||||
std::mutex lock;
|
||||
// Mupen64Plus PS5: one condition variable each way, signalled only when the other side sleeps, and the
|
||||
// worker takes every queued command at once (command_ring.cpp): a wake-up per RDP command cost more
|
||||
// than the GPU's work on the PS5.
|
||||
std::condition_variable cond_data; // the worker waits for commands
|
||||
std::condition_variable cond_space; // the producer waits for room, or for drain()
|
||||
bool worker_waiting = false;
|
||||
bool producer_waiting = false;
|
||||
std::atomic<uint64_t> published{0}; // write_count, for the worker's spin without the lock
|
||||
void wait_for_room(std::unique_lock<std::mutex> &holder, size_t count);
|
||||
void publish();
|
||||
|
||||
std::vector<uint32_t> ring;
|
||||
uint64_t write_count = 0;
|
||||
uint64_t read_count = 0;
|
||||
uint64_t completed_count = 0;
|
||||
|
||||
void thread_loop();
|
||||
void teardown_thread();
|
||||
#ifdef PARALLEL_RDP_SHADER_DIR
|
||||
Granite::Global::GlobalManagersHandle global_handles;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
+1556
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,410 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Vulkan
|
||||
{
|
||||
class Program;
|
||||
class Shader;
|
||||
}
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
template <typename Program, typename Shader> struct Shaders;
|
||||
using ShaderBank = Shaders<Vulkan::Program *, Vulkan::Shader *>;
|
||||
|
||||
// list of command IDs
|
||||
enum class Op
|
||||
{
|
||||
Nop = 0,
|
||||
|
||||
MetaSignalTimeline = 1,
|
||||
MetaFlush = 2,
|
||||
MetaIdle = 3,
|
||||
MetaSetQuirks = 4,
|
||||
|
||||
FillTriangle = 0x08,
|
||||
FillZBufferTriangle = 0x09,
|
||||
TextureTriangle = 0x0a,
|
||||
TextureZBufferTriangle = 0x0b,
|
||||
ShadeTriangle = 0x0c,
|
||||
ShadeZBufferTriangle = 0x0d,
|
||||
ShadeTextureTriangle = 0x0e,
|
||||
ShadeTextureZBufferTriangle = 0x0f,
|
||||
TextureRectangle = 0x24,
|
||||
TextureRectangleFlip = 0x25,
|
||||
SyncLoad = 0x26,
|
||||
SyncPipe = 0x27,
|
||||
SyncTile = 0x28,
|
||||
SyncFull = 0x29,
|
||||
SetKeyGB = 0x2a,
|
||||
SetKeyR = 0x2b,
|
||||
SetConvert = 0x2c,
|
||||
SetScissor = 0x2d,
|
||||
SetPrimDepth = 0x2e,
|
||||
SetOtherModes = 0x2f,
|
||||
LoadTLut = 0x30,
|
||||
SetTileSize = 0x32,
|
||||
LoadBlock = 0x33,
|
||||
LoadTile = 0x34,
|
||||
SetTile = 0x35,
|
||||
FillRectangle = 0x36,
|
||||
SetFillColor = 0x37,
|
||||
SetFogColor = 0x38,
|
||||
SetBlendColor = 0x39,
|
||||
SetPrimColor = 0x3a,
|
||||
SetEnvColor = 0x3b,
|
||||
SetCombine = 0x3c,
|
||||
SetTextureImage = 0x3d,
|
||||
SetMaskImage = 0x3e,
|
||||
SetColorImage = 0x3f
|
||||
};
|
||||
|
||||
enum class RGBMul : uint8_t
|
||||
{
|
||||
Combined = 0,
|
||||
Texel0 = 1,
|
||||
Texel1 = 2,
|
||||
Primitive = 3,
|
||||
Shade = 4,
|
||||
Env = 5,
|
||||
KeyScale = 6,
|
||||
CombinedAlpha = 7,
|
||||
Texel0Alpha = 8,
|
||||
Texel1Alpha = 9,
|
||||
PrimitiveAlpha = 10,
|
||||
ShadeAlpha = 11,
|
||||
EnvAlpha = 12,
|
||||
LODFrac = 13,
|
||||
PrimLODFrac = 14,
|
||||
ConvertK5 = 15,
|
||||
Zero = 16
|
||||
};
|
||||
|
||||
enum class RGBMulAdd : uint8_t
|
||||
{
|
||||
Combined = 0,
|
||||
Texel0 = 1,
|
||||
Texel1 = 2,
|
||||
Primitive = 3,
|
||||
Shade = 4,
|
||||
Env = 5,
|
||||
One = 6,
|
||||
Noise = 7,
|
||||
Zero = 8
|
||||
};
|
||||
|
||||
enum class RGBMulSub : uint8_t
|
||||
{
|
||||
Combined = 0,
|
||||
Texel0 = 1,
|
||||
Texel1 = 2,
|
||||
Primitive = 3,
|
||||
Shade = 4,
|
||||
Env = 5,
|
||||
KeyCenter = 6,
|
||||
ConvertK4 = 7,
|
||||
Zero = 8
|
||||
};
|
||||
|
||||
enum class RGBAdd : uint8_t
|
||||
{
|
||||
Combined = 0,
|
||||
Texel0 = 1,
|
||||
Texel1 = 2,
|
||||
Primitive = 3,
|
||||
Shade = 4,
|
||||
Env = 5,
|
||||
One = 6,
|
||||
Zero = 7
|
||||
};
|
||||
|
||||
enum class AlphaAddSub : uint8_t
|
||||
{
|
||||
CombinedAlpha = 0,
|
||||
Texel0Alpha = 1,
|
||||
Texel1Alpha = 2,
|
||||
PrimitiveAlpha = 3,
|
||||
ShadeAlpha = 4,
|
||||
EnvAlpha = 5,
|
||||
One = 6,
|
||||
Zero = 7
|
||||
};
|
||||
|
||||
enum class AlphaMul : uint8_t
|
||||
{
|
||||
LODFrac = 0,
|
||||
Texel0Alpha = 1,
|
||||
Texel1Alpha = 2,
|
||||
PrimitiveAlpha = 3,
|
||||
ShadeAlpha = 4,
|
||||
EnvAlpha = 5,
|
||||
PrimLODFrac = 6,
|
||||
Zero = 7
|
||||
};
|
||||
|
||||
enum class TextureSize : uint8_t
|
||||
{
|
||||
Bpp4 = 0,
|
||||
Bpp8 = 1,
|
||||
Bpp16 = 2,
|
||||
Bpp32 = 3
|
||||
};
|
||||
|
||||
enum class TextureFormat : uint8_t
|
||||
{
|
||||
RGBA = 0,
|
||||
YUV = 1,
|
||||
CI = 2,
|
||||
IA = 3,
|
||||
I = 4
|
||||
};
|
||||
|
||||
enum class RGBDitherMode : uint8_t
|
||||
{
|
||||
Magic = 0,
|
||||
Bayer = 1,
|
||||
Noise = 2,
|
||||
Off = 3
|
||||
};
|
||||
|
||||
enum class AlphaDitherMode : uint8_t
|
||||
{
|
||||
Pattern = 0,
|
||||
InvPattern = 1,
|
||||
Noise = 2,
|
||||
Off = 3
|
||||
};
|
||||
|
||||
enum class CycleType : uint8_t
|
||||
{
|
||||
Cycle1 = 0,
|
||||
Cycle2 = 1,
|
||||
Copy = 2,
|
||||
Fill = 3
|
||||
};
|
||||
|
||||
enum class BlendMode1A : uint8_t
|
||||
{
|
||||
PixelColor = 0,
|
||||
MemoryColor = 1,
|
||||
BlendColor = 2,
|
||||
FogColor = 3
|
||||
};
|
||||
|
||||
enum class BlendMode1B : uint8_t
|
||||
{
|
||||
PixelAlpha = 0,
|
||||
FogAlpha = 1,
|
||||
ShadeAlpha = 2,
|
||||
Zero = 3
|
||||
};
|
||||
|
||||
enum class BlendMode2A : uint8_t
|
||||
{
|
||||
PixelColor = 0,
|
||||
MemoryColor = 1,
|
||||
BlendColor = 2,
|
||||
FogColor = 3
|
||||
};
|
||||
|
||||
enum class BlendMode2B : uint8_t
|
||||
{
|
||||
InvPixelAlpha = 0,
|
||||
MemoryAlpha = 1,
|
||||
One = 2,
|
||||
Zero = 3
|
||||
};
|
||||
|
||||
enum class CoverageMode : uint8_t
|
||||
{
|
||||
Clamp = 0,
|
||||
Wrap = 1,
|
||||
Zap = 2,
|
||||
Save = 3
|
||||
};
|
||||
|
||||
enum class ZMode : uint8_t
|
||||
{
|
||||
Opaque = 0,
|
||||
Interpenetrating = 1,
|
||||
Transparent = 2,
|
||||
Decal = 3
|
||||
};
|
||||
|
||||
enum TileInfoFlagBits
|
||||
{
|
||||
TILE_INFO_CLAMP_S_BIT = 1 << 0,
|
||||
TILE_INFO_MIRROR_S_BIT = 1 << 1,
|
||||
TILE_INFO_CLAMP_T_BIT = 1 << 2,
|
||||
TILE_INFO_MIRROR_T_BIT = 1 << 3
|
||||
};
|
||||
using TileInfoFlags = uint8_t;
|
||||
|
||||
struct TileSize
|
||||
{
|
||||
uint32_t slo = 0;
|
||||
uint32_t shi = 0;
|
||||
uint32_t tlo = 0;
|
||||
uint32_t thi = 0;
|
||||
};
|
||||
|
||||
struct TileMeta
|
||||
{
|
||||
uint32_t offset = 0;
|
||||
uint32_t stride = 0;
|
||||
TextureFormat fmt = TextureFormat::RGBA;
|
||||
TextureSize size = TextureSize::Bpp16;
|
||||
uint8_t palette = 0;
|
||||
uint8_t mask_s = 0;
|
||||
uint8_t shift_s = 0;
|
||||
uint8_t mask_t = 0;
|
||||
uint8_t shift_t = 0;
|
||||
TileInfoFlags flags = 0;
|
||||
};
|
||||
|
||||
struct TileInfo
|
||||
{
|
||||
TileSize size;
|
||||
TileMeta meta;
|
||||
};
|
||||
|
||||
struct CombinerInputsRGB
|
||||
{
|
||||
RGBMulAdd muladd;
|
||||
RGBMulSub mulsub;
|
||||
RGBMul mul;
|
||||
RGBAdd add;
|
||||
};
|
||||
|
||||
struct CombinerInputsAlpha
|
||||
{
|
||||
AlphaAddSub muladd;
|
||||
AlphaAddSub mulsub;
|
||||
AlphaMul mul;
|
||||
AlphaAddSub add;
|
||||
};
|
||||
|
||||
struct CombinerInputs
|
||||
{
|
||||
CombinerInputsRGB rgb;
|
||||
CombinerInputsAlpha alpha;
|
||||
};
|
||||
|
||||
struct BlendModes
|
||||
{
|
||||
BlendMode1A blend_1a;
|
||||
BlendMode1B blend_1b;
|
||||
BlendMode2A blend_2a;
|
||||
BlendMode2B blend_2b;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TileInfo) == 32, "TileInfo must be 32 bytes.");
|
||||
|
||||
enum class VIRegister
|
||||
{
|
||||
Control = 0,
|
||||
Origin,
|
||||
Width,
|
||||
Intr,
|
||||
VCurrentLine,
|
||||
Timing,
|
||||
VSync,
|
||||
HSync,
|
||||
Leap,
|
||||
HStart,
|
||||
VStart,
|
||||
VBurst,
|
||||
XScale,
|
||||
YScale,
|
||||
Count
|
||||
};
|
||||
|
||||
enum VIControlFlagBits
|
||||
{
|
||||
VI_CONTROL_TYPE_BLANK_BIT = 0 << 0,
|
||||
VI_CONTROL_TYPE_RESERVED_BIT = 1 << 0,
|
||||
VI_CONTROL_TYPE_RGBA5551_BIT = 2 << 0,
|
||||
VI_CONTROL_TYPE_RGBA8888_BIT = 3 << 0,
|
||||
VI_CONTROL_TYPE_MASK = 3 << 0,
|
||||
VI_CONTROL_GAMMA_DITHER_ENABLE_BIT = 1 << 2,
|
||||
VI_CONTROL_GAMMA_ENABLE_BIT = 1 << 3,
|
||||
VI_CONTROL_DIVOT_ENABLE_BIT = 1 << 4,
|
||||
VI_CONTROL_SERRATE_BIT = 1 << 6,
|
||||
VI_CONTROL_AA_MODE_RESAMP_EXTRA_ALWAYS_BIT = 0 << 8,
|
||||
VI_CONTROL_AA_MODE_RESAMP_EXTRA_BIT = 1 << 8,
|
||||
VI_CONTROL_AA_MODE_RESAMP_ONLY_BIT = 2 << 8,
|
||||
VI_CONTROL_AA_MODE_RESAMP_REPLICATE_BIT = 3 << 8,
|
||||
VI_CONTROL_AA_MODE_MASK = 3 << 8,
|
||||
VI_CONTROL_DITHER_FILTER_ENABLE_BIT = 1 << 16,
|
||||
VI_CONTROL_META_AA_BIT = 1 << 17,
|
||||
VI_CONTROL_META_SCALE_BIT = 1 << 18
|
||||
};
|
||||
using VIControlFlags = uint32_t;
|
||||
|
||||
static inline uint32_t make_vi_start_register(uint32_t start_value, uint32_t end_value)
|
||||
{
|
||||
return ((start_value & 0x3ff) << 16) | (end_value & 0x3ff);
|
||||
}
|
||||
|
||||
static inline uint32_t make_vi_scale_register(uint32_t scale_factor, uint32_t bias)
|
||||
{
|
||||
return ((bias & 0xfff) << 16) | (scale_factor & 0xfff);
|
||||
}
|
||||
|
||||
constexpr uint32_t VI_V_SYNC_NTSC = 525;
|
||||
constexpr uint32_t VI_V_SYNC_PAL = 625;
|
||||
constexpr uint32_t VI_H_OFFSET_NTSC = 108;
|
||||
constexpr uint32_t VI_H_OFFSET_PAL = 128;
|
||||
constexpr uint32_t VI_V_OFFSET_NTSC = 34;
|
||||
constexpr uint32_t VI_V_OFFSET_PAL = 44;
|
||||
constexpr uint32_t VI_V_RES_NTSC = 480;
|
||||
constexpr uint32_t VI_V_RES_PAL = 576;
|
||||
constexpr int VI_SCANOUT_WIDTH = 640;
|
||||
|
||||
constexpr uint32_t VI_V_RES_MAX = VI_V_RES_PAL > VI_V_RES_NTSC ? VI_V_RES_PAL : VI_V_RES_NTSC;
|
||||
// Handle odd v_start as well. Needed for rounding to work in our favor.
|
||||
constexpr uint32_t VI_V_END_PAL = (VI_V_OFFSET_PAL + VI_V_RES_PAL) | 1;
|
||||
constexpr uint32_t VI_V_END_NTSC = (VI_V_OFFSET_NTSC + VI_V_RES_NTSC) | 1;
|
||||
constexpr uint32_t VI_V_END_MAX = VI_V_END_PAL > VI_V_END_NTSC ? VI_V_END_PAL : VI_V_END_NTSC;
|
||||
|
||||
constexpr uint32_t VI_MAX_OUTPUT_SCANLINES = (VI_V_RES_MAX >> 1);
|
||||
|
||||
static inline uint32_t make_default_v_start()
|
||||
{
|
||||
return make_vi_start_register(VI_V_OFFSET_NTSC, VI_V_OFFSET_NTSC + 224 * 2);
|
||||
}
|
||||
|
||||
static inline uint32_t make_default_h_start()
|
||||
{
|
||||
return make_vi_start_register(VI_H_OFFSET_NTSC, VI_H_OFFSET_NTSC + VI_SCANOUT_WIDTH);
|
||||
}
|
||||
|
||||
template <int bits>
|
||||
static int32_t sext(int32_t v)
|
||||
{
|
||||
struct { int32_t dummy : bits; } d;
|
||||
d.dummy = v;
|
||||
return d.dummy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "rdp_common.hpp"
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
enum TriangleSetupFlagBits
|
||||
{
|
||||
TRIANGLE_SETUP_FLIP_BIT = 1 << 0,
|
||||
TRIANGLE_SETUP_DO_OFFSET_BIT = 1 << 1,
|
||||
TRIANGLE_SETUP_SKIP_XFRAC_BIT = 1 << 2,
|
||||
TRIANGLE_SETUP_INTERLACE_FIELD_BIT = 1 << 3,
|
||||
TRIANGLE_SETUP_INTERLACE_KEEP_ODD_BIT = 1 << 4,
|
||||
TRIANGLE_SETUP_DISABLE_UPSCALING_BIT = 1 << 5,
|
||||
TRIANGLE_SETUP_NATIVE_LOD_BIT = 1 << 6,
|
||||
TRIANGLE_SETUP_FILL_COPY_RASTER_BIT = 1 << 7
|
||||
};
|
||||
using TriangleSetupFlags = uint8_t;
|
||||
|
||||
enum StaticRasterizationFlagBits
|
||||
{
|
||||
RASTERIZATION_INTERLACE_FIELD_BIT = 1 << 0,
|
||||
RASTERIZATION_INTERLACE_KEEP_ODD_BIT = 1 << 1,
|
||||
RASTERIZATION_AA_BIT = 1 << 2,
|
||||
RASTERIZATION_PERSPECTIVE_CORRECT_BIT = 1 << 3,
|
||||
RASTERIZATION_TLUT_BIT = 1 << 4,
|
||||
RASTERIZATION_TLUT_TYPE_BIT = 1 << 5,
|
||||
RASTERIZATION_CVG_TIMES_ALPHA_BIT = 1 << 6,
|
||||
RASTERIZATION_ALPHA_CVG_SELECT_BIT = 1 << 7,
|
||||
RASTERIZATION_MULTI_CYCLE_BIT = 1 << 8,
|
||||
RASTERIZATION_TEX_LOD_ENABLE_BIT = 1 << 9,
|
||||
RASTERIZATION_SHARPEN_LOD_ENABLE_BIT = 1 << 10,
|
||||
RASTERIZATION_DETAIL_LOD_ENABLE_BIT = 1 << 11,
|
||||
RASTERIZATION_FILL_BIT = 1 << 12,
|
||||
RASTERIZATION_COPY_BIT = 1 << 13,
|
||||
RASTERIZATION_SAMPLE_MODE_BIT = 1 << 14,
|
||||
RASTERIZATION_ALPHA_TEST_BIT = 1 << 15,
|
||||
RASTERIZATION_ALPHA_TEST_DITHER_BIT = 1 << 16,
|
||||
RASTERIZATION_SAMPLE_MID_TEXEL_BIT = 1 << 17,
|
||||
RASTERIZATION_USES_TEXEL0_BIT = 1 << 18,
|
||||
RASTERIZATION_USES_TEXEL1_BIT = 1 << 19,
|
||||
RASTERIZATION_USES_LOD_BIT = 1 << 20,
|
||||
RASTERIZATION_USES_PIPELINED_TEXEL1_BIT = 1 << 21,
|
||||
RASTERIZATION_CONVERT_ONE_BIT = 1 << 22,
|
||||
RASTERIZATION_BILERP_0_BIT = 1 << 23,
|
||||
RASTERIZATION_BILERP_1_BIT = 1 << 24,
|
||||
RASTERIZATION_NEED_NOISE_DUAL_BIT = 1 << 25,
|
||||
RASTERIZATION_UPSCALING_LOG2_BIT_OFFSET = 26,
|
||||
// Bit 26 and 27 holds upscaling factor in LOG2.
|
||||
RASTERIZATION_NEED_NOISE_BIT = 1 << 28,
|
||||
RASTERIZATION_USE_STATIC_TEXTURE_SIZE_FORMAT_BIT = 1 << 29,
|
||||
RASTERIZATION_USE_SPECIALIZATION_CONSTANT_BIT = 1 << 30
|
||||
};
|
||||
using StaticRasterizationFlags = uint32_t;
|
||||
|
||||
enum DepthBlendFlagBits
|
||||
{
|
||||
DEPTH_BLEND_DEPTH_TEST_BIT = 1 << 0,
|
||||
DEPTH_BLEND_DEPTH_UPDATE_BIT = 1 << 1,
|
||||
DEPTH_BLEND_FORCE_BLEND_BIT = 1 << 3,
|
||||
DEPTH_BLEND_IMAGE_READ_ENABLE_BIT = 1 << 4,
|
||||
DEPTH_BLEND_COLOR_ON_COVERAGE_BIT = 1 << 5,
|
||||
DEPTH_BLEND_MULTI_CYCLE_BIT = 1 << 6,
|
||||
DEPTH_BLEND_AA_BIT = 1 << 7,
|
||||
DEPTH_BLEND_DITHER_ENABLE_BIT = 1 << 8
|
||||
};
|
||||
using DepthBlendFlags = uint32_t;
|
||||
|
||||
struct TriangleSetup
|
||||
{
|
||||
int32_t xh, xm, xl;
|
||||
int16_t yh, ym;
|
||||
|
||||
int32_t dxhdy, dxmdy, dxldy;
|
||||
int16_t yl;
|
||||
TriangleSetupFlags flags;
|
||||
uint8_t tile;
|
||||
};
|
||||
|
||||
struct AttributeSetup
|
||||
{
|
||||
int32_t r, g, b, a;
|
||||
int32_t drdx, dgdx, dbdx, dadx;
|
||||
int32_t drde, dgde, dbde, dade;
|
||||
int32_t drdy, dgdy, dbdy, dady;
|
||||
|
||||
int32_t s, t, z, w;
|
||||
int32_t dsdx, dtdx, dzdx, dwdx;
|
||||
int32_t dsde, dtde, dzde, dwde;
|
||||
int32_t dsdy, dtdy, dzdy, dwdy;
|
||||
};
|
||||
|
||||
struct ConstantCombinerInputs
|
||||
{
|
||||
uint8_t muladd[4];
|
||||
uint8_t mulsub[4];
|
||||
uint8_t mul[4];
|
||||
uint8_t add[4];
|
||||
};
|
||||
|
||||
// Per-primitive state which is very dynamic in nature and does not change anything about the shader itself.
|
||||
struct DerivedSetup
|
||||
{
|
||||
ConstantCombinerInputs constants[2];
|
||||
uint8_t fog_color[4];
|
||||
uint8_t blend_color[4];
|
||||
uint32_t fill_color;
|
||||
uint16_t dz;
|
||||
uint8_t dz_compressed;
|
||||
uint8_t min_lod;
|
||||
int16_t convert_factors[4];
|
||||
};
|
||||
|
||||
static_assert((sizeof(TriangleSetup) & 15) == 0, "TriangleSetup must be aligned to 16 bytes.");
|
||||
static_assert((sizeof(AttributeSetup) & 15) == 0, "AttributeSetup must be aligned to 16 bytes.");
|
||||
static_assert(sizeof(DerivedSetup) == 56, "DerivedSetup is not 56 bytes.");
|
||||
|
||||
struct ScissorState
|
||||
{
|
||||
uint32_t xlo;
|
||||
uint32_t ylo;
|
||||
uint32_t xhi;
|
||||
uint32_t yhi;
|
||||
};
|
||||
|
||||
struct StaticRasterizationState
|
||||
{
|
||||
CombinerInputs combiner[2];
|
||||
StaticRasterizationFlags flags;
|
||||
uint32_t dither;
|
||||
uint32_t texture_size;
|
||||
uint32_t texture_fmt;
|
||||
};
|
||||
static_assert(sizeof(StaticRasterizationState) == 32, "StaticRasterizationState must be 32 bytes.");
|
||||
|
||||
struct DepthBlendState
|
||||
{
|
||||
BlendModes blend_cycles[2];
|
||||
DepthBlendFlags flags;
|
||||
CoverageMode coverage_mode;
|
||||
ZMode z_mode;
|
||||
uint8_t padding[2];
|
||||
};
|
||||
static_assert(sizeof(DepthBlendState) == 16, "DepthBlendState must be 16 bytes.");
|
||||
|
||||
struct InstanceIndices
|
||||
{
|
||||
uint8_t static_index;
|
||||
uint8_t depth_blend_index;
|
||||
uint8_t tile_instance_index;
|
||||
uint8_t padding[5];
|
||||
uint8_t tile_indices[8];
|
||||
};
|
||||
static_assert((sizeof(InstanceIndices) & 15) == 0, "InstanceIndices must be aligned to 16 bytes.");
|
||||
|
||||
struct UploadInfo
|
||||
{
|
||||
int32_t width, height;
|
||||
float min_t_mod, max_t_mod;
|
||||
|
||||
int32_t vram_addr;
|
||||
int32_t vram_width;
|
||||
int32_t vram_size;
|
||||
int32_t vram_effective_width;
|
||||
|
||||
int32_t tmem_offset;
|
||||
int32_t tmem_stride_words;
|
||||
int32_t tmem_size;
|
||||
int32_t tmem_fmt;
|
||||
|
||||
int32_t mode;
|
||||
float inv_tmem_stride_words;
|
||||
int32_t dxt;
|
||||
int32_t padding;
|
||||
};
|
||||
static_assert((sizeof(UploadInfo) & 15) == 0, "UploadInfo must be aligned to 16 bytes.");
|
||||
|
||||
struct SpanSetup
|
||||
{
|
||||
int32_t r, g, b, a;
|
||||
int32_t s, t, w, z;
|
||||
|
||||
int16_t xlo[4];
|
||||
int16_t xhi[4];
|
||||
|
||||
int32_t interpolation_base_x;
|
||||
int32_t start_x;
|
||||
int32_t end_x;
|
||||
int16_t lodlength;
|
||||
uint16_t valid_line;
|
||||
};
|
||||
static_assert((sizeof(SpanSetup) & 15) == 0, "SpanSetup is not aligned to 16 bytes.");
|
||||
|
||||
struct SpanInfoOffsets
|
||||
{
|
||||
int32_t offset, ylo, yhi, padding;
|
||||
};
|
||||
static_assert((sizeof(SpanInfoOffsets) == 16), "SpanInfoOffsets is not 16 bytes.");
|
||||
|
||||
struct SpanInterpolationJob
|
||||
{
|
||||
uint16_t primitive_index, base_y, max_y, padding;
|
||||
};
|
||||
static_assert((sizeof(SpanInterpolationJob) == 8), "SpanInterpolationJob is not 8 bytes.");
|
||||
|
||||
struct GlobalState
|
||||
{
|
||||
uint32_t addr_index;
|
||||
uint32_t depth_addr_index;
|
||||
uint32_t fb_width, fb_height;
|
||||
uint32_t group_mask;
|
||||
};
|
||||
|
||||
struct TileRasterWork
|
||||
{
|
||||
uint32_t tile_x, tile_y;
|
||||
uint32_t tile_instance;
|
||||
uint32_t primitive;
|
||||
};
|
||||
static_assert((sizeof(TileRasterWork) == 16), "TileRasterWork is not 16 bytes.");
|
||||
|
||||
struct GlobalFBInfo
|
||||
{
|
||||
uint32_t dx_shift;
|
||||
uint32_t dx_mask;
|
||||
uint32_t fb_size;
|
||||
uint32_t base_primitive_index;
|
||||
};
|
||||
|
||||
template <typename T, unsigned N>
|
||||
class StateCache
|
||||
{
|
||||
public:
|
||||
unsigned add(const T &t)
|
||||
{
|
||||
if (cached_index >= 0)
|
||||
if (memcmp(&elements[cached_index], &t, sizeof(T)) == 0)
|
||||
return unsigned(cached_index);
|
||||
|
||||
for (int i = int(count) - 1; i >= 0; i--)
|
||||
{
|
||||
if (memcmp(&elements[i], &t, sizeof(T)) == 0)
|
||||
{
|
||||
cached_index = i;
|
||||
return unsigned(i);
|
||||
}
|
||||
}
|
||||
|
||||
assert(count < N);
|
||||
memcpy(elements + count, &t, sizeof(T));
|
||||
unsigned ret = count++;
|
||||
cached_index = int(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool full() const
|
||||
{
|
||||
return count == N;
|
||||
}
|
||||
|
||||
unsigned size() const
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
unsigned byte_size() const
|
||||
{
|
||||
return size() * sizeof(T);
|
||||
}
|
||||
|
||||
const T *data() const
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
count = 0;
|
||||
cached_index = -1;
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned count = 0;
|
||||
int cached_index = -1;
|
||||
T elements[N];
|
||||
};
|
||||
|
||||
template <typename T, unsigned N>
|
||||
class StreamCache
|
||||
{
|
||||
public:
|
||||
void add(const T &t)
|
||||
{
|
||||
assert(count < N);
|
||||
memcpy(&elements[count++], &t, sizeof(T));
|
||||
}
|
||||
|
||||
bool full() const
|
||||
{
|
||||
return count == N;
|
||||
}
|
||||
|
||||
unsigned size() const
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
unsigned byte_size() const
|
||||
{
|
||||
return size() * sizeof(T);
|
||||
}
|
||||
|
||||
const T *data() const
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
count = 0;
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned count = 0;
|
||||
T elements[N];
|
||||
};
|
||||
|
||||
namespace Limits
|
||||
{
|
||||
constexpr unsigned MaxPrimitives = 256;
|
||||
constexpr unsigned MaxStaticRasterizationStates = 64;
|
||||
constexpr unsigned MaxDepthBlendStates = 64;
|
||||
constexpr unsigned MaxTileInfoStates = 256;
|
||||
constexpr unsigned NumSyncStates = 32;
|
||||
constexpr unsigned MaxNumTiles = 8;
|
||||
constexpr unsigned MaxTMEMInstances = 256;
|
||||
constexpr unsigned MaxSpanSetups = 32 * 1024;
|
||||
constexpr unsigned MaxWidth = 1024;
|
||||
constexpr unsigned MaxHeight = 1024;
|
||||
constexpr unsigned MaxTileInstances = 0x8000;
|
||||
}
|
||||
|
||||
namespace ImplementationConstants
|
||||
{
|
||||
constexpr unsigned DefaultWorkgroupSize = 64;
|
||||
|
||||
constexpr unsigned TileWidth = 8;
|
||||
constexpr unsigned TileHeight = 8;
|
||||
constexpr unsigned MaxTilesX = Limits::MaxWidth / TileWidth;
|
||||
constexpr unsigned MaxTilesY = Limits::MaxHeight / TileHeight;
|
||||
constexpr unsigned IncoherentPageSize = 1024;
|
||||
constexpr unsigned MaxPendingRenderPassesBeforeFlush = 8;
|
||||
constexpr unsigned MinimumPrimitivesForIdleFlush = 32;
|
||||
constexpr unsigned MinimumRenderPassesForIdleFlush = 2;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,289 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <queue>
|
||||
#include "device.hpp"
|
||||
#include "video_interface.hpp"
|
||||
#include "rdp_renderer.hpp"
|
||||
#include "rdp_common.hpp"
|
||||
#include "command_ring.hpp"
|
||||
#include "worker_thread.hpp"
|
||||
#include "rdp_dump_write.hpp"
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
struct RGBA
|
||||
{
|
||||
uint8_t r, g, b, a;
|
||||
};
|
||||
|
||||
enum CommandProcessorFlagBits
|
||||
{
|
||||
COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_HIDDEN_RDRAM_BIT = 1 << 0,
|
||||
COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_TMEM_BIT = 1 << 1,
|
||||
COMMAND_PROCESSOR_FLAG_UPSCALING_2X_BIT = 1 << 2,
|
||||
COMMAND_PROCESSOR_FLAG_UPSCALING_4X_BIT = 1 << 3,
|
||||
COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT = 1 << 4,
|
||||
COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_READ_BACK_BIT = 1 << 5,
|
||||
COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT = 1 << 6
|
||||
};
|
||||
using CommandProcessorFlags = uint32_t;
|
||||
|
||||
struct CoherencyCopy
|
||||
{
|
||||
size_t src_offset = 0;
|
||||
size_t mask_offset = 0;
|
||||
size_t dst_offset = 0;
|
||||
size_t size = 0;
|
||||
std::atomic_uint32_t *counter_base = nullptr;
|
||||
unsigned counters = 0;
|
||||
};
|
||||
|
||||
struct CoherencyOperation
|
||||
{
|
||||
Vulkan::Fence fence;
|
||||
uint64_t timeline_value = 0;
|
||||
|
||||
uint8_t *dst = nullptr;
|
||||
const Vulkan::Buffer *src = nullptr;
|
||||
std::vector<CoherencyCopy> copies;
|
||||
std::atomic_uint32_t *unlock_cookie = nullptr;
|
||||
};
|
||||
|
||||
// These options control various behavior when upscaling to workaround glitches which arise naturally as part of upscaling.
|
||||
struct Quirks
|
||||
{
|
||||
inline Quirks()
|
||||
{
|
||||
u.options.native_resolution_tex_rect = true;
|
||||
u.options.native_texture_lod = false;
|
||||
}
|
||||
|
||||
inline void set_native_resolution_tex_rect(bool enable)
|
||||
{
|
||||
u.options.native_resolution_tex_rect = enable;
|
||||
}
|
||||
|
||||
inline void set_native_texture_lod(bool enable)
|
||||
{
|
||||
u.options.native_texture_lod = enable;
|
||||
}
|
||||
|
||||
union
|
||||
{
|
||||
struct Opts
|
||||
{
|
||||
// If true, force TEX_RECT and TEX_RECT_FLIP to render without upscaling.
|
||||
// Works around bilinear filtering bugs in Cycle1/Cycle2 mode where game assumed 1:1 pixel transfer.
|
||||
bool native_resolution_tex_rect;
|
||||
|
||||
// Forces LOD to be computed as 1x upscale.
|
||||
// Fixes content which relies on LOD computation to select textures in clever ways.
|
||||
bool native_texture_lod;
|
||||
} options;
|
||||
uint32_t words[1];
|
||||
} u;
|
||||
};
|
||||
|
||||
class CommandProcessor
|
||||
{
|
||||
public:
|
||||
CommandProcessor(Vulkan::Device &device,
|
||||
void *rdram_ptr,
|
||||
size_t rdram_offset,
|
||||
size_t rdram_size,
|
||||
size_t hidden_rdram_size,
|
||||
CommandProcessorFlags flags);
|
||||
|
||||
~CommandProcessor();
|
||||
|
||||
void set_validation_interface(ValidationInterface *iface);
|
||||
|
||||
bool device_is_supported() const;
|
||||
|
||||
// Synchronization.
|
||||
void flush();
|
||||
uint64_t signal_timeline();
|
||||
void wait_for_timeline(uint64_t index);
|
||||
void idle();
|
||||
void begin_frame_context();
|
||||
|
||||
// Queues up state and drawing commands.
|
||||
void enqueue_command(unsigned num_words, const uint32_t *words);
|
||||
void enqueue_command_direct(unsigned num_words, const uint32_t *words);
|
||||
// Mupen64Plus PS5: a run of commands framed as (num_words, words...), queued in one go (CommandRing)
|
||||
void enqueue_commands(size_t count, const uint32_t *framed);
|
||||
|
||||
void set_quirks(const Quirks &quirks);
|
||||
|
||||
// Mupen64Plus PS5: the ring's timing counters, for the plugin's report
|
||||
CommandRing &get_command_ring() { return ring; }
|
||||
|
||||
// Interact with memory.
|
||||
void *begin_read_rdram();
|
||||
void end_write_rdram();
|
||||
void *begin_read_hidden_rdram();
|
||||
void end_write_hidden_rdram();
|
||||
size_t get_rdram_size() const;
|
||||
size_t get_hidden_rdram_size() const;
|
||||
void *get_tmem();
|
||||
|
||||
// Sets VI register
|
||||
void set_vi_register(VIRegister reg, uint32_t value);
|
||||
|
||||
Vulkan::ImageHandle scanout(const ScanoutOptions &opts = {});
|
||||
void scanout_sync(std::vector<RGBA> &colors, unsigned &width, unsigned &height, const ScanoutOptions &opts = {});
|
||||
void scanout_async_buffer(VIScanoutBuffer &buffer, const ScanoutOptions &opts = {});
|
||||
|
||||
// Support for modifying certain registers per-scanline.
|
||||
// The idea is that before we scanout(), we use set_vi_register() to
|
||||
// set frame-global VI register state.
|
||||
// While scanning out, we can support changing some state, in particular HStart and XStart
|
||||
// which allows various raster effects ala HDMA.
|
||||
// For sanity's sake, scanout() reads all memory at once. A fully beam-raced implementation
|
||||
// would render out images every scanline, but that would cripple performance and it's questionable
|
||||
// how this is useful, especially on a 3D console. The only failure case of this style of implementation
|
||||
// would be if a demo attempted to modify VRAM *after* it has been scanned out, i.e. a write-after-read
|
||||
// hazard.
|
||||
|
||||
// Latch registers are initialized to the values in set_vi_register() for each respective register.
|
||||
// After scanout(), the flags state is cleared to 0.
|
||||
void begin_vi_register_per_scanline(VideoInterface::PerScanlineRegisterFlags flags);
|
||||
void set_vi_register_for_scanline(VideoInterface::PerScanlineRegisterBits reg, uint32_t value);
|
||||
|
||||
// Between begin_vi_register_per_scanline() and scanout(), line must be monotonically increasing,
|
||||
// or the call is ignored. Initial value for the line counter is 0
|
||||
// (to set parameters for line 0, use global VI register state).
|
||||
// Currently set registers in set_vi_register_for_scanline() are considered to be the active VI register
|
||||
// values starting with VI line "vi_line", until the bottom of the frame or a new vi_line is set.
|
||||
// Register state is assumed to have been fixed from the last latched scanline up until vi_line.
|
||||
//
|
||||
// The units used for this value matches the hardware YStart registers,
|
||||
// i.e. the first active scanline is not 0, but VI_H_OFFSET_{NTSC,PAL}.
|
||||
// For every scanned line, vi_line should increment by 2.
|
||||
// vi_line must be less than VI_V_END_MAX (really, VI_V_END_{NTSC,PAL}), or it is ignored.
|
||||
void latch_vi_register_for_scanline(unsigned vi_line);
|
||||
|
||||
// Assumes that scanline register state does not change until end of frame.
|
||||
// Must be called before scanout(), or all per-scanline register state is ignored for the scanout.
|
||||
void end_vi_register_per_scanline();
|
||||
|
||||
// Intended flow is something like:
|
||||
// set_vi_register(reg, value0) // value0 used for line [0, 99]
|
||||
// begin_vi_register_per_scanline(flags);
|
||||
// set_vi_register_for_scanline(reg, value1); // value1 used for line [100, 199]
|
||||
// latch_vi_register_for_scanline(100);
|
||||
// set_vi_register_for_scanline(reg, value2);
|
||||
// latch_vi_register_for_scanline(200); // value2 used for line [200, VBlank]
|
||||
// end_vi_register_per_scanline();
|
||||
// scanout();
|
||||
|
||||
private:
|
||||
Vulkan::Device &device;
|
||||
Vulkan::BufferHandle rdram;
|
||||
Vulkan::BufferHandle hidden_rdram;
|
||||
Vulkan::BufferHandle tmem;
|
||||
size_t rdram_offset;
|
||||
size_t rdram_size;
|
||||
CommandProcessorFlags flags;
|
||||
#ifndef PARALLEL_RDP_SHADER_DIR
|
||||
std::unique_ptr<ShaderBank> shader_bank;
|
||||
#endif
|
||||
|
||||
// Tear-down order is important here.
|
||||
Renderer renderer;
|
||||
VideoInterface vi;
|
||||
CommandRing ring;
|
||||
|
||||
void clear_hidden_rdram();
|
||||
void clear_tmem();
|
||||
void clear_buffer(Vulkan::Buffer &buffer, uint32_t value);
|
||||
void init_renderer();
|
||||
void enqueue_command_inner(unsigned num_words, const uint32_t *words);
|
||||
|
||||
Vulkan::ImageHandle scanout(const ScanoutOptions &opts, VkImageLayout target_layout);
|
||||
|
||||
#define OP(x) void op_##x(const uint32_t *words)
|
||||
OP(fill_triangle); OP(fill_z_buffer_triangle); OP(texture_triangle); OP(texture_z_buffer_triangle);
|
||||
OP(shade_triangle); OP(shade_z_buffer_triangle); OP(shade_texture_triangle); OP(shade_texture_z_buffer_triangle);
|
||||
OP(texture_rectangle); OP(texture_rectangle_flip); OP(sync_load); OP(sync_pipe);
|
||||
OP(sync_tile); OP(sync_full); OP(set_key_gb); OP(set_key_r);
|
||||
OP(set_convert); OP(set_scissor); OP(set_prim_depth); OP(set_other_modes);
|
||||
OP(load_tlut); OP(set_tile_size); OP(load_block);
|
||||
OP(load_tile); OP(set_tile); OP(fill_rectangle); OP(set_fill_color);
|
||||
OP(set_fog_color); OP(set_blend_color); OP(set_prim_color); OP(set_env_color);
|
||||
OP(set_combine); OP(set_texture_image); OP(set_mask_image); OP(set_color_image);
|
||||
#undef OP
|
||||
|
||||
ScissorState scissor_state = {};
|
||||
StaticRasterizationState static_state = {};
|
||||
DepthBlendState depth_blend = {};
|
||||
|
||||
struct
|
||||
{
|
||||
uint32_t addr;
|
||||
uint32_t width;
|
||||
TextureFormat fmt;
|
||||
TextureSize size;
|
||||
} texture_image = {};
|
||||
|
||||
uint64_t timeline_value = 0;
|
||||
uint64_t thread_timeline_value = 0;
|
||||
|
||||
struct FenceExecutor
|
||||
{
|
||||
explicit inline FenceExecutor(Vulkan::Device *device_, uint64_t *ptr)
|
||||
: device(device_), value(ptr)
|
||||
{
|
||||
}
|
||||
|
||||
Vulkan::Device *device;
|
||||
uint64_t *value;
|
||||
bool is_sentinel(const CoherencyOperation &work) const;
|
||||
void perform_work(CoherencyOperation &work);
|
||||
void notify_work_locked(const CoherencyOperation &work);
|
||||
};
|
||||
WorkerThread<CoherencyOperation, FenceExecutor> timeline_worker;
|
||||
|
||||
uint8_t *host_rdram = nullptr;
|
||||
bool measure_stall_time = false;
|
||||
bool single_threaded_processing = false;
|
||||
bool is_supported = false;
|
||||
bool is_host_coherent = true;
|
||||
bool timestamp = false;
|
||||
|
||||
friend class Renderer;
|
||||
|
||||
void enqueue_coherency_operation(CoherencyOperation &&op);
|
||||
void drain_command_ring();
|
||||
void decode_triangle_setup(TriangleSetup &setup, const uint32_t *words) const;
|
||||
|
||||
Quirks quirks;
|
||||
|
||||
std::unique_ptr<RDPDumpWriter> dump_writer;
|
||||
bool dump_in_command_list = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/* Copyright (c) 2021 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "rdp_dump_write.hpp"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
RDPDumpWriter::~RDPDumpWriter()
|
||||
{
|
||||
end();
|
||||
if (file)
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
bool RDPDumpWriter::init(const char *path, uint32_t dram_size, uint32_t hidden_dram_size)
|
||||
{
|
||||
if (file)
|
||||
return false;
|
||||
|
||||
rdp_dram_cache.clear();
|
||||
rdp_dram_cache.resize(dram_size);
|
||||
rdp_hidden_dram_cache.clear();
|
||||
rdp_hidden_dram_cache.resize(hidden_dram_size);
|
||||
|
||||
file = fopen(path, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
fwrite("RDPDUMP2", 8, 1, file);
|
||||
fwrite(&dram_size, sizeof(dram_size), 1, file);
|
||||
fwrite(&hidden_dram_size, sizeof(hidden_dram_size), 1, file);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RDPDumpWriter::end_frame()
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
uint32_t cmd = RDP_DUMP_CMD_END_FRAME;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
}
|
||||
|
||||
void RDPDumpWriter::end()
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
uint32_t cmd = RDP_DUMP_CMD_EOF;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
|
||||
fclose(file);
|
||||
file = nullptr;
|
||||
|
||||
rdp_dram_cache.clear();
|
||||
rdp_hidden_dram_cache.clear();
|
||||
}
|
||||
|
||||
void RDPDumpWriter::flush(const void *dram_, uint32_t size,
|
||||
RDPDumpCmd block_cmd, RDPDumpCmd flush_cmd,
|
||||
uint8_t *cache)
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
const auto *dram = static_cast<const uint8_t *>(dram_);
|
||||
const uint32_t block_size = 4 * 1024;
|
||||
uint32_t i = 0;
|
||||
|
||||
for (i = 0; i < size; i += block_size)
|
||||
{
|
||||
if (memcmp(dram + i, cache + i, block_size) != 0)
|
||||
{
|
||||
uint32_t cmd = block_cmd;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
fwrite(&i, sizeof(i), 1, file);
|
||||
fwrite(&block_size, sizeof(block_size), 1, file);
|
||||
fwrite(dram + i, 1, block_size, file);
|
||||
memcpy(cache + i, dram + i, block_size);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t cmd = flush_cmd;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
|
||||
}
|
||||
|
||||
void RDPDumpWriter::flush_dram(const void *dram_, uint32_t size)
|
||||
{
|
||||
flush(dram_, size, RDP_DUMP_CMD_UPDATE_DRAM, RDP_DUMP_CMD_UPDATE_DRAM_FLUSH, rdp_dram_cache.data());
|
||||
}
|
||||
|
||||
void RDPDumpWriter::flush_hidden_dram(const void *dram_, uint32_t size)
|
||||
{
|
||||
flush(dram_, size, RDP_DUMP_CMD_UPDATE_HIDDEN_DRAM, RDP_DUMP_CMD_UPDATE_HIDDEN_DRAM_FLUSH, rdp_hidden_dram_cache.data());
|
||||
}
|
||||
|
||||
void RDPDumpWriter::signal_complete()
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
uint32_t cmd = RDP_DUMP_CMD_SIGNAL_COMPLETE;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
}
|
||||
|
||||
void RDPDumpWriter::emit_command(uint32_t command, const uint32_t *cmd_data, uint32_t cmd_words)
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
uint32_t cmd = RDP_DUMP_CMD_RDP_COMMAND;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
fwrite(&command, sizeof(command), 1, file);
|
||||
fwrite(&cmd_words, sizeof(cmd_words), 1, file);
|
||||
fwrite(cmd_data, sizeof(*cmd_data), cmd_words, file);
|
||||
}
|
||||
|
||||
void RDPDumpWriter::set_vi_register(uint32_t vi_register, uint32_t value)
|
||||
{
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
uint32_t cmd = RDP_DUMP_CMD_SET_VI_REGISTER;
|
||||
fwrite(&cmd, sizeof(cmd), 1, file);
|
||||
fwrite(&vi_register, sizeof(vi_register), 1, file);
|
||||
fwrite(&value, sizeof(value), 1, file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* Copyright (c) 2021 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <vector>
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
class RDPDumpWriter
|
||||
{
|
||||
public:
|
||||
~RDPDumpWriter();
|
||||
bool init(const char *path, uint32_t dram_size, uint32_t hidden_dram_size);
|
||||
void flush_dram(const void *dram, uint32_t size);
|
||||
void flush_hidden_dram(const void *dram, uint32_t size);
|
||||
void signal_complete();
|
||||
void emit_command(uint32_t command, const uint32_t *cmd_data, uint32_t cmd_words);
|
||||
void set_vi_register(uint32_t vi_register, uint32_t value);
|
||||
void end_frame();
|
||||
|
||||
private:
|
||||
enum RDPDumpCmd : uint32_t
|
||||
{
|
||||
RDP_DUMP_CMD_INVALID = 0,
|
||||
RDP_DUMP_CMD_UPDATE_DRAM = 1,
|
||||
RDP_DUMP_CMD_RDP_COMMAND = 2,
|
||||
RDP_DUMP_CMD_SET_VI_REGISTER = 3,
|
||||
RDP_DUMP_CMD_END_FRAME = 4,
|
||||
RDP_DUMP_CMD_SIGNAL_COMPLETE = 5,
|
||||
RDP_DUMP_CMD_EOF = 6,
|
||||
RDP_DUMP_CMD_UPDATE_DRAM_FLUSH = 7,
|
||||
RDP_DUMP_CMD_UPDATE_HIDDEN_DRAM = 8,
|
||||
RDP_DUMP_CMD_UPDATE_HIDDEN_DRAM_FLUSH = 9,
|
||||
RDP_DUMP_CMD_INT_MAX = 0x7fffffff
|
||||
};
|
||||
|
||||
FILE *file = nullptr;
|
||||
std::vector<uint8_t> rdp_dram_cache;
|
||||
std::vector<uint8_t> rdp_hidden_dram_cache;
|
||||
void flush(const void *dram_, uint32_t size, RDPDumpCmd block_cmd, RDPDumpCmd flush_cmd, uint8_t *cache);
|
||||
void end();
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,420 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rdp_data_structures.hpp"
|
||||
#include "device.hpp"
|
||||
#include "rdp_common.hpp"
|
||||
#include "worker_thread.hpp"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace RDP
|
||||
{
|
||||
struct CoherencyOperation;
|
||||
|
||||
struct SyncObject
|
||||
{
|
||||
Vulkan::Fence fence;
|
||||
};
|
||||
|
||||
enum class FBFormat : uint32_t
|
||||
{
|
||||
I4 = 0,
|
||||
I8 = 1,
|
||||
RGBA5551 = 2,
|
||||
IA88 = 3,
|
||||
RGBA8888 = 4
|
||||
};
|
||||
|
||||
enum class UploadMode : uint32_t
|
||||
{
|
||||
Tile = 0,
|
||||
TLUT = 1,
|
||||
Block = 2
|
||||
};
|
||||
|
||||
struct LoadTileInfo
|
||||
{
|
||||
uint32_t tex_addr;
|
||||
uint32_t tex_width;
|
||||
uint16_t slo, tlo, shi, thi;
|
||||
TextureFormat fmt;
|
||||
TextureSize size;
|
||||
UploadMode mode;
|
||||
};
|
||||
|
||||
class CommandProcessor;
|
||||
|
||||
struct RendererOptions
|
||||
{
|
||||
unsigned upscaling_factor = 1;
|
||||
bool super_sampled_readback = false;
|
||||
bool super_sampled_readback_dither = false;
|
||||
};
|
||||
|
||||
enum class ValidationError
|
||||
{
|
||||
Fill4bpp,
|
||||
LoadTile4bpp,
|
||||
InvalidMultilineLoadTlut,
|
||||
FillDepthTest,
|
||||
FillDepthWrite,
|
||||
FillImageReadEnable,
|
||||
Copy32bpp
|
||||
};
|
||||
|
||||
class ValidationInterface
|
||||
{
|
||||
public:
|
||||
virtual ~ValidationInterface() = default;
|
||||
// Validation errors may be called from a thread as errors are encountered.
|
||||
// Reports situations that would cause fatal error on a real RDP.
|
||||
// We only opt to report these situations rather than deliberately crashing the renderer.
|
||||
// Handling crashes is only relevant during development of N64 homebrew.
|
||||
virtual void report_rdp_crash(ValidationError err, const char *msg) = 0;
|
||||
};
|
||||
|
||||
class Renderer : public Vulkan::DebugChannelInterface
|
||||
{
|
||||
public:
|
||||
explicit Renderer(CommandProcessor &processor);
|
||||
~Renderer();
|
||||
void set_device(Vulkan::Device *device);
|
||||
|
||||
void set_validation_interface(ValidationInterface *iface);
|
||||
|
||||
// If coherent is false, RDRAM is a buffer split into data in lower half and writemask state in upper half, each part being size large.
|
||||
// offset must be 0 in this case.
|
||||
void set_rdram(Vulkan::Buffer *buffer, uint8_t *host_rdram, size_t offset, size_t size, bool coherent);
|
||||
void set_hidden_rdram(Vulkan::Buffer *buffer);
|
||||
void set_tmem(Vulkan::Buffer *buffer);
|
||||
void set_shader_bank(const ShaderBank *bank);
|
||||
|
||||
bool init_renderer(const RendererOptions &options);
|
||||
|
||||
// setup may be mutated to apply various fixups to triangle setup.
|
||||
void draw_flat_primitive(TriangleSetup &setup);
|
||||
void draw_shaded_primitive(TriangleSetup &setup, const AttributeSetup &attr);
|
||||
|
||||
void set_color_framebuffer(uint32_t addr, uint32_t width, FBFormat fmt);
|
||||
void set_depth_framebuffer(uint32_t addr);
|
||||
|
||||
void set_scissor_state(const ScissorState &state);
|
||||
void set_static_rasterization_state(const StaticRasterizationState &state);
|
||||
void set_depth_blend_state(const DepthBlendState &state);
|
||||
|
||||
void set_tile(uint32_t tile, const TileMeta &info);
|
||||
void set_tile_size(uint32_t tile, uint32_t slo, uint32_t shi, uint32_t tlo, uint32_t thi);
|
||||
void load_tile(uint32_t tile, const LoadTileInfo &info);
|
||||
void load_tile_iteration(uint32_t tile, const LoadTileInfo &info, uint32_t tmem_offset);
|
||||
|
||||
void set_blend_color(uint32_t color);
|
||||
void set_fog_color(uint32_t color);
|
||||
void set_env_color(uint32_t color);
|
||||
void set_primitive_color(uint8_t min_level, uint8_t prim_lod_frac, uint32_t color);
|
||||
void set_fill_color(uint32_t color);
|
||||
void set_primitive_depth(uint16_t prim_depth, uint16_t prim_dz);
|
||||
void set_enable_primitive_depth(bool enable);
|
||||
void set_convert(uint16_t k0, uint16_t k1, uint16_t k2, uint16_t k3, uint16_t k4, uint16_t k5);
|
||||
void set_color_key(unsigned component, uint32_t width, uint32_t center, uint32_t scale);
|
||||
|
||||
// Called when the command thread has not seen any activity in a given period of time.
|
||||
// This is useful so we don't needlessly queue up work when we might as well kick it to the GPU.
|
||||
void notify_idle_command_thread();
|
||||
void flush_and_signal();
|
||||
|
||||
int resolve_shader_define(const char *name, const char *define) const;
|
||||
|
||||
void resolve_coherency_external(unsigned offset, unsigned length);
|
||||
void submit_update_upscaled_domain_external(Vulkan::CommandBuffer &cmd,
|
||||
unsigned addr, unsigned pixels, unsigned pixel_size_log2);
|
||||
unsigned get_scaling_factor() const;
|
||||
|
||||
const Vulkan::Buffer *get_upscaled_rdram_buffer() const;
|
||||
const Vulkan::Buffer *get_upscaled_hidden_rdram_buffer() const;
|
||||
|
||||
void lock_command_processing();
|
||||
void unlock_command_processing();
|
||||
|
||||
private:
|
||||
CommandProcessor &processor;
|
||||
Vulkan::Device *device = nullptr;
|
||||
Vulkan::Buffer *rdram = nullptr;
|
||||
ValidationInterface *validation_iface = nullptr;
|
||||
|
||||
Vulkan::BufferHandle upscaling_reference_rdram;
|
||||
Vulkan::BufferHandle upscaling_multisampled_rdram;
|
||||
Vulkan::BufferHandle upscaling_multisampled_hidden_rdram;
|
||||
|
||||
void validate_draw_state() const;
|
||||
|
||||
struct
|
||||
{
|
||||
uint8_t *host_rdram = nullptr;
|
||||
Vulkan::BufferHandle staging_rdram;
|
||||
Vulkan::BufferHandle staging_readback;
|
||||
std::unique_ptr<std::atomic_uint32_t[]> pending_writes_for_page;
|
||||
std::vector<uint32_t> page_to_direct_copy;
|
||||
std::vector<uint32_t> page_to_masked_copy;
|
||||
std::vector<uint32_t> page_to_pending_readback;
|
||||
unsigned num_pages = 0;
|
||||
unsigned staging_readback_pages = 0;
|
||||
unsigned staging_readback_index = 0; // Ringbuffer the readbacks.
|
||||
} incoherent;
|
||||
|
||||
size_t rdram_offset = 0;
|
||||
size_t rdram_size = 0;
|
||||
bool is_host_coherent = false;
|
||||
Vulkan::Buffer *hidden_rdram = nullptr;
|
||||
Vulkan::Buffer *tmem = nullptr;
|
||||
const ShaderBank *shader_bank = nullptr;
|
||||
|
||||
bool init_caps();
|
||||
void init_blender_lut();
|
||||
void init_buffers(const RendererOptions &options);
|
||||
bool init_internal_upscaling_factor(const RendererOptions &options);
|
||||
|
||||
struct
|
||||
{
|
||||
uint32_t addr = 0;
|
||||
uint32_t depth_addr = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t deduced_height = 0;
|
||||
FBFormat fmt = FBFormat::I8;
|
||||
bool depth_write_pending = false;
|
||||
bool color_write_pending = false;
|
||||
} fb;
|
||||
|
||||
struct StreamCaches
|
||||
{
|
||||
ScissorState scissor_state = {};
|
||||
StaticRasterizationState static_raster_state = {};
|
||||
DepthBlendState depth_blend_state = {};
|
||||
|
||||
StateCache<StaticRasterizationState, Limits::MaxStaticRasterizationStates> static_raster_state_cache;
|
||||
StateCache<DepthBlendState, Limits::MaxDepthBlendStates> depth_blend_state_cache;
|
||||
StateCache<TileInfo, Limits::MaxTileInfoStates> tile_info_state_cache;
|
||||
|
||||
StreamCache<TriangleSetup, Limits::MaxPrimitives> triangle_setup;
|
||||
StreamCache<ScissorState, Limits::MaxPrimitives> scissor_setup;
|
||||
StreamCache<AttributeSetup, Limits::MaxPrimitives> attribute_setup;
|
||||
StreamCache<DerivedSetup, Limits::MaxPrimitives> derived_setup;
|
||||
StreamCache<InstanceIndices, Limits::MaxPrimitives> state_indices;
|
||||
StreamCache<SpanInfoOffsets, Limits::MaxPrimitives> span_info_offsets;
|
||||
StreamCache<SpanInterpolationJob, Limits::MaxSpanSetups> span_info_jobs;
|
||||
|
||||
std::vector<UploadInfo> tmem_upload_infos;
|
||||
unsigned max_shaded_tiles = 0;
|
||||
Vulkan::CommandBufferHandle cmd;
|
||||
} stream;
|
||||
|
||||
void ensure_command_buffer();
|
||||
|
||||
TileInfo tiles[Limits::MaxNumTiles];
|
||||
Vulkan::BufferHandle tmem_instances;
|
||||
Vulkan::BufferHandle span_setups;
|
||||
Vulkan::BufferHandle blender_divider_lut_buffer;
|
||||
Vulkan::BufferViewHandle blender_divider_buffer;
|
||||
|
||||
Vulkan::BufferHandle tile_binning_buffer;
|
||||
Vulkan::BufferHandle tile_binning_buffer_coarse;
|
||||
|
||||
Vulkan::BufferHandle indirect_dispatch_buffer;
|
||||
Vulkan::BufferHandle tile_work_list;
|
||||
Vulkan::BufferHandle per_tile_offsets;
|
||||
Vulkan::BufferHandle per_tile_shaded_color;
|
||||
Vulkan::BufferHandle per_tile_shaded_depth;
|
||||
Vulkan::BufferHandle per_tile_shaded_shaded_alpha;
|
||||
Vulkan::BufferHandle per_tile_shaded_coverage;
|
||||
|
||||
struct MappedBuffer
|
||||
{
|
||||
Vulkan::BufferHandle buffer;
|
||||
bool is_host = false;
|
||||
};
|
||||
|
||||
struct RenderBuffers
|
||||
{
|
||||
void init(Vulkan::Device &device, Vulkan::BufferDomain domain, RenderBuffers *borrow);
|
||||
static MappedBuffer create_buffer(Vulkan::Device &device, Vulkan::BufferDomain domain, VkDeviceSize size, MappedBuffer *borrow);
|
||||
|
||||
MappedBuffer triangle_setup;
|
||||
MappedBuffer attribute_setup;
|
||||
MappedBuffer derived_setup;
|
||||
MappedBuffer scissor_setup;
|
||||
|
||||
MappedBuffer static_raster_state;
|
||||
MappedBuffer depth_blend_state;
|
||||
MappedBuffer tile_info_state;
|
||||
|
||||
MappedBuffer state_indices;
|
||||
MappedBuffer span_info_offsets;
|
||||
|
||||
MappedBuffer span_info_jobs;
|
||||
Vulkan::BufferViewHandle span_info_jobs_view;
|
||||
};
|
||||
|
||||
struct RenderBuffersUpdater
|
||||
{
|
||||
void init(Vulkan::Device &device);
|
||||
void upload(Vulkan::Device &device, const StreamCaches &caches, Vulkan::CommandBuffer &cmd);
|
||||
|
||||
template <typename Cache>
|
||||
void upload(Vulkan::CommandBuffer &cmd, Vulkan::Device &device,
|
||||
const MappedBuffer &gpu, const MappedBuffer &cpu, const Cache &cache, bool &did_upload);
|
||||
|
||||
RenderBuffers cpu, gpu;
|
||||
};
|
||||
|
||||
struct InternalSynchronization
|
||||
{
|
||||
Vulkan::Fence fence;
|
||||
};
|
||||
|
||||
struct Constants
|
||||
{
|
||||
uint32_t blend_color = 0;
|
||||
uint32_t fog_color = 0;
|
||||
uint32_t env_color = 0;
|
||||
uint32_t primitive_color = 0;
|
||||
uint32_t fill_color = 0;
|
||||
uint8_t min_level = 0;
|
||||
uint8_t prim_lod_frac = 0;
|
||||
int32_t prim_depth = 0;
|
||||
uint16_t prim_dz = 0;
|
||||
uint16_t convert[6] = {};
|
||||
|
||||
uint16_t key_width[3] = {};
|
||||
uint8_t key_center[3] = {};
|
||||
uint8_t key_scale[3] = {};
|
||||
|
||||
bool use_prim_depth = false;
|
||||
} constants;
|
||||
|
||||
RenderBuffersUpdater buffer_instances[Limits::NumSyncStates];
|
||||
InternalSynchronization internal_sync[Limits::NumSyncStates];
|
||||
uint32_t sync_indices_needs_flush = 0;
|
||||
unsigned buffer_instance = 0;
|
||||
uint32_t base_primitive_index = 0;
|
||||
unsigned pending_render_passes = 0;
|
||||
unsigned pending_render_passes_upscaled = 0;
|
||||
unsigned pending_primitives = 0;
|
||||
unsigned pending_primitives_upscaled = 0;
|
||||
|
||||
bool tmem_upload_needs_flush(uint32_t addr) const;
|
||||
|
||||
bool render_pass_is_upscaled() const;
|
||||
bool should_render_upscaled() const;
|
||||
|
||||
void flush_queues();
|
||||
void submit_render_pass(Vulkan::CommandBuffer &cmd);
|
||||
void submit_render_pass_upscaled(Vulkan::CommandBuffer &cmd);
|
||||
void submit_render_pass_end(Vulkan::CommandBuffer &cmd);
|
||||
void submit_to_queue();
|
||||
void begin_new_context();
|
||||
void reset_context();
|
||||
bool need_flush() const;
|
||||
void maintain_queues();
|
||||
void maintain_queues_idle();
|
||||
void update_tmem_instances(Vulkan::CommandBuffer &cmd);
|
||||
void submit_span_setup_jobs(Vulkan::CommandBuffer &cmd, bool upscaled);
|
||||
void update_deduced_height(const TriangleSetup &setup);
|
||||
void submit_tile_binning_combined(Vulkan::CommandBuffer &cmd, bool upscaled);
|
||||
void clear_indirect_buffer(Vulkan::CommandBuffer &cmd);
|
||||
void submit_rasterization(Vulkan::CommandBuffer &cmd, Vulkan::Buffer &tmem, bool upscaled);
|
||||
void submit_depth_blend(Vulkan::CommandBuffer &cmd, Vulkan::Buffer &tmem, bool upscaled, bool force_write_mask);
|
||||
|
||||
enum class ResolveStage { Pre, Post, SSAAResolve };
|
||||
void submit_update_upscaled_domain(Vulkan::CommandBuffer &cmd, ResolveStage stage);
|
||||
void submit_update_upscaled_domain(Vulkan::CommandBuffer &cmd, ResolveStage stage,
|
||||
unsigned addr, unsigned depth_addr,
|
||||
unsigned width, unsigned height,
|
||||
unsigned pixel_size_log2);
|
||||
void submit_clear_super_sample_write_mask(Vulkan::CommandBuffer &cmd, unsigned width, unsigned height);
|
||||
|
||||
SpanInfoOffsets allocate_span_jobs(const TriangleSetup &setup);
|
||||
|
||||
DerivedSetup build_derived_attributes(const AttributeSetup &attr) const;
|
||||
void build_combiner_constants(DerivedSetup &setup, unsigned cycle) const;
|
||||
int filter_debug_channel_x = -1;
|
||||
int filter_debug_channel_y = -1;
|
||||
bool debug_channel = false;
|
||||
|
||||
void message(const std::string &tag, uint32_t code,
|
||||
uint32_t x, uint32_t y, uint32_t z,
|
||||
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words) override;
|
||||
|
||||
bool can_support_minimum_subgroup_size(unsigned size) const;
|
||||
bool supports_subgroup_size_control(uint32_t minimum_size, uint32_t maximum_size) const;
|
||||
|
||||
std::unordered_set<Util::Hash> pending_async_pipelines;
|
||||
|
||||
unsigned compute_conservative_max_num_tiles(const TriangleSetup &setup) const;
|
||||
|
||||
void deduce_static_texture_state(unsigned tile, unsigned max_lod_level);
|
||||
void deduce_noise_state();
|
||||
static StaticRasterizationState normalize_static_state(StaticRasterizationState state);
|
||||
void fixup_triangle_setup(TriangleSetup &setup) const;
|
||||
|
||||
struct Caps
|
||||
{
|
||||
int timestamp = 0;
|
||||
bool force_sync = false;
|
||||
bool ubershader = false;
|
||||
bool supports_small_integer_arithmetic = false;
|
||||
bool subgroup_tile_binning = false;
|
||||
bool subgroup_depth_blend = false;
|
||||
bool super_sample_readback = false;
|
||||
bool super_sample_readback_dither = false;
|
||||
unsigned upscaling = 1;
|
||||
unsigned max_num_tile_instances = Limits::MaxTileInstances;
|
||||
unsigned max_tiles_x = ImplementationConstants::MaxTilesX;
|
||||
unsigned max_tiles_y = ImplementationConstants::MaxTilesY;
|
||||
unsigned max_width = Limits::MaxWidth;
|
||||
unsigned max_height = Limits::MaxHeight;
|
||||
} caps;
|
||||
|
||||
struct PipelineExecutor
|
||||
{
|
||||
Vulkan::Device *device;
|
||||
bool is_sentinel(const Vulkan::DeferredPipelineCompile &compile) const;
|
||||
void perform_work(const Vulkan::DeferredPipelineCompile &compile) const;
|
||||
void notify_work_locked(const Vulkan::DeferredPipelineCompile &compile) const;
|
||||
};
|
||||
|
||||
std::unique_ptr<WorkerThread<Vulkan::DeferredPipelineCompile, PipelineExecutor>> pipeline_worker;
|
||||
|
||||
void resolve_coherency_host_to_gpu(Vulkan::CommandBuffer &cmd);
|
||||
void resolve_coherency_gpu_to_host(CoherencyOperation &op, Vulkan::CommandBuffer &cmd);
|
||||
uint32_t get_byte_size_for_bound_color_framebuffer() const;
|
||||
uint32_t get_byte_size_for_bound_depth_framebuffer() const;
|
||||
void mark_pages_for_gpu_read(uint32_t base_addr, uint32_t byte_count);
|
||||
void lock_pages_for_gpu_write(uint32_t base_addr, uint32_t byte_count);
|
||||
|
||||
std::atomic_uint32_t active_submissions;
|
||||
void enqueue_fence_wait(Vulkan::Fence fence);
|
||||
uint64_t last_submit_ns = 0;
|
||||
|
||||
std::mutex idle_lock;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef BINNING_H_
|
||||
#define BINNING_H_
|
||||
|
||||
// There are 4 critical Y coordinates to test when binning. Top, bottom, mid, and mid - 1.
|
||||
|
||||
const int SUBPIXELS_Y = 4;
|
||||
|
||||
ivec4 quantize_x(ivec4 x)
|
||||
{
|
||||
return x >> 15;
|
||||
}
|
||||
|
||||
int minimum4(ivec4 v)
|
||||
{
|
||||
ivec2 minimum2 = min(v.xy, v.zw);
|
||||
return min(minimum2.x, minimum2.y);
|
||||
}
|
||||
|
||||
int maximum4(ivec4 v)
|
||||
{
|
||||
ivec2 maximum2 = max(v.xy, v.zw);
|
||||
return max(maximum2.x, maximum2.y);
|
||||
}
|
||||
|
||||
ivec4 madd_32_64(ivec4 a, int b, int c, out ivec4 hi_bits)
|
||||
{
|
||||
ivec4 lo, hi;
|
||||
imulExtended(a, ivec4(b), hi, lo);
|
||||
uvec4 carry;
|
||||
lo = ivec4(uaddCarry(lo, uvec4(c), carry));
|
||||
hi += ivec4(carry);
|
||||
hi_bits = hi;
|
||||
return lo;
|
||||
}
|
||||
|
||||
ivec2 interpolate_xs(TriangleSetup setup, ivec4 ys, bool flip, int scaling)
|
||||
{
|
||||
int yh_interpolation_base = setup.yh & ~(SUBPIXELS_Y - 1);
|
||||
int ym_interpolation_base = setup.ym;
|
||||
|
||||
yh_interpolation_base *= scaling;
|
||||
ym_interpolation_base *= scaling;
|
||||
|
||||
// Interpolate in 64-bit so we can detect quirky overflow scenarios.
|
||||
ivec4 xh_hi, xm_hi, xl_hi;
|
||||
ivec4 xh = madd_32_64(ys - yh_interpolation_base, setup.dxhdy, scaling * setup.xh, xh_hi);
|
||||
ivec4 xm = madd_32_64(ys - yh_interpolation_base, setup.dxmdy, scaling * setup.xm, xm_hi);
|
||||
ivec4 xl = madd_32_64(ys - ym_interpolation_base, setup.dxldy, scaling * setup.xl, xl_hi);
|
||||
xl = mix(xl, xm, lessThan(ys, ivec4(scaling * setup.ym)));
|
||||
xl_hi = mix(xl_hi, xm_hi, lessThan(ys, ivec4(scaling * setup.ym)));
|
||||
|
||||
// Handle overflow scenarios. Saturate 64-bit signed to 32-bit signed without 64-bit math.
|
||||
xh = mix(xh, ivec4(0x7fffffff), greaterThan(xh_hi, ivec4(0)));
|
||||
xh = mix(xh, ivec4(-0x80000000), lessThan(xh_hi, ivec4(-1)));
|
||||
xl = mix(xl, ivec4(0x7fffffff), greaterThan(xl_hi, ivec4(0)));
|
||||
xl = mix(xl, ivec4(-0x80000000), lessThan(xl_hi, ivec4(-1)));
|
||||
|
||||
ivec4 xh_shifted = quantize_x(xh);
|
||||
ivec4 xl_shifted = quantize_x(xl);
|
||||
|
||||
ivec4 xleft, xright;
|
||||
if (flip)
|
||||
{
|
||||
xleft = xh_shifted;
|
||||
xright = xl_shifted;
|
||||
}
|
||||
else
|
||||
{
|
||||
xleft = xl_shifted;
|
||||
xright = xh_shifted;
|
||||
}
|
||||
|
||||
// If one of the results are out of range, we have overflow, and we need to be conservative when binning.
|
||||
int max_range = maximum4(max(abs(xleft), abs(xright)));
|
||||
ivec2 range;
|
||||
if (max_range <= 2047 * scaling)
|
||||
range = ivec2(minimum4(xleft), maximum4(xright));
|
||||
else
|
||||
range = ivec2(0, 0x7fffffff);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
bool bin_primitive(TriangleSetup setup, ivec2 lo, ivec2 hi, int scaling, ScissorState scissor)
|
||||
{
|
||||
// First clip Y range based on scissor.
|
||||
lo.y = max(lo.y, scaling * (scissor.ylo >> 2));
|
||||
hi.y = min(hi.y, scaling * ((scissor.yhi + 3) >> 2) - 1);
|
||||
|
||||
int start_y = lo.y * SUBPIXELS_Y;
|
||||
int end_y = (hi.y * SUBPIXELS_Y) + (SUBPIXELS_Y - 1);
|
||||
|
||||
// First, we clip start/end against y_lo, y_hi.
|
||||
start_y = max(start_y, scaling * int(setup.yh));
|
||||
end_y = min(end_y, scaling * int(setup.yl) - 1);
|
||||
|
||||
// Y is clipped out, exit early.
|
||||
if (end_y < start_y)
|
||||
return false;
|
||||
|
||||
bool flip = (setup.flags & TRIANGLE_SETUP_FLIP_BIT) != 0;
|
||||
|
||||
// Sample the X ranges for min and max Y, and potentially the mid-point as well.
|
||||
ivec4 ys = ivec4(start_y, end_y, clamp(setup.ym * scaling + ivec2(-1, 0), ivec2(start_y), ivec2(end_y)));
|
||||
ivec2 x_range = interpolate_xs(setup, ys, flip, scaling);
|
||||
|
||||
// For FILL_COPY_RASTER_BIT we're inclusive, if not, exclusive.
|
||||
int x_bias = (setup.flags & TRIANGLE_SETUP_FILL_COPY_RASTER_BIT) != 0 ? 4 : 3;
|
||||
ivec2 scissor_x = ivec2(scaling * (scissor.xlo >> 2), scaling * ((scissor.xhi + x_bias) >> 2) - 1);
|
||||
|
||||
// Scissor is applied through a clamp with a mask being generated for overshoot which affects if the line is valid.
|
||||
// Since this is a conservative test we don't compute valid line here, so we have to assume it is valid.
|
||||
// We can end up creating fake coverage in FILL/COPY modes in some cases
|
||||
// if we clamp scissor to outside the primitive's range as long as at least one sub-line passes the scissor test.
|
||||
// The x_range ends up being degenerate, but these fill modes are conservative and generate one pixel of coverage
|
||||
// anyways.
|
||||
x_range = clamp(x_range, scissor_x.xx, scissor_x.yy);
|
||||
|
||||
x_range.x = max(x_range.x, lo.x);
|
||||
x_range.y = min(x_range.y, hi.x);
|
||||
return x_range.x <= x_range.y;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,145 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef BLENDER_H_
|
||||
#define BLENDER_H_
|
||||
|
||||
struct BlendInputs
|
||||
{
|
||||
u8x4 pixel_color;
|
||||
u8x4 memory_color;
|
||||
u8x4 fog_color;
|
||||
u8x4 blend_color;
|
||||
u8 shade_alpha;
|
||||
};
|
||||
|
||||
const int BLEND_MODE_1A_PIXEL_COLOR = 0;
|
||||
const int BLEND_MODE_1A_MEMORY_COLOR = 1;
|
||||
const int BLEND_MODE_1A_BLEND_COLOR = 2;
|
||||
const int BLEND_MODE_1A_FOG_COLOR = 3;
|
||||
|
||||
const int BLEND_MODE_1B_PIXEL_ALPHA = 0;
|
||||
const int BLEND_MODE_1B_FOG_ALPHA = 1;
|
||||
const int BLEND_MODE_1B_SHADE_ALPHA = 2;
|
||||
const int BLEND_MODE_1B_ZERO = 3;
|
||||
|
||||
const int BLEND_MODE_2A_PIXEL_COLOR = 0;
|
||||
const int BLEND_MODE_2A_MEMORY_COLOR = 1;
|
||||
const int BLEND_MODE_2A_BLEND_COLOR = 2;
|
||||
const int BLEND_MODE_2A_FOG_COLOR = 3;
|
||||
|
||||
const int BLEND_MODE_2B_INV_PIXEL_ALPHA = 0;
|
||||
const int BLEND_MODE_2B_MEMORY_ALPHA = 1;
|
||||
const int BLEND_MODE_2B_ONE = 2;
|
||||
const int BLEND_MODE_2B_ZERO = 3;
|
||||
|
||||
u8x3 blender(BlendInputs inputs, u8x4 blend_modes,
|
||||
bool force_blend, bool blend_en, bool color_on_coverage, bool coverage_wrap, u8x2 blend_shift,
|
||||
bool final_cycle)
|
||||
{
|
||||
u8x3 rgb1;
|
||||
switch (int(blend_modes.z))
|
||||
{
|
||||
case BLEND_MODE_2A_PIXEL_COLOR: rgb1 = inputs.pixel_color.rgb; break;
|
||||
case BLEND_MODE_2A_MEMORY_COLOR: rgb1 = inputs.memory_color.rgb; break;
|
||||
case BLEND_MODE_2A_BLEND_COLOR: rgb1 = inputs.blend_color.rgb; break;
|
||||
case BLEND_MODE_2A_FOG_COLOR: rgb1 = inputs.fog_color.rgb; break;
|
||||
}
|
||||
|
||||
if (final_cycle)
|
||||
{
|
||||
if (color_on_coverage && !coverage_wrap)
|
||||
return rgb1;
|
||||
}
|
||||
|
||||
u8x3 rgb0;
|
||||
switch (int(blend_modes.x))
|
||||
{
|
||||
case BLEND_MODE_1A_PIXEL_COLOR: rgb0 = inputs.pixel_color.rgb; break;
|
||||
case BLEND_MODE_1A_MEMORY_COLOR: rgb0 = inputs.memory_color.rgb; break;
|
||||
case BLEND_MODE_1A_BLEND_COLOR: rgb0 = inputs.blend_color.rgb; break;
|
||||
case BLEND_MODE_1A_FOG_COLOR: rgb0 = inputs.fog_color.rgb; break;
|
||||
}
|
||||
|
||||
if (final_cycle)
|
||||
{
|
||||
if (!blend_en || (blend_modes.y == BLEND_MODE_1B_PIXEL_ALPHA &&
|
||||
blend_modes.w == BLEND_MODE_2B_INV_PIXEL_ALPHA &&
|
||||
inputs.pixel_color.a == U8_C(0xff)))
|
||||
{
|
||||
return rgb0;
|
||||
}
|
||||
}
|
||||
|
||||
u8 a0;
|
||||
u8 a1;
|
||||
|
||||
switch (int(blend_modes.y))
|
||||
{
|
||||
case BLEND_MODE_1B_PIXEL_ALPHA: a0 = inputs.pixel_color.a; break;
|
||||
case BLEND_MODE_1B_FOG_ALPHA: a0 = inputs.fog_color.a; break;
|
||||
case BLEND_MODE_1B_SHADE_ALPHA: a0 = inputs.shade_alpha; break;
|
||||
case BLEND_MODE_1B_ZERO: a0 = U8_C(0); break;
|
||||
}
|
||||
|
||||
switch (int(blend_modes.w))
|
||||
{
|
||||
case BLEND_MODE_2B_INV_PIXEL_ALPHA: a1 = ~a0 & U8_C(0xff); break;
|
||||
case BLEND_MODE_2B_MEMORY_ALPHA: a1 = inputs.memory_color.a; break;
|
||||
case BLEND_MODE_2B_ONE: a1 = U8_C(0xff); break;
|
||||
case BLEND_MODE_2B_ZERO: a1 = U8_C(0); break;
|
||||
}
|
||||
|
||||
a0 >>= U8_C(3);
|
||||
a1 >>= U8_C(3);
|
||||
|
||||
if (blend_modes.w == BLEND_MODE_2B_MEMORY_ALPHA)
|
||||
{
|
||||
a0 = (a0 >> blend_shift.x) & U8_C(0x3c);
|
||||
a1 = (a1 >> blend_shift.y) | U8_C(3);
|
||||
}
|
||||
|
||||
i16x3 blended = i16x3(rgb0) * i16(a0) + i16x3(rgb1) * (i16(a1) + I16_C(1));
|
||||
|
||||
if (!final_cycle || force_blend)
|
||||
{
|
||||
rgb0 = u8x3(blended >> I16_C(5));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Serious funk here. Somehow the RDP implemented a divider to deal with weighted average.
|
||||
// Typically relevant when using blender shifters from interpenetrating Z mode.
|
||||
// Under normal condition, this is implemented as a straight integer divider, but
|
||||
// for edge cases, we need a look-up table. The results make no sense.
|
||||
int blend_sum = (int(a0) >> 2) + (int(a1) >> 2) + 1;
|
||||
blended >>= I16_C(2);
|
||||
blended &= I16_C(0x7ff);
|
||||
|
||||
rgb0.r = u8(texelFetch(uBlenderDividerLUT, (blend_sum << 11) | blended.x).x);
|
||||
rgb0.g = u8(texelFetch(uBlenderDividerLUT, (blend_sum << 11) | blended.y).x);
|
||||
rgb0.b = u8(texelFetch(uBlenderDividerLUT, (blend_sum << 11) | blended.z).x);
|
||||
}
|
||||
|
||||
return rgb0 & U8_C(0xff);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,78 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef CLAMPING_H_
|
||||
#define CLAMPING_H_
|
||||
|
||||
#if SMALL_TYPES && 0
|
||||
// This path is buggy on RADV LLVM, disable for time being.
|
||||
i16x4 clamp_9bit_notrunc(i16x4 color)
|
||||
{
|
||||
// [-129, -256] should clamp to 0xff, subtracting by 0x80 will underflow back to positive numbers.
|
||||
// [-128, -1] should clamp to 0.
|
||||
color -= I16_C(0x80);
|
||||
// Sign-extend to 9-bit.
|
||||
color <<= I16_C(7);
|
||||
color >>= I16_C(7);
|
||||
color += I16_C(0x80);
|
||||
return clamp(color, i16x4(0), i16x4(0xff));
|
||||
}
|
||||
#else
|
||||
i16x4 clamp_9bit_notrunc(ivec4 color)
|
||||
{
|
||||
// [-129, -256] should clamp to 0xff, subtracting by 0x80 will underflow back to positive numbers.
|
||||
// [-128, -1] should clamp to 0.
|
||||
color -= 0x80;
|
||||
// Sign-extend to 9-bit.
|
||||
color = bitfieldExtract(color, 0, 9);
|
||||
color += 0x80;
|
||||
return i16x4(clamp(color, ivec4(0), ivec4(0xff)));
|
||||
}
|
||||
#endif
|
||||
|
||||
u8x4 clamp_9bit(i16x4 color)
|
||||
{
|
||||
return u8x4(clamp_9bit_notrunc(color));
|
||||
}
|
||||
|
||||
int clamp_9bit(int color)
|
||||
{
|
||||
return clamp(bitfieldExtract(color - 0x80, 0, 9) + 0x80, 0, 0xff);
|
||||
}
|
||||
|
||||
// Returns 18-bit UNORM depth.
|
||||
int clamp_z(int z)
|
||||
{
|
||||
// Similar to RGBA, we reserve an extra bit to deal with overflow and underflow.
|
||||
z -= (1 << 17);
|
||||
z <<= (31 - 18);
|
||||
z >>= (31 - 18);
|
||||
z += (1 << 17);
|
||||
|
||||
// [0x00000, 0x3ffff] maps to self.
|
||||
// [0x40000, 0x5ffff] maps to 0x3ffff.
|
||||
// [0x60000, 0x7ffff] maps to 0.
|
||||
|
||||
return clamp(z, 0, 0x3ffff);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
layout(local_size_x_id = 0) in;
|
||||
|
||||
layout(set = 0, binding = 0, std430) writeonly buffer ClearIndirectBuffer
|
||||
{
|
||||
uvec4 indirects[];
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
indirects[gl_GlobalInvocationID.x] = uvec4(0, 1, 1, 0);
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
layout(local_size_x_id = 0) in;
|
||||
|
||||
layout(set = 0, binding = 0, std430) writeonly buffer ToClear
|
||||
{
|
||||
uint elems[];
|
||||
} mask_ram;
|
||||
|
||||
void main()
|
||||
{
|
||||
mask_ram.elems[gl_GlobalInvocationID.x] = 0u;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
layout(local_size_x_id = 0) in;
|
||||
layout(constant_id = 1) const int PAGE_STRIDE = 256;
|
||||
|
||||
layout(set = 0, binding = 0, std430) writeonly buffer SSBO
|
||||
{
|
||||
uint write_mask[];
|
||||
};
|
||||
|
||||
layout(set = 1, binding = 0, std140) uniform UBO
|
||||
{
|
||||
uvec4 offsets[1024];
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
uint offset = offsets[gl_WorkGroupID.x >> 2u][gl_WorkGroupID.x & 3u];
|
||||
offset *= PAGE_STRIDE;
|
||||
write_mask[offset + gl_LocalInvocationIndex] = 0u;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef COMBINER_H_
|
||||
#define COMBINER_H_
|
||||
|
||||
#include "clamping.h"
|
||||
|
||||
ivec4 special_expand(ivec4 value)
|
||||
{
|
||||
// Special sign-extend without explicit clamp.
|
||||
return bitfieldExtract(value - 0x80, 0, 9) + 0x80;
|
||||
}
|
||||
|
||||
i16x4 combiner_equation(ivec4 a, ivec4 b, ivec4 c, ivec4 d)
|
||||
{
|
||||
// Sign-extend multiplier to 9 bits.
|
||||
c = bitfieldExtract(c, 0, 9);
|
||||
|
||||
// Need this to deal with very specific 9-bit sign bits ...
|
||||
a = special_expand(a);
|
||||
b = special_expand(b);
|
||||
d = special_expand(d);
|
||||
|
||||
ivec4 color = (a - b) * c;
|
||||
color += 0x80;
|
||||
return i16x4(color >> 8) + i16x4(d);
|
||||
}
|
||||
|
||||
struct CombinerInputs
|
||||
{
|
||||
u8x4 constant_muladd;
|
||||
u8x4 constant_mulsub;
|
||||
u8x4 constant_mul;
|
||||
u8x4 constant_add;
|
||||
|
||||
u8x4 shade;
|
||||
i16x4 combined;
|
||||
i16x4 texel0;
|
||||
i16x4 texel1;
|
||||
i16 lod_frac;
|
||||
i16 noise;
|
||||
};
|
||||
|
||||
const int RGB_MULADD_COMBINED = 0;
|
||||
const int RGB_MULADD_TEXEL0 = 1;
|
||||
const int RGB_MULADD_TEXEL1 = 2;
|
||||
const int RGB_MULADD_SHADE = 4;
|
||||
const int RGB_MULADD_ONE = 6;
|
||||
const int RGB_MULADD_NOISE = 7;
|
||||
|
||||
const int RGB_MULSUB_COMBINED = 0;
|
||||
const int RGB_MULSUB_TEXEL0 = 1;
|
||||
const int RGB_MULSUB_TEXEL1 = 2;
|
||||
const int RGB_MULSUB_SHADE = 4;
|
||||
const int RGB_MULSUB_K4 = 7;
|
||||
|
||||
const int RGB_MUL_COMBINED = 0;
|
||||
const int RGB_MUL_TEXEL0 = 1;
|
||||
const int RGB_MUL_TEXEL1 = 2;
|
||||
const int RGB_MUL_SHADE = 4;
|
||||
const int RGB_MUL_COMBINED_ALPHA = 7;
|
||||
const int RGB_MUL_TEXEL0_ALPHA = 8;
|
||||
const int RGB_MUL_TEXEL1_ALPHA = 9;
|
||||
const int RGB_MUL_SHADE_ALPHA = 11;
|
||||
const int RGB_MUL_LOD_FRAC = 13;
|
||||
const int RGB_MUL_K5 = 15;
|
||||
|
||||
const int RGB_ADD_COMBINED = 0;
|
||||
const int RGB_ADD_TEXEL0 = 1;
|
||||
const int RGB_ADD_TEXEL1 = 2;
|
||||
const int RGB_ADD_SHADE = 4;
|
||||
const int RGB_ADD_ONE = 6;
|
||||
|
||||
const int ALPHA_ADDSUB_COMBINED = 0;
|
||||
const int ALPHA_ADDSUB_TEXEL0_ALPHA = 1;
|
||||
const int ALPHA_ADDSUB_TEXEL1_ALPHA = 2;
|
||||
const int ALPHA_ADDSUB_SHADE_ALPHA = 4;
|
||||
const int ALPHA_ADDSUB_ONE = 6;
|
||||
|
||||
const int ALPHA_MUL_LOD_FRAC = 0;
|
||||
const int ALPHA_MUL_TEXEL0_ALPHA = 1;
|
||||
const int ALPHA_MUL_TEXEL1_ALPHA = 2;
|
||||
const int ALPHA_MUL_SHADE_ALPHA = 4;
|
||||
|
||||
ivec4 select_muladd(CombinerInputs inputs, int selector_rgb, int selector_alpha)
|
||||
{
|
||||
ivec3 res;
|
||||
switch (selector_rgb)
|
||||
{
|
||||
case RGB_MULADD_COMBINED: res = inputs.combined.rgb; break;
|
||||
case RGB_MULADD_TEXEL0: res = inputs.texel0.rgb; break;
|
||||
case RGB_MULADD_TEXEL1: res = inputs.texel1.rgb; break;
|
||||
case RGB_MULADD_SHADE: res = inputs.shade.rgb; break;
|
||||
case RGB_MULADD_NOISE: res = ivec3(inputs.noise); break;
|
||||
case RGB_MULADD_ONE: res = ivec3(0x100); break;
|
||||
default: res = inputs.constant_muladd.rgb; break;
|
||||
}
|
||||
|
||||
int alpha;
|
||||
switch (selector_alpha)
|
||||
{
|
||||
case ALPHA_ADDSUB_COMBINED: alpha = inputs.combined.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL0_ALPHA: alpha = inputs.texel0.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL1_ALPHA: alpha = inputs.texel1.a; break;
|
||||
case ALPHA_ADDSUB_SHADE_ALPHA: alpha = inputs.shade.a; break;
|
||||
case ALPHA_ADDSUB_ONE: alpha = 0x100; break;
|
||||
default: alpha = inputs.constant_muladd.a; break;
|
||||
}
|
||||
return ivec4(res, alpha);
|
||||
}
|
||||
|
||||
ivec4 select_mulsub(CombinerInputs inputs, int selector_rgb, int selector_alpha)
|
||||
{
|
||||
ivec3 res;
|
||||
switch (selector_rgb)
|
||||
{
|
||||
case RGB_MULSUB_COMBINED: res = inputs.combined.rgb; break;
|
||||
case RGB_MULSUB_TEXEL0: res = inputs.texel0.rgb; break;
|
||||
case RGB_MULSUB_TEXEL1: res = inputs.texel1.rgb; break;
|
||||
case RGB_MULSUB_SHADE: res = inputs.shade.rgb; break;
|
||||
case RGB_MULSUB_K4: res = ivec3((int(inputs.constant_mulsub.g) << 8) | inputs.constant_mulsub.b); break;
|
||||
default: res = inputs.constant_mulsub.rgb; break;
|
||||
}
|
||||
|
||||
int alpha;
|
||||
switch (selector_alpha)
|
||||
{
|
||||
case ALPHA_ADDSUB_COMBINED: alpha = inputs.combined.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL0_ALPHA: alpha = inputs.texel0.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL1_ALPHA: alpha = inputs.texel1.a; break;
|
||||
case ALPHA_ADDSUB_SHADE_ALPHA: alpha = inputs.shade.a; break;
|
||||
case ALPHA_ADDSUB_ONE: alpha = 0x100; break;
|
||||
default: alpha = inputs.constant_mulsub.a; break;
|
||||
}
|
||||
return ivec4(res, alpha);
|
||||
}
|
||||
|
||||
ivec4 select_mul(CombinerInputs inputs, int selector_rgb, int selector_alpha)
|
||||
{
|
||||
ivec3 res;
|
||||
switch (selector_rgb)
|
||||
{
|
||||
case RGB_MUL_COMBINED: res = inputs.combined.rgb; break;
|
||||
case RGB_MUL_COMBINED_ALPHA: res = inputs.combined.aaa; break;
|
||||
case RGB_MUL_TEXEL0: res = inputs.texel0.rgb; break;
|
||||
case RGB_MUL_TEXEL1: res = inputs.texel1.rgb; break;
|
||||
case RGB_MUL_SHADE: res = inputs.shade.rgb; break;
|
||||
case RGB_MUL_TEXEL0_ALPHA: res = inputs.texel0.aaa; break;
|
||||
case RGB_MUL_TEXEL1_ALPHA: res = inputs.texel1.aaa; break;
|
||||
case RGB_MUL_SHADE_ALPHA: res = inputs.shade.aaa; break;
|
||||
case RGB_MUL_LOD_FRAC: res = ivec3(inputs.lod_frac); break;
|
||||
case RGB_MUL_K5: res = ivec3((int(inputs.constant_mul.g) << 8) | inputs.constant_mul.b); break;
|
||||
default: res = inputs.constant_mul.rgb; break;
|
||||
}
|
||||
|
||||
int alpha;
|
||||
switch (selector_alpha)
|
||||
{
|
||||
case ALPHA_MUL_LOD_FRAC: alpha = inputs.lod_frac; break;
|
||||
case ALPHA_MUL_TEXEL0_ALPHA: alpha = inputs.texel0.a; break;
|
||||
case ALPHA_MUL_TEXEL1_ALPHA: alpha = inputs.texel1.a; break;
|
||||
case ALPHA_MUL_SHADE_ALPHA: alpha = inputs.shade.a; break;
|
||||
default: alpha = inputs.constant_mul.a; break;
|
||||
}
|
||||
return ivec4(res, alpha);
|
||||
}
|
||||
|
||||
ivec4 select_add(CombinerInputs inputs, int selector_rgb, int selector_alpha)
|
||||
{
|
||||
ivec3 res;
|
||||
switch (selector_rgb)
|
||||
{
|
||||
case RGB_ADD_COMBINED: res = inputs.combined.rgb; break;
|
||||
case RGB_ADD_TEXEL0: res = inputs.texel0.rgb; break;
|
||||
case RGB_ADD_TEXEL1: res = inputs.texel1.rgb; break;
|
||||
case RGB_ADD_SHADE: res = inputs.shade.rgb; break;
|
||||
case RGB_ADD_ONE: res = ivec3(0x100); break;
|
||||
default: res = inputs.constant_add.rgb; break;
|
||||
}
|
||||
|
||||
int alpha;
|
||||
switch (selector_alpha)
|
||||
{
|
||||
case ALPHA_ADDSUB_COMBINED: alpha = inputs.combined.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL0_ALPHA: alpha = inputs.texel0.a; break;
|
||||
case ALPHA_ADDSUB_TEXEL1_ALPHA: alpha = inputs.texel1.a; break;
|
||||
case ALPHA_ADDSUB_SHADE_ALPHA: alpha = inputs.shade.a; break;
|
||||
case ALPHA_ADDSUB_ONE: alpha = 0x100; break;
|
||||
default: alpha = inputs.constant_add.a; break;
|
||||
}
|
||||
return ivec4(res, alpha);
|
||||
}
|
||||
|
||||
i16x4 combiner_cycle0(CombinerInputs inputs, u8x4 combiner_inputs_rgb, u8x4 combiner_inputs_alpha, int alpha_dith,
|
||||
int coverage, bool cvg_times_alpha, bool alpha_cvg_select, bool alpha_test, out u8 alpha_test_reference)
|
||||
{
|
||||
ivec4 muladd = select_muladd(inputs, combiner_inputs_rgb.x, combiner_inputs_alpha.x);
|
||||
ivec4 mulsub = select_mulsub(inputs, combiner_inputs_rgb.y, combiner_inputs_alpha.y);
|
||||
ivec4 mul = select_mul(inputs, combiner_inputs_rgb.z, combiner_inputs_alpha.z);
|
||||
ivec4 add = select_add(inputs, combiner_inputs_rgb.w, combiner_inputs_alpha.w);
|
||||
|
||||
i16x4 combined = combiner_equation(muladd, mulsub, mul, add);
|
||||
|
||||
if (alpha_test)
|
||||
{
|
||||
int clamped_alpha = clamp_9bit(combined.a);
|
||||
// Expands 0xff to 0x100 to avoid having to divide by 2**n - 1.
|
||||
int expanded_alpha = clamped_alpha + ((clamped_alpha + 1) >> 8);
|
||||
|
||||
if (alpha_cvg_select)
|
||||
{
|
||||
int modulated_alpha;
|
||||
if (cvg_times_alpha)
|
||||
modulated_alpha = (expanded_alpha * coverage + 4) >> 3;
|
||||
else
|
||||
modulated_alpha = coverage << 5;
|
||||
expanded_alpha = modulated_alpha;
|
||||
}
|
||||
else
|
||||
expanded_alpha += alpha_dith;
|
||||
|
||||
alpha_test_reference = u8(clamp(expanded_alpha, 0, 0xff));
|
||||
}
|
||||
else
|
||||
alpha_test_reference = U8_C(0);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
i16x4 combiner_cycle1(CombinerInputs inputs, u8x4 combiner_inputs_rgb, u8x4 combiner_inputs_alpha, int alpha_dith,
|
||||
inout int coverage, bool cvg_times_alpha, bool alpha_cvg_select)
|
||||
{
|
||||
ivec4 muladd = select_muladd(inputs, combiner_inputs_rgb.x, combiner_inputs_alpha.x);
|
||||
ivec4 mulsub = select_mulsub(inputs, combiner_inputs_rgb.y, combiner_inputs_alpha.y);
|
||||
ivec4 mul = select_mul(inputs, combiner_inputs_rgb.z, combiner_inputs_alpha.z);
|
||||
ivec4 add = select_add(inputs, combiner_inputs_rgb.w, combiner_inputs_alpha.w);
|
||||
|
||||
i16x4 combined = combiner_equation(muladd, mulsub, mul, add);
|
||||
|
||||
combined = clamp_9bit_notrunc(combined);
|
||||
|
||||
// Expands 0xff to 0x100 to avoid having to divide by 2**n - 1.
|
||||
int expanded_alpha = combined.a + ((combined.a + 1) >> 8);
|
||||
|
||||
int modulated_alpha;
|
||||
if (cvg_times_alpha)
|
||||
{
|
||||
modulated_alpha = (expanded_alpha * coverage + 4) >> 3;
|
||||
coverage = modulated_alpha >> 5;
|
||||
}
|
||||
else
|
||||
modulated_alpha = coverage << 5;
|
||||
|
||||
if (alpha_cvg_select)
|
||||
expanded_alpha = modulated_alpha;
|
||||
else
|
||||
expanded_alpha += alpha_dith;
|
||||
|
||||
combined.a = i16(clamp(expanded_alpha, 0, 0xff));
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef COVERAGE_H_
|
||||
#define COVERAGE_H_
|
||||
|
||||
#include "data_structures.h"
|
||||
|
||||
const int SUBPIXELS_LOG2 = 2;
|
||||
const int SUBPIXELS = 1 << SUBPIXELS_LOG2;
|
||||
|
||||
u8 compute_coverage(u16x4 xleft, u16x4 xright, int x)
|
||||
{
|
||||
u16x4 xshift = u16x4(0, 4, 2, 6) + (u16(x) << U16_C(3));
|
||||
bvec4 clip_lo_x01 = lessThan(xshift, xleft.xxyy);
|
||||
bvec4 clip_lo_x23 = lessThan(xshift, xleft.zzww);
|
||||
bvec4 clip_hi_x01 = greaterThanEqual(xshift, xright.xxyy);
|
||||
bvec4 clip_hi_x23 = greaterThanEqual(xshift, xright.zzww);
|
||||
|
||||
u8x4 clip_x0 = u8x4(clip_lo_x01) | u8x4(clip_hi_x01);
|
||||
u8x4 clip_x1 = u8x4(clip_lo_x23) | u8x4(clip_hi_x23);
|
||||
u8x4 clip_x = clip_x0 * u8x4(1, 2, 4, 8) + clip_x1 * u8x4(16, 32, 64, 128);
|
||||
u8 clip_coverage = (clip_x.x | clip_x.y) | (clip_x.z | clip_x.w);
|
||||
return ~clip_coverage & U8_C(0xff);
|
||||
}
|
||||
|
||||
const int COVERAGE_CLAMP = 0;
|
||||
const int COVERAGE_WRAP = 1;
|
||||
const int COVERAGE_ZAP = 2;
|
||||
const int COVERAGE_SAVE = 3;
|
||||
|
||||
int blend_coverage(int coverage, int memory_coverage, bool blend_en, int mode)
|
||||
{
|
||||
int res = 0;
|
||||
switch (mode)
|
||||
{
|
||||
case COVERAGE_CLAMP:
|
||||
{
|
||||
if (blend_en)
|
||||
res = min(7, memory_coverage + coverage); // image_read_en to read memory coverage, otherwise, it's 7.
|
||||
else
|
||||
res = (coverage - 1) & 7;
|
||||
break;
|
||||
}
|
||||
|
||||
case COVERAGE_WRAP:
|
||||
res = (coverage + memory_coverage) & 7;
|
||||
break;
|
||||
|
||||
case COVERAGE_ZAP:
|
||||
res = 7;
|
||||
break;
|
||||
|
||||
case COVERAGE_SAVE:
|
||||
res = memory_coverage;
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,347 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef DATA_STRUCTURES_H_
|
||||
#define DATA_STRUCTURES_H_
|
||||
|
||||
// Data structures which are supposed to match up with rdp_data_structures.hpp.
|
||||
// A little dirty to duplicate like this, but it's non-trivial to share headers with C++,
|
||||
// especially when we need to deal with small integer types.
|
||||
|
||||
const int TRIANGLE_SETUP_FLIP_BIT = 1 << 0;
|
||||
const int TRIANGLE_SETUP_DO_OFFSET_BIT = 1 << 1;
|
||||
const int TRIANGLE_SETUP_SKIP_XFRAC_BIT = 1 << 2;
|
||||
const int TRIANGLE_SETUP_INTERLACE_FIELD_BIT = 1 << 3;
|
||||
const int TRIANGLE_SETUP_INTERLACE_KEEP_ODD_BIT = 1 << 4;
|
||||
const int TRIANGLE_SETUP_DISABLE_UPSCALING_BIT = 1 << 5;
|
||||
const int TRIANGLE_SETUP_NATIVE_LOD_BIT = 1 << 6;
|
||||
const int TRIANGLE_SETUP_FILL_COPY_RASTER_BIT = 1 << 7;
|
||||
|
||||
const int RASTERIZATION_INTERLACE_FIELD_BIT = 1 << 0;
|
||||
const int RASTERIZATION_INTERLACE_KEEP_ODD_BIT = 1 << 1;
|
||||
const int RASTERIZATION_AA_BIT = 1 << 2;
|
||||
const int RASTERIZATION_PERSPECTIVE_CORRECT_BIT = 1 << 3;
|
||||
const int RASTERIZATION_TLUT_BIT = 1 << 4;
|
||||
const int RASTERIZATION_TLUT_TYPE_BIT = 1 << 5;
|
||||
const int RASTERIZATION_CVG_TIMES_ALPHA_BIT = 1 << 6;
|
||||
const int RASTERIZATION_ALPHA_CVG_SELECT_BIT = 1 << 7;
|
||||
const int RASTERIZATION_MULTI_CYCLE_BIT = 1 << 8;
|
||||
const int RASTERIZATION_TEX_LOD_ENABLE_BIT = 1 << 9;
|
||||
const int RASTERIZATION_SHARPEN_LOD_ENABLE_BIT = 1 << 10;
|
||||
const int RASTERIZATION_DETAIL_LOD_ENABLE_BIT = 1 << 11;
|
||||
const int RASTERIZATION_FILL_BIT = 1 << 12;
|
||||
const int RASTERIZATION_COPY_BIT = 1 << 13;
|
||||
const int RASTERIZATION_SAMPLE_MODE_BIT = 1 << 14;
|
||||
const int RASTERIZATION_ALPHA_TEST_BIT = 1 << 15;
|
||||
const int RASTERIZATION_ALPHA_TEST_DITHER_BIT = 1 << 16;
|
||||
const int RASTERIZATION_SAMPLE_MID_TEXEL_BIT = 1 << 17;
|
||||
const int RASTERIZATION_USES_TEXEL0_BIT = 1 << 18;
|
||||
const int RASTERIZATION_USES_TEXEL1_BIT = 1 << 19;
|
||||
const int RASTERIZATION_USES_LOD_BIT = 1 << 20;
|
||||
const int RASTERIZATION_USES_PIPELINED_TEXEL1_BIT = 1 << 21;
|
||||
const int RASTERIZATION_CONVERT_ONE_BIT = 1 << 22;
|
||||
const int RASTERIZATION_BILERP_0_BIT = 1 << 23;
|
||||
const int RASTERIZATION_BILERP_1_BIT = 1 << 24;
|
||||
const int RASTERIZATION_NEED_NOISE_DUAL_BIT = 1 << 25;
|
||||
const int RASTERIZATION_UPSCALING_LOG2_BIT_OFFSET = 26;
|
||||
const int RASTERIZATION_NEED_NOISE_BIT = 1 << 28;
|
||||
const int RASTERIZATION_USE_STATIC_TEXTURE_SIZE_FORMAT_BIT = 1 << 29;
|
||||
const int RASTERIZATION_USE_SPECIALIZATION_CONSTANT_BIT = 1 << 30;
|
||||
|
||||
const int DEPTH_BLEND_DEPTH_TEST_BIT = 1 << 0;
|
||||
const int DEPTH_BLEND_DEPTH_UPDATE_BIT = 1 << 1;
|
||||
const int DEPTH_BLEND_FORCE_BLEND_BIT = 1 << 3;
|
||||
const int DEPTH_BLEND_IMAGE_READ_ENABLE_BIT = 1 << 4;
|
||||
const int DEPTH_BLEND_COLOR_ON_COVERAGE_BIT = 1 << 5;
|
||||
const int DEPTH_BLEND_MULTI_CYCLE_BIT = 1 << 6;
|
||||
const int DEPTH_BLEND_AA_BIT = 1 << 7;
|
||||
const int DEPTH_BLEND_DITHER_ENABLE_BIT = 1 << 8;
|
||||
|
||||
struct TriangleSetupMem
|
||||
{
|
||||
int xh, xm, xl;
|
||||
mem_i16 yh, ym;
|
||||
int dxhdy, dxmdy, dxldy;
|
||||
mem_i16 yl; mem_u8 flags; mem_u8 tile;
|
||||
};
|
||||
|
||||
#if SMALL_TYPES
|
||||
#define TriangleSetup TriangleSetupMem
|
||||
#else
|
||||
struct TriangleSetup
|
||||
{
|
||||
int xh, xm, xl;
|
||||
i16 yh, ym;
|
||||
int dxhdy, dxmdy, dxldy;
|
||||
i16 yl; u8 flags; u8 tile;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct AttributeSetupMem
|
||||
{
|
||||
ivec4 rgba;
|
||||
ivec4 drgba_dx;
|
||||
ivec4 drgba_de;
|
||||
ivec4 drgba_dy;
|
||||
|
||||
ivec4 stzw;
|
||||
ivec4 dstzw_dx;
|
||||
ivec4 dstzw_de;
|
||||
ivec4 dstzw_dy;
|
||||
};
|
||||
#define AttributeSetup AttributeSetupMem
|
||||
|
||||
struct SpanSetupMem
|
||||
{
|
||||
ivec4 rgba;
|
||||
ivec4 stzw;
|
||||
|
||||
mem_u16x4 xleft;
|
||||
mem_u16x4 xright;
|
||||
|
||||
int interpolation_base_x;
|
||||
int start_x;
|
||||
int end_x;
|
||||
mem_i16 lodlength;
|
||||
mem_u16 valid_line;
|
||||
};
|
||||
#if SMALL_TYPES
|
||||
#define SpanSetup SpanSetupMem
|
||||
#else
|
||||
struct SpanSetup
|
||||
{
|
||||
ivec4 rgba;
|
||||
ivec4 stzw;
|
||||
|
||||
u16x4 xleft;
|
||||
u16x4 xright;
|
||||
|
||||
int interpolation_base_x;
|
||||
int start_x;
|
||||
int end_x;
|
||||
i16 lodlength;
|
||||
u16 valid_line;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct SpanInfoOffsetsMem
|
||||
{
|
||||
int offset;
|
||||
int ylo;
|
||||
int yhi;
|
||||
int padding;
|
||||
};
|
||||
#define SpanInfoOffsets SpanInfoOffsetsMem
|
||||
|
||||
struct DerivedSetupMem
|
||||
{
|
||||
mem_u8x4 constant_muladd0;
|
||||
mem_u8x4 constant_mulsub0;
|
||||
mem_u8x4 constant_mul0;
|
||||
mem_u8x4 constant_add0;
|
||||
|
||||
mem_u8x4 constant_muladd1;
|
||||
mem_u8x4 constant_mulsub1;
|
||||
mem_u8x4 constant_mul1;
|
||||
mem_u8x4 constant_add1;
|
||||
|
||||
mem_u8x4 fog_color;
|
||||
mem_u8x4 blend_color;
|
||||
uint fill_color;
|
||||
|
||||
mem_u16 dz;
|
||||
mem_u8 dz_compressed;
|
||||
mem_u8 min_lod;
|
||||
|
||||
mem_i16x4 factors;
|
||||
};
|
||||
|
||||
#if SMALL_TYPES
|
||||
#define DerivedSetup DerivedSetupMem
|
||||
#else
|
||||
struct DerivedSetup
|
||||
{
|
||||
u8x4 constant_muladd0;
|
||||
u8x4 constant_mulsub0;
|
||||
u8x4 constant_mul0;
|
||||
u8x4 constant_add0;
|
||||
|
||||
u8x4 constant_muladd1;
|
||||
u8x4 constant_mulsub1;
|
||||
u8x4 constant_mul1;
|
||||
u8x4 constant_add1;
|
||||
|
||||
u8x4 fog_color;
|
||||
u8x4 blend_color;
|
||||
uint fill_color;
|
||||
|
||||
u16 dz;
|
||||
u8 dz_compressed;
|
||||
u8 min_lod;
|
||||
|
||||
i16x4 factors;
|
||||
};
|
||||
#endif
|
||||
|
||||
#define ScissorStateMem ivec4
|
||||
|
||||
struct ScissorState
|
||||
{
|
||||
int xlo, ylo, xhi, yhi;
|
||||
};
|
||||
|
||||
const int TILE_INFO_CLAMP_S_BIT = 1 << 0;
|
||||
const int TILE_INFO_MIRROR_S_BIT = 1 << 1;
|
||||
const int TILE_INFO_CLAMP_T_BIT = 1 << 2;
|
||||
const int TILE_INFO_MIRROR_T_BIT = 1 << 3;
|
||||
|
||||
struct TileInfoMem
|
||||
{
|
||||
uint slo;
|
||||
uint shi;
|
||||
uint tlo;
|
||||
uint thi;
|
||||
uint offset;
|
||||
uint stride;
|
||||
mem_u8 fmt;
|
||||
mem_u8 size;
|
||||
mem_u8 palette;
|
||||
mem_u8 mask_s;
|
||||
mem_u8 shift_s;
|
||||
mem_u8 mask_t;
|
||||
mem_u8 shift_t;
|
||||
mem_u8 flags;
|
||||
};
|
||||
|
||||
#if SMALL_TYPES
|
||||
#define TileInfo TileInfoMem
|
||||
#else
|
||||
struct TileInfo
|
||||
{
|
||||
uint slo;
|
||||
uint shi;
|
||||
uint tlo;
|
||||
uint thi;
|
||||
uint offset;
|
||||
uint stride;
|
||||
u8 fmt;
|
||||
u8 size;
|
||||
u8 palette;
|
||||
u8 mask_s;
|
||||
u8 shift_s;
|
||||
u8 mask_t;
|
||||
u8 shift_t;
|
||||
u8 flags;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct StaticRasterizationStateMem
|
||||
{
|
||||
mem_u8x4 combiner_inputs_rgb0;
|
||||
mem_u8x4 combiner_inputs_alpha0;
|
||||
mem_u8x4 combiner_inputs_rgb1;
|
||||
mem_u8x4 combiner_inputs_alpha1;
|
||||
uint flags;
|
||||
int dither;
|
||||
int texture_size;
|
||||
int texture_fmt;
|
||||
};
|
||||
|
||||
#if SMALL_TYPES
|
||||
#define StaticRasterizationState StaticRasterizationStateMem
|
||||
#else
|
||||
struct StaticRasterizationState
|
||||
{
|
||||
u8x4 combiner_inputs_rgb0;
|
||||
u8x4 combiner_inputs_alpha0;
|
||||
u8x4 combiner_inputs_rgb1;
|
||||
u8x4 combiner_inputs_alpha1;
|
||||
uint flags;
|
||||
int dither;
|
||||
int texture_size;
|
||||
int texture_fmt;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct DepthBlendStateMem
|
||||
{
|
||||
mem_u8x4 blend_modes0;
|
||||
mem_u8x4 blend_modes1;
|
||||
uint flags;
|
||||
mem_u8 coverage_mode;
|
||||
mem_u8 z_mode;
|
||||
mem_u8 padding0;
|
||||
mem_u8 padding1;
|
||||
};
|
||||
|
||||
#if SMALL_TYPES
|
||||
#define DepthBlendState DepthBlendStateMem
|
||||
#else
|
||||
struct DepthBlendState
|
||||
{
|
||||
u8x4 blend_modes0;
|
||||
u8x4 blend_modes1;
|
||||
uint flags;
|
||||
u8 coverage_mode;
|
||||
u8 z_mode;
|
||||
u8 padding0;
|
||||
u8 padding1;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct InstanceIndicesMem
|
||||
{
|
||||
mem_u8x4 static_depth_tmem;
|
||||
mem_u8x4 other;
|
||||
mem_u8 tile_infos[8];
|
||||
};
|
||||
|
||||
struct TMEMInstance16Mem
|
||||
{
|
||||
mem_u16 elems[2048];
|
||||
};
|
||||
|
||||
struct TMEMInstance8Mem
|
||||
{
|
||||
mem_u8 elems[4096];
|
||||
};
|
||||
|
||||
struct ShadedData
|
||||
{
|
||||
u8x4 combined;
|
||||
int z_dith;
|
||||
u8 coverage_count;
|
||||
u8 shade_alpha;
|
||||
};
|
||||
|
||||
const int COVERAGE_FILL_BIT = 0x40;
|
||||
const int COVERAGE_COPY_BIT = 0x20;
|
||||
|
||||
struct GlobalFBInfo
|
||||
{
|
||||
int dx_shift;
|
||||
int dx_mask;
|
||||
int fb_size;
|
||||
uint base_primitive_index;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,134 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef DATA_STRUCTURES_BUFFERS_H_
|
||||
#define DATA_STRUCTURES_BUFFERS_H_
|
||||
|
||||
#include "data_structures.h"
|
||||
|
||||
layout(set = 0, binding = 0, std430) buffer VRAM32
|
||||
{
|
||||
uint data[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 0, std430) buffer VRAM16
|
||||
{
|
||||
mem_u16 data[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 0, std430) buffer VRAM8
|
||||
{
|
||||
mem_u8 data[];
|
||||
} vram8;
|
||||
|
||||
layout(set = 0, binding = 1, std430) buffer HiddenVRAM
|
||||
{
|
||||
mem_u8 data[];
|
||||
} hidden_vram;
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer TMEM16
|
||||
{
|
||||
TMEMInstance16Mem instances[];
|
||||
} tmem16;
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer TMEM8
|
||||
{
|
||||
TMEMInstance8Mem instances[];
|
||||
} tmem8;
|
||||
|
||||
layout(set = 1, binding = 0, std430) readonly buffer TriangleSetupBuffer
|
||||
{
|
||||
TriangleSetupMem elems[];
|
||||
} triangle_setup;
|
||||
#include "load_triangle_setup.h"
|
||||
|
||||
layout(set = 1, binding = 1, std430) readonly buffer AttributeSetupBuffer
|
||||
{
|
||||
AttributeSetupMem elems[];
|
||||
} attribute_setup;
|
||||
#include "load_attribute_setup.h"
|
||||
|
||||
layout(set = 1, binding = 2, std430) readonly buffer DerivedSetupBuffer
|
||||
{
|
||||
DerivedSetupMem elems[];
|
||||
} derived_setup;
|
||||
#include "load_derived_setup.h"
|
||||
|
||||
layout(set = 1, binding = 3, std430) readonly buffer ScissorStateBuffer
|
||||
{
|
||||
ScissorStateMem elems[];
|
||||
} scissor_state;
|
||||
#include "load_scissor_state.h"
|
||||
|
||||
layout(set = 1, binding = 4, std430) readonly buffer StaticRasterStateBuffer
|
||||
{
|
||||
StaticRasterizationStateMem elems[];
|
||||
} static_raster_state;
|
||||
#include "load_static_raster_state.h"
|
||||
|
||||
layout(set = 1, binding = 5, std430) readonly buffer DepthBlendStateBuffer
|
||||
{
|
||||
DepthBlendStateMem elems[];
|
||||
} depth_blend_state;
|
||||
#include "load_depth_blend_state.h"
|
||||
|
||||
layout(set = 1, binding = 6, std430) readonly buffer StateIndicesBuffer
|
||||
{
|
||||
InstanceIndicesMem elems[];
|
||||
} state_indices;
|
||||
|
||||
layout(set = 1, binding = 7, std430) readonly buffer TileInfoBuffer
|
||||
{
|
||||
TileInfoMem elems[];
|
||||
} tile_infos;
|
||||
#include "load_tile_info.h"
|
||||
|
||||
layout(set = 1, binding = 8, std430) readonly buffer SpanSetups
|
||||
{
|
||||
SpanSetupMem elems[];
|
||||
} span_setups;
|
||||
#include "load_span_setup.h"
|
||||
|
||||
layout(set = 1, binding = 9, std430) readonly buffer SpanInfoOffsetBuffer
|
||||
{
|
||||
SpanInfoOffsetsMem elems[];
|
||||
} span_offsets;
|
||||
#include "load_span_offsets.h"
|
||||
|
||||
layout(set = 1, binding = 10) uniform utextureBuffer uBlenderDividerLUT;
|
||||
|
||||
layout(set = 1, binding = 11, std430) readonly buffer TileBinning
|
||||
{
|
||||
uint elems[];
|
||||
} tile_binning;
|
||||
|
||||
layout(set = 1, binding = 12, std430) readonly buffer TileBinningCoarse
|
||||
{
|
||||
uint elems[];
|
||||
} tile_binning_coarse;
|
||||
|
||||
layout(set = 2, binding = 0, std140) uniform GlobalConstants
|
||||
{
|
||||
GlobalFBInfo fb_info;
|
||||
} global_constants;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef DEBUG_H_
|
||||
#define DEBUG_H_
|
||||
|
||||
#if defined(DEBUG_ENABLE) && DEBUG_ENABLE
|
||||
#include "debug_channel.h"
|
||||
|
||||
const uint CODE_ASSERT_EQUAL = 0;
|
||||
const uint CODE_ASSERT_NOT_EQUAL = 1;
|
||||
const uint CODE_ASSERT_LESS_THAN = 2;
|
||||
const uint CODE_ASSERT_LESS_THAN_EQUAL = 3;
|
||||
const uint CODE_GENERIC = 4;
|
||||
const uint CODE_HEX = 5;
|
||||
|
||||
void ASSERT_EQUAL_(int line, int a, int b)
|
||||
{
|
||||
if (a != b)
|
||||
add_debug_message(CODE_ASSERT_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_NOT_EQUAL_(int line, int a, int b)
|
||||
{
|
||||
if (a == b)
|
||||
add_debug_message(CODE_ASSERT_NOT_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_LESS_THAN_(int line, int a, int b)
|
||||
{
|
||||
if (a >= b)
|
||||
add_debug_message(CODE_ASSERT_LESS_THAN, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_LESS_THAN_EQUAL_(int line, int a, int b)
|
||||
{
|
||||
if (a > b)
|
||||
add_debug_message(CODE_ASSERT_LESS_THAN_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_EQUAL_(int line, uint a, uint b)
|
||||
{
|
||||
if (a != b)
|
||||
add_debug_message(CODE_ASSERT_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_NOT_EQUAL_(int line, uint a, uint b)
|
||||
{
|
||||
if (a == b)
|
||||
add_debug_message(CODE_ASSERT_NOT_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_LESS_THAN_(int line, uint a, uint b)
|
||||
{
|
||||
if (a >= b)
|
||||
add_debug_message(CODE_ASSERT_LESS_THAN, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void ASSERT_LESS_THAN_EQUAL_(int line, uint a, uint b)
|
||||
{
|
||||
if (a > b)
|
||||
add_debug_message(CODE_ASSERT_LESS_THAN_EQUAL, gl_GlobalInvocationID, ivec3(line, a, b));
|
||||
}
|
||||
|
||||
void GENERIC_MESSAGE_(int line)
|
||||
{
|
||||
add_debug_message(CODE_GENERIC, gl_GlobalInvocationID, line);
|
||||
}
|
||||
|
||||
void GENERIC_MESSAGE_(int line, uint v)
|
||||
{
|
||||
add_debug_message(CODE_GENERIC, gl_GlobalInvocationID, uvec2(line, v));
|
||||
}
|
||||
|
||||
void GENERIC_MESSAGE_(int line, uvec2 v)
|
||||
{
|
||||
add_debug_message(CODE_GENERIC, gl_GlobalInvocationID, uvec3(line, v));
|
||||
}
|
||||
|
||||
void GENERIC_MESSAGE_(int line, uvec3 v)
|
||||
{
|
||||
add_debug_message(CODE_GENERIC, gl_GlobalInvocationID, uvec4(line, v));
|
||||
}
|
||||
|
||||
void HEX_MESSAGE_(int line)
|
||||
{
|
||||
add_debug_message(CODE_HEX, gl_GlobalInvocationID, line);
|
||||
}
|
||||
|
||||
void HEX_MESSAGE_(int line, uint v)
|
||||
{
|
||||
add_debug_message(CODE_HEX, gl_GlobalInvocationID, uvec2(line, v));
|
||||
}
|
||||
|
||||
void HEX_MESSAGE_(int line, uvec2 v)
|
||||
{
|
||||
add_debug_message(CODE_HEX, gl_GlobalInvocationID, uvec3(line, v));
|
||||
}
|
||||
|
||||
void HEX_MESSAGE_(int line, uvec3 v)
|
||||
{
|
||||
add_debug_message(CODE_HEX, gl_GlobalInvocationID, uvec4(line, v));
|
||||
}
|
||||
|
||||
#define ASSERT_EQUAL(a, b) ASSERT_EQUAL_(__LINE__, a, b)
|
||||
#define ASSERT_NOT_EQUAL(a, b) ASSERT_NOT_EQUAL_(__LINE__, a, b)
|
||||
#define ASSERT_LESS_THAN(a, b) ASSERT_LESS_THAN_(__LINE__, a, b)
|
||||
#define ASSERT_LESS_THAN_EQUAL(a, b) ASSERT_LESS_THAN_EQUAL_(__LINE__, a, b)
|
||||
#define GENERIC_MESSAGE0() GENERIC_MESSAGE_(__LINE__)
|
||||
#define GENERIC_MESSAGE1(a) GENERIC_MESSAGE_(__LINE__, a)
|
||||
#define GENERIC_MESSAGE2(a, b) GENERIC_MESSAGE_(__LINE__, uvec2(a, b))
|
||||
#define GENERIC_MESSAGE3(a, b, c) GENERIC_MESSAGE_(__LINE__, uvec3(a, b, c))
|
||||
#define HEX_MESSAGE0() HEX_MESSAGE_(__LINE__)
|
||||
#define HEX_MESSAGE1(a) HEX_MESSAGE_(__LINE__, a)
|
||||
#define HEX_MESSAGE2(a, b) HEX_MESSAGE_(__LINE__, uvec2(a, b))
|
||||
#define HEX_MESSAGE3(a, b, c) HEX_MESSAGE_(__LINE__, uvec3(a, b, c))
|
||||
#else
|
||||
#define ASSERT_EQUAL(a, b)
|
||||
#define ASSERT_NOT_EQUAL(a, b)
|
||||
#define ASSERT_LESS_THAN(a, b)
|
||||
#define ASSERT_LESS_THAN_EQUAL(a, b)
|
||||
#define GENERIC_MESSAGE0()
|
||||
#define GENERIC_MESSAGE1(a)
|
||||
#define GENERIC_MESSAGE2(a, b)
|
||||
#define GENERIC_MESSAGE3(a, b, c)
|
||||
#define HEX_MESSAGE0()
|
||||
#define HEX_MESSAGE1(a)
|
||||
#define HEX_MESSAGE2(a, b)
|
||||
#define HEX_MESSAGE3(a, b, c)
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,149 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#if SUBGROUP
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_vote : require
|
||||
#extension GL_KHR_shader_subgroup_ballot : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
#endif
|
||||
#include "small_types.h"
|
||||
|
||||
layout(local_size_x_id = 3, local_size_y_id = 4) in;
|
||||
|
||||
#include "noise.h"
|
||||
#include "debug.h"
|
||||
#include "data_structures_buffers.h"
|
||||
#include "memory_interfacing.h"
|
||||
|
||||
layout(set = 0, binding = 3, std430) readonly buffer ColorBuffer
|
||||
{
|
||||
mem_u8x4 elems[];
|
||||
} color;
|
||||
|
||||
layout(set = 0, binding = 3, std430) readonly buffer ColorRawBuffer
|
||||
{
|
||||
uint elems[];
|
||||
} raw_color;
|
||||
|
||||
layout(set = 0, binding = 4, std430) readonly buffer DepthBuffer
|
||||
{
|
||||
int elems[];
|
||||
} depth;
|
||||
|
||||
layout(set = 0, binding = 5, std430) readonly buffer ShadeAlpha
|
||||
{
|
||||
mem_u8 elems[];
|
||||
} shade_alpha;
|
||||
|
||||
layout(set = 0, binding = 6, std430) readonly buffer Coverage
|
||||
{
|
||||
mem_i8 elems[];
|
||||
} coverage;
|
||||
|
||||
layout(std430, set = 0, binding = 7) readonly buffer TileInstanceOffset
|
||||
{
|
||||
uint elems[];
|
||||
} tile_instance_offsets;
|
||||
|
||||
layout(push_constant, std430) uniform Registers
|
||||
{
|
||||
uint fb_addr_index;
|
||||
uint fb_depth_addr_index;
|
||||
uint fb_width;
|
||||
uint fb_height;
|
||||
uint group_mask;
|
||||
} registers;
|
||||
|
||||
layout(constant_id = 5) const int MAX_PRIMITIVES = 256;
|
||||
layout(constant_id = 6) const int MAX_WIDTH = 1024;
|
||||
|
||||
const int TILE_BINNING_STRIDE = MAX_PRIMITIVES / 32;
|
||||
const int MAX_TILES_X = MAX_WIDTH / int(gl_WorkGroupSize.x);
|
||||
|
||||
// Overall architecture of the tiling is from RetroWarp.
|
||||
|
||||
void main()
|
||||
{
|
||||
int x = int(gl_GlobalInvocationID.x);
|
||||
int y = int(gl_GlobalInvocationID.y);
|
||||
ivec2 tile = ivec2(gl_WorkGroupID.xy);
|
||||
|
||||
int linear_tile = tile.x + tile.y * MAX_TILES_X;
|
||||
int linear_tile_base = linear_tile * TILE_BINNING_STRIDE;
|
||||
|
||||
uint coarse_binned = tile_binning_coarse.elems[linear_tile] & registers.group_mask;
|
||||
if (coarse_binned == 0u)
|
||||
return;
|
||||
|
||||
init_tile(gl_GlobalInvocationID.xy,
|
||||
registers.fb_width, registers.fb_height,
|
||||
registers.fb_addr_index, registers.fb_depth_addr_index);
|
||||
|
||||
while (coarse_binned != 0u)
|
||||
{
|
||||
int mask_index = findLSB(coarse_binned);
|
||||
coarse_binned &= ~uint(1 << mask_index);
|
||||
|
||||
uint tile_instance = tile_instance_offsets.elems[linear_tile_base + mask_index];
|
||||
uint binned = tile_binning.elems[linear_tile_base + mask_index];
|
||||
|
||||
while (binned != 0u)
|
||||
{
|
||||
int i = findLSB(binned);
|
||||
binned &= ~uint(1 << i);
|
||||
uint primitive_index = uint(i + 32 * mask_index);
|
||||
|
||||
uint index = tile_instance * (gl_WorkGroupSize.x * gl_WorkGroupSize.y) + gl_LocalInvocationIndex;
|
||||
int coverage = int(coverage.elems[index]);
|
||||
|
||||
if (coverage >= 0)
|
||||
{
|
||||
if ((coverage & COVERAGE_FILL_BIT) != 0)
|
||||
{
|
||||
fill_color(derived_setup.elems[primitive_index].fill_color);
|
||||
}
|
||||
else if ((coverage & COVERAGE_COPY_BIT) != 0)
|
||||
{
|
||||
uint word = raw_color.elems[index];
|
||||
copy_pipeline(word, primitive_index);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShadedData shaded;
|
||||
shaded.combined = u8x4(color.elems[index]);
|
||||
shaded.z_dith = depth.elems[index];
|
||||
shaded.shade_alpha = u8(shade_alpha.elems[index]);
|
||||
shaded.coverage_count = u8(coverage);
|
||||
depth_blend(x, y, primitive_index, shaded);
|
||||
}
|
||||
}
|
||||
|
||||
tile_instance++;
|
||||
}
|
||||
}
|
||||
|
||||
finish_tile(gl_GlobalInvocationID.xy,
|
||||
registers.fb_width, registers.fb_height,
|
||||
registers.fb_addr_index, registers.fb_depth_addr_index);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef DEPTH_TEST_H_
|
||||
#define DEPTH_TEST_H_
|
||||
|
||||
#include "z_encode.h"
|
||||
|
||||
const int Z_MODE_OPAQUE = 0;
|
||||
const int Z_MODE_INTERPENETRATING = 1;
|
||||
const int Z_MODE_TRANSPARENT = 2;
|
||||
const int Z_MODE_DECAL = 3;
|
||||
|
||||
int combine_dz(int dz)
|
||||
{
|
||||
// Find largest POT which is <= dz.
|
||||
if (dz != 0)
|
||||
dz = 1 << findMSB(dz);
|
||||
return dz;
|
||||
}
|
||||
|
||||
bool depth_test(int z, int dz, int dz_compressed,
|
||||
u16 current_depth, u8 current_dz,
|
||||
inout int coverage_count, int current_coverage_count,
|
||||
bool z_compare, int z_mode,
|
||||
bool force_blend, bool aa_enable,
|
||||
out bool blend_en, out bool coverage_wrap, out u8x2 blend_shift)
|
||||
{
|
||||
bool depth_pass;
|
||||
|
||||
if (z_compare)
|
||||
{
|
||||
int memory_z = z_decompress(current_depth);
|
||||
int memory_dz = dz_decompress(current_dz);
|
||||
int precision_factor = (int(current_depth) >> 11) & 0xf;
|
||||
bool coplanar = false;
|
||||
|
||||
blend_shift.x = u8(clamp(dz_compressed - current_dz, 0, 4));
|
||||
blend_shift.y = u8(clamp(current_dz - dz_compressed, 0, 4));
|
||||
|
||||
if (precision_factor < 3)
|
||||
{
|
||||
if (memory_dz != 0x8000)
|
||||
memory_dz = max(memory_dz << 1, 16 >> precision_factor);
|
||||
else
|
||||
{
|
||||
coplanar = true;
|
||||
memory_dz = 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
int combined_dz = combine_dz(dz | memory_dz);
|
||||
int combined_dz_interpenetrate = combined_dz;
|
||||
combined_dz <<= 3;
|
||||
|
||||
bool farther = coplanar || ((z + combined_dz) >= memory_z);
|
||||
bool overflow = (coverage_count + current_coverage_count) >= 8;
|
||||
|
||||
blend_en = force_blend || (!overflow && aa_enable && farther);
|
||||
coverage_wrap = overflow;
|
||||
|
||||
depth_pass = false;
|
||||
bool max_z = memory_z == 0x3ffff;
|
||||
bool front = z < memory_z;
|
||||
int z_closest_possible = z - combined_dz;
|
||||
bool nearer = coplanar || (z_closest_possible <= memory_z);
|
||||
|
||||
switch (z_mode)
|
||||
{
|
||||
case Z_MODE_OPAQUE:
|
||||
{
|
||||
// The OPAQUE mode is normal less-than.
|
||||
// However, if z is sufficiently close enough to memory Z, we assume that we have the same surface
|
||||
// and we should simply increment coverage (blend_en).
|
||||
// If we overflow coverage, it is clear that we have a different surface, and here we should only
|
||||
// consider pure in-front test and overwrite coverage.
|
||||
depth_pass = max_z || (overflow ? front : nearer);
|
||||
break;
|
||||
}
|
||||
|
||||
case Z_MODE_INTERPENETRATING:
|
||||
{
|
||||
// This one is ... interesting as it affects coverage.
|
||||
if (!front || !farther || !overflow)
|
||||
{
|
||||
// If there is no decal-like intersect, treat this as normal opaque mode.
|
||||
depth_pass = max_z || (overflow ? front : nearer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Modify coverage based on how far away current surface we are somehow?
|
||||
combined_dz_interpenetrate = dz_compress(combined_dz_interpenetrate & 0xffff);
|
||||
int cvg_coeff = ((memory_z >> combined_dz_interpenetrate) - (z >> combined_dz_interpenetrate)) & 0xf;
|
||||
coverage_count = min((cvg_coeff * coverage_count) >> 3, 8);
|
||||
depth_pass = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Z_MODE_TRANSPARENT:
|
||||
{
|
||||
depth_pass = front || max_z;
|
||||
break;
|
||||
}
|
||||
|
||||
case Z_MODE_DECAL:
|
||||
{
|
||||
// Decals pass if |z - memory_z| <= max(dz, memory_dz).
|
||||
depth_pass = farther && nearer && !max_z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
blend_shift.x = u8(0);
|
||||
blend_shift.y = u8(min(0xf - dz_compressed, 4));
|
||||
|
||||
bool overflow = (coverage_count + current_coverage_count) >= 8;
|
||||
blend_en = force_blend || (!overflow && aa_enable);
|
||||
coverage_wrap = overflow;
|
||||
depth_pass = true;
|
||||
}
|
||||
return depth_pass;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef DITHER_H_
|
||||
#define DITHER_H_
|
||||
|
||||
const u8 dither_matrices[2][16] = u8[][](
|
||||
u8[](U8_C(0), U8_C(6), U8_C(1), U8_C(7), U8_C(4), U8_C(2), U8_C(5), U8_C(3), U8_C(3), U8_C(5), U8_C(2), U8_C(4), U8_C(7), U8_C(1), U8_C(6), U8_C(0)),
|
||||
u8[](U8_C(0), U8_C(4), U8_C(1), U8_C(5), U8_C(4), U8_C(0), U8_C(5), U8_C(1), U8_C(3), U8_C(7), U8_C(2), U8_C(6), U8_C(7), U8_C(3), U8_C(6), U8_C(2)));
|
||||
|
||||
u8x3 rgb_dither(ivec3 orig_rgb, int dith)
|
||||
{
|
||||
ivec3 rgb_dith = (ivec3(dith) >> ivec3(0, 3, 6)) & 7;
|
||||
ivec3 rgb = mix((orig_rgb & 0xf8) + 8, ivec3(255), greaterThan(orig_rgb, ivec3(247)));
|
||||
ivec3 replace_sign = (rgb_dith - (orig_rgb & 7)) >> 31;
|
||||
ivec3 dither_diff = rgb - orig_rgb;
|
||||
rgb = orig_rgb + (dither_diff & replace_sign);
|
||||
return u8x3(rgb & 0xff);
|
||||
}
|
||||
|
||||
void dither_coefficients(int x, int y, int dither_mode_rgb, int dither_mode_alpha, out int rgb_dither, out int alpha_dither)
|
||||
{
|
||||
const int DITHER_SPLAT = (1 << 0) | (1 << 3) | (1 << 6);
|
||||
|
||||
if (dither_mode_rgb < 2)
|
||||
rgb_dither = int(dither_matrices[dither_mode_rgb][(y & 3) * 4 + (x & 3)]) * DITHER_SPLAT;
|
||||
else if (dither_mode_rgb == 2)
|
||||
rgb_dither = noise_get_dither_color();
|
||||
else
|
||||
rgb_dither = 0;
|
||||
|
||||
if (dither_mode_alpha == 3)
|
||||
alpha_dither = 0;
|
||||
else
|
||||
{
|
||||
if (dither_mode_alpha == 2)
|
||||
{
|
||||
alpha_dither = noise_get_dither_alpha();
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha_dither = dither_mode_rgb >= 2 ?
|
||||
int(dither_matrices[dither_mode_rgb & 1][(y & 3) * 4 + (x & 3)]) : (rgb_dither & 7);
|
||||
|
||||
if (dither_mode_alpha == 1)
|
||||
alpha_dither = ~alpha_dither & 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#include "small_types.h"
|
||||
layout(local_size_x = 16, local_size_y = 8) in;
|
||||
|
||||
// Copies VRAM into a texture which is then consumed by VI scanout.
|
||||
|
||||
layout(set = 0, binding = 0, rgba8ui) uniform writeonly uimage2D uAAInput;
|
||||
layout(set = 0, binding = 1, std430) readonly buffer RDRAM16
|
||||
{
|
||||
mem_u16 elems[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 1, std430) readonly buffer RDRAM32
|
||||
{
|
||||
uint elems[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer HiddenRDRAM
|
||||
{
|
||||
mem_u8 elems[];
|
||||
} hidden_vram;
|
||||
|
||||
layout(push_constant, std430) uniform Registers
|
||||
{
|
||||
int fb_offset;
|
||||
int fb_width;
|
||||
ivec2 offset;
|
||||
ivec2 resolution;
|
||||
} registers;
|
||||
|
||||
layout(constant_id = 0) const int RDRAM_SIZE = 0;
|
||||
const int RDRAM_MASK_8 = RDRAM_SIZE - 1;
|
||||
const int RDRAM_MASK_16 = RDRAM_MASK_8 >> 1;
|
||||
const int RDRAM_MASK_32 = RDRAM_MASK_16 >> 1;
|
||||
layout(constant_id = 2) const int SCALING_LOG2 = 0;
|
||||
const int SCALING_FACTOR = 1 << SCALING_LOG2;
|
||||
|
||||
#include "vi_status.h"
|
||||
|
||||
uvec4 fetch_color(ivec2 coord)
|
||||
{
|
||||
ivec2 slice2d = coord & (SCALING_FACTOR - 1);
|
||||
coord >>= SCALING_LOG2;
|
||||
int slice = slice2d.y * SCALING_FACTOR + slice2d.x;
|
||||
|
||||
uvec4 color;
|
||||
if (FMT_RGBA8888)
|
||||
{
|
||||
int linear_coord = coord.y * registers.fb_width + coord.x + registers.fb_offset;
|
||||
linear_coord &= RDRAM_MASK_32;
|
||||
linear_coord += slice * (RDRAM_SIZE >> 2);
|
||||
uint word = uint(vram32.elems[linear_coord]);
|
||||
color = (uvec4(word) >> uvec4(24, 16, 8, 5)) & uvec4(0xff, 0xff, 0xff, 7);
|
||||
}
|
||||
else if (FMT_RGBA5551)
|
||||
{
|
||||
int linear_coord = coord.y * registers.fb_width + coord.x + registers.fb_offset;
|
||||
linear_coord &= RDRAM_MASK_16;
|
||||
linear_coord += slice * (RDRAM_SIZE >> 1);
|
||||
uint word = uint(vram16.elems[linear_coord ^ 1]);
|
||||
uint hidden_word = uint(hidden_vram.elems[linear_coord]);
|
||||
|
||||
uint r = (word >> 8u) & 0xf8u;
|
||||
uint g = (word >> 3u) & 0xf8u;
|
||||
uint b = (word << 2u) & 0xf8u;
|
||||
uint a = ((word & 1u) << 2u) | hidden_word;
|
||||
color = uvec4(r, g, b, a);
|
||||
}
|
||||
else
|
||||
color = uvec4(0);
|
||||
|
||||
if (!FETCH_AA)
|
||||
color.a = 7u;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(greaterThanEqual(gl_GlobalInvocationID.xy, registers.resolution)))
|
||||
return;
|
||||
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy) + registers.offset;
|
||||
uvec4 col = fetch_color(coord);
|
||||
imageStore(uAAInput, ivec2(gl_GlobalInvocationID.xy), col);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef FB_FORMATS_H_
|
||||
#define FB_FORMATS_H_
|
||||
|
||||
const int FB_FMT_I4 = 0;
|
||||
const int FB_FMT_I8 = 1;
|
||||
const int FB_FMT_RGBA5551 = 2;
|
||||
const int FB_FMT_IA88 = 3;
|
||||
const int FB_FMT_RGBA8888 = 4;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
#version 450
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
void main()
|
||||
{
|
||||
if (gl_VertexIndex == 0)
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
|
||||
else if (gl_VertexIndex == 1)
|
||||
gl_Position = vec4(-1.0, +3.0, 0.0, 1.0);
|
||||
else
|
||||
gl_Position = vec4(+3.0, -1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef INTERPOLATION_H_
|
||||
#define INTERPOLATION_H_
|
||||
|
||||
#include "data_structures.h"
|
||||
#include "clamping.h"
|
||||
#include "perspective.h"
|
||||
|
||||
u8x4 interpolate_rgba(ivec4 rgba, ivec4 drgba_dx, ivec4 drgba_dy, int dx, int coverage)
|
||||
{
|
||||
rgba += ((drgba_dx & ~0x1f) >> SCALING_LOG2) * dx;
|
||||
|
||||
// RGBA is interpolated to 9-bit. The last bit is used to deal with clamping.
|
||||
// Slight underflow below 0 is clamped to 0 and slight overflow above 0xff is clamped to 0xff.
|
||||
|
||||
// Keep 2 sign bits of precision before we complete the centroid interpolation.
|
||||
i16x4 snapped_rgba = i16x4(rgba >> 14);
|
||||
|
||||
// Centroid clipping is based on the first coverage bit, and we interpolate at the first subpixel in scanline order.
|
||||
// With this layout we can just use findLSB to get correct result.
|
||||
// 0x01 0x02
|
||||
// 0x04 0x08
|
||||
// 0x10 0x20
|
||||
// 0x40 0x80
|
||||
int first_coverage = findLSB(coverage);
|
||||
i16 yoff = i16(first_coverage >> 1);
|
||||
i16 xoff = i16((first_coverage & 1) << 1) + (yoff & I16_C(1));
|
||||
snapped_rgba <<= I16_C(2 + SCALING_LOG2);
|
||||
snapped_rgba += xoff * i16x4(drgba_dx >> 14) + yoff * i16x4(drgba_dy >> 14);
|
||||
snapped_rgba >>= I16_C(4 + SCALING_LOG2);
|
||||
return clamp_9bit(snapped_rgba);
|
||||
}
|
||||
|
||||
void interpolate_st_copy(SpanSetup span, ivec4 dstzw_dx, int x, bool perspective, bool flip,
|
||||
out ivec2 st, out int s_offset)
|
||||
{
|
||||
int dx = flip ? (x - span.start_x) : (span.end_x - x);
|
||||
|
||||
// For copy pipe, we should duplicate pixels when scaling, there is no filtering we can (or should!) do.
|
||||
dx >>= SCALING_LOG2;
|
||||
|
||||
// Snap DX to where we perform interpolation (once per N output pixels).
|
||||
int snapped_dx = dx & global_constants.fb_info.dx_mask;
|
||||
s_offset = dx - snapped_dx;
|
||||
int lerp_dx = (dx >> global_constants.fb_info.dx_shift) * (flip ? 1 : -1);
|
||||
ivec3 stw = span.stzw.xyw + (dstzw_dx.xyw & ~0x1f) * lerp_dx;
|
||||
|
||||
if (perspective)
|
||||
{
|
||||
bool st_overflow;
|
||||
st = perspective_divide(stw >> 16, st_overflow);
|
||||
}
|
||||
else
|
||||
st = no_perspective_divide(stw >> 16);
|
||||
}
|
||||
|
||||
ivec2 interpolate_st_single(ivec4 stzw, ivec4 dstzw_dx, int dx, bool perspective)
|
||||
{
|
||||
ivec3 stw = stzw.xyw + ((dstzw_dx.xyw & ~0x1f) >> SCALING_LOG2) * dx;
|
||||
stw >>= 16;
|
||||
ivec2 st;
|
||||
|
||||
if (perspective)
|
||||
{
|
||||
bool st_overflow;
|
||||
st = perspective_divide(stw, st_overflow);
|
||||
}
|
||||
else
|
||||
st = no_perspective_divide(stw);
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
void interpolate_stz(ivec4 stzw, ivec4 dstzw_dx, ivec4 dstzw_dy, int dx, int coverage, bool perspective, bool uses_lod,
|
||||
int flip_direction, out ivec2 st, out ivec2 st_dx, out ivec2 st_dy, out int z, inout bool st_overflow)
|
||||
{
|
||||
ivec3 stw = stzw.xyw + ((dstzw_dx.xyw & ~0x1f) >> SCALING_LOG2) * dx;
|
||||
ivec3 stw_dx, stw_dy;
|
||||
|
||||
if (uses_lod)
|
||||
{
|
||||
stw_dx = stw + flip_direction * ((dstzw_dx.xyw & ~0x1f) >> SCALING_LOG2);
|
||||
if (SCALING_FACTOR > 1)
|
||||
stw_dy = stw + abs(flip_direction) * ((dstzw_dy.xyw & ~0x7fff) >> SCALING_LOG2);
|
||||
else
|
||||
stw_dy = stw + ((dstzw_dy.xyw & ~0x7fff) >> SCALING_LOG2);
|
||||
}
|
||||
|
||||
if (perspective)
|
||||
{
|
||||
st = perspective_divide(stw >> 16, st_overflow);
|
||||
if (uses_lod)
|
||||
{
|
||||
st_dx = perspective_divide(stw_dx >> 16, st_overflow);
|
||||
st_dy = perspective_divide(stw_dy >> 16, st_overflow);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
st = no_perspective_divide(stw >> 16);
|
||||
if (uses_lod)
|
||||
{
|
||||
st_dx = no_perspective_divide(stw_dx >> 16);
|
||||
st_dy = no_perspective_divide(stw_dy >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that interpolation snaps as we expect on every "main" pixel,
|
||||
// for subpixels, interpolate with quantized step factor.
|
||||
z = stzw.z + dstzw_dx.z * (dx >> SCALING_LOG2) + (dstzw_dx.z >> SCALING_LOG2) * (dx & (SCALING_FACTOR - 1));
|
||||
|
||||
int snapped_z = z >> 10;
|
||||
int first_coverage = findLSB(coverage);
|
||||
int yoff = first_coverage >> 1;
|
||||
int xoff = ((first_coverage & 1) << 1) + (yoff & I16_C(1));
|
||||
snapped_z <<= 2 + SCALING_LOG2;
|
||||
snapped_z += xoff * (dstzw_dx.z >> 10) + yoff * (dstzw_dy.z >> 10);
|
||||
snapped_z >>= 5 + SCALING_LOG2;
|
||||
|
||||
z = clamp_z(snapped_z);
|
||||
}
|
||||
|
||||
#if 0
|
||||
u8x4 interpolate_rgba(TriangleSetup setup, AttributeSetup attr, int x, int y, int coverage)
|
||||
{
|
||||
bool do_offset = (setup.flags & TRIANGLE_SETUP_DO_OFFSET_BIT) != 0;
|
||||
int y_interpolation_base = int(setup.yh) >> 2;
|
||||
int xh = setup.xh + (y - y_interpolation_base) * (setup.dxhdy << 2);
|
||||
|
||||
ivec4 drgba_diff = ivec4(0);
|
||||
|
||||
// In do_offset mode, varyings are latched at last subpixel line instead of first (for some reason).
|
||||
if (do_offset)
|
||||
{
|
||||
xh += 3 * setup.dxhdy;
|
||||
ivec4 drgba_deh = attr.drgba_de & ~0x1ff;
|
||||
ivec4 drgba_dyh = attr.drgba_dy & ~0x1ff;
|
||||
drgba_diff = drgba_deh - (drgba_deh >> 2) - drgba_dyh + (drgba_dyh >> 2);
|
||||
}
|
||||
|
||||
int base_x = xh >> 16;
|
||||
int xfrac = (xh >> 8) & 0xff;
|
||||
|
||||
ivec4 rgba = attr.rgba;
|
||||
rgba += attr.drgba_de * (y - y_interpolation_base);
|
||||
rgba = ((rgba & ~0x1ff) + drgba_diff - xfrac * ((attr.drgba_dx >> 8) & ~1)) & ~0x3ff;
|
||||
rgba += (attr.drgba_dx & ~0x1f) * (x - base_x);
|
||||
|
||||
// RGBA is interpolated to 9-bit. The last bit is used to deal with clamping.
|
||||
// Slight underflow below 0 is clamped to 0 and slight overflow above 0xff is clamped to 0xff.
|
||||
|
||||
// Keep 2 sign bits of precision before we complete the centroid interpolation.
|
||||
i16x4 snapped_rgba = i16x4(rgba >> 14);
|
||||
|
||||
// Centroid clipping is based on the first coverage bit, and we interpolate at the first subpixel in scanline order.
|
||||
// FWIW, Angrylion has a very different coverage bit assignment, but we need this layout to avoid an awkward LUT.
|
||||
// With this layout we can just use findLSB instead.
|
||||
// 0x01 0x02
|
||||
// 0x04 0x08
|
||||
// 0x10 0x20
|
||||
// 0x40 0x80
|
||||
int first_coverage = findLSB(coverage);
|
||||
i16 yoff = i16(first_coverage >> 1);
|
||||
i16 xoff = i16((first_coverage & 1) << 1) + (yoff & I16_C(1));
|
||||
snapped_rgba <<= I16_C(2);
|
||||
snapped_rgba += xoff * i16x4(attr.drgba_dx >> 14) + yoff * i16x4(attr.drgba_dy >> 14);
|
||||
snapped_rgba >>= I16_C(4);
|
||||
return clamp_9bit(snapped_rgba);
|
||||
}
|
||||
|
||||
ivec3 interpolate_stw(TriangleSetup setup, AttributeSetup attr, int x, int y)
|
||||
{
|
||||
bool do_offset = (setup.flags & TRIANGLE_SETUP_DO_OFFSET_BIT) != 0;
|
||||
int y_interpolation_base = int(setup.yh) >> 2;
|
||||
int xh = setup.xh + (y - y_interpolation_base) * (setup.dxhdy << 2);
|
||||
|
||||
ivec3 dstw_diff = ivec3(0);
|
||||
|
||||
// In do_offset mode, varyings are latched at last subpixel line instead of first (for some reason).
|
||||
if (do_offset)
|
||||
{
|
||||
xh += 3 * setup.dxhdy;
|
||||
ivec3 dstw_deh = attr.dstzw_de.xyw & ~0x1ff;
|
||||
ivec3 dstw_dyh = attr.dstzw_dy.xyw & ~0x1ff;
|
||||
dstw_diff = dstw_deh - (dstw_deh >> 2) - dstw_dyh + (dstw_dyh >> 2);
|
||||
}
|
||||
|
||||
int base_x = xh >> 16;
|
||||
int xfrac = (xh >> 8) & 0xff;
|
||||
|
||||
ivec3 stw = attr.stzw.xyw;
|
||||
stw += attr.dstzw_de.xyw * (y - y_interpolation_base);
|
||||
stw = ((stw & ~0x1ff) + dstw_diff - xfrac * ((attr.dstzw_dx.xyw >> 8) & ~1)) & ~0x3ff;
|
||||
stw += (attr.dstzw_dx.xyw & ~0x1f) * (x - base_x);
|
||||
|
||||
ivec3 snapped_stw = stw >> 16;
|
||||
return snapped_stw;
|
||||
}
|
||||
|
||||
int interpolate_z(TriangleSetup setup, AttributeSetup attr, int x, int y, int coverage)
|
||||
{
|
||||
bool do_offset = (setup.flags & TRIANGLE_SETUP_DO_OFFSET_BIT) != 0;
|
||||
int y_interpolation_base = int(setup.yh) >> 2;
|
||||
int xh = setup.xh + (y - y_interpolation_base) * (setup.dxhdy << 2);
|
||||
|
||||
int dzdiff = 0;
|
||||
// In do_offset mode, varyings are latched at last subpixel line instead of first (for some reason).
|
||||
if (do_offset)
|
||||
{
|
||||
xh += 3 * setup.dxhdy;
|
||||
int dzdeh = attr.dstzw_de.z & ~0x1ff;
|
||||
int dzdyh = attr.dstzw_dy.z & ~0x1ff;
|
||||
dzdiff = dzdeh - (dzdeh >> 2) - dzdyh + (dzdyh >> 2);
|
||||
}
|
||||
|
||||
int base_x = xh >> 16;
|
||||
int xfrac = (xh >> 8) & 0xff;
|
||||
int z = attr.stzw.z;
|
||||
z += attr.dstzw_de.z * (y - y_interpolation_base);
|
||||
z = ((z & ~0x1ff) + dzdiff - xfrac * ((attr.dstzw_dx.z >> 8) & ~1)) & ~0x3ff;
|
||||
z += attr.dstzw_dx.z * (x - base_x);
|
||||
|
||||
int snapped_z = z >> 10;
|
||||
int first_coverage = findLSB(coverage);
|
||||
int yoff = first_coverage >> 1;
|
||||
int xoff = ((first_coverage & 1) << 1) + (yoff & 1s);
|
||||
snapped_z <<= 2;
|
||||
snapped_z += xoff * (attr.dstzw_dx.z >> 10) + yoff * (attr.dstzw_dy.z >> 10);
|
||||
snapped_z >>= 5;
|
||||
return clamp_z(snapped_z);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_ATTRIBUTE_SETUP_H_
|
||||
#define LOAD_ATTRIBUTE_SETUP_H_
|
||||
|
||||
AttributeSetup load_attribute_setup(uint index)
|
||||
{
|
||||
return attribute_setup.elems[index];
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_DEPTH_BLEND_STATE_H_
|
||||
#define LOAD_DEPTH_BLEND_STATE_H_
|
||||
|
||||
DepthBlendState load_depth_blend_state(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return depth_blend_state.elems[index];
|
||||
#else
|
||||
return DepthBlendState(
|
||||
u8x4(depth_blend_state.elems[index].blend_modes0),
|
||||
u8x4(depth_blend_state.elems[index].blend_modes1),
|
||||
depth_blend_state.elems[index].flags,
|
||||
u8(depth_blend_state.elems[index].coverage_mode),
|
||||
u8(depth_blend_state.elems[index].z_mode),
|
||||
u8(0), u8(0));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_DERIVED_SETUP_H_
|
||||
#define LOAD_DERIVED_SETUP_H_
|
||||
|
||||
DerivedSetup load_derived_setup(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return derived_setup.elems[index];
|
||||
#else
|
||||
return DerivedSetup(
|
||||
u8x4(derived_setup.elems[index].constant_muladd0),
|
||||
u8x4(derived_setup.elems[index].constant_mulsub0),
|
||||
u8x4(derived_setup.elems[index].constant_mul0),
|
||||
u8x4(derived_setup.elems[index].constant_add0),
|
||||
u8x4(derived_setup.elems[index].constant_muladd1),
|
||||
u8x4(derived_setup.elems[index].constant_mulsub1),
|
||||
u8x4(derived_setup.elems[index].constant_mul1),
|
||||
u8x4(derived_setup.elems[index].constant_add1),
|
||||
u8x4(derived_setup.elems[index].fog_color),
|
||||
u8x4(derived_setup.elems[index].blend_color),
|
||||
uint(derived_setup.elems[index].fill_color),
|
||||
u16(derived_setup.elems[index].dz),
|
||||
u8(derived_setup.elems[index].dz_compressed),
|
||||
u8(derived_setup.elems[index].min_lod),
|
||||
i16x4(derived_setup.elems[index].factors));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_SCISSOR_STATE_H_
|
||||
#define LOAD_SCISSOR_STATE_H_
|
||||
|
||||
ScissorState load_scissor_state(uint index)
|
||||
{
|
||||
ivec4 values = scissor_state.elems[index];
|
||||
return ScissorState(values.x, values.y, values.z, values.w);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_SPAN_OFFSETS_H_
|
||||
#define LOAD_SPAN_OFFSETS_H_
|
||||
|
||||
SpanInfoOffsets load_span_offsets(uint index)
|
||||
{
|
||||
return span_offsets.elems[index];
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_SPAN_SETUP_H_
|
||||
#define LOAD_SPAN_SETUP_H_
|
||||
|
||||
SpanSetup load_span_setup(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return span_setups.elems[index];
|
||||
#else
|
||||
return SpanSetup(
|
||||
span_setups.elems[index].rgba,
|
||||
span_setups.elems[index].stzw,
|
||||
u16x4(uvec4(span_setups.elems[index].xleft)),
|
||||
u16x4(uvec4(span_setups.elems[index].xright)),
|
||||
span_setups.elems[index].interpolation_base_x,
|
||||
span_setups.elems[index].start_x,
|
||||
span_setups.elems[index].end_x,
|
||||
i16(span_setups.elems[index].lodlength),
|
||||
u16(span_setups.elems[index].valid_line));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_STATIC_RASTER_STATE_H_
|
||||
#define LOAD_STATIC_RASTER_STATE_H_
|
||||
|
||||
StaticRasterizationState load_static_rasterization_state(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return static_raster_state.elems[index];
|
||||
#else
|
||||
return StaticRasterizationState(
|
||||
u8x4(static_raster_state.elems[index].combiner_inputs_rgb0),
|
||||
u8x4(static_raster_state.elems[index].combiner_inputs_alpha0),
|
||||
u8x4(static_raster_state.elems[index].combiner_inputs_rgb1),
|
||||
u8x4(static_raster_state.elems[index].combiner_inputs_alpha1),
|
||||
static_raster_state.elems[index].flags,
|
||||
static_raster_state.elems[index].dither,
|
||||
0, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_TILE_INFO_H_
|
||||
#define LOAD_TILE_INFO_H_
|
||||
|
||||
TileInfo load_tile_info(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return tile_infos.elems[index];
|
||||
#else
|
||||
return TileInfo(
|
||||
tile_infos.elems[index].slo,
|
||||
tile_infos.elems[index].shi,
|
||||
tile_infos.elems[index].tlo,
|
||||
tile_infos.elems[index].thi,
|
||||
tile_infos.elems[index].offset,
|
||||
tile_infos.elems[index].stride,
|
||||
u8(tile_infos.elems[index].fmt),
|
||||
u8(tile_infos.elems[index].size),
|
||||
u8(tile_infos.elems[index].palette),
|
||||
u8(tile_infos.elems[index].mask_s),
|
||||
u8(tile_infos.elems[index].shift_s),
|
||||
u8(tile_infos.elems[index].mask_t),
|
||||
u8(tile_infos.elems[index].shift_t),
|
||||
u8(tile_infos.elems[index].flags));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Copyright (c) 2020 Themaister
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef LOAD_TRIANGLE_SETUP_H_
|
||||
#define LOAD_TRIANGLE_SETUP_H_
|
||||
|
||||
TriangleSetup load_triangle_setup(uint index)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
return triangle_setup.elems[index];
|
||||
#else
|
||||
return TriangleSetup(
|
||||
triangle_setup.elems[index].xh,
|
||||
triangle_setup.elems[index].xm,
|
||||
triangle_setup.elems[index].xl,
|
||||
i16(triangle_setup.elems[index].yh),
|
||||
i16(triangle_setup.elems[index].ym),
|
||||
triangle_setup.elems[index].dxhdy,
|
||||
triangle_setup.elems[index].dxmdy,
|
||||
triangle_setup.elems[index].dxldy,
|
||||
i16(triangle_setup.elems[index].yl),
|
||||
u8(triangle_setup.elems[index].flags),
|
||||
u8(triangle_setup.elems[index].tile));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
Loaded 100 of 282 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user