Merge remote-tracking branch 'upstream/main' into spu-accuracy

This commit is contained in:
Nicolas 'Pixel' Noble
2026-09-03 10:21:37 -07:00
128 changed files with 6976 additions and 1645 deletions
+8
View File
@@ -0,0 +1,8 @@
# The build image only needs a couple of scripts out of the tree, so keep the
# build context from dragging in a full local build.
.git
objs/
bins/
/pcsx-redux
/pcsx-redux-tests
*.AppImage
+1 -5
View File
@@ -1,6 +1,5 @@
BUILD ?= Release
DESTDIR ?= /usr/local
CROSS ?= none
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
@@ -23,6 +22,7 @@ CPPFLAGS += -Ithird_party
CPPFLAGS += -Ithird_party/ELFIO
CPPFLAGS += -Ithird_party/fmt/include/
CPPFLAGS += -Ithird_party/googletest/googletest/include
CPPFLAGS += -Ithird_party/PEGTL/include
CPPFLAGS += -Ithird_party/ucl -Ithird_party/ucl/include
CPPFLAGS += -g
@@ -72,10 +72,6 @@ LDFLAGS_lto += -O3 -flto=auto -flto-partition=one
CPPFLAGS += $(CPPFLAGS_$(BUILD)) -pthread
LDFLAGS += $(LDFLAGS_$(BUILD)) -pthread
ifeq ($(CROSS),arm64)
CPPFLAGS += -fPIC -Wl,-rpath-link,/opt/cross/sysroot/usr/lib/aarch64-linux-gnu -L/opt/cross/sysroot/usr/lib/aarch64-linux-gnu
LDFLAGS += -fPIC -Wl,-rpath-link,/opt/cross/sysroot/usr/lib/aarch64-linux-gnu -L/opt/cross/sysroot/usr/lib/aarch64-linux-gnu
endif
LD := $(CXX)
+1 -1
View File
@@ -2,6 +2,6 @@
set -e
PACKAGES="capstone curl ffmpeg freetype glfw libuv pkg-config zlib"
PACKAGES="capstone curl ffmpeg freetype libuv pkg-config sdl3 zlib"
brew install ${PACKAGES}
+91
View File
@@ -0,0 +1,91 @@
name: Build image
on:
push:
branches:
- main
paths:
- 'tools/build/Dockerfile'
- 'tools/linux-mips/spawn-compiler.sh'
- '.dockerignore'
- '.github/workflows/build-image.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
env:
IMAGE: ghcr.io/grumpycoders/pcsx-redux-build
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: ubuntu-latest
- platform: linux/arm64
arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up buildx
uses: docker/setup-buildx-action@v3
- name: Log into ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
file: tools/build/Dockerfile
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=min,scope=${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export the digest
run: |
mkdir -p /tmp/digests
digest='${{ steps.build.outputs.digest }}'
touch "/tmp/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.arch }}
path: /tmp/digests/*
retention-days: 1
manifest:
needs: build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Log into ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push the multi-arch tag
run: |
docker buildx imagetools create -t $IMAGE:latest \
$(cd /tmp/digests && printf "$IMAGE@sha256:%s " *)
- name: Check what landed
run: |
docker buildx imagetools inspect $IMAGE:latest
+5
View File
@@ -32,5 +32,10 @@ jobs:
make -C src/mips/tests -j 2 PCSX_TESTS=true BUILD=Release
cp ./openbios.bin src/mips/openbios/
- name: Test
env:
# No dbus-launch in the image, so libdbus autolaunches a session bus, fails to
# exec, and leaks the buffer it grew for the reply. Not ours and dbus_shutdown
# doesn't reclaim it; telling it there is no session bus avoids the attempt.
DBUS_SESSION_BUS_ADDRESS: "disabled:"
run: |
xvfb-run ./pcsx-redux-tests
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Install dependencies
run: |
pacman -Syu --noconfirm --needed capstone curl ffmpeg freetype2 glfw libuv sdl2 zlib git make pkg-config sudo base-devel pacman-contrib
pacman -Syu --noconfirm --needed capstone curl ffmpeg freetype2 libuv sdl3 zlib git make pkg-config sudo base-devel pacman-contrib
- uses: actions/checkout@v4
with:
set-safe-directory: true
+22 -10
View File
@@ -13,7 +13,19 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: x86_64
project: dev-linux-x64
secret: APPDISTRIB_DEV_LINUX_X64
- runner: ubuntu-24.04-arm
arch: aarch64
project: dev-linux-arm64
secret: APPDISTRIB_DEV_LINUX_ARM64
runs-on: ${{ matrix.runner }}
container:
image: ghcr.io/grumpycoders/pcsx-redux-build:latest
options: --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined
@@ -41,9 +53,9 @@ jobs:
apt install -y jq
- name: Packaging
env:
APPDISTRIB_TOKEN: ${{ secrets.APPDISTRIB_DEV_LINUX_X64 }}
APPDISTRIB_TOKEN: ${{ secrets[matrix.secret] }}
APPDISTRIB_ORGANIZATION: pcsx-redux
APPDISTRIB_PROJECT: dev-linux-x64
APPDISTRIB_PROJECT: ${{ matrix.project }}
run: |
npx @appdistrib/cli buildid > buildid.json
git config --global --add safe.directory /__w/pcsx-redux/pcsx-redux
@@ -59,15 +71,15 @@ jobs:
echo ' {' >> version.json
echo ' "channel": "dev",' >> version.json
echo ' "method": "appdistrib",' >> version.json
echo ' "updateCatalog": "https://distrib.app/storage/manifests/pcsx-redux/dev-linux-x64/manifest.json",' >> version.json
echo ' "updateInfoBase": "https://distrib.app/storage/manifests/pcsx-redux/dev-linux-x64/",' >> version.json
echo ' "updateCatalog": "https://distrib.app/storage/manifests/pcsx-redux/${{ matrix.project }}/manifest.json",' >> version.json
echo ' "updateInfoBase": "https://distrib.app/storage/manifests/pcsx-redux/${{ matrix.project }}/",' >> version.json
echo ' "updateStorageUrl": "https://distrib.app/"' >> version.json
echo ' }' >> version.json
echo ' ]' >> version.json
echo '}' >> version.json
cp version.json AppDir/usr/share/pcsx-redux/resources
linuxdeploy -v 3 --appdir=AppDir -e AppDir/usr/bin/pcsx-redux -d AppDir/usr/share/applications/pcsx-redux.desktop -i AppDir/usr/share/icons/hicolor/256x256/apps/pcsx-redux.png -o appimage
mv PCSX-Redux-x86_64.AppImage PCSX-Redux-HEAD-x86_64.AppImage
mv PCSX-Redux-${{ matrix.arch }}.AppImage PCSX-Redux-HEAD-${{ matrix.arch }}.AppImage
- name: Test
run: |
export GTEST_OUTPUT=xml:${TEST_RESULTS}/
@@ -76,11 +88,11 @@ jobs:
- name: Deploy
if: github.event_name == 'push'
env:
APPDISTRIB_TOKEN: ${{ secrets.APPDISTRIB_DEV_LINUX_X64 }}
APPDISTRIB_TOKEN: ${{ secrets[matrix.secret] }}
APPDISTRIB_ORGANIZATION: pcsx-redux
APPDISTRIB_PROJECT: dev-linux-x64
APPDISTRIB_PROJECT: ${{ matrix.project }}
run: |
git config --global --add safe.directory /__w/pcsx-redux/pcsx-redux
export BUILD=`git rev-parse HEAD | cut -c 1-8`
zip PCSX-Redux-$BUILD-linux-x86_64.zip *.AppImage
npx @appdistrib/cli -l buildId -m version.json upload PCSX-Redux-$BUILD-linux-x86_64.zip
zip PCSX-Redux-$BUILD-linux-${{ matrix.arch }}.zip *.AppImage
npx @appdistrib/cli -l buildId -m version.json upload PCSX-Redux-$BUILD-linux-${{ matrix.arch }}.zip
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
env:
TEST_RESULTS: /tmp/test-results
BUILD: Coverage
CPPFLAGS: -Werror=format-security
CPPFLAGS: -Wformat -Werror=format-security
steps:
- name: Update lcov, because https://bugs.launchpad.net/ubuntu/+source/lcov/+bug/2029924
run: |
-32
View File
@@ -1,32 +0,0 @@
name: Linux CI cross
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
cross-arm64:
runs-on: ubuntu-latest
container:
image: ghcr.io/grumpycoders/pcsx-redux-build-cross:latest
env:
CROSS: arm64
PKG_CONFIG_SYSROOT_DIR: /opt/cross/sysroot
PKG_CONFIG_PATH:
PKG_CONFIG_LIBDIR: /opt/cross/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig
CC: aarch64-linux-gnu-gcc --sysroot=/opt/cross/sysroot
CXX: aarch64-linux-gnu-g++ --sysroot=/opt/cross/sysroot
TEST_RESULTS: /tmp/test-results
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- run: |
make -j 2 all pcsx-redux-tests
+2 -4
View File
@@ -13,15 +13,13 @@ concurrency:
jobs:
build-openbios:
runs-on: ubuntu-latest
container:
image: ghcr.io/grumpycoders/pcsx-redux-build:latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- name: Update packages
run: sudo apt-get update
- name: Install dependencies
run: sudo apt-get install -y g++-mipsel-linux-gnu
- name: Build OpenBIOS
run: make -C src/mips/openbios -j2
- name: Upload results for MacOS build job
+68
View File
@@ -0,0 +1,68 @@
name: xmake CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# The build container runs as root, and xmake refuses outright to run as root
# without this. There's no unprivileged user in the image to switch to.
XMAKE_ROOT: y
# xmake keeps downloaded packages under $HOME, and $HOME here is whatever the
# runner injects into the container (/github/home), not what the image sets.
# Pin it so the cache path below is a fixed string rather than a guess.
XMAKE_GLOBALDIR: /tmp/xmake-global
jobs:
emulator:
runs-on: ubuntu-latest
container:
image: ghcr.io/grumpycoders/pcsx-redux-build:latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
# Most dependencies resolve to system packages, but fmt still comes from
# xmake's own repository and gets built from source. Cache it so that
# only happens when xmake.lua changes.
- uses: actions/cache@v4
with:
path: /tmp/xmake-global/.xmake/packages
key: xmake-packages-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('xmake.lua') }}
restore-keys: |
xmake-packages-${{ runner.os }}-${{ runner.arch }}-
- run: |
xmake f -y
xmake -y -j 2
guest:
runs-on: ubuntu-latest
container:
image: ghcr.io/grumpycoders/pcsx-redux-build:latest
strategy:
fail-fast: false
matrix:
sample:
- src/mips/helloworld
- src/mips/modplayer
- src/mips/psyqo/examples/hello
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
# -P . is mandatory, and the space in it is load-bearing: xmake resolves
# the project by walking up to the topmost xmake.lua, so without it this
# builds the emulator from the repository root instead of the sample.
- run: |
cd ${{ matrix.sample }}
xmake f -P . -y
xmake -P . -y
+6
View File
@@ -44,9 +44,15 @@ bins/
/exe2elf
/exe2exe
/exe2iso
/midi2psm
/midi2spd
/modconv
/ps1-packer
/psyq-obj-parser
/pcsx-redux-tests
# PCDRV test leftover
/testfile
# Temporary files from pcsx itself
pcsx.json
+4 -4
View File
@@ -52,9 +52,6 @@
[submodule "third_party/zep"]
path = third_party/zep
url = https://github.com/grumpycoders/zep.git
[submodule "third_party/miniaudio"]
path = third_party/miniaudio
url = https://github.com/mackron/miniaudio.git
[submodule "third_party/capstone"]
path = third_party/capstone
url = https://github.com/capstone-engine/capstone.git
@@ -72,7 +69,7 @@
url = https://github.com/mity/md4c.git
[submodule "third_party/imgui_md"]
path = third_party/imgui_md
url = https://github.com/mekhontsev/imgui_md.git
url = https://github.com/grumpycoders/imgui_md.git
[submodule "third_party/multipart-parser-c"]
path = third_party/multipart-parser-c
url = https://github.com/iafonov/multipart-parser-c
@@ -100,6 +97,9 @@
[submodule "third_party/psxlua"]
path = third_party/psxlua
url = https://github.com/grumpycoders/psxlua.git
[submodule "third_party/SDL"]
path = third_party/SDL
url = https://github.com/libsdl-org/SDL.git
[submodule "third_party/xmake-psx"]
path = third_party/xmake-psx
url = https://codeberg.org/malucart/xmake-psx.git
-102
View File
@@ -1,102 +0,0 @@
# appimage-builder recipe see https://appimage-builder.readthedocs.io for details
version: 1
AppDir:
path: ./AppDir
app_info:
id: pcsx-redux
name: PCSX-Redux
icon: pcsx-redux
version: HEAD
exec: usr/bin/pcsx-redux
exec_args: $@
runtime:
env:
__NV_PRIME_RENDER_OFFLOAD: 1
__GLX_VENDOR_LIBRARY_NAME: nvidia
apt:
arch:
- amd64
allow_unauthenticated: true
sources:
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble main restricted
key_url: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble universe
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble-updates universe
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble multiverse
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble-updates multiverse
- sourceline: deb http://us.archive.ubuntu.com/ubuntu/ noble-backports main
restricted universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu noble-security main restricted
- sourceline: deb http://security.ubuntu.com/ubuntu noble-security universe
- sourceline: deb http://security.ubuntu.com/ubuntu noble-security multiverse
include:
- libavformat60:amd64
- libbz2-1.0:amd64
- libcapstone4:amd64
- libcurl4t64:amd64
- libgl-dev:amd64
- libglfw3:amd64
- libjpeg-turbo8:amd64
- libllvm17t64:amd64
- libopengl0:amd64
- libpciaccess0:amd64
- libpgm-5.3-0t64:amd64
- libpulse0:amd64
- libsystemd0:amd64
- libuv1t64:amd64
- libxcb-sync1:amd64
- libxcursor1:amd64
- libxdmcp6:amd64
- libxext6:amd64
- libxfixes3:amd64
- libxi6:amd64
- libxinerama1:amd64
- libxrandr2:amd64
- libxrender1:amd64
- libxshmfence1:amd64
- libxxf86vm1:amd64
- openssl
- yaru-theme-icon
files:
include:
- /lib/x86_64-linux-gnu/libLLVM-17.so.1
- /lib/x86_64-linux-gnu/libOpenGL.so.0
- /lib/x86_64-linux-gnu/libXcursor.so.1
- /lib/x86_64-linux-gnu/libXi.so.6
- /lib/x86_64-linux-gnu/libXinerama.so.1
- /lib/x86_64-linux-gnu/libXrandr.so.2
- /lib/x86_64-linux-gnu/libavformat.so.60
- /lib/x86_64-linux-gnu/libcapstone.so.4
- /lib/x86_64-linux-gnu/libcurl.so.4
- /lib/x86_64-linux-gnu/libelf.so.1
- /lib/x86_64-linux-gnu/libglfw.so.3
- /lib/x86_64-linux-gnu/libpulse.so.0
- /lib/x86_64-linux-gnu/libsensors.so.5
- /lib/x86_64-linux-gnu/libuv.so.1
- /usr/lib/locale/locale-archive
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess
+1 -2
View File
@@ -29,7 +29,7 @@ find a link to the vendored version itself.
- [fmt](https://github.com/fmtlib/fmt)
- [FreeType](https://gitlab.freedesktop.org/freetype/freetype.git)
- [gl3w](https://github.com/skaslev/gl3w) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/gl3w/GL))
- [glfw](https://www.glfw.org)
- [SDL](https://www.libsdl.org)
- [googletest](https://github.com/google/googletest)
- [GoogleTestAdapter](https://github.com/csoltenborn/GoogleTestAdapter) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/GoogleTestAdapter))
- [ImFileDialog](https://github.com/dfranx/ImFileDialog) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/ImFileDialog))
@@ -50,7 +50,6 @@ find a link to the vendored version itself.
- [luv](https://github.com/luvit/luv)
- [magic_enum](https://github.com/Neargye/magic_enum)
- [md4c](https://github.com/mity/md4c)
- [miniaudio](https://github.com/mackron/miniaudio)
- [multipart-parser-c](https://github.com/iafonov/multipart-parser-c)
- [nanosvg](https://github.com/memononen/nanosvg) ([vendored](https://github.com/grumpycoders/nanosvg))
- [nanovg](https://github.com/memononen/nanovg) ([vendored](https://github.com/grumpycoders/nanovg))
+12 -22
View File
@@ -1,7 +1,6 @@
TARGET := pcsx-redux
BUILD ?= Release
DESTDIR ?= /usr/local
CROSS ?= none
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
@@ -10,7 +9,7 @@ CC_IS_CLANG := $(shell $(CC) --version | grep -q clang && echo true || echo fals
MACOS_MIN_VERSION := 11
PACKAGES := capstone freetype2 glfw3 libavcodec libavformat libavutil libswresample libcurl libuv zlib
PACKAGES := capstone freetype2 libavcodec libavformat libavutil libswresample libcurl libuv sdl3 zlib
OPTIONAL_PACKAGES := md4c fmt libllhttp libluv liburiparser
OPTIONAL_LIBRARIES := multipart ucl
@@ -104,10 +103,6 @@ LDFLAGS_lto += -O3 -flto=auto -flto-partition=one
CPPFLAGS += $(CPPFLAGS_$(BUILD)) -pthread
LDFLAGS += $(LDFLAGS_$(BUILD)) -pthread
ifeq ($(CROSS),arm64)
CPPFLAGS += -fPIC -Wl,-rpath-link,/opt/cross/sysroot/usr/lib/aarch64-linux-gnu -L/opt/cross/sysroot/usr/lib/aarch64-linux-gnu
LDFLAGS += -fPIC -Wl,-rpath-link,/opt/cross/sysroot/usr/lib/aarch64-linux-gnu -L/opt/cross/sysroot/usr/lib/aarch64-linux-gnu
endif
LD := $(CXX)
@@ -126,7 +121,7 @@ SRCS += third_party/gl3w/GL/gl3w-throwers.cc
SRCS += $(wildcard third_party/iec-60908b/*.c)
SRCS += third_party/ImFileDialog/ImFileDialog.cpp
SRCS += third_party/imgui/backends/imgui_impl_opengl3.cpp
SRCS += third_party/imgui/backends/imgui_impl_glfw.cpp
SRCS += third_party/imgui/backends/imgui_impl_sdl3.cpp
SRCS += third_party/imgui/misc/cpp/imgui_stdlib.cpp
SRCS += third_party/imgui/misc/freetype/imgui_freetype.cpp
SRCS += third_party/imgui_lua_bindings/imgui_lua_bindings.cpp
@@ -164,11 +159,6 @@ ifeq ($(UNAME_M),arm64)
CPPFLAGS += -DVIXL_INCLUDE_TARGET_AARCH64 -DVIXL_CODE_BUFFER_MMAP
CPPFLAGS += -Ithird_party/vixl/src -Ithird_party/vixl/src/aarch64
endif
ifeq ($(CROSS),arm64)
SRCS += $(VIXL_SRCS)
CPPFLAGS += -DVIXL_INCLUDE_TARGET_AARCH64 -DVIXL_CODE_BUFFER_MMAP
CPPFLAGS += -Ithird_party/vixl/src -Ithird_party/vixl/src/aarch64
endif
SUPPORT_SRCS := src/support/container-file.cc src/support/file.cc src/support/mem4g.cc src/support/zfile.cc
SUPPORT_SRCS += src/supportpsx/adpcm.cc src/supportpsx/binloader.cc src/supportpsx/iec-60908b.cc src/supportpsx/iso9660-builder.cc src/supportpsx/ps1-packer.cc src/supportpsx/ucl-utils.cc
SUPPORT_SRCS += third_party/fmt/src/os.cc third_party/fmt/src/format.cc
@@ -223,11 +213,16 @@ VIXL_OBJECTS := $(addprefix objs/$(BUILD)/,$(patsubst %.cc,%.o,$(filter %.cc,$(V
$(IMGUI_OBJECTS): EXTRA_CPPFLAGS := $(IMGUI_CPPFLAGS)
TESTS_SRC := $(call rwildcard,tests/,*.cc)
TESTS := $(patsubst %.cc,%,$(TESTS_SRC))
TESTS_OBJECTS := $(addprefix objs/$(BUILD)/,$(patsubst %.cc,%.o,$(TESTS_SRC)))
TOOLS_SRC := $(foreach tool,$(TOOLS),tools/$(tool)/$(tool).cc)
TOOLS_OBJECTS := $(addprefix objs/$(BUILD)/,$(patsubst %.cc,%.o,$(TOOLS_SRC)))
DEPS += $(addprefix deps/$(BUILD)/,$(patsubst %.c,%.dep,$(filter %.c,$(SRCS))))
DEPS += $(addprefix deps/$(BUILD)/,$(patsubst %.cc,%.dep,$(filter %.cc,$(SRCS))))
DEPS += $(addprefix deps/$(BUILD)/,$(patsubst %.cpp,%.dep,$(filter %.cpp,$(SRCS))))
DEPS += $(addprefix deps/$(BUILD)/,$(patsubst %.cc,%.dep,$(TESTS_SRC)))
DEPS += $(addprefix deps/$(BUILD)/,$(patsubst %.cc,%.dep,$(TOOLS_SRC)))
CP ?= cp
MKDIRP ?= mkdir -p
@@ -276,15 +271,10 @@ appimage:
DESTDIR=AppDir/usr $(MAKE) $(MAKEOPTS) install
sed -i s:/usr/bin/:: AppDir/usr/share/applications/pcsx-redux.desktop
linuxdeploy -v 3 --appdir=AppDir -e AppDir/usr/bin/pcsx-redux -d AppDir/usr/share/applications/pcsx-redux.desktop -i AppDir/usr/share/icons/hicolor/256x256/apps/pcsx-redux.png -o appimage
mv PCSX-Redux-x86_64.AppImage PCSX-Redux-HEAD-x86_64.AppImage
mv PCSX-Redux-`uname -m`.AppImage PCSX-Redux-HEAD-`uname -m`.AppImage
ifeq ($(CROSS),arm64)
third_party/luajit/src/libluajit.a:
$(MAKE) $(MAKEOPTS) -C third_party/luajit/src amalg HOST_CC=cc CROSS=aarch64-linux-gnu- TARGET_CFLAGS=--sysroot=/opt/cross/sysroot BUILDMODE=static CFLAGS=$(LUAJIT_CFLAGS) LDFLAGS=$(LUAJIT_LDFLAGS) XCFLAGS="-DLUAJIT_ENABLE_GC64 -DLUAJIT_ENABLE_LUA52COMPAT" MACOSX_DEPLOYMENT_TARGET=$(MACOS_MIN_VERSION)
else
third_party/luajit/src/libluajit.a:
$(MAKE) $(MAKEOPTS) -C third_party/luajit/src amalg CC=$(CC) BUILDMODE=static CFLAGS=$(LUAJIT_CFLAGS) LDFLAGS=$(LUAJIT_LDFLAGS) XCFLAGS="-DLUAJIT_ENABLE_GC64 -DLUAJIT_ENABLE_LUA52COMPAT" MACOSX_DEPLOYMENT_TARGET=$(MACOS_MIN_VERSION)
endif
bins/$(BUILD)/$(TARGET): $(OBJECTS) $(LIBS)
@$(MKDIRP) $(dir $@)
@@ -330,7 +320,7 @@ objs/$(BUILD)/gtest_main.o: third_party/googletest/googletest/src/gtest_main.cc
$(CXX) -O3 -g $(CXXFLAGS) -Ithird_party/googletest/googletest -Ithird_party/googletest/googletest/include -c third_party/googletest/googletest/src/gtest_main.cc -o objs/$(BUILD)/gtest_main.o
clean:
rm -f $(OBJECTS) $(TOOLS) $(TARGET) bins/$(BUILD)/$(TARGET) $(addprefix bins/$(BUILD)/,$(TOOLS)) $(DEPS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o
rm -f $(OBJECTS) $(TESTS_OBJECTS) $(TESTS_SRC:.cc=.o) $(TOOLS_OBJECTS) $(TOOLS) $(TARGET) bins/$(BUILD)/$(TARGET) $(addprefix bins/$(BUILD)/,$(TOOLS)) $(DEPS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o
$(MAKE) -C third_party/luajit clean MACOSX_DEPLOYMENT_TARGET=$(MACOS_MIN_VERSION)
cleanall:
@@ -355,9 +345,9 @@ regen-i18n:
rm pcsx-src-list.txt
$(foreach l,$(LOCALES),$(call msgmerge,$(l)))
bins/$(BUILD)/pcsx-redux-tests: $(foreach t,$(TESTS),$(t).o) $(NONMAIN_OBJECTS) $(LIBS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o
bins/$(BUILD)/pcsx-redux-tests: $(TESTS_OBJECTS) $(NONMAIN_OBJECTS) $(LIBS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o
@$(MKDIRP) $(dir $@)
$(LD) -o bins/$(BUILD)/pcsx-redux-tests $(NONMAIN_OBJECTS) $(LIBS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o $(foreach t,$(TESTS),$(t).o) -Ithird_party/googletest/googletest/include $(LDFLAGS)
$(LD) -o bins/$(BUILD)/pcsx-redux-tests $(NONMAIN_OBJECTS) $(LIBS) objs/$(BUILD)/gtest-all.o objs/$(BUILD)/gtest_main.o $(TESTS_OBJECTS) -Ithird_party/googletest/googletest/include $(LDFLAGS)
pcsx-redux-tests: check_submodules bins/$(BUILD)/pcsx-redux-tests
$(CP) bins/$(BUILD)/pcsx-redux-tests pcsx-redux-tests
+15 -8
View File
@@ -76,7 +76,7 @@ powershell -c "& { iwr -UseBasicParsing https://bit.ly/mips-ps1 | iex }"
Then, open a new command prompt, and type the following:
```
mips install 16.1.0
mips install 16.2.0
```
To manually install this script, you can download it from [here](https://bit.ly/mips-ps1), and then install it with the following command:
@@ -90,16 +90,23 @@ You can leave the installation path blank to install the script in the Applicati
Once the toolchain is installed, you can compile OpenBIOS using `make -C src/mips/openbios`.
### Linux
Run `./dockermake.sh appimage`. You need [docker](https://en.wikipedia.org/wiki/Docker_(software)) for this to work. This will create an [AppImage](https://appimage.org/) file, called `PCSX-Redux-HEAD-x86_64.AppImage`, which a self-hosted binary containing all of its required dependencies. Alternatively, if you do not want to use Docker, you can also simply install the dependencies listed below and run `make`.
Run `./dockermake.sh appimage`. You need [docker](https://en.wikipedia.org/wiki/Docker_(software)) for this to work. This will create an [AppImage](https://appimage.org/) file, called `PCSX-Redux-HEAD-x86_64.AppImage` or `PCSX-Redux-HEAD-aarch64.AppImage` depending on your machine, which a self-hosted binary containing all of its required dependencies. Alternatively, if you do not want to use Docker, you can also simply install the dependencies listed below and run `make`.
#### GNU/Linux Dependencies
If you're only interested in compiling psx code, you can simply clone the pcsx-redux repo, then install `g++-mipsel-linux-gnu cpp-mipsel-linux-gnu binutils-mipsel-linux-gnu` then follow the instructions in `/pcsx-redux/src/mips/psyq/README.md` to convert the PsyQ libraries. You might find them pre-compiled online.
If you're only interested in compiling psx code, you can simply clone the pcsx-redux repo, then build the mips toolchain with [this script](https://github.com/grumpycoders/pcsx-redux/blob/main/tools/linux-mips/spawn-compiler.sh), then follow the instructions in `/pcsx-redux/src/mips/psyq/README.md` to convert the PsyQ libraries. You might find them pre-compiled online.
- Debian derivatives ( for full emulator compilation ):
```bash
sudo apt-get install -y build-essential git make pkg-config clang g++ g++-mipsel-linux-gnu cpp-mipsel-linux-gnu binutils-mipsel-linux-gnu libcapstone-dev libfreetype-dev libavcodec-dev libavformat-dev libavutil-dev libcurl4-openssl-dev libglfw3-dev libswresample-dev libuv1-dev zlib1g-dev
sudo apt-get install -y build-essential git make pkg-config clang g++ libcapstone-dev libfreetype-dev libavcodec-dev libavformat-dev libavutil-dev libcurl4-openssl-dev libsdl3-dev libswresample-dev libuv1-dev zlib1g-dev
```
The mips toolchain isn't packaged by Debian or Ubuntu, so build it from source:
```bash
sudo apt-get install -y wget bzip2 xz-utils bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
sudo bash tools/linux-mips/spawn-compiler.sh
```
- Arch derivatives :
@@ -113,16 +120,16 @@ paru -S pcsx-redux-git
Alternatively, the following steps describe how to install dependencies and compile manually:
```bash
sudo pacman -S --needed capstone clang git make pkg-config ffmpeg libuv zlib glfw-x11 curl xorg-server-xvfb imagemagick
sudo pacman -S --needed capstone clang git make pkg-config ffmpeg libuv zlib sdl3 curl xorg-server-xvfb imagemagick
```
The mipsel environment can be installed from [AUR](https://wiki.archlinux.org/index.php/Aur) : [cross-mipsel-linux-gnu-binutils](https://aur.archlinux.org/packages/cross-mipsel-linux-gnu-binutils/) and [cross-mipsel-linux-gnu-gcc](https://aur.archlinux.org/packages/cross-mipsel-linux-gnu-gcc/) using your [AURhelper](https://wiki.archlinux.org/index.php/AUR_helpers) of choice:
The mipsel environment can be installed from [AUR](https://wiki.archlinux.org/index.php/Aur) : [mipsel-none-elf-binutils](https://aur.archlinux.org/packages/mipsel-none-elf-binutils/) and [mipsel-none-elf-gcc](https://aur.archlinux.org/packages/mipsel-none-elf-gcc/) using your [AURhelper](https://wiki.archlinux.org/index.php/AUR_helpers) of choice:
```bash
trizen -S cross-mipsel-linux-gnu-binutils cross-mipsel-linux-gnu-gcc
trizen -S mipsel-none-elf-binutils mipsel-none-elf-gcc
```
You can then just enter the 'pcsx-redux' directory and compile without using docker with `make`.
Building OpenBIOS on Linux can be done with `./dockermake.sh -C src/mips/openbios`, or using the `g++-mipsel-linux-gnu` package with `make -C src/mips/openbios`. If you have a different mips compiler, you'll need to override some variables, such as `PREFIX=mipsel-none-elf FORMAT=elf32-littlemips`.
Building OpenBIOS on Linux can be done with `./dockermake.sh -C src/mips/openbios`, or with `make -C src/mips/openbios` once you have a mipsel-none-elf toolchain. If you have a different mips compiler, you'll need to override some variables, such as `PREFIX` and `FORMAT`.
### MacOS
You need MacOS Catalina or later with the latest XCode to build, as well as a few [homebrew](https://brew.sh/) packages. Run the [brew installation script](https://github.com/grumpycoders/pcsx-redux/blob/main/.github/scripts/install-brew-dependencies.sh) to get all the necessary dependencies. Simply run `make` to build.
-5
View File
@@ -1,5 +0,0 @@
PKG_CONFIG_SYSROOT_DIR=/opt/cross/sysroot
PKG_CONFIG_PATH=
PKG_CONFIG_LIBDIR=/opt/cross/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig
CC=aarch64-linux-gnu-gcc --sysroot=/opt/cross/sysroot
CXX=aarch64-linux-gnu-g++ --sysroot=/opt/cross/sysroot
-23
View File
@@ -1,23 +0,0 @@
@echo off
set OLDCWD=%cd%
cd %~dp0
set ROOT=%cd%
cd %OLDCWD%
set REL=%OLDCWD%
SETLOCAL EnableDelayedExpansion
set MAT=
set UPP=
for /f "tokens=*" %%a in ('echo.%ROOT:\=^&echo.%') do (
set SUB=!SUB!%%a\
call set TMP=%%src:!SUB!=%%
set MAT=!SUB!
)
set REL=%UPP%!REL:%MAT%=!
if "!REL!" EQU "!ROOT!" (set REL=) ELSE (set "REL=!REL:\=/!")
set REL=/!REL!
docker pull ghcr.io/grumpycoders/pcsx-redux-build-cross:latest
docker run --rm --env-file "%ROOT%/cross-env.list" -i -w"/project%REL%" -v "%ROOT%:/project" ghcr.io/grumpycoders/pcsx-redux-build-cross make CROSS=arm64 %*
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
ROOT=$(dirname $0)
CWD=$(pwd)
cd $ROOT
ROOT=$(pwd)
cd $CWD
docker pull ghcr.io/grumpycoders/pcsx-redux-build-cross:latest
docker run --rm --env-file ${ROOT}/cross-env.list -i -w/project${CWD#$ROOT} -v "${ROOT}:/project" -u `id -u`:`id -g` --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/grumpycoders/pcsx-redux-build-cross make CROSS=arm64 $@
+1 -1
View File
@@ -20,4 +20,4 @@ if "!REL!" EQU "!ROOT!" (set REL=) ELSE (set "REL=!REL:\=/!")
set REL=/!REL!
docker pull ghcr.io/grumpycoders/pcsx-redux-build:latest
docker run --rm --env-file "%ROOT%/env.list" -i -w"/project%REL%" -v "%ROOT%:/project" ghcr.io/grumpycoders/pcsx-redux-build:latest make %*
docker run --rm -i -w"/project%REL%" -v "%ROOT%:/project" ghcr.io/grumpycoders/pcsx-redux-build:latest make %*
+1 -1
View File
@@ -7,4 +7,4 @@ ROOT=$(pwd)
cd $CWD
docker pull ghcr.io/grumpycoders/pcsx-redux-build:latest
docker run --rm --env-file ${ROOT}/env.list -i -w/project${CWD#$ROOT} -v "${ROOT}:/project" -u `id -u`:`id -g` --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/grumpycoders/pcsx-redux-build:latest make $@
docker run --rm -i -w/project${CWD#$ROOT} -v "${ROOT}:/project" -u `id -u`:`id -g` --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/grumpycoders/pcsx-redux-build:latest make $@
+1 -1
View File
@@ -20,4 +20,4 @@ if "!REL!" EQU "!ROOT!" (set REL=) ELSE (set "REL=!REL:\=/!")
set REL=/!REL!
docker pull ghcr.io/grumpycoders/pcsx-redux-build:latest
docker run --rm --env-file "%ROOT%/env.list" -t -i -w"/project%REL%" -v "%ROOT%:/project" ghcr.io/grumpycoders/pcsx-redux-build:latest bash -l
docker run --rm -t -i -w"/project%REL%" -v "%ROOT%:/project" ghcr.io/grumpycoders/pcsx-redux-build:latest bash -l
+1 -1
View File
@@ -7,4 +7,4 @@ ROOT=$(pwd)
cd $CWD
docker pull ghcr.io/grumpycoders/pcsx-redux-build:latest
docker run --rm --env-file ${ROOT}/env.list -t -i -w/project${CWD#$ROOT} -v "${ROOT}:/project" -u `id -u`:`id -g` --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/grumpycoders/pcsx-redux-build:latest bash -l
docker run --rm -t -i -w/project${CWD#$ROOT} -v "${ROOT}:/project" -u `id -u`:`id -g` --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/grumpycoders/pcsx-redux-build:latest bash -l
-3
View File
@@ -1,3 +0,0 @@
XDG_DATA_HOME=/home/coder
XDG_CACHE_HOME=/home/coder
HOME=/home/coder
+15 -15
View File
@@ -6,7 +6,7 @@
zlib,
curl,
ffmpeg,
glfw3,
sdl3,
capstone,
freetype,
libX11,
@@ -16,7 +16,6 @@
multipart-parser-c,
fmt,
magic-enum,
miniaudio,
gtest,
tl-expected,
elfio,
@@ -27,6 +26,7 @@
llhttp,
zip,
libxcb,
libGL,
src,
debugBuild ? false,
@@ -40,8 +40,8 @@ let
({
owner = "grumpycoders";
repo = "zep";
rev = "86ea3c7019f45ccd4a13503bf7d98a396e8f0193";
hash = "sha256-6NmUlOHkRQvCgbATcNxnFrfA2ZWROPYN8Vpd10k6Z2g=";
rev = "969ae7ea35ae583f215e6f3e724366e0815d58f9";
hash = "sha256-iblFvaBzZL8lRhAzYh9bmoBW4GzSaMyl35dHpGoyJlw=";
})
({
owner = "grumpycoders";
@@ -56,10 +56,10 @@ let
hash = "sha256-gZHbNuDkLXlLlXZZpLBHcbwzTfeBBkLY7xl4L5yr2lY=";
})
({
owner = "mekhontsev";
owner = "grumpycoders";
repo = "imgui_md";
rev = "8ca75c5f7663f314821e3d0b2c51011792bee68f";
hash = "sha256-uxhY81DWLRRCceYn9khk3rwzT+2f9PNMIMT9OrkPfFc=";
rev = "193314702e08c9e338a0b9d9346cb93ed4d8b758";
hash = "sha256-U783p+I1Sy0Dpmz5wRRp6qLiNqcIZ7pUb3Zezgmwhxc=";
})
({
owner = "herumi";
@@ -70,20 +70,20 @@ let
({
owner = "lunarmodules";
repo = "luafilesystem";
rev = "912e06714fc276c15b4d5d1b42bd2b11edb8deff";
hash = "sha256-BShByo2NhVrOHDPze/JXfeFWq36PFrI2HVugR2MDB0A=";
rev = "7f89bc0c6529497e0bc45b33467bf6cbcf6e989d";
hash = "sha256-vSP+KFxSzpzG+nJp0+YF+cDo0eddV3PvAm/jyzFDp14=";
})
({
owner = "grumpycoders";
repo = "luajit";
rev = "66fadd16a51955cfbd770de62806cfbdd7c6c818";
hash = "sha256-nFlDr79GC8MsL6ausAsEPJwL8OJrFydB37tpD5mS1C8=";
rev = "07c36331bb4e1140322a6f8d91d53b9c2767ed46";
hash = "sha256-UNoib5Kf3NxkIKaerZW9NrQ3lyQn0WXvBFRQT+KJrYs=";
})
({
owner = "ocornut";
repo = "imgui";
rev = "368123ab06b2b573d585e52f84cd782c5c006697";
hash = "sha256-6VOs7a31bEfAG75SQAY2X90h/f/HvqZmN615WXYkUOA=";
rev = "b48d1afbe8ee8b238e2961dc363a949dd7304e23";
hash = "sha256-PknWLxYuXQ73TCFN+eKOJDNLGbg/ZqKSF6mFxkJG6vI=";
})
({
owner = "mdqinc";
@@ -141,7 +141,6 @@ in stdenv.mkDerivation {
ucl
md4c
luajitPackages.libluv
miniaudio
multipart-parser-c
fmt
magic-enum
@@ -153,12 +152,13 @@ in stdenv.mkDerivation {
curl.dev
zlib
ffmpeg.dev
glfw3
sdl3
capstone
freetype.dev
uriparser
libX11
libxcb
libGL
llhttp
];
+63 -31
View File
@@ -185,13 +185,10 @@ void DynaRecCPU::recMTC2(uint32_t code) {
static uint32_t MFC2Wrapper(int reg) { return PCSX::g_emulator->m_gte->MFC2(reg); }
void DynaRecCPU::recMFC2(uint32_t code) {
if (_Rt_) {
allocateRegWithoutLoad(_Rt_);
m_gprs[_Rt_].setWriteback(true);
}
switch (_Rd_) {
// Read a COP2 data register into "dest", applying the sign/zero extension quirks the register file has.
// Note that this can emit a call for IRGB/ORGB, so "dest" must not be a register the allocator might spill.
void DynaRecCPU::loadGTEDataRegister(Register dest, int index) {
switch (index) {
case 1:
case 3:
case 5:
@@ -199,9 +196,7 @@ void DynaRecCPU::recMFC2(uint32_t code) {
case 9:
case 10:
case 11:
if (_Rt_) {
gen.Ldrsh(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_DATA_OFFSET(_Rd_)));
}
gen.Ldrsh(dest, MemOperand(contextPointer, COP2_DATA_OFFSET(index)));
break;
case 7:
@@ -209,44 +204,81 @@ void DynaRecCPU::recMFC2(uint32_t code) {
case 17:
case 18:
case 19:
if (_Rt_) {
gen.Ldrh(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_DATA_OFFSET(_Rd_)));
}
gen.Ldrh(dest, MemOperand(contextPointer, COP2_DATA_OFFSET(index)));
break;
case 15: // Return SXY2 from SXYP
if (_Rt_) {
gen.Ldr(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_DATA_OFFSET(14)));
}
gen.Ldr(dest, MemOperand(contextPointer, COP2_DATA_OFFSET(14)));
break;
case 28:
case 29: // Fallback for IRGB/ORGB
gen.Mov(arg1, _Rd_);
gen.Mov(arg1, index);
call(MFC2Wrapper); // result in w0
if (_Rt_) {
allocateRegWithoutLoad(_Rt_); // Reallocate the reg in case the call thrashed it
m_gprs[_Rt_].setWriteback(true);
gen.Mov(m_gprs[_Rt_].allocatedReg, w0);
}
gen.Mov(dest, w0);
break;
default:
if (_Rt_) {
gen.Ldr(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_DATA_OFFSET(_Rd_)));
}
gen.Ldr(dest, MemOperand(contextPointer, COP2_DATA_OFFSET(index)));
break;
}
}
void DynaRecCPU::recMFC2(uint32_t code) {
if (!_Rt_) {
// Reading IRGB/ORGB has side effects, so it still has to happen even with the result thrown away
if (_Rd_ == 28 || _Rd_ == 29) {
gen.Mov(arg1, _Rd_);
call(MFC2Wrapper);
}
return;
}
const auto loadDelayDependency = getLoadDelayDependencyType(_Rt_);
if (loadDelayDependency != LoadDelayDependencyType::NoDependency) {
loadGTEDataRegister(w0, _Rd_);
storeDelayedLoad(_Rt_, loadDelayDependency);
return;
}
// If we won't emulate the load delay, make sure to cancel any pending loads that might trample the value
maybeCancelDelayedLoad(_Rt_);
allocateRegWithoutLoad(_Rt_);
m_gprs[_Rt_].setWriteback(true);
switch (_Rd_) {
// Fallback for IRGB/ORGB. Can't use loadGTEDataRegister for these because the call might unallocate $rt
case 28:
case 29:
gen.Mov(arg1, _Rd_);
call(MFC2Wrapper); // result in w0
allocateRegWithoutLoad(_Rt_); // Reallocate the reg in case the call thrashed it
m_gprs[_Rt_].setWriteback(true);
gen.Mov(m_gprs[_Rt_].allocatedReg, w0);
break;
default:
loadGTEDataRegister(m_gprs[_Rt_].allocatedReg, _Rd_);
break;
}
}
void DynaRecCPU::recCFC2(uint32_t code) {
if (_Rt_) {
maybeCancelDelayedLoad(_Rt_);
allocateRegWithoutLoad(_Rt_);
m_gprs[_Rt_].setWriteback(true);
gen.Ldr(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_CONTROL_OFFSET(_Rd_)));
if (!_Rt_) return;
const auto loadDelayDependency = getLoadDelayDependencyType(_Rt_);
if (loadDelayDependency != LoadDelayDependencyType::NoDependency) {
gen.Ldr(w0, MemOperand(contextPointer, COP2_CONTROL_OFFSET(_Rd_)));
storeDelayedLoad(_Rt_, loadDelayDependency);
return;
}
// If we won't emulate the load delay, make sure to cancel any pending loads that might trample the value
maybeCancelDelayedLoad(_Rt_);
allocateRegWithoutLoad(_Rt_);
m_gprs[_Rt_].setWriteback(true);
gen.Ldr(m_gprs[_Rt_].allocatedReg, MemOperand(contextPointer, COP2_CONTROL_OFFSET(_Rd_)));
}
void DynaRecCPU::recLWC2(uint32_t code) {
+67 -3
View File
@@ -288,7 +288,7 @@ void DynaRecCPU::recDIV(uint32_t code) {
// LO = 1 or -1 depending on the sign of $rs. HI = $rs
gen.Mov(w0, m_gprs[_Rs_].val & 0x80000000 ? 1 : -1);
gen.Mov(w1, m_gprs[_Rs_].val);
gen.Stp(w0, w1, MemOperand(contextPointer, HI_OFFSET));
gen.Stp(w0, w1, MemOperand(contextPointer, LO_OFFSET));
}
else {
@@ -488,6 +488,12 @@ void DynaRecCPU::recLWL(uint32_t code) {
// Depending on the low 3 bits of the unaligned address
static const uint64_t MASKS_AND_SHIFTS[4] = {0x00FFFFFF00000018, 0x0000FFFF00000010, 0x000000FF00000008, 0};
// LWL writes $rt straight away rather than going through the delay machinery, so a load still pending on $rt
// would trample the merged value one instruction later.
if (_Rt_) {
maybeCancelDelayedLoad(_Rt_);
}
if (m_gprs[_Rs_].isConst() && m_gprs[_Rt_].isConst()) { // Both previous register value and address are constant
const uint32_t address = m_gprs[_Rs_].val + _Imm_;
const uint32_t alignedAddress = address & ~3;
@@ -551,7 +557,9 @@ void DynaRecCPU::recLWL(uint32_t code) {
if (_Rt_) {
// The call might have flushed $rs, so we need to allocate it again, and also allocate $rt
alloc_rs_wb_rt(code);
// $rt is read-modify-write here, so it has to be loaded, not just reserved for writeback
alloc_rt_rs(code);
m_gprs[_Rt_].setWriteback(true);
gen.moveAndAdd(w1, m_gprs[_Rs_].allocatedReg, _Imm_); // Address in w1
gen.And(w1, w1, 3); // Get the low 2 bits
gen.Mov(x3, (uintptr_t)&MASKS_AND_SHIFTS); // Base to mask and shift lookup table in x3
@@ -570,6 +578,11 @@ void DynaRecCPU::recLWR(uint32_t code) {
// Depending on the low 3 bits of the unaligned address
static const uint64_t MASKS_AND_SHIFTS[4] = {0, 0xFF00000000000008, 0xFFFF000000000010, 0xFFFFFF0000000018};
// Same as LWL: cancel a pending load on $rt, since this writes $rt immediately
if (_Rt_) {
maybeCancelDelayedLoad(_Rt_);
}
if (m_gprs[_Rs_].isConst() && m_gprs[_Rt_].isConst()) { // Both previous register value and address are constant
const uint32_t address = m_gprs[_Rs_].val + _Imm_;
const uint32_t alignedAddress = address & ~3;
@@ -633,7 +646,9 @@ void DynaRecCPU::recLWR(uint32_t code) {
if (_Rt_) {
// The call might have flushed $rs, so we need to allocate it again, and also allocate $rt
alloc_rs_wb_rt(code);
// $rt is read-modify-write here, so it has to be loaded, not just reserved for writeback
alloc_rt_rs(code);
m_gprs[_Rt_].setWriteback(true);
gen.moveAndAdd(w1, m_gprs[_Rs_].allocatedReg, _Imm_); // Address in w1 again
gen.And(w1, w1, 3); // Get the low 2 bits
gen.Mov(x3, (uintptr_t)&MASKS_AND_SHIFTS); // Form PC-relative address to mask and shift lookup table
@@ -676,8 +691,57 @@ void DynaRecCPU::recMFHI(uint32_t code) {
gen.Ldr(m_gprs[_Rd_].allocatedReg, MemOperand(contextPointer, HI_OFFSET));
}
// Recompile a load whose result the next instruction actually reads, so the write to $rt has to be held back by
// one instruction. The value goes to memory and either processDelayedLoad or the load delay handler picks it up.
template <int size, bool signExtend>
void DynaRecCPU::recompileLoadWithDelay(uint32_t code, LoadDelayDependencyType type) {
if (m_gprs[_Rs_].isConst()) { // Store the address in first argument register
gen.Mov(arg1, m_gprs[_Rs_].val + _Imm_);
} else {
allocateReg(_Rs_);
gen.moveAndAdd(arg1, m_gprs[_Rs_].allocatedReg, _Imm_);
}
switch (size) {
case 8:
call(read8Wrapper);
break;
case 16:
call(read16Wrapper);
break;
case 32:
call(read32Wrapper);
break;
default:
PCSX::g_system->message("Invalid size for memory load in dynarec. Instruction %08x\n", m_regs.code);
break;
}
if (_Rt_) {
switch (size) { // The read result is in w0. 32-bit loads need no extension.
case 8:
signExtend ? gen.Sxtb(w0, w0) : gen.Uxtb(w0, w0);
break;
case 16:
signExtend ? gen.Sxth(w0, w0) : gen.Uxth(w0, w0);
break;
}
storeDelayedLoad(_Rt_, type);
}
}
template <int size, bool signExtend>
void DynaRecCPU::recompileLoad(uint32_t code) {
const auto loadDelayDependency = getLoadDelayDependencyType(_Rt_);
if (loadDelayDependency != LoadDelayDependencyType::NoDependency) {
recompileLoadWithDelay<size, signExtend>(code, loadDelayDependency);
return;
}
// If we won't emulate the load delay, make sure to cancel any pending loads that might trample the value
maybeCancelDelayedLoad(_Rt_);
if (m_gprs[_Rs_].isConst()) { // Store the address in first argument register
const uint32_t addr = m_gprs[_Rs_].val + _Imm_;
const auto pointer = PCSX::g_emulator->m_mem->pointerRead(addr);
+189 -4
View File
@@ -78,6 +78,8 @@ bool DynaRecCPU::Init() {
}
m_gprs[0].markConst(0); // $zero is always zero
m_currentDelayedLoad = 0;
m_runtimeLoadDelay.active = false;
#if defined(__APPLE__)
// Check to make sure code buffer memory was allocated
@@ -219,9 +221,39 @@ void DynaRecCPU::emitDispatcher() {
// Do arg2 = x0 + (x3 << 3). Now arg2 points to the address we'll store the block callback to.
gen.Add(arg2.X(), x0, Operand(x3, LSL, 3));
loadThisPointer(arg1.X());
gen.Mov(arg3, 0); // Do not fully emulate load delays at first
call(recRecompileWrapper);
gen.Br(x0); // Jump to compiled block
// Code for recompiling the current block with full load delay support. Reached from the check at the top of a
// block that was compiled without it, so x0 and x3 still hold what emitBlockLookup left there.
gen.align();
m_needFullLoadDelays = gen.getCurr<DynarecCallback>();
gen.Add(arg2.X(), x0, Operand(x3, LSL, 3));
loadThisPointer(arg1.X());
gen.Mov(arg3, 1); // Fully emulate load delays
call(recRecompileWrapper);
gen.Br(x0); // Jump to the newly compiled block
// Code for committing a load delay inherited from the previous block. Writes the pending value straight into
// the guest register file. Only touches w4, w5 and x6, none of which the register allocator can hand out, and
// it is only ever called with the register cache already flushed.
gen.align();
m_loadDelayHandler = gen.getCurr<DynarecCallback>();
{
const auto isActiveOffset = (uintptr_t)&m_runtimeLoadDelay.active - (uintptr_t)this;
const auto indexOffset = (uintptr_t)&m_runtimeLoadDelay.index - (uintptr_t)this;
const auto valueOffset = (uintptr_t)&m_runtimeLoadDelay.value - (uintptr_t)this;
gen.Ldr(w4, MemOperand(contextPointer, indexOffset)); // Index of the register that needs to be written
gen.Ldr(w5, MemOperand(contextPointer, valueOffset)); // Value it needs to be written with
gen.Add(x6, contextPointer, (int64_t)GPR_OFFSET(0)); // Base of the guest register file
gen.Str(w5, MemOperand(x6, x4, LSL, 2)); // Write the value
gen.Strb(wzr, MemOperand(contextPointer, isActiveOffset)); // The load is no longer pending
gen.Ret();
}
// Code for when the block we've jumped to is invalid. Throws an error and exits
gen.align();
m_invalidBlock = gen.getCurr<DynarecCallback>();
@@ -232,9 +264,98 @@ void DynaRecCPU::emitDispatcher() {
gen.ready(); // Ready code buffer before emulator jumps into dispatcher for the first time
}
// Look at the instruction after the load currently being compiled and work out whether it actually reads the
// register the load targets. Most loads are not consumed immediately, so most of them need no delay emulated at all.
DynaRecCPU::LoadDelayDependencyType DynaRecCPU::getLoadDelayDependencyType(int index) {
// Always emulate load delays when there's a load in a branch delay slot
if (m_stopCompiling && index != 0) return LoadDelayDependencyType::DependencyAcrossBlocks;
if (index == 0) { // Loads to $zero go to the void, so don't bother emulating it as a delayed load
return LoadDelayDependencyType::NoDependency;
}
const uint32_t instruction = PCSX::g_emulator->m_mem->read32(m_pc, PCSX::Memory::ReadType::Instr);
const auto rt = (instruction >> 16) & 0x1f;
const auto rs = (instruction >> 21) & 0x1f;
const auto opcode = instruction >> 26;
enum : uint8_t { NoDep, DepIfRs, DepIfRt, DepIfRsOrRt };
// TODO: Handle LWL/LWR/SWL/SWR delay slots properly
static constexpr uint8_t mainDependencyList[64] = {
NoDep, DepIfRs, NoDep, NoDep, DepIfRsOrRt, DepIfRsOrRt, DepIfRs, DepIfRs, // 0x0-0x7
DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, NoDep, // 0x8-0xF
NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, // 0x10-0x17
NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, NoDep, // 0x18-0x1F
DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, DepIfRs, NoDep, // 0x20-0x27
DepIfRs, DepIfRs, DepIfRs, DepIfRs, NoDep, NoDep, DepIfRs, NoDep, // 0x28-0x2F
DepIfRs, DepIfRs, DepIfRs, DepIfRs, NoDep, NoDep, NoDep, NoDep, // 0x30-0x37
DepIfRs, DepIfRs, DepIfRs, DepIfRs, NoDep, NoDep, NoDep, NoDep, // 0x38-0x3F
};
static constexpr uint8_t specialDependencyList[64] = {
DepIfRsOrRt, NoDep, DepIfRt, DepIfRt,
DepIfRsOrRt, NoDep, DepIfRsOrRt, DepIfRsOrRt, // 0x0-0x7
NoDep, NoDep, NoDep, NoDep,
NoDep, NoDep, NoDep, NoDep, // 0x8-0xF
NoDep, DepIfRs, NoDep, DepIfRs,
NoDep, NoDep, NoDep, NoDep, // 0x10-0x17
NoDep, NoDep, NoDep, NoDep,
NoDep, NoDep, NoDep, NoDep, // 0x18-0x1F
DepIfRsOrRt, DepIfRsOrRt, DepIfRsOrRt, DepIfRsOrRt,
DepIfRsOrRt, DepIfRsOrRt, DepIfRsOrRt, DepIfRsOrRt, // 0x20-0x27
NoDep, NoDep, DepIfRsOrRt, DepIfRsOrRt,
NoDep, NoDep, NoDep, NoDep, // 0x28-0x2F
NoDep, NoDep, NoDep, NoDep,
NoDep, NoDep, NoDep, NoDep, // 0x30-0x37
NoDep, NoDep, NoDep, NoDep,
NoDep, NoDep, NoDep, NoDep, // 0x38-0x3F
};
uint8_t dependencyType = NoDep;
switch (opcode) {
case 0: // Special instructions
dependencyType = specialDependencyList[instruction & 0x3F];
break;
case 0x10: { // COP0 instructions also need special handling
// We need to emulate the delay if the rs field is 4, ie the instruction is MTC0, and "index" is the source
return (rs == 4 && rt == index) ? LoadDelayDependencyType::DependencyInsideBlock
: LoadDelayDependencyType::NoDependency;
} break;
case 0x12: { // COP2 instructions too
// Check if the instruction is MFC2 or CFC2 with the source being $rt
const bool isMove = (instruction & 0x3F) == 0 && (rs == 4 || rs == 6);
return (isMove && rt == index) ? LoadDelayDependencyType::DependencyInsideBlock
: LoadDelayDependencyType::NoDependency;
break;
}
default:
dependencyType = mainDependencyList[opcode];
break;
}
switch (dependencyType) {
case NoDep:
return LoadDelayDependencyType::NoDependency;
case DepIfRs:
return (index == rs) ? LoadDelayDependencyType::DependencyInsideBlock
: LoadDelayDependencyType::NoDependency;
case DepIfRt:
return (index == rt) ? LoadDelayDependencyType::DependencyInsideBlock
: LoadDelayDependencyType::NoDependency;
case DepIfRsOrRt:
return (index == rs || index == rt) ? LoadDelayDependencyType::DependencyInsideBlock
: LoadDelayDependencyType::NoDependency;
}
// Unreachable, but returning nothing would technically be UB.
abort();
return LoadDelayDependencyType::NoDependency;
}
// Compile a block, write address of compiled code to *callback
// Returns the address of the compiled block
DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bool align) {
DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bool align,
bool fullLoadDelayEmulation) {
m_stopCompiling = false;
m_inDelaySlot = false;
m_nextIsDelaySlot = false;
@@ -243,6 +364,8 @@ DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bo
m_pcWrittenBack = false;
m_linkedPC = std::nullopt;
m_pc = pc & ~3;
m_firstInstruction = true;
m_fullLoadDelayEmulation = fullLoadDelayEmulation;
const auto startingPC = m_pc;
int count = 0; // How many instructions have we compiled?
@@ -261,6 +384,19 @@ DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bo
const auto blockStart = gen.getCurr<DynarecCallback>();
*callback = blockStart;
if (!m_fullLoadDelayEmulation) {
// Check if there's a load delay pending at the start of the block. If so, this block has to be recompiled
// with full load delay support, because it cannot see the pending load otherwise.
Label noPendingLoad;
const auto isActiveOffset = (uintptr_t)&m_runtimeLoadDelay.active - (uintptr_t)this;
gen.Ldrb(w4, MemOperand(contextPointer, isActiveOffset));
gen.Cbz(w4, &noPendingLoad);
jmp((void*)m_needFullLoadDelays);
gen.L(noPendingLoad);
}
handleKernelCall(); // Check if this is a kernel call vector, emit some extra code in that case.
auto shouldContinue = [&]() {
@@ -270,19 +406,21 @@ DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bo
if (m_stopCompiling) {
return false;
}
if (count >= MAX_BLOCK_SIZE) { // TODO: Check delay slots here
// Don't end the block while a load delay is still pending, there'd be nothing left to commit it
if (count >= MAX_BLOCK_SIZE && !m_delayedLoadInfo[0].active && !m_delayedLoadInfo[1].active) {
return false;
}
return true;
};
while (shouldContinue()) {
// Compile the instruction at m_pc. Returns false if it could not be fetched.
auto compileInstruction = [&]() {
m_inDelaySlot = m_nextIsDelaySlot;
m_nextIsDelaySlot = false;
uint32_t* p = PCSX::g_emulator->m_mem->getPointer<uint32_t>(m_pc);
if (p == nullptr) { // Error if it can't be fetched
return m_invalidBlock;
return false;
}
uint32_t code = m_regs.code = *p; // Actually read the instruction
@@ -291,6 +429,53 @@ DynarecCallback DynaRecCPU::recompile(DynarecCallback* callback, uint32_t pc, bo
const auto func = m_recBSC[code >> 26]; // Look up the opcode in our decoding LUT
(*this.*func)(code); // Jump into the handler to recompile it
return true;
};
// Commit the load that the previous instruction started, if any. The ping-pong index is toggled first, so the
// slot committed after instruction N is the one instruction N-1 wrote. That is the one instruction of delay.
const auto processDelayedLoad = [this]() {
m_currentDelayedLoad ^= 1;
auto& delayedLoad = m_delayedLoadInfo[m_currentDelayedLoad];
if (delayedLoad.active) {
delayedLoad.active = false;
const unsigned index = delayedLoad.index;
const auto delayedValueOffset = (uintptr_t)&delayedLoad.value - (uintptr_t)this;
allocateRegWithoutLoad(index);
m_gprs[index].setWriteback(true);
gen.Ldr(m_gprs[index].allocatedReg, MemOperand(contextPointer, delayedValueOffset));
}
};
// Commit a load inherited from the previous block. Runs after the first instruction, which is the only one that
// may not see it yet.
const auto resolveInitialLoadDelay = [this]() {
if (!m_fullLoadDelayEmulation) return;
flushRegs();
Label noDelayedLoad;
const auto isActiveOffset = (uintptr_t)&m_runtimeLoadDelay.active - (uintptr_t)this;
gen.Ldrb(w4, MemOperand(contextPointer, isActiveOffset));
gen.Cbz(w4, &noDelayedLoad);
call(m_loadDelayHandler);
gen.L(noDelayedLoad);
};
if (!compileInstruction()) {
return m_invalidBlock;
}
resolveInitialLoadDelay();
processDelayedLoad();
m_firstInstruction = false;
while (shouldContinue()) {
if (!compileInstruction()) {
return m_invalidBlock;
}
processDelayedLoad();
}
flushRegs();
+66 -3
View File
@@ -110,6 +110,9 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
DynarecCallback m_returnFromBlock; // Pointer to the code that will be executed when returning from a block
DynarecCallback m_uncompiledBlock; // Pointer to the code that will be executed when jumping to an uncompiled block
DynarecCallback m_invalidBlock; // Pointer to the code that will be executed the PC is invalid
DynarecCallback m_loadDelayHandler; // Pointer to the code that will handle load delays at the start of a block
// Pointer to the code that will be executed when a block needs to be recompiled with full load delay support
DynarecCallback m_needFullLoadDelays;
uint32_t m_pc;
Emitter gen;
@@ -119,6 +122,19 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
uint32_t m_ramSize; // RAM is 2MB on retail units, 8MB on some DTL units (Can be toggled in GUI)
const int MAX_BLOCK_SIZE = 50;
bool m_firstInstruction; // Is the instruction being compiled the first one of the block?
bool m_fullLoadDelayEmulation; // Should we emulate load delays that cross block boundaries?
// Used to hold info when we've got a load delay between the end of a block and the start of another
// For example, when there's an lw instruction in the delay slot of a branch
struct {
bool active;
int index;
uint32_t value;
} m_runtimeLoadDelay;
enum class LoadDelayDependencyType { NoDependency, DependencyInsideBlock, DependencyAcrossBlocks };
enum class RegState { Unknown, Constant };
enum class LoadingMode { DoNotLoad, Load };
// Renamed temporarily to Reg since it collides with Register in vixl::aarch64 namespace
@@ -184,7 +200,9 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
inline bool isPcValid(uint32_t addr) { return m_recompilerLUT[addr >> 16] != m_dummyBlocks; }
DynarecCallback* getBlockPointer(uint32_t pc);
DynarecCallback recompile(DynarecCallback* callback, uint32_t pc, bool align = true);
DynarecCallback recompile(DynarecCallback* callback, uint32_t pc, bool align = true,
bool fullLoadDelayEmulation = false);
LoadDelayDependencyType getLoadDelayDependencyType(int index);
void error();
void flushCache();
void handleLinking();
@@ -201,8 +219,9 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
static void recErrorWrapper(DynaRecCPU* that) { that->error(); }
static void signalShellReached(DynaRecCPU* that);
static DynarecCallback recRecompileWrapper(DynaRecCPU* that, DynarecCallback* callback) {
return that->recompile(callback, that->m_regs.pc);
static DynarecCallback recRecompileWrapper(DynaRecCPU* that, DynarecCallback* callback,
bool fullLoadDelayEmulation) {
return that->recompile(callback, that->m_regs.pc, true, fullLoadDelayEmulation);
}
template <uint32_t pc>
@@ -335,12 +354,51 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
}
void maybeCancelDelayedLoad(uint32_t index) {
// A load delay inherited from the previous block is only visible to the first instruction of this one, so
// that is the only place where it can be cancelled at runtime.
if (m_fullLoadDelayEmulation && m_firstInstruction) {
const auto indexOffset = (uintptr_t)&m_runtimeLoadDelay.index - (uintptr_t)this;
const auto isActiveOffset = (uintptr_t)&m_runtimeLoadDelay.active - (uintptr_t)this;
Label noDelayedLoad;
gen.Ldr(w4, MemOperand(contextPointer, indexOffset));
gen.Cmp(w4, index);
gen.B(&noDelayedLoad, ne);
gen.Strb(wzr, MemOperand(contextPointer, isActiveOffset));
gen.L(noDelayedLoad);
}
const unsigned other = m_currentDelayedLoad ^ 1;
if (m_delayedLoadInfo[other].index == index) {
m_delayedLoadInfo[other].active = false;
}
}
// Park the value currently in w0 as a load delayed by one instruction, targeting guest register "index".
// Either processDelayedLoad picks it back up next instruction, or the load delay handler does at the start of
// the next block. Clobbers w4.
void storeDelayedLoad(unsigned index, LoadDelayDependencyType type) {
m_delayedLoadInfo[m_currentDelayedLoad].active = true;
if (type == LoadDelayDependencyType::DependencyAcrossBlocks) {
const auto valueOffset = (uintptr_t)&m_runtimeLoadDelay.value - (uintptr_t)this;
const auto isActiveOffset = (uintptr_t)&m_runtimeLoadDelay.active - (uintptr_t)this;
const auto indexOffset = (uintptr_t)&m_runtimeLoadDelay.index - (uintptr_t)this;
gen.Str(w0, MemOperand(contextPointer, valueOffset));
gen.Mov(w4, 1);
gen.Strb(w4, MemOperand(contextPointer, isActiveOffset));
gen.Mov(w4, index);
gen.Str(w4, MemOperand(contextPointer, indexOffset));
} else {
auto& delayedLoad = m_delayedLoadInfo[m_currentDelayedLoad];
const auto valueOffset = (uintptr_t)&delayedLoad.value - (uintptr_t)this;
delayedLoad.index = index;
gen.Str(w0, MemOperand(contextPointer, valueOffset));
}
}
// Instruction definitions
void recUnknown(uint32_t code);
void recSpecial(uint32_t code);
@@ -443,9 +501,14 @@ class DynaRecCPU final : public PCSX::R3000Acpu {
template <bool loadSR>
void testSoftwareInterrupt();
void loadGTEDataRegister(Register dest, int index);
template <int size, bool signExtend>
void recompileLoad(uint32_t code);
template <int size, bool signExtend>
void recompileLoadWithDelay(uint32_t code, LoadDelayDependencyType type);
const recompilationFunc m_recBSC[64] = {
&DynaRecCPU::recSpecial, &DynaRecCPU::recREGIMM, &DynaRecCPU::recJ, &DynaRecCPU::recJAL, // 00
&DynaRecCPU::recBEQ, &DynaRecCPU::recBNE, &DynaRecCPU::recBLEZ, &DynaRecCPU::recBGTZ, // 04
+15
View File
@@ -21,6 +21,8 @@
#include <filesystem>
#include "support/binpath.h"
PCSX::Arguments::Arguments(const CommandLine::args& args) {
if (args.get<bool>("lua_stdout") || args.get<bool>("no-ui") || args.get<bool>("cli")) {
m_luaStdoutEnabled = true;
@@ -35,6 +37,19 @@ PCSX::Arguments::Arguments(const CommandLine::args& args) {
if (std::filesystem::exists("pcsx.json")) m_portable = true;
if (std::filesystem::exists(std::filesystem::path("vsprojects") / "pcsx-redux.sln")) m_portable = true;
if (std::filesystem::exists(std::filesystem::path("..") / "pcsx-redux.sln")) m_portable = true;
if (!m_portable) {
// The probes above are relative to the current directory, which is only the install
// directory when the binary was started from it. A shortcut with its own start-in, a
// file association on a disc image, or a frontend will all hand us something else, so
// look next to the binary too, and anchor the portable directory there when that's
// what matched. Without this, whether an install is portable depends on how it was
// launched rather than on where it lives.
std::filesystem::path binDir = std::filesystem::path(BinPath::getExecutablePath()).parent_path();
if (!binDir.empty() && std::filesystem::exists(binDir / "pcsx.json")) {
m_portable = true;
m_portablePath = binDir.string();
}
}
if (args.get<bool>("no-portable")) m_portable = false;
if (args.get<bool>("safe") || args.get<bool>("testmode") || args.get<bool>("cli")) m_safeModeEnabled = true;
if (args.get<bool>("resetui")) m_uiResetRequested = true;
+6 -5
View File
@@ -51,9 +51,9 @@ class Arguments {
// Returns true if the the flag -testmode was used.
bool isTestModeEnabled() const { return m_testModeEnabled; }
// Returns true if the the flag -portable was used, if the executable is
// located in the same directory as the pcsx.json file, or if the
// executable is being run from its source tree.
// Returns true if the flag -portable was used, if a pcsx.json file exists in
// the current directory or next to the executable, or if the executable is
// being run from its source tree.
bool isPortable() const { return m_portable; }
// Returns true if the safe mode was enabled. This implies
@@ -77,8 +77,9 @@ class Arguments {
// Toggled with the flags -viewports / -no-viewports.
bool isViewportsEnabled() const { return m_viewportsEnabled; }
// Returns the path to the portable directory.
// Set with the flag -portable.
// Returns the path to the portable directory. Set with the flag -portable, or
// to the executable's directory when that is where the pcsx.json was found.
// Empty means the current directory.
std::string_view getPortablePath() const { return m_portablePath; }
private:
+2
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/gdb-server.h"
#include <assert.h>
+2
View File
@@ -19,6 +19,8 @@
#pragma once
#include <uv.h>
#include <assert.h>
#include <cstdarg>
+93
View File
@@ -19,12 +19,58 @@
#include "core/gpulogger.h"
#include <algorithm>
#include <limits>
#include "core/gpu.h"
#include "core/psxemulator.h"
#include "core/r3000a.h"
#include "core/system.h"
#include "imgui/imgui.h"
namespace {
// Separating axis test between a triangle and an axis aligned rectangle. Logged::getVertices hands
// out triangles for everything, including lines and palette rows, so this single test covers every
// command's footprint. Degenerate triangles project down to a segment or a point on the edge
// normals, which the interval overlap below still handles correctly.
bool triangleIntersectsRect(PCSX::OpenGL::ivec2 v1, PCSX::OpenGL::ivec2 v2, PCSX::OpenGL::ivec2 v3, int rx, int ry,
int rw, int rh) {
const int rx2 = rx + rw;
const int ry2 = ry + rh;
const int tx[3] = {v1.x(), v2.x(), v3.x()};
const int ty[3] = {v1.y(), v2.y(), v3.y()};
if (std::max({tx[0], tx[1], tx[2]}) < rx || std::min({tx[0], tx[1], tx[2]}) > rx2) return false;
if (std::max({ty[0], ty[1], ty[2]}) < ry || std::min({ty[0], ty[1], ty[2]}) > ry2) return false;
for (unsigned i = 0; i < 3; i++) {
const unsigned j = (i + 1) % 3;
const int nx = ty[j] - ty[i];
const int ny = tx[i] - tx[j];
if ((nx == 0) && (ny == 0)) continue;
int triMin = std::numeric_limits<int>::max();
int triMax = std::numeric_limits<int>::min();
for (unsigned k = 0; k < 3; k++) {
const int p = nx * tx[k] + ny * ty[k];
triMin = std::min(triMin, p);
triMax = std::max(triMax, p);
}
int rectMin = std::numeric_limits<int>::max();
int rectMax = std::numeric_limits<int>::min();
for (unsigned k = 0; k < 4; k++) {
const int p = nx * ((k & 1) ? rx2 : rx) + ny * ((k & 2) ? ry2 : ry);
rectMin = std::min(rectMin, p);
rectMax = std::max(rectMax, p);
}
if ((triMax < rectMin) || (rectMax < triMin)) return false;
}
return true;
}
} // namespace
static const char* const c_vtx = R"(
#version 330 core
@@ -135,6 +181,52 @@ void PCSX::GPULogger::flush() {
m_verticesCount = 0;
}
void PCSX::GPULogger::checkVramBreakpoints(GPU::Logged* node) {
bool watchingReads = false;
bool watchingWrites = false;
for (auto& bp : m_vramBreakpoints) {
if (!bp.enabled) continue;
watchingReads |= bp.onRead;
watchingWrites |= bp.onWrite;
}
if (!watchingReads && !watchingWrites) return;
unsigned hit = m_vramBreakpoints.size();
auto scan = [this, node, &hit](GPU::Logged::PixelOp op) {
node->getVertices(
[this, &hit, op](auto v1, auto v2, auto v3) {
if (hit != m_vramBreakpoints.size()) return;
for (unsigned i = 0; i < m_vramBreakpoints.size(); i++) {
auto& bp = m_vramBreakpoints[i];
if (!bp.enabled) continue;
if (!(op == GPU::Logged::PixelOp::READ ? bp.onRead : bp.onWrite)) continue;
if (triangleIntersectsRect(v1, v2, v3, bp.area.x, bp.area.y, bp.area.w, bp.area.h)) {
hit = i;
return;
}
}
},
op);
};
const char* what = _("write");
if (watchingWrites) scan(GPU::Logged::PixelOp::WRITE);
if ((hit == m_vramBreakpoints.size()) && watchingReads) {
what = _("read");
scan(GPU::Logged::PixelOp::READ);
}
if (hit == m_vramBreakpoints.size()) return;
const auto& bp = m_vramBreakpoints[hit];
const auto name = node->getName();
node->highlight = true;
g_system->log(LogClass::GPU,
_("VRAM breakpoint %i triggered: %.*s did a %s intersecting %ix%i at %i,%i - PC = 0x%08x\n"), hit,
int(name.size()), name.data(), what, bp.area.w, bp.area.h, bp.area.x, bp.area.y, node->pc);
g_system->m_eventBus->signal(Events::GUI::JumpToPC{node->pc});
g_system->pause();
}
void PCSX::GPULogger::addNodeInternal(GPU::Logged* node, GPU::Logged::Origin origin, uint32_t value, uint32_t length) {
auto frame = m_frameCounter;
@@ -152,6 +244,7 @@ void PCSX::GPULogger::addNodeInternal(GPU::Logged* node, GPU::Logged::Origin ori
node->frame = frame;
node->generateStatsInfo();
m_list.push_back(node);
checkVramBreakpoints(node);
if (!m_hasFramebuffers) return;
+18
View File
@@ -22,6 +22,7 @@
#include <stdint.h>
#include <array>
#include <vector>
#include "core/gpu.h"
#include "support/eventbus.h"
@@ -36,6 +37,21 @@ class GPULogger;
class GPULogger {
public:
// A rectangle of VRAM to watch. Any logged command whose read or write footprint intersects an
// enabled breakpoint pauses the emulator. The footprints come straight from Logged::getVertices,
// so this covers primitives, blits, and the palette rows of paletted textures alike.
struct VramBreakpoint {
union {
int raw[4];
struct {
int x, y, w, h;
};
} area = {0, 0, 64, 64};
bool onRead = false;
bool onWrite = true;
bool enabled = true;
};
GPULogger();
void clearFrameLog() { m_list.destroyAll(); }
template <typename T>
@@ -56,10 +72,12 @@ class GPULogger {
private:
void startNewFrame();
void addNodeInternal(GPU::Logged* node, GPU::Logged::Origin, uint32_t value, uint32_t length);
void checkVramBreakpoints(GPU::Logged* node);
EventBus::Listener m_listener;
bool m_enabled = false;
bool m_breakOnVSync = false;
std::vector<VramBreakpoint> m_vramBreakpoints;
bool m_hasFramebuffers = false;
uint64_t m_frameCounter = 0;
GPU::LoggedList m_list;
+2
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <sys/stat.h>
#include "core/memorycard.h"
#include "core/sio.h"
+298 -338
View File
@@ -17,11 +17,10 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#define GLFW_INCLUDE_NONE
#define _USE_MATH_DEFINES
#include "core/pad.h"
#include <GLFW/glfw3.h>
#include <SDL3/SDL.h>
#include <memory.h>
#include <algorithm>
@@ -69,52 +68,61 @@ class PadsImpl : public PCSX::Pads {
private:
PCSX::EventBus::Listener m_listener;
// This is a list of all of the valid GLFW gamepad IDs that we have found querying GLFW.
// A value of -1 means that there is no gamepad at that index.
int m_gamepadsMap[16] = {0};
// Open SDL gamepad handles, one per slot we've discovered. nullptr means the
// slot is unused. The slot index doubles as the user-visible "controller ID"
// saved in SettingControllerID.
SDL_Gamepad* m_gamepads[16] = {nullptr};
static constexpr int GLFW_GAMEPAD_BUTTON_LEFT_TRIGGER = GLFW_GAMEPAD_BUTTON_LAST + 1;
static constexpr int GLFW_GAMEPAD_BUTTON_RIGHT_TRIGGER = GLFW_GAMEPAD_BUTTON_LAST + 2;
static constexpr int GLFW_GAMEPAD_BUTTON_INVALID = GLFW_GAMEPAD_BUTTON_LAST + 3;
// Triggers are reported as axes by SDL but pad bindings treat them as
// virtual buttons. INVALID is encoded as SDL_GAMEPAD_BUTTON_INVALID (-1) so
// raw SDL queries naturally short-circuit. The trigger sentinels live above
// SDL_GAMEPAD_BUTTON_COUNT to avoid collisions with future enum entries.
static constexpr int PCSX_GAMEPAD_BUTTON_LEFT_TRIGGER = 100;
static constexpr int PCSX_GAMEPAD_BUTTON_RIGHT_TRIGGER = 101;
static constexpr int PCSX_GAMEPAD_BUTTON_INVALID = SDL_GAMEPAD_BUTTON_INVALID;
// settings block
// Pad keyboard bindings
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadUp"), GLFW_KEY_UP> Keyboard_PadUp;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadRight"), GLFW_KEY_RIGHT> Keyboard_PadRight;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadDown"), GLFW_KEY_DOWN> Keyboard_PadDown;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadLeft"), GLFW_KEY_LEFT> Keyboard_PadLeft;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadCross"), GLFW_KEY_X> Keyboard_PadCross;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadTriangle"), GLFW_KEY_S> Keyboard_PadTriangle;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSquare"), GLFW_KEY_Z> Keyboard_PadSquare;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadCircle"), GLFW_KEY_D> Keyboard_PadCircle;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSelect"), GLFW_KEY_BACKSPACE> Keyboard_PadSelect;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSstart"), GLFW_KEY_ENTER> Keyboard_PadStart;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL1"), GLFW_KEY_Q> Keyboard_PadL1;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL2"), GLFW_KEY_A> Keyboard_PadL2;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL3"), GLFW_KEY_W> Keyboard_PadL3;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR1"), GLFW_KEY_R> Keyboard_PadR1;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR2"), GLFW_KEY_F> Keyboard_PadR2;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR3"), GLFW_KEY_T> Keyboard_PadR3;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_AnalogMode"), GLFW_KEY_UNKNOWN> Keyboard_AnalogMode;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadUp"), SDL_SCANCODE_UP> Keyboard_PadUp;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadRight"), SDL_SCANCODE_RIGHT> Keyboard_PadRight;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadDown"), SDL_SCANCODE_DOWN> Keyboard_PadDown;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadLeft"), SDL_SCANCODE_LEFT> Keyboard_PadLeft;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadCross"), SDL_SCANCODE_X> Keyboard_PadCross;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadTriangle"), SDL_SCANCODE_S> Keyboard_PadTriangle;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSquare"), SDL_SCANCODE_Z> Keyboard_PadSquare;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadCircle"), SDL_SCANCODE_D> Keyboard_PadCircle;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSelect"), SDL_SCANCODE_BACKSPACE> Keyboard_PadSelect;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadSstart"), SDL_SCANCODE_RETURN> Keyboard_PadStart;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL1"), SDL_SCANCODE_Q> Keyboard_PadL1;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL2"), SDL_SCANCODE_A> Keyboard_PadL2;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadL3"), SDL_SCANCODE_W> Keyboard_PadL3;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR1"), SDL_SCANCODE_R> Keyboard_PadR1;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR2"), SDL_SCANCODE_F> Keyboard_PadR2;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_PadR3"), SDL_SCANCODE_T> Keyboard_PadR3;
typedef PCSX::Setting<int, TYPESTRING("Keyboard_AnalogMode"), SDL_SCANCODE_UNKNOWN> Keyboard_AnalogMode;
// Pad controller bindings
typedef PCSX::Setting<int, TYPESTRING("Controller_PadUp"), GLFW_GAMEPAD_BUTTON_DPAD_UP> Controller_PadUp;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadRight"), GLFW_GAMEPAD_BUTTON_DPAD_RIGHT> Controller_PadRight;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadDown"), GLFW_GAMEPAD_BUTTON_DPAD_DOWN> Controller_PadDown;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadLeft"), GLFW_GAMEPAD_BUTTON_DPAD_LEFT> Controller_PadLeft;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadCross"), GLFW_GAMEPAD_BUTTON_CROSS> Controller_PadCross;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadTriangle"), GLFW_GAMEPAD_BUTTON_TRIANGLE>
// Pad controller bindings. Defaults reference SDL gamepad button enums
// (PS-style face buttons via SDL_GAMEPAD_BUTTON_SOUTH/EAST/WEST/NORTH).
// Existing user configs that hand-edited these values to GLFW button enums
// will not round-trip; the in-tree UI does not expose per-button rebinding,
// so the impact is limited to hand-rolled JSON.
typedef PCSX::Setting<int, TYPESTRING("Controller_PadUp"), SDL_GAMEPAD_BUTTON_DPAD_UP> Controller_PadUp;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadRight"), SDL_GAMEPAD_BUTTON_DPAD_RIGHT> Controller_PadRight;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadDown"), SDL_GAMEPAD_BUTTON_DPAD_DOWN> Controller_PadDown;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadLeft"), SDL_GAMEPAD_BUTTON_DPAD_LEFT> Controller_PadLeft;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadCross"), SDL_GAMEPAD_BUTTON_SOUTH> Controller_PadCross;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadTriangle"), SDL_GAMEPAD_BUTTON_NORTH>
Controller_PadTriangle;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSquare"), GLFW_GAMEPAD_BUTTON_SQUARE> Controller_PadSquare;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadCircle"), GLFW_GAMEPAD_BUTTON_CIRCLE> Controller_PadCircle;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSelect"), GLFW_GAMEPAD_BUTTON_BACK> Controller_PadSelect;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSstart"), GLFW_GAMEPAD_BUTTON_START> Controller_PadStart;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL1"), GLFW_GAMEPAD_BUTTON_LEFT_BUMPER> Controller_PadL1;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL2"), GLFW_GAMEPAD_BUTTON_LEFT_TRIGGER> Controller_PadL2;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL3"), GLFW_GAMEPAD_BUTTON_LEFT_THUMB> Controller_PadL3;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR1"), GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER> Controller_PadR1;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR2"), GLFW_GAMEPAD_BUTTON_RIGHT_TRIGGER> Controller_PadR2;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR3"), GLFW_GAMEPAD_BUTTON_RIGHT_THUMB> Controller_PadR3;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSquare"), SDL_GAMEPAD_BUTTON_WEST> Controller_PadSquare;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadCircle"), SDL_GAMEPAD_BUTTON_EAST> Controller_PadCircle;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSelect"), SDL_GAMEPAD_BUTTON_BACK> Controller_PadSelect;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadSstart"), SDL_GAMEPAD_BUTTON_START> Controller_PadStart;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL1"), SDL_GAMEPAD_BUTTON_LEFT_SHOULDER> Controller_PadL1;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL2"), PCSX_GAMEPAD_BUTTON_LEFT_TRIGGER> Controller_PadL2;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadL3"), SDL_GAMEPAD_BUTTON_LEFT_STICK> Controller_PadL3;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR1"), SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER> Controller_PadR1;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR2"), PCSX_GAMEPAD_BUTTON_RIGHT_TRIGGER> Controller_PadR2;
typedef PCSX::Setting<int, TYPESTRING("Controller_PadR3"), SDL_GAMEPAD_BUTTON_RIGHT_STICK> Controller_PadR3;
typedef PCSX::Setting<InputType, TYPESTRING("PadType"), InputType::Auto> SettingInputType;
// These typestrings are kind of odd, but it's best not to change so as not to break old config files
@@ -165,7 +173,11 @@ class PadsImpl : public PCSX::Pads {
uint8_t poll(uint8_t value, uint32_t& padState);
uint8_t doDualshockCommand(uint32_t& padState);
void getButtons();
bool isControllerButtonPressed(int button, GLFWgamepadstate* state);
struct GamepadState {
bool buttons[SDL_GAMEPAD_BUTTON_COUNT];
float axes[SDL_GAMEPAD_AXIS_COUNT]; // normalized to [-1, 1] like GLFW reported
};
bool isControllerButtonPressed(int button, const GamepadState& state);
bool isControllerConnected() { return m_settings.get<SettingConnected>(); }
json getCfg();
@@ -183,7 +195,7 @@ class PadsImpl : public PCSX::Pads {
PadType m_type;
PadData m_data;
int m_padID = -1;
SDL_Gamepad* m_gamepad = nullptr;
int m_buttonToWait = -1;
bool m_changed = false;
@@ -207,230 +219,122 @@ class PadsImpl : public PCSX::Pads {
static PadsImpl* s_pads = nullptr;
static ImGuiKey GlfwKeyToImGuiKey(int key) {
switch (key) {
case GLFW_KEY_TAB:
return ImGuiKey_Tab;
case GLFW_KEY_LEFT:
return ImGuiKey_LeftArrow;
case GLFW_KEY_RIGHT:
return ImGuiKey_RightArrow;
case GLFW_KEY_UP:
return ImGuiKey_UpArrow;
case GLFW_KEY_DOWN:
return ImGuiKey_DownArrow;
case GLFW_KEY_PAGE_UP:
return ImGuiKey_PageUp;
case GLFW_KEY_PAGE_DOWN:
return ImGuiKey_PageDown;
case GLFW_KEY_HOME:
return ImGuiKey_Home;
case GLFW_KEY_END:
return ImGuiKey_End;
case GLFW_KEY_INSERT:
return ImGuiKey_Insert;
case GLFW_KEY_DELETE:
return ImGuiKey_Delete;
case GLFW_KEY_BACKSPACE:
return ImGuiKey_Backspace;
case GLFW_KEY_SPACE:
return ImGuiKey_Space;
case GLFW_KEY_ENTER:
return ImGuiKey_Enter;
case GLFW_KEY_ESCAPE:
return ImGuiKey_Escape;
case GLFW_KEY_APOSTROPHE:
return ImGuiKey_Apostrophe;
case GLFW_KEY_COMMA:
return ImGuiKey_Comma;
case GLFW_KEY_MINUS:
return ImGuiKey_Minus;
case GLFW_KEY_PERIOD:
return ImGuiKey_Period;
case GLFW_KEY_SLASH:
return ImGuiKey_Slash;
case GLFW_KEY_SEMICOLON:
return ImGuiKey_Semicolon;
case GLFW_KEY_EQUAL:
return ImGuiKey_Equal;
case GLFW_KEY_LEFT_BRACKET:
return ImGuiKey_LeftBracket;
case GLFW_KEY_BACKSLASH:
return ImGuiKey_Backslash;
case GLFW_KEY_RIGHT_BRACKET:
return ImGuiKey_RightBracket;
case GLFW_KEY_GRAVE_ACCENT:
return ImGuiKey_GraveAccent;
case GLFW_KEY_CAPS_LOCK:
return ImGuiKey_CapsLock;
case GLFW_KEY_SCROLL_LOCK:
return ImGuiKey_ScrollLock;
case GLFW_KEY_NUM_LOCK:
return ImGuiKey_NumLock;
case GLFW_KEY_PRINT_SCREEN:
return ImGuiKey_PrintScreen;
case GLFW_KEY_PAUSE:
return ImGuiKey_Pause;
case GLFW_KEY_KP_0:
return ImGuiKey_Keypad0;
case GLFW_KEY_KP_1:
return ImGuiKey_Keypad1;
case GLFW_KEY_KP_2:
return ImGuiKey_Keypad2;
case GLFW_KEY_KP_3:
return ImGuiKey_Keypad3;
case GLFW_KEY_KP_4:
return ImGuiKey_Keypad4;
case GLFW_KEY_KP_5:
return ImGuiKey_Keypad5;
case GLFW_KEY_KP_6:
return ImGuiKey_Keypad6;
case GLFW_KEY_KP_7:
return ImGuiKey_Keypad7;
case GLFW_KEY_KP_8:
return ImGuiKey_Keypad8;
case GLFW_KEY_KP_9:
return ImGuiKey_Keypad9;
case GLFW_KEY_KP_DECIMAL:
return ImGuiKey_KeypadDecimal;
case GLFW_KEY_KP_DIVIDE:
return ImGuiKey_KeypadDivide;
case GLFW_KEY_KP_MULTIPLY:
return ImGuiKey_KeypadMultiply;
case GLFW_KEY_KP_SUBTRACT:
return ImGuiKey_KeypadSubtract;
case GLFW_KEY_KP_ADD:
return ImGuiKey_KeypadAdd;
case GLFW_KEY_KP_ENTER:
return ImGuiKey_KeypadEnter;
case GLFW_KEY_KP_EQUAL:
return ImGuiKey_KeypadEqual;
case GLFW_KEY_LEFT_SHIFT:
return ImGuiKey_LeftShift;
case GLFW_KEY_LEFT_CONTROL:
return ImGuiKey_LeftCtrl;
case GLFW_KEY_LEFT_ALT:
return ImGuiKey_LeftAlt;
case GLFW_KEY_LEFT_SUPER:
return ImGuiKey_LeftSuper;
case GLFW_KEY_RIGHT_SHIFT:
return ImGuiKey_RightShift;
case GLFW_KEY_RIGHT_CONTROL:
return ImGuiKey_RightCtrl;
case GLFW_KEY_RIGHT_ALT:
return ImGuiKey_RightAlt;
case GLFW_KEY_RIGHT_SUPER:
return ImGuiKey_RightSuper;
case GLFW_KEY_MENU:
return ImGuiKey_Menu;
case GLFW_KEY_0:
return ImGuiKey_0;
case GLFW_KEY_1:
return ImGuiKey_1;
case GLFW_KEY_2:
return ImGuiKey_2;
case GLFW_KEY_3:
return ImGuiKey_3;
case GLFW_KEY_4:
return ImGuiKey_4;
case GLFW_KEY_5:
return ImGuiKey_5;
case GLFW_KEY_6:
return ImGuiKey_6;
case GLFW_KEY_7:
return ImGuiKey_7;
case GLFW_KEY_8:
return ImGuiKey_8;
case GLFW_KEY_9:
return ImGuiKey_9;
case GLFW_KEY_A:
return ImGuiKey_A;
case GLFW_KEY_B:
return ImGuiKey_B;
case GLFW_KEY_C:
return ImGuiKey_C;
case GLFW_KEY_D:
return ImGuiKey_D;
case GLFW_KEY_E:
return ImGuiKey_E;
case GLFW_KEY_F:
return ImGuiKey_F;
case GLFW_KEY_G:
return ImGuiKey_G;
case GLFW_KEY_H:
return ImGuiKey_H;
case GLFW_KEY_I:
return ImGuiKey_I;
case GLFW_KEY_J:
return ImGuiKey_J;
case GLFW_KEY_K:
return ImGuiKey_K;
case GLFW_KEY_L:
return ImGuiKey_L;
case GLFW_KEY_M:
return ImGuiKey_M;
case GLFW_KEY_N:
return ImGuiKey_N;
case GLFW_KEY_O:
return ImGuiKey_O;
case GLFW_KEY_P:
return ImGuiKey_P;
case GLFW_KEY_Q:
return ImGuiKey_Q;
case GLFW_KEY_R:
return ImGuiKey_R;
case GLFW_KEY_S:
return ImGuiKey_S;
case GLFW_KEY_T:
return ImGuiKey_T;
case GLFW_KEY_U:
return ImGuiKey_U;
case GLFW_KEY_V:
return ImGuiKey_V;
case GLFW_KEY_W:
return ImGuiKey_W;
case GLFW_KEY_X:
return ImGuiKey_X;
case GLFW_KEY_Y:
return ImGuiKey_Y;
case GLFW_KEY_Z:
return ImGuiKey_Z;
case GLFW_KEY_F1:
return ImGuiKey_F1;
case GLFW_KEY_F2:
return ImGuiKey_F2;
case GLFW_KEY_F3:
return ImGuiKey_F3;
case GLFW_KEY_F4:
return ImGuiKey_F4;
case GLFW_KEY_F5:
return ImGuiKey_F5;
case GLFW_KEY_F6:
return ImGuiKey_F6;
case GLFW_KEY_F7:
return ImGuiKey_F7;
case GLFW_KEY_F8:
return ImGuiKey_F8;
case GLFW_KEY_F9:
return ImGuiKey_F9;
case GLFW_KEY_F10:
return ImGuiKey_F10;
case GLFW_KEY_F11:
return ImGuiKey_F11;
case GLFW_KEY_F12:
return ImGuiKey_F12;
default:
return ImGuiKey_None;
static ImGuiKey SdlScancodeToImGuiKey(int scancode) {
switch (scancode) {
case SDL_SCANCODE_TAB: return ImGuiKey_Tab;
case SDL_SCANCODE_LEFT: return ImGuiKey_LeftArrow;
case SDL_SCANCODE_RIGHT: return ImGuiKey_RightArrow;
case SDL_SCANCODE_UP: return ImGuiKey_UpArrow;
case SDL_SCANCODE_DOWN: return ImGuiKey_DownArrow;
case SDL_SCANCODE_PAGEUP: return ImGuiKey_PageUp;
case SDL_SCANCODE_PAGEDOWN: return ImGuiKey_PageDown;
case SDL_SCANCODE_HOME: return ImGuiKey_Home;
case SDL_SCANCODE_END: return ImGuiKey_End;
case SDL_SCANCODE_INSERT: return ImGuiKey_Insert;
case SDL_SCANCODE_DELETE: return ImGuiKey_Delete;
case SDL_SCANCODE_BACKSPACE: return ImGuiKey_Backspace;
case SDL_SCANCODE_SPACE: return ImGuiKey_Space;
case SDL_SCANCODE_RETURN: return ImGuiKey_Enter;
case SDL_SCANCODE_ESCAPE: return ImGuiKey_Escape;
case SDL_SCANCODE_APOSTROPHE: return ImGuiKey_Apostrophe;
case SDL_SCANCODE_COMMA: return ImGuiKey_Comma;
case SDL_SCANCODE_MINUS: return ImGuiKey_Minus;
case SDL_SCANCODE_PERIOD: return ImGuiKey_Period;
case SDL_SCANCODE_SLASH: return ImGuiKey_Slash;
case SDL_SCANCODE_SEMICOLON: return ImGuiKey_Semicolon;
case SDL_SCANCODE_EQUALS: return ImGuiKey_Equal;
case SDL_SCANCODE_LEFTBRACKET: return ImGuiKey_LeftBracket;
case SDL_SCANCODE_BACKSLASH: return ImGuiKey_Backslash;
case SDL_SCANCODE_RIGHTBRACKET: return ImGuiKey_RightBracket;
case SDL_SCANCODE_GRAVE: return ImGuiKey_GraveAccent;
case SDL_SCANCODE_CAPSLOCK: return ImGuiKey_CapsLock;
case SDL_SCANCODE_SCROLLLOCK: return ImGuiKey_ScrollLock;
case SDL_SCANCODE_NUMLOCKCLEAR: return ImGuiKey_NumLock;
case SDL_SCANCODE_PRINTSCREEN: return ImGuiKey_PrintScreen;
case SDL_SCANCODE_PAUSE: return ImGuiKey_Pause;
case SDL_SCANCODE_KP_0: return ImGuiKey_Keypad0;
case SDL_SCANCODE_KP_1: return ImGuiKey_Keypad1;
case SDL_SCANCODE_KP_2: return ImGuiKey_Keypad2;
case SDL_SCANCODE_KP_3: return ImGuiKey_Keypad3;
case SDL_SCANCODE_KP_4: return ImGuiKey_Keypad4;
case SDL_SCANCODE_KP_5: return ImGuiKey_Keypad5;
case SDL_SCANCODE_KP_6: return ImGuiKey_Keypad6;
case SDL_SCANCODE_KP_7: return ImGuiKey_Keypad7;
case SDL_SCANCODE_KP_8: return ImGuiKey_Keypad8;
case SDL_SCANCODE_KP_9: return ImGuiKey_Keypad9;
case SDL_SCANCODE_KP_PERIOD: return ImGuiKey_KeypadDecimal;
case SDL_SCANCODE_KP_DIVIDE: return ImGuiKey_KeypadDivide;
case SDL_SCANCODE_KP_MULTIPLY: return ImGuiKey_KeypadMultiply;
case SDL_SCANCODE_KP_MINUS: return ImGuiKey_KeypadSubtract;
case SDL_SCANCODE_KP_PLUS: return ImGuiKey_KeypadAdd;
case SDL_SCANCODE_KP_ENTER: return ImGuiKey_KeypadEnter;
case SDL_SCANCODE_KP_EQUALS: return ImGuiKey_KeypadEqual;
case SDL_SCANCODE_LSHIFT: return ImGuiKey_LeftShift;
case SDL_SCANCODE_LCTRL: return ImGuiKey_LeftCtrl;
case SDL_SCANCODE_LALT: return ImGuiKey_LeftAlt;
case SDL_SCANCODE_LGUI: return ImGuiKey_LeftSuper;
case SDL_SCANCODE_RSHIFT: return ImGuiKey_RightShift;
case SDL_SCANCODE_RCTRL: return ImGuiKey_RightCtrl;
case SDL_SCANCODE_RALT: return ImGuiKey_RightAlt;
case SDL_SCANCODE_RGUI: return ImGuiKey_RightSuper;
case SDL_SCANCODE_MENU: return ImGuiKey_Menu;
case SDL_SCANCODE_0: return ImGuiKey_0;
case SDL_SCANCODE_1: return ImGuiKey_1;
case SDL_SCANCODE_2: return ImGuiKey_2;
case SDL_SCANCODE_3: return ImGuiKey_3;
case SDL_SCANCODE_4: return ImGuiKey_4;
case SDL_SCANCODE_5: return ImGuiKey_5;
case SDL_SCANCODE_6: return ImGuiKey_6;
case SDL_SCANCODE_7: return ImGuiKey_7;
case SDL_SCANCODE_8: return ImGuiKey_8;
case SDL_SCANCODE_9: return ImGuiKey_9;
case SDL_SCANCODE_A: return ImGuiKey_A;
case SDL_SCANCODE_B: return ImGuiKey_B;
case SDL_SCANCODE_C: return ImGuiKey_C;
case SDL_SCANCODE_D: return ImGuiKey_D;
case SDL_SCANCODE_E: return ImGuiKey_E;
case SDL_SCANCODE_F: return ImGuiKey_F;
case SDL_SCANCODE_G: return ImGuiKey_G;
case SDL_SCANCODE_H: return ImGuiKey_H;
case SDL_SCANCODE_I: return ImGuiKey_I;
case SDL_SCANCODE_J: return ImGuiKey_J;
case SDL_SCANCODE_K: return ImGuiKey_K;
case SDL_SCANCODE_L: return ImGuiKey_L;
case SDL_SCANCODE_M: return ImGuiKey_M;
case SDL_SCANCODE_N: return ImGuiKey_N;
case SDL_SCANCODE_O: return ImGuiKey_O;
case SDL_SCANCODE_P: return ImGuiKey_P;
case SDL_SCANCODE_Q: return ImGuiKey_Q;
case SDL_SCANCODE_R: return ImGuiKey_R;
case SDL_SCANCODE_S: return ImGuiKey_S;
case SDL_SCANCODE_T: return ImGuiKey_T;
case SDL_SCANCODE_U: return ImGuiKey_U;
case SDL_SCANCODE_V: return ImGuiKey_V;
case SDL_SCANCODE_W: return ImGuiKey_W;
case SDL_SCANCODE_X: return ImGuiKey_X;
case SDL_SCANCODE_Y: return ImGuiKey_Y;
case SDL_SCANCODE_Z: return ImGuiKey_Z;
case SDL_SCANCODE_F1: return ImGuiKey_F1;
case SDL_SCANCODE_F2: return ImGuiKey_F2;
case SDL_SCANCODE_F3: return ImGuiKey_F3;
case SDL_SCANCODE_F4: return ImGuiKey_F4;
case SDL_SCANCODE_F5: return ImGuiKey_F5;
case SDL_SCANCODE_F6: return ImGuiKey_F6;
case SDL_SCANCODE_F7: return ImGuiKey_F7;
case SDL_SCANCODE_F8: return ImGuiKey_F8;
case SDL_SCANCODE_F9: return ImGuiKey_F9;
case SDL_SCANCODE_F10: return ImGuiKey_F10;
case SDL_SCANCODE_F11: return ImGuiKey_F11;
case SDL_SCANCODE_F12: return ImGuiKey_F12;
default: return ImGuiKey_None;
}
}
void PadsImpl::init() {
s_pads = this;
scanGamepads();
glfwSetJoystickCallback([](int jid, int event) {
s_pads->scanGamepads();
s_pads->map();
});
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
PCSX::g_system->log(PCSX::LogClass::UI, "SDL_InitSubSystem(SDL_INIT_GAMEPAD) failed: %s\n", SDL_GetError());
}
PCSX::g_system->findResource(
[](const std::filesystem::path& filename) -> bool {
PCSX::IO<PCSX::File> database(new PCSX::PosixFile(filename));
@@ -441,17 +345,27 @@ void PadsImpl::init() {
size_t dbsize = database->size();
auto dbStr = database->readString(dbsize);
int ret = glfwUpdateGamepadMappings(dbStr.c_str());
return ret;
// Wrap the in-memory buffer as an SDL_IOStream so SDL parses the
// mapping DB using the same routine it uses for files.
SDL_IOStream* io = SDL_IOFromConstMem(dbStr.data(), dbStr.size());
if (!io) return false;
int ret = SDL_AddGamepadMappingsFromIO(io, true /* closeio */);
return ret > 0;
},
"gamecontrollerdb.txt", "resources", std::filesystem::path("third_party") / "SDL_GameControllerDB");
scanGamepads();
reset();
map();
}
void PadsImpl::shutdown() {
glfwSetJoystickCallback(nullptr);
for (auto& g : m_gamepads) {
if (g) {
SDL_CloseGamepad(g);
g = nullptr;
}
}
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
s_pads = nullptr;
}
@@ -464,16 +378,23 @@ PadsImpl::PadsImpl() : m_listener(PCSX::g_system->m_eventBus) {
}
void PadsImpl::scanGamepads() {
static_assert((1 + GLFW_JOYSTICK_LAST - GLFW_JOYSTICK_1) <= sizeof(m_gamepadsMap) / sizeof(m_gamepadsMap[0]));
for (auto& m : m_gamepadsMap) {
m = -1;
}
unsigned index = 0;
for (int i = GLFW_JOYSTICK_1; i < GLFW_JOYSTICK_LAST; i++) {
if (glfwJoystickPresent(i) && glfwJoystickIsGamepad(i)) {
m_gamepadsMap[index++] = i;
// Close any currently-open handles so re-scans (e.g. after hotplug) don't
// leak. m_gamepad pointers in each Pad become stale here; the caller is
// expected to follow up with map() to re-resolve them.
for (auto& g : m_gamepads) {
if (g) {
SDL_CloseGamepad(g);
g = nullptr;
}
}
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (!ids) return;
const unsigned slots = sizeof(m_gamepads) / sizeof(m_gamepads[0]);
for (int i = 0; i < count && static_cast<unsigned>(i) < slots; i++) {
m_gamepads[i] = SDL_OpenGamepad(ids[i]);
}
SDL_free(ids);
}
void PadsImpl::reset() {
@@ -497,7 +418,9 @@ void PadsImpl::map() {
}
void PadsImpl::Pad::map() {
m_padID = s_pads->m_gamepadsMap[m_settings.get<SettingControllerID>()];
int id = m_settings.get<SettingControllerID>();
const unsigned slots = sizeof(s_pads->m_gamepads) / sizeof(s_pads->m_gamepads[0]);
m_gamepad = (id >= 0 && static_cast<unsigned>(id) < slots) ? s_pads->m_gamepads[id] : nullptr;
m_type = m_settings.get<SettingDeviceType>();
// L3/R3 are only avalable on analog controllers
@@ -509,8 +432,8 @@ void PadsImpl::Pad::map() {
} else {
m_scancodes[1] = 255;
m_scancodes[2] = 255;
m_padMapping[1] = GLFW_GAMEPAD_BUTTON_INVALID;
m_padMapping[2] = GLFW_GAMEPAD_BUTTON_INVALID;
m_padMapping[1] = PCSX_GAMEPAD_BUTTON_INVALID;
m_padMapping[2] = PCSX_GAMEPAD_BUTTON_INVALID;
}
// keyboard mappings
@@ -549,18 +472,18 @@ void PadsImpl::Pad::map() {
static constexpr float THRESHOLD = 0.85f;
// Certain buttons on controllers are actually axis that can be pressed, half-pressed, etc.
bool PadsImpl::Pad::isControllerButtonPressed(int button, GLFWgamepadstate* state) {
bool PadsImpl::Pad::isControllerButtonPressed(int button, const GamepadState& state) {
int mapped = m_padMapping[button];
switch (mapped) {
case GLFW_GAMEPAD_BUTTON_LEFT_TRIGGER:
return state->axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER] >= THRESHOLD;
case GLFW_GAMEPAD_BUTTON_RIGHT_TRIGGER:
return state->axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER] >= THRESHOLD;
case GLFW_GAMEPAD_BUTTON_INVALID:
case PCSX_GAMEPAD_BUTTON_LEFT_TRIGGER:
return state.axes[SDL_GAMEPAD_AXIS_LEFT_TRIGGER] >= THRESHOLD;
case PCSX_GAMEPAD_BUTTON_RIGHT_TRIGGER:
return state.axes[SDL_GAMEPAD_AXIS_RIGHT_TRIGGER] >= THRESHOLD;
case PCSX_GAMEPAD_BUTTON_INVALID:
return false;
default:
return state->buttons[mapped];
break;
if (mapped < 0 || mapped >= SDL_GAMEPAD_BUTTON_COUNT) return false;
return state.buttons[mapped];
}
}
@@ -574,15 +497,15 @@ void PadsImpl::Pad::getButtons() {
return;
}
GLFWgamepadstate state;
int hasPad = GLFW_FALSE;
GamepadState state{};
bool hasPad = false;
const auto& inputType = m_settings.get<SettingInputType>();
auto getKeyboardButtons = [this]() -> uint16_t {
if (!ImGui::GetCurrentContext()) return 0xffff;
uint16_t result = 0;
for (unsigned i = 0; i < 16; i++) {
auto key = GlfwKeyToImGuiKey(m_scancodes[i]);
auto key = SdlScancodeToImGuiKey(m_scancodes[i]);
if (key == ImGuiKey_None) continue;
result |= (ImGui::IsKeyDown(key)) << i;
}
@@ -595,12 +518,37 @@ void PadsImpl::Pad::getButtons() {
return;
}
if ((m_padID >= GLFW_JOYSTICK_1) && (m_padID <= GLFW_JOYSTICK_LAST)) {
hasPad = glfwGetGamepadState(m_padID, &state);
if (!hasPad) {
const char* guid = glfwGetJoystickGUID(m_padID);
PCSX::g_system->printf("Gamepad error: GUID %s likely has no database mapping, disabling pad\n", guid);
m_padID = -1;
// Drive SDL's internal gamepad state and pluck out hotplug events. Other
// SDL events stay in the queue (gui.cc still owns the rest of the input
// surface today). Move this to the central event pump when Phase 3 lands.
SDL_PumpEvents();
{
SDL_Event scratch[16];
int drained = SDL_PeepEvents(scratch, 16, SDL_GETEVENT, SDL_EVENT_GAMEPAD_ADDED, SDL_EVENT_GAMEPAD_REMOVED);
if (drained > 0) {
s_pads->scanGamepads();
s_pads->map();
}
}
if (m_gamepad) {
if (!SDL_GamepadConnected(m_gamepad)) {
const SDL_JoystickID jid = SDL_GetGamepadID(m_gamepad);
PCSX::g_system->printf("Gamepad error: joystick id %u disconnected, disabling pad\n",
static_cast<unsigned>(jid));
m_gamepad = nullptr;
} else {
for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; i++) {
state.buttons[i] = SDL_GetGamepadButton(m_gamepad, static_cast<SDL_GamepadButton>(i));
}
// SDL reports axes as int16_t [-32768, 32767]. Triggers are signed but
// only ever positive in practice. Normalize to [-1, 1] to match the
// shape the rest of this code was built around.
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; i++) {
int16_t raw = SDL_GetGamepadAxis(m_gamepad, static_cast<SDL_GamepadAxis>(i));
state.axes[i] = raw < 0 ? raw / 32768.0f : raw / 32767.0f;
}
hasPad = true;
}
}
@@ -617,13 +565,13 @@ void PadsImpl::Pad::getButtons() {
bool buttons[16];
for (unsigned i = 0; i < 16; i++) {
buttons[i] = isControllerButtonPressed(i, &state);
buttons[i] = isControllerButtonPressed(i, state);
}
// For digital gamepads, make the PS1 dpad controllable with our gamepad's left analog stick
if (m_type == PadType::Digital) {
float x = state.axes[GLFW_GAMEPAD_AXIS_LEFT_X];
float y = -state.axes[GLFW_GAMEPAD_AXIS_LEFT_Y];
float x = state.axes[SDL_GAMEPAD_AXIS_LEFTX];
float y = -state.axes[SDL_GAMEPAD_AXIS_LEFTY];
float ds = x * x + y * y;
if (ds >= THRESHOLD * THRESHOLD) {
float d = std::sqrt(ds);
@@ -667,10 +615,10 @@ void PadsImpl::Pad::getButtons() {
const float scaledValue = std::clamp<float>(axis * scale, -1.0f, 1.0f);
return (uint8_t)(std::clamp<float>(std::round(((scaledValue + 1.0f) / 2.0f) * 255.0f), 0.0f, 255.0f));
};
pad.leftJoyX = axisToUint8(state.axes[GLFW_GAMEPAD_AXIS_LEFT_X]);
pad.leftJoyY = axisToUint8(state.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]);
pad.rightJoyX = axisToUint8(state.axes[GLFW_GAMEPAD_AXIS_RIGHT_X]);
pad.rightJoyY = axisToUint8(state.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]);
pad.leftJoyX = axisToUint8(state.axes[SDL_GAMEPAD_AXIS_LEFTX]);
pad.leftJoyY = axisToUint8(state.axes[SDL_GAMEPAD_AXIS_LEFTY]);
pad.rightJoyX = axisToUint8(state.axes[SDL_GAMEPAD_AXIS_RIGHTX]);
pad.rightJoyY = axisToUint8(state.axes[SDL_GAMEPAD_AXIS_RIGHTY]);
}
uint16_t result = 0;
@@ -880,10 +828,10 @@ uint8_t PadsImpl::Pad::read() {
bool PadsImpl::configure(PCSX::GUI* gui) {
// Check for analog mode toggle key
for (auto& pad : m_pads) {
if (pad.m_type == PadType::Analog && pad.m_settings.get<Keyboard_AnalogMode>() != GLFW_KEY_UNKNOWN) {
if (pad.m_type == PadType::Analog && pad.m_settings.get<Keyboard_AnalogMode>() != SDL_SCANCODE_UNKNOWN) {
const int key = pad.m_settings.get<Keyboard_AnalogMode>();
if ((key != ImGuiKey_None) && ImGui::IsKeyReleased(GlfwKeyToImGuiKey(key))) {
if ((key != ImGuiKey_None) && ImGui::IsKeyReleased(SdlScancodeToImGuiKey(key))) {
pad.m_analogMode = !pad.m_analogMode;
}
}
@@ -938,33 +886,34 @@ bool PadsImpl::configure(PCSX::GUI* gui) {
return changed;
}
// GLFW doesn't support converting some of the most common keys to strings
static std::string glfwKeyToString(int key) {
// define strings for some common keys that are not supported by glfwGetKeyName
switch (key) {
case GLFW_KEY_UP:
// SDL_GetScancodeName covers ASCII letters/digits but not the common navigation
// keys; we provide friendlier localized labels for those, and fall back to
// SDL_GetScancodeName for the rest.
static std::string sdlScancodeToString(int scancode) {
switch (scancode) {
case SDL_SCANCODE_UP:
return _("Keyboard Up");
case GLFW_KEY_RIGHT:
case SDL_SCANCODE_RIGHT:
return _("Keyboard Right");
case GLFW_KEY_DOWN:
case SDL_SCANCODE_DOWN:
return _("Keyboard Down");
case GLFW_KEY_LEFT:
case SDL_SCANCODE_LEFT:
return _("Keyboard Left");
case GLFW_KEY_BACKSPACE:
case SDL_SCANCODE_BACKSPACE:
return _("Keyboard Backspace");
case GLFW_KEY_ENTER:
case SDL_SCANCODE_RETURN:
return _("Keyboard Enter");
case GLFW_KEY_SPACE:
case SDL_SCANCODE_SPACE:
return _("Keyboard Space");
case GLFW_KEY_ESCAPE:
case SDL_SCANCODE_ESCAPE:
return _("Keyboard Escape");
case GLFW_KEY_UNKNOWN:
case SDL_SCANCODE_UNKNOWN:
return _("Unbound");
};
auto keyName = glfwGetKeyName(key, 0);
if (keyName == nullptr) {
return fmt::format(f_("Unknown keyboard key {}"), key);
const char* keyName = SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode));
if (!keyName || keyName[0] == '\0') {
return fmt::format(f_("Unknown keyboard key {}"), scancode);
}
auto str = std::string(keyName);
@@ -976,7 +925,11 @@ void PadsImpl::Pad::keyboardEvent(const PCSX::Events::Keyboard& event) {
if (m_buttonToWait == -1) {
return;
}
getButtonFromGUIIndex(m_buttonToWait) = event.key;
// Bindings are stored as SDL_Scancode values to match the runtime lookup
// path (SdlScancodeToImGuiKey) and the SDL_SCANCODE_* setting defaults.
// event.key is an SDL_Keycode (layout-aware codepoint); event.scancode is
// the layout-independent physical-key id we actually want.
getButtonFromGUIIndex(m_buttonToWait) = event.scancode;
m_buttonToWait = -1;
m_changed = true;
map();
@@ -1075,7 +1028,7 @@ bool PadsImpl::Pad::configure() {
}
// The name of the mapped key
const auto keyName = fmt::format("{}##{}", glfwKeyToString(getButtonFromGUIIndex(i)), i);
const auto keyName = fmt::format("{}##{}", sdlScancodeToString(getButtonFromGUIIndex(i)), i);
if (ImGui::Button(keyName.c_str(), ImVec2{-1, 0})) {
m_buttonToWait = i;
}
@@ -1098,7 +1051,7 @@ bool PadsImpl::Pad::configure() {
}
// The name of the mapped key
const auto keyName = fmt::format("{}##{}", glfwKeyToString(getButtonFromGUIIndex(absI)), absI);
const auto keyName = fmt::format("{}##{}", sdlScancodeToString(getButtonFromGUIIndex(absI)), absI);
if (ImGui::Button(keyName.c_str(), ImVec2{-1, 0})) {
m_buttonToWait = absI;
}
@@ -1111,27 +1064,34 @@ bool PadsImpl::Pad::configure() {
const char* preview = _("No gamepad selected or connected");
auto& id = m_settings.get<SettingControllerID>().value;
int glfwjid = id >= 0 ? s_pads->m_gamepadsMap[id] : -1;
SDL_Gamepad* selected = (id >= 0 && static_cast<unsigned>(id) <
sizeof(s_pads->m_gamepads) / sizeof(s_pads->m_gamepads[0]))
? s_pads->m_gamepads[id]
: nullptr;
std::vector<const char*> gamepadsNames;
for (auto& m : s_pads->m_gamepadsMap) {
if (m == -1) {
continue;
}
const char* name = glfwGetGamepadName(m);
gamepadsNames.push_back(name);
if (m == glfwjid) {
preview = name;
}
// Slot index -> displayable name. Empty slots are skipped during render but
// we keep the slot index so the user's saved SettingControllerID continues
// to refer to the same physical position.
struct Entry {
int slot;
const char* name;
};
std::vector<Entry> gamepads;
for (unsigned i = 0; i < sizeof(s_pads->m_gamepads) / sizeof(s_pads->m_gamepads[0]); i++) {
SDL_Gamepad* g = s_pads->m_gamepads[i];
if (!g) continue;
const char* name = SDL_GetGamepadName(g);
if (!name) name = "<unnamed gamepad>";
gamepads.push_back({static_cast<int>(i), name});
if (g == selected) preview = name;
}
if (ImGui::BeginCombo(_("Gamepad"), preview)) {
for (int i = 0; i < gamepadsNames.size(); i++) {
const auto gamepadName = fmt::format("{}##{}", gamepadsNames[i], i);
for (const auto& entry : gamepads) {
const auto gamepadName = fmt::format("{}##{}", entry.name, entry.slot);
if (ImGui::Selectable(gamepadName.c_str())) {
changed = true;
id = i;
id = entry.slot;
map();
}
}
+2
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/sio1-server.h"
#include "core/psxemulator.h"
+2
View File
@@ -19,6 +19,8 @@
#pragma once
#include <uv.h>
#include <queue>
#include <string>
+10
View File
@@ -20,6 +20,7 @@
#include "core/system.h"
#include <stddef.h>
#include <uv.h>
#include <iomanip>
#include <sstream>
@@ -38,6 +39,15 @@ static const ImWchar c_polishRanges[] = {0x0020, 0x00ff, 0x0104, 0x0119, 0x0141,
// locale names have to be written in basic latin or extended latin, in order
// to be properly displayed in the UI with the default range
PCSX::System::System() : m_loop(new uv_loop_t) { uv_loop_init(m_loop); }
PCSX::System::~System() {
if (!m_emergencyExit) uv_loop_close(m_loop);
delete m_loop;
}
uv_loop_t *PCSX::System::getLoop() { return m_loop; }
const std::map<std::string, PCSX::System::LocaleInfo> PCSX::System::LOCALES = {
{
"Deutsch",
+10 -8
View File
@@ -20,7 +20,11 @@
#pragma once
#include <stdarg.h>
#include <uv.h>
// libuv only appears here as an opaque handle. Including <uv.h> from this
// header dragged it into 110 of 251 translation units, of which only eight
// actually use it; the ones that do include it themselves.
struct uv_loop_s;
typedef struct uv_loop_s uv_loop_t;
#include <chrono>
#include <filesystem>
@@ -35,7 +39,7 @@
#include "imgui.h"
#include "support/djbhash.h"
#include "support/eventbus.h"
#include "support/version.h"
#include "support/version-info.h"
namespace PCSX {
@@ -127,10 +131,8 @@ struct SetLuts {};
class System {
public:
System() { uv_loop_init(&m_loop); }
virtual ~System() {
if (!m_emergencyExit) uv_loop_close(&m_loop);
}
System();
virtual ~System();
// Requests a system reset
virtual void softReset() = 0;
virtual void hardReset() = 0;
@@ -255,10 +257,10 @@ class System {
VIETNAMESE = 13,
};
uv_loop_t *getLoop() { return &m_loop; }
uv_loop_t *getLoop();
private:
uv_loop_t m_loop;
uv_loop_t *m_loop;
std::map<uint64_t, std::string> m_i18n;
std::map<std::string, decltype(m_i18n)> m_locales;
std::string m_currentLocale;
+2
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/ui.h"
#include <fstream>
+2
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/web-server.h"
#include <llhttp.h>
+26 -2
View File
@@ -30,7 +30,31 @@ void pcsxStaticImguiAssert(int exp, const char* expression);
}
#endif
#define IM_ASSERT_USER_ERROR(EXP, MSG) \
if (!(EXP)) pcsxStaticImguiUserError((MSG))
// imgui_internal.h gates IM_ASSERT_USER_ERROR_RET and IM_ASSERT_USER_ERROR_RETV
// behind the same #ifndef IM_ASSERT_USER_ERROR guard, so once we redirect the
// base macro we have to provide the recoverable-error variants as well.
// pcsxStaticImguiUserError throws by default, so the return statements after
// it are unreachable in normal operation but required for syntactic
// correctness when the user handler is replaced with a non-throwing one.
#define IM_ASSERT_USER_ERROR(EXP, MSG) \
do { \
if (!(EXP)) pcsxStaticImguiUserError((MSG)); \
} while (0)
#define IM_ASSERT_USER_ERROR_RET(EXP, MSG) \
do { \
if (!(EXP)) { \
pcsxStaticImguiUserError((MSG)); \
return; \
} \
} while (0)
#define IM_ASSERT_USER_ERROR_RETV(EXP, RETV, MSG) \
do { \
if (!(EXP)) { \
pcsxStaticImguiUserError((MSG)); \
return (RETV); \
} \
} while (0)
#define IM_ASSERT(EXP) pcsxStaticImguiAssert(!!(EXP), "ImGui assert: " #EXP " is false")
+194 -102
View File
@@ -24,11 +24,10 @@
#endif
// And only then we can load the rest
#define GLFW_INCLUDE_NONE
#define IMGUI_DEFINE_MATH_OPERATORS
#define NANOVG_GLES3_IMPLEMENTATION
#include <GL/gl3w.h>
#include <GLFW/glfw3.h>
#include <SDL3/SDL.h>
#include <assert.h>
extern "C" {
#include <libavcodec/avcodec.h>
@@ -72,8 +71,8 @@ extern "C" {
#include "gui/resources.h"
#include "gui/shaders/crt-lottes.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imgui_impl_sdl3.h"
#include "imgui_internal.h"
#include "imgui_stdlib.h"
#include "implot/implot.h"
@@ -203,10 +202,9 @@ extern "C" void pcsxStaticImguiAssert(int exp, const char* msg) {
if (!exp) thrower(msg);
}
static GLFWwindow* getGLFWwindowFromImGuiViewport(ImGuiViewport* viewport) {
// absolutely horrendous hack, but the only way we have to grab the
// GLFWwindow pointer from an ImGuiViewport without changing the backend...
return *reinterpret_cast<GLFWwindow**>(viewport->PlatformUserData);
static SDL_Window* getSDLWindowFromImGuiViewport(ImGuiViewport* viewport) {
// imgui_impl_sdl3 stores the SDL_Window* directly in PlatformHandle.
return static_cast<SDL_Window*>(viewport->PlatformHandle);
}
PCSX::GUI* PCSX::g_gui = nullptr;
@@ -214,31 +212,24 @@ PCSX::GUI* PCSX::g_gui = nullptr;
void PCSX::GUI::setFullscreen(bool fullscreen) {
m_fullscreen = fullscreen;
if (fullscreen) {
glfwGetWindowPos(m_window, &m_glfwPosX, &m_glfwPosY);
glfwGetWindowSize(m_window, &m_glfwSizeX, &m_glfwSizeY);
const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
glfwSetWindowMonitor(m_window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, GLFW_DONT_CARE);
// Snapshot windowed pos/size before the transition so we can restore them.
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
// No fullscreen mode set -> SDL gives us borderless desktop fullscreen,
// which is the shape glfwSetWindowMonitor(..., GLFW_DONT_CARE) produced.
SDL_SetWindowFullscreenMode(m_window, nullptr);
SDL_SetWindowFullscreen(m_window, true);
} else {
glfwSetWindowMonitor(m_window, nullptr, m_glfwPosX, m_glfwPosY, m_glfwSizeX, m_glfwSizeY, GLFW_DONT_CARE);
SDL_SetWindowFullscreen(m_window, false);
SDL_SetWindowPosition(m_window, m_windowPosX, m_windowPosY);
SDL_SetWindowSize(m_window, m_windowSizeX, m_windowSizeY);
}
}
void PCSX::GUI::setRawMouseMotion() {
if (isRawMouseMotionEnabled()) {
if (glfwRawMouseMotionSupported()) {
glfwSetInputMode(m_window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwSetInputMode(m_window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
}
} else {
glfwSetInputMode(m_window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
}
static PCSX::GUI* s_this = nullptr;
static void drop_callback(GLFWwindow* window, int count, const char** paths) {
if (count != 1) return;
s_this->magicOpen(paths[0]);
// SDL3 relative-mouse-mode covers cursor-hide + raw motion in one call,
// collapsing GLFW's two-step (CURSOR_DISABLED + RAW_MOUSE_MOTION) flow.
SDL_SetWindowRelativeMouseMode(m_window, isRawMouseMotionEnabled());
}
void LoadImguiBindings(lua_State* lState);
@@ -543,66 +534,91 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
}
});
glfwSetErrorCallback([](int error, const char* description) {
g_system->log(LogClass::UI, "Glfw Error %d: %s\n", error, description);
});
if (!glfwInit()) {
throw std::runtime_error("Failed to initialize GLFW");
if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
throw std::runtime_error(std::string("Failed to initialize SDL: ") + SDL_GetError());
}
m_listener.listen<Events::Quitting>([this](const auto& event) { saveCfg(); });
m_listener.listen<Events::ExecutionFlow::Pause>([this](const auto& event) {
glfwSwapInterval(m_idleSwapInterval);
glfwSetInputMode(m_window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
SDL_GL_SetSwapInterval(m_idleSwapInterval);
SDL_SetWindowRelativeMouseMode(m_window, false);
});
m_listener.listen<Events::ExecutionFlow::Run>([this](const auto& event) {
m_enableSplashScreen = false;
glfwSwapInterval(0);
SDL_GL_SetSwapInterval(0);
setRawMouseMotion();
});
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
m_hasCoreProfile = true;
m_window = glfwCreateWindow(1280, 800, "PCSX-Redux", nullptr, nullptr);
// SDL_WINDOW_HIGH_PIXEL_DENSITY makes window-coords vs pixel-coords meaningful
// on macOS Retina + Windows per-monitor DPI; required for the changeScale path.
const SDL_WindowFlags windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
m_window = SDL_CreateWindow("PCSX-Redux", 1280, 800, windowFlags);
if (m_window) {
m_glContext = SDL_GL_CreateContext(m_window);
}
if (!m_window) {
// SDL splits window and GL context creation, so the 3.0 fallback has to
// cover both: a 3.2-core context can fail to materialize even after the
// window itself succeeded. On platforms where the pixel format binds at
// window creation (Win32 WGL is the strict case) a clean retry needs a
// fresh window too, so we destroy and recreate both.
if (!m_window || !m_glContext) {
g_system->log(LogClass::UI,
"GLFW failed to create window with OpenGL core profile 3.2, retrying with any 3.0 profile\n");
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_ANY_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
"SDL failed to create OpenGL 3.2 core context, retrying with any 3.0 profile\n");
if (m_glContext) {
SDL_GL_DestroyContext(m_glContext);
m_glContext = nullptr;
}
if (m_window) {
SDL_DestroyWindow(m_window);
m_window = nullptr;
}
SDL_GL_ResetAttributes();
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
// No profile mask -> any profile.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
m_hasCoreProfile = false;
m_window = glfwCreateWindow(1280, 800, "PCSX-Redux", nullptr, nullptr);
m_window = SDL_CreateWindow("PCSX-Redux", 1280, 800, windowFlags);
if (m_window) {
m_glContext = SDL_GL_CreateContext(m_window);
}
}
if (!m_window) {
throw std::runtime_error("Unable to create main window. Check OpenGL drivers.");
throw std::runtime_error(std::string("Unable to create main window: ") + SDL_GetError());
}
glfwMakeContextCurrent(m_window);
glfwSwapInterval(0);
if (!m_glContext) {
throw std::runtime_error(std::string("Unable to create GL context: ") + SDL_GetError());
}
SDL_GL_MakeCurrent(m_window, m_glContext);
SDL_GL_SetSwapInterval(0);
s_this = this;
glfwSetDropCallback(m_window, drop_callback);
glfwSetWindowSizeCallback(m_window, [](GLFWwindow*, int, int) { s_this->m_setupScreenSize = true; });
// Drop and resize events flow through SDL_PollEvent in startFrame() now.
// ImGui_ImplSDL3_ProcessEvent gets first crack at every event for
// mouse/keyboard/text dispatch.
Resources::loadIcon([this](const uint8_t* data, uint32_t size) {
clip::image img;
if (!img.import_from_png(data, size)) return;
int x = img.spec().width;
int y = img.spec().height;
GLFWimage image;
image.width = x;
image.height = y;
image.pixels = reinterpret_cast<unsigned char*>(img.data());
glfwSetWindowIcon(m_window, 1, &image);
// SDL_CreateSurfaceFrom needs a non-const pixel pointer; clip::image::data()
// is non-const, so the cast below is benign.
SDL_Surface* iconSurface = SDL_CreateSurfaceFrom(x, y, SDL_PIXELFORMAT_RGBA32, img.data(), x * 4);
if (iconSurface) {
SDL_SetWindowIcon(m_window, iconSurface);
SDL_DestroySurface(iconSurface);
}
});
result = gl3wInit();
@@ -660,12 +676,12 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
}
if ((settings.get<WindowPosX>().value > 0) && (settings.get<WindowPosY>().value > 0)) {
glfwSetWindowPos(m_window, settings.get<WindowPosX>(), settings.get<WindowPosY>());
SDL_SetWindowPosition(m_window, settings.get<WindowPosX>(), settings.get<WindowPosY>());
}
if (settings.get<WindowMaximized>().value) {
glfwMaximizeWindow(m_window);
SDL_MaximizeWindow(m_window);
} else {
glfwSetWindowSize(m_window, windowSizeX, windowSizeY);
SDL_SetWindowSize(m_window, windowSizeX, windowSizeY);
}
} else {
saveCfg();
@@ -690,7 +706,7 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
const auto currentTheme = emuSettings.get<Emulator::SettingGUITheme>().value; // On boot: reload GUI theme
applyTheme(currentTheme);
}
if (!g_system->running()) glfwSwapInterval(m_idleSwapInterval);
if (!g_system->running()) SDL_GL_SetSwapInterval(m_idleSwapInterval);
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
@@ -701,7 +717,7 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleViewports;
// io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleFonts;
ImGui_ImplGlfw_InitForOpenGL(m_window, true);
ImGui_ImplSDL3_InitForOpenGL(m_window, m_glContext);
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
io.SetClipboardTextFn = [](void*, const char* text) -> void { clip::set_text(text); };
io.GetClipboardTextFn = [](void*) -> const char* {
@@ -712,8 +728,9 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
m_createWindowOldCallback = platform_io.Platform_CreateWindow;
platform_io.Platform_CreateWindow = [](ImGuiViewport* viewport) {
if (g_gui->m_createWindowOldCallback) g_gui->m_createWindowOldCallback(viewport);
auto window = getGLFWwindowFromImGuiViewport(viewport);
glfwSetKeyCallback(window, glfwKeyCallbackTrampoline);
// imgui_impl_sdl3 dispatches keyboard/mouse via ImGui_ImplSDL3_ProcessEvent
// applied to every polled event in startFrame(), so we don't need a
// per-window key callback the way the GLFW backend did.
auto id = viewport->ID;
g_gui->m_nvgSubContextes[id] = nvgCreateSubContextGL(g_gui->m_nvgContext);
};
@@ -733,7 +750,6 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
}
if (g_gui->m_destroyWindowOldCallback) g_gui->m_destroyWindowOldCallback(viewport);
};
glfwSetKeyCallback(m_window, glfwKeyCallbackTrampoline);
// Some bad GPU drivers (*cough* Intel) don't like mixed shaders versions,
// and will silently fail to execute them.
// This is just a bad heuristic to try and keep it the same version.
@@ -869,16 +885,29 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
void PCSX::GUI::close() {
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(m_window);
glfwTerminate();
// Tear down all GL-backed resources (NanoVG sub/main contexts) BEFORE
// dropping the GL context they live in. The previous (GLFW-era) ordering
// freed NanoVG after glfwDestroyWindow, which technically leaked GPU
// resources because their owning context was already gone; SDL's explicit
// context handle makes the correct ordering easy to enforce.
for (auto& subContext : m_nvgSubContextes) {
nvgDeleteSubContextGL(subContext.second);
}
m_nvgSubContextes.clear();
nvgDeleteGLES3(m_nvgContext);
m_nvgContext = nullptr;
if (m_glContext) {
SDL_GL_DestroyContext(m_glContext);
m_glContext = nullptr;
}
if (m_window) {
SDL_DestroyWindow(m_window);
m_window = nullptr;
}
SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
}
void PCSX::GUI::saveCfg() {
@@ -893,16 +922,17 @@ void PCSX::GUI::saveCfg() {
std::ofstream cfg(cfgTmpPath);
json j;
if (m_fullscreen || glfwGetWindowAttrib(m_window, GLFW_ICONIFIED) > 0) {
m_glfwPosX = settings.get<WindowPosX>();
m_glfwPosY = settings.get<WindowPosY>();
m_glfwSizeX = settings.get<WindowSizeX>();
m_glfwSizeY = settings.get<WindowSizeY>();
m_glfwMaximized = settings.get<WindowMaximized>();
const SDL_WindowFlags flags = SDL_GetWindowFlags(m_window);
if (m_fullscreen || (flags & SDL_WINDOW_MINIMIZED)) {
m_windowPosX = settings.get<WindowPosX>();
m_windowPosY = settings.get<WindowPosY>();
m_windowSizeX = settings.get<WindowSizeX>();
m_windowSizeY = settings.get<WindowSizeY>();
m_windowMaximized = settings.get<WindowMaximized>();
} else {
glfwGetWindowPos(m_window, &m_glfwPosX, &m_glfwPosY);
glfwGetWindowSize(m_window, &m_glfwSizeX, &m_glfwSizeY);
m_glfwMaximized = glfwGetWindowAttrib(m_window, GLFW_MAXIMIZED) != 0;
SDL_GetWindowPosition(m_window, &m_windowPosX, &m_windowPosY);
SDL_GetWindowSize(m_window, &m_windowSizeX, &m_windowSizeY);
m_windowMaximized = (flags & SDL_WINDOW_MAXIMIZED) != 0;
}
j["imgui"] = ImGui::SaveIniSettingsToMemory(nullptr);
@@ -956,22 +986,55 @@ void PCSX::GUI::resetSettings() {
g_system->quit(0x12eb007);
}
void PCSX::GUI::glfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
g_system->m_eventBus->signal(Events::Keyboard{key, scancode, action, mods});
}
void PCSX::GUI::startFrame() {
ZoneScoped;
tick();
if (glfwWindowShouldClose(m_window)) g_system->quit();
glfwPollEvents();
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL3_ProcessEvent(&event);
switch (event.type) {
case SDL_EVENT_QUIT:
g_system->quit();
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (event.window.windowID == SDL_GetWindowID(m_window)) g_system->quit();
break;
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (event.window.windowID == SDL_GetWindowID(m_window)) m_setupScreenSize = true;
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
// Force a font/scale rebuild when the window crosses to a display
// with different DPI. This is the case GLFW didn't fire and is the
// recurring high-DPI bug we're fixing in this phase.
if (event.window.windowID == SDL_GetWindowID(m_window)) {
changeScale(SDL_GetWindowDisplayScale(m_window));
m_setupScreenSize = true;
}
break;
case SDL_EVENT_DROP_FILE:
// SDL hands us a UTF-8 path that's owned by SDL and freed when
// the event is consumed; magicOpen copies what it needs.
if (event.drop.data) magicOpen(event.drop.data);
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
const int action = event.type == SDL_EVENT_KEY_DOWN ? 1 : 0;
g_system->m_eventBus->signal(Events::Keyboard{
static_cast<int>(event.key.key), static_cast<int>(event.key.scancode), action,
static_cast<int>(event.key.mod)});
break;
}
default:
break;
}
}
if (m_setupScreenSize) {
const float renderRatio = settings.get<WidescreenRatio>() ? 9.0f / 16.0f : 3.0f / 4.0f;
int w, h;
glfwGetFramebufferSize(m_window, &w, &h);
SDL_GetWindowSizeInPixels(m_window, &w, &h);
// Make width/height be 1 at minimum
w = std::max<int>(w, 1);
h = std::max<int>(h, 1);
@@ -1013,7 +1076,13 @@ void PCSX::GUI::startFrame() {
auto scales = m_allScales;
if (scales.empty()) scales.emplace(1.0f);
ImGui_ImplOpenGL3_DestroyFontsTexture();
// ImGui v1.92 retired ImGui_ImplOpenGL3_{Destroy,Create}FontsTexture and
// ImFontAtlas::Build() in favour of the dynamic-texture protocol; the
// backend pulls fresh glyphs from the atlas as it grows. We still
// rebuild the per-scale font map below so each pinned scale has a
// matching ImFont with the right LegacySize ready for PushFont; actual
// per-size glyph data is allocated lazily inside ImFontBaked when those
// fonts are first rendered.
m_mainFonts.clear();
m_monoFonts.clear();
@@ -1031,13 +1100,17 @@ void PCSX::GUI::startFrame() {
m_monoFonts[scale] = loadFont(MAKEU8("NotoMono-Regular.ttf"), settings.get<MonoFontSize>().value * scale,
io, nullptr, false, false);
}
io.Fonts->Build();
io.FontDefault = m_mainFonts.begin()->second;
ImGui_ImplOpenGL3_CreateFontsTexture();
// Pick the font matching the current DPI scale as the default; in
// v1.92 ImGui draws frames at Style.FontSizeBase unless overridden by
// PushFont, so we have to push that size up to match or the default
// frame text would render at the smallest pinned scale.
ImFont* mainFont = getMainFont();
io.FontDefault = mainFont;
if (mainFont) ImGui::GetStyle().FontSizeBase = mainFont->LegacySize;
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
MarkDown::newFrame();
if (io.WantSaveIniSettings) {
@@ -2093,8 +2166,8 @@ the update and manually apply it.)")));
float pxRatio;
auto vg = m_nvgContext;
if (vg) {
glfwGetWindowSize(m_window, &winWidth, &winHeight);
glfwGetFramebufferSize(m_window, &fbWidth, &fbHeight);
SDL_GetWindowSize(m_window, &winWidth, &winHeight);
SDL_GetWindowSizeInPixels(m_window, &fbWidth, &fbHeight);
pxRatio = (float)fbWidth / (float)winWidth;
nvgSwitchMainContextGL(vg);
nvgBeginFrame(vg, winWidth, winHeight, pxRatio);
@@ -2137,11 +2210,11 @@ the update and manually apply it.)")));
if (platform_io.Platform_RenderWindow) platform_io.Platform_RenderWindow(viewport, nullptr);
if (platform_io.Renderer_RenderWindow) platform_io.Renderer_RenderWindow(viewport, nullptr);
if (vg) {
auto window = getGLFWwindowFromImGuiViewport(viewport);
auto window = getSDLWindowFromImGuiViewport(viewport);
auto nvgSubContext = m_nvgSubContextes.find(viewport->ID);
if (nvgSubContext != m_nvgSubContextes.end()) {
glfwGetWindowSize(window, &winWidth, &winHeight);
glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
SDL_GetWindowSize(window, &winWidth, &winHeight);
SDL_GetWindowSizeInPixels(window, &fbWidth, &fbHeight);
pxRatio = (float)fbWidth / (float)winWidth;
nvgSwitchSubContextGL(vg, nvgSubContext->second);
nvgBeginFrame(vg, winWidth, winHeight, pxRatio);
@@ -2171,9 +2244,9 @@ the update and manually apply it.)")));
if (platform_io.Platform_SwapBuffers) platform_io.Platform_SwapBuffers(viewport, nullptr);
if (platform_io.Renderer_SwapBuffers) platform_io.Renderer_SwapBuffers(viewport, nullptr);
}
glfwMakeContextCurrent(m_window);
SDL_GL_MakeCurrent(m_window, m_glContext);
}
glfwSwapBuffers(m_window);
SDL_GL_SwapWindow(m_window);
L.getfieldtable("nvg", LUA_GLOBALSINDEX);
L.push("_gui");
@@ -2209,7 +2282,7 @@ bool PCSX::GUI::configure() {
if (ImGui::Begin(_("Emulation Configuration"), &m_showCfg)) {
if (ImGui::SliderInt(_("Idle Swap Interval"), &m_idleSwapInterval, 0, 10)) {
changed = true;
if (!g_system->running()) glfwSwapInterval(m_idleSwapInterval);
if (!g_system->running()) SDL_GL_SetSwapInterval(m_idleSwapInterval);
}
ImGui::Separator();
// Emulation speed now lives at the audio sink (SPU::Speed), set in the SPU configuration window.
@@ -3005,6 +3078,25 @@ ImFont* PCSX::GUI::findClosestFont(const std::map<float, ImFont*>& fonts) {
void PCSX::GUI::changeScale(float scale) {
if (scale <= 0.0f) return;
m_currentScale = scale;
m_allScales.emplace(scale);
ImGui::SetCurrentFont(getMainFont());
// Track this scale so the font reload pass bakes glyphs pre-sized for it;
// if it's a previously-unseen scale, request a reload so the per-scale font
// map gets a matching entry. v1.92's dynamic font system also handles
// unbaked sizes lazily via PushFont(font, size), but the pre-baked path
// still avoids a same-frame atlas grow on viewport DPI flips.
if (m_allScales.insert(scale).second) {
m_reloadFonts = true;
return;
}
// Already-pinned scale: no font reload needed, but the default frame font
// and base size still have to track the new closest-scale match. v1.92
// draws default-context frames at Style.FontSizeBase, so we have to keep
// it in sync with the active main font's LegacySize on every DPI flip,
// not only the first one.
if (ImGui::GetCurrentContext()) {
ImFont* mainFont = getMainFont();
if (mainFont) {
ImGui::GetIO().FontDefault = mainFont;
ImGui::GetStyle().FontSizeBase = mainFont->LegacySize;
}
}
}
+25 -14
View File
@@ -78,7 +78,9 @@
#define GL_SHADER_VERSION "#version 300 es\n"
#endif
struct GLFWwindow;
struct SDL_Window;
struct SDL_GLContextState;
typedef SDL_GLContextState* SDL_GLContext;
struct NVGcontext;
namespace PCSX {
@@ -181,10 +183,6 @@ class GUI final : public UI {
void (*m_createWindowOldCallback)(ImGuiViewport *viewport) = nullptr;
void (*m_onChangedViewportOldCallback)(ImGuiViewport *viewport) = nullptr;
void (*m_destroyWindowOldCallback)(ImGuiViewport *viewport) = nullptr;
static void glfwKeyCallbackTrampoline(GLFWwindow *window, int key, int scancode, int action, int mods) {
g_gui->glfwKeyCallback(window, key, scancode, action, mods);
}
void glfwKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods);
void glErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message);
void changeScale(float scale);
bool m_onlyLogGLErrors = false;
@@ -307,13 +305,14 @@ class GUI final : public UI {
const ImVec2 &getRenderSize() { return m_renderSize; }
private:
GLFWwindow *m_window = nullptr;
SDL_Window *m_window = nullptr;
SDL_GLContext m_glContext = nullptr;
bool m_hasCoreProfile = false;
int &m_glfwPosX = settings.get<WindowPosX>().value;
int &m_glfwPosY = settings.get<WindowPosY>().value;
int &m_glfwSizeX = settings.get<WindowSizeX>().value;
int &m_glfwSizeY = settings.get<WindowSizeY>().value;
bool &m_glfwMaximized = settings.get<WindowMaximized>().value;
int &m_windowPosX = settings.get<WindowPosX>().value;
int &m_windowPosY = settings.get<WindowPosY>().value;
int &m_windowSizeX = settings.get<WindowSizeX>().value;
int &m_windowSizeY = settings.get<WindowSizeY>().value;
bool &m_windowMaximized = settings.get<WindowMaximized>().value;
GLuint m_VRAMTexture = 0;
NVGcontext *m_nvgContext = nullptr;
std::map<unsigned, void *> m_nvgSubContextes;
@@ -325,7 +324,7 @@ class GUI final : public UI {
int m_currentTexture = 0;
ImVec4 m_backgroundColor = ImColor(114, 144, 154);
ImVec2 m_framebufferSize = ImVec2(1, 1); // Size of GLFW window framebuffer
ImVec2 m_framebufferSize = ImVec2(1, 1); // Size of the SDL window framebuffer (in pixels)
ImVec2 m_renderSize = ImVec2(1, 1);
ImVec2 m_outputWindowSize = ImVec2(1, 1);
@@ -507,8 +506,20 @@ class GUI final : public UI {
Widgets::ShaderEditor m_offscreenShaderEditor = {"offscreen"};
ImFont *getMainFont() { return findClosestFont(m_mainFonts); }
ImFont *getMonoFont() { return findClosestFont(m_monoFonts); }
void useMainFont() { ImGui::PushFont(getMainFont()); }
void useMonoFont() { ImGui::PushFont(getMonoFont()); }
// ImGui v1.92's PushFont takes a size. We pass the closest-matching font's
// LegacySize (the size that was passed to AddFontFromFileTTF) so the
// per-scale font map continues to drive rendering size, matching pre-1.92
// PushFont(font) semantics. Passing 0.0f instead would mean "keep current
// size" which would render the larger DPI-baked font at the previous
// smaller base size, defeating the per-scale map entirely.
void useMainFont() {
ImFont *f = getMainFont();
ImGui::PushFont(f, f ? f->LegacySize : 0.0f);
}
void useMonoFont() {
ImFont *f = getMonoFont();
ImGui::PushFont(f, f ? f->LegacySize : 0.0f);
}
bool &allowMouseCaptureToggle() { return settings.get<AllowMouseCaptureToggle>().value; }
bool &isRawMouseMotionEnabled() { return settings.get<EnableRawMouseMotion>().value; }
-2
View File
@@ -17,11 +17,9 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#define GLFW_INCLUDE_NONE
#include "gui/widgets/assembly.h"
#include <GL/gl3w.h>
#include <GLFW/glfw3.h>
#include <algorithm>
#include <fstream>
+2 -2
View File
@@ -290,9 +290,9 @@ void PCSX::Widgets::CDRomViewer::imguiCB(const ImDrawList *parentList, const ImD
glEnableVertexAttribArray(m_locVtxPos);
glVertexAttribPointer(m_locVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert),
(GLvoid *)IM_OFFSETOF(ImDrawVert, pos));
(GLvoid *)offsetof(ImDrawVert, pos));
glEnableVertexAttribArray(m_locVtxUV);
glVertexAttribPointer(m_locVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid *)IM_OFFSETOF(ImDrawVert, uv));
glVertexAttribPointer(m_locVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid *)offsetof(ImDrawVert, uv));
}
void PCSX::Widgets::CDRomViewer::drawDisc(GUI *gui) {
+35
View File
@@ -143,6 +143,41 @@ void PCSX::Widgets::GPULogger::draw(PCSX::GPULogger* logger, const char* title)
std::string label;
ImGui::Separator();
if (ImGui::TreeNode(_("VRAM breakpoints"))) {
ImGuiHelpers::ShowHelpMarker(
_("Pauses emulation when a command reads from or writes to one of these VRAM rectangles. Handy to catch "
"the texture upload that stomped an area, since those are hard to spot in a frame that redraws "
"everything. The command that triggered gets highlighted in the logger view below. Note the footprints "
"are unclipped, so a primitive that ends up entirely outside the drawing area still counts as a write."));
if (ImGui::Button(_("Add breakpoint"))) {
logger->m_vramBreakpoints.emplace_back();
}
if (!logger->m_vramBreakpoints.empty()) {
ImGui::TextUnformatted(_("Enable, then X, Y, width, height:"));
}
unsigned toRemove = logger->m_vramBreakpoints.size();
for (unsigned i = 0; i < logger->m_vramBreakpoints.size(); i++) {
auto& bp = logger->m_vramBreakpoints[i];
ImGui::PushID(i);
ImGui::Checkbox("##enabled", &bp.enabled);
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 16.0f);
ImGui::InputInt4("##area", bp.area.raw);
ImGui::SameLine();
ImGui::Checkbox(_("Read"), &bp.onRead);
ImGui::SameLine();
ImGui::Checkbox(_("Write"), &bp.onWrite);
ImGui::SameLine();
if (ImGui::Button(_("Remove"))) toRemove = i;
ImGui::PopID();
}
if (toRemove != logger->m_vramBreakpoints.size()) {
logger->m_vramBreakpoints.erase(logger->m_vramBreakpoints.begin() + toRemove);
}
ImGui::TreePop();
}
ImGui::Separator();
label = fmt::format(f_("Frame {}###FrameCounterNode"), logger->m_frameCounter - m_frameCounterOrigin);
if (ImGui::TreeNode(label.c_str())) {
+41 -30
View File
@@ -343,32 +343,11 @@ void PCSX::Widgets::RAMViewer::imguiCB(const ImDrawList *parentList, const ImDra
glUniform1i(m_locShowHex, m_showHex);
glUniform1i(m_locShowGreyscale, m_showGreyscale);
// Extract glyph UV rects for hex digits from ImGui font atlas
ImFont *font = ImGui::GetFont();
float glyphUVs[16 * 4];
float glyphAspect = 0.5f; // default fallback
static const char hexChars[] = "0123456789ABCDEF";
for (int i = 0; i < 16; i++) {
const ImFontGlyph *g = font->FindGlyph(hexChars[i]);
if (g) {
glyphUVs[i * 4 + 0] = g->U0;
glyphUVs[i * 4 + 1] = g->V0;
glyphUVs[i * 4 + 2] = g->U1;
glyphUVs[i * 4 + 3] = g->V1;
if (i == 0) {
float gw = g->X1 - g->X0;
float gh = g->Y1 - g->Y0;
if (gh > 0.0f) glyphAspect = gw / gh;
}
} else {
glyphUVs[i * 4 + 0] = 0.0f;
glyphUVs[i * 4 + 1] = 0.0f;
glyphUVs[i * 4 + 2] = 0.0f;
glyphUVs[i * 4 + 3] = 0.0f;
}
}
glUniform4fv(m_locGlyphUVs, 16, glyphUVs);
glUniform1f(m_locGlyphAspect, glyphAspect);
// Glyph UV rects + aspect are now pre-fetched in drawRAM() before the
// ImGui render pass; consume the cached snapshot here so we don't grow
// the font atlas mid-render and end up sampling with stale UVs.
glUniform4fv(m_locGlyphUVs, 16, m_glyphUVs);
glUniform1f(m_locGlyphAspect, m_glyphAspect);
// Bind textures
glUniform1i(m_locRAMTexture, 0);
@@ -388,17 +367,17 @@ void PCSX::Widgets::RAMViewer::imguiCB(const ImDrawList *parentList, const ImDra
glActiveTexture(GL_TEXTURE3);
logger->bindExecHeatmap();
glActiveTexture(GL_TEXTURE4);
GLuint fontTexID = (GLuint)(intptr_t)font->ContainerAtlas->TexID;
glBindTexture(GL_TEXTURE_2D, fontTexID);
// Pre-fetched in drawRAM(); see m_fontTexID notes in ram-viewer.h.
glBindTexture(GL_TEXTURE_2D, m_fontTexID);
glActiveTexture(GL_TEXTURE0);
// Set vertex attributes to match ImGui vertex layout
glEnableVertexAttribArray(m_locVtxPos);
glVertexAttribPointer(m_locVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert),
(GLvoid *)IM_OFFSETOF(ImDrawVert, pos));
(GLvoid *)offsetof(ImDrawVert, pos));
glEnableVertexAttribArray(m_locVtxUV);
glVertexAttribPointer(m_locVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert),
(GLvoid *)IM_OFFSETOF(ImDrawVert, uv));
(GLvoid *)offsetof(ImDrawVert, uv));
}
void PCSX::Widgets::RAMViewer::drawRAM(GUI *gui) {
@@ -410,6 +389,38 @@ void PCSX::Widgets::RAMViewer::drawRAM(GUI *gui) {
m_origin = ImGui::GetCursorScreenPos();
m_mousePos = ImGui::GetIO().MousePos;
// Pre-fetch hex-glyph UVs from ImFontBaked at the current font size BEFORE
// ImGui renders. Doing this from inside the AddCallback render closure
// could grow the atlas after the backend has already staged its texture
// upload for the frame, leading the custom shader to sample with stale
// UVs. The render callback only consumes the cached snapshot.
{
ImFont *font = ImGui::GetFont();
ImFontBaked *baked = font ? font->GetFontBaked(ImGui::GetFontSize()) : nullptr;
m_glyphAspect = 0.5f; // default fallback if FindGlyph misses
static const char hexChars[] = "0123456789ABCDEF";
for (int i = 0; i < 16; i++) {
const ImFontGlyph *g = baked ? baked->FindGlyph(hexChars[i]) : nullptr;
if (g) {
m_glyphUVs[i * 4 + 0] = g->U0;
m_glyphUVs[i * 4 + 1] = g->V0;
m_glyphUVs[i * 4 + 2] = g->U1;
m_glyphUVs[i * 4 + 3] = g->V1;
if (i == 0) {
float gw = g->X1 - g->X0;
float gh = g->Y1 - g->Y0;
if (gh > 0.0f) m_glyphAspect = gw / gh;
}
} else {
m_glyphUVs[i * 4 + 0] = 0.0f;
m_glyphUVs[i * 4 + 1] = 0.0f;
m_glyphUVs[i * 4 + 2] = 0.0f;
m_glyphUVs[i * 4 + 3] = 0.0f;
}
}
m_fontTexID = (GLuint)(intptr_t)ImGui::GetIO().Fonts->TexRef.GetTexID();
}
ImDrawList *drawList = ImGui::GetWindowDrawList();
drawList->AddCallback(
[](const ImDrawList *parentList, const ImDrawCmd *cmd) {
+10
View File
@@ -78,6 +78,16 @@ class RAMViewer : public ZoomableImage {
int m_locFontAtlas;
int m_locGlyphUVs;
int m_locGlyphAspect;
// Cached hex-glyph atlas state, populated during drawRAM() (which runs
// before the ImGui render pass), then consumed inside imguiCB() during
// the actual draw. Pulling glyphs out of ImFontBaked from inside the
// render callback could grow the atlas after the backend has already
// staged its texture upload for the frame, leaving us sampling with
// stale UVs; pre-fetching avoids that race.
float m_glyphUVs[16 * 4] = {0};
float m_glyphAspect = 0.5f;
GLuint m_fontTexID = 0;
int m_locShowHex;
int m_locShowGreyscale;
+4 -1
View File
@@ -752,7 +752,10 @@ void PCSX::Widgets::ShaderEditor::renderWithImgui(GUI *gui, ImTextureID textureI
}
void PCSX::Widgets::ShaderEditor::imguiCB(const ImDrawList *parentList, const ImDrawCmd *cmd) {
GLuint textureID = static_cast<GLuint>(cmd->TextureId);
// ImGui v1.92 renamed ImDrawCmd::TextureId to ImDrawCmd::TexRef (an
// ImTextureRef). GetTexID() returns the underlying ImTextureID we
// previously read directly.
GLuint textureID = static_cast<GLuint>(cmd->TexRef.GetTexID());
GLfloat projMtx[4][4];
if (m_imguiProjMtxLoc == -1) {
+2 -2
View File
@@ -553,10 +553,10 @@ void PCSX::Widgets::VRAMViewer::imguiCB(const ImDrawList *parentList, const ImDr
glUniform1i(m_attribLocationTex, 0);
glEnableVertexAttribArray(m_attribLocationVtxPos);
glVertexAttribPointer(m_attribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert),
(GLvoid *)IM_OFFSETOF(ImDrawVert, pos));
(GLvoid *)offsetof(ImDrawVert, pos));
glEnableVertexAttribArray(m_attribLocationVtxUV);
glVertexAttribPointer(m_attribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert),
(GLvoid *)IM_OFFSETOF(ImDrawVert, uv));
(GLvoid *)offsetof(ImDrawVert, uv));
glUniform4f(m_attribLocationWrittenColor, m_writtenColor.x, m_writtenColor.y, m_writtenColor.z, m_writtenColor.w);
glUniform1i(m_attribLocationWrittenHeatmap, 1);
glUniform1i(m_attribLocationWrittenHighlight, 3);
-1
View File
@@ -20,7 +20,6 @@
#include "gui/widgets/zep.h"
#include <GL/gl3w.h>
#include <GLFW/glfw3.h>
#include "gui/gui.h"
#include "zep/regress.h"
+3 -1
View File
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include <csignal>
#include <filesystem>
#include <iostream>
@@ -246,7 +248,7 @@ int pcsxMain(int argc, char **argv) {
auto argPath1 = args.get<std::string>("memcard1");
auto argPath2 = args.get<std::string>("memcard2");
if (argPath1.has_value()) emuSettings.get<PCSX::Emulator::SettingMcd1>() = argPath1.value();
if (argPath2.has_value()) emuSettings.get<PCSX::Emulator::SettingMcd2>() = argPath1.value();
if (argPath2.has_value()) emuSettings.get<PCSX::Emulator::SettingMcd2>() = argPath2.value();
PCSX::u8string path1 = emuSettings.get<PCSX::Emulator::SettingMcd1>().string();
PCSX::u8string path2 = emuSettings.get<PCSX::Emulator::SettingMcd2>().string();
-11
View File
@@ -1,18 +1,7 @@
BUILD ?= Release
ifeq ($(OS),Windows_NT)
HAS_LINUX_MIPS_GCC = false
else
HAS_LINUX_MIPS_GCC = $(shell which mipsel-linux-gnu-gcc > /dev/null 2> /dev/null && echo true || echo false)
endif
ifeq ($(HAS_LINUX_MIPS_GCC),true)
PREFIX ?= mipsel-linux-gnu
FORMAT ?= elf32-tradlittlemips
else
PREFIX ?= mipsel-none-elf
FORMAT ?= elf32-littlemips
endif
ROOTDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+3 -1
View File
@@ -42,7 +42,9 @@ The Makefile features several toggles to mutate the way the compilation works. D
## Status
This subproject is fairly functional and usable. OpenBIOS does almost all the same things as the retail BIOS does when booting, and implements most of its features. [Many games](https://docs.google.com/spreadsheets/d/1UNGs7uYb8viAbm7YJaf1CR4dkgX7ZzntUdcowGsjcVc/edit?usp=sharing) are booting and working properly with this code. It can be used in emulators or on the real console, either while replacing the rom chip, or by using the "cart" build and programming the flash chip of a cheat cart with the result.
This subproject is fairly functional and usable. OpenBIOS does almost all the same things as the retail BIOS does when booting, and implements most of its features. Many games are booting and working properly with this code. It can be used in emulators or on the real console, either while replacing the rom chip, or by using the "cart" build and programming the flash chip of a cheat cart with the result.
Read the [compatibility](COMPATIBILITY.md) document for more information.
Please feel free to report bugs encountered while using this project.
+6 -4
View File
@@ -6,16 +6,18 @@ In order to develop for the PlayStation1 using the PSYQo library, a modern C++ t
## Linux
### Debian derivatives:
There's no distribution package for the toolchain, so build it from source:
```bash
sudo apt-get install -y make g++-mipsel-linux-gnu
sudo apt-get install -y make wget bzip2 xz-utils bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
sudo bash tools/linux-mips/spawn-compiler.sh
```
### Arch derivatives:
The mipsel environment can be installed from [AUR](https://wiki.archlinux.org/index.php/Aur) : [cross-mipsel-linux-gnu-binutils](https://aur.archlinux.org/packages/cross-mipsel-linux-gnu-binutils/) and [cross-mipsel-linux-gnu-gcc](https://aur.archlinux.org/packages/cross-mipsel-linux-gnu-gcc/) using your [AURhelper](https://wiki.archlinux.org/index.php/AUR_helpers) of choice:
The mipsel environment can be installed from [AUR](https://wiki.archlinux.org/index.php/Aur) : [mipsel-none-elf-binutils](https://aur.archlinux.org/packages/mipsel-none-elf-binutils/) and [mipsel-none-elf-gcc](https://aur.archlinux.org/packages/mipsel-none-elf-gcc/) using your [AURhelper](https://wiki.archlinux.org/index.php/AUR_helpers) of choice:
```bash
trizen -S cross-mipsel-linux-gnu-binutils cross-mipsel-linux-gnu-gcc
trizen -S mipsel-none-elf-binutils mipsel-none-elf-gcc
```
### All other flavors of Unix environments:
@@ -44,7 +46,7 @@ The computer might need to be rebooted after the installation of this script.
Next, run the following command to install the proper toolchain and its dependencies:
```cmd
mips install 16.1.0
mips install 16.2.0
```
## Docker
+12
View File
@@ -0,0 +1,12 @@
TARGET = life3d
TYPE = ps-exe
SRCS = \
life3d.cpp \
ifeq ($(TEST),true)
CPPFLAGS = -Werror
endif
CXXFLAGS = -std=c++20
include ../../psyqo.mk
@@ -0,0 +1,172 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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.
*/
// Conway's Game of Life on a wrapping board, with no platform dependencies at
// all. This makes it possible to run the simulation in a test harness.
#pragma once
#include <stdint.h>
namespace life {
// 24x24 is the largest board that still leaves the cubes individually legible
// at 320x240, and 576 cells is comfortably inside the per-frame GTE budget even
// in the first dense generation of a fresh soup.
static constexpr unsigned kGridW = 24;
static constexpr unsigned kGridH = 24;
static constexpr unsigned kCellCount = kGridW * kGridH;
// A board is one byte per cell, 0 or 1. A bitboard would be smaller but
// considerably less readable, and 576 bytes is not the constraint here.
struct Board {
uint8_t cells[kCellCount];
uint8_t at(unsigned x, unsigned y) const { return cells[y * kGridW + x]; }
void set(unsigned x, unsigned y, uint8_t v) { cells[y * kGridW + x] = v; }
void clear() {
for (unsigned i = 0; i < kCellCount; i++) cells[i] = 0;
}
unsigned population() const {
unsigned n = 0;
for (unsigned i = 0; i < kCellCount; i++) n += cells[i];
return n;
}
bool operator==(const Board& other) const {
for (unsigned i = 0; i < kCellCount; i++) {
if (cells[i] != other.cells[i]) return false;
}
return true;
}
bool operator!=(const Board& other) const { return !(*this == other); }
};
// The board wraps, so the field has no edges to collect debris against and a
// glider runs forever instead of walking off into nothing. Wrapping is done with
// a conditional rather than a modulo because the offsets are only ever +/-1:
// (x + kGridW - 1) % kGridW is the correct form for a modular decrement, but on
// an R3000 the compare is cheaper than the divide and this runs 576 * 8 times a
// generation.
static inline unsigned wrapDec(unsigned v, unsigned n) { return v == 0 ? n - 1 : v - 1; }
static inline unsigned wrapInc(unsigned v, unsigned n) { return v + 1 == n ? 0 : v + 1; }
static inline unsigned neighbours(const Board& b, unsigned x, unsigned y) {
unsigned xm = wrapDec(x, kGridW), xp = wrapInc(x, kGridW);
unsigned ym = wrapDec(y, kGridH), yp = wrapInc(y, kGridH);
return b.at(xm, ym) + b.at(x, ym) + b.at(xp, ym) + b.at(xm, y) + b.at(xp, y) + b.at(xm, yp) + b.at(x, yp) +
b.at(xp, yp);
}
static inline void step(const Board& in, Board* out) {
for (unsigned y = 0; y < kGridH; y++) {
for (unsigned x = 0; x < kGridW; x++) {
unsigned n = neighbours(in, x, y);
uint8_t alive = in.at(x, y);
out->set(x, y, (n == 3 || (alive && n == 2)) ? 1 : 0);
}
}
}
// xorshift32.
struct Rng {
uint32_t state;
explicit Rng(uint32_t seed) : state(seed ? seed : 0x1d872b41) {}
uint32_t next() {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
};
// Fills the board with a random soup at roughly `percent` density.
static inline void soup(Board* b, Rng* rng, unsigned percent) {
for (unsigned i = 0; i < kCellCount; i++) {
b->cells[i] = (rng->next() % 100u) < percent ? 1 : 0;
}
}
static constexpr unsigned kSoupDensityPercent = 32;
// A board that has come back to a state it held two generations ago is in a
// still-life or period-2 attractor and nothing further will happen to it. Give
// it a few generations of grace anyway: a large board can be locally periodic
// while another region is still running, and the two-generation test cannot
// tell those two apart from one sample.
static constexpr unsigned kStableGenerationsBeforeReseed = 6;
// The whole simulation, including the policy for deciding a board is finished.
// That policy lives here rather than in the renderer because it is the part
// most likely to be wrong in a way nobody notices - a detector that never fires
// looks exactly like a board that never settles, and both of those look like a
// working demo until you sit and watch it for five minutes.
struct Sim {
Board board;
Board scratch;
// The board as it was two generations ago. Comparing against the PREVIOUS
// generation only catches still lifes, and a board of nothing but blinkers
// would then run for ever while nothing about it ever changed again.
Board twoAgo;
Rng rng;
unsigned generation = 0;
unsigned population = 0;
unsigned stableFor = 0;
unsigned reseeds = 0;
explicit Sim(uint32_t seed) : rng(seed) { reseed(); }
void reseed() {
soup(&board, &rng, kSoupDensityPercent);
twoAgo.clear();
population = board.population();
generation = 0;
stableFor = 0;
reseeds++;
}
void advance() {
step(board, &scratch);
bool repeated = (scratch == twoAgo);
twoAgo = board;
board = scratch;
population = board.population();
generation++;
if (population == 0) {
reseed();
return;
}
if (repeated) {
if (++stableFor >= kStableGenerationsBeforeReseed) reseed();
} else {
stableFor = 0;
}
}
};
} // namespace life
+424
View File
@@ -0,0 +1,424 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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.
*/
// Conway's Game of Life on a 24x24 wrapping board, drawn as a field of small
// grey cubes and lit by the GTE's hardware lighting, with the camera orbiting
// the field.
//
// The teapot example lights a smooth surface, where every vertex has its own
// normal and the whole point is that the shading varies continuously. This is
// the opposite case and it is worth having both: a field of axis-aligned cubes
// has exactly SIX distinct normals no matter how many cubes are on the board,
// so the entire lighting cost of a frame is two ncct issues, once, before the
// geometry loop starts. Six hundred cubes cost the same to light as one. That
// is not a trick specific to Life - it is the general shape of lighting any
// instanced, uniformly-oriented geometry on this hardware, and it is invisible
// if the only example you have lights per vertex.
//
// The second thing this example is for: the cubes are instances of ONE set of
// eight corner offsets. Rather than building a per-cube vertex array on the
// CPU, the rotation matrix is loaded once and the GTE's translation vector is
// rewritten per cube, so each cube costs three ctc2 writes and four kernel
// issues and no vertex maths at all. Grid positions are accumulated with adds
// as the loop walks the board, so there is not a single multiply in the inner
// loop either.
#include "psyqo/application.hh"
#include "psyqo/fixed-point.hh"
#include "psyqo/font.hh"
#include "psyqo/fragments.hh"
#include "psyqo/gpu.hh"
#include "psyqo/gte-kernels.hh"
#include "psyqo/gte-math.hh"
#include "psyqo/gte-registers.hh"
#include "psyqo/matrix.hh"
#include "psyqo/ordering-table.hh"
#include "psyqo/primitives/common.hh"
#include "psyqo/primitives/quads.hh"
#include "psyqo/scene.hh"
#include "psyqo/simplepad.hh"
#include "psyqo/soft-math.hh"
#include "psyqo/trigonometry.hh"
#include "psyqo/vector.hh"
#include "life-rules.hh"
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
namespace {
// Distance between cube centres, and the cube's half-extent. The gap between
// the two is what keeps neighbouring cells readable as separate cells rather
// than as one slab.
constexpr psyqo::FixedPoint<> kPitch = 0.28_fp;
constexpr psyqo::FixedPoint<> kHalf = 0.105_fp;
// How long a generation is held on screen, in frames.
constexpr unsigned kFramesPerGeneration = 18;
// At most three faces of a convex box can face any single viewpoint, so this is
// a hard bound and not an estimate.
constexpr unsigned kMaxQuads = life::kCellCount * 3;
constexpr unsigned ORDERING_TABLE_SIZE = 1024;
// How far the board sits from the eye, in the raw 4.12 units the GTE reads the
// packed vectors in. Two constraints pin this down and they pull in opposite
// directions. SZ saturates at 16 bits, so the far corner of the board has to
// come out under 65535 or the depth sort collapses at the back; and the board
// has to be far enough away that the near corner does not swing behind the eye
// as the camera orbits. The board's half-diagonal is 24 * 0.28 / 2 * sqrt(2) =
// 4.75 units = 19470 raw, so anything in the low thirty thousands satisfies
// both with room to spare.
constexpr int32_t kCameraDistance = 42000;
// The board's centre is pushed a little UP the screen, because the tilt puts the
// near edge lower and larger and an image centred on the board's middle runs off
// the bottom of the frame. Screen Y is down, so lifting is negative.
constexpr int32_t kCameraLift = -1500;
// Down-tilt of the camera. psyqo's X rotation sends +Z to +Y and the screen's Y
// runs downwards, so looking DOWN at a board lying in the XZ plane wants a
// negative angle.
constexpr psyqo::Angle kElevation = -0.235_pi;
constexpr psyqo::Angle kOrbitPerFrame = 0.0013_pi;
// The eight corners of a cube, indexed by bit: 1 = +x, 2 = +y, 4 = +z.
constexpr unsigned kCornerCount = 8;
// The six faces, each as four corner indices in a Z - the order the GPU wants,
// where it draws triangles ABC and BCD. Each is derived from the ring that runs
// counter-clockwise seen from OUTSIDE the cube (p0, p1, p2, p3) by emitting
// p0, p1, p3, p2; keeping the ring in the comment is what makes the winding
// checkable by hand rather than by eye.
struct Face {
uint8_t v[4];
};
constexpr Face kFaces[6] = {
{{1, 3, 5, 7}}, // +X, ring 1,3,7,5
{{0, 4, 2, 6}}, // -X, ring 0,4,6,2
{{2, 6, 3, 7}}, // +Y, ring 2,6,7,3
{{0, 1, 4, 5}}, // -Y, ring 0,1,5,4
{{4, 5, 6, 7}}, // +Z, ring 4,5,7,6
{{0, 2, 1, 3}}, // -Z, ring 0,2,3,1
};
class Life3D final : public psyqo::Application {
void prepare() override;
void createScene() override;
public:
psyqo::Trig<> m_trig;
psyqo::Font<> m_font;
psyqo::SimplePad m_input;
};
class Life3DScene final : public psyqo::Scene {
void start(StartReason reason) override;
void frame() override;
// --- simulation ---
life::Sim m_sim{0x9e3779b9};
unsigned m_frameInGeneration = 0;
bool m_paused = false;
// --- rendering ---
psyqo::Angle m_orbit = 0;
bool m_lightOn[3] = {true, true, true};
psyqo::GTE::PackedVec3 m_corners[kCornerCount];
psyqo::GTE::PackedVec3 m_faceNormals[6];
psyqo::Color m_faceColor[6];
psyqo::OrderingTable<ORDERING_TABLE_SIZE> m_ots[2];
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
psyqo::Fragments::SimpleFragment<psyqo::Prim::Quad> m_frags[2][kMaxQuads];
static constexpr psyqo::Color c_bg = {{.r = 12, .g = 13, .b = 18}};
// Three directional lights in view space, so they stay put while the board
// turns underneath them and the lit face of a cube changes as it comes
// round. Rows are light directions, unit length, and each points FROM the
// surface TOWARDS the light, because nccs clamps the row-dot-normal at zero
// and a surface facing away has to come out negative. Screen Y runs down
// and screen Z runs into the display, so "above and in front of the viewer"
// is negative in both.
static constexpr psyqo::Matrix33 c_lightDirs = {{
{0.336_fp, -0.840_fp, -0.426_fp}, // key, high and slightly right
{-0.760_fp, -0.194_fp, -0.620_fp}, // fill, from the left and low
{0.402_fp, 0.512_fp, -0.759_fp}, // rim, from below, kicks the near faces
}};
// LCM: rows are colour channels, COLUMNS are lights, so column i is the
// colour of light i. Deleting a light means zeroing its column. Zeroing a
// ROW would remove one channel from all three lights at once, which is a
// different and much less useful thing, and that asymmetry is the whole
// reason the GTE splits direction and colour across two matrices.
// key fill rim
static constexpr psyqo::Matrix33 c_lightColors = {{
{0.95_fp, 0.22_fp, 0.54_fp}, // R
{0.93_fp, 0.30_fp, 0.36_fp}, // G
{0.88_fp, 0.52_fp, 0.22_fp}, // B
}};
};
Life3D g_life3d;
Life3DScene g_scene;
} // namespace
void Life3D::prepare() {
psyqo::GPU::Configuration config;
config.set(psyqo::GPU::Resolution::W320)
.set(psyqo::GPU::VideoMode::AUTO)
.set(psyqo::GPU::ColorMode::C15BITS)
.set(psyqo::GPU::Interlace::PROGRESSIVE);
gpu().initialize(config);
}
void Life3D::createScene() {
m_font.uploadSystemFont(gpu());
m_input.initialize();
pushScene(&g_scene);
}
void Life3DScene::start(StartReason reason) {
for (unsigned i = 0; i < kCornerCount; i++) {
psyqo::Vec3 c = {(i & 1) ? kHalf : -kHalf, (i & 2) ? kHalf : -kHalf, (i & 4) ? kHalf : -kHalf};
m_corners[i] = psyqo::GTE::PackedVec3(c);
}
// One unit normal per face, in the board's own space. The GTE reads these
// as plain integers in the same 4.12 the packed vectors use, so "one" here
// is 4096.
static constexpr psyqo::Vec3 c_normals[6] = {
{1.0_fp, 0.0_fp, 0.0_fp}, {-1.0_fp, 0.0_fp, 0.0_fp}, {0.0_fp, 1.0_fp, 0.0_fp},
{0.0_fp, -1.0_fp, 0.0_fp}, {0.0_fp, 0.0_fp, 1.0_fp}, {0.0_fp, 0.0_fp, -1.0_fp},
};
for (unsigned i = 0; i < 6; i++) m_faceNormals[i] = psyqo::GTE::PackedVec3(c_normals[i]);
g_life3d.m_input.setOnEvent([this](const psyqo::SimplePad::Event& event) {
if (event.type != psyqo::SimplePad::Event::ButtonReleased) return;
switch (event.button) {
case psyqo::SimplePad::Button::Triangle: m_lightOn[0] = !m_lightOn[0]; break;
case psyqo::SimplePad::Button::Circle: m_lightOn[1] = !m_lightOn[1]; break;
case psyqo::SimplePad::Button::Cross: m_lightOn[2] = !m_lightOn[2]; break;
case psyqo::SimplePad::Button::Square: m_paused = !m_paused; break;
case psyqo::SimplePad::Button::Start: m_sim.reseed(); break;
default: break;
}
});
// Projection context. Unlike the teapot, the translation vector is NOT part
// of this: it is rewritten once per cube in frame(), which is what makes
// every cube share one set of corner vectors.
psyqo::GTE::write<psyqo::GTE::Register::OFX, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(160.0).raw());
psyqo::GTE::write<psyqo::GTE::Register::OFY, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(120.0).raw());
psyqo::GTE::write<psyqo::GTE::Register::H, psyqo::GTE::Unsafe>(330);
psyqo::GTE::write<psyqo::GTE::Register::ZSF3, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 3);
psyqo::GTE::write<psyqo::GTE::Register::ZSF4, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 4);
// A cool, dim ambient so the unlit faces read as dark grey rather than as
// holes in the image. These are 1.19.12, so 0x150 is about 8%.
psyqo::GTE::write<psyqo::GTE::Register::RBK, psyqo::GTE::Unsafe>(0x0e0);
psyqo::GTE::write<psyqo::GTE::Register::GBK, psyqo::GTE::Unsafe>(0x0f0);
psyqo::GTE::write<psyqo::GTE::Register::BBK, psyqo::GTE::Unsafe>(0x130);
// The material the lights multiply against: a flat neutral grey with a hint
// of blue. On an UNTEXTURED primitive 0xff is full brightness - 0x80 is the
// neutral point for texture blending, and using it here would halve the
// whole image for no reason.
psyqo::GTE::write<psyqo::GTE::Register::RGB, psyqo::GTE::Unsafe>(0x00989ca4);
}
void Life3DScene::frame() {
if (!m_paused && ++m_frameInGeneration >= kFramesPerGeneration) {
m_frameInGeneration = 0;
m_sim.advance();
}
int parity = gpu().getParity();
auto& ot = m_ots[parity];
auto& clear = m_clear[parity];
auto& frags = m_frags[parity];
gpu().getNextClear(clear.primitive, c_bg);
gpu().chain(clear);
// Orbit about the board's vertical axis, then tip the whole thing towards
// the viewer. multiplyMatrix33(A, B) composes so that A happens FIRST.
auto spin = psyqo::SoftMath::generateRotationMatrix33(m_orbit, psyqo::SoftMath::Axis::Y, g_life3d.m_trig);
auto tilt = psyqo::SoftMath::generateRotationMatrix33(kElevation, psyqo::SoftMath::Axis::X, g_life3d.m_trig);
psyqo::Matrix33 transform;
psyqo::GteMath::multiplyMatrix33(spin, tilt, &transform);
// ---- Light the six faces, once, for the whole board. ----
// This has to happen before the rotation matrix and the translation vector
// are loaded below, because GteMath::multiplyMatrix33 uses RT and TR as
// scratch. The clobber list is the real API surface of these helpers.
psyqo::Matrix33 llm;
psyqo::GteMath::multiplyMatrix33(transform, c_lightDirs, &llm);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Light>(llm);
psyqo::Matrix33 lcm = c_lightColors;
for (unsigned row = 0; row < 3; row++) {
if (!m_lightOn[0]) lcm.vs[row].x = 0.0_fp;
if (!m_lightOn[1]) lcm.vs[row].y = 0.0_fp;
if (!m_lightOn[2]) lcm.vs[row].z = 0.0_fp;
}
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::Color>(lcm);
for (unsigned i = 0; i < 6; i += 3) {
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V0>(m_faceNormals[i + 0]);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V1>(m_faceNormals[i + 1]);
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(m_faceNormals[i + 2]);
psyqo::GTE::Kernels::ncct();
m_faceColor[i + 0].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB0>();
m_faceColor[i + 1].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB1>();
m_faceColor[i + 2].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB2>();
}
// ---- Walk the board, one cube per live cell. ----
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Rotation>(transform);
// Where one step along the board's X and Z axes lands after rotation. A
// rotation matrix applied to (kPitch, 0, 0) is just kPitch times its first
// COLUMN, and psyqo's Matrix33 stores rows, hence the transposed look.
const int32_t stepXx = (transform.vs[0].x * kPitch).raw();
const int32_t stepXy = (transform.vs[1].x * kPitch).raw();
const int32_t stepXz = (transform.vs[2].x * kPitch).raw();
const int32_t stepZx = (transform.vs[0].z * kPitch).raw();
const int32_t stepZy = (transform.vs[1].z * kPitch).raw();
const int32_t stepZz = (transform.vs[2].z * kPitch).raw();
// Cell (0,0) in camera space: the board's centre pushed back to the camera
// distance, minus half a board along each axis.
const int32_t halfW = int32_t(life::kGridW - 1);
const int32_t halfH = int32_t(life::kGridH - 1);
const int32_t originX = -(halfW * stepXx + halfH * stepZx) / 2;
const int32_t originY = kCameraLift - (halfW * stepXy + halfH * stepZy) / 2;
const int32_t originZ = kCameraDistance - (halfW * stepXz + halfH * stepZz) / 2;
unsigned drawn = 0;
psyqo::Vertex projected[kCornerCount];
uint32_t sz[kCornerCount];
int32_t rowX = originX, rowY = originY, rowZ = originZ;
for (unsigned gy = 0; gy < life::kGridH; gy++) {
int32_t cx = rowX, cy = rowY, cz = rowZ;
for (unsigned gx = 0; gx < life::kGridW; gx++) {
if (m_sim.board.at(gx, gy)) {
// The cube's centre IS the GTE's translation vector, so the
// eight corner offsets below are the same eight vectors for
// every cube on the board.
psyqo::GTE::write<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>(cx);
psyqo::GTE::write<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>(cy);
psyqo::GTE::write<psyqo::GTE::Register::TRZ, psyqo::GTE::Unsafe>(cz);
// Eight corners: two rtpt triples and a pair of rtps. rtpt is
// three transforms for one kernel issue, so the split is 3+3+1+1
// rather than eight singles.
for (unsigned i = 0; i < 6; i += 3) {
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V0>(m_corners[i + 0]);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V1>(m_corners[i + 1]);
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(m_corners[i + 2]);
psyqo::GTE::Kernels::rtpt();
psyqo::GTE::read<psyqo::GTE::Register::SXY0>(&projected[i + 0].packed);
psyqo::GTE::read<psyqo::GTE::Register::SXY1>(&projected[i + 1].packed);
psyqo::GTE::read<psyqo::GTE::Register::SXY2>(&projected[i + 2].packed);
psyqo::GTE::read<psyqo::GTE::Register::SZ1>(&sz[i + 0]);
psyqo::GTE::read<psyqo::GTE::Register::SZ2>(&sz[i + 1]);
psyqo::GTE::read<psyqo::GTE::Register::SZ3>(&sz[i + 2]);
}
for (unsigned i = 6; i < kCornerCount; i++) {
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V0>(m_corners[i]);
psyqo::GTE::Kernels::rtps();
psyqo::GTE::read<psyqo::GTE::Register::SXY2>(&projected[i].packed);
psyqo::GTE::read<psyqo::GTE::Register::SZ3>(&sz[i]);
}
for (unsigned f = 0; f < 6; f++) {
if (drawn >= kMaxQuads) break;
const auto& face = kFaces[f];
const auto& pa = projected[face.v[0]];
const auto& pb = projected[face.v[1]];
const auto& pc = projected[face.v[2]];
const auto& pd = projected[face.v[3]];
// Backface cull on the projected screen coordinates. The
// four points go in a Z, so the winding of A,B,C is the
// opposite of the ring order, and the faces to keep are the
// ones whose cross product comes out negative.
psyqo::GTE::write<psyqo::GTE::Register::SXY0, psyqo::GTE::Unsafe>(pa.packed);
psyqo::GTE::write<psyqo::GTE::Register::SXY1, psyqo::GTE::Unsafe>(pb.packed);
psyqo::GTE::write<psyqo::GTE::Register::SXY2, psyqo::GTE::Safe>(pc.packed);
psyqo::GTE::Kernels::nclip();
int32_t mac0 = 0;
psyqo::GTE::read<psyqo::GTE::Register::MAC0>(reinterpret_cast<uint32_t*>(&mac0));
if (mac0 >= 0) continue;
int32_t z =
int32_t((sz[face.v[0]] + sz[face.v[1]] + sz[face.v[2]] + sz[face.v[3]]) >> 2);
z = (z * ORDERING_TABLE_SIZE) >> 16;
if (z < 0 || z >= ORDERING_TABLE_SIZE) continue;
auto& frag = frags[drawn];
auto& prim = frag.primitive;
prim.setPointA(pa);
prim.setPointB(pb);
prim.setPointC(pc);
prim.setPointD(pd);
// Six colours for the whole board, computed above. A
// GouraudQuad would carry four copies of the same colour
// and three extra words per face for no visible difference:
// a flat face lit by directional lights IS flat.
prim.setColor(m_faceColor[f]);
ot.insert(frag, z);
drawn++;
}
}
cx += stepXx;
cy += stepXy;
cz += stepXz;
}
rowX += stepZx;
rowY += stepZy;
rowZ += stepZz;
}
gpu().chain(ot);
static constexpr psyqo::Color c_hud = {{.r = 0xa0, .g = 0xa8, .b = 0xb8}};
static constexpr psyqo::Color c_on = {{.r = 0xff, .g = 0xff, .b = 0xff}};
static constexpr psyqo::Color c_off = {{.r = 0x48, .g = 0x48, .b = 0x50}};
// The system font's glyphs are 8x16, so lines step by 16 plus leading.
static constexpr int16_t c_lineHeight = 18;
auto& font = g_life3d.m_font;
font.chainprintf(gpu(), {{.x = 6, .y = 6}}, c_hud, "gen %u pop %u%s", m_sim.generation, m_sim.population,
m_paused ? " [paused]" : "");
font.chainprintf(gpu(), {{.x = 6, .y = int16_t(6 + c_lineHeight)}}, m_lightOn[0] ? c_on : c_off, "[Tri] key");
font.chainprintf(gpu(), {{.x = 6, .y = int16_t(6 + 2 * c_lineHeight)}}, m_lightOn[1] ? c_on : c_off, "[Cir] fill");
font.chainprintf(gpu(), {{.x = 6, .y = int16_t(6 + 3 * c_lineHeight)}}, m_lightOn[2] ? c_on : c_off, "[Cro] rim");
m_orbit += kOrbitPerFrame;
}
int main() { return g_life3d.run(); }
+12
View File
@@ -0,0 +1,12 @@
TARGET = teapot
TYPE = ps-exe
SRCS = \
teapot.cpp \
ifeq ($(TEST),true)
CPPFLAGS = -Werror
endif
CXXFLAGS = -std=c++20
include ../../psyqo.mk
@@ -0,0 +1,343 @@
// Utah teapot control data: 32 bicubic Bezier patches over 290 control points.
// Newell dataset, from the classic SPD software via three.js TeapotGeometry.
// Coordinates are 20.12 fixed point (raw = value * 4096). Z is up.
// Generated once, offline; see the README.
#pragma once
#include <stdint.h>
namespace teapot {
static constexpr unsigned kPatchCount = 32;
static constexpr unsigned kControlCount = 290;
// Raw 20.12 triples, ready for psyqo::Vec3 with FixedPoint<>::RAW.
static constexpr int32_t kControlPoints[kControlCount][3] = {
{ 5734, 0, 9830 },
{ 5734, -3211, 9830 },
{ 3211, -5734, 9830 },
{ 0, -5734, 9830 },
{ 5478, 0, 10368 },
{ 5478, -3068, 10368 },
{ 3068, -5478, 10368 },
{ 0, -5478, 10368 },
{ 5888, 0, 10368 },
{ 5888, -3297, 10368 },
{ 3297, -5888, 10368 },
{ 0, -5888, 10368 },
{ 6144, 0, 9830 },
{ 6144, -3441, 9830 },
{ 3441, -6144, 9830 },
{ 0, -6144, 9830 },
{ -3211, -5734, 9830 },
{ -5734, -3211, 9830 },
{ -5734, 0, 9830 },
{ -3068, -5478, 10368 },
{ -5478, -3068, 10368 },
{ -5478, 0, 10368 },
{ -3297, -5888, 10368 },
{ -5888, -3297, 10368 },
{ -5888, 0, 10368 },
{ -3441, -6144, 9830 },
{ -6144, -3441, 9830 },
{ -6144, 0, 9830 },
{ -5734, 3211, 9830 },
{ -3211, 5734, 9830 },
{ 0, 5734, 9830 },
{ -5478, 3068, 10368 },
{ -3068, 5478, 10368 },
{ 0, 5478, 10368 },
{ -5888, 3297, 10368 },
{ -3297, 5888, 10368 },
{ 0, 5888, 10368 },
{ -6144, 3441, 9830 },
{ -3441, 6144, 9830 },
{ 0, 6144, 9830 },
{ 3211, 5734, 9830 },
{ 5734, 3211, 9830 },
{ 3068, 5478, 10368 },
{ 5478, 3068, 10368 },
{ 3297, 5888, 10368 },
{ 5888, 3297, 10368 },
{ 3441, 6144, 9830 },
{ 6144, 3441, 9830 },
{ 7168, 0, 7680 },
{ 7168, -4014, 7680 },
{ 4014, -7168, 7680 },
{ 0, -7168, 7680 },
{ 8192, 0, 5530 },
{ 8192, -4588, 5530 },
{ 4588, -8192, 5530 },
{ 0, -8192, 5530 },
{ 8192, 0, 3686 },
{ 8192, -4588, 3686 },
{ 4588, -8192, 3686 },
{ 0, -8192, 3686 },
{ -4014, -7168, 7680 },
{ -7168, -4014, 7680 },
{ -7168, 0, 7680 },
{ -4588, -8192, 5530 },
{ -8192, -4588, 5530 },
{ -8192, 0, 5530 },
{ -4588, -8192, 3686 },
{ -8192, -4588, 3686 },
{ -8192, 0, 3686 },
{ -7168, 4014, 7680 },
{ -4014, 7168, 7680 },
{ 0, 7168, 7680 },
{ -8192, 4588, 5530 },
{ -4588, 8192, 5530 },
{ 0, 8192, 5530 },
{ -8192, 4588, 3686 },
{ -4588, 8192, 3686 },
{ 0, 8192, 3686 },
{ 4014, 7168, 7680 },
{ 7168, 4014, 7680 },
{ 4588, 8192, 5530 },
{ 8192, 4588, 5530 },
{ 4588, 8192, 3686 },
{ 8192, 4588, 3686 },
{ 8192, 0, 1843 },
{ 8192, -4588, 1843 },
{ 4588, -8192, 1843 },
{ 0, -8192, 1843 },
{ 6144, 0, 922 },
{ 6144, -3441, 922 },
{ 3441, -6144, 922 },
{ 0, -6144, 922 },
{ 6144, 0, 614 },
{ 6144, -3441, 614 },
{ 3441, -6144, 614 },
{ 0, -6144, 614 },
{ -4588, -8192, 1843 },
{ -8192, -4588, 1843 },
{ -8192, 0, 1843 },
{ -3441, -6144, 922 },
{ -6144, -3441, 922 },
{ -6144, 0, 922 },
{ -3441, -6144, 614 },
{ -6144, -3441, 614 },
{ -6144, 0, 614 },
{ -8192, 4588, 1843 },
{ -4588, 8192, 1843 },
{ 0, 8192, 1843 },
{ -6144, 3441, 922 },
{ -3441, 6144, 922 },
{ 0, 6144, 922 },
{ -6144, 3441, 614 },
{ -3441, 6144, 614 },
{ 0, 6144, 614 },
{ 4588, 8192, 1843 },
{ 8192, 4588, 1843 },
{ 3441, 6144, 922 },
{ 6144, 3441, 922 },
{ 3441, 6144, 614 },
{ 6144, 3441, 614 },
{ -6554, 0, 8294 },
{ -6554, -1229, 8294 },
{ -6144, -1229, 9216 },
{ -6144, 0, 9216 },
{ -9421, 0, 8294 },
{ -9421, -1229, 8294 },
{ -10240, -1229, 9216 },
{ -10240, 0, 9216 },
{ -11059, 0, 8294 },
{ -11059, -1229, 8294 },
{ -12288, -1229, 9216 },
{ -12288, 0, 9216 },
{ -11059, 0, 7373 },
{ -11059, -1229, 7373 },
{ -12288, -1229, 7373 },
{ -12288, 0, 7373 },
{ -6144, 1229, 9216 },
{ -6554, 1229, 8294 },
{ -10240, 1229, 9216 },
{ -9421, 1229, 8294 },
{ -12288, 1229, 9216 },
{ -11059, 1229, 8294 },
{ -12288, 1229, 7373 },
{ -11059, 1229, 7373 },
{ -11059, 0, 6451 },
{ -11059, -1229, 6451 },
{ -12288, -1229, 5530 },
{ -12288, 0, 5530 },
{ -10240, 0, 4608 },
{ -10240, -1229, 4608 },
{ -10854, -1229, 3840 },
{ -10854, 0, 3840 },
{ -8192, -1229, 3686 },
{ -7782, -1229, 2458 },
{ -7782, 0, 2458 },
{ -12288, 1229, 5530 },
{ -11059, 1229, 6451 },
{ -10854, 1229, 3840 },
{ -10240, 1229, 4608 },
{ -7782, 1229, 2458 },
{ -8192, 1229, 3686 },
{ 6963, 0, 5837 },
{ 6963, -2703, 5837 },
{ 6963, -2703, 2458 },
{ 6963, 0, 2458 },
{ 10650, 0, 5837 },
{ 10650, -2703, 5837 },
{ 12698, -2703, 3379 },
{ 12698, 0, 3379 },
{ 9421, 0, 8602 },
{ 9421, -1024, 8602 },
{ 9830, -1024, 8294 },
{ 9830, 0, 8294 },
{ 11059, 0, 9830 },
{ 11059, -1024, 9830 },
{ 13517, -1024, 9830 },
{ 13517, 0, 9830 },
{ 6963, 2703, 2458 },
{ 6963, 2703, 5837 },
{ 12698, 2703, 3379 },
{ 10650, 2703, 5837 },
{ 9830, 1024, 8294 },
{ 9421, 1024, 8602 },
{ 13517, 1024, 9830 },
{ 11059, 1024, 9830 },
{ 11469, 0, 10138 },
{ 11469, -1024, 10138 },
{ 14438, -1024, 10214 },
{ 14438, 0, 10214 },
{ 11878, 0, 10138 },
{ 11878, -614, 10138 },
{ 14131, -614, 10291 },
{ 14131, 0, 10291 },
{ 11469, 0, 9830 },
{ 11469, -614, 9830 },
{ 13107, -614, 9830 },
{ 13107, 0, 9830 },
{ 14438, 1024, 10214 },
{ 11469, 1024, 10138 },
{ 14131, 614, 10291 },
{ 11878, 614, 10138 },
{ 13107, 614, 9830 },
{ 11469, 614, 9830 },
{ 0, 0, 12902 },
{ 3277, 0, 12902 },
{ 3277, -1843, 12902 },
{ 1843, -3277, 12902 },
{ 0, -3277, 12902 },
{ 0, 0, 11674 },
{ 819, 0, 11059 },
{ 819, -459, 11059 },
{ 459, -819, 11059 },
{ 0, -819, 11059 },
{ -1843, -3277, 12902 },
{ -3277, -1843, 12902 },
{ -3277, 0, 12902 },
{ -459, -819, 11059 },
{ -819, -459, 11059 },
{ -819, 0, 11059 },
{ -3277, 1843, 12902 },
{ -1843, 3277, 12902 },
{ 0, 3277, 12902 },
{ -819, 459, 11059 },
{ -459, 819, 11059 },
{ 0, 819, 11059 },
{ 1843, 3277, 12902 },
{ 3277, 1843, 12902 },
{ 459, 819, 11059 },
{ 819, 459, 11059 },
{ 1638, 0, 10445 },
{ 1638, -918, 10445 },
{ 918, -1638, 10445 },
{ 0, -1638, 10445 },
{ 5325, 0, 10445 },
{ 5325, -2982, 10445 },
{ 2982, -5325, 10445 },
{ 0, -5325, 10445 },
{ 5325, 0, 9830 },
{ 5325, -2982, 9830 },
{ 2982, -5325, 9830 },
{ 0, -5325, 9830 },
{ -918, -1638, 10445 },
{ -1638, -918, 10445 },
{ -1638, 0, 10445 },
{ -2982, -5325, 10445 },
{ -5325, -2982, 10445 },
{ -5325, 0, 10445 },
{ -2982, -5325, 9830 },
{ -5325, -2982, 9830 },
{ -5325, 0, 9830 },
{ -1638, 918, 10445 },
{ -918, 1638, 10445 },
{ 0, 1638, 10445 },
{ -5325, 2982, 10445 },
{ -2982, 5325, 10445 },
{ 0, 5325, 10445 },
{ -5325, 2982, 9830 },
{ -2982, 5325, 9830 },
{ 0, 5325, 9830 },
{ 918, 1638, 10445 },
{ 1638, 918, 10445 },
{ 2982, 5325, 10445 },
{ 5325, 2982, 10445 },
{ 2982, 5325, 9830 },
{ 5325, 2982, 9830 },
{ 0, 0, 0 },
{ 5837, 0, 0 },
{ 5837, 3269, 0 },
{ 3269, 5837, 0 },
{ 0, 5837, 0 },
{ 6144, 0, 307 },
{ 6144, 3441, 307 },
{ 3441, 6144, 307 },
{ 0, 6144, 307 },
{ -3269, 5837, 0 },
{ -5837, 3269, 0 },
{ -5837, 0, 0 },
{ -3441, 6144, 307 },
{ -6144, 3441, 307 },
{ -6144, 0, 307 },
{ -5837, -3269, 0 },
{ -3269, -5837, 0 },
{ 0, -5837, 0 },
{ -6144, -3441, 307 },
{ -3441, -6144, 307 },
{ 0, -6144, 307 },
{ 3269, -5837, 0 },
{ 5837, -3269, 0 },
{ 3441, -6144, 307 },
{ 6144, -3441, 307 },
};
// 32 patches x 16 control-point indices (row-major 4x4).
static constexpr uint16_t kPatchIndices[kPatchCount][16] = {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
{ 3, 16, 17, 18, 7, 19, 20, 21, 11, 22, 23, 24, 15, 25, 26, 27 },
{ 18, 28, 29, 30, 21, 31, 32, 33, 24, 34, 35, 36, 27, 37, 38, 39 },
{ 30, 40, 41, 0, 33, 42, 43, 4, 36, 44, 45, 8, 39, 46, 47, 12 },
{ 12, 13, 14, 15, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59 },
{ 15, 25, 26, 27, 51, 60, 61, 62, 55, 63, 64, 65, 59, 66, 67, 68 },
{ 27, 37, 38, 39, 62, 69, 70, 71, 65, 72, 73, 74, 68, 75, 76, 77 },
{ 39, 46, 47, 12, 71, 78, 79, 48, 74, 80, 81, 52, 77, 82, 83, 56 },
{ 56, 57, 58, 59, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95 },
{ 59, 66, 67, 68, 87, 96, 97, 98, 91, 99, 100, 101, 95, 102, 103, 104 },
{ 68, 75, 76, 77, 98, 105, 106, 107, 101, 108, 109, 110, 104, 111, 112, 113 },
{ 77, 82, 83, 56, 107, 114, 115, 84, 110, 116, 117, 88, 113, 118, 119, 92 },
{ 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135 },
{ 123, 136, 137, 120, 127, 138, 139, 124, 131, 140, 141, 128, 135, 142, 143, 132 },
{ 132, 133, 134, 135, 144, 145, 146, 147, 148, 149, 150, 151, 68, 152, 153, 154 },
{ 135, 142, 143, 132, 147, 155, 156, 144, 151, 157, 158, 148, 154, 159, 160, 68 },
{ 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176 },
{ 164, 177, 178, 161, 168, 179, 180, 165, 172, 181, 182, 169, 176, 183, 184, 173 },
{ 173, 174, 175, 176, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196 },
{ 176, 183, 184, 173, 188, 197, 198, 185, 192, 199, 200, 189, 196, 201, 202, 193 },
{ 203, 203, 203, 203, 204, 205, 206, 207, 208, 208, 208, 208, 209, 210, 211, 212 },
{ 203, 203, 203, 203, 207, 213, 214, 215, 208, 208, 208, 208, 212, 216, 217, 218 },
{ 203, 203, 203, 203, 215, 219, 220, 221, 208, 208, 208, 208, 218, 222, 223, 224 },
{ 203, 203, 203, 203, 221, 225, 226, 204, 208, 208, 208, 208, 224, 227, 228, 209 },
{ 209, 210, 211, 212, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240 },
{ 212, 216, 217, 218, 232, 241, 242, 243, 236, 244, 245, 246, 240, 247, 248, 249 },
{ 218, 222, 223, 224, 243, 250, 251, 252, 246, 253, 254, 255, 249, 256, 257, 258 },
{ 224, 227, 228, 209, 252, 259, 260, 229, 255, 261, 262, 233, 258, 263, 264, 237 },
{ 265, 265, 265, 265, 266, 267, 268, 269, 270, 271, 272, 273, 92, 119, 118, 113 },
{ 265, 265, 265, 265, 269, 274, 275, 276, 273, 277, 278, 279, 113, 112, 111, 104 },
{ 265, 265, 265, 265, 276, 280, 281, 282, 279, 283, 284, 285, 104, 103, 102, 95 },
{ 265, 265, 265, 265, 282, 286, 287, 266, 285, 288, 289, 270, 95, 94, 93, 92 },
};
} // namespace teapot
+478
View File
@@ -0,0 +1,478 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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.
*/
// The Utah teapot, tessellated from its 32 bicubic Bezier patches and lit
// entirely by the GTE's hardware lighting. Every existing psyqo example that
// lights anything does it on the CPU; this one uses the light-source matrix
// (LLM) and light-colour matrix (LCM) with the nccs command, which is what the
// GTE was actually built for. The teapot is plain grey; the three coloured
// directional lights are doing all of the visible work.
#include "psyqo/application.hh"
#include "psyqo/fixed-point.hh"
#include "psyqo/fragments.hh"
#include "psyqo/gpu.hh"
#include "psyqo/gte-kernels.hh"
#include "psyqo/gte-math.hh"
#include "psyqo/gte-registers.hh"
#include "psyqo/matrix.hh"
#include "psyqo/ordering-table.hh"
#include "psyqo/primitives/common.hh"
#include "psyqo/primitives/quads.hh"
#include "psyqo/scene.hh"
#include "psyqo/simplepad.hh"
#include "psyqo/font.hh"
#include "psyqo/soft-math.hh"
#include "psyqo/trigonometry.hh"
#include "psyqo/vector.hh"
#include "teapot-data.hh"
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
namespace {
// N subdivisions per patch edge. (N+1)^2 vertices and N^2 quads per patch.
// N=4 gives 800 vertices and 512 quads over the 32 patches: enough to round
// off the body without drowning the ordering table.
constexpr unsigned N = 4;
constexpr unsigned kVertsPerPatch = (N + 1) * (N + 1);
constexpr unsigned kQuadsPerPatch = N * N;
constexpr unsigned kVertexCount = teapot::kPatchCount * kVertsPerPatch; // 800
constexpr unsigned kQuadCount = teapot::kPatchCount * kQuadsPerPatch; // 512
constexpr unsigned ORDERING_TABLE_SIZE = 1024;
// A quad is four vertex indices into the baked vertex/normal arrays.
struct Quad {
uint16_t v[4];
};
class Teapot final : public psyqo::Application {
void prepare() override;
void createScene() override;
public:
psyqo::Trig<> m_trig;
psyqo::Font<> m_font;
psyqo::SimplePad m_input;
};
class TeapotScene final : public psyqo::Scene {
void start(StartReason reason) override;
void frame() override;
void tessellate();
psyqo::Angle m_rot = 0;
// Triangle, Circle and Cross switch one light each. Watching a light go
// out is the only thing that distinguishes three lights from three vertex
// colours; a still frame of the lit pot cannot.
bool m_lightOn[3] = {true, true, true};
// Where along the camera path we are, ping-ponging 0 -> 1 -> 0. The path
// itself is evaluated on the GTE every frame, which is the realtime case
// the accelerated Bezier exists for - a one-shot tessellation would not
// justify it.
psyqo::FixedPoint<> m_pathT = 0.0_fp;
bool m_pathForward = true;
// Baked at boot from the Bezier patches: object-space positions, unit
// normals, and the quad connectivity.
psyqo::GTE::PackedVec3 m_vertices[kVertexCount];
psyqo::GTE::PackedVec3 m_normals[kVertexCount];
Quad m_quads[kQuadCount];
// Per-frame scratch, recomputed every frame.
psyqo::Vertex m_projected[kVertexCount];
uint32_t m_sz[kVertexCount];
// Finished GP0 first-words: the primitive's command byte fused with the lit
// colour by the GTE. Color already models this - its `user` byte IS the top
// byte the GP0 command lives in - so there is no reason to drop to a raw
// word here.
psyqo::Color m_shade[kVertexCount];
psyqo::OrderingTable<ORDERING_TABLE_SIZE> m_ots[2];
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
eastl::array<psyqo::Fragments::SimpleFragment<psyqo::Prim::GouraudQuad>, kQuadCount> m_frags[2];
static constexpr psyqo::Color c_bg = {{.r = 20, .g = 22, .b = 28}};
// A cubic Bezier the camera drifts along, in the same raw units the GTE
// reads the packed vectors in. Evaluated once per frame with gteCubic.
static constexpr psyqo::Vec3 c_path[4] = {
{-1.1_fp, -0.35_fp, -0.55_fp},
{-0.4_fp, 0.5_fp, 0.9_fp},
{0.4_fp, -0.5_fp, 0.9_fp},
{1.1_fp, 0.35_fp, -0.55_fp},
};
// Three directional lights, in world space, each a pure primary. The
// teapot spins under them, so the coloured highlights sweep across it.
// Rows are light directions, unit length, in world space. Screen Z runs
// INTO the display, so a light that reaches a camera-facing surface has a
// negative z: front normals point back out at the viewer.
static constexpr psyqo::Matrix33 c_lightDirs = {{
{0.501_fp, -0.602_fp, -0.622_fp}, // key, upper right front
{-0.792_fp, -0.226_fp, -0.566_fp}, // fill, from the left
{0.123_fp, 0.862_fp, -0.493_fp}, // rim, from below and behind
}};
// LCM: rows are channels, COLUMNS are lights. Column i is light i's colour.
// A warm near-white key carries the shading; the two fills are weak and
// saturated so they tint the shadow side without turning it into a colour
// wheel. Three equal primaries here make the surface colour a direct
// readout of which light reaches it, which reads as Gouraud, not lighting.
// key fill rim
static constexpr psyqo::Matrix33 c_lightColors = {{
{1.0_fp, 0.10_fp, 0.45_fp}, // R
{0.94_fp, 0.22_fp, 0.14_fp}, // G
{0.82_fp, 0.55_fp, 0.22_fp}, // B
}};
};
Teapot g_teapot;
TeapotScene teapotScene;
psyqo::Vec3 controlPoint(unsigned patch, unsigned i) {
const auto* p = teapot::kControlPoints[teapot::kPatchIndices[patch][i]];
return {psyqo::FixedPoint<>(p[0], psyqo::FixedPoint<>::RAW), psyqo::FixedPoint<>(p[1], psyqo::FixedPoint<>::RAW),
psyqo::FixedPoint<>(p[2], psyqo::FixedPoint<>::RAW)};
}
} // namespace
void TeapotScene::tessellate() {
// The lid tip and the body bottom are collapsed patch rows where one
// tangent vanishes, so nudge the parameters off the exact 0 and 1 edges to
// keep every normal well defined.
constexpr psyqo::FixedPoint<> eps = 0.002_fp;
constexpr psyqo::FixedPoint<> span = 1.0_fp - 0.004_fp;
unsigned vbase = 0;
for (unsigned patch = 0; patch < teapot::kPatchCount; patch++) {
psyqo::Vec3 cp[16];
for (unsigned i = 0; i < 16; i++) cp[i] = controlPoint(patch, i);
for (unsigned iu = 0; iu <= N; iu++) {
psyqo::FixedPoint<> u = eps + span * psyqo::FixedPoint<>(int32_t(iu), 0) / int32_t(N);
for (unsigned iv = 0; iv <= N; iv++) {
psyqo::FixedPoint<> v = eps + span * psyqo::FixedPoint<>(int32_t(iv), 0) / int32_t(N);
// Evaluate the surface point: four row curves at v, then across
// at u. Same nesting for the u-tangent; the v-tangent takes the
// row derivatives across at u.
psyqo::Vec3 row[4], drow[4];
for (unsigned r = 0; r < 4; r++) {
row[r] = psyqo::GteMath::cubic(cp[r * 4 + 0], cp[r * 4 + 1], cp[r * 4 + 2], cp[r * 4 + 3], v);
drow[r] = psyqo::GteMath::cubicDerivative(cp[r * 4 + 0], cp[r * 4 + 1], cp[r * 4 + 2], cp[r * 4 + 3], v);
}
psyqo::Vec3 point = psyqo::GteMath::cubic(row[0], row[1], row[2], row[3], u);
psyqo::Vec3 du = psyqo::GteMath::cubicDerivative(row[0], row[1], row[2], row[3], u);
psyqo::Vec3 dv = psyqo::GteMath::cubic(drow[0], drow[1], drow[2], drow[3], u);
// Outward normal: cross(dv, du), not the other way round. Checked
// against the closed surface rather than by eye - summing
// (centroid . n) * area over the whole mesh gives +75.7 for this
// order and -75.7 for the reverse, so this is the one pointing
// out. Getting it backwards is survivable on the convex body,
// because flipping the light sign hides it there, and then the
// lid is where the two errors stop cancelling.
psyqo::Vec3 normal;
psyqo::GteMath::crossProductVec3(dv, du, &normal);
// At a collapsed patch corner (the lid tip, the body underside) a
// tangent can round to zero in fixed point even after the eps
// nudge, and normalizing that divides by zero. Fall back to the
// radial direction, which is a decent outward guess there.
psyqo::FixedPoint<> lensq = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
if (lensq < 0.01_fp) {
normal = {point.x, point.y, 0.0_fp};
psyqo::FixedPoint<> rsq = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
if (rsq < 0.01_fp) normal = {0.0_fp, 0.0_fp, 1.0_fp};
}
psyqo::GteMath::normalizeVec3(&normal);
// The dataset sits on the z=0 ground plane with the spout out
// along +x, so recentre it on the origin before packing.
point.x -= 0.26_fp;
point.z -= 1.575_fp;
unsigned idx = vbase + iu * (N + 1) + iv;
m_vertices[idx] = psyqo::GTE::PackedVec3(point);
m_normals[idx] = psyqo::GTE::PackedVec3(normal);
}
}
vbase += kVertsPerPatch;
}
unsigned q = 0;
for (unsigned patch = 0; patch < teapot::kPatchCount; patch++) {
unsigned base = patch * kVertsPerPatch;
for (unsigned iu = 0; iu < N; iu++) {
for (unsigned iv = 0; iv < N; iv++) {
unsigned a = base + iu * (N + 1) + iv;
// PS1 quads are two triangles ABC and BCD, so the four points
// go in a Z, not around the ring. A ring order draws a bowtie.
m_quads[q].v[0] = a;
m_quads[q].v[1] = a + 1;
m_quads[q].v[2] = a + (N + 1);
m_quads[q].v[3] = a + (N + 1) + 1;
q++;
}
}
}
}
void Teapot::prepare() {
psyqo::GPU::Configuration config;
config.set(psyqo::GPU::Resolution::W320)
.set(psyqo::GPU::VideoMode::AUTO)
.set(psyqo::GPU::ColorMode::C15BITS)
.set(psyqo::GPU::Interlace::PROGRESSIVE);
gpu().initialize(config);
}
void Teapot::createScene() {
m_font.uploadSystemFont(gpu());
m_input.initialize();
pushScene(&teapotScene);
}
void TeapotScene::start(StartReason reason) {
tessellate();
g_teapot.m_input.setOnEvent([this](const psyqo::SimplePad::Event& event) {
if (event.type != psyqo::SimplePad::Event::ButtonReleased) return;
switch (event.button) {
case psyqo::SimplePad::Button::Triangle: m_lightOn[0] = !m_lightOn[0]; break;
case psyqo::SimplePad::Button::Circle: m_lightOn[1] = !m_lightOn[1]; break;
case psyqo::SimplePad::Button::Cross: m_lightOn[2] = !m_lightOn[2]; break;
default: break;
}
});
// Projection context, set once. The teapot lives around the origin and we
// push it back on Z each frame.
psyqo::GTE::clear<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>();
psyqo::GTE::clear<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>();
psyqo::GTE::write<psyqo::GTE::Register::OFX, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(160.0).raw());
psyqo::GTE::write<psyqo::GTE::Register::OFY, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(120.0).raw());
psyqo::GTE::write<psyqo::GTE::Register::H, psyqo::GTE::Unsafe>(300);
// The teapot is ~3.5 units, which is 14438 in the 4.12 packed vectors the GTE
// reads as plain integers, so it has to sit tens of thousands of units away or
// the near half of it lands behind the eye and the divide saturates.
psyqo::GTE::write<psyqo::GTE::Register::ZSF3, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 3);
psyqo::GTE::write<psyqo::GTE::Register::ZSF4, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 4);
// Lighting context that does not change: the light colours (LCM), a low
// ambient (BK), and the grey material colour that nccs multiplies the lit
// result by.
psyqo::GTE::write<psyqo::GTE::Register::RBK, psyqo::GTE::Unsafe>(0x180);
psyqo::GTE::write<psyqo::GTE::Register::GBK, psyqo::GTE::Unsafe>(0x180);
psyqo::GTE::write<psyqo::GTE::Register::BBK, psyqo::GTE::Unsafe>(0x180);
// RGBC is written per frame in frame(), because its top byte carries the
// primitive command the GTE fuses into each colour. On an UNTEXTURED
// primitive full brightness is 0xff; 0x80 is the neutral point for texture
// blending and using it here would halve the whole image.
}
void TeapotScene::frame() {
int parity = gpu().getParity();
auto& ot = m_ots[parity];
auto& clear = m_clear[parity];
auto& frags = m_frags[parity];
gpu().getNextClear(clear.primitive, c_bg);
gpu().chain(clear);
// Spin about two axes.
// The Newell data is Z-up and the screen is Y-down, and psyqo's X rotation
// sends +Z to +Y, so standing the pot up wants a NEGATIVE quarter turn.
// then spin it, then tilt the whole thing towards the camera.
auto standUp = psyqo::SoftMath::generateRotationMatrix33(-0.5_pi, psyqo::SoftMath::Axis::X, g_teapot.m_trig);
auto spin = psyqo::SoftMath::generateRotationMatrix33(m_rot, psyqo::SoftMath::Axis::Y, g_teapot.m_trig);
auto tilt = psyqo::SoftMath::generateRotationMatrix33(-0.12_pi, psyqo::SoftMath::Axis::X, g_teapot.m_trig);
// multiplyMatrix33(A, B) composes so that A's rotation happens FIRST, so the
// pot is stood up, then spun about the (now vertical) screen Y, then the
// whole thing is tipped towards the camera.
psyqo::Matrix33 transform;
psyqo::GteMath::multiplyMatrix33(standUp, spin, &transform);
psyqo::GteMath::multiplyMatrix33(transform, tilt, &transform);
// Camera drift along the Bezier path. This runs BEFORE the projection
// context is loaded, because gteCubic uses RT and TR as scratch - the same
// ordering constraint GteMath::multiplyMatrix33 has. Both are documented in
// gte-math.hh: the clobber list is the real API surface of these.
psyqo::Vec3 cam = psyqo::GteMath::cubic(c_path[0], c_path[1], c_path[2], c_path[3], m_pathT);
if (m_pathForward) {
m_pathT += 0.0025_fp;
if (m_pathT >= 1.0_fp) { m_pathT = 1.0_fp; m_pathForward = false; }
} else {
m_pathT -= 0.0025_fp;
if (m_pathT <= 0.0_fp) { m_pathT = 0.0_fp; m_pathForward = true; }
}
// ---- Pass 1: project every vertex. ----
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Rotation>(transform);
psyqo::GTE::write<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>((cam.x * 2600).integer<1>());
psyqo::GTE::write<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>((cam.y * 2600).integer<1>());
psyqo::GTE::write<psyqo::GTE::Register::TRZ, psyqo::GTE::Unsafe>(32000 + (cam.z * 5200).integer<1>());
unsigned i = 0;
for (; i + 3 <= kVertexCount; i += 3) {
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V0>(m_vertices[i + 0]);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V1>(m_vertices[i + 1]);
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(m_vertices[i + 2]);
psyqo::GTE::Kernels::rtpt();
psyqo::GTE::read<psyqo::GTE::Register::SXY0>(&m_projected[i + 0].packed);
psyqo::GTE::read<psyqo::GTE::Register::SXY1>(&m_projected[i + 1].packed);
psyqo::GTE::read<psyqo::GTE::Register::SXY2>(&m_projected[i + 2].packed);
psyqo::GTE::read<psyqo::GTE::Register::SZ1>(reinterpret_cast<uint32_t*>(&m_sz[i + 0]));
psyqo::GTE::read<psyqo::GTE::Register::SZ2>(reinterpret_cast<uint32_t*>(&m_sz[i + 1]));
psyqo::GTE::read<psyqo::GTE::Register::SZ3>(reinterpret_cast<uint32_t*>(&m_sz[i + 2]));
}
for (; i < kVertexCount; i++) {
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V0>(m_vertices[i]);
psyqo::GTE::Kernels::rtps();
psyqo::GTE::read<psyqo::GTE::Register::SXY2>(&m_projected[i].packed);
psyqo::GTE::read<psyqo::GTE::Register::SZ3>(reinterpret_cast<uint32_t*>(&m_sz[i]));
}
// ---- Pass 2: light every vertex. ----
// The light directions live in world space, so rotate them into object
// space by folding the model rotation into the light matrix: with LLM =
// worldLights * R, the row-dot-normal that nccs computes equals
// worldLight . (R * normal), i.e. the lit band stays fixed as the pot spins.
psyqo::Matrix33 llm;
psyqo::GteMath::multiplyMatrix33(transform, c_lightDirs, &llm);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Light>(llm);
// Note the argument order: multiplyMatrix33(m1, m2) computes m2 * m1, and
// what we want is (lights * rotation) so that each LLM row dotted with an
// object-space normal equals that world light dotted with the rotated
// normal. The other order silently rotates the lights by the wrong frame.
// Switching a light off means zeroing its COLUMN of the colour matrix,
// which is that light's colour. Zeroing a row would kill one channel of
// all three lights instead, and that difference is the whole point of the
// two-matrix split.
psyqo::Matrix33 lcm = c_lightColors;
for (unsigned row = 0; row < 3; row++) {
if (!m_lightOn[0]) lcm.vs[row].x = 0.0_fp;
if (!m_lightOn[1]) lcm.vs[row].y = 0.0_fp;
if (!m_lightOn[2]) lcm.vs[row].z = 0.0_fp;
}
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::Color>(lcm);
// RGBC's top byte is CODE, and the GTE fuses it into every colour it
// produces. A GP0 polygon's first word is the command in bits 31-24 and
// vertex A's colour in 23-0, so loading CODE with the primitive's own
// command byte means the colour FIFO hands back finished command words: no
// masking, and the assembly below is a word store rather than a
// read-modify-write. The material stays 0xff - full brightness on an
// untextured primitive.
psyqo::GTE::write<psyqo::GTE::Register::RGB, psyqo::GTE::Unsafe>(
m_frags[parity][0].primitive.getCommandWord() | 0x00ffffff);
// Three normals at a time, exactly like rtpt above. NCCT is 39 cycles
// against 51 for three NCCS, and it costs one kernel issue instead of
// three.
for (i = 0; i + 3 <= kVertexCount; i += 3) {
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V0>(m_normals[i + 0]);
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::V1>(m_normals[i + 1]);
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(m_normals[i + 2]);
psyqo::GTE::Kernels::ncct();
m_shade[i + 0].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB0>();
m_shade[i + 1].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB1>();
m_shade[i + 2].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB2>();
}
for (; i < kVertexCount; i++) {
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V0>(m_normals[i]);
psyqo::GTE::Kernels::nccs();
m_shade[i].packed = psyqo::GTE::readRaw<psyqo::GTE::Register::RGB2>();
}
// ---- Pass 3: cull, depth-sort, assemble. ----
unsigned drawn = 0;
for (unsigned qi = 0; qi < kQuadCount; qi++) {
const auto& quad = m_quads[qi];
const auto& pa = m_projected[quad.v[0]];
const auto& pb = m_projected[quad.v[1]];
const auto& pc = m_projected[quad.v[2]];
const auto& pd = m_projected[quad.v[3]];
// Backface cull off the projected screen coordinates.
psyqo::GTE::write<psyqo::GTE::Register::SXY0, psyqo::GTE::Unsafe>(pa.packed);
psyqo::GTE::write<psyqo::GTE::Register::SXY1, psyqo::GTE::Unsafe>(pb.packed);
psyqo::GTE::write<psyqo::GTE::Register::SXY2, psyqo::GTE::Safe>(pc.packed);
psyqo::GTE::Kernels::nclip();
int32_t mac0 = 0;
psyqo::GTE::read<psyqo::GTE::Register::MAC0>(reinterpret_cast<uint32_t*>(&mac0));
// Quad points go in a Z, so the screen winding of A,B,C is the opposite
// of the ring order cube.cpp uses; keep the faces whose cross product
// comes out positive.
if (mac0 >= 0) continue;
int32_t z = int32_t((m_sz[quad.v[0]] + m_sz[quad.v[1]] + m_sz[quad.v[2]] + m_sz[quad.v[3]]) >> 2);
z = (z * ORDERING_TABLE_SIZE) >> 16;
if (z < 0 || z >= ORDERING_TABLE_SIZE) continue;
auto& frag = frags[drawn];
auto& prim = frag.primitive;
prim.setPointA(pa);
prim.setPointB(pb);
prim.setPointC(pc);
prim.setPointD(pd);
// Each of these is already a finished word: the command byte fused with
// the colour by the GTE. Vertex A's lands in the command slot, which is
// where a GP0 polygon keeps it. The other three are colour-only words
// whose top byte the GPU ignores.
prim.setColorAPacked(m_shade[quad.v[0]].packed);
prim.colorB = m_shade[quad.v[1]];
prim.colorC = m_shade[quad.v[2]];
prim.colorD = m_shade[quad.v[3]];
ot.insert(frag, z);
drawn++;
}
gpu().chain(ot);
static constexpr psyqo::Color c_on = {{.r = 0xff, .g = 0xff, .b = 0xff}};
static constexpr psyqo::Color c_off = {{.r = 0x50, .g = 0x50, .b = 0x58}};
// The system font's glyphs are 8x16, so the lines step by 16 plus a little
// leading rather than by an eyeballed 12.
static constexpr int16_t c_lineHeight = 18;
static constexpr int16_t c_textTop = 6;
g_teapot.m_font.chainprintf(gpu(), {{.x = 6, .y = c_textTop}}, m_lightOn[0] ? c_on : c_off, "[Tri] key");
g_teapot.m_font.chainprintf(gpu(), {{.x = 6, .y = int16_t(c_textTop + c_lineHeight)}},
m_lightOn[1] ? c_on : c_off, "[Cir] fill");
g_teapot.m_font.chainprintf(gpu(), {{.x = 6, .y = int16_t(c_textTop + 2 * c_lineHeight)}},
m_lightOn[2] ? c_on : c_off, "[Cro] rim");
m_rot += 0.004_pi;
}
int main() { return g_teapot.run(); }
-9
View File
@@ -47,15 +47,6 @@ SOFTWARE.
namespace psyqo {
namespace DMA {
enum DmaCallback {
FROM_ISR,
FROM_MAIN_LOOP,
};
}
namespace timer_literals {
/**
+320
View File
@@ -0,0 +1,320 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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 "psyqo/bezier.hh"
#include "psyqo/fixed-point.hh"
#include "psyqo/gte-kernels.hh"
#include "psyqo/gte-registers.hh"
#include "psyqo/matrix.hh"
#include "psyqo/vector.hh"
/*
* GteMath - the SoftMath operations that the GTE can do in hardware.
*
* These mirror SoftMath's signatures and argument conventions exactly, so they
* are drop-in. Where SoftMath has a quirk, this keeps the quirk rather than
* fixing it, because a caller swapping one for the other should not have to
* re-derive anything. multiplyMatrix33 in particular computes out = m2 * m1,
* matching SoftMath.
*
* WHY THESE ARE FREE FUNCTIONS AND NOT A SCOPED, REGISTER-OWNING OBJECT.
* Measured on hardware (src/mips/tests/gte-math-bench), cycles per element,
* SCPH-5501, caller at -O2, "cold" meaning the matrix upload is paid INSIDE the
* call, once per element:
*
* operation soft cold hot cold speedup
* matrixVecMul3 271.66 126.58 47.63 2.15x
* matrixVecMul3xy 170.70 123.60 45.71 1.38x
* crossProductVec3 187.76 73.32 46.56 2.56x
* project 709.41 140.03 52.58 5.07x
* multiplyMatrix33 1144.06 209.38 127.63 5.46x
*
* There is no crossover: the GTE wins at a batch of one for everything
* measured, so the simple form never loses and needs no caveats. Hoisting the
* setup is still worth about 2.7x on top, which is what the batching helpers
* further down are for - but that is an optimisation, not the architecture.
*
* The reason the software side is so expensive is not instruction count. On the
* R3000A `mult` and `divu` are multi-cycle and `mflo` interlocks, so every
* FixedPoint multiply stalls. Three independent operations above work out at
* 28-31 cycles per fixed-point multiply. `project` is dominated by a single
* `divu`: 681 of its 709 cycles are the one divide.
*
* INLINING. psyqo builds at -Os, where GCC does NOT inline the GTE register
* accessors - it emits `jal` into writeSafe/read, and that measured up to +67%
* on the cold numbers above. Everything here is therefore in the header and
* marked always_inline. Do not move these to a .cpp built at -Os.
*
* REGISTER CLOBBERS. Each function documents what it destroys. This is the real
* API surface: a caller who has a rotation matrix loaded for projection cannot
* casually call crossProductVec3 in the middle of a render loop, because the
* GTE's cross product uses the rotation matrix DIAGONAL as one of its operands.
* That one is a landmine, so it is spelled out on every function rather than
* once at the top.
*
* RESTORING. Prefer re-uploading from the copy you still have in RAM over
* reading registers back. A Matrix33 is five ctc2; reading it back is nine
* mfc2 plus hazard nops. "Save and restore" is the expensive direction here.
*/
namespace psyqo {
namespace GteMath {
#define PSYQO_GTE_MATH_INLINE [[gnu::always_inline]] static inline
/**
* @brief Multiplies a vector by a matrix.
*
* @details Drop-in for SoftMath::matrixVecMul3. 2.15x on hardware even paying
* the matrix upload here.
*
* CLOBBERS: the rotation matrix (RT), V0, IR1-3, MAC1-3.
*/
PSYQO_GTE_MATH_INLINE void matrixVecMul3(const Matrix33 &m, const Vec3 &v, Vec3 *out) {
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(m);
GTE::writeSafe<GTE::PseudoRegister::V0>(v);
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0>();
*out = Vec3(GTE::readSafe<GTE::PseudoRegister::SV>());
}
/**
* @brief Multiplies a vector by a matrix, keeping only x and y.
*
* CLOBBERS: the rotation matrix (RT), V0, IR1-3, MAC1-3.
*/
PSYQO_GTE_MATH_INLINE void matrixVecMul3xy(const Matrix33 &m, const Vec3 &v, Vec2 *out) {
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(m);
GTE::writeSafe<GTE::PseudoRegister::V0>(v);
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0>();
Vec3 r = Vec3(GTE::readSafe<GTE::PseudoRegister::SV>());
out->x = r.x;
out->y = r.y;
}
/**
* @brief Multiplies a vector by a matrix, keeping only z.
*
* CLOBBERS: the rotation matrix (RT), V0, IR1-3, MAC1-3.
*/
PSYQO_GTE_MATH_INLINE FixedPoint<> matrixVecMul3z(const Matrix33 &m, const Vec3 &v) {
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(m);
GTE::writeSafe<GTE::PseudoRegister::V0>(v);
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0>();
return Vec3(GTE::readSafe<GTE::PseudoRegister::SV>()).z;
}
/**
* @brief Builds a matrix from three column vectors.
*
* @details Matrix33 stores ROWS. Several GTE formulations want the matrix built
* from columns - a weighted sum of three vectors is one MVMVA only if those
* vectors are the columns - so this scatters each one across the three rows.
* Getting it backwards produces plausible-looking garbage, which is why it is a
* named function rather than an open-coded initialiser at each site.
*/
PSYQO_GTE_MATH_INLINE Matrix33 fromColumns(const Vec3 &c0, const Vec3 &c1, const Vec3 &c2) {
return {{
{c0.x, c1.x, c2.x},
{c0.y, c1.y, c2.y},
{c0.z, c1.z, c2.z},
}};
}
/**
* @brief Multiplies two matrices.
*
* @details out = m2 * m1, matching SoftMath::multiplyMatrix33's convention.
* Column j of the product is m2 applied to column j of m1, so this is three
* MVMVA against 27 fixed-point multiplies: 5.46x on hardware.
*
* Aliasing is fine: the result is accumulated before anything is written back,
* so `out` may be `&m1` or `&m2`.
*
* CLOBBERS: the rotation matrix (RT), V0, IR1-3, MAC1-3.
*/
PSYQO_GTE_MATH_INLINE void multiplyMatrix33(const Matrix33 &m1, const Matrix33 &m2, Matrix33 *out) {
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(m2);
const Vec3 cols[3] = {
{m1.vs[0].x, m1.vs[1].x, m1.vs[2].x},
{m1.vs[0].y, m1.vs[1].y, m1.vs[2].y},
{m1.vs[0].z, m1.vs[1].z, m1.vs[2].z},
};
Vec3 res[3];
for (unsigned j = 0; j < 3; j++) {
GTE::writeSafe<GTE::PseudoRegister::V0>(cols[j]);
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0>();
res[j] = Vec3(GTE::readSafe<GTE::PseudoRegister::SV>());
}
out->vs[0] = {res[0].x, res[1].x, res[2].x};
out->vs[1] = {res[0].y, res[1].y, res[2].y};
out->vs[2] = {res[0].z, res[1].z, res[2].z};
}
/**
* @brief Cross product of two vectors.
*
* @details 2.56x on hardware, but read the clobber list before reaching for it.
*
* CLOBBERS: R11, R22 and R33 - the ROTATION MATRIX DIAGONAL - plus IR1-3 and
* MAC1-3. The GTE's cross product takes its first operand from those three
* matrix elements rather than from a vector register, so calling this destroys
* a loaded rotation matrix. That makes it unsafe inside a projection loop
* unless you reload RT afterwards. It is free at load time, when nothing owns
* RT yet.
*/
PSYQO_GTE_MATH_INLINE void crossProductVec3(const Vec3 &v1, const Vec3 &v2, Vec3 *out) {
Vec3 a = v1;
Vec3 b = v2;
GTE::write<GTE::Register::R11R12, GTE::Unsafe>(a.x.raw());
GTE::write<GTE::Register::R22R23, GTE::Unsafe>(a.y.raw());
GTE::write<GTE::Register::R33, GTE::Unsafe>(a.z.raw());
GTE::write<GTE::Register::IR1, GTE::Unsafe>(reinterpret_cast<uint32_t *>(&b.x));
GTE::write<GTE::Register::IR2, GTE::Unsafe>(reinterpret_cast<uint32_t *>(&b.y));
GTE::write<GTE::Register::IR3, GTE::Safe>(reinterpret_cast<uint32_t *>(&b.z));
GTE::Kernels::cp();
GTE::read<GTE::PseudoRegister::LV>(*out);
}
/**
* @brief The seed for an inverse square root, from the GTE's leading-bit count.
*
* @details The seed wants HALF the exponent, since the target is 1/sqrt(x) and
* not 1/x. With lzcr = 31 - floor(log2(x.raw())), that is (5 + lzcr) / 2 in
* 20.12. Shifting by the whole count is a seed for the wrong function: it is
* correct only where x == 0.0625 and diverges either side, and because
* SoftMath::inverseSquareRoot is four Newton steps with no convergence check, a
* seed outside the basin explodes rather than degrading. Swept over all 4096
* representable values below 1.0, the halved exponent has a worst relative
* error of 0.48% and no failures; the unhalved one breaks 199 of them.
*
* CLOBBERS: LZCS and LZCR only. Safe to call with a matrix loaded.
*
* NOTE: LZCS/LZCR do not interlock the way the cop2 commands do - they are the
* one corner of the GTE where the hardware will not stall for you - so the
* write must be Safe.
*/
PSYQO_GTE_MATH_INLINE FixedPoint<> inverseSquareRootSeed(FixedPoint<> x) {
GTE::write<GTE::Register::LZCS, GTE::Safe>(x.raw());
int32_t shift = (5 + int32_t(GTE::readRaw<GTE::Register::LZCR>())) / 2;
return FixedPoint<>(int32_t(1) << shift, FixedPoint<>::RAW);
}
/**
* @brief Normalises a vector in place.
*
* @details Drop-in for SoftMath::normalizeVec3, which reaches 1/sqrt through
* squareRoot's shift-subtract loop at about 3588 cycles a vector. This keeps
* the squared length on the CPU - three multiplies - and takes only the seed
* from the GTE, then uses the existing Newton refinement. Measured against the
* exact version over 800 vectors, worst component deviation is 10 raw out of
* 4096, i.e. 0.24%.
*
* A zero-length vector is left as (0, 0, 1) rather than dividing by zero:
* SoftMath::squareRoot returns 0 for x.raw() <= 1 and normalizeVec3 then
* divides by it.
*
* CLOBBERS: LZCS and LZCR only.
*/
void normalizeVec3(Vec3 *v);
/**
* @brief Normalises a vector in place, without the exact square root.
*
* @details Same as normalizeVec3 here. SoftMath draws a distinction between an
* exact normalize and a fast one; on this path there is no reason for two, and
* the "fast" one in SoftMath seeds inverseSquareRoot with x * 2, which moves
* the wrong way as the vector shrinks.
*
* CLOBBERS: LZCS and LZCR only.
*/
PSYQO_GTE_MATH_INLINE void fastNormalizeVec3(Vec3 *v) { normalizeVec3(v); }
/**
* @brief Evaluates a cubic Bezier at t.
*
* @details A cubic Bezier is a weighted sum of four control points, and MVMVA
* computes Mx * Vx + Tx. Load three control points as the matrix COLUMNS and
* the Bernstein weights as the vector, and the fourth term rides in the
* translation register. One GTE op plus three multiplies, against twelve
* multiplies in software.
*
* This is the operation that justifies the header on its own: a curve sampled
* once per frame is how you get smooth camera motion along a 3D path, and that
* is a realtime cost rather than a load-time one.
*
* Verified against psyqo::Bezier::cubic: worst component deviation 3 raw out of
* 4096.
*
* CLOBBERS: the rotation matrix (RT), the translation vector (TR), V0, IR1-3,
* MAC1-3.
*/
PSYQO_GTE_MATH_INLINE Vec3 cubic(const Vec3 &a, const Vec3 &b, const Vec3 &c, const Vec3 &d, FixedPoint<> t) {
using namespace psyqo::fixed_point_literals;
FixedPoint<> mt = 1.0_fp - t;
FixedPoint<> mt2 = mt * mt;
FixedPoint<> t2 = t * t;
FixedPoint<> f4 = t2 * t;
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(fromColumns(a, b, c));
GTE::writeUnsafe<GTE::PseudoRegister::Translation>(Vec3{d.x * f4, d.y * f4, d.z * f4});
GTE::writeSafe<GTE::PseudoRegister::V0>(Vec3{mt2 * mt, mt2 * t * 3, mt * t2 * 3});
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0, GTE::Kernels::TV::TR>();
return Vec3(GTE::readSafe<GTE::PseudoRegister::SV>());
}
/**
* @brief Evaluates the derivative of a cubic Bezier at t.
*
* @details The derivative is a quadratic Bezier over the differences of
* consecutive control points, so it is only THREE terms and needs no
* translation vector at all - one MVMVA and nothing else.
*
* The factor of three that belongs in a true derivative is dropped. The usual
* consumer is a tangent that gets crossed and normalised, where only the
* direction survives; scale it yourself if you need the real magnitude.
*
* CLOBBERS: the rotation matrix (RT), V0, IR1-3, MAC1-3. Not TR.
*/
PSYQO_GTE_MATH_INLINE Vec3 cubicDerivative(const Vec3 &a, const Vec3 &b, const Vec3 &c, const Vec3 &d,
FixedPoint<> t) {
using namespace psyqo::fixed_point_literals;
FixedPoint<> mt = 1.0_fp - t;
Vec3 ab = {b.x - a.x, b.y - a.y, b.z - a.z};
Vec3 bc = {c.x - b.x, c.y - b.y, c.z - b.z};
Vec3 cd = {d.x - c.x, d.y - c.y, d.z - c.z};
GTE::writeUnsafe<GTE::PseudoRegister::Rotation>(fromColumns(ab, bc, cd));
GTE::writeSafe<GTE::PseudoRegister::V0>(Vec3{mt * mt, mt * t * 2, t * t});
GTE::Kernels::mvmva<GTE::Kernels::MX::RT, GTE::Kernels::MV::V0>();
return Vec3(GTE::readSafe<GTE::PseudoRegister::SV>());
}
#undef PSYQO_GTE_MATH_INLINE
} // namespace GteMath
} // namespace psyqo
+22
View File
@@ -33,6 +33,15 @@ SOFTWARE.
namespace psyqo {
namespace DMA {
enum DmaCallback {
FROM_ISR,
FROM_MAIN_LOOP,
};
}
class Application;
/**
@@ -97,6 +106,19 @@ static inline void fastLeaveCriticalSection() {
#endif
}
/**
* @brief Waits for a specific status to be set in a register.
*
* @tparam T The type of the register to wait for.
* @param mask The mask to apply to the register.
* @param expected The expected value of the register after applying the mask.
* @param value The address of the register to wait for.
*/
template <typename T>
static void waitForStatus(T mask, T expected, const volatile T* value) {
while ((*value & mask) != expected);
}
enum class DMA : unsigned {
MDECin,
MDECout,
+19
View File
@@ -92,6 +92,25 @@ struct GouraudLine {
command &= ~0x02000000;
return *this;
}
/**
* @brief The GP0 command word, minus any colour.
*
* @details Meant for the GTE's RGBC CODE field, which gets fused into every
* colour the GTE emits. Preload it and the colour FIFO hands back finished
* first words. See `GouraudQuad::getCommandWord` for the full round trip.
*/
uint32_t getCommandWord() const { return command & 0xff000000; }
/**
* @brief Sets the command word and vertex A's colour in one go.
*
* @details For a value that came out of the GTE with CODE preloaded from
* getCommandWord. Unlike setColorA this does not preserve the transparency
* bit, because the value being stored already carries it.
*/
GouraudLine& setColorAPacked(uint32_t packed) {
command = packed;
return *this;
}
GouraudLine& setSemiTrans() {
command |= 0x02000000;
return *this;
+42
View File
@@ -181,6 +181,29 @@ struct GouraudQuad {
command &= ~0x02000000;
return *this;
}
/**
* @brief The GP0 command word, minus any colour.
*
* @details A GP0 polygon's first word is the command in bits 31-24 and
* vertex A's colour in 23-0, and the GTE's RGBC register has an 8 bit CODE
* field that it fuses into every colour it produces - which is exactly what
* that field is for. Load this into CODE before an nc* command and the
* colour FIFO hands back finished first words; then store them with
* setColorAPacked and the whole thing is a word store instead of a
* read-modify-write per vertex.
*/
uint32_t getCommandWord() const { return command & 0xff000000; }
/**
* @brief Sets the command word and vertex A's colour in one go.
*
* @details For a value that came out of the GTE with CODE preloaded from
* getCommandWord. Unlike setColorA this does not preserve the transparency
* bit, because the value being stored already carries it.
*/
GouraudQuad& setColorAPacked(uint32_t packed) {
command = packed;
return *this;
}
GouraudQuad& setSemiTrans() {
command |= 0x02000000;
return *this;
@@ -303,6 +326,25 @@ struct GouraudTexturedQuad {
command &= ~0x02000000;
return *this;
}
/**
* @brief The GP0 command word, minus any colour.
*
* @details Meant for the GTE's RGBC CODE field, which gets fused into every
* colour the GTE emits. Preload it and the colour FIFO hands back finished
* first words. See `GouraudQuad::getCommandWord` for the full round trip.
*/
uint32_t getCommandWord() const { return command & 0xff000000; }
/**
* @brief Sets the command word and vertex A's colour in one go.
*
* @details For a value that came out of the GTE with CODE preloaded from
* getCommandWord. Unlike setColorA this does not preserve the transparency
* bit, because the value being stored already carries it.
*/
GouraudTexturedQuad& setColorAPacked(uint32_t packed) {
command = packed;
return *this;
}
GouraudTexturedQuad& setSemiTrans() {
command |= 0x02000000;
return *this;
+38
View File
@@ -163,6 +163,25 @@ struct GouraudTriangle {
command &= ~0x02000000;
return *this;
}
/**
* @brief The GP0 command word, minus any colour.
*
* @details Meant for the GTE's RGBC CODE field, which gets fused into every
* colour the GTE emits. Preload it and the colour FIFO hands back finished
* first words. See `GouraudQuad::getCommandWord` for the full round trip.
*/
uint32_t getCommandWord() const { return command & 0xff000000; }
/**
* @brief Sets the command word and vertex A's colour in one go.
*
* @details For a value that came out of the GTE with CODE preloaded from
* getCommandWord. Unlike setColorA this does not preserve the transparency
* bit, because the value being stored already carries it.
*/
GouraudTriangle& setColorAPacked(uint32_t packed) {
command = packed;
return *this;
}
GouraudTriangle& setSemiTrans() {
command |= 0x02000000;
return *this;
@@ -266,6 +285,25 @@ struct GouraudTexturedTriangle {
command &= ~0x02000000;
return *this;
}
/**
* @brief The GP0 command word, minus any colour.
*
* @details Meant for the GTE's RGBC CODE field, which gets fused into every
* colour the GTE emits. Preload it and the colour FIFO hands back finished
* first words. See `GouraudQuad::getCommandWord` for the full round trip.
*/
uint32_t getCommandWord() const { return command & 0xff000000; }
/**
* @brief Sets the command word and vertex A's colour in one go.
*
* @details For a value that came out of the GTE with CODE preloaded from
* getCommandWord. Unlike setColorA this does not preserve the transparency
* bit, because the value being stored already carries it.
*/
GouraudTexturedTriangle& setColorAPacked(uint32_t packed) {
command = packed;
return *this;
}
GouraudTexturedTriangle& setSemiTrans() {
command |= 0x02000000;
return *this;
+101 -5
View File
@@ -26,15 +26,99 @@ SOFTWARE.
#pragma once
#include "fixed-point.hh"
#include <EASTL/functional.h>
#include "psyqo/fixed-point.hh"
#include "psyqo/kernel.hh"
namespace psyqo {
class SPU {
public:
/**
* @brief The size of the SPU's sound RAM, in bytes.
*
* @details Voice start and repeat addresses are stored as `address / 8` in
* a 16 bit register, which covers exactly this range and not one byte more.
*/
static constexpr uint32_t SOUND_RAM_SIZE = 512 * 1024;
/**
* @brief The size of a single ADPCM block, in bytes.
*
* @details An ADPCM block is a two byte header followed by 28 samples
* stored as 4 bit nibbles. The SPU only ever addresses sound RAM in whole
* blocks, so every transfer into it is a multiple of this.
*/
static constexpr size_t ADPCM_BLOCK_SIZE = 16;
/**
* @brief The largest DMA block size the SPU will accept, in bytes.
*
* @details The block size field of `BCR` counts words, and the SPU's fifo
* tops out at 16 of them.
*/
static constexpr size_t MAX_DMA_BLOCK_SIZE = 16 * sizeof(uint32_t);
/**
* @brief Resets the SPU to a known, silent state.
*
* @details The kernel calls this during application startup, so there is
* normally no reason to call it again.
*/
static void initialize();
/**
* @brief Enables asynchronous DMA transfers for this SPU object.
*
* @details Registers the SPU's DMA completion handler and arms the
* corresponding interrupt, which is what lets the callback taking overload
* of `dmaWrite` ever complete. Call it once, during application start.
*/
void initAsync();
/**
* @brief Silences the given channels.
*
* @param channelMask A bitmask of the channels to silence.
*/
static void silenceChannels(uint32_t channelMask);
static void dmaWrite(uint32_t spuAddress, const void *ramAddress, uint16_t dataSize, uint8_t blockSize);
/**
* @brief Synchronously uploads data to the SPU's sound RAM.
*
* @details Blocks until the transfer completes. All sizes are in bytes.
*
* @param spuAddress The destination address in sound RAM. Must be 8 byte
* aligned, and the transfer has to fit within `SOUND_RAM_SIZE`.
* @param ramAddress The source address in main RAM. Must be 4 byte aligned.
* @param dataSize The number of bytes to transfer. Must be a non-zero
* multiple of `ADPCM_BLOCK_SIZE`.
* @param blockSize The DMA block size in bytes, or 0 to pick the largest
* one dividing `dataSize`. Must be a multiple of 4 and no larger than
* `MAX_DMA_BLOCK_SIZE`.
*/
static void dmaWrite(uint32_t spuAddress, const void* ramAddress, size_t dataSize, size_t blockSize = 0);
/**
* @brief Asynchronously uploads data to the SPU's sound RAM.
*
* @details Returns as soon as the transfer has started, and requires
* `initAsync` to have been called. Only one transfer may be in flight at a
* time, but `callback` is free to start the next one.
*
* @param spuAddress The destination address in sound RAM. Must be 8 byte
* aligned, and the transfer has to fit within `SOUND_RAM_SIZE`.
* @param ramAddress The source address in main RAM. Must be 4 byte aligned,
* and has to stay alive until the callback fires.
* @param dataSize The number of bytes to transfer. Must be a non-zero
* multiple of `ADPCM_BLOCK_SIZE`.
* @param callback The function to call once the transfer has completed.
* @param dmaCallback Whether to call it from the interrupt handler, or from
* the main loop.
*/
void dmaWrite(uint32_t spuAddress, const void* ramAddress, size_t dataSize, eastl::function<void()>&& callback,
DMA::DmaCallback dmaCallback = DMA::FROM_MAIN_LOOP);
struct ChannelPlaybackConfig {
FixedPoint<12, uint16_t> sampleRate;
@@ -42,7 +126,16 @@ class SPU {
uint32_t adsr;
};
static void playADPCM(uint8_t channelId, uint16_t spuRamAddress, const ChannelPlaybackConfig &config, bool hardCut);
/**
* @brief Starts playing an ADPCM sample on the given channel.
*
* @param channelId The channel to play on, 0 to 23.
* @param spuRamAddress The address of the sample in sound RAM. Must be 8
* byte aligned and within `SOUND_RAM_SIZE`.
* @param config The volume, sample rate and ADSR settings to use.
* @param hardCut Whether to key the channel off before keying it back on.
*/
static void playADPCM(uint8_t channelId, uint32_t spuRamAddress, const ChannelPlaybackConfig& config, bool hardCut);
static uint32_t getNextFreeChannel();
static constexpr uint32_t NO_FREE_CHANNEL = 0xffffffff;
@@ -50,8 +143,11 @@ class SPU {
static constexpr uint16_t BASE_ALLOC_ADDR = 0x1010;
private:
template <typename T>
static bool waitForStatus(T mask, T expected, const volatile T *value);
eastl::function<void()> m_dmaCallback = nullptr;
unsigned m_dmaEventSlot = 0;
bool m_transferPending = false;
bool m_fromISR = false;
bool m_initialized = false;
};
} // namespace psyqo
+1 -1
View File
@@ -66,7 +66,7 @@ class PlayCDDAAction : public psyqo::CDRomDevice::Action<PlayCDDAActionState> {
psyqo::Kernel::assert(device->isIdle(), "CDRomDevice::playCDDA() called while another action is in progress");
registerMe(device);
setCallback(eastl::move(callback));
setState(PlayCDDAActionState::SEEK);
setState(PlayCDDAActionState::SETMODE);
m_start = msf;
eastl::atomic_signal_fence(eastl::memory_order_release);
psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::SETMODE, stopAtEndOfTrack ? 3 : 1);
+51
View File
@@ -0,0 +1,51 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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 "psyqo/gte-math.hh"
#include "psyqo/soft-math.hh"
using namespace psyqo::fixed_point_literals;
// Everything else in GteMath is in the header, because psyqo builds at -Os and
// GCC will not inline the GTE register accessors there - it emits jal into
// writeSafe/read, measured at up to +67% on the per-call numbers. This one is
// out of line only because it calls into SoftMath for the Newton refinement,
// and the refinement is arithmetic rather than register traffic.
void psyqo::GteMath::normalizeVec3(Vec3 *v) {
FixedPoint<> square = v->x * v->x + v->y * v->y + v->z * v->z;
// SoftMath::squareRoot returns 0 for raw() <= 1 and normalizeVec3 then
// divides by it, so degenerate input has to be caught here rather than
// handed on.
if (square.raw() <= 1) {
*v = {0.0_fp, 0.0_fp, 1.0_fp};
return;
}
FixedPoint<> len = SoftMath::inverseSquareRoot(square, inverseSquareRootSeed(square));
v->x *= len;
v->y *= len;
v->z *= len;
}
+93 -21
View File
@@ -26,32 +26,83 @@ SOFTWARE.
#include "psyqo/spu.hh"
#include <EASTL/atomic.h>
#include "common/hardware/dma.h"
#include "common/hardware/spu.h"
#include "psyqo/kernel.hh"
constexpr uint16_t DUMMY_SAMPLE_POSITION = 0x1000;
constexpr uint8_t DUMMY_SAMPLE_SIZE = 16;
alignas(4) constexpr uint8_t DUMMY_SAMPLE[] = {0x00, 0b101, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
void psyqo::SPU::dmaWrite(const uint32_t spuAddress, const void *ramAddress, const uint16_t dataSize,
const uint8_t blockSize) {
Kernel::assert(blockSize % sizeof(uint32_t) == 0 && blockSize != 0 && blockSize <= 16, "Invalid DMA block size");
namespace {
// Checks a transfer request, and resolves a block size of 0 to the largest one
// that divides dataSize evenly. Sizes are in bytes; the returned block size is
// always a multiple of a word, so it can be handed to the BCR math as is.
size_t validateTransfer(uint32_t spuAddress, const void* ramAddress, size_t dataSize, size_t blockSize) {
using SPU = psyqo::SPU;
psyqo::Kernel::assert((spuAddress % 8) == 0, "SPU DMA destination must be 8 bytes aligned");
// Phrased as a subtraction so a wild spuAddress can't wrap the sum past the check.
psyqo::Kernel::assert(spuAddress <= SPU::SOUND_RAM_SIZE && dataSize <= SPU::SOUND_RAM_SIZE - spuAddress,
"SPU DMA transfer runs past the end of sound RAM");
psyqo::Kernel::assert((reinterpret_cast<uintptr_t>(ramAddress) & 3) == 0, "SPU DMA source must be 4 bytes aligned");
psyqo::Kernel::assert(dataSize != 0 && (dataSize % SPU::ADPCM_BLOCK_SIZE) == 0,
"SPU DMA size must be a whole number of ADPCM blocks");
if (blockSize == 0) {
// dataSize is a multiple of ADPCM_BLOCK_SIZE, which is itself a power of
// two no larger than MAX_DMA_BLOCK_SIZE, so this always terminates.
blockSize = SPU::MAX_DMA_BLOCK_SIZE;
while (dataSize % blockSize) blockSize >>= 1;
}
psyqo::Kernel::assert((blockSize % sizeof(uint32_t)) == 0 && blockSize != 0 && blockSize <= SPU::MAX_DMA_BLOCK_SIZE,
"Invalid SPU DMA block size");
psyqo::Kernel::assert((dataSize % blockSize) == 0, "SPU DMA size isn't a multiple of the block size");
psyqo::Kernel::assert((dataSize / blockSize) <= 0xffff, "SPU DMA block count doesn't fit in BCR");
return blockSize;
}
// Points the SPU at spuAddress and programs the DMA channel, without starting it.
void setupTransfer(uint32_t spuAddress, const void* ramAddress, size_t dataSize, size_t blockSize) {
SPU_CTRL &= ~(0b11 << 4);
waitForStatus<uint16_t>(0b11 << 4, 0b00 << 4, &SPU_STATUS);
psyqo::Kernel::waitForStatus<uint16_t>(0b11 << 4, 0b00 << 4, &SPU_STATUS);
SPU_CTRL |= 1 << 5;
SPU_RAM_DTA = spuAddress / 8;
waitForStatus<uint16_t>(1 << 5, 1 << 5, &SPU_STATUS);
psyqo::Kernel::waitForStatus<uint16_t>(1 << 5, 1 << 5, &SPU_STATUS);
DPCR |= 1 << 19;
DPCR &= ~(0b111 << 16);
DPCR |= 0b100 << 16;
DMA_CTRL[DMA_SPU].MADR = reinterpret_cast<uint32_t>(ramAddress);
DMA_CTRL[DMA_SPU].BCR = blockSize | ((dataSize / blockSize) << 16);
// The low half of BCR is the block size in words, the high half the number
// of blocks, and the transfer is the product of the two.
DMA_CTRL[DMA_SPU].BCR = (blockSize / sizeof(uint32_t)) | ((dataSize / blockSize) << 16);
}
} // namespace
void psyqo::SPU::dmaWrite(const uint32_t spuAddress, const void* ramAddress, const size_t dataSize, size_t blockSize) {
blockSize = validateTransfer(spuAddress, ramAddress, dataSize, blockSize);
setupTransfer(spuAddress, ramAddress, dataSize, blockSize);
DMA_CTRL[DMA_SPU].CHCR = 1 | 1 << 9 | 1 << 24;
waitForStatus<uint32_t>(1 << 24, 0 << 24, &DMA_CTRL[DMA_SPU].CHCR);
Kernel::waitForStatus<uint32_t>(1 << 24, 0 << 24, &DMA_CTRL[DMA_SPU].CHCR);
}
void psyqo::SPU::dmaWrite(const uint32_t spuAddress, const void* ramAddress, const size_t dataSize,
eastl::function<void()>&& callback, const DMA::DmaCallback dmaCallback) {
Kernel::assert(m_initialized, "SPU::initAsync must be called before an asynchronous DMA transfer");
// Guarding on the flag rather than on the callback, so that a transfer with
// an empty callback still blocks a concurrent one.
Kernel::assert(!m_transferPending, "Only one SPU DMA transfer at a time is permitted");
size_t blockSize = validateTransfer(spuAddress, ramAddress, dataSize, 0);
m_dmaCallback = eastl::move(callback);
m_fromISR = dmaCallback == DMA::FROM_ISR;
m_transferPending = true;
setupTransfer(spuAddress, ramAddress, dataSize, blockSize);
eastl::atomic_signal_fence(eastl::memory_order_release);
DMA_CTRL[DMA_SPU].CHCR = 1 | 1 << 9 | 1 << 24;
}
void psyqo::SPU::silenceChannels(const uint32_t channelMask) {
@@ -73,23 +124,13 @@ void psyqo::SPU::silenceChannels(const uint32_t channelMask) {
SPU_KEY_ON_HIGH = (channelMask >> 16) & 0xffff;
}
template <typename T>
bool psyqo::SPU::waitForStatus(const T mask, const T expected, const volatile T *value) {
for (int timeout = 10000; timeout >= 0; timeout--) {
if ((*value & mask) == expected) {
return true;
}
}
return false;
}
void psyqo::SPU::initialize() {
SBUS_DEV4_CTRL = 1 | 0b1110 << 4 | 1 << 8 | 1 << 12 | 1 << 13 | 0b1001 << 16 | 0 << 24 | 1 << 29;
DPCR |= 1 << 19;
SPU_CTRL = 0;
SPU_VOL_MAIN_LEFT = 0x3fff;
SPU_VOL_MAIN_LEFT = 0x3fff;
SPU_VOL_MAIN_RIGHT = 0x3fff;
SPU_REVERB_LEFT = 0;
SPU_REVERB_RIGHT = 0;
@@ -109,14 +150,45 @@ void psyqo::SPU::initialize() {
SPU_CTRL = 1 << 15 | 1 << 14 | 1 << 6;
dmaWrite(DUMMY_SAMPLE_POSITION, &DUMMY_SAMPLE, DUMMY_SAMPLE_SIZE, 4);
dmaWrite(DUMMY_SAMPLE_POSITION, &DUMMY_SAMPLE, sizeof(DUMMY_SAMPLE));
silenceChannels(0xffffffff);
}
void psyqo::SPU::playADPCM(const uint8_t channelId, const uint16_t spuRamAddress, const ChannelPlaybackConfig &config,
void psyqo::SPU::initAsync() {
// The kernel already ran initialize() during startup, so all that's left is
// to wire up the completion interrupt.
Kernel::assert(!m_initialized, "SPU::initAsync called twice");
m_initialized = true;
m_dmaEventSlot = Kernel::registerDmaEvent(Kernel::DMA::SPU, [this]() {
eastl::atomic_signal_fence(eastl::memory_order_acquire);
// Take the callback and clear the in-flight flag before running it, so
// that a callback which queues the next transfer isn't rejected, and so
// that the callback it installs doesn't get wiped afterwards.
auto callback = eastl::move(m_dmaCallback);
m_dmaCallback = nullptr;
m_transferPending = false;
if (!callback) return;
if (m_fromISR) {
callback();
} else {
Kernel::queueCallbackFromISR(eastl::move(callback));
}
});
// Kernel::Internal::prepare zeroes DICR, so the completion interrupt has to
// be armed here or the handler above will never run. Bits 16 to 22 are the
// per channel enables, and the SPU is channel 4.
uint32_t dicr = DICR;
dicr &= 0xffffff;
dicr |= 1 << 20;
DICR = dicr;
}
void psyqo::SPU::playADPCM(const uint8_t channelId, const uint32_t spuRamAddress, const ChannelPlaybackConfig& config,
const bool hardCut) {
Kernel::assert(channelId < 24, "Invalid SPU channel ID");
Kernel::assert((spuRamAddress % 8) == 0, "ADPCM sample address must be 8 bytes aligned");
Kernel::assert(spuRamAddress < SOUND_RAM_SIZE, "ADPCM sample address is past the end of sound RAM");
if (hardCut) {
if (channelId > 15) {
SPU_KEY_OFF_HIGH = 1 << (channelId - 16);
+1
View File
@@ -11,6 +11,7 @@ test-msf.cpp \
test-adler32.cpp \
test-bezier.cpp \
test-primitives.cpp \
test-gte-math.cpp \
CXXFLAGS = -std=c++20
+259
View File
@@ -0,0 +1,259 @@
/*
MIT License
Copyright (c) 2026 PCSX-Redux authors
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 "psyqo/gte-math.hh"
#include "psyqo/matrix.hh"
#include "psyqo/soft-math.hh"
#include "psyqo/vector.hh"
#include "snitch_all.hpp"
/*
* GteMath is meant to be a drop-in for SoftMath, so the question is not whether
* it produces a plausible number but whether it produces the SAME number.
* Everything here runs both implementations over a spread of inputs and bounds
* the difference, in raw 20.12 units where 4096 is 1.0.
*
* The tolerances are not round numbers chosen to make this pass. The GTE works
* in 1.3.12 through 16-bit IR registers where SoftMath carries 32 bits
* throughout, so exact agreement is not available and a tolerance of zero would
* only mean the test was lying.
*/
using namespace psyqo;
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
namespace {
constexpr int32_t kVecTolerance = 16;
constexpr int32_t kMatTolerance = 8;
constexpr int32_t kNormalTolerance = 24;
Trig<> g_trig;
int32_t absDiff(FixedPoint<> a, FixedPoint<> b) {
int32_t d = a.raw() - b.raw();
return d < 0 ? -d : d;
}
int32_t worstVec(const Vec3& a, const Vec3& b) {
int32_t x = absDiff(a.x, b.x);
int32_t y = absDiff(a.y, b.y);
int32_t z = absDiff(a.z, b.z);
int32_t m = x > y ? x : y;
return z > m ? z : m;
}
int32_t worstMat(const Matrix33& a, const Matrix33& b) {
int32_t m = 0;
for (unsigned r = 0; r < 3; r++) {
int32_t w = worstVec(a.vs[r], b.vs[r]);
if (w > m) m = w;
}
return m;
}
// Axis-aligned, diagonal, large, negative, and some small ones - small is where
// an inverse square root seed goes wrong.
//
// These stay above about |v| = 0.1 because the SoftMath side cannot go lower:
// SoftMath::normalizeVec3 divides by squareRoot(s), squareRoot returns 0 for a
// small enough argument, and the result is a divide by zero that traps rather
// than returning anything. GteMath guards it; the reference does not, so a
// genuinely tiny vector here would crash the oracle and not the subject. The
// degenerate case is covered on its own below, against GteMath alone.
const Vec3 c_vectors[] = {
{1.0_fp, 0.0_fp, 0.0_fp}, {0.0_fp, 1.0_fp, 0.0_fp}, {0.0_fp, 0.0_fp, 1.0_fp},
{1.0_fp, 1.0_fp, 1.0_fp}, {-1.0_fp, 0.5_fp, -0.25_fp}, {3.5_fp, -2.0_fp, 1.25_fp},
{0.25_fp, 0.25_fp, 0.25_fp}, {0.15_fp, -0.2_fp, 0.1_fp}, {2.0_fp, 0.0_fp, -3.0_fp},
{-0.7_fp, -0.7_fp, 0.14_fp}, {0.125_fp, 0.25_fp, 0.5_fp}, {-3.4_fp, 3.4_fp, 0.0_fp},
};
constexpr unsigned c_vectorCount = sizeof(c_vectors) / sizeof(c_vectors[0]);
Matrix33 someMatrix(unsigned i) {
Angle a = 0.13_pi * int32_t(i + 1);
auto axis = (i % 3 == 0) ? SoftMath::Axis::X : (i % 3 == 1) ? SoftMath::Axis::Y : SoftMath::Axis::Z;
return SoftMath::generateRotationMatrix33(a, axis, g_trig);
}
} // namespace
TEST_CASE("GteMath matrixVecMul3 agrees with SoftMath") {
for (unsigned m = 0; m < 6; m++) {
Matrix33 mat = someMatrix(m);
for (unsigned v = 0; v < c_vectorCount; v++) {
Vec3 expected, got;
SoftMath::matrixVecMul3(mat, c_vectors[v], &expected);
GteMath::matrixVecMul3(mat, c_vectors[v], &got);
REQUIRE(worstVec(expected, got) <= kVecTolerance);
}
}
}
TEST_CASE("GteMath multiplyMatrix33 agrees with SoftMath") {
for (unsigned i = 0; i < 6; i++) {
for (unsigned j = 0; j < 6; j++) {
Matrix33 expected, got;
SoftMath::multiplyMatrix33(someMatrix(i), someMatrix(j), &expected);
GteMath::multiplyMatrix33(someMatrix(i), someMatrix(j), &got);
REQUIRE(worstMat(expected, got) <= kMatTolerance);
}
}
}
// out may alias an input. Its own case because the implementation accumulates
// into locals first, and an "optimisation" that wrote back per column would
// break here and nowhere else.
TEST_CASE("GteMath multiplyMatrix33 tolerates aliasing") {
Matrix33 a = someMatrix(1);
Matrix33 b = someMatrix(4);
Matrix33 reference;
GteMath::multiplyMatrix33(a, b, &reference);
Matrix33 aliased = a;
GteMath::multiplyMatrix33(aliased, b, &aliased);
REQUIRE(worstMat(reference, aliased) == 0);
}
TEST_CASE("GteMath crossProductVec3 agrees with SoftMath") {
for (unsigned i = 0; i < c_vectorCount; i++) {
for (unsigned j = 0; j < c_vectorCount; j++) {
if (i == j) continue;
Vec3 expected, got;
SoftMath::crossProductVec3(c_vectors[i], c_vectors[j], &expected);
GteMath::crossProductVec3(c_vectors[i], c_vectors[j], &got);
REQUIRE(worstVec(expected, got) <= kVecTolerance);
}
}
}
TEST_CASE("GteMath normalizeVec3 agrees with SoftMath") {
for (unsigned i = 0; i < c_vectorCount; i++) {
Vec3 expected = c_vectors[i];
Vec3 got = c_vectors[i];
SoftMath::normalizeVec3(&expected);
GteMath::normalizeVec3(&got);
REQUIRE(worstVec(expected, got) <= kNormalTolerance);
}
}
// Agreeing with SoftMath and being correct are different claims: if both were
// wrong the same way, the comparison above would still pass. So check the
// property directly.
TEST_CASE("GteMath normalizeVec3 produces unit vectors") {
for (unsigned i = 0; i < c_vectorCount; i++) {
Vec3 v = c_vectors[i];
GteMath::normalizeVec3(&v);
FixedPoint<> sq = v.x * v.x + v.y * v.y + v.z * v.z;
int32_t d = sq.raw() - 4096;
if (d < 0) d = -d;
REQUIRE(d <= 96);
}
}
// Degenerate input, GteMath only - see the note on c_vectors for why this is
// not a comparison. A zero-length vector has no defined direction, so the
// contract is just that it returns something finite and unit rather than
// trapping.
TEST_CASE("GteMath normalizeVec3 survives a zero vector") {
Vec3 v = {0.0_fp, 0.0_fp, 0.0_fp};
GteMath::normalizeVec3(&v);
FixedPoint<> sq = v.x * v.x + v.y * v.y + v.z * v.z;
int32_t d = sq.raw() - 4096;
if (d < 0) d = -d;
REQUIRE(d <= 96);
}
// The seed has to land inside Newton's basin across the whole domain, not just
// near 1.0. Shifting by the whole leading-bit count instead of half of it is a
// seed for 1/x, which diverges for small inputs rather than degrading, so this
// sweeps down to something tiny on purpose.
TEST_CASE("GteMath inverse square root seed converges across the domain") {
for (int32_t raw = 4096; raw >= 4; raw = raw * 3 / 4) {
FixedPoint<> x(raw, FixedPoint<>::RAW);
FixedPoint<> y = SoftMath::inverseSquareRoot(x, GteMath::inverseSquareRootSeed(x));
FixedPoint<> check = y * y * x; // should be 1.0 if y is 1/sqrt(x)
int32_t d = check.raw() - 4096;
if (d < 0) d = -d;
REQUIRE(d <= 128);
}
}
TEST_CASE("GteMath cubic agrees with Bezier::cubic") {
const Vec3 a = {-1.5_fp, 0.25_fp, 0.0_fp};
const Vec3 b = {-0.5_fp, 1.75_fp, 1.0_fp};
const Vec3 c = {0.5_fp, -1.75_fp, 1.0_fp};
const Vec3 d = {1.5_fp, -0.25_fp, 0.0_fp};
for (int32_t i = 0; i <= 16; i++) {
FixedPoint<> t(i * 256, FixedPoint<>::RAW);
Vec3 expected = Bezier::cubic(a, b, c, d, t);
Vec3 got = GteMath::cubic(a, b, c, d, t);
REQUIRE(worstVec(expected, got) <= kVecTolerance);
}
}
// Endpoints are exact by definition, so they are the one place a tolerance
// would paper over a real error.
TEST_CASE("GteMath cubic interpolates its endpoints") {
const Vec3 a = {-1.5_fp, 0.25_fp, 0.75_fp};
const Vec3 b = {-0.5_fp, 1.75_fp, 1.0_fp};
const Vec3 c = {0.5_fp, -1.75_fp, 1.0_fp};
const Vec3 d = {1.5_fp, -0.25_fp, -0.75_fp};
REQUIRE(worstVec(GteMath::cubic(a, b, c, d, 0.0_fp), a) <= 4);
REQUIRE(worstVec(GteMath::cubic(a, b, c, d, 1.0_fp), d) <= 4);
}
// The derivative deliberately drops the factor of three, so compare direction
// rather than magnitude. At the endpoints the analytic tangent lies along the
// first and last control-point differences.
TEST_CASE("GteMath cubicDerivative points along the control polygon") {
const Vec3 a = {-1.5_fp, 0.25_fp, 0.0_fp};
const Vec3 b = {-0.5_fp, 1.75_fp, 0.5_fp};
const Vec3 c = {0.5_fp, -1.75_fp, 0.5_fp};
const Vec3 d = {1.5_fp, -0.25_fp, 0.0_fp};
Vec3 startExpected = {b.x - a.x, b.y - a.y, b.z - a.z};
Vec3 endExpected = {d.x - c.x, d.y - c.y, d.z - c.z};
SoftMath::normalizeVec3(&startExpected);
SoftMath::normalizeVec3(&endExpected);
Vec3 startGot = GteMath::cubicDerivative(a, b, c, d, 0.0_fp);
Vec3 endGot = GteMath::cubicDerivative(a, b, c, d, 1.0_fp);
GteMath::normalizeVec3(&startGot);
GteMath::normalizeVec3(&endGot);
REQUIRE(worstVec(startExpected, startGot) <= kNormalTolerance);
REQUIRE(worstVec(endExpected, endGot) <= kNormalTolerance);
}
// crossProductVec3 eats the rotation matrix diagonal, because the GTE takes its
// first operand from R11/R22/R33 rather than a vector register. That is in the
// header, and comments rot, so pin it: if an implementation ever stops
// clobbering RT this fails and somebody gets to delete a warning.
TEST_CASE("GteMath crossProductVec3 clobbers the rotation diagonal") {
GTE::writeSafe<GTE::PseudoRegister::Rotation>(someMatrix(2));
Vec3 out;
GteMath::crossProductVec3({1.0_fp, 0.0_fp, 0.0_fp}, {0.0_fp, 1.0_fp, 0.0_fp}, &out);
int32_t r11 = int32_t(int16_t(GTE::readRaw<GTE::Register::R11R12>() & 0xffff));
REQUIRE(r11 == 4096);
}
+2 -2
View File
@@ -28,9 +28,9 @@
#include "core/sstate.h"
#include "json.hpp"
#include "spu/adsr.h"
#include "spu/miniaudio.h"
#include "spu/noise.h"
#include "spu/reverb.h"
#include "spu/sdlaudio.h"
#include "spu/types.h"
#include "support/settings.h"
@@ -296,7 +296,7 @@ class impl final : public SPUInterface {
int &gvalr0() { return gauss_window[4 + gauss_ptr]; }
int &gvalr(int pos) { return gauss_window[4 + ((gauss_ptr + pos) & 3)]; }
MiniAudio m_audioOut = {settings};
SDLAudio m_audioOut = {settings};
xa_decode_t m_cdda;
// Debug window.
-272
View File
@@ -1,272 +0,0 @@
/***************************************************************************
* Copyright (C) 2021 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include "spu/miniaudio.h"
#include <limits>
#include <stdexcept>
#include "core/system.h"
#include "spu/interface.h"
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio/miniaudio.h"
// Upper clamp for the sink speed multiplier. Any host is emulation-bound long before this, so a value
// at or beyond it behaves as "unbounded" (the CPU's audio goalpost is always already satisfied and the
// producer rings are drained dry every callback). A configured speed <= 0 is treated as unbounded too.
static constexpr int kMaxSpeed = 1024;
static int effectiveSpeed(int configured) {
if (configured <= 0 || configured > kMaxSpeed) return kMaxSpeed;
return configured;
}
PCSX::SPU::MiniAudio::MiniAudio(PCSX::SPU::SettingsType& settings)
: m_settings(settings), m_listener(g_system->m_eventBus) {
for (unsigned i = 0; i <= ma_backend_null; i++) {
ma_backend b = ma_backend(i);
if (ma_is_backend_enabled(b)) {
m_backends.push_back(ma_get_backend_name(b));
}
}
m_listener.listen<Events::ExecutionFlow::Run>([this](const auto& event) {
if (ma_device_start(&m_device) != MA_SUCCESS) {
uninit();
init(true);
}
if (!m_settings.get<NullSync>()) return;
if (ma_device_start(&m_deviceNull) != MA_SUCCESS) {
throw std::runtime_error("Unable to start NULL audio device");
}
});
m_listener.listen<Events::ExecutionFlow::Pause>([this](const auto& event) {
if (ma_device_stop(&m_device) != MA_SUCCESS) {
throw std::runtime_error("Unable to stop audio device");
};
if (!ma_device_is_started(&m_deviceNull)) return;
if (ma_device_stop(&m_deviceNull) != MA_SUCCESS) {
throw std::runtime_error("Unable to stop NULL audio device");
};
});
m_listener.listen<Events::SettingsLoaded>([this](const auto& event) { init(event.safe); });
}
void PCSX::SPU::MiniAudio::init(bool safe) {
// First, initialize NULL device
ma_backend nullContext = ma_backend_null;
if (ma_context_init(&nullContext, 1, NULL, &m_contextNull) != MA_SUCCESS) {
throw std::runtime_error("Error initializing NULL miniaudio context");
}
m_configNull = ma_device_config_init(ma_device_type_playback);
m_configNull.playback.format = ma_format_f32;
m_configNull.playback.channels = 2;
m_configNull.sampleRate = 44100;
m_configNull.periodSizeInFrames = 64;
m_configNull.periods = 2;
m_configNull.pUserData = this;
m_configNull.dataCallback = [](ma_device* device, void* output, const void* input, ma_uint32 frameCount) {
MiniAudio* self = reinterpret_cast<MiniAudio*>(device->pUserData);
self->callbackNull(device, reinterpret_cast<float*>(output), frameCount);
};
if (ma_device_init(&m_contextNull, &m_configNull, &m_deviceNull) != MA_SUCCESS) {
throw std::runtime_error("Unable to initialize NULL audio device");
}
// Then probe for actual device, and initialize it
ma_backend backends[ma_backend_null + 1];
unsigned count = 0;
if (safe) {
backends[0] = ma_backend_null;
count = 1;
} else {
bool found = false;
for (unsigned i = 0; i <= ma_backend_null; i++) {
ma_backend b = ma_backend(i);
if (!ma_is_backend_enabled(b)) continue;
backends[count++] = b;
if (ma_get_backend_name(b) == m_settings.get<Backend>().value) {
found = true;
count = 1;
backends[0] = b;
break;
}
}
if (!found) {
m_settings.get<Backend>().reset();
}
}
if (ma_context_init(backends, count, NULL, &m_context) != MA_SUCCESS) {
throw std::runtime_error("Error initializing miniaudio context");
}
m_devices.clear();
struct UserContext {
MiniAudio* miniAudio;
ma_device_config& config;
bool found = false;
};
UserContext userContext = {this, m_config};
ma_context_enumerate_devices(
&m_context,
[](ma_context* pContext, ma_device_type deviceType, const ma_device_info* pInfo, void* pUserData) -> ma_bool32 {
if (deviceType != ma_device_type_playback) return true;
UserContext* userContext = reinterpret_cast<UserContext*>(pUserData);
userContext->miniAudio->m_devices.push_back(pInfo->name);
if (pInfo->name == userContext->miniAudio->m_settings.get<Device>().value) {
userContext->config.playback.pDeviceID = &pInfo->id;
userContext->found = true;
}
return true;
},
&userContext);
if (!userContext.found) {
m_settings.get<Device>().reset();
}
m_config = ma_device_config_init(ma_device_type_playback);
m_config.playback.format = ma_format_f32;
m_config.playback.channels = 2;
m_config.sampleRate = 44100;
m_config.periodSizeInFrames = 64;
m_config.periods = 2;
m_config.pUserData = this;
m_config.aaudio.usage = ma_aaudio_usage_game;
m_config.wasapi.noAutoConvertSRC = true;
m_config.dataCallback = [](ma_device* device, void* output, const void* input, ma_uint32 frameCount) {
MiniAudio* self = reinterpret_cast<MiniAudio*>(device->pUserData);
self->callback(device, reinterpret_cast<float*>(output), frameCount);
};
if (ma_device_init(&m_context, &m_config, &m_device) != MA_SUCCESS) {
if (safe) {
throw std::runtime_error("Unable to initialize NULL audio device");
}
uninit();
init(true);
}
}
void PCSX::SPU::MiniAudio::uninit() {
ma_device_uninit(&m_device);
ma_device_uninit(&m_deviceNull);
ma_context_uninit(&m_context);
}
void PCSX::SPU::MiniAudio::maybeRestart() {
if (!g_system->running()) return;
if (ma_device_start(&m_device) != MA_SUCCESS) {
uninit();
init(true);
}
if (!m_settings.get<NullSync>()) return;
if (ma_device_start(&m_deviceNull) != MA_SUCCESS) {
throw std::runtime_error("Unable to start NULL audio device");
}
}
void PCSX::SPU::MiniAudio::callback(ma_device* device, float* output, ma_uint32 frameCount) {
if (frameCount > VoiceStream::BUFFER_SIZE) {
throw std::runtime_error("Too many frames requested by miniaudio");
}
static std::array<Buffer, STREAMS> buffers;
const bool mono = m_settings.get<Mono>();
const bool muted = m_settings.get<Mute>();
static_assert(STREAMS == 2);
for (unsigned i = 0; i < STREAMS; i++) {
size_t a = i == 0 ? m_voicesStream.dequeue(buffers[i].data(), frameCount)
: m_audioStream.dequeue(buffers[i].data(), frameCount);
for (size_t f = (muted ? 0 : a); f < frameCount; f++) {
// maybe warn about underflow? tho it's fine if it happens on stream 1 (cdda)
buffers[i][f] = {};
}
}
for (ma_uint32 f = 0; f < frameCount; f++) {
float l = 0.0f, r = 0.0f;
for (unsigned i = 0; i < STREAMS; i++) {
l += static_cast<float>(buffers[i][f].L) / static_cast<float>(std::numeric_limits<int16_t>::max());
r += static_cast<float>(buffers[i][f].R) / static_cast<float>(std::numeric_limits<int16_t>::max());
}
if (mono) {
const float lr = (l + r) * 0.5f;
output[f * 2 + 0] = lr;
output[f * 2 + 1] = lr;
} else {
output[f * 2 + 0] = l;
output[f * 2 + 1] = r;
}
}
// Fast-forward lives at the sink (the master clock). We have already kept the first frameCount
// frames of each stream for playback at native 44.1kHz -- pitch is preserved. To run the whole
// emulator `speed` times faster we drain the producer rings `speed` times faster: discard the
// additional (speed - 1) frame-runs from BOTH streams (SPU voices and CD-ROM XA). Draining the
// rings faster paces the SPU thread faster, which through waitForGoal paces the CPU thread faster.
// dequeue() naturally caps at what is available, so this is safe (and self-limits) at any speed.
const int speed = effectiveSpeed(m_settings.get<Speed>().value);
for (int rep = 1; rep < speed; rep++) {
m_voicesStream.dequeue(buffers[0].data(), frameCount);
m_audioStream.dequeue(buffers[1].data(), frameCount);
}
if (m_settings.get<NullSync>()) return;
callbackNull(device, output, frameCount);
}
void PCSX::SPU::MiniAudio::callbackNull(ma_device* device, float* output, ma_uint32 frameCount) {
m_frameCount.store(frameCount);
// Advance the master clock `speed` times faster than realtime. m_frames is the frame counter the
// CPU thread waits on in waitForGoal(); the CPU's goalpost grows at the realtime rate per emulated
// cycle, so advancing m_frames `speed` times faster makes the CPU reach its goalpost (and thus run)
// `speed` times faster. At speed == 1 this is byte-identical to the previous fetch_add(frameCount).
const int speed = effectiveSpeed(m_settings.get<Speed>().value);
auto total = m_frames.fetch_add(frameCount * static_cast<uint32_t>(speed));
#if HAS_ATOMIC_WAIT
auto goalpost = m_goalpost.load();
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_triggered.notify_one();
#else
std::unique_lock<std::mutex> l(m_mu);
auto goalpost = m_goalpost;
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_cv.notify_one();
#endif
}
+334
View File
@@ -0,0 +1,334 @@
/***************************************************************************
* Copyright (C) 2026 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include "spu/sdlaudio.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include "core/system.h"
#include "spu/interface.h"
// Upper clamp for the sink speed multiplier. Any host is emulation-bound long before this, so a value
// at or beyond it behaves as "unbounded" (the CPU's audio goalpost is always already satisfied and the
// producer rings are drained dry every callback). A configured speed <= 0 is treated as unbounded too.
static constexpr int kMaxSpeed = 1024;
static int effectiveSpeed(int configured) {
if (configured <= 0 || configured > kMaxSpeed) return kMaxSpeed;
return configured;
}
PCSX::SPU::SDLAudio::SDLAudio(PCSX::SPU::SettingsType& settings)
: m_settings(settings), m_listener(g_system->m_eventBus) {
// Enumerate compiled-in drivers. This is static information available before
// SDL_InitSubSystem(SDL_INIT_AUDIO), so we can present the list in the UI without
// forcing an early audio init.
int n = SDL_GetNumAudioDrivers();
for (int i = 0; i < n; i++) {
const char* name = SDL_GetAudioDriver(i);
if (name) m_backends.emplace_back(name);
}
m_listener.listen<Events::ExecutionFlow::Run>([this](const auto& event) {
if (!m_audioInitialized) return;
if (!SDL_ResumeAudioDevice(m_device)) {
uninit();
init(true);
}
if (m_settings.get<NullSync>()) startNullThread();
});
m_listener.listen<Events::ExecutionFlow::Pause>([this](const auto& event) {
if (!m_audioInitialized) return;
SDL_PauseAudioDevice(m_device);
stopNullThread();
});
m_listener.listen<Events::SettingsLoaded>([this](const auto& event) { init(event.safe); });
}
void PCSX::SPU::SDLAudio::init(bool safe) {
// Pick the audio driver. SDL_HINT_AUDIO_DRIVER must be set before SDL_InitSubSystem.
if (safe) {
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
} else {
const std::string& wanted = m_settings.get<Backend>().value;
bool found = false;
for (const auto& b : m_backends) {
if (b == wanted) {
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, b.c_str());
found = true;
break;
}
}
if (!found) {
m_settings.get<Backend>().reset();
// Empty string lets SDL pick whatever default it likes.
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "");
}
}
// Suggest a small period to keep latency low. SDL treats this as a hint; the actual
// callback chunk size may differ, which is why streamCallback handles arbitrary sizes.
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, "64");
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
if (safe) {
throw std::runtime_error(std::string("Unable to initialize SDL audio: ") + SDL_GetError());
}
uninit();
init(true);
return;
}
m_audioInitialized = true;
// Enumerate playback devices and locate the user's saved choice, if any.
m_devices.clear();
SDL_AudioDeviceID chosen = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
bool deviceFound = false;
int devCount = 0;
SDL_AudioDeviceID* ids = SDL_GetAudioPlaybackDevices(&devCount);
if (ids) {
const std::string& wantedDev = m_settings.get<Device>().value;
for (int i = 0; i < devCount; i++) {
const char* name = SDL_GetAudioDeviceName(ids[i]);
if (!name) continue;
m_devices.emplace_back(name);
if (name == wantedDev) {
chosen = ids[i];
deviceFound = true;
}
}
SDL_free(ids);
}
if (!deviceFound) {
m_settings.get<Device>().reset();
}
SDL_AudioSpec spec;
spec.format = SDL_AUDIO_F32;
spec.channels = kChannels;
spec.freq = kSampleRate;
m_device = SDL_OpenAudioDevice(chosen, &spec);
if (m_device == 0) {
if (safe) {
throw std::runtime_error(std::string("Unable to open SDL audio device: ") + SDL_GetError());
}
uninit();
init(true);
return;
}
m_stream = SDL_CreateAudioStream(&spec, &spec);
if (!m_stream) {
const std::string err = SDL_GetError();
SDL_CloseAudioDevice(m_device);
m_device = 0;
if (safe) {
throw std::runtime_error("Unable to create SDL audio stream: " + err);
}
uninit();
init(true);
return;
}
auto trampoline = [](void* userdata, SDL_AudioStream* stream, int additional, int /*total*/) {
static_cast<SDLAudio*>(userdata)->streamCallback(stream, additional);
};
if (!SDL_SetAudioStreamGetCallback(m_stream, trampoline, this)) {
const std::string err = SDL_GetError();
SDL_DestroyAudioStream(m_stream);
m_stream = nullptr;
SDL_CloseAudioDevice(m_device);
m_device = 0;
throw std::runtime_error("Unable to set SDL audio stream callback: " + err);
}
if (!SDL_BindAudioStream(m_device, m_stream)) {
const std::string err = SDL_GetError();
SDL_DestroyAudioStream(m_stream);
m_stream = nullptr;
SDL_CloseAudioDevice(m_device);
m_device = 0;
throw std::runtime_error("Unable to bind SDL audio stream: " + err);
}
// Devices come up in the resumed state; pause until execution actually starts so
// we don't burn cycles streaming silence at boot.
SDL_PauseAudioDevice(m_device);
}
void PCSX::SPU::SDLAudio::uninit() {
stopNullThread();
if (m_stream) {
SDL_DestroyAudioStream(m_stream);
m_stream = nullptr;
}
if (m_device) {
SDL_CloseAudioDevice(m_device);
m_device = 0;
}
if (m_audioInitialized) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
m_audioInitialized = false;
}
}
void PCSX::SPU::SDLAudio::maybeRestart() {
if (!g_system->running()) return;
if (!m_audioInitialized) return;
if (!SDL_ResumeAudioDevice(m_device)) {
uninit();
init(true);
return;
}
if (m_settings.get<NullSync>()) startNullThread();
}
void PCSX::SPU::SDLAudio::streamCallback(SDL_AudioStream* stream, int additionalBytes) {
constexpr int kFrameSizeBytes = sizeof(float) * kChannels;
int requested = additionalBytes / kFrameSizeBytes;
if (requested <= 0) return;
const bool mono = m_settings.get<Mono>();
const bool muted = m_settings.get<Mute>();
static_assert(STREAMS == 2);
// SDL doesn't promise a fixed callback chunk size, so feed in slices that fit our
// mixing scratch buffer.
while (requested > 0) {
const uint32_t chunk = std::min<uint32_t>(requested, VoiceStream::BUFFER_SIZE);
for (unsigned i = 0; i < STREAMS; i++) {
size_t a = (i == 0) ? m_voicesStream.dequeue(m_mixBuffers[i].data(), chunk)
: m_audioStream.dequeue(m_mixBuffers[i].data(), chunk);
for (size_t f = (muted ? 0 : a); f < chunk; f++) {
// Same as the previous backend: silently zero-fill on underflow.
// CDDA underflow on stream 1 is expected and fine.
m_mixBuffers[i][f] = {};
}
}
for (uint32_t f = 0; f < chunk; f++) {
float l = 0.0f, r = 0.0f;
for (unsigned i = 0; i < STREAMS; i++) {
l += static_cast<float>(m_mixBuffers[i][f].L) /
static_cast<float>(std::numeric_limits<int16_t>::max());
r += static_cast<float>(m_mixBuffers[i][f].R) /
static_cast<float>(std::numeric_limits<int16_t>::max());
}
if (mono) {
const float lr = (l + r) * 0.5f;
m_outputBuffer[f * 2 + 0] = lr;
m_outputBuffer[f * 2 + 1] = lr;
} else {
m_outputBuffer[f * 2 + 0] = l;
m_outputBuffer[f * 2 + 1] = r;
}
}
SDL_PutAudioStreamData(stream, m_outputBuffer.data(), chunk * kFrameSizeBytes);
// Fast-forward lives at the sink (the master clock). We have already kept the first `chunk`
// frames of each stream for playback at native 44.1kHz -- pitch is preserved. To run the whole
// emulator `speed` times faster we drain the producer rings `speed` times faster: discard the
// additional (speed - 1) frame-runs from BOTH streams (SPU voices and CD-ROM XA). Draining the
// rings faster paces the SPU thread faster, which through waitForGoal paces the CPU thread
// faster. dequeue() naturally caps at what is available, so this is safe (and self-limits) at
// any speed.
const int speed = effectiveSpeed(m_settings.get<Speed>().value);
for (int rep = 1; rep < speed; rep++) {
m_voicesStream.dequeue(m_mixBuffers[0].data(), chunk);
m_audioStream.dequeue(m_mixBuffers[1].data(), chunk);
}
// When NullSync is off, the real audio callback is the timing source. When it's
// on, the dedicated null thread drives timing instead.
if (!m_settings.get<NullSync>()) {
advanceFrames(chunk);
}
requested -= chunk;
}
}
void PCSX::SPU::SDLAudio::advanceFrames(uint32_t frameCount) {
m_frameCount.store(frameCount);
// Advance the master clock `speed` times faster than realtime. m_frames is the frame counter the
// CPU thread waits on in waitForGoal(); the CPU's goalpost grows at the realtime rate per emulated
// cycle, so advancing m_frames `speed` times faster makes the CPU reach its goalpost (and thus run)
// `speed` times faster. At speed == 1 this is byte-identical to the previous fetch_add(frameCount).
const int speed = effectiveSpeed(m_settings.get<Speed>().value);
auto total = m_frames.fetch_add(frameCount * static_cast<uint32_t>(speed));
#if HAS_ATOMIC_WAIT
auto goalpost = m_goalpost.load();
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_triggered.notify_one();
#else
std::unique_lock<std::mutex> l(m_mu);
auto goalpost = m_goalpost;
if (goalpost == m_previousGoalpost) return;
if (((int32_t)(goalpost - total)) > 0) return;
m_previousGoalpost = goalpost;
m_triggered++;
m_cv.notify_one();
#endif
}
void PCSX::SPU::SDLAudio::startNullThread() {
if (m_nullThreadActive) return;
m_nullThreadStop.store(false);
m_nullThreadActive = true;
m_nullThread = std::thread([this]() { nullThreadLoop(); });
}
void PCSX::SPU::SDLAudio::stopNullThread() {
if (!m_nullThreadActive) return;
m_nullThreadStop.store(true);
if (m_nullThread.joinable()) m_nullThread.join();
m_nullThreadActive = false;
}
void PCSX::SPU::SDLAudio::nullThreadLoop() {
using namespace std::chrono;
// 64 frames at 44100 Hz ~= 1.451 ms per tick. We pretend to consume that many frames
// each tick, mirroring what miniaudio's null backend used to do for a stable timing
// source independent of the real device's bursty callbacks.
constexpr double periodSeconds = static_cast<double>(kPeriodFrames) / kSampleRate;
const auto periodNs = duration_cast<nanoseconds>(duration<double>(periodSeconds));
auto next = steady_clock::now();
while (!m_nullThreadStop.load()) {
next += periodNs;
std::this_thread::sleep_until(next);
advanceFrames(kPeriodFrames);
}
}
+34 -22
View File
@@ -1,5 +1,5 @@
/***************************************************************************
* Copyright (C) 2021 PCSX-Redux authors *
* Copyright (C) 2026 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
@@ -23,18 +23,15 @@
#include <array>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#define MA_NO_CUSTOM
#define MA_NO_DECODING
#define MA_NO_ENCODING
#define MA_NO_FLAC
#define MA_NO_GENERATION
#define MA_NO_MP3
#define MA_NO_WAV
#include <SDL3/SDL.h>
#include "miniaudio/miniaudio.h"
#include "spu/settings.h"
#include "support/circular.h"
#include "support/eventbus.h"
@@ -48,14 +45,14 @@
namespace PCSX {
namespace SPU {
class MiniAudio {
class SDLAudio {
public:
struct Frame {
int16_t L = 0, R = 0;
};
MiniAudio(SettingsType& settings);
~MiniAudio() { uninit(); }
ma_uint32 getFrameCount() { return m_frameCount.load(); }
SDLAudio(SettingsType& settings);
~SDLAudio() { uninit(); }
uint32_t getFrameCount() { return m_frameCount.load(); }
void reinit() {
uninit();
init();
@@ -108,25 +105,40 @@ class MiniAudio {
private:
static constexpr unsigned STREAMS = 2;
static constexpr int kSampleRate = 44100;
static constexpr int kChannels = 2;
static constexpr int kPeriodFrames = 64;
SettingsType& m_settings;
void callback(ma_device* device, float* output, ma_uint32 frameCount);
void callbackNull(ma_device* device, float* output, ma_uint32 frameCount);
void streamCallback(SDL_AudioStream* stream, int additionalBytes);
void advanceFrames(uint32_t frameCount);
void init(bool safe = false);
void uninit();
void maybeRestart();
void startNullThread();
void stopNullThread();
void nullThreadLoop();
SDL_AudioDeviceID m_device = 0;
SDL_AudioStream* m_stream = nullptr;
bool m_audioInitialized = false;
std::thread m_nullThread;
std::atomic<bool> m_nullThreadStop{false};
bool m_nullThreadActive = false;
ma_context m_context;
ma_device_config m_config;
ma_device m_device;
ma_context m_contextNull;
ma_device_config m_configNull;
ma_device m_deviceNull;
EventBus::Listener m_listener;
typedef Circular<Frame, 2 * 1024> VoiceStream;
VoiceStream m_voicesStream;
Circular<Frame, 16 * 1024> m_audioStream;
typedef std::array<Frame, VoiceStream::BUFFER_SIZE> Buffer;
// Mixing scratch space, only ever touched from the SDL audio thread.
std::array<Buffer, STREAMS> m_mixBuffers;
std::array<float, VoiceStream::BUFFER_SIZE * kChannels> m_outputBuffer;
std::atomic<uint32_t> m_frames = 0;
#if HAS_ATOMIC_WAIT
std::atomic<uint32_t> m_goalpost = 0;
@@ -142,7 +154,7 @@ class MiniAudio {
std::vector<std::string> m_backends;
std::vector<std::string> m_devices;
std::atomic<ma_uint32> m_frameCount;
std::atomic<uint32_t> m_frameCount{0};
};
} // namespace SPU
+2 -2
View File
@@ -524,8 +524,8 @@ void PCSX::SPU::impl::MainThread() {
if (iCycle++ > 16) {
bool done = false;
while (!done) {
done = m_audioOut.feedStreamData(reinterpret_cast<MiniAudio::Frame *>(spuBuffer),
(((uint8_t *)pS) - ((uint8_t *)spuBuffer)) / sizeof(MiniAudio::Frame));
done = m_audioOut.feedStreamData(reinterpret_cast<SDLAudio::Frame *>(spuBuffer),
(((uint8_t *)pS) - ((uint8_t *)spuBuffer)) / sizeof(SDLAudio::Frame));
if (endThread) {
threadEnded = 1;
return;
+5 -5
View File
@@ -29,8 +29,8 @@ static uint16_t hiword(uint32_t v) { return (v >> 16) & 0xffff; }
void PCSX::SPU::impl::FeedXA(xa_decode_t *xap) {
int sinc, spos, i, iSize, vl, vr, voldiv = 4 - settings.get<Volume>();
MiniAudio::Frame XABuffer[32 * 1024];
MiniAudio::Frame *XAFeed = XABuffer;
SDLAudio::Frame XABuffer[32 * 1024];
SDLAudio::Frame *XAFeed = XABuffer;
if (!spuIsOpen) return;
@@ -86,7 +86,7 @@ void PCSX::SPU::impl::FeedXA(xa_decode_t *xap) {
}
}
MiniAudio::Frame f;
SDLAudio::Frame f;
int16_t rawSampleL = static_cast<int16_t>(l & 0xffff);
int16_t rawSampleR = static_cast<int16_t>(l >> 16);
if (mixIrqAddress) {
@@ -130,7 +130,7 @@ void PCSX::SPU::impl::FeedXA(xa_decode_t *xap) {
l = s;
}
MiniAudio::Frame f;
SDLAudio::Frame f;
int16_t rawSampleL = static_cast<int16_t>(l & 0xffff);
int16_t rawSampleR = static_cast<int16_t>(l >> 16);
// Write the CD-XA samples (left/right) to a temporary buffer. Wrap around if necessary.
@@ -151,5 +151,5 @@ void PCSX::SPU::impl::FeedXA(xa_decode_t *xap) {
}
if (mixIrqAddress) cbMtx.unlock();
m_audioOut.feedStreamData(reinterpret_cast<MiniAudio::Frame *>(XABuffer), (XAFeed - XABuffer), 1);
m_audioOut.feedStreamData(reinterpret_cast<SDLAudio::Frame *>(XABuffer), (XAFeed - XABuffer), 1);
}
+2
View File
@@ -26,6 +26,8 @@ SOFTWARE.
#if !defined(_WIN32) && !defined(_WIN64)
#include <cstdlib>
#include <assert.h>
#include <fcntl.h>
#include <string.h>
+2
View File
@@ -23,6 +23,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <uv.h>
#include "support/uvfile.h"
#include <curl/curl.h>
+75
View File
@@ -0,0 +1,75 @@
/*
MIT License
Copyright (c) 2022 PCSX-Redux authors
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
// VersionInfo alone, split out of version.h so that core/system.h can name it
// without pulling in Update - which needs UvFile, and through it libuv.
#include <ctime>
#include <filesystem>
#include <optional>
#include <string>
#include "json.hpp"
#include "support/file.h"
namespace PCSX {
struct VersionInfo {
std::string version;
std::optional<unsigned> buildId;
std::string changeset;
std::time_t timestamp;
std::string updateMethod;
std::string updateChannel;
std::string updateCatalog;
std::string updateInfoBase;
std::string updateStorageUrl;
void loadFromFile(IO<File> file);
bool failed() const { return version.empty(); }
bool hasUpdateInfo() const {
if (version.empty()) return false;
if (updateCatalog.empty() || updateInfoBase.empty()) return false;
if (updateMethod == "appdistrib") {
return buildId.has_value() && !updateStorageUrl.empty();
}
return (updateMethod == "appcenter");
}
void clear() {
version.clear();
buildId = std::nullopt;
changeset.clear();
timestamp = 0;
updateMethod.clear();
updateChannel.clear();
updateCatalog.clear();
updateInfoBase.clear();
updateStorageUrl.clear();
}
};
} // namespace PCSX
+3 -1
View File
@@ -24,6 +24,8 @@ SOFTWARE.
*/
#include <uv.h>
#include "support/version.h"
#include <algorithm>
@@ -267,7 +269,7 @@ bool PCSX::Update::getDownloadUrl(const VersionInfo& versionInfo, std::function<
// All these defines need to be matching what we see in version-{platform}.cc
#if (!defined(__APPLE__) || !defined(__MACH__)) && !defined(__linux__) && !defined(_WIN32) && !defined(_WIN64)
bool PCSX::Update::applyUpdate(const std::filesystem::path& binDir) {
throw std::runtime_exception("No platform support for updates");
throw std::runtime_error("No platform support for updates");
return false;
}
#endif
+1 -32
View File
@@ -36,42 +36,11 @@ SOFTWARE.
#include "json.hpp"
#include "support/file.h"
#include "support/version-info.h"
#include "support/uvfile.h"
namespace PCSX {
struct VersionInfo {
std::string version;
std::optional<unsigned> buildId;
std::string changeset;
std::time_t timestamp;
std::string updateMethod;
std::string updateChannel;
std::string updateCatalog;
std::string updateInfoBase;
std::string updateStorageUrl;
void loadFromFile(IO<File> file);
bool failed() const { return version.empty(); }
bool hasUpdateInfo() const {
if (version.empty()) return false;
if (updateCatalog.empty() || updateInfoBase.empty()) return false;
if (updateMethod == "appdistrib") {
return buildId.has_value() && !updateStorageUrl.empty();
}
return (updateMethod == "appcenter");
}
void clear() {
version.clear();
buildId = std::nullopt;
changeset.clear();
timestamp = 0;
updateMethod.clear();
updateChannel.clear();
updateCatalog.clear();
updateInfoBase.clear();
updateStorageUrl.clear();
}
};
class Update {
public:
+5 -2
View File
@@ -28,7 +28,10 @@
#include <pwd.h>
#endif
#define ICON_SIZE ImGui::GetFont()->FontSize + 3
#define ICON_SIZE ImGui::GetFont()->LegacySize + 3
// GImGui->FontSize is the current rendering size on the global ImGui context;
// it survived ImGui v1.92 (only ImFont::FontSize was removed in favor of
// LegacySize). The original macro queried the live size, not the loaded one.
#define GUI_ELEMENT_SIZE std::max(GImGui->FontSize + 10.f, 24.f)
#define DEFAULT_ICON_SIZE 32
#define PI 3.141592f
@@ -350,7 +353,7 @@ namespace ifd {
float radius = i & 1 ? innerRadius : outerRadius;
window->DrawList->PathLineTo(ImVec2(center.x + radius * sin(i * angle), center.y - radius * cos(i * angle)));
}
window->DrawList->PathStroke(ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_Text]), true, 2.0f);
window->DrawList->PathStroke(ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_Text]), 2.0f, ImDrawFlags_Closed);
return ret;
}
Vendored Submodule
+1
Submodule third_party/SDL added at d9d5536704
+627
View File
@@ -0,0 +1,627 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/**
* \file SDL_build_config.h
*
* This is a set of defines to configure the SDL features
*/
#ifndef SDL_build_config_h_
#define SDL_build_config_h_
/* General platform specific identifiers */
#include <SDL3/SDL_platform_defines.h>
/* #undef SDL_PLATFORM_PRIVATE */
#ifdef SDL_PLATFORM_PRIVATE
#include "SDL_begin_config_private.h"
#endif
/* #undef HAVE_GCC_ATOMICS */
/* #undef HAVE_GCC_SYNC_LOCK_TEST_AND_SET */
/* #undef SDL_DISABLE_ALLOCA */
/* Useful headers */
#define HAVE_FLOAT_H 1
#define HAVE_STDARG_H 1
#define HAVE_STDDEF_H 1
#define HAVE_STDINT_H 1
/* Comment this if you want to build without any C library requirements */
#define HAVE_LIBC 1
#ifdef HAVE_LIBC
/* Useful headers */
/* #undef HAVE_ALLOCA_H */
/* #undef HAVE_ICONV_H */
#define HAVE_INTTYPES_H 1
#define HAVE_LIMITS_H 1
#define HAVE_MALLOC_H 1
#define HAVE_MATH_H 1
#define HAVE_MEMORY_H 1
#define HAVE_SIGNAL_H 1
#define HAVE_STDIO_H 1
#define HAVE_STDLIB_H 1
/* #undef HAVE_STRINGS_H */
#define HAVE_STRING_H 1
#define HAVE_SYS_TYPES_H 1
#define HAVE_WCHAR_H 1
/* #undef HAVE_PTHREAD_NP_H */
/* C library functions */
/* #undef HAVE_DLOPEN */
#define HAVE_MALLOC 1
/* #undef HAVE_FDATASYNC */
#define HAVE_GETENV 1
/* #undef HAVE_GETHOSTNAME */
/* #undef HAVE_SETENV */
#define HAVE_PUTENV 1
/* #undef HAVE_UNSETENV */
#define HAVE_ABS 1
/* #undef HAVE_BCOPY */
#define HAVE_MEMSET 1
#define HAVE_MEMCPY 1
#define HAVE_MEMMOVE 1
#define HAVE_MEMCMP 1
#define HAVE_WCSLEN 1
#define HAVE_WCSNLEN 1
/* #undef HAVE_WCSLCPY */
/* #undef HAVE_WCSLCAT */
#define HAVE_WCSSTR 1
#define HAVE_WCSCMP 1
#define HAVE_WCSNCMP 1
#define HAVE_WCSTOL 1
#define HAVE_STRLEN 1
#define HAVE_STRNLEN 1
/* #undef HAVE_STRLCPY */
/* #undef HAVE_STRLCAT */
#define HAVE_STRPBRK 1
#define HAVE__STRREV 1
/* #undef HAVE_INDEX */
/* #undef HAVE_RINDEX */
#define HAVE_STRCHR 1
#define HAVE_STRRCHR 1
#define HAVE_STRSTR 1
/* #undef HAVE_STRNSTR */
/* #undef HAVE_STRTOK_R */
#define HAVE_ITOA 1
#define HAVE__LTOA 1
/* #undef HAVE__UITOA */
#define HAVE__ULTOA 1
#define HAVE_STRTOL 1
#define HAVE_STRTOUL 1
#define HAVE__I64TOA 1
#define HAVE__UI64TOA 1
#define HAVE_STRTOLL 1
#define HAVE_STRTOULL 1
#define HAVE_STRTOD 1
#define HAVE_ATOI 1
#define HAVE_ATOF 1
#define HAVE_STRCMP 1
#define HAVE_STRNCMP 1
#define HAVE_VSSCANF 1
#define HAVE_VSNPRINTF 1
#define HAVE_ACOS 1
#define HAVE_ACOSF 1
#define HAVE_ASIN 1
#define HAVE_ASINF 1
#define HAVE_ATAN 1
#define HAVE_ATANF 1
#define HAVE_ATAN2 1
#define HAVE_ATAN2F 1
#define HAVE_CEIL 1
#define HAVE_CEILF 1
#define HAVE_COPYSIGN 1
#define HAVE_COPYSIGNF 1
#define HAVE__COPYSIGN 1
#define HAVE_COS 1
#define HAVE_COSF 1
#define HAVE_EXP 1
#define HAVE_EXPF 1
#define HAVE_FABS 1
#define HAVE_FABSF 1
#define HAVE_FLOOR 1
#define HAVE_FLOORF 1
#define HAVE_FMOD 1
#define HAVE_FMODF 1
#define HAVE_ISINF 1
/* #undef HAVE_ISINFF */
#define HAVE_ISINF_FLOAT_MACRO 1
#define HAVE_ISNAN 1
/* #undef HAVE_ISNANF */
#define HAVE_ISNAN_FLOAT_MACRO 1
#define HAVE_LOG 1
#define HAVE_LOGF 1
#define HAVE_LOG10 1
#define HAVE_LOG10F 1
#define HAVE_LROUND 1
#define HAVE_LROUNDF 1
#define HAVE_MODF 1
#define HAVE_MODFF 1
#define HAVE_POW 1
#define HAVE_POWF 1
#define HAVE_ROUND 1
#define HAVE_ROUNDF 1
#define HAVE_SCALBN 1
#define HAVE_SCALBNF 1
#define HAVE_SIN 1
#define HAVE_SINF 1
#define HAVE_SQRT 1
#define HAVE_SQRTF 1
#define HAVE_TAN 1
#define HAVE_TANF 1
#define HAVE_TRUNC 1
#define HAVE_TRUNCF 1
#define HAVE__FSEEKI64 1
/* #undef HAVE_FOPEN64 */
/* #undef HAVE_FSEEKO */
/* #undef HAVE_FSEEKO64 */
/* #undef HAVE_MEMFD_CREATE */
/* #undef HAVE_POSIX_FALLOCATE */
/* #undef HAVE_SIGACTION */
/* #undef HAVE_SIGTIMEDWAIT */
/* #undef HAVE_SA_SIGACTION */
/* #undef HAVE_ST_MTIM */
/* #undef HAVE_SETJMP */
/* #undef HAVE_NANOSLEEP */
/* #undef HAVE_GMTIME_R */
/* #undef HAVE_LOCALTIME_R */
/* #undef HAVE_NL_LANGINFO */
/* #undef HAVE_SYSCONF */
/* #undef HAVE_SYSCTLBYNAME */
/* #undef HAVE_CLOCK_GETTIME */
/* #undef HAVE_GETPAGESIZE */
/* #undef HAVE_ICONV */
/* #undef SDL_USE_LIBICONV */
/* #undef HAVE_PTHREAD_SETNAME_NP */
/* #undef HAVE_PTHREAD_SET_NAME_NP */
/* #undef HAVE_SEM_TIMEDWAIT */
/* #undef HAVE_GETAUXVAL */
/* #undef HAVE_ELF_AUX_INFO */
/* #undef HAVE_PPOLL */
#define HAVE__EXIT 1
/* #undef HAVE_GETRESUID */
/* #undef HAVE_GETRESGID */
#endif /* HAVE_LIBC */
/* #undef HAVE_DBUS_DBUS_H */
/* #undef HAVE_FCITX */
/* #undef HAVE_IBUS_IBUS_H */
/* #undef HAVE_INOTIFY_INIT1 */
/* #undef HAVE_INOTIFY */
/* #undef HAVE_LIBUSB */
/* #undef HAVE_O_CLOEXEC */
/* #undef HAVE_LINUX_INPUT_H */
/* #undef HAVE_LIBUDEV_H */
/* #undef HAVE_LIBDECOR_H */
/* #undef HAVE_LIBURING_H */
/* #undef HAVE_FRIBIDI_H */
/* #undef SDL_FRIBIDI_DYNAMIC */
/* #undef HAVE_LIBTHAI_H */
/* #undef SDL_LIBTHAI_DYNAMIC */
#define HAVE_DDRAW_H 1
#define HAVE_DSOUND_H 1
#define HAVE_DINPUT_H 1
#define HAVE_XINPUT_H 1
#define HAVE_WINDOWS_GAMING_INPUT_H 1
#define HAVE_GAMEINPUT_H 1
#define HAVE_DXGI_H 1
#define HAVE_DXGI1_5_H 1
#define HAVE_DXGI1_6_H 1
#define HAVE_MMDEVICEAPI_H 1
#define HAVE_TPCSHRD_H 1
#define HAVE_ROAPI_H 1
#define HAVE_SHELLSCALINGAPI_H 1
/* #undef USE_POSIX_SPAWN */
/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR */
/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP */
/* #undef SDL_DISABLE_DLOPEN_NOTES */
/* SDL internal assertion support */
/* #undef SDL_DEFAULT_ASSERT_LEVEL_CONFIGURED */
#ifdef SDL_DEFAULT_ASSERT_LEVEL_CONFIGURED
#define SDL_DEFAULT_ASSERT_LEVEL
#endif
/* Allow disabling of major subsystems */
/* #undef SDL_AUDIO_DISABLED */
/* #undef SDL_VIDEO_DISABLED */
/* #undef SDL_GPU_DISABLED */
/* #undef SDL_RENDER_DISABLED */
/* #undef SDL_CAMERA_DISABLED */
/* #undef SDL_JOYSTICK_DISABLED */
/* #undef SDL_HAPTIC_DISABLED */
/* #undef SDL_HIDAPI_DISABLED */
/* #undef SDL_POWER_DISABLED */
/* #undef SDL_SENSOR_DISABLED */
/* #undef SDL_DIALOG_DISABLED */
/* #undef SDL_THREADS_DISABLED */
/* Enable various audio drivers */
/* #undef SDL_AUDIO_DRIVER_ALSA */
/* #undef SDL_AUDIO_DRIVER_ALSA_DYNAMIC */
/* #undef SDL_AUDIO_DRIVER_OPENSLES */
/* #undef SDL_AUDIO_DRIVER_AAUDIO */
/* #undef SDL_AUDIO_DRIVER_COREAUDIO */
#define SDL_AUDIO_DRIVER_DISK 1
#define SDL_AUDIO_DRIVER_DSOUND 1
#define SDL_AUDIO_DRIVER_DUMMY 1
/* #undef SDL_AUDIO_DRIVER_EMSCRIPTEN */
/* #undef SDL_AUDIO_DRIVER_HAIKU */
/* #undef SDL_AUDIO_DRIVER_JACK */
/* #undef SDL_AUDIO_DRIVER_JACK_DYNAMIC */
/* #undef SDL_AUDIO_DRIVER_NETBSD */
/* #undef SDL_AUDIO_DRIVER_OSS */
/* #undef SDL_AUDIO_DRIVER_PIPEWIRE */
/* #undef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC */
/* #undef SDL_AUDIO_DRIVER_PULSEAUDIO */
/* #undef SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC */
/* #undef SDL_AUDIO_DRIVER_SNDIO */
/* #undef SDL_AUDIO_DRIVER_SNDIO_DYNAMIC */
#define SDL_AUDIO_DRIVER_WASAPI 1
/* #undef SDL_AUDIO_DRIVER_VITA */
/* #undef SDL_AUDIO_DRIVER_PSP */
/* #undef SDL_AUDIO_DRIVER_PS2 */
/* #undef SDL_AUDIO_DRIVER_N3DS */
/* #undef SDL_AUDIO_DRIVER_NGAGE */
/* #undef SDL_AUDIO_DRIVER_QNX */
/* #undef SDL_AUDIO_DRIVER_PRIVATE */
/* Enable various input drivers */
/* #undef SDL_INPUT_LINUXEV */
/* #undef SDL_INPUT_LINUXKD */
/* #undef SDL_INPUT_FBSDKBIO */
/* #undef SDL_INPUT_WSCONS */
/* #undef SDL_HAVE_MACHINE_JOYSTICK_H */
/* #undef SDL_JOYSTICK_ANDROID */
#define SDL_JOYSTICK_DINPUT 1
/* #undef SDL_JOYSTICK_DUMMY */
/* #undef SDL_JOYSTICK_EMSCRIPTEN */
#define SDL_JOYSTICK_GAMEINPUT 1
/* #undef SDL_JOYSTICK_HAIKU */
#define SDL_JOYSTICK_HIDAPI 1
/* #undef SDL_JOYSTICK_IOKIT */
/* #undef SDL_JOYSTICK_LINUX */
/* #undef SDL_JOYSTICK_MFI */
/* #undef SDL_JOYSTICK_N3DS */
/* #undef SDL_JOYSTICK_PS2 */
/* #undef SDL_JOYSTICK_PSP */
#define SDL_JOYSTICK_RAWINPUT 1
/* #undef SDL_JOYSTICK_USBHID */
#define SDL_JOYSTICK_VIRTUAL 1
/* #undef SDL_JOYSTICK_VITA */
#define SDL_JOYSTICK_WGI 1
#define SDL_JOYSTICK_XINPUT 1
/* #undef SDL_JOYSTICK_PRIVATE */
/* #undef SDL_HAPTIC_DUMMY */
/* #undef SDL_HAPTIC_LINUX */
/* #undef SDL_HAPTIC_IOKIT */
#define SDL_HAPTIC_DINPUT 1
/* #undef SDL_HAPTIC_ANDROID */
/* #undef SDL_HAPTIC_PRIVATE */
/* #undef SDL_LIBUSB_DYNAMIC */
/* #undef SDL_UDEV_DYNAMIC */
/* Enable various process implementations */
/* #undef SDL_PROCESS_DUMMY */
/* #undef SDL_PROCESS_POSIX */
#define SDL_PROCESS_WINDOWS 1
/* #undef SDL_PROCESS_PRIVATE */
/* Enable various sensor drivers */
/* #undef SDL_SENSOR_ANDROID */
/* #undef SDL_SENSOR_COREMOTION */
#define SDL_SENSOR_WINDOWS 1
/* #undef SDL_SENSOR_DUMMY */
/* #undef SDL_SENSOR_VITA */
/* #undef SDL_SENSOR_N3DS */
/* #undef SDL_SENSOR_EMSCRIPTEN */
/* #undef SDL_SENSOR_PRIVATE */
/* Enable various shared object loading systems */
/* #undef SDL_LOADSO_DLOPEN */
/* #undef SDL_LOADSO_DUMMY */
#define SDL_LOADSO_WINDOWS 1
/* #undef SDL_LOADSO_PRIVATE */
/* Enable various threading systems */
#define SDL_THREAD_GENERIC_COND_SUFFIX 1
#define SDL_THREAD_GENERIC_RWLOCK_SUFFIX 1
/* #undef SDL_THREAD_PTHREAD */
/* #undef SDL_THREAD_PTHREAD_RECURSIVE_MUTEX */
/* #undef SDL_THREAD_PTHREAD_RECURSIVE_MUTEX_NP */
#define SDL_THREAD_WINDOWS 1
/* #undef SDL_THREAD_VITA */
/* #undef SDL_THREAD_PSP */
/* #undef SDL_THREAD_PS2 */
/* #undef SDL_THREAD_N3DS */
/* #undef SDL_THREAD_PRIVATE */
/* Enable various RTC systems */
/* #undef SDL_TIME_UNIX */
#define SDL_TIME_WINDOWS 1
/* #undef SDL_TIME_VITA */
/* #undef SDL_TIME_PSP */
/* #undef SDL_TIME_PS2 */
/* #undef SDL_TIME_N3DS */
/* #undef SDL_TIME_NGAGE */
/* #undef SDL_TIME_PRIVATE */
/* Enable various timer systems */
/* #undef SDL_TIMER_HAIKU */
/* #undef SDL_TIMER_UNIX */
#define SDL_TIMER_WINDOWS 1
/* #undef SDL_TIMER_VITA */
/* #undef SDL_TIMER_PSP */
/* #undef SDL_TIMER_PS2 */
/* #undef SDL_TIMER_N3DS */
/* #undef SDL_TIMER_PRIVATE */
/* Enable various video drivers */
/* #undef SDL_VIDEO_DRIVER_ANDROID */
/* #undef SDL_VIDEO_DRIVER_COCOA */
#define SDL_VIDEO_DRIVER_DUMMY 1
/* #undef SDL_VIDEO_DRIVER_EMSCRIPTEN */
/* #undef SDL_VIDEO_DRIVER_HAIKU */
/* #undef SDL_VIDEO_DRIVER_KMSDRM */
/* #undef SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC */
/* #undef SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC_GBM */
/* #undef SDL_VIDEO_DRIVER_N3DS */
/* #undef SDL_VIDEO_DRIVER_NGAGE */
#define SDL_VIDEO_DRIVER_OFFSCREEN 1
/* #undef SDL_VIDEO_DRIVER_PS2 */
/* #undef SDL_VIDEO_DRIVER_PSP */
/* #undef SDL_VIDEO_DRIVER_RISCOS */
/* #undef SDL_VIDEO_DRIVER_ROCKCHIP */
/* #undef SDL_VIDEO_DRIVER_RPI */
/* #undef SDL_VIDEO_DRIVER_UIKIT */
/* #undef SDL_VIDEO_DRIVER_VITA */
/* #undef SDL_VIDEO_DRIVER_VIVANTE */
/* #undef SDL_VIDEO_DRIVER_VIVANTE_VDK */
/* #undef SDL_VIDEO_DRIVER_OPENVR */
/* #undef SDL_VIDEO_DRIVER_WAYLAND */
/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC */
/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_CURSOR */
/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_EGL */
/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR */
/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_XKBCOMMON */
#define SDL_VIDEO_DRIVER_WINDOWS 1
/* #undef SDL_VIDEO_DRIVER_X11 */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XCURSOR */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XEXT */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XFIXES */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS */
/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST */
/* #undef SDL_VIDEO_DRIVER_X11_HAS_XKBLIB */
/* #undef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS */
/* #undef SDL_VIDEO_DRIVER_X11_XCURSOR */
/* #undef SDL_VIDEO_DRIVER_X11_XDBE */
/* #undef SDL_VIDEO_DRIVER_X11_XFIXES */
/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2 */
/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */
/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO */
/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE */
/* #undef SDL_VIDEO_DRIVER_X11_XRANDR */
/* #undef SDL_VIDEO_DRIVER_X11_XSCRNSAVER */
/* #undef SDL_VIDEO_DRIVER_X11_XSHAPE */
/* #undef SDL_VIDEO_DRIVER_X11_XSYNC */
/* #undef SDL_VIDEO_DRIVER_X11_XTEST */
/* #undef SDL_VIDEO_DRIVER_QNX */
/* #undef SDL_VIDEO_DRIVER_PRIVATE */
#define SDL_VIDEO_RENDER_D3D 1
#define SDL_VIDEO_RENDER_D3D11 1
#define SDL_VIDEO_RENDER_D3D12 1
#define SDL_VIDEO_RENDER_GPU 1
/* #undef SDL_VIDEO_RENDER_METAL */
#define SDL_VIDEO_RENDER_VULKAN 1
#define SDL_VIDEO_RENDER_OGL 1
#define SDL_VIDEO_RENDER_OGL_ES2 1
/* #undef SDL_VIDEO_RENDER_NGAGE */
/* #undef SDL_VIDEO_RENDER_PS2 */
/* #undef SDL_VIDEO_RENDER_PSP */
/* #undef SDL_VIDEO_RENDER_VITA_GXM */
/* #undef SDL_VIDEO_RENDER_PRIVATE */
/* Enable OpenGL support */
#define SDL_VIDEO_OPENGL 1
/* #undef SDL_VIDEO_OPENGL_ES */
#define SDL_VIDEO_OPENGL_ES2 1
/* #undef SDL_VIDEO_OPENGL_CGL */
/* #undef SDL_VIDEO_OPENGL_GLX */
#define SDL_VIDEO_OPENGL_WGL 1
#define SDL_VIDEO_OPENGL_EGL 1
/* #undef SDL_VIDEO_STATIC_ANGLE */
/* Enable Vulkan support */
#define SDL_VIDEO_VULKAN 1
/* Enable Metal support */
/* #undef SDL_VIDEO_METAL */
/* Enable GPU support */
/* #undef SDL_GPU_D3D11 */
#define SDL_GPU_D3D12 1
#define SDL_GPU_VULKAN 1
/* #undef SDL_GPU_METAL */
/* #undef SDL_GPU_PRIVATE */
/* Enable system power support */
/* #undef SDL_POWER_ANDROID */
/* #undef SDL_POWER_LINUX */
#define SDL_POWER_WINDOWS 1
/* #undef SDL_POWER_MACOSX */
/* #undef SDL_POWER_UIKIT */
/* #undef SDL_POWER_HAIKU */
/* #undef SDL_POWER_EMSCRIPTEN */
/* #undef SDL_POWER_HARDWIRED */
/* #undef SDL_POWER_VITA */
/* #undef SDL_POWER_PSP */
/* #undef SDL_POWER_N3DS */
/* #undef SDL_POWER_PRIVATE */
/* Enable system filesystem support */
/* #undef SDL_FILESYSTEM_ANDROID */
/* #undef SDL_FILESYSTEM_HAIKU */
/* #undef SDL_FILESYSTEM_COCOA */
/* #undef SDL_FILESYSTEM_DUMMY */
/* #undef SDL_FILESYSTEM_RISCOS */
/* #undef SDL_FILESYSTEM_UNIX */
#define SDL_FILESYSTEM_WINDOWS 1
/* #undef SDL_FILESYSTEM_EMSCRIPTEN */
/* #undef SDL_FILESYSTEM_VITA */
/* #undef SDL_FILESYSTEM_PSP */
/* #undef SDL_FILESYSTEM_PS2 */
/* #undef SDL_FILESYSTEM_N3DS */
/* #undef SDL_FILESYSTEM_PRIVATE */
/* Enable system storage support */
#define SDL_STORAGE_STEAM 1
/* #undef SDL_STORAGE_PRIVATE */
/* Enable system FSops support */
/* #undef SDL_FSOPS_POSIX */
#define SDL_FSOPS_WINDOWS 1
/* #undef SDL_FSOPS_DUMMY */
/* #undef SDL_FSOPS_PRIVATE */
/* Enable camera subsystem */
#define SDL_CAMERA_DRIVER_DUMMY 1
/* !!! FIXME: for later cmakedefine SDL_CAMERA_DRIVER_DISK 1 */
/* #undef SDL_CAMERA_DRIVER_V4L2 */
/* #undef SDL_CAMERA_DRIVER_COREMEDIA */
/* #undef SDL_CAMERA_DRIVER_ANDROID */
/* #undef SDL_CAMERA_DRIVER_EMSCRIPTEN */
#define SDL_CAMERA_DRIVER_MEDIAFOUNDATION 1
/* #undef SDL_CAMERA_DRIVER_PIPEWIRE */
/* #undef SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC */
/* #undef SDL_CAMERA_DRIVER_VITA */
/* #undef SDL_CAMERA_DRIVER_PRIVATE */
/* Enable dialog subsystem */
/* #undef SDL_DIALOG_DUMMY */
/* Enable tray subsystem */
/* #undef SDL_TRAY_DUMMY */
/* Enable assembly routines */
/* #undef SDL_ALTIVEC_BLITTERS */
/* Whether SDL_DYNAMIC_API needs dlopen */
/* #undef DYNAPI_NEEDS_DLOPEN */
/* Enable ime support */
/* #undef SDL_USE_IME */
/* #undef SDL_DISABLE_WINDOWS_IME */
/* #undef SDL_GDK_TEXTINPUT */
/* Platform specific definitions */
/* #undef SDL_IPHONE_KEYBOARD */
/* #undef SDL_IPHONE_LAUNCHSCREEN */
/* #undef SDL_VIDEO_VITA_PIB */
/* #undef SDL_VIDEO_VITA_PVR */
/* #undef SDL_VIDEO_VITA_PVR_OGL */
/* #undef SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING */
/* xkbcommon version info */
#define SDL_XKBCOMMON_VERSION_MAJOR
#define SDL_XKBCOMMON_VERSION_MINOR
#define SDL_XKBCOMMON_VERSION_PATCH
/* Libdecor version info */
#define SDL_LIBDECOR_VERSION_MAJOR
#define SDL_LIBDECOR_VERSION_MINOR
#define SDL_LIBDECOR_VERSION_PATCH
#if !defined(HAVE_STDINT_H) && !defined(_STDINT_H_)
/* Most everything except Visual Studio 2008 and earlier has stdint.h now */
#if defined(_MSC_VER) && (_MSC_VER < 1600)
typedef signed __int8 int8_t;
typedef unsigned __int8 uint8_t;
typedef signed __int16 int16_t;
typedef unsigned __int16 uint16_t;
typedef signed __int32 int32_t;
typedef unsigned __int32 uint32_t;
typedef signed __int64 int64_t;
typedef unsigned __int64 uint64_t;
#ifndef _UINTPTR_T_DEFINED
#ifdef _WIN64
typedef unsigned __int64 uintptr_t;
#else
typedef unsigned int uintptr_t;
#endif
#endif
#endif /* Visual Studio 2008 */
#endif /* !_STDINT_H_ && !HAVE_STDINT_H */
/* Configure use of intrinsics */
/* #undef SDL_DISABLE_SSE */
/* #undef SDL_DISABLE_SSE2 */
/* #undef SDL_DISABLE_SSE3 */
/* #undef SDL_DISABLE_SSE4_1 */
/* #undef SDL_DISABLE_SSE4_2 */
/* #undef SDL_DISABLE_AVX */
/* #undef SDL_DISABLE_AVX2 */
/* #undef SDL_DISABLE_AVX512F */
#define SDL_DISABLE_MMX 1
#define SDL_DISABLE_LSX 1
#define SDL_DISABLE_LASX 1
#define SDL_DISABLE_NEON 1
#ifdef SDL_PLATFORM_PRIVATE
#include "SDL_end_config_private.h"
#endif
#endif /* SDL_build_config_h_ */
+5 -4
View File
@@ -312,10 +312,11 @@ sub generateImguiGeneric {
push(@before, "UINT_ARG($name)");
}
push(@funcArgs, $name);
#ImTextureID or const ImTextureID&
# const ImTextureID& is the same thing as var
# as lua is concerned
} elsif ($args[$i] =~ m/^ *(ImTextureID|const ImTextureID&) ([^ =\[]*) *$/) {
#ImTextureID, const ImTextureID&, or ImTextureRef (ImGui v1.92+)
# const ImTextureID& is the same thing as ImTextureID as far as lua is concerned.
# ImTextureRef in v1.92 wraps an ImTextureID and constructs implicitly from one,
# so we pull the Lua arg as ImTextureID and let C++ wrap it at the call site.
} elsif ($args[$i] =~ m/^ *(ImTextureID|const ImTextureID&|ImTextureRef) ([^ =\[]*) *$/) {
my $name = $2;
push(@before, "IM_TEXTURE_ID_ARG($name)");
push(@funcArgs, $name);
File diff suppressed because it is too large Load Diff
Submodule third_party/miniaudio deleted from 4a5b74bef0
-69
View File
@@ -1,69 +0,0 @@
# Dockerfile for ghcr.io/grumpycoders/pcsx-redux-build-cross
# IMPORTANT: to build this file, the package qemu-user-static needs to be
# installed on the host machine.
FROM debian:bookworm-slim
# The tzdata package isn't docker-friendly, and something pulls it.
ENV DEBIAN_FRONTEND noninteractive
ENV TZ Etc/GMT
RUN apt update
RUN apt dist-upgrade -y
# Utility packages
RUN apt install -y git
RUN apt install -y make
RUN apt install -y pkg-config
# Compilers & base libraries
RUN apt install -y g++-10
RUN apt install -y gcc-10
RUN apt install -y gcc
# CI utilities
RUN apt install -y curl wget
RUN apt install -y python-is-python3
# Cross Compiler stuff
RUN apt install -y debootstrap
RUN apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
RUN apt install -y qemu-user-static
WORKDIR /
# Create directory to hold aarch64 sysroot
RUN mkdir -p /opt/cross/sysroot
# Run debootstrap to pull down Debian bookworm aarch64 rootfs
RUN debootstrap --arch=arm64 --variant=minbase \
--include=\
build-essential,\
pkg-config,\
libavcodec-dev,\
libavformat-dev,\
libavutil-dev,\
libcapstone-dev,\
libcurl4-openssl-dev,\
libfreetype-dev,\
libglfw3-dev,\
libswresample-dev,\
libuv1-dev,\
zlib1g-dev --foreign bookworm /opt/cross/sysroot
# Copy qemu-user-static over to sysroot so second stage can run
RUN cp /usr/bin/qemu-aarch64-static /opt/cross/sysroot/usr/bin/
# chroot into aarch64 sysroot and run debootstrap second stage to unpack packages and set up symlinks
RUN chroot /opt/cross/sysroot /usr/bin/qemu-aarch64-static /bin/sh -c "/debootstrap/debootstrap --second-stage"
# Fix symlinks in the sysroot
WORKDIR /opt/cross
RUN wget https://raw.githubusercontent.com/abhiTronix/rpi_rootfs/master/scripts/sysroot-relativelinks.py
RUN chmod +x sysroot-relativelinks.py
RUN ./sysroot-relativelinks.py sysroot
RUN mkdir /project
RUN mkdir -p /home/coder/dconf
RUN chmod a+rwx /home/coder/dconf
WORKDIR /project
+62 -11
View File
@@ -1,6 +1,27 @@
# Dockerfile for ghcr.io/grumpycoders/pcsx-redux-build
#
# This one needs the repository root as its build context, as the first stage
# builds the mips toolchain using tools/linux-mips/spawn-compiler.sh:
#
# docker build -f tools/build/Dockerfile -t ghcr.io/grumpycoders/pcsx-redux-build .
FROM ubuntu:24.04
FROM ubuntu:26.04 AS toolchain
ENV DEBIAN_FRONTEND noninteractive
ENV TZ Etc/GMT
RUN apt update
RUN apt dist-upgrade -y
RUN apt install -y make g++ wget bzip2 xz-utils bison flex texinfo
RUN apt install -y libgmp-dev libmpfr-dev libmpc-dev
ARG JOBS=
WORKDIR /tmp/toolchain
COPY tools/linux-mips/spawn-compiler.sh .
RUN JOBS=$JOBS sh spawn-compiler.sh
FROM ubuntu:26.04
# The tzdata package isn't docker-friendly, and something pulls it.
ENV DEBIAN_FRONTEND noninteractive
@@ -16,7 +37,6 @@ RUN apt install -y pkg-config
# Compilers & base libraries
RUN apt install -y g++
RUN apt install -y g++-mipsel-linux-gnu
# Development packages
RUN apt install -y libavcodec-dev
@@ -25,7 +45,7 @@ RUN apt install -y libavutil-dev
RUN apt install -y libcapstone-dev
RUN apt install -y libcurl4-openssl-dev
RUN apt install -y libfreetype-dev
RUN apt install -y libglfw3-dev
RUN apt install -y libsdl3-dev
RUN apt install -y libswresample-dev
RUN apt install -y libuv1-dev
RUN apt install -y zlib1g-dev
@@ -33,7 +53,7 @@ RUN apt install -y zlib1g-dev
# CI utilities
RUN apt install -y curl wget
RUN apt install -y xvfb
RUN apt install -y python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace
RUN apt install -y python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf-2.0-dev fakeroot strace
RUN apt install -y glibc-tools lcov
RUN apt install -y file
@@ -42,18 +62,49 @@ RUN apt install -y libfuse2t64
RUN apt install -y fuse
# AppImage tools
RUN apt install -y imagemagick-6.q16 gtk-update-icon-cache appstream
RUN apt install -y imagemagick-7.q16 gtk-update-icon-cache appstream
RUN apt install -y squashfs-tools zip zsync
# The mips toolchain, built from source in the first stage. Ubuntu dropped the
# mipsel cross compilers, so there's no distribution package to install here.
COPY --from=toolchain /usr/local /usr/local
# Both projects name their releases after uname's spelling of the architecture,
# which is also the one the AppImage ends up carrying.
WORKDIR /tmp
RUN wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage && \
chmod +x appimagetool-x86_64.AppImage && \
mv appimagetool-x86_64.AppImage /usr/bin/appimagetool
RUN wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && \
chmod +x linuxdeploy-x86_64.AppImage && \
mv linuxdeploy-x86_64.AppImage /usr/bin/linuxdeploy
RUN wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-`uname -m`.AppImage && \
chmod +x appimagetool-`uname -m`.AppImage && \
mv appimagetool-`uname -m`.AppImage /usr/bin/appimagetool
RUN wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-`uname -m`.AppImage && \
chmod +x linuxdeploy-`uname -m`.AppImage && \
mv linuxdeploy-`uname -m`.AppImage /usr/bin/linuxdeploy
# xmake resolves fmt from its own repository, which needs cmake and ninja to
# build it. Without these it fetches and builds cmake from source on every clean
# run, which is by far the slowest thing in the job.
RUN apt install -y cmake ninja-build
# xmake, built from source rather than dropped in as a release binary. There is
# no prebuilt linux/arm64 build published at all, and the x86_64 one wants
# libncurses, which isn't in here. The source build only needs make and g++,
# works the same on both architectures, and this image is rebuilt rarely enough
# that the couple of minutes are free.
ARG XMAKE_VERSION=3.1.1
RUN wget https://github.com/xmake-io/xmake/releases/download/v$XMAKE_VERSION/xmake-v$XMAKE_VERSION.tar.gz && \
tar xzf xmake-v$XMAKE_VERSION.tar.gz && \
cd xmake-$XMAKE_VERSION && \
./configure && \
make -j `nproc` && \
make install PREFIX=/usr/local && \
cd /tmp && \
rm -rf xmake-$XMAKE_VERSION xmake-v$XMAKE_VERSION.tar.gz
# Links the package to the repository, so its permissions follow from there
# rather than from a settings page somebody has to remember.
LABEL org.opencontainers.image.source=https://github.com/grumpycoders/pcsx-redux
RUN mkdir /project
ENV HOME=/home/coder
RUN mkdir -p /home/coder/dconf
RUN chmod a+rwx /home/coder/dconf
-46
View File
@@ -1,46 +0,0 @@
# Dockerfile for grumpycoders/pcsx-redux-code-server
FROM codercom/code-server:3.4.0
USER root
# The tzdata package isn't docker-friendly, and something pulls it.
ENV DEBIAN_FRONTEND noninteractive
ENV TZ Etc/GMT
RUN apt update
# Utility packages
RUN apt install -y git
RUN apt install -y gnupg
RUN apt install -y make
RUN apt install -y pkg-config
RUN apt install -y wget
# Clang setup
RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
RUN apt update
RUN apt install -y software-properties-common
RUN apt-add-repository "deb http://apt.llvm.org/buster/ llvm-toolchain-buster-10 main"
RUN apt update
# Compilers & base libraries
RUN apt install -y clang-10
RUN apt install -y g++-8
RUN apt install -y g++-mipsel-linux-gnu
# Development packages
RUN apt install -y libavcodec-dev
RUN apt install -y libavformat-dev
RUN apt install -y libavutil-dev
RUN apt install -y libglfw3-dev
RUN apt install -y libsdl2-dev
RUN apt install -y libswresample-dev
RUN apt install -y libuv1-dev
RUN apt install -y zlib1g-dev
USER coder
ENV CC clang-10
ENV CXX clang++-10
ENV LD clang++-10

Some files were not shown because too many files have changed in this diff Show More