mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-27 13:17:07 +01:00
Compare commits
21
Commits
00.006.002
...
00.007.002
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -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,18 +59,22 @@ 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.
|
||||
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.
|
||||
|
||||
@@ -9,7 +9,10 @@ Mupen64Plus PS5 as a whole (the `eboot.bin` it builds) is distributed under the
|
||||
| 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"
|
||||
|
||||
+142
-38
@@ -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
|
||||
@@ -22,8 +22,8 @@
|
||||
# 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;
|
||||
# - 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 +32,39 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
PS5_HOST ?= ps5
|
||||
PS5_PORT ?= 9021
|
||||
VERSION ?= 0.6.2
|
||||
VERSION ?= 0.7.2
|
||||
# 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
|
||||
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 +95,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 +109,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 +117,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
|
||||
# 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,16 +131,23 @@ 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)
|
||||
@@ -141,18 +171,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))
|
||||
@@ -183,7 +219,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 $@ $<
|
||||
|
||||
@@ -199,6 +235,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 +251,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 $@ $<
|
||||
@@ -250,29 +301,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,28 +382,72 @@ $(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)
|
||||
$(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 $@
|
||||
@@ -384,7 +483,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)
|
||||
@@ -414,7 +514,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 $@ $<
|
||||
|
||||
@@ -430,6 +530,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 $@ $<
|
||||
|
||||
+66
-28
@@ -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,20 @@ 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.
|
||||
|
||||
**What the core needs from SDL, libpng and dlopen is replaced:**
|
||||
|
||||
@@ -87,7 +99,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,7 +126,7 @@ 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
|
||||
@@ -148,18 +160,27 @@ 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 |
|
||||
| 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 |
|
||||
| Game list view | Shelf (3D, default), Grid or List; the touchpad on the game list switches too |
|
||||
| Show FPS | the speed (`VI/s`, 100% = full speed) and, below it, where the emulation thread's time goes: `cpu` (VR4300 interpreter), `rsp` (rsp-hle + cxd4), `rdp` (the renderer: angrylion's rasteriser incl. waiting for its threads, or handing commands to the GPU), `vi` (the VI filter, or reading the GPU's picture back), `idle` (waiting for the TV: spare time). Also logged every 10 s in `boot.log` |
|
||||
| Sound, Download covers | |
|
||||
| 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
|
||||
- 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
|
||||
- Touchpad: the next view (remembered; also Settings → Game list view)
|
||||
- Triangle: settings
|
||||
- Square: fetch the cover again
|
||||
- OPTIONS: quit
|
||||
@@ -170,12 +191,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 +211,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,8 +227,8 @@ 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.
|
||||
- **The RSP is the next bottleneck.** cxd4 interprets every graphics microcode instruction (GoldenEye:
|
||||
~30% of the time); audio goes through rsp-hle (Audio processing: Fast).
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, a cheats menu, netplay.
|
||||
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
|
||||
|
||||
@@ -208,7 +237,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 +250,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 +267,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)
|
||||
@@ -263,7 +298,8 @@ generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`
|
||||
## 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 +313,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 +325,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.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99064",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
|
||||
"contentVersion": "00.006.002",
|
||||
"contentVersion": "00.007.002",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
@@ -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.
|
||||
@@ -35,8 +35,8 @@ 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"
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -117,9 +117,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;
|
||||
}
|
||||
+39
-3
@@ -206,6 +206,7 @@ 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;
|
||||
@@ -557,6 +558,23 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
|
||||
if (frame && !g.stopping && (!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)
|
||||
{
|
||||
static const uint32_t kBlack[320 * 240] = {};
|
||||
g_presenter.Submit(kBlack, 320, 240, 320, !ff && !g.pal);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================================================
|
||||
@@ -693,17 +711,34 @@ ExitReason RunGame(const std::string& path, std::string* error)
|
||||
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");
|
||||
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]);
|
||||
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 +754,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);
|
||||
@@ -771,7 +807,7 @@ void ApplySettings()
|
||||
const fe::Settings& cfg = fe::Config();
|
||||
n64ps5_audio_set_mute(!cfg.audio);
|
||||
n64ps5_audio_set_fast_forward(g.fast_forward.load());
|
||||
const n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak};
|
||||
const n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick};
|
||||
n64ps5_input_set_options(&in);
|
||||
ps5video::InvalidateN64();
|
||||
}
|
||||
|
||||
@@ -209,7 +209,11 @@ void DrawOptionBox(const char* title, const std::vector<Row>& rows, int sel, boo
|
||||
const int scale = 3;
|
||||
const int row_h = 44;
|
||||
const int bw = 1000;
|
||||
const int bh = 120 + int(rows.size()) * row_h + 30;
|
||||
// more rows than fit on the screen (the pause menu): a window that follows the selection, with a scrollbar
|
||||
const int n = int(rows.size());
|
||||
const int visible = std::min(n, (H - 40 - 150) / row_h);
|
||||
const int top = std::max(0, std::min(sel - visible / 2, n - visible));
|
||||
const int bh = 120 + visible * row_h + 30;
|
||||
const int bx = (W - bw) / 2;
|
||||
const int by = std::max(20, (H - bh) / 2);
|
||||
if (!over_game)
|
||||
@@ -217,19 +221,27 @@ void DrawOptionBox(const char* title, const std::vector<Row>& rows, int sel, boo
|
||||
ps5video::FillRect(bx, by, bw, bh, kPanel);
|
||||
ps5video::FillRect(bx, by + 86, bw, 3, kAccent);
|
||||
DrawText(bx + 40, by + 24, title, 5, kAccent);
|
||||
for (size_t i = 0; i < rows.size(); i++)
|
||||
for (int i = top; i < top + visible; i++)
|
||||
{
|
||||
const int y = by + 110 + int(i) * row_h;
|
||||
if (int(i) == sel)
|
||||
const int y = by + 110 + (i - top) * row_h;
|
||||
if (i == sel)
|
||||
ps5video::FillRect(bx + 16, y - 6, bw - 32, row_h, kSel);
|
||||
const uint32_t col = rows[i].enabled ? kText : kDim;
|
||||
DrawText(bx + 40, y, rows[i].label.c_str(), scale, col);
|
||||
if (!rows[i].value.empty())
|
||||
{
|
||||
const std::string v = (int(i) == sel ? "< " + rows[i].value + " >" : rows[i].value);
|
||||
DrawText(bx + bw - 40 - TextWidth(v.c_str(), scale), y, v.c_str(), scale, int(i) == sel ? kText : kAccent);
|
||||
const std::string v = (i == sel ? "< " + rows[i].value + " >" : rows[i].value);
|
||||
DrawText(bx + bw - 40 - TextWidth(v.c_str(), scale), y, v.c_str(), scale, i == sel ? kText : kAccent);
|
||||
}
|
||||
}
|
||||
if (visible < n)
|
||||
{
|
||||
const int track = visible * row_h;
|
||||
const int thumb = std::max(30, track * visible / n);
|
||||
const int ty = by + 104 + (track - thumb) * top / (n - visible);
|
||||
ps5video::FillRect(bx + bw - 12, by + 104, 6, track, kSel);
|
||||
ps5video::FillRect(bx + bw - 12, ty, 6, thumb, kAccent);
|
||||
}
|
||||
}
|
||||
|
||||
const char* YesNo(bool b)
|
||||
@@ -247,10 +259,19 @@ enum SettingRow
|
||||
S_OVERSCAN,
|
||||
S_FPS,
|
||||
S_AUDIO,
|
||||
#ifdef N64PS5_VULKAN
|
||||
S_RENDERER,
|
||||
S_UPSCALE,
|
||||
S_GPU_SYNC,
|
||||
#endif
|
||||
S_CPU,
|
||||
S_RSP_AUDIO,
|
||||
S_THREADS,
|
||||
S_PAK,
|
||||
S_DEADZONE,
|
||||
S_INVERT,
|
||||
S_INVERT_C,
|
||||
S_VIEW,
|
||||
S_COVERS,
|
||||
S_UPDATES,
|
||||
S_COUNT
|
||||
@@ -267,10 +288,19 @@ std::string SettingLabel(int s)
|
||||
case S_OVERSCAN: return "Hide overscan *";
|
||||
case S_FPS: return "Show FPS";
|
||||
case S_AUDIO: return "Sound";
|
||||
#ifdef N64PS5_VULKAN
|
||||
case S_RENDERER: return "Renderer *";
|
||||
case S_UPSCALE: return "Internal resolution *";
|
||||
case S_GPU_SYNC: return "GPU sync *";
|
||||
#endif
|
||||
case S_CPU: return "CPU core *";
|
||||
case S_RSP_AUDIO: return "Audio processing *";
|
||||
case S_THREADS: return "Render threads *";
|
||||
case S_PAK: return "Controller pak *";
|
||||
case S_DEADZONE: return "Stick dead zone";
|
||||
case S_INVERT: return "Invert left stick";
|
||||
case S_INVERT_C: return "Invert right stick";
|
||||
case S_VIEW: return "Game list view";
|
||||
case S_COVERS: return "Download covers";
|
||||
case S_UPDATES: return "Check for updates";
|
||||
default: return "";
|
||||
@@ -299,10 +329,31 @@ std::string SettingValue(int s)
|
||||
case S_OVERSCAN: return YesNo(c.hide_overscan);
|
||||
case S_FPS: return YesNo(c.show_fps);
|
||||
case S_AUDIO: return YesNo(c.audio);
|
||||
#ifdef N64PS5_VULKAN
|
||||
case S_RENDERER: return c.gpu ? "GPU (paraLLEl-RDP)" : "CPU (angrylion)";
|
||||
case S_UPSCALE:
|
||||
if (!c.gpu)
|
||||
return "GPU only";
|
||||
snprintf(buf, sizeof(buf), c.upscale == 1 ? "1x (native)" : "%dx", c.upscale);
|
||||
return buf;
|
||||
case S_GPU_SYNC: return !c.gpu ? "GPU only" : c.gpu_sync ? "Accurate" : "Fast";
|
||||
#endif
|
||||
case S_CPU: return c.dynarec ? "Dynarec" : "Interpreter";
|
||||
case S_RSP_AUDIO: return c.hle_audio ? "Fast (HLE)" : "Accurate (LLE)";
|
||||
case S_THREADS: snprintf(buf, sizeof(buf), "%d", c.render_threads); return buf;
|
||||
case S_PAK: return PakName(c.pak);
|
||||
case S_DEADZONE: snprintf(buf, sizeof(buf), "%d%%", c.deadzone); return buf;
|
||||
case S_INVERT:
|
||||
case S_INVERT_C:
|
||||
{
|
||||
static const char* const kInvert[] = {"Off", "X (left/right)", "Y (up/down)", "X and Y"};
|
||||
return kInvert[(s == S_INVERT ? c.invert_stick : c.invert_c_stick) & 3];
|
||||
}
|
||||
case S_VIEW:
|
||||
{
|
||||
static const char* const kViews[] = {"Shelf (3D)", "Grid", "List"};
|
||||
return kViews[std::clamp(c.game_view, 0, 2)];
|
||||
}
|
||||
case S_COVERS: return YesNo(c.covers_download);
|
||||
case S_UPDATES: return YesNo(c.updates);
|
||||
default: return "";
|
||||
@@ -330,7 +381,22 @@ void ChangeSetting(int s, int dir)
|
||||
case S_OVERSCAN: c.hide_overscan = !c.hide_overscan; break;
|
||||
case S_FPS: c.show_fps = !c.show_fps; break;
|
||||
case S_AUDIO: c.audio = !c.audio; break;
|
||||
#ifdef N64PS5_VULKAN
|
||||
case S_RENDERER: c.gpu = !c.gpu; break;
|
||||
case S_UPSCALE:
|
||||
{
|
||||
static const int steps[] = {1, 2, 4, 8};
|
||||
if (c.gpu)
|
||||
c.upscale = Step(c.upscale, dir, steps, 4);
|
||||
break;
|
||||
}
|
||||
case S_GPU_SYNC:
|
||||
if (c.gpu)
|
||||
c.gpu_sync = !c.gpu_sync;
|
||||
break;
|
||||
#endif
|
||||
case S_CPU: c.dynarec = !c.dynarec; break;
|
||||
case S_RSP_AUDIO: c.hle_audio = !c.hle_audio; break;
|
||||
case S_THREADS:
|
||||
{
|
||||
static const int steps[] = {1, 2, 4, 6, 8, 10, 12};
|
||||
@@ -344,6 +410,9 @@ void ChangeSetting(int s, int dir)
|
||||
c.deadzone = Step(c.deadzone, dir, steps, 8);
|
||||
break;
|
||||
}
|
||||
case S_INVERT: c.invert_stick = (c.invert_stick + dir + 4) & 3; break;
|
||||
case S_INVERT_C: c.invert_c_stick = (c.invert_c_stick + dir + 4) & 3; break;
|
||||
case S_VIEW: c.game_view = (std::clamp(c.game_view, 0, 2) + dir + 3) % 3; break;
|
||||
case S_COVERS: c.covers_download = !c.covers_download; break;
|
||||
case S_UPDATES: c.updates = !c.updates; break;
|
||||
}
|
||||
@@ -762,16 +831,25 @@ void ShowBusy(const std::string& text)
|
||||
|
||||
void MessageBox(const std::string& title, const std::string& text)
|
||||
{
|
||||
// laid out as Confirm: the text wrapped over as many lines as it needs
|
||||
const int bw = 1400, max_lines = 14, line_h = 38;
|
||||
std::vector<std::string> lines = WrapText(text, 3, bw - 80);
|
||||
if (int(lines.size()) > max_lines)
|
||||
{
|
||||
lines.resize(max_lines);
|
||||
lines.back() = "...";
|
||||
}
|
||||
const int bh = 130 + int(lines.size()) * line_h + 100;
|
||||
NavReader nav;
|
||||
for (;;)
|
||||
{
|
||||
Header("");
|
||||
const int bw = 1400, bh = 300;
|
||||
const int bx = (W - bw) / 2, by = (H - bh) / 2;
|
||||
const int bx = (W - bw) / 2, by = std::max(170, (H - bh) / 2);
|
||||
ps5video::FillRect(bx - 4, by - 4, bw + 8, bh + 8, kAccent);
|
||||
ps5video::FillRect(bx, by, bw, bh, kPanel);
|
||||
DrawText(bx + 40, by + 30, FitText(title, 5, bw - 80).c_str(), 5, kAccent);
|
||||
DrawText(bx + 40, by + 130, FitText(text, 3, bw - 80).c_str(), 3, kText);
|
||||
for (size_t i = 0; i < lines.size(); i++)
|
||||
DrawText(bx + 40, by + 120 + int(i) * line_h, lines[i].c_str(), 3, kText);
|
||||
DrawText(bx + 40, by + bh - 70, (std::string(icon::Cross) + " OK").c_str(), 3, kDim);
|
||||
Present();
|
||||
const Nav n = nav.Read();
|
||||
|
||||
@@ -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
|
||||
@@ -59,6 +59,17 @@ 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 == "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")
|
||||
@@ -67,6 +78,12 @@ void Settings::Load()
|
||||
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 == "game_view")
|
||||
game_view = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "state_slot")
|
||||
state_slot = Clamp(atoi(val), 0, 9);
|
||||
else if (key == "covers_download")
|
||||
@@ -99,10 +116,17 @@ 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, "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, "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, "game_view=%d\n", game_view);
|
||||
fprintf(f, "state_slot=%d\n", state_slot);
|
||||
fprintf(f, "covers_download=%d\n", covers_download ? 1 : 0);
|
||||
fprintf(f, "updates=%d\n", updates ? 1 : 0);
|
||||
|
||||
@@ -16,14 +16,21 @@ 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
|
||||
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;
|
||||
// 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
|
||||
// misc
|
||||
int state_slot = 0; // 0..9
|
||||
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
|
||||
|
||||
+398
-55
@@ -44,8 +44,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 +174,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 +251,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 +274,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 +425,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 +457,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
|
||||
{
|
||||
@@ -570,6 +658,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()
|
||||
@@ -611,18 +740,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 +768,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,20 +776,56 @@ 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;
|
||||
}
|
||||
@@ -665,6 +835,12 @@ std::string Shelf()
|
||||
cfg.Save();
|
||||
return st.games[size_t(sel)].path;
|
||||
}
|
||||
if (down & SCE_PAD_BUTTON_TOUCH_PAD)
|
||||
{
|
||||
cfg.game_view = (view + 1) % kViewCount;
|
||||
cfg.Save();
|
||||
dirty = true;
|
||||
}
|
||||
if (down & SCE_PAD_BUTTON_TRIANGLE)
|
||||
{
|
||||
SettingsMenu();
|
||||
@@ -699,7 +875,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 +936,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 +950,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 +975,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 +1058,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 +1108,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 +1126,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 +1181,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
|
||||
|
||||
@@ -45,6 +45,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
|
||||
|
||||
+48
-36
@@ -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
|
||||
@@ -43,7 +48,7 @@ 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";
|
||||
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
|
||||
@@ -564,32 +569,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 +595,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
|
||||
|
||||
@@ -760,14 +755,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 +774,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 +799,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,15 +29,27 @@ 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);
|
||||
// Fast forward: the audio plugin drops what doesn't fit instead of slowing the game down.
|
||||
@@ -51,6 +63,8 @@ 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 C buttons' right stick turned round: bit 0 X (C-left/right), bit 1 Y (C-up/down)
|
||||
} n64ps5_input_options;
|
||||
|
||||
void n64ps5_input_set_options(const n64ps5_input_options* opt);
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
// 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 <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)");
|
||||
}
|
||||
|
||||
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);
|
||||
g_gpu.context = std::move(context);
|
||||
g_gpu.device = std::move(device);
|
||||
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
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ 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};
|
||||
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.
|
||||
@@ -133,14 +133,19 @@ void GetKeys(int control, BUTTONS* keys)
|
||||
keys->L_TRIG = (b & SCE_PAD_BUTTON_L1) != 0;
|
||||
keys->R_TRIG = (b & SCE_PAD_BUTTON_R1) != 0;
|
||||
|
||||
// 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;
|
||||
// C buttons: the right stick (half tilt), plus Triangle / Circle. Settings "Invert left stick" and
|
||||
// "Invert right stick" (GitHub issue #3) turn the sticks' axes round; Triangle / Circle stay as they are.
|
||||
const bool c_right = p.rx > 192, c_left = p.rx < 64, c_down = p.ry > 192, c_up = p.ry < 64;
|
||||
const bool c_flip_x = (g_opt.invert_c & 1) != 0, c_flip_y = (g_opt.invert_c & 2) != 0;
|
||||
keys->R_CBUTTON = c_flip_x ? c_left : c_right;
|
||||
keys->L_CBUTTON = c_flip_x ? c_right : c_left;
|
||||
keys->D_CBUTTON = (c_flip_y ? c_up : c_down) || (b & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
keys->U_CBUTTON = (c_flip_y ? c_down : c_up) || (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,70 @@
|
||||
#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) buffer RDRAM
|
||||
{
|
||||
uint rdram[];
|
||||
};
|
||||
|
||||
layout(set = 0, binding = 1, std430) readonly buffer StagingRDRAM
|
||||
{
|
||||
uint staging_rdram[];
|
||||
};
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer WriteMaskRDRAM
|
||||
{
|
||||
uint writemask[];
|
||||
};
|
||||
|
||||
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;
|
||||
offset += gl_LocalInvocationIndex;
|
||||
uint mask = writemask[offset];
|
||||
|
||||
if (mask == ~0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (mask == 0u)
|
||||
{
|
||||
uint staging = staging_rdram[offset];
|
||||
rdram[offset] = staging;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint word = rdram[offset];
|
||||
uint staging = staging_rdram[offset];
|
||||
word = (word & mask) | (staging & ~mask);
|
||||
rdram[offset] = word;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
/* 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 MEMORY_INTERFACING_H_
|
||||
#define MEMORY_INTERFACING_H_
|
||||
|
||||
#include "dither.h"
|
||||
#include "z_encode.h"
|
||||
#include "blender.h"
|
||||
#include "depth_test.h"
|
||||
#include "coverage.h"
|
||||
#include "fb_formats.h"
|
||||
|
||||
layout(constant_id = 0) const uint RDRAM_SIZE = 0;
|
||||
|
||||
layout(constant_id = 7) const int RDRAM_INCOHERENT_SCALING = 0;
|
||||
const bool RDRAM_INCOHERENT = (RDRAM_INCOHERENT_SCALING & 1) != 0;
|
||||
const int SCALING_LOG2 = RDRAM_INCOHERENT_SCALING >> 1;
|
||||
const int SCALING_FACTOR = 1 << SCALING_LOG2;
|
||||
const bool RDRAM_UNSCALED_WRITE_MASK = RDRAM_INCOHERENT && SCALING_LOG2 == 0;
|
||||
const bool RDRAM_SCALED_WRITE_MASK = RDRAM_INCOHERENT && SCALING_LOG2 != 0;
|
||||
|
||||
const uint RDRAM_MASK_8 = RDRAM_SIZE - 1u;
|
||||
const uint RDRAM_MASK_16 = RDRAM_MASK_8 >> 1u;
|
||||
const uint RDRAM_MASK_32 = RDRAM_MASK_8 >> 2u;
|
||||
|
||||
layout(constant_id = 1) const int FB_FMT = 0;
|
||||
layout(constant_id = 2) const bool FB_COLOR_DEPTH_ALIAS = false;
|
||||
|
||||
u8x4 current_color;
|
||||
bool current_color_dirty;
|
||||
|
||||
u16 current_depth;
|
||||
u8 current_dz;
|
||||
bool current_depth_dirty;
|
||||
|
||||
void load_vram_color(uint index, uint slice)
|
||||
{
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_I4:
|
||||
case FB_FMT_I8:
|
||||
{
|
||||
index &= RDRAM_MASK_8;
|
||||
index += slice * RDRAM_SIZE;
|
||||
u8 word = u8(vram8.data[index ^ 3u]);
|
||||
current_color = u8x4(word, word, word, u8(hidden_vram.data[index >> 1]));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
uint word = uint(vram16.data[index ^ 1u]);
|
||||
uvec3 rgb = uvec3(word >> 8u, word >> 3u, word << 2u) & 0xf8u;
|
||||
current_color = u8x4(rgb, (u8(hidden_vram.data[index]) << U8_C(5)) | u8((word & 1) << 7));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_IA88:
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
uint word = uint(vram16.data[index ^ 1u]);
|
||||
current_color = u8x4(u8x3(word >> 8u), word & 0xff);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA8888:
|
||||
{
|
||||
index &= RDRAM_MASK_32;
|
||||
index += slice * (RDRAM_SIZE >> 2);
|
||||
uint word = vram32.data[index];
|
||||
current_color = u8x4((uvec4(word) >> uvec4(24, 16, 8, 0)) & uvec4(0xff));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alias_color_to_depth()
|
||||
{
|
||||
/* Inherit memory depth from color. */
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
current_dz = (current_color.a >> U8_C(3)) | (current_color.b & U8_C(8));
|
||||
uint word = (current_color.r & 0xf8u) << 6u;
|
||||
word |= (current_color.g & 0xf8u) << 1u;
|
||||
word |= (current_color.b & 0xf8u) >> 4u;
|
||||
current_depth = u16(word);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_IA88:
|
||||
{
|
||||
uvec2 col = current_color.ra;
|
||||
uint word = (col.x << 8u) | col.y;
|
||||
uint hidden_word = (word & 1u) * 3u;
|
||||
current_depth = u16(word >> 2u);
|
||||
current_dz = u8(((word & 3u) << 2u) | hidden_word);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void alias_depth_to_color()
|
||||
{
|
||||
uint word = (uint(current_depth) << 4u) | current_dz;
|
||||
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
current_color.r = u8((word >> 10u) & 0xf8u);
|
||||
current_color.g = u8((word >> 5u) & 0xf8u);
|
||||
current_color.b = u8((word >> 0u) & 0xf8u);
|
||||
current_color.a = u8((word & 7u) << 5u);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_IA88:
|
||||
{
|
||||
current_color.r = u8((word >> 10u) & 0xffu);
|
||||
current_color.a = u8((word >> 2u) & 0xffu);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current_color_dirty = true;
|
||||
}
|
||||
|
||||
void load_vram_depth(uint index, uint slice)
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
u16 word = u16(vram16.data[index ^ 1u]);
|
||||
current_depth = word >> U16_C(2);
|
||||
current_dz = u8(hidden_vram.data[index]) | u8((word & U16_C(3)) << U16_C(2));
|
||||
}
|
||||
|
||||
void store_unscaled_write_mask(uint index)
|
||||
{
|
||||
if (current_color_dirty)
|
||||
{
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_I4:
|
||||
case FB_FMT_I8:
|
||||
vram8.data[(index ^ 3u) + RDRAM_SIZE] = mem_u8(0xff);
|
||||
break;
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
case FB_FMT_IA88:
|
||||
vram16.data[(index ^ 1u) + (RDRAM_SIZE >> 1u)] = mem_u16(0xffff);
|
||||
break;
|
||||
|
||||
case FB_FMT_RGBA8888:
|
||||
vram32.data[index + (RDRAM_SIZE >> 2u)] = ~0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void store_vram_color(uint index, uint slice)
|
||||
{
|
||||
if (current_color_dirty)
|
||||
{
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_I4:
|
||||
{
|
||||
index &= RDRAM_MASK_8;
|
||||
index += slice * RDRAM_SIZE;
|
||||
vram8.data[index ^ 3u] = mem_u8(0);
|
||||
if ((index & 1u) != 0u)
|
||||
hidden_vram.data[index >> 1u] = mem_u8(current_color.a);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_I8:
|
||||
{
|
||||
index &= RDRAM_MASK_8;
|
||||
index += slice * RDRAM_SIZE;
|
||||
uint col = (index & 1) != 0 ? current_color.g : current_color.r;
|
||||
vram8.data[index ^ 3u] = mem_u8(col);
|
||||
if ((index & 1u) != 0u)
|
||||
hidden_vram.data[index >> 1u] = mem_u8((col & 1) * 3);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
uvec4 c = uvec4(current_color);
|
||||
c.rgb &= 0xf8u;
|
||||
uint cov = c.w >> 5u;
|
||||
uint word = (c.x << 8u) | (c.y << 3u) | (c.z >> 2u) | (cov >> 2u);
|
||||
vram16.data[index ^ 1u] = mem_u16(word);
|
||||
hidden_vram.data[index] = mem_u8(cov & U8_C(3));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_IA88:
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
uvec2 col = current_color.ra;
|
||||
uint word = (col.x << 8u) | col.y;
|
||||
vram16.data[index ^ 1u] = mem_u16(word);
|
||||
hidden_vram.data[index] = mem_u8((col.y & 1) * 3);
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA8888:
|
||||
{
|
||||
index &= RDRAM_MASK_32;
|
||||
index += slice * (RDRAM_SIZE >> 2);
|
||||
uvec4 col = current_color;
|
||||
uint word = (col.r << 24u) | (col.g << 16u) | (col.b << 8u) | (col.a << 0u);
|
||||
vram32.data[index] = word;
|
||||
hidden_vram.data[2u * index] = mem_u8((current_color.g & 1) * 3);
|
||||
hidden_vram.data[2u * index + 1u] = mem_u8((current_color.a & 1) * 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (RDRAM_UNSCALED_WRITE_MASK)
|
||||
{
|
||||
// Need this memory barrier to ensure the mask readback does not read
|
||||
// an invalid value from RDRAM. If the mask is seen, the valid RDRAM value is
|
||||
// also coherent.
|
||||
memoryBarrierBuffer();
|
||||
store_unscaled_write_mask(index);
|
||||
}
|
||||
}
|
||||
|
||||
void store_vram_depth(uint index, uint slice)
|
||||
{
|
||||
if (!FB_COLOR_DEPTH_ALIAS)
|
||||
{
|
||||
if (current_depth_dirty)
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
index += slice * (RDRAM_SIZE >> 1);
|
||||
vram16.data[index ^ 1u] = mem_u16((current_depth << U16_C(2)) | (current_dz >> U16_C(2)));
|
||||
hidden_vram.data[index] = mem_u8(current_dz & U16_C(3));
|
||||
}
|
||||
|
||||
if (RDRAM_UNSCALED_WRITE_MASK)
|
||||
{
|
||||
// Need this memory barrier to ensure the mask readback does not read
|
||||
// an invalid value from RDRAM. If the mask is seen, the valid RDRAM value is
|
||||
// also coherent.
|
||||
memoryBarrierBuffer();
|
||||
if (current_depth_dirty)
|
||||
vram16.data[(index ^ 1) + (RDRAM_SIZE >> 1u)] = mem_u16(0xffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint color_fb_index;
|
||||
|
||||
void init_tile(uvec2 coord, uint fb_width, uint fb_height, uint fb_addr_index, uint fb_depth_addr_index)
|
||||
{
|
||||
current_color_dirty = false;
|
||||
current_depth_dirty = false;
|
||||
if (all(lessThan(coord, uvec2(fb_width, fb_height))))
|
||||
{
|
||||
uvec2 slice2d = coord & (SCALING_FACTOR - 1);
|
||||
coord >>= SCALING_LOG2;
|
||||
uint slice = slice2d.y * SCALING_FACTOR + slice2d.x;
|
||||
|
||||
uint index = fb_addr_index + (fb_width >> SCALING_LOG2) * coord.y + coord.x;
|
||||
color_fb_index = index;
|
||||
load_vram_color(index, slice);
|
||||
|
||||
index = fb_depth_addr_index + (fb_width >> SCALING_LOG2) * coord.y + coord.x;
|
||||
load_vram_depth(index, slice);
|
||||
}
|
||||
}
|
||||
|
||||
void emit_scaled_write_masks(uvec2 unscaled_coord, uint unscaled_fb_width)
|
||||
{
|
||||
// Merge write masks across pixels.
|
||||
// We reserved a chunk of memory after scaled RDRAM to store 2 bits per pixel holding
|
||||
// a write mask for color and depth. The resolve stage will only resolve a pixel
|
||||
// and trigger a write if any sub-sample was marked as written.
|
||||
|
||||
// Write masks are organized in 4x4 blocks of unscaled pixels for locality purposes.
|
||||
// This guarantees a minimum number of loop iterations to resolve the write masks.
|
||||
uint unscaled_block = (unscaled_coord.y >> 2u) * ((unscaled_fb_width + 3u) >> 2u) + (unscaled_coord.x >> 2u);
|
||||
uvec2 unscaled_sub = unscaled_coord & 3u;
|
||||
uint word = uint(current_color_dirty) + 2u * uint(current_depth_dirty);
|
||||
word <<= 2u * (unscaled_sub.x + unscaled_sub.y * 4u);
|
||||
|
||||
#if SUBGROUP
|
||||
// This should only need one iteration .
|
||||
bool is_active = true;
|
||||
do
|
||||
{
|
||||
if (subgroupBroadcastFirst(unscaled_block) == unscaled_block)
|
||||
{
|
||||
uint merged = subgroupOr(word);
|
||||
if (subgroupElect())
|
||||
atomicOr(vram32.data[SCALING_FACTOR * SCALING_FACTOR * (RDRAM_SIZE >> 2) + unscaled_block], merged);
|
||||
is_active = false;
|
||||
}
|
||||
} while (is_active);
|
||||
#else
|
||||
// Just use atomics directly. With subgroup support, we can be a bit smarter about it.
|
||||
if (word != 0u)
|
||||
atomicOr(vram32.data[SCALING_FACTOR * SCALING_FACTOR * (RDRAM_SIZE >> 2) + unscaled_block], word);
|
||||
#endif
|
||||
}
|
||||
|
||||
void finish_tile(uvec2 coord, uint fb_width, uint fb_height, uint fb_addr_index, uint fb_depth_addr_index)
|
||||
{
|
||||
// MSL portability: Need to maintain uniform control flow.
|
||||
if (any(greaterThanEqual(coord, uvec2(fb_width, fb_height))))
|
||||
{
|
||||
current_color_dirty = false;
|
||||
current_depth_dirty = false;
|
||||
}
|
||||
|
||||
uint unscaled_fb_width = fb_width >> SCALING_LOG2;
|
||||
|
||||
uvec2 slice2d = coord & (SCALING_FACTOR - 1);
|
||||
coord >>= SCALING_LOG2;
|
||||
uint slice = slice2d.y * SCALING_FACTOR + slice2d.x;
|
||||
|
||||
uint index = fb_addr_index + unscaled_fb_width * coord.y + coord.x;
|
||||
store_vram_color(index, slice);
|
||||
|
||||
index = fb_depth_addr_index + unscaled_fb_width * coord.y + coord.x;
|
||||
store_vram_depth(index, slice);
|
||||
|
||||
if (RDRAM_SCALED_WRITE_MASK)
|
||||
emit_scaled_write_masks(coord, unscaled_fb_width);
|
||||
}
|
||||
|
||||
u8x4 decode_memory_color(bool image_read_en)
|
||||
{
|
||||
u8 memory_coverage = image_read_en ? (current_color.a & U8_C(0xe0)) : U8_C(0xe0);
|
||||
|
||||
u8x3 color;
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_I4:
|
||||
color = u8x3(0);
|
||||
memory_coverage = U8_C(0xe0);
|
||||
break;
|
||||
|
||||
case FB_FMT_I8:
|
||||
color = current_color.rrr;
|
||||
memory_coverage = U8_C(0xe0);
|
||||
break;
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
color = current_color.rgb & U8_C(0xf8);
|
||||
break;
|
||||
|
||||
case FB_FMT_IA88:
|
||||
color = current_color.rrr;
|
||||
break;
|
||||
|
||||
case FB_FMT_RGBA8888:
|
||||
color = current_color.rgb;
|
||||
break;
|
||||
}
|
||||
return u8x4(color, memory_coverage);
|
||||
}
|
||||
|
||||
void write_color(u8x4 col)
|
||||
{
|
||||
if (FB_FMT == FB_FMT_I4)
|
||||
current_color.rgb = col.rgb;
|
||||
else
|
||||
current_color = col;
|
||||
current_color_dirty = true;
|
||||
}
|
||||
|
||||
void copy_pipeline(uint word, uint primitive_index)
|
||||
{
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_I4:
|
||||
{
|
||||
current_color = u8x4(0);
|
||||
current_color_dirty = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_I8:
|
||||
{
|
||||
// Alpha testing needs to only look at the low dword for some bizarre reason.
|
||||
// I don't think alpha testing is supposed to be used at all with 8-bit FB ...
|
||||
word &= 0xffu;
|
||||
write_color(u8x4(word));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
uint r = (word >> 8) & 0xf8u;
|
||||
uint g = (word >> 3) & 0xf8u;
|
||||
uint b = (word << 2) & 0xf8u;
|
||||
uint a = (word & 1) * 0xe0u;
|
||||
write_color(u8x4(r, g, b, a));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (FB_COLOR_DEPTH_ALIAS)
|
||||
alias_color_to_depth();
|
||||
}
|
||||
|
||||
void fill_color(uint col)
|
||||
{
|
||||
switch (FB_FMT)
|
||||
{
|
||||
case FB_FMT_RGBA8888:
|
||||
{
|
||||
uint r = (col >> 24u) & 0xffu;
|
||||
uint g = (col >> 16u) & 0xffu;
|
||||
uint b = (col >> 8u) & 0xffu;
|
||||
uint a = (col >> 0u) & 0xffu;
|
||||
write_color(u8x4(r, g, b, a));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_RGBA5551:
|
||||
{
|
||||
col >>= ((color_fb_index & 1u) ^ 1u) * 16u;
|
||||
uint r = (col >> 8u) & 0xf8u;
|
||||
uint g = (col >> 3u) & 0xf8u;
|
||||
uint b = (col << 2u) & 0xf8u;
|
||||
uint a = (col & 1u) * 0xe0u;
|
||||
write_color(u8x4(r, g, b, a));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_IA88:
|
||||
{
|
||||
col >>= ((color_fb_index & 1u) ^ 1u) * 16u;
|
||||
col &= 0xffffu;
|
||||
uint r = (col >> 8u) & 0xffu;
|
||||
uint a = (col >> 0u) & 0xffu;
|
||||
write_color(u8x4(r, r, r, a));
|
||||
break;
|
||||
}
|
||||
|
||||
case FB_FMT_I8:
|
||||
{
|
||||
col >>= ((color_fb_index & 3u) ^ 3u) * 8u;
|
||||
col &= 0xffu;
|
||||
write_color(u8x4(col));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (FB_COLOR_DEPTH_ALIAS)
|
||||
alias_color_to_depth();
|
||||
}
|
||||
|
||||
void depth_blend(int x, int y, uint primitive_index, ShadedData shaded)
|
||||
{
|
||||
int z = shaded.z_dith >> 9;
|
||||
int dith = shaded.z_dith & 0x1ff;
|
||||
int coverage_count = shaded.coverage_count;
|
||||
u8x4 combined = shaded.combined;
|
||||
u8 shade_alpha = shaded.shade_alpha;
|
||||
|
||||
uint blend_state_index = uint(state_indices.elems[primitive_index].static_depth_tmem.y);
|
||||
DerivedSetup derived = load_derived_setup(primitive_index);
|
||||
DepthBlendState depth_blend = load_depth_blend_state(blend_state_index);
|
||||
|
||||
bool force_blend = (depth_blend.flags & DEPTH_BLEND_FORCE_BLEND_BIT) != 0;
|
||||
bool z_compare = (depth_blend.flags & DEPTH_BLEND_DEPTH_TEST_BIT) != 0;
|
||||
bool z_update = (depth_blend.flags & DEPTH_BLEND_DEPTH_UPDATE_BIT) != 0;
|
||||
bool image_read_enable = (depth_blend.flags & DEPTH_BLEND_IMAGE_READ_ENABLE_BIT) != 0;
|
||||
bool color_on_coverage = (depth_blend.flags & DEPTH_BLEND_COLOR_ON_COVERAGE_BIT) != 0;
|
||||
bool blend_multicycle = (depth_blend.flags & DEPTH_BLEND_MULTI_CYCLE_BIT) != 0;
|
||||
bool aa_enable = (depth_blend.flags & DEPTH_BLEND_AA_BIT) != 0;
|
||||
bool dither_en = (depth_blend.flags & DEPTH_BLEND_DITHER_ENABLE_BIT) != 0;
|
||||
|
||||
bool blend_en;
|
||||
bool coverage_wrap;
|
||||
u8x2 blend_shift;
|
||||
|
||||
u8x4 memory_color = decode_memory_color(image_read_enable);
|
||||
u8 memory_coverage = memory_color.a >> U8_C(5);
|
||||
|
||||
bool z_pass = depth_test(z, derived.dz, derived.dz_compressed,
|
||||
current_depth, current_dz,
|
||||
coverage_count, memory_coverage,
|
||||
z_compare, depth_blend.z_mode,
|
||||
force_blend, aa_enable,
|
||||
blend_en, coverage_wrap, blend_shift);
|
||||
|
||||
GENERIC_MESSAGE3(combined.x, combined.y, combined.z);
|
||||
|
||||
// Pixel tests.
|
||||
if (z_pass && (!aa_enable || coverage_count != 0))
|
||||
{
|
||||
// Blending
|
||||
BlendInputs blender_inputs =
|
||||
BlendInputs(combined, memory_color,
|
||||
derived.fog_color, derived.blend_color, shade_alpha);
|
||||
|
||||
u8x4 blend_modes = depth_blend.blend_modes0;
|
||||
if (blend_multicycle)
|
||||
{
|
||||
blender_inputs.pixel_color.rgb =
|
||||
blender(blender_inputs,
|
||||
blend_modes,
|
||||
force_blend, blend_en, color_on_coverage, coverage_wrap, blend_shift, false);
|
||||
blend_modes = depth_blend.blend_modes1;
|
||||
}
|
||||
u8x3 rgb = blender(blender_inputs,
|
||||
blend_modes,
|
||||
force_blend, blend_en, color_on_coverage, coverage_wrap, blend_shift, true);
|
||||
|
||||
// Dither
|
||||
if (dither_en)
|
||||
rgb = rgb_dither(rgb, dith);
|
||||
|
||||
// Coverage blending
|
||||
int new_coverage = blend_coverage(coverage_count, memory_coverage, blend_en, depth_blend.coverage_mode);
|
||||
|
||||
GENERIC_MESSAGE3(rgb.x, rgb.y, rgb.z);
|
||||
|
||||
// Writeback
|
||||
write_color(u8x4(rgb, new_coverage << 5));
|
||||
|
||||
// Z-writeback.
|
||||
if (z_update)
|
||||
{
|
||||
current_depth = z_compress(z);
|
||||
current_dz = u8(derived.dz_compressed);
|
||||
current_depth_dirty = true;
|
||||
|
||||
if (FB_COLOR_DEPTH_ALIAS)
|
||||
alias_depth_to_color();
|
||||
}
|
||||
else if (FB_COLOR_DEPTH_ALIAS)
|
||||
alias_color_to_depth();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,71 @@
|
||||
/* 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 NOISE_H_
|
||||
#define NOISE_H_
|
||||
|
||||
u16 seeded_noise = U16_C(0);
|
||||
|
||||
// From: https://www.shadertoy.com/view/XlXcW4 with slight modifications.
|
||||
void reseed_noise(uint x, uint y, uint primitive_offset)
|
||||
{
|
||||
const uint NOISE_PRIME = 1103515245u;
|
||||
uvec3 seed = uvec3(x, y, primitive_offset);
|
||||
seed = ((seed >> 8u) ^ seed.yzx) * NOISE_PRIME;
|
||||
seed = ((seed >> 8u) ^ seed.yzx) * NOISE_PRIME;
|
||||
seed = ((seed >> 8u) ^ seed.yzx) * NOISE_PRIME;
|
||||
seeded_noise = u16(seed.x >> 16u);
|
||||
}
|
||||
|
||||
i16 noise_get_combiner()
|
||||
{
|
||||
return i16(((seeded_noise & U16_C(7u)) << U16_C(6u)) | U16_C(0x20u));
|
||||
}
|
||||
|
||||
int noise_get_dither_alpha()
|
||||
{
|
||||
return int(seeded_noise & U16_C(7u));
|
||||
}
|
||||
|
||||
int noise_get_dither_color()
|
||||
{
|
||||
// 3 bits of noise for RGB separately.
|
||||
return int(seeded_noise & U16_C(0x1ff));
|
||||
}
|
||||
|
||||
u8 noise_get_blend_threshold()
|
||||
{
|
||||
return u8(seeded_noise & U16_C(0xffu));
|
||||
}
|
||||
|
||||
uvec3 noise_get_full_gamma_dither()
|
||||
{
|
||||
uint seed = seeded_noise;
|
||||
return uvec3(seed & 0x3f, (seed >> 6u) & 0x3f, ((seed >> 9u) & 0x38) | (seed & 7u));
|
||||
}
|
||||
|
||||
uvec3 noise_get_partial_gamma_dither()
|
||||
{
|
||||
return (uvec3(seeded_noise) >> uvec3(0, 1, 2)) & 1u;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,114 @@
|
||||
/* 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 PERSPECTIVE_H_
|
||||
#define PERSPECTIVE_H_
|
||||
|
||||
const i16x2 perspective_table[64] = i16x2[](
|
||||
i16x2(0x4000, -252 * 4), i16x2(0x3f04, -244 * 4), i16x2(0x3e10, -238 * 4), i16x2(0x3d22, -230 * 4),
|
||||
i16x2(0x3c3c, -223 * 4), i16x2(0x3b5d, -218 * 4), i16x2(0x3a83, -210 * 4), i16x2(0x39b1, -205 * 4),
|
||||
i16x2(0x38e4, -200 * 4), i16x2(0x381c, -194 * 4), i16x2(0x375a, -189 * 4), i16x2(0x369d, -184 * 4),
|
||||
i16x2(0x35e5, -179 * 4), i16x2(0x3532, -175 * 4), i16x2(0x3483, -170 * 4), i16x2(0x33d9, -166 * 4),
|
||||
i16x2(0x3333, -162 * 4), i16x2(0x3291, -157 * 4), i16x2(0x31f4, -155 * 4), i16x2(0x3159, -150 * 4),
|
||||
i16x2(0x30c3, -147 * 4), i16x2(0x3030, -143 * 4), i16x2(0x2fa1, -140 * 4), i16x2(0x2f15, -137 * 4),
|
||||
i16x2(0x2e8c, -134 * 4), i16x2(0x2e06, -131 * 4), i16x2(0x2d83, -128 * 4), i16x2(0x2d03, -125 * 4),
|
||||
i16x2(0x2c86, -123 * 4), i16x2(0x2c0b, -120 * 4), i16x2(0x2b93, -117 * 4), i16x2(0x2b1e, -115 * 4),
|
||||
i16x2(0x2aab, -113 * 4), i16x2(0x2a3a, -110 * 4), i16x2(0x29cc, -108 * 4), i16x2(0x2960, -106 * 4),
|
||||
i16x2(0x28f6, -104 * 4), i16x2(0x288e, -102 * 4), i16x2(0x2828, -100 * 4), i16x2(0x27c4, -98 * 4),
|
||||
i16x2(0x2762, -96 * 4), i16x2(0x2702, -94 * 4), i16x2(0x26a4, -92 * 4), i16x2(0x2648, -91 * 4),
|
||||
i16x2(0x25ed, -89 * 4), i16x2(0x2594, -87 * 4), i16x2(0x253d, -86 * 4), i16x2(0x24e7, -85 * 4),
|
||||
i16x2(0x2492, -83 * 4), i16x2(0x243f, -81 * 4), i16x2(0x23ee, -80 * 4), i16x2(0x239e, -79 * 4),
|
||||
i16x2(0x234f, -77 * 4), i16x2(0x2302, -76 * 4), i16x2(0x22b6, -74 * 4), i16x2(0x226c, -74 * 4),
|
||||
i16x2(0x2222, -72 * 4), i16x2(0x21da, -71 * 4), i16x2(0x2193, -70 * 4), i16x2(0x214d, -69 * 4),
|
||||
i16x2(0x2108, -67 * 4), i16x2(0x20c5, -67 * 4), i16x2(0x2082, -65 * 4), i16x2(0x2041, -65 * 4)
|
||||
);
|
||||
|
||||
ivec2 perspective_get_lut(int w)
|
||||
{
|
||||
int shift = min(14 - findMSB(w), 14);
|
||||
int normout = (w << shift) & 0x3fff;
|
||||
int wnorm = normout & 0xff;
|
||||
ivec2 table = ivec2(perspective_table[normout >> 8]);
|
||||
int rcp = ((table.y * wnorm) >> 10) + table.x;
|
||||
return ivec2(rcp, shift);
|
||||
}
|
||||
|
||||
ivec2 no_perspective_divide(ivec3 stw)
|
||||
{
|
||||
return stw.xy;
|
||||
}
|
||||
|
||||
// s16 divided by s1.15.
|
||||
// Classic approximation of a (x * rcp) >> shift with a LUT to find rcp.
|
||||
ivec2 perspective_divide(ivec3 stw, inout bool overflow)
|
||||
{
|
||||
int w = stw.z;
|
||||
bool w_carry = w <= 0;
|
||||
w &= 0x7fff;
|
||||
|
||||
ivec2 table = perspective_get_lut(w);
|
||||
int shift = table.y;
|
||||
ivec2 prod = stw.xy * table.x;
|
||||
|
||||
int temp_mask = ((1 << 30) - 1) & -((1 << 29) >> shift);
|
||||
ivec2 out_of_bounds = prod & temp_mask;
|
||||
|
||||
ivec2 temp;
|
||||
if (shift != 14)
|
||||
temp = prod = prod >> (13 - shift);
|
||||
else
|
||||
temp = prod << 1;
|
||||
|
||||
if (any(notEqual(out_of_bounds, ivec2(0))))
|
||||
{
|
||||
if (out_of_bounds.x != temp_mask && out_of_bounds.x != 0)
|
||||
{
|
||||
if ((prod.x & (1 << 29)) == 0)
|
||||
temp.x = 0x7fff;
|
||||
else
|
||||
temp.x = -0x8000;
|
||||
overflow = true;
|
||||
}
|
||||
|
||||
if (out_of_bounds.y != temp_mask && out_of_bounds.y != 0)
|
||||
{
|
||||
if ((prod.y & (1 << 29)) == 0)
|
||||
temp.y = 0x7fff;
|
||||
else
|
||||
temp.y = -0x8000;
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (w_carry)
|
||||
{
|
||||
temp = ivec2(0x7fff);
|
||||
overflow = true;
|
||||
}
|
||||
|
||||
// Perspective divide produces a 17-bit signed coordinate, which is later clamped to 16-bit signed.
|
||||
// However, the LOD computation happens in 17 bits ...
|
||||
return clamp(temp, ivec2(-0x10000), ivec2(0xffff));
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,191 @@
|
||||
#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_id = 0, local_size_y_id = 1) in;
|
||||
|
||||
#include "debug.h"
|
||||
#include "data_structures.h"
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer TriangleSetupBuffer
|
||||
{
|
||||
TriangleSetupMem elems[];
|
||||
} triangle_setup;
|
||||
#include "load_triangle_setup.h"
|
||||
|
||||
layout(set = 0, binding = 1, std430) readonly buffer AttributeSetupBuffer
|
||||
{
|
||||
AttributeSetupMem elems[];
|
||||
} attribute_setup;
|
||||
#include "load_attribute_setup.h"
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer DerivedSetupBuffer
|
||||
{
|
||||
DerivedSetupMem elems[];
|
||||
} derived_setup;
|
||||
#include "load_derived_setup.h"
|
||||
|
||||
layout(set = 0, binding = 3, std430) readonly buffer StaticRasterStateBuffer
|
||||
{
|
||||
StaticRasterizationStateMem elems[];
|
||||
} static_raster_state;
|
||||
#include "load_static_raster_state.h"
|
||||
|
||||
layout(set = 0, binding = 4, std430) readonly buffer StateIndicesBuffer
|
||||
{
|
||||
InstanceIndicesMem elems[];
|
||||
} state_indices;
|
||||
|
||||
layout(set = 0, binding = 5, std430) readonly buffer SpanInfoOffsetBuffer
|
||||
{
|
||||
SpanInfoOffsetsMem elems[];
|
||||
} span_offsets;
|
||||
#include "load_span_offsets.h"
|
||||
|
||||
layout(set = 0, binding = 6, std430) readonly buffer SpanSetups
|
||||
{
|
||||
SpanSetupMem elems[];
|
||||
} span_setups;
|
||||
#include "load_span_setup.h"
|
||||
|
||||
layout(set = 0, binding = 7, std430) readonly buffer TMEM16
|
||||
{
|
||||
TMEMInstance16Mem instances[];
|
||||
} tmem16;
|
||||
|
||||
layout(set = 0, binding = 7, std430) readonly buffer TMEM8
|
||||
{
|
||||
TMEMInstance8Mem instances[];
|
||||
} tmem8;
|
||||
|
||||
layout(set = 0, binding = 8, std430) readonly buffer TileInfoBuffer
|
||||
{
|
||||
TileInfoMem elems[];
|
||||
} tile_infos;
|
||||
#include "load_tile_info.h"
|
||||
|
||||
layout(set = 2, binding = 0, std140) uniform GlobalConstants
|
||||
{
|
||||
GlobalFBInfo fb_info;
|
||||
} global_constants;
|
||||
|
||||
layout(constant_id = 2) const int STATIC_STATE_FLAGS = 0;
|
||||
layout(constant_id = 3) const int COMBINER_INPUTS_RGB0 = 0;
|
||||
layout(constant_id = 4) const int COMBINER_INPUTS_ALPHA0 = 0;
|
||||
layout(constant_id = 5) const int COMBINER_INPUTS_RGB1 = 0;
|
||||
layout(constant_id = 6) const int COMBINER_INPUTS_ALPHA1 = 0;
|
||||
layout(constant_id = 7) const int DITHER_TEX_SIZE_TEX_FMT = 0;
|
||||
|
||||
const int COMBINER_INPUT_RGB0_MULADD = (COMBINER_INPUTS_RGB0 >> 0) & 0xff;
|
||||
const int COMBINER_INPUT_RGB0_MULSUB = (COMBINER_INPUTS_RGB0 >> 8) & 0xff;
|
||||
const int COMBINER_INPUT_RGB0_MUL = (COMBINER_INPUTS_RGB0 >> 16) & 0xff;
|
||||
const int COMBINER_INPUT_RGB0_ADD = (COMBINER_INPUTS_RGB0 >> 24) & 0xff;
|
||||
|
||||
const int COMBINER_INPUT_ALPHA0_MULADD = (COMBINER_INPUTS_ALPHA0 >> 0) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA0_MULSUB = (COMBINER_INPUTS_ALPHA0 >> 8) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA0_MUL = (COMBINER_INPUTS_ALPHA0 >> 16) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA0_ADD = (COMBINER_INPUTS_ALPHA0 >> 24) & 0xff;
|
||||
|
||||
const int COMBINER_INPUT_RGB1_MULADD = (COMBINER_INPUTS_RGB1 >> 0) & 0xff;
|
||||
const int COMBINER_INPUT_RGB1_MULSUB = (COMBINER_INPUTS_RGB1 >> 8) & 0xff;
|
||||
const int COMBINER_INPUT_RGB1_MUL = (COMBINER_INPUTS_RGB1 >> 16) & 0xff;
|
||||
const int COMBINER_INPUT_RGB1_ADD = (COMBINER_INPUTS_RGB1 >> 24) & 0xff;
|
||||
|
||||
const int COMBINER_INPUT_ALPHA1_MULADD = (COMBINER_INPUTS_ALPHA1 >> 0) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA1_MULSUB = (COMBINER_INPUTS_ALPHA1 >> 8) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA1_MUL = (COMBINER_INPUTS_ALPHA1 >> 16) & 0xff;
|
||||
const int COMBINER_INPUT_ALPHA1_ADD = (COMBINER_INPUTS_ALPHA1 >> 24) & 0xff;
|
||||
|
||||
const int DITHER = (DITHER_TEX_SIZE_TEX_FMT >> 0) & 0xff;
|
||||
const int TEX_SIZE = (DITHER_TEX_SIZE_TEX_FMT >> 8) & 0xff;
|
||||
const int TEX_FMT = (DITHER_TEX_SIZE_TEX_FMT >> 16) & 0xff;
|
||||
|
||||
#define RASTERIZER_SPEC_CONSTANT
|
||||
|
||||
#include "noise.h"
|
||||
#include "shading.h"
|
||||
|
||||
layout(set = 0, binding = 9, std430) writeonly buffer ColorBuffer
|
||||
{
|
||||
mem_u8x4 elems[];
|
||||
} color;
|
||||
|
||||
layout(set = 0, binding = 9, std430) writeonly buffer ColorBufferRaw
|
||||
{
|
||||
uint elems[];
|
||||
} raw_color;
|
||||
|
||||
layout(set = 0, binding = 10, std430) writeonly buffer DepthBuffer
|
||||
{
|
||||
int elems[];
|
||||
} depth;
|
||||
|
||||
layout(set = 0, binding = 11, std430) writeonly buffer ShadeAlpha
|
||||
{
|
||||
mem_u8 elems[];
|
||||
} shade_alpha;
|
||||
|
||||
layout(set = 0, binding = 12, std430) writeonly buffer Coverage
|
||||
{
|
||||
mem_i8 elems[];
|
||||
} coverage;
|
||||
|
||||
layout(set = 1, binding = 0, std430) readonly buffer TileWorkList
|
||||
{
|
||||
uvec4 elems[];
|
||||
} tile_work_list;
|
||||
|
||||
void main()
|
||||
{
|
||||
uvec4 work = tile_work_list.elems[gl_WorkGroupID.x];
|
||||
int x = int(work.x * gl_WorkGroupSize.x + gl_LocalInvocationID.x);
|
||||
int y = int(work.y * gl_WorkGroupSize.y + gl_LocalInvocationID.y);
|
||||
uint tile_instance = work.z;
|
||||
uint primitive_index = work.w;
|
||||
|
||||
ShadedData shaded;
|
||||
i8 coverage_value;
|
||||
uint index = tile_instance * (gl_WorkGroupSize.x * gl_WorkGroupSize.y) + gl_LocalInvocationIndex;
|
||||
|
||||
if (shade_pixel(x, y, primitive_index, shaded))
|
||||
{
|
||||
coverage_value = i8(shaded.coverage_count);
|
||||
|
||||
if (coverage_value <= I8_C(8))
|
||||
{
|
||||
// Workaround curious bug with glslang, need to cast manually to uvec4 first.
|
||||
color.elems[index] = mem_u8x4(uvec4(shaded.combined));
|
||||
shade_alpha.elems[index] = mem_u8(shaded.shade_alpha);
|
||||
depth.elems[index] = shaded.z_dith;
|
||||
}
|
||||
else if ((coverage_value & COVERAGE_COPY_BIT) != 0)
|
||||
{
|
||||
// For copy pipe, we use a raw 32-bit word to represent the loaded texel.
|
||||
raw_color.elems[index] = shaded.z_dith;
|
||||
}
|
||||
}
|
||||
else
|
||||
coverage_value = I8_C(-1);
|
||||
|
||||
coverage.elems[index] = mem_i8(coverage_value);
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/* 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 SHADING_H_
|
||||
#define SHADING_H_
|
||||
|
||||
#ifdef RASTERIZER_SPEC_CONSTANT
|
||||
const int SCALING_LOG2 = (STATIC_STATE_FLAGS >> RASTERIZATION_UPSCALING_LOG2_BIT_OFFSET) & 3;
|
||||
const int SCALING_FACTOR = 1 << SCALING_LOG2;
|
||||
#endif
|
||||
|
||||
#include "coverage.h"
|
||||
#include "interpolation.h"
|
||||
#include "perspective.h"
|
||||
#include "texture.h"
|
||||
#include "dither.h"
|
||||
#include "combiner.h"
|
||||
|
||||
bool shade_pixel(int x, int y, uint primitive_index, out ShadedData shaded)
|
||||
{
|
||||
SpanInfoOffsets span_offsets = load_span_offsets(primitive_index);
|
||||
if ((y < (SCALING_FACTOR * span_offsets.ylo)) || (y > (span_offsets.yhi * SCALING_FACTOR + (SCALING_FACTOR - 1))))
|
||||
return false;
|
||||
|
||||
uint setup_flags = uint(triangle_setup.elems[primitive_index].flags);
|
||||
if (SCALING_FACTOR > 1)
|
||||
{
|
||||
if ((setup_flags & TRIANGLE_SETUP_DISABLE_UPSCALING_BIT) != 0u)
|
||||
{
|
||||
x &= ~(SCALING_FACTOR - 1);
|
||||
y &= ~(SCALING_FACTOR - 1);
|
||||
}
|
||||
}
|
||||
|
||||
SpanSetup span_setup = load_span_setup(SCALING_FACTOR * span_offsets.offset + (y - SCALING_FACTOR * span_offsets.ylo));
|
||||
if (span_setup.valid_line == U16_C(0))
|
||||
return false;
|
||||
|
||||
uint setup_tile = uint(triangle_setup.elems[primitive_index].tile);
|
||||
AttributeSetup attr = load_attribute_setup(primitive_index);
|
||||
|
||||
uvec4 states = uvec4(state_indices.elems[primitive_index].static_depth_tmem);
|
||||
uint static_state_index = states.x;
|
||||
uint tmem_instance_index = states.z;
|
||||
|
||||
StaticRasterizationState static_state = load_static_rasterization_state(static_state_index);
|
||||
uint static_state_flags = static_state.flags;
|
||||
int static_state_dither = static_state.dither;
|
||||
u8x4 combiner_inputs_rgb0 = static_state.combiner_inputs_rgb0;
|
||||
u8x4 combiner_inputs_alpha0 = static_state.combiner_inputs_alpha0;
|
||||
u8x4 combiner_inputs_rgb1 = static_state.combiner_inputs_rgb1;
|
||||
u8x4 combiner_inputs_alpha1 = static_state.combiner_inputs_alpha1;
|
||||
|
||||
#ifdef RASTERIZER_SPEC_CONSTANT
|
||||
if ((STATIC_STATE_FLAGS & RASTERIZATION_USE_SPECIALIZATION_CONSTANT_BIT) != 0)
|
||||
{
|
||||
static_state_flags = STATIC_STATE_FLAGS;
|
||||
static_state_dither = DITHER;
|
||||
|
||||
combiner_inputs_rgb0.x = u8(COMBINER_INPUT_RGB0_MULADD);
|
||||
combiner_inputs_rgb0.y = u8(COMBINER_INPUT_RGB0_MULSUB);
|
||||
combiner_inputs_rgb0.z = u8(COMBINER_INPUT_RGB0_MUL);
|
||||
combiner_inputs_rgb0.w = u8(COMBINER_INPUT_RGB0_ADD);
|
||||
|
||||
combiner_inputs_alpha0.x = u8(COMBINER_INPUT_ALPHA0_MULADD);
|
||||
combiner_inputs_alpha0.y = u8(COMBINER_INPUT_ALPHA0_MULSUB);
|
||||
combiner_inputs_alpha0.z = u8(COMBINER_INPUT_ALPHA0_MUL);
|
||||
combiner_inputs_alpha0.w = u8(COMBINER_INPUT_ALPHA0_ADD);
|
||||
|
||||
combiner_inputs_rgb1.x = u8(COMBINER_INPUT_RGB1_MULADD);
|
||||
combiner_inputs_rgb1.y = u8(COMBINER_INPUT_RGB1_MULSUB);
|
||||
combiner_inputs_rgb1.z = u8(COMBINER_INPUT_RGB1_MUL);
|
||||
combiner_inputs_rgb1.w = u8(COMBINER_INPUT_RGB1_ADD);
|
||||
|
||||
combiner_inputs_alpha1.x = u8(COMBINER_INPUT_ALPHA1_MULADD);
|
||||
combiner_inputs_alpha1.y = u8(COMBINER_INPUT_ALPHA1_MULSUB);
|
||||
combiner_inputs_alpha1.z = u8(COMBINER_INPUT_ALPHA1_MUL);
|
||||
combiner_inputs_alpha1.w = u8(COMBINER_INPUT_ALPHA1_ADD);
|
||||
}
|
||||
#endif
|
||||
|
||||
// This is a great case for specialization constants.
|
||||
bool tlut = (static_state_flags & RASTERIZATION_TLUT_BIT) != 0;
|
||||
bool tlut_type = (static_state_flags & RASTERIZATION_TLUT_TYPE_BIT) != 0;
|
||||
bool sample_quad = (static_state_flags & RASTERIZATION_SAMPLE_MODE_BIT) != 0;
|
||||
bool cvg_times_alpha = (static_state_flags & RASTERIZATION_CVG_TIMES_ALPHA_BIT) != 0;
|
||||
bool alpha_cvg_select = (static_state_flags & RASTERIZATION_ALPHA_CVG_SELECT_BIT) != 0;
|
||||
bool perspective = (static_state_flags & RASTERIZATION_PERSPECTIVE_CORRECT_BIT) != 0;
|
||||
bool tex_lod_en = (static_state_flags & RASTERIZATION_TEX_LOD_ENABLE_BIT) != 0;
|
||||
bool sharpen_lod_en = (static_state_flags & RASTERIZATION_SHARPEN_LOD_ENABLE_BIT) != 0;
|
||||
bool detail_lod_en = (static_state_flags & RASTERIZATION_DETAIL_LOD_ENABLE_BIT) != 0;
|
||||
bool aa_enable = (static_state_flags & RASTERIZATION_AA_BIT) != 0;
|
||||
bool multi_cycle = (static_state_flags & RASTERIZATION_MULTI_CYCLE_BIT) != 0;
|
||||
bool interlace_en = (static_state_flags & RASTERIZATION_INTERLACE_FIELD_BIT) != 0;
|
||||
bool fill_en = (static_state_flags & RASTERIZATION_FILL_BIT) != 0;
|
||||
bool copy_en = (static_state_flags & RASTERIZATION_COPY_BIT) != 0;
|
||||
bool alpha_test = (static_state_flags & RASTERIZATION_ALPHA_TEST_BIT) != 0;
|
||||
bool alpha_test_dither = (static_state_flags & RASTERIZATION_ALPHA_TEST_DITHER_BIT) != 0;
|
||||
bool mid_texel = (static_state_flags & RASTERIZATION_SAMPLE_MID_TEXEL_BIT) != 0;
|
||||
bool uses_texel0 = (static_state_flags & RASTERIZATION_USES_TEXEL0_BIT) != 0;
|
||||
bool uses_texel1 = (static_state_flags & RASTERIZATION_USES_TEXEL1_BIT) != 0;
|
||||
bool uses_pipelined_texel1 = (static_state_flags & RASTERIZATION_USES_PIPELINED_TEXEL1_BIT) != 0;
|
||||
bool uses_lod = (static_state_flags & RASTERIZATION_USES_LOD_BIT) != 0;
|
||||
bool convert_one = (static_state_flags & RASTERIZATION_CONVERT_ONE_BIT) != 0;
|
||||
bool bilerp0 = (static_state_flags & RASTERIZATION_BILERP_0_BIT) != 0;
|
||||
bool bilerp1 = (static_state_flags & RASTERIZATION_BILERP_1_BIT) != 0;
|
||||
|
||||
if ((static_state_flags & RASTERIZATION_NEED_NOISE_BIT) != 0)
|
||||
reseed_noise(x, y, primitive_index + global_constants.fb_info.base_primitive_index);
|
||||
|
||||
bool flip = (setup_flags & TRIANGLE_SETUP_FLIP_BIT) != 0;
|
||||
|
||||
if (copy_en)
|
||||
{
|
||||
bool valid = x >= span_setup.start_x && x <= span_setup.end_x;
|
||||
if (!valid)
|
||||
return false;
|
||||
|
||||
ivec2 st;
|
||||
int s_offset;
|
||||
interpolate_st_copy(span_setup, attr.dstzw_dx, x, perspective, flip, st, s_offset);
|
||||
|
||||
uint tile0 = uint(setup_tile) & 7u;
|
||||
uint tile_info_index0 = uint(state_indices.elems[primitive_index].tile_infos[tile0]);
|
||||
TileInfo tile_info0 = load_tile_info(tile_info_index0);
|
||||
#ifdef RASTERIZER_SPEC_CONSTANT
|
||||
if ((STATIC_STATE_FLAGS & RASTERIZATION_USE_STATIC_TEXTURE_SIZE_FORMAT_BIT) != 0)
|
||||
{
|
||||
tile_info0.fmt = u8(TEX_FMT);
|
||||
tile_info0.size = u8(TEX_SIZE);
|
||||
}
|
||||
#endif
|
||||
int texel0 = sample_texture_copy(tile_info0, tmem_instance_index, st, s_offset, tlut, tlut_type);
|
||||
shaded.z_dith = texel0;
|
||||
shaded.coverage_count = U8_C(COVERAGE_COPY_BIT);
|
||||
|
||||
if (alpha_test && global_constants.fb_info.fb_size == 2 && (texel0 & 1) == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (fill_en)
|
||||
{
|
||||
shaded.coverage_count = U8_C(COVERAGE_FILL_BIT);
|
||||
return x >= span_setup.start_x && x <= span_setup.end_x;
|
||||
}
|
||||
|
||||
int coverage = compute_coverage(span_setup.xleft, span_setup.xright, x);
|
||||
|
||||
// There is no way we can gain coverage here.
|
||||
// Reject work as fast as possible.
|
||||
if (coverage == 0)
|
||||
return false;
|
||||
|
||||
int coverage_count = bitCount(coverage);
|
||||
|
||||
// If we're not using AA, only the first coverage bit is relevant.
|
||||
if (!aa_enable && (coverage & 1) == 0)
|
||||
return false;
|
||||
|
||||
DerivedSetup derived = load_derived_setup(primitive_index);
|
||||
|
||||
int dx = x - span_setup.interpolation_base_x;
|
||||
int interpolation_direction = flip ? 1 : -1;
|
||||
|
||||
// Interpolate attributes.
|
||||
u8x4 shade = interpolate_rgba(span_setup.rgba, attr.drgba_dx, attr.drgba_dy,
|
||||
dx, coverage);
|
||||
|
||||
ivec2 st, st_dx, st_dy;
|
||||
int z;
|
||||
bool perspective_overflow = false;
|
||||
|
||||
int tex_interpolation_direction = interpolation_direction;
|
||||
if (SCALING_FACTOR > 1 && uses_lod)
|
||||
if ((setup_flags & TRIANGLE_SETUP_NATIVE_LOD_BIT) != 0)
|
||||
tex_interpolation_direction *= SCALING_FACTOR;
|
||||
|
||||
interpolate_stz(span_setup.stzw, attr.dstzw_dx, attr.dstzw_dy, dx, coverage, perspective, uses_lod,
|
||||
tex_interpolation_direction, st, st_dx, st_dy, z, perspective_overflow);
|
||||
|
||||
// Sample textures.
|
||||
uint tile0 = uint(setup_tile) & 7u;
|
||||
uint tile1 = (tile0 + 1) & 7u;
|
||||
uint max_level = uint(setup_tile) >> 3u;
|
||||
int min_lod = derived.min_lod;
|
||||
|
||||
i16 lod_frac;
|
||||
if (uses_lod)
|
||||
{
|
||||
compute_lod_2cycle(tile0, tile1, lod_frac, max_level, min_lod, st, st_dx, st_dy, perspective_overflow,
|
||||
tex_lod_en, sharpen_lod_en, detail_lod_en);
|
||||
}
|
||||
|
||||
i16x4 texel0, texel1;
|
||||
|
||||
if (uses_texel0)
|
||||
{
|
||||
uint tile_info_index0 = uint(state_indices.elems[primitive_index].tile_infos[tile0]);
|
||||
TileInfo tile_info0 = load_tile_info(tile_info_index0);
|
||||
#ifdef RASTERIZER_SPEC_CONSTANT
|
||||
if ((STATIC_STATE_FLAGS & RASTERIZATION_USE_STATIC_TEXTURE_SIZE_FORMAT_BIT) != 0)
|
||||
{
|
||||
tile_info0.fmt = u8(TEX_FMT);
|
||||
tile_info0.size = u8(TEX_SIZE);
|
||||
}
|
||||
#endif
|
||||
texel0 = sample_texture(tile_info0, tmem_instance_index, st, tlut, tlut_type,
|
||||
sample_quad, mid_texel, false, bilerp0, derived.factors, i16x4(0));
|
||||
}
|
||||
|
||||
// A very awkward mechanism where we peek into the next pixel, or in some cases, the next scanline's first pixel.
|
||||
if (uses_pipelined_texel1)
|
||||
{
|
||||
bool valid_line = uint(span_setups.elems[SCALING_FACTOR * span_offsets.offset + (y - SCALING_FACTOR * span_offsets.ylo + 1)].valid_line) != 0u;
|
||||
bool long_span = span_setup.lodlength >= 8;
|
||||
bool end_span = x == (flip ? span_setup.end_x : span_setup.start_x);
|
||||
|
||||
if (end_span && long_span && valid_line)
|
||||
{
|
||||
ivec3 stw = span_setups.elems[SCALING_FACTOR * span_offsets.offset + (y - SCALING_FACTOR * span_offsets.ylo + 1)].stzw.xyw >> 16;
|
||||
if (perspective)
|
||||
{
|
||||
bool st_overflow;
|
||||
st = perspective_divide(stw, st_overflow);
|
||||
}
|
||||
else
|
||||
st = no_perspective_divide(stw);
|
||||
}
|
||||
else
|
||||
st = interpolate_st_single(span_setup.stzw, attr.dstzw_dx, dx + interpolation_direction * SCALING_FACTOR, perspective);
|
||||
|
||||
tile1 = tile0;
|
||||
uses_texel1 = true;
|
||||
}
|
||||
|
||||
if (uses_texel1)
|
||||
{
|
||||
if (convert_one && !bilerp1)
|
||||
{
|
||||
texel1 = texture_convert_factors(texel0, derived.factors);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint tile_info_index1 = uint(state_indices.elems[primitive_index].tile_infos[tile1]);
|
||||
TileInfo tile_info1 = load_tile_info(tile_info_index1);
|
||||
#ifdef RASTERIZER_SPEC_CONSTANT
|
||||
if ((STATIC_STATE_FLAGS & RASTERIZATION_USE_STATIC_TEXTURE_SIZE_FORMAT_BIT) != 0)
|
||||
{
|
||||
tile_info1.fmt = u8(TEX_FMT);
|
||||
tile_info1.size = u8(TEX_SIZE);
|
||||
}
|
||||
#endif
|
||||
texel1 = sample_texture(tile_info1, tmem_instance_index, st, tlut, tlut_type, sample_quad, mid_texel,
|
||||
convert_one, bilerp1, derived.factors, texel0);
|
||||
}
|
||||
}
|
||||
|
||||
int rgb_dith, alpha_dith;
|
||||
dither_coefficients(x, y >> int(interlace_en), static_state_dither >> 2, static_state_dither & 3, rgb_dith, alpha_dith);
|
||||
|
||||
// Run combiner.
|
||||
u8x4 combined;
|
||||
u8 alpha_reference;
|
||||
if (multi_cycle)
|
||||
{
|
||||
CombinerInputs combined_inputs =
|
||||
CombinerInputs(derived.constant_muladd0, derived.constant_mulsub0, derived.constant_mul0, derived.constant_add0,
|
||||
shade, u8x4(0), texel0, texel1, lod_frac, noise_get_combiner());
|
||||
|
||||
combined_inputs.combined = combiner_cycle0(combined_inputs,
|
||||
combiner_inputs_rgb0,
|
||||
combiner_inputs_alpha0,
|
||||
alpha_dith, coverage_count, cvg_times_alpha, alpha_cvg_select,
|
||||
alpha_test, alpha_reference);
|
||||
|
||||
combined_inputs.constant_muladd = derived.constant_muladd1;
|
||||
combined_inputs.constant_mulsub = derived.constant_mulsub1;
|
||||
combined_inputs.constant_mul = derived.constant_mul1;
|
||||
combined_inputs.constant_add = derived.constant_add1;
|
||||
|
||||
// Pipelining, texel1 is promoted to texel0 in cycle1.
|
||||
// I don't think hardware ever intended for you to access texels in second cycle due to this nature.
|
||||
i16x4 tmp_texel = combined_inputs.texel0;
|
||||
combined_inputs.texel0 = combined_inputs.texel1;
|
||||
// Following the pipelining, texel1 should become texel0 of next pixel,
|
||||
// but let's not go there ...
|
||||
combined_inputs.texel1 = tmp_texel;
|
||||
|
||||
// Resample the noise at some arbitrary other offset.
|
||||
// This only matters if both noise combiner inputs take noise (very weird).
|
||||
if ((static_state_flags & RASTERIZATION_NEED_NOISE_DUAL_BIT) != 0)
|
||||
{
|
||||
reseed_noise(x + 1023, y + 7, primitive_index + global_constants.fb_info.base_primitive_index + 11);
|
||||
combined_inputs.noise = noise_get_combiner();
|
||||
}
|
||||
|
||||
combined = u8x4(combiner_cycle1(combined_inputs,
|
||||
combiner_inputs_rgb1,
|
||||
combiner_inputs_alpha1,
|
||||
alpha_dith, coverage_count, cvg_times_alpha, alpha_cvg_select));
|
||||
}
|
||||
else
|
||||
{
|
||||
CombinerInputs combined_inputs =
|
||||
CombinerInputs(derived.constant_muladd1, derived.constant_mulsub1, derived.constant_mul1, derived.constant_add1,
|
||||
shade, u8x4(0), texel0, texel1, lod_frac, noise_get_combiner());
|
||||
|
||||
combined = u8x4(combiner_cycle1(combined_inputs,
|
||||
combiner_inputs_rgb1,
|
||||
combiner_inputs_alpha1,
|
||||
alpha_dith, coverage_count, cvg_times_alpha, alpha_cvg_select));
|
||||
|
||||
alpha_reference = combined.a;
|
||||
}
|
||||
|
||||
// After combiner, color can be modified to 0 through alpha-to-cvg, so check for potential write_enable here.
|
||||
// If we're not using AA, the first coverage bit is used instead, coverage count is ignored.
|
||||
if (aa_enable && coverage_count == 0)
|
||||
return false;
|
||||
|
||||
if (alpha_test)
|
||||
{
|
||||
u8 alpha_threshold;
|
||||
if (alpha_test_dither)
|
||||
alpha_threshold = noise_get_blend_threshold();
|
||||
else
|
||||
alpha_threshold = derived.blend_color.a;
|
||||
|
||||
if (alpha_reference < alpha_threshold)
|
||||
return false;
|
||||
}
|
||||
|
||||
shaded.combined = combined;
|
||||
shaded.z_dith = (z << 9) | rgb_dith;
|
||||
shaded.coverage_count = u8(coverage_count);
|
||||
// Shade alpha needs to be passed separately since it might affect the blending stage.
|
||||
shaded.shade_alpha = u8(min(shade.a + alpha_dith, 0xff));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"include": [ "../../Granite/assets/shaders/inc" ],
|
||||
"shaders": [
|
||||
{
|
||||
"name": "tmem_update",
|
||||
"compute": true,
|
||||
"path": "tmem_update.comp"
|
||||
},
|
||||
{
|
||||
|
||||
"name": "span_setup",
|
||||
"compute": true,
|
||||
"path": "span_setup.comp"
|
||||
},
|
||||
{
|
||||
"name": "clear_indirect_buffer",
|
||||
"compute": true,
|
||||
"path": "clear_indirect_buffer.comp"
|
||||
},
|
||||
{
|
||||
"name": "tile_binning_combined",
|
||||
"compute": true,
|
||||
"path": "tile_binning_combined.comp",
|
||||
"variants": [
|
||||
{ "define": "SUBGROUP", "count": 2, "resolve": true },
|
||||
{ "define": "UBERSHADER", "count": 2, "resolve": true },
|
||||
{ "define": "SMALL_TYPES", "count": 2, "resolve": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ubershader",
|
||||
"path": "ubershader.comp",
|
||||
"compute": true,
|
||||
"variants": [
|
||||
{ "define": "SUBGROUP", "count": 2, "resolve": true },
|
||||
{ "define": "SMALL_TYPES", "count": 2, "resolve": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "depth_blend",
|
||||
"path": "depth_blend.comp",
|
||||
"compute": true,
|
||||
"variants": [
|
||||
{ "define": "SUBGROUP", "count": 2, "resolve": true },
|
||||
{ "define": "SMALL_TYPES", "count": 2, "resolve": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rasterizer",
|
||||
"path": "rasterizer.comp",
|
||||
"compute": true,
|
||||
"variants": [
|
||||
{ "define": "SMALL_TYPES", "count": 2, "resolve": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fullscreen",
|
||||
"path": "fullscreen.vert"
|
||||
},
|
||||
{
|
||||
"name": "vi_scale",
|
||||
"path": "vi_scale.frag"
|
||||
},
|
||||
{
|
||||
"name": "vi_divot",
|
||||
"path": "vi_divot.frag",
|
||||
"variants": [
|
||||
{ "define": "FETCH_BUG", "count": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vi_fetch",
|
||||
"path": "vi_fetch.frag",
|
||||
"variants": [
|
||||
{ "define": "FETCH_BUG", "count": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vi_blend_fields",
|
||||
"path": "vi_blend_fields.frag"
|
||||
},
|
||||
{
|
||||
"name": "extract_vram",
|
||||
"path": "extract_vram.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "masked_rdram_resolve",
|
||||
"path": "masked_rdram_resolve.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "clear_write_mask",
|
||||
"path": "clear_write_mask.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "update_upscaled_domain_post",
|
||||
"path": "update_upscaled_domain_post.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "update_upscaled_domain_pre",
|
||||
"path": "update_upscaled_domain_pre.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "update_upscaled_domain_resolve",
|
||||
"path": "update_upscaled_domain_resolve.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "clear_super_sampled_write_mask",
|
||||
"path": "clear_super_sampled_write_mask.comp",
|
||||
"compute": true
|
||||
},
|
||||
{
|
||||
"name": "vi_deinterlace_vert",
|
||||
"path": "vi_deinterlace.vert"
|
||||
},
|
||||
{
|
||||
"name": "vi_deinterlace_frag",
|
||||
"path": "vi_deinterlace.frag"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
// Utility header to smooth over the difference between
|
||||
// 8/16-bit integer arithmetic vs. just 8/16-bit storage.
|
||||
|
||||
#ifndef SMALL_INTEGERS_H_
|
||||
#define SMALL_INTEGERS_H_
|
||||
|
||||
#extension GL_EXT_shader_16bit_storage : require
|
||||
#extension GL_EXT_shader_8bit_storage : require
|
||||
|
||||
#if SMALL_TYPES
|
||||
#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require
|
||||
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : require
|
||||
|
||||
#define mem_u8 uint8_t
|
||||
#define mem_u16 uint16_t
|
||||
#define mem_u8x2 u8vec2
|
||||
#define mem_u16x2 u16vec2
|
||||
#define mem_u8x3 u8vec3
|
||||
#define mem_u16x3 u16vec3
|
||||
#define mem_u8x4 u8vec4
|
||||
#define mem_u16x4 u16vec4
|
||||
|
||||
#define mem_i8 int8_t
|
||||
#define mem_i16 int16_t
|
||||
#define mem_i8x2 i8vec2
|
||||
#define mem_i16x2 i16vec2
|
||||
#define mem_i8x3 i8vec3
|
||||
#define mem_i16x3 i16vec3
|
||||
#define mem_i8x4 i8vec4
|
||||
#define mem_i16x4 i16vec4
|
||||
|
||||
#define u8 uint8_t
|
||||
#define u16 uint16_t
|
||||
#define u8x2 u8vec2
|
||||
#define u16x2 u16vec2
|
||||
#define u8x3 u8vec3
|
||||
#define u16x3 u16vec3
|
||||
#define u8x4 u8vec4
|
||||
#define u16x4 u16vec4
|
||||
|
||||
#define i8 int8_t
|
||||
#define i16 int16_t
|
||||
#define i8x2 i8vec2
|
||||
#define i16x2 i16vec2
|
||||
#define i8x3 i8vec3
|
||||
#define i16x3 i16vec3
|
||||
#define i8x4 i8vec4
|
||||
#define i16x4 i16vec4
|
||||
|
||||
#define U8_C(x) uint8_t(x)
|
||||
#define I8_C(x) int8_t(x)
|
||||
#define U16_C(x) uint16_t(x)
|
||||
#define I16_C(x) int16_t(x)
|
||||
|
||||
#else
|
||||
|
||||
#define mem_u8 uint8_t
|
||||
#define mem_u16 uint16_t
|
||||
#define mem_u8x2 u8vec2
|
||||
#define mem_u16x2 u16vec2
|
||||
#define mem_u8x3 u8vec3
|
||||
#define mem_u16x3 u16vec3
|
||||
#define mem_u8x4 u8vec4
|
||||
#define mem_u16x4 u16vec4
|
||||
|
||||
#define mem_i8 int8_t
|
||||
#define mem_i16 int16_t
|
||||
#define mem_i8x2 i8vec2
|
||||
#define mem_i16x2 i16vec2
|
||||
#define mem_i8x3 i8vec3
|
||||
#define mem_i16x3 i16vec3
|
||||
#define mem_i8x4 i8vec4
|
||||
#define mem_i16x4 i16vec4
|
||||
|
||||
#define u8 int
|
||||
#define u16 int
|
||||
#define u8x2 ivec2
|
||||
#define u16x2 ivec2
|
||||
#define u8x3 ivec3
|
||||
#define u16x3 ivec3
|
||||
#define u8x4 ivec4
|
||||
#define u16x4 ivec4
|
||||
|
||||
#define i8 int
|
||||
#define i16 int
|
||||
#define i8x2 ivec2
|
||||
#define i16x2 ivec2
|
||||
#define i8x3 ivec3
|
||||
#define i16x3 ivec3
|
||||
#define i8x4 ivec4
|
||||
#define i16x4 ivec4
|
||||
|
||||
#define U8_C(x) int(x)
|
||||
#define I8_C(x) int(x)
|
||||
#define U16_C(x) int(x)
|
||||
#define I16_C(x) int(x)
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,227 @@
|
||||
#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"
|
||||
#include "debug.h"
|
||||
|
||||
layout(local_size_x_id = 0) in;
|
||||
layout(constant_id = 1) const int SCALING_LOG2 = 0;
|
||||
const int SCALING_FACTOR = 1 << SCALING_LOG2;
|
||||
#include "data_structures.h"
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer TriangleSetupBuffer
|
||||
{
|
||||
TriangleSetupMem elems[];
|
||||
} triangle_setup;
|
||||
#include "load_triangle_setup.h"
|
||||
|
||||
layout(std430, set = 0, binding = 1) readonly buffer AttributeSetupBuffer
|
||||
{
|
||||
AttributeSetupMem elems[];
|
||||
} attribute_setup;
|
||||
#include "load_attribute_setup.h"
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer ScissorStateBuffer
|
||||
{
|
||||
ScissorStateMem elems[];
|
||||
} scissor_state;
|
||||
#include "load_scissor_state.h"
|
||||
|
||||
layout(std430, set = 0, binding = 3) writeonly buffer SpanSetups
|
||||
{
|
||||
SpanSetupMem elems[];
|
||||
} span_setups;
|
||||
#include "store_span_setup.h"
|
||||
|
||||
layout(set = 1, binding = 0) uniform utextureBuffer uInterpolationJobs;
|
||||
|
||||
const int SUBPIXELS = 4;
|
||||
const int SUBPIXELS_LOG2 = 2;
|
||||
|
||||
// Convert a 16.16 signed value to 16.3. We have 8 subpixels in X direction after snapping.
|
||||
ivec4 quantize_x(ivec4 x)
|
||||
{
|
||||
ivec4 sticky = ivec4(notEqual(x & 0xfff, ivec4(0)));
|
||||
ivec4 snapped = ivec4((x >> 12) | sticky);
|
||||
return snapped;
|
||||
}
|
||||
|
||||
int min4(ivec4 v)
|
||||
{
|
||||
ivec2 v2 = min(v.xy, v.zw);
|
||||
return min(v2.x, v2.y);
|
||||
}
|
||||
|
||||
int max4(ivec4 v)
|
||||
{
|
||||
ivec2 v2 = max(v.xy, v.zw);
|
||||
return max(v2.x, v2.y);
|
||||
}
|
||||
|
||||
ivec4 interpolate_snapped(ivec4 dvalue, int dy)
|
||||
{
|
||||
int dy_shifted = dy >> SCALING_LOG2;
|
||||
int dy_masked = dy & (SCALING_FACTOR - 1);
|
||||
return dy_shifted * dvalue + dy_masked * (dvalue >> SCALING_LOG2);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec3 job_indices = ivec3(texelFetch(uInterpolationJobs, int(gl_WorkGroupID.x)).xyz);
|
||||
int primitive_index = job_indices.x;
|
||||
int base_y = job_indices.y * SCALING_FACTOR;
|
||||
int max_y = job_indices.z * SCALING_FACTOR + (SCALING_FACTOR - 1);
|
||||
int y = base_y + int(gl_LocalInvocationIndex);
|
||||
if (y > max_y)
|
||||
return;
|
||||
|
||||
TriangleSetup setup = load_triangle_setup(primitive_index);
|
||||
AttributeSetup attr = load_attribute_setup(primitive_index);
|
||||
ScissorState scissor = load_scissor_state(primitive_index);
|
||||
|
||||
bool flip = (setup.flags & TRIANGLE_SETUP_FLIP_BIT) != 0;
|
||||
bool interlace_en = (setup.flags & TRIANGLE_SETUP_INTERLACE_FIELD_BIT) != 0;
|
||||
bool keep_odd_field = (setup.flags & TRIANGLE_SETUP_INTERLACE_KEEP_ODD_BIT) != 0;
|
||||
|
||||
SpanSetup span_setup;
|
||||
|
||||
// Interpolate RGBA, STZW to their scanline.
|
||||
{
|
||||
bool do_offset = (setup.flags & TRIANGLE_SETUP_DO_OFFSET_BIT) != 0;
|
||||
bool skip_xfrac = (setup.flags & TRIANGLE_SETUP_SKIP_XFRAC_BIT) != 0;
|
||||
int y_interpolation_base = int(setup.yh) >> 2;
|
||||
y_interpolation_base *= SCALING_FACTOR;
|
||||
|
||||
// For high-resolution interpolation, make sure we snap interpolation correctly at whole pixels,
|
||||
// and quantize derivatives in-between pixels.
|
||||
int dy = y - y_interpolation_base;
|
||||
|
||||
int xh = setup.xh * SCALING_FACTOR + dy * (setup.dxhdy << 2);
|
||||
|
||||
ivec4 drgba_diff = ivec4(0);
|
||||
ivec4 dstzw_diff = ivec4(0);
|
||||
|
||||
// In do_offset mode, varyings are latched at last subpixel line instead of first (for some reason).
|
||||
if (do_offset)
|
||||
{
|
||||
xh += (SCALING_FACTOR * 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);
|
||||
|
||||
ivec4 dstzw_deh = attr.dstzw_de & ~0x1ff;
|
||||
ivec4 dstzw_dyh = attr.dstzw_dy & ~0x1ff;
|
||||
dstzw_diff = dstzw_deh - (dstzw_deh >> 2) - dstzw_dyh + (dstzw_dyh >> 2);
|
||||
}
|
||||
|
||||
int base_x = xh >> 15;
|
||||
int xfrac = skip_xfrac ? 0 : ((xh >> 7) & 0xff);
|
||||
|
||||
ivec4 rgba = attr.rgba + interpolate_snapped(attr.drgba_de, dy);
|
||||
rgba = ((rgba & ~0x1ff) + drgba_diff - interpolate_snapped((attr.drgba_dx >> 8) & ~1, xfrac)) & ~0x3ff;
|
||||
|
||||
ivec4 stzw = attr.stzw + interpolate_snapped(attr.dstzw_de, dy);
|
||||
stzw = ((stzw & ~0x1ff) + dstzw_diff - interpolate_snapped((attr.dstzw_dx >> 8) & ~1, xfrac)) & ~0x3ff;
|
||||
|
||||
span_setup.rgba = rgba;
|
||||
span_setup.stzw = stzw;
|
||||
span_setup.interpolation_base_x = base_x;
|
||||
}
|
||||
|
||||
// Check Y dimension.
|
||||
int yh_interpolation_base = int(setup.yh) & ~(SUBPIXELS - 1);
|
||||
int ym_interpolation_base = int(setup.ym);
|
||||
yh_interpolation_base *= SCALING_FACTOR;
|
||||
ym_interpolation_base *= SCALING_FACTOR;
|
||||
|
||||
int y_sub = int(y * SUBPIXELS);
|
||||
ivec4 y_subs = y_sub + ivec4(0, 1, 2, 3);
|
||||
int ylo = max(setup.yh, scissor.ylo) * SCALING_FACTOR;
|
||||
int yhi = min(setup.yl, scissor.yhi) * SCALING_FACTOR;
|
||||
|
||||
bvec4 clip_lo_y = lessThan(y_subs, ivec4(ylo));
|
||||
bvec4 clip_hi_y = greaterThanEqual(y_subs, ivec4(yhi));
|
||||
uvec4 clip_y = uvec4(clip_lo_y) | uvec4(clip_hi_y);
|
||||
|
||||
// Interpolate X at all 4 Y-subpixels.
|
||||
ivec4 xh = setup.xh * SCALING_FACTOR + (y_subs - yh_interpolation_base) * setup.dxhdy;
|
||||
ivec4 xm = setup.xm * SCALING_FACTOR + (y_subs - yh_interpolation_base) * setup.dxmdy;
|
||||
ivec4 xl = setup.xl * SCALING_FACTOR + (y_subs - ym_interpolation_base) * setup.dxldy;
|
||||
xl = mix(xl, xm, lessThan(y_subs, ivec4(SCALING_FACTOR * setup.ym)));
|
||||
|
||||
// If we have overflows, we can become sensitive to this in invalid_line check, where
|
||||
// checks that should pass fail, and vice versa.
|
||||
// Note that we shaved off one bit in triangle setup for upscaling purposes,
|
||||
// so this should be 28 bits normally.
|
||||
xl = bitfieldExtract(xl, 0, 27 + SCALING_LOG2);
|
||||
xh = bitfieldExtract(xh, 0, 27 + SCALING_LOG2);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bvec4 invalid_line = greaterThan(xleft >> 1, xright >> 1);
|
||||
|
||||
ivec4 lo_scissor = ivec4(SCALING_FACTOR * (scissor.xlo << 1));
|
||||
ivec4 hi_scissor = ivec4(SCALING_FACTOR * (scissor.xhi << 1));
|
||||
|
||||
bool all_over = all(greaterThanEqual(min(xleft, xright), hi_scissor));
|
||||
bool all_under = all(lessThan(max(xleft, xright), lo_scissor));
|
||||
|
||||
xleft = max(xleft, lo_scissor);
|
||||
xleft = min(xleft, hi_scissor);
|
||||
xright = max(xright, lo_scissor);
|
||||
xright = min(xright, hi_scissor);
|
||||
|
||||
invalid_line = bvec4(uvec4(invalid_line) | clip_y);
|
||||
|
||||
xleft = mix(xleft, ivec4(0xffff), invalid_line);
|
||||
xright = mix(xright, ivec4(0), invalid_line);
|
||||
|
||||
int start_x = min4(xleft) >> 3;
|
||||
int end_x = max4(xright) >> 3;
|
||||
|
||||
span_setup.xleft = xleft;
|
||||
span_setup.xright = xright;
|
||||
span_setup.start_x = start_x;
|
||||
span_setup.end_x = end_x;
|
||||
span_setup.valid_line = int(!all(invalid_line) && !all_over && !all_under);
|
||||
|
||||
if (interlace_en)
|
||||
if (((y >> SCALING_LOG2) & 1) != int(keep_odd_field))
|
||||
span_setup.valid_line = U16_C(0);
|
||||
|
||||
span_setup.lodlength = int(flip ? (end_x - span_setup.interpolation_base_x) : (span_setup.interpolation_base_x - start_x));
|
||||
store_span_setup(gl_GlobalInvocationID.x, span_setup);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* 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 STORE_SPAN_SETUP_H_
|
||||
#define STORE_SPAN_SETUP_H_
|
||||
|
||||
void store_span_setup(uint index, SpanSetup setup)
|
||||
{
|
||||
#if SMALL_TYPES
|
||||
span_setups.elems[index] = setup;
|
||||
#else
|
||||
span_setups.elems[index].rgba = setup.rgba;
|
||||
span_setups.elems[index].stzw = setup.stzw;
|
||||
span_setups.elems[index].xleft = mem_u16x4(uvec4(setup.xleft));
|
||||
span_setups.elems[index].xright = mem_u16x4(uvec4(setup.xright));
|
||||
span_setups.elems[index].interpolation_base_x = setup.interpolation_base_x;
|
||||
span_setups.elems[index].start_x = setup.start_x;
|
||||
span_setups.elems[index].end_x = setup.end_x;
|
||||
span_setups.elems[index].lodlength = mem_i16(setup.lodlength);
|
||||
span_setups.elems[index].valid_line = mem_u16(setup.valid_line);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
+270
@@ -0,0 +1,270 @@
|
||||
#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.
|
||||
*/
|
||||
// Consumes result from tile_binning_prepass.comp, bins at a finer resolution (8x8 or 16x16 blocks).
|
||||
#include "small_types.h"
|
||||
|
||||
#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
|
||||
layout(local_size_x_id = 0) in;
|
||||
#else
|
||||
// Reasonable default. For AMD (64 threads), subgroups are definitely supported, so this won't be hit.
|
||||
layout(local_size_x = 32) in;
|
||||
#endif
|
||||
|
||||
#include "debug.h"
|
||||
#include "data_structures.h"
|
||||
#include "binning.h"
|
||||
|
||||
layout(constant_id = 1) const int TILE_WIDTH = 8;
|
||||
layout(constant_id = 2) const int TILE_HEIGHT = 8;
|
||||
layout(constant_id = 3) const int MAX_PRIMITIVES = 256;
|
||||
layout(constant_id = 4) const int MAX_WIDTH = 1024;
|
||||
layout(constant_id = 5) const int TILE_INSTANCE_STRIDE = 0x8000;
|
||||
layout(constant_id = 6) const int SCALE_FACTOR = 1;
|
||||
|
||||
const int TILE_BINNING_STRIDE = MAX_PRIMITIVES / 32;
|
||||
const int MAX_TILES_X = MAX_WIDTH / TILE_WIDTH;
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer TriangleSetupBuffer
|
||||
{
|
||||
TriangleSetupMem elems[];
|
||||
} triangle_setup;
|
||||
#include "load_triangle_setup.h"
|
||||
|
||||
layout(set = 0, binding = 1, std430) readonly buffer ScissorStateBuffer
|
||||
{
|
||||
ScissorStateMem elems[];
|
||||
} scissor_state;
|
||||
#include "load_scissor_state.h"
|
||||
|
||||
layout(set = 0, binding = 2, std430) readonly buffer StateIndicesBuffer
|
||||
{
|
||||
InstanceIndicesMem elems[];
|
||||
} state_indices;
|
||||
|
||||
layout(std430, set = 0, binding = 3) writeonly buffer TileBitmask
|
||||
{
|
||||
uint binned_bitmask[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 4) writeonly buffer TileBitmaskCoarse
|
||||
{
|
||||
uint binned_bitmask_coarse[];
|
||||
};
|
||||
|
||||
#if !UBERSHADER
|
||||
layout(std430, set = 0, binding = 5) writeonly buffer TileInstanceOffset
|
||||
{
|
||||
uint elems[];
|
||||
} tile_instance_offsets;
|
||||
|
||||
layout(std430, set = 0, binding = 6) buffer IndirectBuffer
|
||||
{
|
||||
uvec4 elems[];
|
||||
} indirect_counts;
|
||||
|
||||
// This can actually be uint16_t, but AMD doesn't seem to support loading uint16_t in SMEM unit,
|
||||
// the memory traffic for this data structure is not relevant anyways.
|
||||
struct TileRasterWork
|
||||
{
|
||||
uint tile_x, tile_y;
|
||||
uint tile_instance;
|
||||
uint primitive;
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 7) writeonly buffer WorkList
|
||||
{
|
||||
uvec4 elems[];
|
||||
} tile_raster_work;
|
||||
#endif
|
||||
|
||||
#if !UBERSHADER
|
||||
uint allocate_work_offset(uint variant_index)
|
||||
{
|
||||
#if !SUBGROUP
|
||||
return atomicAdd(indirect_counts.elems[variant_index].x, 1u);
|
||||
#else
|
||||
// Merge atomic operations. Compiler would normally do this,
|
||||
// but it might not have figured out that variant_index is uniform.
|
||||
uvec4 active_mask = subgroupBallot(true);
|
||||
uint count = subgroupBallotBitCount(active_mask);
|
||||
uint work_offset = 0u;
|
||||
if (subgroupElect())
|
||||
work_offset = atomicAdd(indirect_counts.elems[variant_index].x, count);
|
||||
work_offset = subgroupBroadcastFirst(work_offset);
|
||||
work_offset += subgroupBallotExclusiveBitCount(active_mask);
|
||||
return work_offset;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
layout(push_constant, std430) uniform Registers
|
||||
{
|
||||
uvec2 resolution;
|
||||
int primitive_count;
|
||||
} fb_info;
|
||||
|
||||
#if !SUBGROUP
|
||||
shared uint merged_mask_shared;
|
||||
#endif
|
||||
|
||||
void main()
|
||||
{
|
||||
int group_index = int(gl_WorkGroupID.x);
|
||||
ivec2 meta_tile = ivec2(gl_WorkGroupID.yz);
|
||||
|
||||
const int TILES_X = 8;
|
||||
const int TILES_Y = int(gl_WorkGroupSize.x) >> 3;
|
||||
|
||||
#if SUBGROUP
|
||||
// Spec is unclear how gl_LocalInvocationIndex is mapped to gl_SubgroupInvocationID, so synthesize our own.
|
||||
// We know the subgroups are fully occupied with VK_EXT_subgroup_size_control already.
|
||||
int local_index = int(gl_SubgroupInvocationID);
|
||||
int SUBGROUP_TILES_Y = int(gl_SubgroupSize) >> 3;
|
||||
#else
|
||||
int local_index = int(gl_LocalInvocationIndex);
|
||||
#endif
|
||||
|
||||
int inner_tile_x = local_index & 7;
|
||||
int inner_tile_y = local_index >> 3;
|
||||
#if SUBGROUP
|
||||
inner_tile_y += SUBGROUP_TILES_Y * int(gl_SubgroupID);
|
||||
#endif
|
||||
ivec2 tile = meta_tile * ivec2(TILES_X, TILES_Y) + ivec2(inner_tile_x, inner_tile_y);
|
||||
|
||||
int linear_tile = tile.y * MAX_TILES_X + tile.x;
|
||||
|
||||
ivec2 base_coord_meta = meta_tile * ivec2(TILE_WIDTH * TILES_X, TILE_HEIGHT * TILES_Y);
|
||||
#if SUBGROUP
|
||||
base_coord_meta.y += SUBGROUP_TILES_Y * TILE_HEIGHT * int(gl_SubgroupID);
|
||||
ivec2 end_coord_meta = min(base_coord_meta + ivec2(TILE_WIDTH * TILES_X, TILE_HEIGHT * SUBGROUP_TILES_Y), ivec2(fb_info.resolution)) - 1;
|
||||
#else
|
||||
ivec2 end_coord_meta = min(base_coord_meta + ivec2(TILE_WIDTH * TILES_X, TILE_HEIGHT * TILES_Y), ivec2(fb_info.resolution)) - 1;
|
||||
#endif
|
||||
|
||||
ivec2 base_coord = tile * ivec2(TILE_WIDTH, TILE_HEIGHT);
|
||||
ivec2 end_coord = min(base_coord + ivec2(TILE_WIDTH, TILE_HEIGHT), ivec2(fb_info.resolution)) - 1;
|
||||
|
||||
int primitive_count = fb_info.primitive_count;
|
||||
|
||||
#if !SUBGROUP
|
||||
if (local_index == 0)
|
||||
merged_mask_shared = 0u;
|
||||
barrier();
|
||||
#endif
|
||||
|
||||
bool binned = false;
|
||||
if (local_index < 32)
|
||||
{
|
||||
uint primitive_index = group_index * 32 + local_index;
|
||||
if (primitive_index < primitive_count)
|
||||
{
|
||||
ScissorState scissor = load_scissor_state(primitive_index);
|
||||
TriangleSetup setup = load_triangle_setup(primitive_index);
|
||||
binned = bin_primitive(setup, base_coord_meta, end_coord_meta, SCALE_FACTOR, scissor);
|
||||
}
|
||||
}
|
||||
|
||||
#if SUBGROUP
|
||||
uint merged_mask = subgroupBallot(binned).x;
|
||||
#else
|
||||
if (binned)
|
||||
atomicOr(merged_mask_shared, 1u << local_index);
|
||||
barrier();
|
||||
uint merged_mask = merged_mask_shared;
|
||||
#endif
|
||||
|
||||
uint binned_mask = 0u;
|
||||
while (merged_mask != 0u)
|
||||
{
|
||||
int bit = findLSB(merged_mask);
|
||||
merged_mask &= ~(1u << bit);
|
||||
uint primitive_index = group_index * 32 + bit;
|
||||
ScissorState scissor = load_scissor_state(primitive_index);
|
||||
TriangleSetup setup = load_triangle_setup(primitive_index);
|
||||
if (bin_primitive(setup, base_coord, end_coord, SCALE_FACTOR, scissor))
|
||||
binned_mask |= 1u << bit;
|
||||
}
|
||||
|
||||
binned_bitmask[linear_tile * TILE_BINNING_STRIDE + group_index] = binned_mask;
|
||||
if (binned_mask != 0u)
|
||||
atomicOr(binned_bitmask_coarse[linear_tile], 1u << group_index);
|
||||
else
|
||||
atomicAnd(binned_bitmask_coarse[linear_tile], ~(1u << group_index));
|
||||
|
||||
#if SUBGROUP
|
||||
#if !UBERSHADER
|
||||
uint bit_count = uint(bitCount(binned_mask));
|
||||
uint instance_offset = 0u;
|
||||
if (subgroupAny(bit_count != 0u))
|
||||
{
|
||||
// Allocate tile instance space for all threads in subgroup in one go.
|
||||
uint total_bit_count = subgroupAdd(bit_count);
|
||||
|
||||
if (subgroupElect())
|
||||
if (total_bit_count != 0u)
|
||||
instance_offset = atomicAdd(indirect_counts.elems[0].w, total_bit_count);
|
||||
|
||||
instance_offset = subgroupBroadcastFirst(instance_offset);
|
||||
instance_offset += subgroupInclusiveAdd(bit_count) - bit_count;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
#if !UBERSHADER
|
||||
uint bit_count = uint(bitCount(binned_mask));
|
||||
uint instance_offset = 0u;
|
||||
if (bit_count != 0u)
|
||||
instance_offset = atomicAdd(indirect_counts.elems[0].w, bit_count);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !UBERSHADER
|
||||
if (bit_count != 0u)
|
||||
tile_instance_offsets.elems[linear_tile * TILE_BINNING_STRIDE + group_index] = instance_offset;
|
||||
|
||||
#if SUBGROUP
|
||||
uint variant_mask = subgroupOr(binned_mask);
|
||||
#else
|
||||
uint variant_mask = binned_mask;
|
||||
#endif
|
||||
|
||||
while (variant_mask != 0u)
|
||||
{
|
||||
int bit = findLSB(variant_mask);
|
||||
variant_mask &= ~(1u << bit);
|
||||
int primitive_index = group_index * 32 + bit;
|
||||
|
||||
if ((binned_mask & (1u << bit)) != 0u)
|
||||
{
|
||||
uint variant_index = uint(state_indices.elems[primitive_index].static_depth_tmem.x);
|
||||
uint work_offset = allocate_work_offset(variant_index);
|
||||
tile_raster_work.elems[work_offset + uint(TILE_INSTANCE_STRIDE) * variant_index] =
|
||||
uvec4(tile.x, tile.y, instance_offset, primitive_index);
|
||||
instance_offset++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
#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 "debug.h"
|
||||
#include "small_types.h"
|
||||
layout(local_size_x_id = 0) in;
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer VRAM8Buffer
|
||||
{
|
||||
mem_u8 data[];
|
||||
} vram8;
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer VRAM16Buffer
|
||||
{
|
||||
mem_u16 data[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 0, std430) readonly buffer VRAM32Buffer
|
||||
{
|
||||
uint data[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 1, std430) buffer TMEM16Buffer
|
||||
{
|
||||
mem_u16 data[2048];
|
||||
} tmem16;
|
||||
|
||||
struct TileInstance
|
||||
{
|
||||
mem_u16 data[2048];
|
||||
};
|
||||
|
||||
layout(set = 0, binding = 2, std430) writeonly buffer TMEMInstances
|
||||
{
|
||||
TileInstance instances[];
|
||||
} tile_instances;
|
||||
|
||||
layout(push_constant, std430) uniform Registers
|
||||
{
|
||||
int num_uploads;
|
||||
} registers;
|
||||
|
||||
const int TEXTURE_FMT_RGBA = 0;
|
||||
const int TEXTURE_FMT_YUV = 1;
|
||||
const int TEXTURE_FMT_CI = 2;
|
||||
const int TEXTURE_FMT_IA = 3;
|
||||
const int TEXTURE_FMT_I = 4;
|
||||
|
||||
const int UPLOAD_MODE_TILE = 0;
|
||||
const int UPLOAD_MODE_TLUT = 1;
|
||||
const int UPLOAD_MODE_BLOCK = 2;
|
||||
|
||||
struct UploadInfo
|
||||
{
|
||||
int width, height;
|
||||
float min_t_mod, max_t_mod;
|
||||
|
||||
int vram_addr;
|
||||
int vram_width;
|
||||
int vram_size;
|
||||
int vram_effective_width;
|
||||
|
||||
int tmem_offset;
|
||||
int tmem_stride_words;
|
||||
int tmem_size;
|
||||
int tmem_fmt;
|
||||
|
||||
int mode;
|
||||
float inv_tmem_stride_words;
|
||||
int dxt;
|
||||
int padding;
|
||||
};
|
||||
|
||||
layout(set = 1, binding = 0, std140) uniform UploadInfos
|
||||
{
|
||||
UploadInfo upload_info[256];
|
||||
};
|
||||
|
||||
bool tmem_dirty;
|
||||
uint current_tmem_value;
|
||||
|
||||
int compute_upload_t(int offset, float inv_stride)
|
||||
{
|
||||
// This is still exact for all relevant inputs, and much faster than integer divide.
|
||||
return int((float(offset) + 0.5) * inv_stride);
|
||||
}
|
||||
|
||||
// In 32bpp upload mode we read 64 bits and split the result over the lower and upper TMEM.
|
||||
void update_tmem_32(UploadInfo info, int tmem16_index, bool upper_tmem, bool yuv)
|
||||
{
|
||||
int tmem16_offset = (info.tmem_offset & 0x7ff) >> 1;
|
||||
int tmem16_stride = info.tmem_stride_words;
|
||||
|
||||
int pixel_offset = (tmem16_index - tmem16_offset) & 0x3ff;
|
||||
int upload_x, upload_y;
|
||||
int upload_x_xor = 0;
|
||||
|
||||
if (info.mode == UPLOAD_MODE_BLOCK)
|
||||
{
|
||||
int word_offset = pixel_offset >> 1;
|
||||
|
||||
if (info.tmem_stride_words == 0)
|
||||
{
|
||||
// Trivial case, we can just compute T factor directly and set upload_x_xor.
|
||||
// Other than that, it works like a simple 1D upload.
|
||||
|
||||
// However, if DxT is weird, we might end up in a situation where this word is written multiple times,
|
||||
// or zero times.
|
||||
|
||||
int iteration_candidate_first = word_offset & ~1;
|
||||
int iteration_candidate_second = iteration_candidate_first + 1;
|
||||
int first_t = (iteration_candidate_first * info.dxt) >> 16;
|
||||
int second_t = (iteration_candidate_second * info.dxt) >> 16;
|
||||
if (first_t != second_t)
|
||||
{
|
||||
int iteration_candidate_first_write_index = iteration_candidate_first ^ (first_t & 1);
|
||||
int iteration_candidate_second_write_index = iteration_candidate_second ^ (second_t & 1);
|
||||
if (iteration_candidate_second_write_index == word_offset)
|
||||
upload_x_xor = (second_t & 1) << 1;
|
||||
else if (iteration_candidate_first_write_index == word_offset)
|
||||
upload_x_xor = (first_t & 1) << 1;
|
||||
else
|
||||
return;
|
||||
}
|
||||
else
|
||||
upload_x_xor ^= (first_t & 1) << 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Welp ... This is pure insanity, but if we want to be completely correct ...
|
||||
int min_t = compute_upload_t(word_offset & ~1, info.min_t_mod);
|
||||
int max_t = compute_upload_t(word_offset | 1, info.max_t_mod);
|
||||
|
||||
// If t has a range, then the solution to Y = (t = floor(X * dt / 2048)) * stride + X has a range space of:
|
||||
// Y - t_max * stride <= X <= Y - t_min * stride.
|
||||
int max_word_candidate = (word_offset | 1) - tmem16_stride * min_t;
|
||||
int min_word_candidate = (word_offset & ~1) - tmem16_stride * max_t;
|
||||
|
||||
// If we have constraints for X, we constraint T further.
|
||||
min_t = max(min_t, (min_word_candidate * info.dxt) >> 16);
|
||||
max_t = min(max_t, (max_word_candidate * info.dxt) >> 16);
|
||||
|
||||
bool found_candidate = false;
|
||||
for (int t = max_t; t >= min_t; t--)
|
||||
{
|
||||
// Check to see if t is a solution to the equation.
|
||||
// Potentially two targets could write here.
|
||||
int candidate_solution_first = (word_offset & ~1) - tmem16_stride * t;
|
||||
int candidate_solution_second = (word_offset | 1) - tmem16_stride * t;
|
||||
|
||||
int candidate_t_first = (candidate_solution_first * info.dxt) >> 16;
|
||||
int candidate_t_second = (candidate_solution_second * info.dxt) >> 16;
|
||||
|
||||
if (((candidate_solution_second + candidate_t_second * tmem16_stride) ^ (candidate_t_second & 1)) == word_offset)
|
||||
{
|
||||
found_candidate = true;
|
||||
pixel_offset = (candidate_solution_second << 1) + (pixel_offset & 1);
|
||||
break;
|
||||
}
|
||||
else if (((candidate_solution_first + candidate_t_first * tmem16_stride) ^ (candidate_t_first & 1)) == word_offset)
|
||||
{
|
||||
found_candidate = true;
|
||||
pixel_offset = (candidate_solution_first << 1) + (pixel_offset & 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We strided over this 64bpp word.
|
||||
if (!found_candidate)
|
||||
return;
|
||||
}
|
||||
|
||||
upload_x = pixel_offset;
|
||||
upload_y = 0;
|
||||
}
|
||||
else if (tmem16_stride == 0)
|
||||
{
|
||||
// For TMEM stride of 0 we're essentially replaying the same line over and over and the final visible result
|
||||
// is what happened in Y == height - 1.
|
||||
upload_x = pixel_offset;
|
||||
upload_y = info.height - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
upload_y = compute_upload_t(pixel_offset, info.inv_tmem_stride_words);
|
||||
upload_x = pixel_offset - upload_y * tmem16_stride;
|
||||
|
||||
// If stride is smaller than width, we'll need to unroll the last line.
|
||||
if (upload_y >= info.height)
|
||||
{
|
||||
upload_x += tmem16_stride * (upload_y - info.height + 1);
|
||||
upload_y = info.height - 1;
|
||||
}
|
||||
}
|
||||
|
||||
int last_line_upload_x = upload_x ^ ((upload_y & 1) << 1);
|
||||
if (last_line_upload_x >= info.width && upload_y > 0)
|
||||
{
|
||||
// If the last line won't trigger a write, the previous line probably did.
|
||||
upload_y--;
|
||||
upload_x += tmem16_stride;
|
||||
}
|
||||
|
||||
int iteration_offset;
|
||||
|
||||
upload_x ^= ((upload_y & 1) << 1) | upload_x_xor;
|
||||
|
||||
if (info.vram_size == 3 || yuv)
|
||||
{
|
||||
iteration_offset = 4 * (upload_x & ~1);
|
||||
}
|
||||
else if (info.vram_size == 2)
|
||||
{
|
||||
// In 16bpp VRAM mode, we are supposed to step 4 pixels at a time (8 bytes), which will form 2 complete pixels.
|
||||
// However, in 32bpp tile mode we're not shifting the X value appropriately.
|
||||
// So, we're writing texels [0, 1, ..., 4, 5, ...], etc.
|
||||
if ((upload_x & 2) != 0)
|
||||
{
|
||||
// We're not writing in this line, but the previous line might have!
|
||||
// Interleaving patterns will form ...
|
||||
if (upload_y > 0)
|
||||
{
|
||||
upload_y--;
|
||||
upload_x += tmem16_stride;
|
||||
upload_x ^= 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// These 2 words will never be written to.
|
||||
return;
|
||||
}
|
||||
}
|
||||
iteration_offset = 2 * (upload_x & ~1);
|
||||
}
|
||||
else if (info.vram_size == 1)
|
||||
{
|
||||
// 4 potential mirrors.
|
||||
for (int i = 0; i < 4 && upload_y > 0 && (upload_x & 6) != 0; i++)
|
||||
{
|
||||
upload_y--;
|
||||
upload_x += tmem16_stride;
|
||||
upload_x ^= 2;
|
||||
}
|
||||
|
||||
if ((upload_x & 6) != 0)
|
||||
{
|
||||
// These 6 words will never be written to.
|
||||
return;
|
||||
}
|
||||
|
||||
iteration_offset = upload_x & ~1;
|
||||
}
|
||||
|
||||
if (upload_x >= info.width)
|
||||
return;
|
||||
|
||||
int line_rdram_addr = info.vram_addr + ((upload_y * info.vram_width) << (info.vram_size - 1));
|
||||
|
||||
// The loading pipeline reads 64 bits per iteration.
|
||||
int rdram_addr = line_rdram_addr + iteration_offset + 4 * (upload_x & 1);
|
||||
|
||||
uint word;
|
||||
if ((rdram_addr & 3) == 0)
|
||||
{
|
||||
word = uint(vram32.data[rdram_addr >> 2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
word = (uint(vram8.data[rdram_addr ^ 3]) << 24) |
|
||||
(uint(vram8.data[(rdram_addr + 1) ^ 3]) << 16) |
|
||||
(uint(vram8.data[(rdram_addr + 2) ^ 3]) << 8) |
|
||||
uint(vram8.data[(rdram_addr + 3) ^ 3]);
|
||||
}
|
||||
|
||||
if (yuv)
|
||||
{
|
||||
// Lower TMEM receives interleaved UV samples, while upper receives Y.
|
||||
if (upper_tmem)
|
||||
{
|
||||
uint y0 = (word >> 16u) & 0xffu;
|
||||
uint y1 = (word >> 0u) & 0xffu;
|
||||
word = (y0 << 8u) | y1;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint u = (word >> 24u) & 0xffu;
|
||||
uint v = (word >> 8u) & 0xffu;
|
||||
word = (u << 8u) | v;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
word >>= 16u - 16u * uint(upper_tmem);
|
||||
word &= 0xffffu;
|
||||
}
|
||||
current_tmem_value = word;
|
||||
tmem_dirty = true;
|
||||
}
|
||||
|
||||
void update_tmem_16(UploadInfo info, int tmem16_index)
|
||||
{
|
||||
int tmem16_offset = (info.tmem_offset & 0xfff) >> 1;
|
||||
int tmem16_stride = info.tmem_stride_words;
|
||||
|
||||
int pixel_offset = (tmem16_index - tmem16_offset) & 0x7ff;
|
||||
int upload_x, upload_y;
|
||||
int upload_x_xor = 0;
|
||||
|
||||
if (info.mode == UPLOAD_MODE_BLOCK)
|
||||
{
|
||||
int word_offset = pixel_offset >> 2;
|
||||
|
||||
if (info.tmem_stride_words == 0)
|
||||
{
|
||||
// Trivial case, we can just compute T factor directly and set upload_x_xor.
|
||||
// Other than that, it works like a simple 1D upload.
|
||||
upload_x_xor = (((word_offset * info.dxt) >> 16) & 1) << 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Welp ... This is pure insanity, but if we want to be completely correct ...
|
||||
int min_t = compute_upload_t(word_offset, info.min_t_mod);
|
||||
int max_t = compute_upload_t(word_offset, info.max_t_mod);
|
||||
|
||||
// If t has a range, then the solution to Y = (t = floor(X * dt / 2048)) * stride + X has a range space of:
|
||||
// Y - t_max * stride <= X <= Y - t_min * stride.
|
||||
int max_word_candidate = word_offset - tmem16_stride * min_t;
|
||||
int min_word_candidate = word_offset - tmem16_stride * max_t;
|
||||
|
||||
// If we have constraints for X, we constraint T further.
|
||||
min_t = max(min_t, (min_word_candidate * info.dxt) >> 16);
|
||||
max_t = min(max_t, (max_word_candidate * info.dxt) >> 16);
|
||||
|
||||
bool found_candidate = false;
|
||||
for (int t = max_t; t >= min_t; t--)
|
||||
{
|
||||
// Check to see if t is a solution to the equation.
|
||||
int candidate_solution = word_offset - tmem16_stride * t;
|
||||
int computed_t = (candidate_solution * info.dxt) >> 16;
|
||||
if (candidate_solution + computed_t * tmem16_stride == word_offset)
|
||||
{
|
||||
found_candidate = true;
|
||||
upload_x_xor = (computed_t & 1) << 1;
|
||||
pixel_offset = (candidate_solution << 2) + (pixel_offset & 3);
|
||||
}
|
||||
}
|
||||
|
||||
// We strided over this 64bpp word.
|
||||
if (!found_candidate)
|
||||
return;
|
||||
}
|
||||
|
||||
upload_x = pixel_offset;
|
||||
upload_y = 0;
|
||||
}
|
||||
else if (tmem16_stride == 0)
|
||||
{
|
||||
// For TMEM stride of 0 we're essentially replaying the same line over and over and the final visible result
|
||||
// is what happened in Y == height - 1.
|
||||
upload_x = pixel_offset;
|
||||
upload_y = info.height - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
upload_y = compute_upload_t(pixel_offset, info.inv_tmem_stride_words);
|
||||
upload_x = pixel_offset - upload_y * tmem16_stride;
|
||||
|
||||
// If stride is smaller than width, we'll need to unroll the last line.
|
||||
if (upload_y >= info.height)
|
||||
{
|
||||
upload_x += tmem16_stride * (upload_y - info.height + 1);
|
||||
upload_y = info.height - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// This is pure bullshit magic which arises as an edge case when
|
||||
// tile pixel size does not match texture image size.
|
||||
// Should not happen in normal applications.
|
||||
// This is basically doing scatter-as-gather, so we need to figure out
|
||||
// if there is no write to our texel after all (striding), or if there are multiple writes
|
||||
// to our texel, in which case we need to figure out the last writer.
|
||||
// This code is black magic, and it's made with blood, sweat and tears from testing with lots of trial and error.
|
||||
int iteration_offset;
|
||||
if (info.tmem_size != info.vram_size)
|
||||
{
|
||||
if (info.vram_size - info.tmem_size == 1)
|
||||
{
|
||||
// If TMEM is N bpp but VRAM is 2N bpp, we will get mirrored writes here.
|
||||
// Select which half of the 2N bpp load we observe in TMEM.
|
||||
iteration_offset = (upload_x & ~3) * 4;
|
||||
if ((upload_x & ~3) + 2 < (info.vram_effective_width >> (3 - info.vram_size)))
|
||||
iteration_offset += 8;
|
||||
}
|
||||
else if (info.tmem_size == 2 && info.vram_size == 1)
|
||||
{
|
||||
// In 8bpp VRAM mode, we are supposed to step 8 pixels at a time (8 bytes), which will form 4 complete pixels.
|
||||
// However, in 16bpp tile mode we're not shifting the X value appropriately.
|
||||
// So, we're writing texels [0, 1, 2, 3, ..., 8, 9, 10, 11], etc.
|
||||
if ((upload_x & 4) != 0)
|
||||
{
|
||||
// We're not writing in this line, but the previous line might have!
|
||||
// Interleaving patterns will form ...
|
||||
if ((tmem16_stride & 4) != 0 && upload_y > 0)
|
||||
{
|
||||
upload_y--;
|
||||
upload_x += tmem16_stride;
|
||||
}
|
||||
else
|
||||
{
|
||||
// These 4 words will never be written to.
|
||||
return;
|
||||
}
|
||||
}
|
||||
iteration_offset = upload_x & ~3;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal case TMEM size aligns with VRAM size.
|
||||
iteration_offset = (upload_x & ~3) * 2;
|
||||
}
|
||||
|
||||
if (upload_x >= info.width)
|
||||
return;
|
||||
|
||||
int line_rdram_addr = info.vram_addr + ((upload_y * info.vram_width) << (info.vram_size - 1));
|
||||
upload_x ^= ((upload_y & 1) << 1) | upload_x_xor;
|
||||
|
||||
// The loading pipeline reads 64 bits per iteration.
|
||||
int rdram_addr = line_rdram_addr + iteration_offset + 2 * (upload_x & 3);
|
||||
|
||||
uint word;
|
||||
if ((rdram_addr & 1) == 0)
|
||||
word = uint(vram16.data[(rdram_addr >> 1) ^ 1]);
|
||||
else
|
||||
word = (uint(vram8.data[rdram_addr ^ 3]) << 8) | uint(vram8.data[(rdram_addr + 1) ^ 3]);
|
||||
|
||||
current_tmem_value = word;
|
||||
tmem_dirty = true;
|
||||
}
|
||||
|
||||
void update_tmem_lut(UploadInfo info, int tmem16_index)
|
||||
{
|
||||
int tmem16_offset = (info.tmem_offset & 0xfff) >> 1;
|
||||
int pixel_offset = (tmem16_index - tmem16_offset) & 0x7ff;
|
||||
int pixel_offset_splat;
|
||||
|
||||
if (info.vram_size - info.tmem_size == 2)
|
||||
{
|
||||
pixel_offset_splat = pixel_offset >> 2;
|
||||
pixel_offset_splat <<= info.vram_size - 2;
|
||||
if (pixel_offset_splat >= info.vram_effective_width)
|
||||
return;
|
||||
}
|
||||
else if (info.vram_size - info.tmem_size == 1)
|
||||
{
|
||||
if ((pixel_offset & 4) == 0)
|
||||
{
|
||||
int shamt = info.tmem_size + (info.vram_size == 2 ? 2 : 0);
|
||||
pixel_offset_splat = (pixel_offset & ~7) >> shamt;
|
||||
if (pixel_offset_splat >= info.vram_effective_width)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (info.vram_size == info.tmem_size)
|
||||
{
|
||||
if ((pixel_offset & 0xc) == 0)
|
||||
{
|
||||
int shamt = info.tmem_size + (info.vram_size == 2 ? 2 : 0);
|
||||
pixel_offset_splat = (pixel_offset & ~3) >> shamt;
|
||||
if (pixel_offset_splat >= info.vram_effective_width)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (info.vram_size - info.tmem_size == -1)
|
||||
{
|
||||
if ((pixel_offset & 0x1c) == 0)
|
||||
{
|
||||
int shamt = info.tmem_size;
|
||||
pixel_offset_splat = (pixel_offset >> shamt) & ~7;
|
||||
if (pixel_offset_splat >= info.vram_effective_width)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 4bpp tile, 32bpp VRAM. Mirrored writes.
|
||||
int span_iteration = pixel_offset >> 2;
|
||||
span_iteration = span_iteration * 2;
|
||||
int span_pixel = span_iteration * 2;
|
||||
if (span_pixel + 2 < info.vram_effective_width)
|
||||
span_pixel += 2;
|
||||
|
||||
if (span_pixel >= info.vram_effective_width)
|
||||
return;
|
||||
|
||||
pixel_offset_splat = span_pixel;
|
||||
}
|
||||
|
||||
int rdram_addr = info.vram_addr + (pixel_offset_splat << (info.vram_size - 1));
|
||||
|
||||
// Odd behavior when we have unaligned TLUT uploads.
|
||||
rdram_addr += 2 * (rdram_addr & 1) * (pixel_offset & 3);
|
||||
|
||||
uint word;
|
||||
if ((rdram_addr & 1) == 0)
|
||||
word = uint(vram16.data[(rdram_addr >> 1) ^ 1]);
|
||||
else
|
||||
word = (uint(vram8.data[rdram_addr ^ 3]) << 8) | uint(vram8.data[(rdram_addr + 1) ^ 3]);
|
||||
|
||||
current_tmem_value = word;
|
||||
tmem_dirty = true;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
tmem_dirty = false;
|
||||
current_tmem_value = uint(tmem16.data[gl_GlobalInvocationID.x]);
|
||||
int tmem16_index = int(gl_GlobalInvocationID.x) ^ 1;
|
||||
bool upper_tmem = tmem16_index >= 0x400;
|
||||
|
||||
tile_instances.instances[0].data[gl_GlobalInvocationID.x] = mem_u16(current_tmem_value);
|
||||
|
||||
int num_uploads = registers.num_uploads;
|
||||
for (int i = 0; i < num_uploads; i++)
|
||||
{
|
||||
UploadInfo info = upload_info[i];
|
||||
if (info.mode == UPLOAD_MODE_TLUT)
|
||||
{
|
||||
update_tmem_lut(info, tmem16_index);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool yuv = info.tmem_fmt == TEXTURE_FMT_YUV;
|
||||
if (info.tmem_size == 3 || yuv)
|
||||
update_tmem_32(info, tmem16_index & 0x3ff, upper_tmem, yuv);
|
||||
else
|
||||
update_tmem_16(info, tmem16_index);
|
||||
}
|
||||
|
||||
tile_instances.instances[i + 1].data[gl_GlobalInvocationID.x] = mem_u16(current_tmem_value);
|
||||
}
|
||||
|
||||
if (tmem_dirty)
|
||||
tmem16.data[gl_GlobalInvocationID.x] = mem_u16(current_tmem_value);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#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.
|
||||
*/
|
||||
|
||||
// RIP to any GPU which attempts to execute this monstrosity :)
|
||||
|
||||
#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 "debug.h"
|
||||
#include "data_structures_buffers.h"
|
||||
|
||||
#include "noise.h"
|
||||
#include "memory_interfacing.h"
|
||||
#include "shading.h"
|
||||
|
||||
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);
|
||||
|
||||
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 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);
|
||||
|
||||
ShadedData shaded;
|
||||
if (shade_pixel(x, y, primitive_index, shaded))
|
||||
{
|
||||
if ((shaded.coverage_count & COVERAGE_FILL_BIT) != 0)
|
||||
fill_color(derived_setup.elems[primitive_index].fill_color);
|
||||
else if ((shaded.coverage_count & COVERAGE_COPY_BIT) != 0)
|
||||
copy_pipeline(shaded.z_dith, primitive_index);
|
||||
else
|
||||
depth_blend(x, y, primitive_index, shaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finish_tile(gl_GlobalInvocationID.xy,
|
||||
registers.fb_width, registers.fb_height,
|
||||
registers.fb_addr_index, registers.fb_depth_addr_index);
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
#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"
|
||||
#include "fb_formats.h"
|
||||
|
||||
layout(local_size_x_id = 3) in;
|
||||
|
||||
layout(constant_id = 0) const int RDRAM_SIZE = 8 * 1024 * 1024;
|
||||
const int RDRAM_MASK_8 = RDRAM_SIZE - 1;
|
||||
const int RDRAM_MASK_16 = RDRAM_MASK_8 >> 1;
|
||||
const int RDRAM_MASK_32 = RDRAM_MASK_8 >> 2;
|
||||
layout(constant_id = 1) const int FB_SIZE_LOG2 = 0;
|
||||
layout(constant_id = 2) const bool COLOR_DEPTH_ALIAS = false;
|
||||
layout(constant_id = 4) const int NUM_SAMPLES = 1;
|
||||
|
||||
layout(push_constant) uniform Registers
|
||||
{
|
||||
uint num_pixels, fb_addr, fb_depth_addr;
|
||||
} registers;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram8;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled32
|
||||
{
|
||||
uint elems[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram_reference8;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram_reference16;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference32
|
||||
{
|
||||
uint elems[];
|
||||
} vram_reference32;
|
||||
|
||||
void copy_rdram_8(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_8;
|
||||
uint real_word = uint(vram8.elems[index]);
|
||||
vram_reference8.elems[index] = uint8_t(real_word);
|
||||
}
|
||||
|
||||
void copy_rdram_16(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
uint real_word = uint(vram16.elems[index]);
|
||||
vram_reference16.elems[index] = uint16_t(real_word);
|
||||
}
|
||||
|
||||
void copy_rdram_32(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_32;
|
||||
uint real_word = vram32.elems[index];
|
||||
vram_reference32.elems[index] = real_word;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
if (index >= registers.num_pixels)
|
||||
return;
|
||||
|
||||
uint depth_index = index + registers.fb_depth_addr;
|
||||
uint color_index = index + registers.fb_addr;
|
||||
|
||||
switch (FB_SIZE_LOG2)
|
||||
{
|
||||
case 0:
|
||||
copy_rdram_8(color_index);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
copy_rdram_16(color_index);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
copy_rdram_32(color_index);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!COLOR_DEPTH_ALIAS)
|
||||
copy_rdram_16(depth_index);
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
#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_id = 3) in;
|
||||
|
||||
layout(constant_id = 0) const int RDRAM_SIZE = 8 * 1024 * 1024;
|
||||
const int RDRAM_MASK_8 = RDRAM_SIZE - 1;
|
||||
const int RDRAM_MASK_16 = RDRAM_MASK_8 >> 1;
|
||||
const int RDRAM_MASK_32 = RDRAM_MASK_8 >> 2;
|
||||
layout(constant_id = 1) const int FB_SIZE_LOG2 = 0;
|
||||
layout(constant_id = 2) const bool COLOR_DEPTH_ALIAS = false;
|
||||
layout(constant_id = 4) const int NUM_SAMPLES = 1;
|
||||
|
||||
layout(push_constant) uniform Registers
|
||||
{
|
||||
uint num_pixels, fb_addr, fb_depth_addr;
|
||||
} registers;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram8;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 0) readonly buffer RDRAMSingleSampled32
|
||||
{
|
||||
uint elems[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 1) readonly buffer RDRAMHiddenSingleSampled
|
||||
{
|
||||
uint8_t elems[];
|
||||
} hidden_vram;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram_reference8;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram_reference16;
|
||||
|
||||
layout(set = 0, binding = 2) buffer RDRAMUpscalingReference32
|
||||
{
|
||||
uint elems[];
|
||||
} vram_reference32;
|
||||
|
||||
layout(set = 0, binding = 3) buffer RDRAMUpscaling8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram_upscaled8;
|
||||
|
||||
layout(set = 0, binding = 3) buffer RDRAMUpscaling16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram_upscaled16;
|
||||
|
||||
layout(set = 0, binding = 3) buffer RDRAMUpscaling32
|
||||
{
|
||||
uint elems[];
|
||||
} vram_upscaled32;
|
||||
|
||||
layout(set = 0, binding = 4) buffer RDRAMHiddenUpscaling
|
||||
{
|
||||
uint8_t elems[];
|
||||
} hidden_vram_upscaled;
|
||||
|
||||
void update_rdram_8(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_8;
|
||||
|
||||
uint real_word = uint(vram8.elems[index]);
|
||||
uint reference_word = uint(vram_reference8.elems[index]);
|
||||
|
||||
if (real_word != reference_word)
|
||||
{
|
||||
uint mirrored_index = index ^ 3u;
|
||||
uint real_hidden_word = uint(hidden_vram.elems[mirrored_index >> 1u]);
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
vram_upscaled8.elems[index + i * RDRAM_SIZE] = uint8_t(real_word);
|
||||
if ((mirrored_index & 1u) != 0u)
|
||||
hidden_vram_upscaled.elems[(mirrored_index >> 1u) + i * (RDRAM_SIZE >> 1)] = uint8_t(real_hidden_word);
|
||||
}
|
||||
vram_reference8.elems[index] = uint8_t(real_word);
|
||||
}
|
||||
}
|
||||
|
||||
void update_rdram_16(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_16;
|
||||
|
||||
uint real_word = uint(vram16.elems[index]);
|
||||
uint reference_word = uint(vram_reference16.elems[index]);
|
||||
|
||||
if (real_word != reference_word)
|
||||
{
|
||||
uint mirrored_index = index ^ 1u;
|
||||
uint real_hidden_word = uint(hidden_vram.elems[mirrored_index]);
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
vram_upscaled16.elems[index + i * (RDRAM_SIZE >> 1)] = uint16_t(real_word);
|
||||
hidden_vram_upscaled.elems[mirrored_index + i * (RDRAM_SIZE >> 1)] = uint8_t(real_hidden_word);
|
||||
}
|
||||
vram_reference16.elems[index] = uint16_t(real_word);
|
||||
}
|
||||
}
|
||||
|
||||
void update_rdram_32(uint index)
|
||||
{
|
||||
index &= RDRAM_MASK_32;
|
||||
|
||||
uint real_word = vram32.elems[index];
|
||||
uint reference_word = vram_reference32.elems[index];
|
||||
|
||||
if (real_word != reference_word)
|
||||
{
|
||||
uint real_hidden_word0 = uint(hidden_vram.elems[2u * index]);
|
||||
uint real_hidden_word1 = uint(hidden_vram.elems[2u * index + 1u]);
|
||||
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
vram_upscaled32.elems[index + i * (RDRAM_SIZE >> 2)] = real_word;
|
||||
hidden_vram_upscaled.elems[2u * index + i * (RDRAM_SIZE >> 1)] = uint8_t(real_hidden_word0);
|
||||
hidden_vram_upscaled.elems[2u * index + 1u + i * (RDRAM_SIZE >> 1)] = uint8_t(real_hidden_word1);
|
||||
}
|
||||
vram_reference32.elems[index] = real_word;
|
||||
}
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
if (index >= registers.num_pixels)
|
||||
return;
|
||||
|
||||
uint depth_index = index + registers.fb_depth_addr;
|
||||
uint color_index = index + registers.fb_addr;
|
||||
|
||||
switch (FB_SIZE_LOG2)
|
||||
{
|
||||
case 0:
|
||||
update_rdram_8(color_index);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
update_rdram_16(color_index);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
update_rdram_32(color_index);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!COLOR_DEPTH_ALIAS)
|
||||
update_rdram_16(depth_index);
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
#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"
|
||||
#include "fb_formats.h"
|
||||
|
||||
layout(local_size_x_id = 3) in;
|
||||
|
||||
layout(constant_id = 0) const int RDRAM_SIZE = 8 * 1024 * 1024;
|
||||
const int RDRAM_MASK_8 = RDRAM_SIZE - 1;
|
||||
const int RDRAM_MASK_16 = RDRAM_MASK_8 >> 1;
|
||||
const int RDRAM_MASK_32 = RDRAM_MASK_8 >> 2;
|
||||
layout(constant_id = 1) const int FB_SIZE_LOG2 = 0;
|
||||
layout(constant_id = 2) const bool COLOR_DEPTH_ALIAS = false;
|
||||
layout(constant_id = 4) const int NUM_SAMPLES = 1;
|
||||
layout(constant_id = 5) const bool DITHER = false;
|
||||
layout(constant_id = 6) const bool RDRAM_UNSCALED_WRITE_MASK = false;
|
||||
|
||||
layout(push_constant) uniform Registers
|
||||
{
|
||||
uint num_pixels, fb_addr, fb_depth_addr, width, height;
|
||||
} registers;
|
||||
|
||||
layout(set = 0, binding = 0) writeonly buffer RDRAMSingleSampled8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram8;
|
||||
|
||||
layout(set = 0, binding = 0) writeonly buffer RDRAMSingleSampled16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram16;
|
||||
|
||||
layout(set = 0, binding = 0) writeonly buffer RDRAMSingleSampled32
|
||||
{
|
||||
uint elems[];
|
||||
} vram32;
|
||||
|
||||
layout(set = 0, binding = 2) writeonly buffer RDRAMUpscalingReference8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram_reference8;
|
||||
|
||||
layout(set = 0, binding = 2) writeonly buffer RDRAMUpscalingReference16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram_reference16;
|
||||
|
||||
layout(set = 0, binding = 2) writeonly buffer RDRAMUpscalingReference32
|
||||
{
|
||||
uint elems[];
|
||||
} vram_reference32;
|
||||
|
||||
layout(set = 0, binding = 3) readonly buffer RDRAMUpscaling8
|
||||
{
|
||||
uint8_t elems[];
|
||||
} vram_upscaled8;
|
||||
|
||||
layout(set = 0, binding = 3) readonly buffer RDRAMUpscaling16
|
||||
{
|
||||
uint16_t elems[];
|
||||
} vram_upscaled16;
|
||||
|
||||
layout(set = 0, binding = 3) readonly buffer RDRAMUpscaling32
|
||||
{
|
||||
uint elems[];
|
||||
} vram_upscaled32;
|
||||
|
||||
layout(set = 0, binding = 4) readonly buffer RDRAMHiddenUpscaling
|
||||
{
|
||||
uint8_t elems[];
|
||||
} hidden_vram_upscaled;
|
||||
|
||||
void copy_rdram_8(uint index)
|
||||
{
|
||||
uint r = 0u;
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
uint real_word = uint(vram_upscaled8.elems[index]);
|
||||
r += real_word;
|
||||
}
|
||||
|
||||
r = (r + (NUM_SAMPLES >> 1)) / NUM_SAMPLES;
|
||||
vram_reference8.elems[index] = uint8_t(r);
|
||||
vram8.elems[index] = uint8_t(r);
|
||||
}
|
||||
|
||||
uvec4 decode_rgba5551(uint word)
|
||||
{
|
||||
return (uvec4(word) >> uvec4(11, 6, 1, 0)) & uvec4(0x1f, 0x1f, 0x1f, 1);
|
||||
}
|
||||
|
||||
uint encode_rgba5551(uvec4 color)
|
||||
{
|
||||
return (color.r << 11u) | (color.g << 6u) | (color.b << 1u) | color.a;
|
||||
}
|
||||
|
||||
const uint bayer_dither_lut[16] = uint[](
|
||||
0, 4, 1, 5,
|
||||
4, 0, 5, 1,
|
||||
3, 7, 2, 6,
|
||||
7, 3, 6, 2);
|
||||
|
||||
void copy_rdram_16(uint index, uint x, uint y)
|
||||
{
|
||||
uvec4 rgba = uvec4(0u);
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
uint real_word = uint(vram_upscaled16.elems[index + i * (RDRAM_SIZE >> 1)]);
|
||||
rgba += decode_rgba5551(real_word);
|
||||
}
|
||||
|
||||
if (DITHER)
|
||||
{
|
||||
uint dither_value = bayer_dither_lut[(y & 3u) * 4u + (x & 3u)] * NUM_SAMPLES;
|
||||
rgba = (8u * rgba + dither_value) / (8 * NUM_SAMPLES);
|
||||
}
|
||||
else
|
||||
{
|
||||
rgba = (rgba + (NUM_SAMPLES >> 1)) / NUM_SAMPLES;
|
||||
}
|
||||
|
||||
uint encoded = encode_rgba5551(rgba);
|
||||
vram16.elems[index] = uint16_t(encoded);
|
||||
vram_reference16.elems[index] = uint16_t(encoded);
|
||||
}
|
||||
|
||||
void copy_rdram_16_single_sample(uint index)
|
||||
{
|
||||
// Copies the first sample. We cannot meaningfully filter depth samples.
|
||||
// The first sample should overlap exactly with the single-sampled version.
|
||||
// Coverage clipping might slightly change the result, but shouldn't be different enough to break things.
|
||||
uint upscaled_word = uint(vram_upscaled16.elems[index]);
|
||||
vram16.elems[index] = uint16_t(upscaled_word);
|
||||
vram_reference16.elems[index] = uint16_t(upscaled_word);
|
||||
}
|
||||
|
||||
uvec4 decode_rgba8(uint word)
|
||||
{
|
||||
return (uvec4(word) >> uvec4(24, 16, 8, 0)) & uvec4(0xff);
|
||||
}
|
||||
|
||||
uint encode_rgba8(uvec4 color)
|
||||
{
|
||||
return (color.r << 24u) | (color.g << 16u) | (color.b << 8u) | (color.a << 0u);
|
||||
}
|
||||
|
||||
void copy_rdram_32(uint index)
|
||||
{
|
||||
uvec4 rgba = uvec4(0u);
|
||||
for (int i = 0; i < NUM_SAMPLES; i++)
|
||||
{
|
||||
uint real_word = vram_upscaled32.elems[index + i * (RDRAM_SIZE >> 2)];
|
||||
rgba += decode_rgba8(real_word);
|
||||
}
|
||||
|
||||
rgba = (rgba + (NUM_SAMPLES >> 1)) / NUM_SAMPLES;
|
||||
uint encoded = encode_rgba8(rgba);
|
||||
vram32.elems[index] = encoded;
|
||||
vram_reference32.elems[index] = encoded;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
uvec2 coord = gl_GlobalInvocationID.xy;
|
||||
uint index = coord.y * registers.width + coord.x;
|
||||
uint depth_index = index + registers.fb_depth_addr;
|
||||
uint color_index = index + registers.fb_addr;
|
||||
|
||||
uvec2 mask_coord = coord >> 2u;
|
||||
uint mask_index = mask_coord.x + mask_coord.y * ((registers.width + 3) >> 2u);
|
||||
|
||||
uint write_mask;
|
||||
if (coord.x < registers.width)
|
||||
write_mask = vram_upscaled32.elems[NUM_SAMPLES * (RDRAM_SIZE >> 2) + mask_index];
|
||||
else
|
||||
write_mask = 0u;
|
||||
|
||||
uint shamt = 2u * ((coord.x & 3u) + 4u * (coord.y & 3u));
|
||||
write_mask = write_mask >> shamt;
|
||||
bool color_write_mask = (write_mask & 1u) != 0u;
|
||||
bool depth_write_mask = (write_mask & 2u) != 0u;
|
||||
|
||||
if (color_write_mask)
|
||||
{
|
||||
switch (FB_SIZE_LOG2)
|
||||
{
|
||||
case 0:
|
||||
color_index &= RDRAM_MASK_8;
|
||||
color_index ^= 3u;
|
||||
copy_rdram_8(color_index);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
color_index &= RDRAM_MASK_16;
|
||||
color_index ^= 1u;
|
||||
copy_rdram_16(color_index, coord.x, coord.y);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
color_index &= RDRAM_MASK_32;
|
||||
copy_rdram_32(color_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Metal portability: Memory barriers must happen in uniform control flow.
|
||||
if (RDRAM_UNSCALED_WRITE_MASK)
|
||||
{
|
||||
// Need this memory barrier to ensure the mask readback does not read
|
||||
// an invalid value from RDRAM. If the mask is seen, the valid RDRAM value is
|
||||
// also coherent.
|
||||
memoryBarrierBuffer();
|
||||
|
||||
if (color_write_mask)
|
||||
{
|
||||
switch (FB_SIZE_LOG2)
|
||||
{
|
||||
case 0:
|
||||
vram8.elems[color_index + RDRAM_SIZE] = mem_u8(0xff);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
vram16.elems[color_index + (RDRAM_SIZE >> 1)] = mem_u16(0xffff);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
vram32.elems[color_index + (RDRAM_SIZE >> 2)] = ~0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't bother writing back hidden VRAM. It is not visible to host anyways, and coverage is meaningless when it's filtered.
|
||||
// If host later decides to modify the CPU memory, then the hidden VRAM values become complete bogus either way.
|
||||
|
||||
if (!COLOR_DEPTH_ALIAS)
|
||||
{
|
||||
if (depth_write_mask)
|
||||
{
|
||||
depth_index &= RDRAM_MASK_16;
|
||||
depth_index ^= 1u;
|
||||
copy_rdram_16_single_sample(depth_index);
|
||||
}
|
||||
|
||||
// Metal portability: Memory barriers must happen in uniform control flow.
|
||||
if (RDRAM_UNSCALED_WRITE_MASK)
|
||||
{
|
||||
// Need this memory barrier to ensure the mask readback does not read
|
||||
// an invalid value from RDRAM. If the mask is seen, the valid RDRAM value is
|
||||
// also coherent.
|
||||
memoryBarrierBuffer();
|
||||
if (depth_write_mask)
|
||||
vram16.elems[depth_index + (RDRAM_SIZE >> 1u)] = mem_u16(0xffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 261 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user