Merge remote-tracking branch 'origin/spu-accuracy' into fuckit

gte.cc took the deletion, since the GTE got rewritten and split upstream.
cdrom.cc took this branch's rewrite whole and had the two things main grew
since re-applied by hand: the magic_enum include path, and the CDRomLogger
data/seek hooks. There is no audio hook because CDDA playback isn't here yet.
This commit is contained in:
Nicolas 'Pixel' Noble
2026-09-04 23:24:44 -07:00
689 changed files with 125781 additions and 12856 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
+7 -3
View File
@@ -10,8 +10,8 @@ cd $CWD
# Need to delete submodules actively.
# Done in two passes for speed.
git filter-branch -f --tree-filter 'find third_party -depth -not -name uC-sdk -and -not -path third_party/psxlua -and -not -path third_party/EABase -and -not -path third_party/EABase/\* -and -not -path third_party/EASTL -and -not -path third_party/EASTL/\* -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -depth -not -name uC-sdk -and -not -path third_party/psxlua -and -not -path third_party/EABase -and -not -path third_party/EABase/\* -and -not -path third_party/EASTL -and -not -path third_party/EASTL/\* -exec git rm -f {} \; || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -depth -not -name uC-sdk -and -not -path third_party/psxlua -and -not -path third_party/EABase -and -not -path third_party/EABase/\* -and -not -path third_party/EASTL -and -not -path third_party/EASTL/\* -and -not -path third_party/xmake-psx -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -depth -not -name uC-sdk -and -not -path third_party/psxlua -and -not -path third_party/EABase -and -not -path third_party/EABase/\* -and -not -path third_party/EASTL -and -not -path third_party/EASTL/\* -and -not -path third_party/xmake-psx -exec git rm -f {} \; || true' --tag-name-filter cat --prune-empty
# Shuffle files around.
git filter-branch -f --tree-filter 'mkdir -p src/mips ; git mv third_party src/mips ; mv .gitmodules src/mips ; mv docker* src/mips || true' --tag-name-filter cat --prune-empty
@@ -38,5 +38,9 @@ git filter-branch -f --tree-filter "find . -name '*.mk' -exec sed 's|\.\./\.\./\
git filter-branch -f --tree-filter "find . -name Makefile -exec sed 's|\.\./\.\./\.\./third_party/psxlua/|../third_party/psxlua/|' -i {} \;" --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter "find . -name '*.mk' -exec sed 's|\.\./\.\./\.\./third_party/psxlua/|../third_party/psxlua/|' -i {} \;" --tag-name-filter cat --prune-empty
# Adjust paths for xmake
git filter-branch -f --tree-filter "find . -name xmake.lua -exec sed 's|local root = path.join(os.scriptdir(), \"\.\.\", \"\.\.\"|local root = path.join(os.scriptdir()|' -i {} \;" --tag-name-filter cat --prune-empty
# Delete unwanted files
git filter-branch -f --tree-filter 'find . -name compile_flags.txt -or -name Doxyfile -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find . -name compile_flags.txt -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find . -name Doxyfile -delete || true' --tag-name-filter cat --prune-empty
+2 -6
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)
@@ -85,7 +81,7 @@ SRCS += $(wildcard third_party/cueparser/*.c)
SRCS += $(wildcard third_party/iec-60908b/*.c)
SRCS += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c
TOOLS = authoring exe2elf exe2iso modconv ps1-packer psyq-obj-parser
TOOLS = authoring exe2elf exe2exe exe2iso midi2psm midi2spd modconv ps1-packer psyq-obj-parser
##############################################################################
+3 -3
View File
@@ -20,11 +20,11 @@ git filter-branch -f --tree-filter 'rm -rf .github hardware i18n resources .vsco
# Need to delete submodules actively.
# Done in two passes for speed.
git filter-branch -f --tree-filter 'find third_party -maxdepth 1 -type d -and -not -name third_party -and -not -path third_party/cueparser* -and -not -path third_party/ELFIO\* -and -not -path third_party/expected\* -and -not -path third_party/fmt\* -and -not -path third_party/googletest\* -and -not -path third_party/iec-60908b\* -and -not -path third_party/magic_enum\* -and -not -path third_party/ucl\* -exec rm -rf {} \; || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -maxdepth 1 -type d -and -not -name third_party -and -not -path third_party/cueparser* -and -not -path third_party/ELFIO\* -and -not -path third_party/expected\* -and -not -path third_party/fmt\* -and -not -path third_party/googletest\* -and -not -path third_party/iec-60908b\* -and -not -path third_party/magic_enum\* -and -not -path third_party/ucl\* -exec git rm -f {} \; || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -maxdepth 1 -type d -and -not -name third_party -and -not -path third_party/cueparser* -and -not -path third_party/ELFIO\* -and -not -path third_party/expected\* -and -not -path third_party/fmt\* -and -not -path third_party/googletest\* -and -not -path third_party/iec-60908b\* -and -not -path third_party/magic_enum\* -and -not -path third_party/PEGTL\* -and -not -path third_party/ucl\* -exec rm -rf {} \; || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find third_party -maxdepth 1 -type d -and -not -name third_party -and -not -path third_party/cueparser* -and -not -path third_party/ELFIO\* -and -not -path third_party/expected\* -and -not -path third_party/fmt\* -and -not -path third_party/googletest\* -and -not -path third_party/iec-60908b\* -and -not -path third_party/magic_enum\* -and -not -path third_party/PEGTL\* -and -not -path third_party/ucl\* -exec git rm -f {} \; || true' --tag-name-filter cat --prune-empty
# Delete ffmpeg, versionning, Lua, and libuv-related source code
git filter-branch -f --tree-filter 'find src -type f -name \*lua\* -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find src -type f -name \*lua\* -and -not -name xmake.lua -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find src -type f -name assembler.\* -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find src -type f -name ffmpeg\* -delete || true' --tag-name-filter cat --prune-empty
git filter-branch -f --tree-filter 'find src -type f -name uv\* -delete || true' --tag-name-filter cat --prune-empty
+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
@@ -4,6 +4,10 @@ on:
pull_request:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
nix-matrix:
runs-on: ubuntu-latest
@@ -27,8 +31,5 @@ jobs:
matrix: ${{fromJSON(needs.nix-matrix.outputs.matrix)}}
steps:
- uses: actions/checkout@v4
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: cachix/install-nix-action@v30
- run: nix build -L '.#${{ matrix.attr }}'
+8 -16
View File
@@ -5,18 +5,19 @@ on:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
regen-i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install gettext
run: sudo apt-get install -y gettext
- name: Regenerate i18n files
@@ -41,13 +42,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Format code
run: ./tools/code-format/format.sh
- name: Create Pull Request
@@ -68,12 +66,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install doxygen
run: sudo apt-get install -y doxygen graphviz
- name: Filter repository
@@ -112,12 +107,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Filter repository
env:
FILTER_BRANCH_SQUELCH_WARNING: 1
+11 -4
View File
@@ -5,6 +5,11 @@ on:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
asan:
@@ -15,13 +20,10 @@ jobs:
TEST_RESULTS: /tmp/test-results
BUILD: asan
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
make -j 2 all pcsx-redux-tests
make -C src/mips/openbios -j 2 BUILD=Release
@@ -30,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
+8 -3
View File
@@ -6,6 +6,10 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
aur-build:
runs-on: ubuntu-latest
@@ -14,14 +18,15 @@ 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
- uses: actions/checkout@v3
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
- name: Create builduser
run: |
useradd builduser -m
passwd -d builduser
echo "builduser ALL=(ALL) NOPASSWD: ALL" | tee /etc/sudoers.d/builduser && chmod 440 /etc/sudoers.d/builduser
- name: Build AUR Package
run: |
mkdir /build
@@ -29,4 +34,4 @@ jobs:
chown -R builduser:builduser /build
cd /build/pcsx-redux-git
sed -i s,git+https://github.com/grumpycoders/pcsx-redux.git,git+file://$GITHUB_WORKSPACE#commit=$GITHUB_SHA,g PKGBUILD
sudo -u builduser makepkg
sudo -u builduser bash -c 'makepkg --noconfirm --syncdeps'
+32 -14
View File
@@ -5,23 +5,37 @@ on:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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
env:
TEST_RESULTS: /tmp/test-results
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
wget https://github.com/JonathanDotCel/unirom8_bootdisc_and_firmware_for_ps1/releases/download/8.0.K/UNIROM_BOOTDISC_8.0.K.zip
- run: |
@@ -29,6 +43,9 @@ jobs:
- run: |
make -j 2 all pcsx-redux-tests tools
make -C src/mips/tests -j 2 PCSX_TESTS=true
# The tests above leave third_party/uC-sdk objects built with their own
# flags, and openbios links a different subset of that same libc. Without
# this clean it fails on undefined malloc/free/write. Keep it.
make -C src/mips/openbios -j 2 clean all
- name: Install node
uses: actions/setup-node@v4
@@ -36,12 +53,13 @@ jobs:
node-version: '22.6.0'
- name: Install jq
run: |
apt update
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
@@ -57,15 +75,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
- run: |
make -C src/mips/openbios clean
make -C src/mips/tests -j 2 PCSX_TESTS=true
@@ -80,11 +98,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
+6 -5
View File
@@ -6,6 +6,10 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
coverage:
runs-on: ubuntu-latest
@@ -14,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: |
@@ -25,13 +29,10 @@ jobs:
tar xf lcov-2.0.tar.gz
cd lcov-2.0
make install
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
wget https://github.com/JonathanDotCel/unirom8_bootdisc_and_firmware_for_ps1/releases/download/8.0.K/UNIROM_BOOTDISC_8.0.K.zip
- run: |
-31
View File
@@ -1,31 +0,0 @@
name: Linux CI cross
on:
push:
branches:
- main
pull_request:
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@v3
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: |
make -j 2 all pcsx-redux-tests
+5 -4
View File
@@ -6,17 +6,18 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
toolchain:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update packages registry
run: sudo apt-get update
- name: Install pre-reqs
+9 -141
View File
@@ -6,21 +6,20 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-openbios:
runs-on: ubuntu-latest
container:
image: ghcr.io/grumpycoders/pcsx-redux-build:latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- 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
@@ -38,139 +37,11 @@ jobs:
name: tests
path: '**/*.ps-exe'
macos-intel-build-and-test:
runs-on: macos-13
needs: build-openbios
steps:
- uses: actions/checkout@v1
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: ./.github/scripts/install-brew-dependencies.sh
- name: Fetch submodules
run: git submodule update --init --recursive
- name: Build PCSX-Redux
run: make -j 2 all pcsx-redux-tests
- name: Download OpenBIOS build
uses: actions/download-artifact@v4
with:
name: OpenBIOS
- name: Download mips tests
uses: actions/download-artifact@v4
with:
name: tests
- name: Bundle
run: ./.github/scripts/create-app.sh
- name: Create BUILD environment
run: echo "BUILD=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Injecting OpenBIOS
run: cp openbios.bin PCSX-Redux.app/Contents/Resources/share/pcsx-redux/resources
- name: Install node
uses: actions/setup-node@v4
with:
node-version: '22.6.0'
- name: Install jq
run: |
brew install jq
- name: Packaging
env:
APPDISTRIB_TOKEN: ${{ secrets.APPDISTRIB_DEV_MACOS_X64 }}
APPDISTRIB_ORGANIZATION: pcsx-redux
APPDISTRIB_PROJECT: dev-macos-x64
run: |
npx @appdistrib/cli buildid > buildid.json
echo '{' > version.json
echo ' "version": "'`git rev-parse HEAD | cut -c 1-8`'",' >> version.json
echo ' "buildId": '`jq -r .buildid buildid.json`',' >> version.json
echo ' "changeset": "'`git rev-parse HEAD`'",' >> version.json
echo ' "timestamp": '`date '+%s'`',' >> version.json
echo ' "channel": "dev",' >> version.json
echo ' "updateInfo": [' >> version.json
echo ' {' >> version.json
echo ' "channel": "dev",' >> version.json
echo ' "method": "appdistrib",' >> version.json
echo ' "updateCatalog": "https://distrib.app/storage/manifests/pcsx-redux/dev-macos-x64/manifest.json",' >> version.json
echo ' "updateInfoBase": "https://distrib.app/storage/manifests/pcsx-redux/dev-macos-x64/",' >> version.json
echo ' "updateStorageUrl": "https://distrib.app/"' >> version.json
echo ' }' >> version.json
echo ' ]' >> version.json
echo '}' >> version.json
cp version.json PCSX-Redux.app/Contents/Resources/share/pcsx-redux/resources/version.json
- name: Signing Application
run: codesign --force -s - -vvvv PCSX-Redux.app
- name: Adjusting for dmg folder
run: |
mkdir dmgdir
mv PCSX-Redux.app dmgdir
cp dmgdir/PCSX-Redux.app/Contents/Resources/AppIcon.icns .
- name: Creating dmg icon
run: |
sips -i AppIcon.icns
DeRez -only icns AppIcon.icns > icns.rsrc
cp AppIcon.icns dmgdir/.VolumeIcon.icns
SetFile -c icnC dmgdir/.VolumeIcon.icns
SetFile -a C dmgdir
- name: Creating Application link
run: ln -s /Applications dmgdir
- name: Creating dmg file
run: hdiutil create -volname PCSX-Redux-$BUILD -srcfolder dmgdir -ov -format UDZO PCSX-Redux-$BUILD-Intel.dmg
- name: Applying icon to dmg file
run: |
Rez -append icns.rsrc -o PCSX-Redux-$BUILD-Intel.dmg
SetFile -a C PCSX-Redux-$BUILD-Intel.dmg
- name: Removing temporary dmg folder
run: rm -rf dmgdir
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: Dmg-intel
path: '**/*.dmg'
- name: Upload version
uses: actions/upload-artifact@v4
with:
name: Version-intel
path: version.json
publish-intel-app:
runs-on: ubuntu-latest
needs: macos-intel-build-and-test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v1
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install node
uses: actions/setup-node@v4
with:
node-version: '22.6.0'
- name: Create BUILD environment
run: echo "BUILD=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Download DMG
uses: actions/download-artifact@v4
with:
name: Dmg-intel
- name: Download version
uses: actions/download-artifact@v4
with:
name: Version-intel
- name: Distribute app
env:
APPDISTRIB_TOKEN: ${{ secrets.APPDISTRIB_DEV_MACOS_X64 }}
APPDISTRIB_ORGANIZATION: pcsx-redux
APPDISTRIB_PROJECT: dev-macos-x64
run: |
npx @appdistrib/cli -l buildId -m version.json upload PCSX-Redux-$BUILD-Intel.dmg
macos-arm-build-and-test:
runs-on: macos-14
needs: build-openbios
steps:
- uses: actions/checkout@v1
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- name: Install dependencies
run: ./.github/scripts/install-brew-dependencies.sh
- name: Fetch submodules
@@ -262,10 +133,7 @@ jobs:
needs: macos-arm-build-and-test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v1
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v4
with:
+5 -4
View File
@@ -6,19 +6,20 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
macos-build-and-test-toolchain:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
set-safe-directory: true
- uses: n1hility/cancel-previous-runs@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: install dependencies
run: brew install --verbose nikitabobko/tap/brew-install-path
- name: build mips binutils
+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
+16 -1
View File
@@ -39,12 +39,20 @@ deps/
objs/
bins/
/pcsx-redux
/pcsx-redux-tests
/authoring
/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
@@ -135,4 +143,11 @@ luacov.stats.out
/appimage-build
/AppDir
/*.AppImage
/*.AppImage.zsync
/*.AppImage.zsync
# xmake
.xmake/
build/
# gtest objects are built next to their sources
tests/**/*.o
+11 -5
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
@@ -84,7 +81,7 @@
url = https://github.com/grumpycoders/nanosvg
[submodule "third_party/luafilesystem"]
path = third_party/luafilesystem
url = https://github.com/lunarmodules/luafilesystem.git
url = https://github.com/nicolasnoble/luafilesystem.git
[submodule "third_party/luacov"]
path = third_party/luacov
url = https://github.com/grumpycoders/luacov.git
@@ -100,3 +97,12 @@
[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
[submodule "third_party/implot"]
path = third_party/implot
url = https://github.com/epezent/implot.git
+306 -151
View File
@@ -4,218 +4,373 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch OpenBIOS",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch OpenBIOS",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/openbios/openbios.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/openbios/openbios.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset halt",
"load ./src/mips/openbios/openbios.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset halt"
},
{
"text": "load ./src/mips/openbios/openbios.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch Cube",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch Cube",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/cube/cube.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/cube/cube.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/cube/cube.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/cube/cube.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch modplayer demo",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch modplayer demo",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/modplayer/modplay-demo.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/modplayer/modplay-demo.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/modplayer/modplay-demo.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/modplayer/modplay-demo.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch shell",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch shell",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/shell/shell.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/shell/shell.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/shell/shell.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/shell/shell.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch Basic Test",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch Basic Test",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/tests/basic/basic.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/tests/basic/basic.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/tests/basic/basic.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/tests/basic/basic.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch libc Test",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch libc Test",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/tests/libc/libc.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/tests/libc/libc.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/tests/libc/libc.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/tests/libc/libc.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch cpu Test",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch cpu Test",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/tests/cpu/cpu.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/tests/cpu/cpu.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set substitute-path /project .",
"monitor reset shellhalt",
"load ./src/mips/tests/cpu/cpu.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/tests/cpu/cpu.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch Tetris",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch Tetris",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/psyqo/examples/tetris/tetris.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/psyqo/examples/tetris/tetris.elf",
"gdbpath": "/usr/bin/gdb-multiarch",
"windows": {
"gdbpath": "C:/gdb-multiarch/bin/gdb.exe",
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set confirm off",
"set substitute-path /project .",
"load ./src/mips/openbios/openbios.elf",
"add-symbol-file ./src/mips/openbios/openbios.elf",
"monitor reset shellhalt",
"load ./src/mips/psyqo/examples/tetris/tetris.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set confirm off"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "load ./src/mips/openbios/openbios.elf"
},
{
"text": "add-symbol-file ./src/mips/openbios/openbios.elf"
},
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/psyqo/examples/tetris/tetris.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
"type": "gdb",
"request": "attach",
"name": "(gdb) Launch Torus demo",
"target": "localhost:3333",
"remote": true,
"name": "(cppdbg) Launch Torus demo",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/src/mips/psyqo/examples/torus/torus.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"targetArchitecture": "mips",
"miDebuggerPath": "gdb-multiarch",
"miDebuggerServerAddress": "localhost:3333",
"stopAtConnect": true,
"executable": "./src/mips/psyqo/examples/torus/torus.elf",
"gdbpath": "gdb-multiarch",
"windows": {
"gdbpath": "gdb-multiarch.exe"
"miDebuggerPath": "gdb-multiarch.exe"
},
"cwd": "${workspaceRoot}",
"autorun": [
"set confirm off",
"set substitute-path /project .",
"load ./src/mips/openbios/openbios.elf",
"add-symbol-file ./src/mips/openbios/openbios.elf",
"monitor reset shellhalt",
"load ./src/mips/psyqo/examples/torus/torus.elf",
"tbreak main",
"continue",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "cd ${workspaceFolder}"
},
{
"text": "set confirm off"
},
{
"text": "set substitute-path /project ."
}
],
"valuesFormatting": "parseText"
"postRemoteConnectCommands": [
{
"text": "load ./src/mips/openbios/openbios.elf"
},
{
"text": "add-symbol-file ./src/mips/openbios/openbios.elf"
},
{
"text": "monitor reset shellhalt"
},
{
"text": "load ./src/mips/psyqo/examples/torus/torus.elf"
}
],
"launchCompleteCommand": "None",
"visualizerFile": "${workspaceFolder}/src/mips/psyqo/psyqo.natvis",
"showDisplayString": true
},
{
+2 -1
View File
@@ -53,7 +53,7 @@ PCSX
----
Linuzappz
Shadow
Pete Bernett
Pete Bernert
NoComp
Nik3d
Akumax
@@ -74,6 +74,7 @@ Extra code
smf
iCatButler
Neill Corlett
Chris Moeller / kode54
Vincent "Yaz0r" Hamm
Dokta Jigokusai / Dr. Hell
-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
+16 -13
View File
@@ -1,21 +1,22 @@
R"(
PCSX-Redux, Copyright (C) 2019-2025 [PCSX-Redux authors](AUTHORS)
PCSX-Redux comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
PCSX-Redux, Copyright (C) 2019-2025 [PCSX-Redux authors](AUTHORS)
PCSX-Redux comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. See [the license](https://github.com/grumpycoders/pcsx-redux/blob/main/LICENSE) for details.
OpenBIOS, Copyright (C) 2019-2025 [PCSX-Redux authors](AUTHORS)
OpenBIOS comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
OpenBIOS, Copyright (C) 2019-2025 [PCSX-Redux authors](AUTHORS)
OpenBIOS comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. See [the license](https://github.com/grumpycoders/pcsx-redux/blob/main/src/mips/LICENSE) for details.
PCSX-Redux uses many third party libraries to function,
each with its own license. The following is an alphabetical
list of the libraries used by PCSX-Redux. Some of them have been
vendored with modifications. If this is the case, you will also
PCSX-Redux uses many third party libraries to function,
each with its own license. The following is an alphabetical
list of the libraries used by PCSX-Redux. Some of them have been
vendored with modifications. If this is the case, you will also
find a link to the vendored version itself.
- [arith64](https://github.com/glitchub/arith64) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/arith64))
- [Capstone](https://github.com/capstone-engine/capstone)
- [Clip](https://github.com/dacap/clip) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/clip))
- [CompletionQueue](https://github.com/bhhbazinga/ConcurrentQueue) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/cq))
@@ -28,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))
@@ -36,24 +37,26 @@ find a link to the vendored version itself.
- [imgui_lua_bindings](https://github.com/patrickriordan/imgui_lua_bindings) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/imgui_lua_bindings))
- [imgui_md](https://github.com/mekhontsev/imgui_md)
- [imgui_memory_editor](https://github.com/ocornut/imgui_memory_editor) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/imgui_memory_editor))
- [ImPlot](https://github.com/epezent/implot)
- [json](https://github.com/nlohmann/json) ([vendored](https://github.com/grumpycoders/pcsx-redux/blob/main/third_party/json.hpp))
- [libcester](https://github.com/exoticlibraries/libcester)
- [libuv](https://github.com/libuv/libuv)
- [llhttp](https://github.com/nodejs/llhttp) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/llhttp))
- [LPeg](http://www.inf.puc-rio.br/~roberto/lpeg/)
- [lua-protobuf](https://github.com/starwing/lua-protobuf) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/lua-protobuf))
- [luafilesystem](https://github.com/lunarmodules/luafilesystem)
- [luafilesystem](https://github.com/lunarmodules/luafilesystem) ([vendored](https://github.com/nicolasnoble/luafilesystem))
- [LuaJIT](https://github.com/LuaJIT/LuaJIT) ([vendored](https://github.com/grumpycoders/LuaJIT/tree/vendored-clib-virtual))
- [luajit-opencl](https://github.com/malkia/luajit-opencl) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/glffi))
- [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))
- [noto](https://fonts.google.com/noto) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/noto))
- [PEGTL](https://github.com/taocpp/PEGTL)
- [pprint.lua](https://github.com/jagt/pprint.lua) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/pprint.lua))
- [snitch](https://github.com/snitch-org/snitch) ([vendored](https://github.com/grumpycoders/pcsx-redux/tree/main/third_party/snitch))
- [stb](https://github.com/nothings/stb)
- [tracy](https://github.com/wolfpld/tracy)
- [typestring](https://github.com/irrequietus/typestring) ([vendored](https://github.com/grumpycoders/pcsx-redux/blob/main/third_party/typestring.hh))
+29 -35
View File
@@ -1,14 +1,15 @@
TARGET := pcsx-redux
BUILD ?= Release
DESTDIR ?= /usr/local
CROSS ?= none
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
rwildcard = $(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))
CC_IS_CLANG := $(shell $(CC) --version | grep -q clang && echo true || echo false)
PACKAGES := capstone freetype2 glfw3 libavcodec libavformat libavutil libswresample libcurl libuv zlib
MACOS_MIN_VERSION := 11
PACKAGES := capstone freetype2 libavcodec libavformat libavutil libswresample libcurl libuv sdl3 zlib
OPTIONAL_PACKAGES := md4c fmt libllhttp libluv liburiparser
OPTIONAL_LIBRARIES := multipart ucl
@@ -37,9 +38,10 @@ CPPFLAGS_pkg_libllhttp += -Ithird_party/llhttp
CPPFLAGS += -Ithird_party/luajit/src
CPPFLAGS_pkg_libluv += -Ithird_party/luv/src
CPPFLAGS_pkg_libluv += -Ithird_party/luv/deps/lua-compat-5.3/c-api
CPPFLAGS += -Ithird_party/magic_enum/include/magic_enum
CPPFLAGS += -Ithird_party/magic_enum/include
CPPFLAGS_pkg_md4c += -Ithird_party/md4c/src
CPPFLAGS_lib_multipart += -Ithird_party/multipart-parser-c
CPPFLAGS += -Ithird_party/PEGTL/include
CPPFLAGS += -Ithird_party/tracy/public
CPPFLAGS_lib_ucl += -Ithird_party/ucl -Ithird_party/ucl/include
CPPFLAGS_pkg_liburiparser += -Ithird_party/uriparser/include
@@ -71,7 +73,7 @@ ifeq ($(CC_IS_CLANG),true)
endif
ifeq ($(UNAME_S),Darwin)
CPPFLAGS += -mmacosx-version-min=10.15
CPPFLAGS += -mmacosx-version-min=$(MACOS_MIN_VERSION)
CPPFLAGS += -stdlib=libc++
endif
@@ -79,7 +81,7 @@ LUAJIT_LDFLAGS := $(LDFLAGS)
ifeq ($(UNAME_S),Darwin)
LDFLAGS += -lc++ -framework GLUT -framework OpenGL -framework CoreFoundation -framework Cocoa
LDFLAGS += -mmacosx-version-min=10.15
LDFLAGS += -mmacosx-version-min=$(MACOS_MIN_VERSION)
else
LDFLAGS += -lstdc++fs
LDFLAGS += -lGL -lX11 -lxcb
@@ -101,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)
@@ -123,12 +121,13 @@ 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
SRCS += third_party/imgui_md/imgui_md.cpp
SRCS += third_party/imgui_memory_editor/imgui_memory_editor.cpp
SRCS += $(wildcard third_party/implot/*.cpp)
SRCS_pkg_libllhttp += $(wildcard third_party/llhttp/*.c)
SRCS += $(wildcard third_party/lpeg/*.c)
SRCS += third_party/lua-protobuf/pb.c
@@ -138,7 +137,7 @@ SRCS_pkg_md4c += third_party/md4c/src/md4c.c
SRCS_lib_multipart += third_party/multipart-parser-c/multipart_parser.c
SRCS += third_party/nanovg/src/nanovg.c
SRCS_ReleaseWithTracy += third_party/tracy/public/TracyClient.cpp
SRCS_lib_ucl += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c
SRCS_lib_ucl += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c third_party/ucl/src/n2e_ds.c
SRCS += $(wildcard third_party/uriparser/src/*.c)
SRCS += third_party/zep/extensions/repl/mode_repl.cpp
SRCS += $(wildcard third_party/zep/src/*.cpp)
@@ -160,19 +159,14 @@ 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
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
SUPPORT_SRCS += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c
SUPPORT_SRCS += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c third_party/ucl/src/n2e_ds.c
SUPPORT_SRCS += $(wildcard third_party/iec-60908b/*.c)
LIBS := third_party/luajit/src/libluajit.a
TOOLS = authoring exe2elf exe2iso modconv ps1-packer psyq-obj-parser
TOOLS = authoring exe2elf exe2exe exe2iso midi2psm midi2spd modconv ps1-packer psyq-obj-parser
##############################################################################
@@ -219,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
@@ -272,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=10.15
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=10.15
endif
$(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)
bins/$(BUILD)/$(TARGET): $(OBJECTS) $(LIBS)
@$(MKDIRP) $(dir $@)
@@ -307,15 +301,15 @@ objs/$(BUILD)/%.o: %.mm
deps/$(BUILD)/%.dep: third_party/luajit/src/luajit.h %.c
@$(MKDIRP) $(dir $@)
$(CC) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CFLAGS) -M -MT $(addprefix objs/$(BUILD)/,$(addsuffix .o,$(basename $@))) -MF $@ $<
$(CC) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CFLAGS) -M -MT objs/$(BUILD)/$*.o -MF $@ $*.c
deps/$(BUILD)/%.dep: third_party/luajit/src/luajit.h %.cc
@$(MKDIRP) $(dir $@)
$(CXX) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CXXFLAGS) -M -MT $(addprefix objs/$(BUILD)/,$(addsuffix .o,$(basename $@))) -MF $@ $<
$(CXX) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CXXFLAGS) -M -MT objs/$(BUILD)/$*.o -MF $@ $*.cc
deps/$(BUILD)/%.dep: third_party/luajit/src/luajit.h %.cpp
@$(MKDIRP) $(dir $@)
$(CXX) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CXXFLAGS) -M -MT $(addprefix objs/$(BUILD)/,$(addsuffix .o,$(basename $@))) -MF $@ $<
$(CXX) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CXXFLAGS) -M -MT objs/$(BUILD)/$*.o -MF $@ $*.cpp
objs/$(BUILD)/gtest-all.o: $(wildcard third_party/googletest/googletest/src/*.cc)
@$(MKDIRP) $(dir $@)
@@ -326,12 +320,12 @@ 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
$(MAKE) -C third_party/luajit clean MACOSX_DEPLOYMENT_TARGET=10.15
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:
rm -rf bins objs deps $(TOOLS) $(TARGET)
$(MAKE) -C third_party/luajit clean MACOSX_DEPLOYMENT_TARGET=10.15
$(MAKE) -C third_party/luajit clean MACOSX_DEPLOYMENT_TARGET=$(MACOS_MIN_VERSION)
gitclean:
git clean -f -d -x
@@ -351,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
+17 -11
View File
@@ -6,7 +6,7 @@
|--------|------------|--------|
|Windows x64 build|[![Windows x64 CI](https://dev.azure.com/grumpycoders/pcsx-redux/_apis/build/status/grumpycoders.pcsx-redux?branchName=main)](https://dev.azure.com/grumpycoders/pcsx-redux/_build/latest?definitionId=1&branchName=main)|[Windows Intel 64-bits](https://distrib.app/pub/org/pcsx-redux/project/dev-win-x64)|
|Linux x64 build|[![Linux x64 CI](https://github.com/grumpycoders/pcsx-redux/workflows/Linux%20CI/badge.svg?branch=main)](https://github.com/grumpycoders/pcsx-redux/actions?query=workflow%3A%22Linux+CI%22+branch%3Amain)|[Linux Intel 64-bits (AppImage)](https://distrib.app/pub/org/pcsx-redux/project/dev-linux-x64)|
|MacOS x64 build|[![MacOS x64 CI](https://github.com/grumpycoders/pcsx-redux/workflows/macOS%20CI/badge.svg?branch=main)](https://github.com/grumpycoders/pcsx-redux/actions?query=workflow%3A%22macOS+CI%22+branch%3Amain)|[MacOS Intel](https://distrib.app/pub/org/pcsx-redux/project/dev-macos-x64)|
|macOS build|[![macOS CI](https://github.com/grumpycoders/pcsx-redux/workflows/macOS%20CI/badge.svg?branch=main)](https://github.com/grumpycoders/pcsx-redux/actions?query=workflow%3A%22macOS+CI%22+branch%3Amain)|[macOS Arm](https://distrib.app/pub/org/pcsx-redux/project/dev-macos-arm)|
To discuss this emulator specifically, please join our Discord server:
@@ -37,7 +37,6 @@ The [tools](tools) directory contains a few tools that can be used to work with
|[Windows Intel 64-bits](https://distrib.app/pub/org/pcsx-redux/project/dev-win-x64)|
|[Windows Intel 64-bits CLI](https://distrib.app/pub/org/pcsx-redux/project/dev-win-cli-x64)|
|[Linux Intel 64-bits (AppImage)](https://distrib.app/pub/org/pcsx-redux/project/dev-linux-x64)|
|[MacOS Intel](https://distrib.app/pub/org/pcsx-redux/project/dev-macos-x64)|
|[MacOS Arm](https://distrib.app/pub/org/pcsx-redux/project/dev-macos-arm)|
### Note:
@@ -77,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 14.2.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:
@@ -91,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 :
@@ -114,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.
@@ -146,7 +152,7 @@ Since the inception of this codebase, several people have contributed to it. Ple
When Sony released the Playstation Classic recently, I came to realize two things: first, the state of the Playstation emulation isn't that great, and second, the only half-decent debugging tool still available for this console is that old telnet debugger I wrote eons ago, while other emulators out there for other consoles gained a lot of debugging superpowers. I think it was time for the Playstation emulation to get to better standards with regards to debuggability. I also felt I had a responsability to cleaning up some of the horrors I've introduced myself in the codebase long ago, and that made me cry a little looking at them. Hopefully, I got better at programming. Hopefully.
## Status?
The codebase still requires a lot of cleanup, and while the product is usable in its current state and lots can be achieved with it, there is still ways to go for reaching the first stable release. If you want to help with localization, you can find the translation project [on transifex](https://www.transifex.com/grumpycoders/pcsx-redux/languages/).
The codebase still requires a lot of cleanup, and while the product is usable in its current state and lots can be achieved with it, there is still ways to go for reaching the first stable release. If you want to help with localization, you can find the translation project [on transifex](https://explore.transifex.com/grumpycoders/pcsx-redux/).
### What works?
- Dynamic Recompiler (x86-64, experimental arm64 support)
+6 -2
View File
@@ -2,7 +2,7 @@ trigger:
- main
pool:
vmImage: 'windows-2022'
vmImage: 'windows-2025-vs2026'
variables:
solution: 'vsprojects/pcsx-redux.sln'
@@ -64,6 +64,7 @@ steps:
vsprojects/x64/ReleaseCLI/crashpad_handler.exe
vsprojects/x64/ReleaseCLI/authoring.exe
vsprojects/x64/ReleaseCLI/exe2elf.exe
vsprojects/x64/ReleaseCLI/exe2exe.exe
vsprojects/x64/ReleaseCLI/exe2iso.exe
vsprojects/x64/ReleaseCLI/modconv.exe
vsprojects/x64/ReleaseCLI/ps1-packer.exe
@@ -77,7 +78,7 @@ steps:
vsprojects/x64/ReleaseCLI/*.pdb
TargetFolder: '$(build.artifactStagingDirectory)/pdbs'
- task: VSTest@2
- task: VSTest@3
inputs:
testSelector: 'testAssemblies'
testAssemblyVer2: |
@@ -85,8 +86,11 @@ steps:
!**\crashpad_handler.exe
!**\authoring.exe
!**\exe2elf.exe
!**\exe2exe.exe
!**\exe2iso.exe
!**\modconv.exe
!**\midi2psm.exe
!**\midi2spd.exe
!**\pcsx-redux.exe
!**\pcsx-wrapper.exe
!**\pcsxrunner.exe
+26
View File
@@ -0,0 +1,26 @@
trigger: none
pr: none
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@1
displayName: 'Use Node.js 24'
inputs:
version: '24.x'
- script: |
npm install -g @vscode/vsce
npm i --prefix ./tools/vscode-extension
displayName: 'Install vsce and dependencies'
- task: AzureCLI@2
displayName: 'Publish VS Code extension'
inputs:
azureSubscription: 'azure-vsce-publish'
scriptType: 'bash'
scriptLocation: 'inlineScript'
cwd: '$(Build.SourcesDirectory)/tools/vscode-extension'
inlineScript: |
vsce publish --azure-credential
+5 -2
View File
@@ -2,7 +2,7 @@ trigger:
- main
pool:
vmImage: 'windows-2022'
vmImage: 'windows-2025-vs2026'
variables:
solution: 'vsprojects/pcsx-redux.sln'
@@ -85,7 +85,7 @@ steps:
vsprojects/x64/ReleaseWithClangCL/*.pdb
TargetFolder: '$(build.artifactStagingDirectory)/pdbs'
- task: VSTest@2
- task: VSTest@3
inputs:
testSelector: 'testAssemblies'
testAssemblyVer2: |
@@ -93,8 +93,11 @@ steps:
!**\crashpad_handler.exe
!**\authoring.exe
!**\exe2elf.exe
!**\exe2exe.exe
!**\exe2iso.exe
!**\modconv.exe
!**\midi2psm.exe
!**\midi2spd.exe
!**\pcsx-redux.main
!**\pcsx-redux.exe
!**\pcsxrunner.exe
+10 -7
View File
@@ -1,8 +1,9 @@
-std=c++2a
-std=c++2b
-fexceptions
-I.
-Isrc
-Ithird_party
-Ithird_party/ELFIO
-Ithird_party/fmt/include/
-Ithird_party/gl3w
-Ithird_party/googletest/googletest/include
@@ -10,18 +11,20 @@
-Ithird_party/imgui/backends
-Ithird_party/imgui/examples
-Ithird_party/imgui/misc/cpp
-Ithird_party/http-parser
-Ithird_party/libelfin
-Ithird_party/luajit/src
-Ithird_party/luv/src
-Ithird_party/luv/deps/lua-compat-5.3/c-api
-Ithird_party/ucl
-Ithird_party/ucl/include
-Ithird_party/libuv/include
-Ithird_party/zlib
-Ithird_party/md4c/src
-Ithird_party/PEGTL/include
-Ithird_party/ucl -Ithird_party/ucl/include
-Ithird_party/uriparser/include
-Ithird_party/zep/extensions
-Ithird_party/zep/include
-Ithird_party/xbyak/xbyak
-DLUAJIT_ENABLE_LUA52COMPAT
-DIMGUI_IMPL_OPENGL_LOADER_GL3W
-DIMGUI_ENABLE_FREETYPE
-DLUAJIT_ENABLE_LUA52COMPAT
-DZEP_FEATURE_CPP_FILE_SYSTEM
-DNVG_NO_STB
-DPB_STATIC_API
-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
Generated
+3 -3
View File
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744232761,
"narHash": "sha256-gbl9hE39nQRpZaLjhWKmEu5ejtQsgI5TWYrIVVJn30U=",
"lastModified": 1784356753,
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f675531bc7e6657c10a18b565cfebd8aa9e24c14",
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
"type": "github"
},
"original": {
+18 -2
View File
@@ -24,7 +24,7 @@
forGithubSystems = lib.genAttrs supportedSystems;
in {
packages = forAllSystems (system:
let
let
pkgs = nixpkgs.legacyPackages.${system};
cross = import nixpkgs {
inherit system;
@@ -38,10 +38,26 @@
platforms = lib.systems.flakeExposed;
gccMips = cross.buildPackages.gccWithoutTargetLibc;
};
# Use this one if you want to provide your own bios and don't care for
# openbios being bundled in.
pcsx-redux-no-bios = pkgs.callPackage ./pcsx-redux.nix {
src = self;
platforms = lib.systems.flakeExposed;
withOpenbios = false;
gccMips = null;
};
});
devShells = forAllSystems (system: {
# Using this as the default dev shell because we probably don't want to
# work on mips code, and shouldn't waste time building the mipsel
# toolchain.
default = self.packages.${system}.pcsx-redux-no-bios;
});
githubActions = nix-github-actions.lib.mkGithubMatrix {
checks = forGithubSystems (system:
checks = forGithubSystems (system:
# Prevent double build
builtins.removeAttrs self.packages.${system} ["default"]
);
+3975
View File
File diff suppressed because it is too large Load Diff
+32 -20
View File
@@ -6,7 +6,7 @@
zlib,
curl,
ffmpeg,
glfw3,
sdl3,
capstone,
freetype,
libX11,
@@ -16,17 +16,17 @@
multipart-parser-c,
fmt,
magic-enum,
miniaudio,
gtest,
tl-expected,
elfio,
tracy,
md4c,
stb,
uriparser,
ucl,
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";
@@ -91,6 +91,18 @@ let
rev = "b1e342774cbb35467dfdd3634d4f0181a76cbc89";
hash = "sha256-LYvO+chDVo6D++fuFbxqSRltGW3y82SESmtFj39TdSA=";
})
({
owner = "taocpp";
repo = "PEGTL";
rev = "d7b821b1e5ed6ab321625f50427c4ae0b78909d5";
hash = "sha256-1hTwoTCkfOX7e0unAlZ8TnYva3enkCgfrfriZfx2AoE=";
})
({
owner = "nothings";
repo = "stb";
rev = "ae721c50eaf761660b4f90cc590453cdb0c2acd0";
hash = "sha256-BIhbhXV7q5vodJ3N14vN9mEVwqrP6z9zqEEQrfLPzvI=";
})
] ++ lib.optional stdenv.hostPlatform.isAarch {
owner = "grumpycoders";
repo = "vixl";
@@ -104,16 +116,15 @@ let
};
fetchSubmodule = { owner, repo, rev, hash }@args:
"cp -ruT --no-preserve=all ${(fetchFromGitHub args).out} source/third_party/${repo}";
"cp -ruT --no-preserve=all ${(fetchFromGitHub args).out} third_party/${repo}";
in stdenv.mkDerivation {
pname = "pcsx-redux";
version = "0.99test";
inherit src;
postUnpack = ''
cp -ruT --no-preserve=all ${miniaudio.out} source/third_party/miniaudio
cp -ruT --no-preserve=all ${tracy.src} source/third_party/tracy
preConfigure = ''
cp -ruT --no-preserve=all ${tracy.src} third_party/tracy
'' + builtins.concatStringsSep "\n" (map fetchSubmodule submodules);
nativeBuildInputs = [
@@ -127,7 +138,6 @@ in stdenv.mkDerivation {
];
buildInputs = [
stb
ucl
md4c
luajitPackages.libluv
@@ -142,11 +152,13 @@ in stdenv.mkDerivation {
curl.dev
zlib
ffmpeg.dev
glfw3
sdl3
capstone
freetype.dev
uriparser
libX11
libxcb
libGL
llhttp
];
+6 -6
View File
@@ -227,13 +227,13 @@ ssize_t PCSX::CDRIso::cdread_2048(IO<File> f, unsigned int base, void *dest_, in
dest[10] = 0xff;
dest[11] = 0x00;
IEC60908b::MSF(sector + 150).toBCD(dest + 12);
m_cdbuffer[15] = 2;
m_cdbuffer[16] = m_cdbuffer[20] = 0;
m_cdbuffer[17] = m_cdbuffer[21] = 0;
m_cdbuffer[18] = m_cdbuffer[22] = 8;
m_cdbuffer[19] = m_cdbuffer[23] = 0;
dest[15] = 2;
dest[16] = dest[20] = 0;
dest[17] = dest[21] = 0;
dest[18] = dest[22] = 8;
dest[19] = dest[23] = 0;
IEC60908b::computeEDCECC(m_cdbuffer);
IEC60908b::computeEDCECC(dest);
return ret;
}
+1 -1
View File
@@ -19,7 +19,7 @@
#include "cdrom/file.h"
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "cdrom/cdriso.h"
#include "iec-60908b/edcecc.h"
+16 -8
View File
@@ -25,9 +25,12 @@
PCSX::ISO9660Reader::ISO9660Reader(std::shared_ptr<CDRIso> iso) : m_iso(iso) {
unsigned pvdSector = 16;
bool foundPVD = false;
// Scan the full Volume Descriptor Set (LBA 16..terminator) to both find
// the PVD and record where the set actually ends.
while (true) {
IO<File> pvdFile(new CDRIsoFile(iso, pvdSector++, 2048));
IO<File> pvdFile(new CDRIsoFile(iso, pvdSector, 2048));
if (pvdFile->failed()) {
m_failed = true;
return;
@@ -36,22 +39,27 @@ PCSX::ISO9660Reader::ISO9660Reader(std::shared_ptr<CDRIso> iso) : m_iso(iso) {
uint8_t vd[7];
pvdFile->readAt(vd, 7, 0);
if ((vd[1] != 'C') || (vd[2] != 'D') || (vd[3] != '0') || (vd[4] != '0') || (vd[5] != '1') || (vd[6] != 1)) {
// Malformed descriptor set: even if we already have the PVD, the
// set is broken so we can't trust m_vdEnd.
m_failed = true;
return;
}
if (vd[0] == 255) {
m_failed = true;
// Terminator: the VD set ends at the sector after this one.
m_vdEnd = pvdSector + 1;
if (!foundPVD) m_failed = true;
return;
}
if (vd[0] != 1) continue;
if (vd[0] == 1 && !foundPVD) {
ISO9660LowLevel::PVD pvd;
pvd.deserialize(pvdFile);
m_pvd = pvd;
foundPVD = true;
}
ISO9660LowLevel::PVD pvd;
pvd.deserialize(pvdFile);
m_pvd = pvd;
break;
pvdSector++;
}
}
+9 -2
View File
@@ -40,13 +40,20 @@ class ISO9660Reader {
return std::string_view(m_pvd.get<ISO9660LowLevel::PVD_VolumeIdent>());
}
typedef std::pair<ISO9660LowLevel::DirEntry, ISO9660LowLevel::DirEntry_XA> FullDirEntry;
std::vector<FullDirEntry> listAllEntriesFrom(const ISO9660LowLevel::DirEntry& entry);
const ISO9660LowLevel::DirEntry& getRootDirEntry() { return m_pvd.get<ISO9660LowLevel::PVD_RootDir>(); }
const ISO9660LowLevel::PVD& getPVD() { return m_pvd; }
// Returns the LBA just past the end of the volume descriptor set, i.e. the
// first sector after the VD terminator (type 255). Returns 17 if not found.
uint32_t getVDEnd() { return m_vdEnd; }
private:
std::shared_ptr<CDRIso> m_iso;
bool m_failed = false;
typedef std::pair<ISO9660LowLevel::DirEntry, ISO9660LowLevel::DirEntry_XA> FullDirEntry;
uint32_t m_vdEnd = 17;
std::optional<FullDirEntry> findEntry(const std::string_view& filename);
std::vector<FullDirEntry> listAllEntriesFrom(const ISO9660LowLevel::DirEntry& entry);
ISO9660LowLevel::PVD m_pvd;
};
+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;
unsigned count = 0; // How many instructions have we compiled?
@@ -262,6 +385,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 = [&]() {
@@ -271,19 +407,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
@@ -293,6 +431,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:
+9 -1
View File
@@ -23,10 +23,11 @@
#include "core/cdrom.h"
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include <string_view>
#include "cdrom/iso9660-reader.h"
#include "core/cdromlogger.h"
#include "core/debug.h"
#include "core/psxdma.h"
#include "core/psxemulator.h"
@@ -183,6 +184,9 @@ class CDRomImpl final : public PCSX::CDRom {
} else {
m_invalidLocL = false;
m_iso->readTrack(m_currentPosition);
PCSX::g_emulator->m_cdromLogger->recordAccess(m_currentPosition.toLBA(),
PCSX::CDRomLogger::AccessType::Data,
PCSX::g_emulator->m_cpu->m_regs.cycle);
auto buffer = m_iso->getBuffer();
memcpy(m_lastLocL, buffer, sizeof(m_lastLocL));
uint32_t size = 0;
@@ -818,6 +822,9 @@ class CDRomImpl final : public PCSX::CDRom {
cause = Cause::Acknowledge;
maybeTriggerIRQ(cause, response);
m_seekPosition = maybeMSF.value();
PCSX::g_emulator->m_cdromLogger->recordAccess(m_seekPosition.toLBA(),
PCSX::CDRomLogger::AccessType::Seek,
PCSX::g_emulator->m_cpu->m_regs.cycle);
} else {
maybeEnqueueError(1, 0x10);
}
@@ -1286,6 +1293,7 @@ PCSX::CDRom *PCSX::CDRom::factory() { return new CDRomImpl; }
void PCSX::CDRom::parseIso() {
m_cdromId.clear();
m_cdromLabel.clear();
if (m_iso) g_emulator->m_cdromLogger->setDiscSectors(m_iso->getTD(0).toLBA());
ISO9660Reader reader(m_iso);
if (reader.failed()) return;
IO<File> systemcnf(reader.open("SYSTEM.CNF;1"));
+110
View File
@@ -0,0 +1,110 @@
/***************************************************************************
* 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 "core/cdromlogger.h"
void PCSX::CDRomLogger::recordAccess(uint32_t lba, AccessType type, uint32_t cycle) {
if (!m_enabled) return;
if (lba >= c_maxSectors) return;
uint32_t* timestamps = (type == AccessType::Data) ? m_dataTimestamps
: (type == AccessType::Audio) ? m_audioTimestamps
: m_seekTimestamps;
timestamps[lba] = cycle;
}
void PCSX::CDRomLogger::enable() {
if (m_hasResources) {
m_enabled = true;
return;
}
auto setupTex = [](OpenGL::Texture& tex) {
tex.bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
// Create heatmap textures (GL_R32UI - unsigned integer)
m_dataHeatmapTex.create(c_side, c_side, GL_R32UI);
m_audioHeatmapTex.create(c_side, c_side, GL_R32UI);
m_seekHeatmapTex.create(c_side, c_side, GL_R32UI);
if (!m_dataHeatmapTex.exists() || !m_audioHeatmapTex.exists() || !m_seekHeatmapTex.exists()) return;
setupTex(m_dataHeatmapTex);
setupTex(m_audioHeatmapTex);
setupTex(m_seekHeatmapTex);
// Per-radius ring-max textures (640 x 1)
m_dataRingTex.create(c_side, 1, GL_R32UI);
m_audioRingTex.create(c_side, 1, GL_R32UI);
m_seekRingTex.create(c_side, 1, GL_R32UI);
if (!m_dataRingTex.exists() || !m_audioRingTex.exists() || !m_seekRingTex.exists()) return;
setupTex(m_dataRingTex);
setupTex(m_audioRingTex);
setupTex(m_seekRingTex);
// Clear timestamp arrays
memset(m_dataTimestamps, 0, sizeof(m_dataTimestamps));
memset(m_audioTimestamps, 0, sizeof(m_audioTimestamps));
memset(m_seekTimestamps, 0, sizeof(m_seekTimestamps));
m_hasResources = true;
m_enabled = true;
}
void PCSX::CDRomLogger::disable() { m_enabled = false; }
void PCSX::CDRomLogger::uploadHeatmaps() {
if (!m_hasResources) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
m_dataHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_side, c_side, GL_RED_INTEGER, GL_UNSIGNED_INT, m_dataTimestamps);
m_audioHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_side, c_side, GL_RED_INTEGER, GL_UNSIGNED_INT, m_audioTimestamps);
m_seekHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_side, c_side, GL_RED_INTEGER, GL_UNSIGNED_INT, m_seekTimestamps);
// Reduce each row (= one radial band / ring) to its most recent timestamp,
// and upload the 640 x 1 ring-max textures the polar whole-ring view samples.
// Raw max (not decay-aware) is fine: the ~127s wrap window dwarfs the decay
// half-life, so the largest raw timestamp is the most recent except for a
// single-frame glitch every ~127s.
auto uploadRing = [](const uint32_t* ts, OpenGL::Texture& tex) {
uint32_t ring[c_side];
for (int r = 0; r < c_side; r++) {
const uint32_t* row = ts + static_cast<size_t>(r) * c_side;
uint32_t m = 0;
for (int c = 0; c < c_side; c++) {
if (row[c] > m) m = row[c];
}
ring[r] = m;
}
tex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_side, 1, GL_RED_INTEGER, GL_UNSIGNED_INT, ring);
};
uploadRing(m_dataTimestamps, m_dataRingTex);
uploadRing(m_audioTimestamps, m_audioRingTex);
uploadRing(m_seekTimestamps, m_seekRingTex);
}
+101
View File
@@ -0,0 +1,101 @@
/***************************************************************************
* 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. *
***************************************************************************/
#pragma once
#include <stdint.h>
#include <string.h>
#include "support/opengl.h"
namespace PCSX {
namespace Widgets {
class CDRomViewer;
}
// Per-sector access logger for the CD-ROM, mirroring RAMLogger's cycle-timestamp
// design. One pixel per sector, laid out row-major into a 640x640 grid.
//
// 640x640 = 409600 sectors = 91 minutes of audio. The theoretical MSF maximum
// (99:57:74 = 449849 sectors) does not fit, but that disc cannot physically
// exist: Red Book caps at 74 minutes, 80-minute discs are common, and even
// aggressive 90-minute overburn (405000 sectors) lands under the 91-minute
// ceiling. So the grid covers every physically real disc without wasting a
// texture on the impossible tail. Anything past c_maxSectors is dropped.
class CDRomLogger {
public:
enum class AccessType { Data, Audio, Seek };
static constexpr int c_side = 640;
static constexpr size_t c_maxSectors = static_cast<size_t>(c_side) * c_side; // 409600
// Record one sector access. lba is the raw MSF->LBA (0-based at 00:00:00).
void recordAccess(uint32_t lba, AccessType type, uint32_t cycle);
void enable();
void disable();
bool isEnabled() const { return m_enabled; }
void uploadHeatmaps();
// Total sector count of the mounted disc (lead-out LBA), for the backdrop
// that shades the valid disc extent. 0 = unknown / no disc.
// Ignore 0: getTD(0) returns 0 during the reset transient (before tracks are
// re-parsed), and we don't want a hard reset to wipe the known disc extent.
void setDiscSectors(uint32_t n) {
if (n != 0) m_discSectors = n;
}
uint32_t getDiscSectors() const { return m_discSectors; }
void bindDataHeatmap() { m_dataHeatmapTex.bind(); }
void bindAudioHeatmap() { m_audioHeatmapTex.bind(); }
void bindSeekHeatmap() { m_seekHeatmapTex.bind(); }
void bindDataRing() { m_dataRingTex.bind(); }
void bindAudioRing() { m_audioRingTex.bind(); }
void bindSeekRing() { m_seekRingTex.bind(); }
GLuint getDataHeatmapID() { return m_dataHeatmapTex.handle(); }
// Configurable decay half-life in cycles (how many cycles until intensity halves).
// CD activity is sparse (75 sectors/s max), so the default memory is long.
float m_decayHalfLife = 169344000.0f; // ~5 seconds at 33.8688 MHz
private:
bool m_enabled = false;
bool m_hasResources = false;
uint32_t m_discSectors = 0;
// Per-sector cycle timestamp arrays (low 32 bits of the CPU cycle counter).
uint32_t m_dataTimestamps[c_maxSectors] = {};
uint32_t m_audioTimestamps[c_maxSectors] = {};
uint32_t m_seekTimestamps[c_maxSectors] = {};
// Heatmap textures (GL_R32UI, 640 x 640).
OpenGL::Texture m_dataHeatmapTex, m_audioHeatmapTex, m_seekHeatmapTex;
// Per-radius ring-max textures (GL_R32UI, 640 x 1): each texel is the most
// recent timestamp across the whole row (one radial band = one ring). Polar
// mode samples these so the entire ring at a read radius lights up, instead
// of a thin arc that is invisible at the physical rotation speed.
OpenGL::Texture m_dataRingTex, m_audioRingTex, m_seekRingTex;
friend class Widgets::CDRomViewer;
};
} // namespace PCSX
+36
View File
@@ -28,6 +28,7 @@
#include "core/psxemulator.h"
#include "core/psxmem.h"
#include "core/r3000a.h"
#include "core/ramlogger.h"
#include "supportpsx/memory.h"
enum {
@@ -157,6 +158,17 @@ void PCSX::Debug::process(uint32_t oldPC, uint32_t newPC, uint32_t oldCode, uint
if (isJALR) markMap(regs.GPR.r[rd], MAP_EXEC_JAL);
}
// RAM logger: record execution
auto *ramLogger = g_emulator->m_ramLogger.get();
const bool ramLoggerEnabled = ramLogger->isEnabled();
const uint32_t cycle = static_cast<uint32_t>(regs.cycle);
if (ramLoggerEnabled) {
PSXAddress pcAddr(normalizeAddress(newPC));
if (pcAddr.type == PSXAddress::Type::RAM) {
ramLogger->recordAccess(pcAddr.physical, 4, RAMLogger::AccessType::Execute, cycle);
}
}
// Are we jumping from a non-kernel address to a kernel address which:
// - is not a jr to $ra (aka a return from a callback)
// - is not a jump to 0xa0 / 0xb0 / 0xc0 (aka the syscall gates)
@@ -213,6 +225,20 @@ void PCSX::Debug::process(uint32_t oldPC, uint32_t newPC, uint32_t oldCode, uint
}
if (m_mapping_w32) markMap(offset, MAP_W32);
}
// RAM logger: record loads and stores
if (ramLoggerEnabled) {
PSXAddress loadStoreAddr(normalizeAddress(offset));
if (loadStoreAddr.type == PSXAddress::Type::RAM) {
if (isLoad) {
unsigned width = (isLB || isLBU) ? 1 : (isLH || isLHU) ? 2 : 4;
ramLogger->recordAccess(loadStoreAddr.physical, width, RAMLogger::AccessType::Read, cycle);
}
if (isStore) {
unsigned width = isSB ? 1 : isSH ? 2 : 4;
ramLogger->recordAccess(loadStoreAddr.physical, width, RAMLogger::AccessType::Write, cycle);
}
}
}
// Are we accessing a kernel address from a non-kernel address, while not in IRQ?
if (!g_emulator->m_cpu->m_inISR && offsetIsInKernel && !wasInKernel) {
if (m_checkKernel) {
@@ -292,6 +318,16 @@ bool PCSX::Debug::triggerBP(Breakpoint* bp, uint32_t address, unsigned width, co
return keepBP;
}
void PCSX::Debug::logDMAAccess(uint32_t address, uint32_t len, bool isWrite) {
auto *ramLogger = g_emulator->m_ramLogger.get();
if (!ramLogger->isEnabled()) return;
PSXAddress addr(normalizeAddress(address));
if (addr.type != PSXAddress::Type::RAM) return;
uint32_t cycle = static_cast<uint32_t>(g_emulator->m_cpu->m_regs.cycle);
auto type = isWrite ? RAMLogger::AccessType::Write : RAMLogger::AccessType::Read;
ramLogger->recordAccess(addr.physical, len, type, cycle);
}
void PCSX::Debug::checkBP(uint32_t address, BreakpointType type, uint32_t width, const char* cause) {
auto& cpu = g_emulator->m_cpu;
auto& regs = cpu->m_regs;
+3
View File
@@ -42,14 +42,17 @@ class Debug {
void checkDMAread(unsigned c, uint32_t address, uint32_t len) {
std::string cause = fmt::format("DMA channel {} read", c);
checkBP(address, BreakpointType::Read, len, cause.c_str());
logDMAAccess(address, len, false);
}
void checkDMAwrite(unsigned c, uint32_t address, uint32_t len) {
std::string cause = fmt::format("DMA channel {} write", c);
checkBP(address, BreakpointType::Write, len, cause.c_str());
logDMAAccess(address, len, true);
}
private:
void checkBP(uint32_t address, BreakpointType type, uint32_t width, const char* cause = "");
void logDMAAccess(uint32_t address, uint32_t len, bool isWrite);
public:
// call this if PC is being set, like when the emulation is being reset, or when doing fastboot
+1 -1
View File
@@ -17,7 +17,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <algorithm>
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "core/gpu.h"
#include "core/psxemulator.h"
+12 -1
View File
@@ -17,11 +17,13 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/gdb-server.h"
#include <assert.h>
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "core/cdrom.h"
#include "core/debug.h"
@@ -770,6 +772,15 @@ void PCSX::GdbClient::processMonitorCommand(const std::string& cmd) {
writeEscaped("Unknown type. Valid types: wram");
}
}
} else if (words[0] == "cache") {
// Writing memory over gdb won't invalidate anything by itself: most of what goes through
// there is data, and there's no d-cache to worry about. Patching code needs this after.
if ((words.size() != 2) || (words[1] != "flush")) {
writeEscaped("Usage: cache flush\n");
} else {
writeEscaped("Flushing i-cache\n");
g_emulator->m_cpu->invalidateCache();
}
}
write("OK");
}
+2
View File
@@ -19,6 +19,8 @@
#pragma once
#include <uv.h>
#include <assert.h>
#include <cstdarg>
+65 -186
View File
@@ -21,7 +21,7 @@
#include "core/gpu.h"
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "core/debug.h"
#include "core/gpulogger.h"
@@ -227,184 +227,62 @@ void GPU::Rect<size, textured, blend, modulation>::processWrite(Buffer & buf, Lo
namespace {
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_poly00;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_poly01;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_poly02;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_poly03;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_poly04;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_poly05;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_poly06;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_poly07;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_poly08;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_poly09;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_poly0a;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_poly0b;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_poly0c;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_poly0d;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_poly0e;
GPU::Poly<GPU::Shading::Flat, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_poly0f;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_poly10;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_poly11;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_poly12;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_poly13;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_poly14;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_poly15;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_poly16;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Tri, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_poly17;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_poly18;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_poly19;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_poly1a;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_poly1b;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_poly1c;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_poly1d;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_poly1e;
GPU::Poly<GPU::Shading::Gouraud, GPU::Shape::Quad, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_poly1f;
template <size_t Index>
auto *polyCommand() {
static GPU::Poly<(Index & 0x10) ? GPU::Shading::Gouraud : GPU::Shading::Flat,
(Index & 0x08) ? GPU::Shape::Quad : GPU::Shape::Tri,
(Index & 0x04) ? GPU::Textured::Yes : GPU::Textured::No,
(Index & 0x02) ? GPU::Blend::Semi : GPU::Blend::Off,
(Index & 0x01) ? GPU::Modulation::Off : GPU::Modulation::On>
command;
return &command;
}
GPU::Line<GPU::Shading::Flat, GPU::LineType::Simple, GPU::Blend::Off> s_line0;
GPU::Line<GPU::Shading::Flat, GPU::LineType::Simple, GPU::Blend::Semi> s_line1;
GPU::Line<GPU::Shading::Flat, GPU::LineType::Poly, GPU::Blend::Off> s_line2;
GPU::Line<GPU::Shading::Flat, GPU::LineType::Poly, GPU::Blend::Semi> s_line3;
GPU::Line<GPU::Shading::Gouraud, GPU::LineType::Simple, GPU::Blend::Off> s_line4;
GPU::Line<GPU::Shading::Gouraud, GPU::LineType::Simple, GPU::Blend::Semi> s_line5;
GPU::Line<GPU::Shading::Gouraud, GPU::LineType::Poly, GPU::Blend::Off> s_line6;
GPU::Line<GPU::Shading::Gouraud, GPU::LineType::Poly, GPU::Blend::Semi> s_line7;
template <size_t Index>
auto *lineCommand() {
static GPU::Line<(Index & 0x10) ? GPU::Shading::Gouraud : GPU::Shading::Flat,
(Index & 0x08) ? GPU::LineType::Poly : GPU::LineType::Simple,
(Index & 0x02) ? GPU::Blend::Semi : GPU::Blend::Off>
command;
return &command;
}
GPU::Rect<GPU::Size::Variable, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_rect00;
GPU::Rect<GPU::Size::Variable, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_rect01;
GPU::Rect<GPU::Size::Variable, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_rect02;
GPU::Rect<GPU::Size::Variable, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_rect03;
GPU::Rect<GPU::Size::Variable, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_rect04;
GPU::Rect<GPU::Size::Variable, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_rect05;
GPU::Rect<GPU::Size::Variable, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_rect06;
GPU::Rect<GPU::Size::Variable, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_rect07;
GPU::Rect<GPU::Size::S1, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_rect08;
GPU::Rect<GPU::Size::S1, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_rect09;
GPU::Rect<GPU::Size::S1, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_rect0a;
GPU::Rect<GPU::Size::S1, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_rect0b;
GPU::Rect<GPU::Size::S1, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_rect0c;
GPU::Rect<GPU::Size::S1, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_rect0d;
GPU::Rect<GPU::Size::S1, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_rect0e;
GPU::Rect<GPU::Size::S1, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_rect0f;
GPU::Rect<GPU::Size::S8, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_rect10;
GPU::Rect<GPU::Size::S8, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_rect11;
GPU::Rect<GPU::Size::S8, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_rect12;
GPU::Rect<GPU::Size::S8, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_rect13;
GPU::Rect<GPU::Size::S8, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_rect14;
GPU::Rect<GPU::Size::S8, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_rect15;
GPU::Rect<GPU::Size::S8, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_rect16;
GPU::Rect<GPU::Size::S8, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_rect17;
GPU::Rect<GPU::Size::S16, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::On> s_rect18;
GPU::Rect<GPU::Size::S16, GPU::Textured::No, GPU::Blend::Off, GPU::Modulation::Off> s_rect19;
GPU::Rect<GPU::Size::S16, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::On> s_rect1a;
GPU::Rect<GPU::Size::S16, GPU::Textured::No, GPU::Blend::Semi, GPU::Modulation::Off> s_rect1b;
GPU::Rect<GPU::Size::S16, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::On> s_rect1c;
GPU::Rect<GPU::Size::S16, GPU::Textured::Yes, GPU::Blend::Off, GPU::Modulation::Off> s_rect1d;
GPU::Rect<GPU::Size::S16, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::On> s_rect1e;
GPU::Rect<GPU::Size::S16, GPU::Textured::Yes, GPU::Blend::Semi, GPU::Modulation::Off> s_rect1f;
template <size_t Index>
consteval GPU::Size rectSize() {
if constexpr (((Index >> 3) & 3) == 0) {
return GPU::Size::Variable;
} else if constexpr (((Index >> 3) & 3) == 1) {
return GPU::Size::S1;
} else if constexpr (((Index >> 3) & 3) == 2) {
return GPU::Size::S8;
} else {
return GPU::Size::S16;
}
}
template <size_t Index>
auto *rectCommand() {
static GPU::Rect<rectSize<Index>(), (Index & 0x04) ? GPU::Textured::Yes : GPU::Textured::No,
(Index & 0x02) ? GPU::Blend::Semi : GPU::Blend::Off,
(Index & 0x01) ? GPU::Modulation::Off : GPU::Modulation::On>
command;
return &command;
}
} // namespace
} // namespace PCSX
PCSX::GPU::GPU() {
m_polygons[0x00] = &s_poly00;
m_polygons[0x01] = &s_poly01;
m_polygons[0x02] = &s_poly02;
m_polygons[0x03] = &s_poly03;
m_polygons[0x04] = &s_poly04;
m_polygons[0x05] = &s_poly05;
m_polygons[0x06] = &s_poly06;
m_polygons[0x07] = &s_poly07;
m_polygons[0x08] = &s_poly08;
m_polygons[0x09] = &s_poly09;
m_polygons[0x0a] = &s_poly0a;
m_polygons[0x0b] = &s_poly0b;
m_polygons[0x0c] = &s_poly0c;
m_polygons[0x0d] = &s_poly0d;
m_polygons[0x0e] = &s_poly0e;
m_polygons[0x0f] = &s_poly0f;
m_polygons[0x10] = &s_poly10;
m_polygons[0x11] = &s_poly11;
m_polygons[0x12] = &s_poly12;
m_polygons[0x13] = &s_poly13;
m_polygons[0x14] = &s_poly14;
m_polygons[0x15] = &s_poly15;
m_polygons[0x16] = &s_poly16;
m_polygons[0x17] = &s_poly17;
m_polygons[0x18] = &s_poly18;
m_polygons[0x19] = &s_poly19;
m_polygons[0x1a] = &s_poly1a;
m_polygons[0x1b] = &s_poly1b;
m_polygons[0x1c] = &s_poly1c;
m_polygons[0x1d] = &s_poly1d;
m_polygons[0x1e] = &s_poly1e;
m_polygons[0x1f] = &s_poly1f;
m_lines[0x00] = &s_line0;
m_lines[0x01] = &s_line0;
m_lines[0x02] = &s_line1;
m_lines[0x03] = &s_line1;
m_lines[0x04] = &s_line0;
m_lines[0x05] = &s_line0;
m_lines[0x06] = &s_line1;
m_lines[0x07] = &s_line1;
m_lines[0x08] = &s_line2;
m_lines[0x09] = &s_line2;
m_lines[0x0a] = &s_line3;
m_lines[0x0b] = &s_line3;
m_lines[0x0c] = &s_line2;
m_lines[0x0d] = &s_line2;
m_lines[0x0e] = &s_line3;
m_lines[0x0f] = &s_line3;
m_lines[0x10] = &s_line4;
m_lines[0x11] = &s_line4;
m_lines[0x12] = &s_line5;
m_lines[0x13] = &s_line5;
m_lines[0x14] = &s_line4;
m_lines[0x15] = &s_line4;
m_lines[0x16] = &s_line5;
m_lines[0x17] = &s_line5;
m_lines[0x18] = &s_line6;
m_lines[0x19] = &s_line6;
m_lines[0x1a] = &s_line7;
m_lines[0x1b] = &s_line7;
m_lines[0x1c] = &s_line6;
m_lines[0x1d] = &s_line6;
m_lines[0x1e] = &s_line7;
m_lines[0x1f] = &s_line7;
m_rects[0x00] = &s_rect00;
m_rects[0x01] = &s_rect01;
m_rects[0x02] = &s_rect02;
m_rects[0x03] = &s_rect03;
m_rects[0x04] = &s_rect04;
m_rects[0x05] = &s_rect05;
m_rects[0x06] = &s_rect06;
m_rects[0x07] = &s_rect07;
m_rects[0x08] = &s_rect08;
m_rects[0x09] = &s_rect09;
m_rects[0x0a] = &s_rect0a;
m_rects[0x0b] = &s_rect0b;
m_rects[0x0c] = &s_rect0c;
m_rects[0x0d] = &s_rect0d;
m_rects[0x0e] = &s_rect0e;
m_rects[0x0f] = &s_rect0f;
m_rects[0x10] = &s_rect10;
m_rects[0x11] = &s_rect11;
m_rects[0x12] = &s_rect12;
m_rects[0x13] = &s_rect13;
m_rects[0x14] = &s_rect14;
m_rects[0x15] = &s_rect15;
m_rects[0x16] = &s_rect16;
m_rects[0x17] = &s_rect17;
m_rects[0x18] = &s_rect18;
m_rects[0x19] = &s_rect19;
m_rects[0x1a] = &s_rect1a;
m_rects[0x1b] = &s_rect1b;
m_rects[0x1c] = &s_rect1c;
m_rects[0x1d] = &s_rect1d;
m_rects[0x1e] = &s_rect1e;
m_rects[0x1f] = &s_rect1f;
[&]<size_t... Is>(std::index_sequence<Is...>) {
((m_polygons[Is] = polyCommand<Is>()), ...);
}(std::make_index_sequence<32>{});
[&]<size_t... Is>(std::index_sequence<Is...>) {
((m_lines[Is] = lineCommand<Is>()), ...);
}(std::make_index_sequence<32>{});
[&]<size_t... Is>(std::index_sequence<Is...>) {
((m_rects[Is] = rectCommand<Is>()), ...);
}(std::make_index_sequence<32>{});
}
int PCSX::GPU::init(UI *ui) {
@@ -700,6 +578,7 @@ void PCSX::GPU::directDMARead(uint32_t *dest, int transferSize, uint32_t hwAddr)
dest += size / 4;
while (transferSize != 0) {
*dest++ = m_dataRet;
transferSize--;
}
}
@@ -764,7 +643,7 @@ void PCSX::GPU::Command::processWrite(Buffer &buf, Logged::Origin origin, uint32
const uint8_t cmdType = value >> 29; // 3 topmost bits = command "type"
const uint8_t command = (value >> 24) & 0x1f; // 5 next bits = "command", which may be a bitfield
const uint32_t packetInfo = value & 0xffffff;
const uint32_t packetInfo = value & c_PacketInfoMask;
switch (cmdType) {
case 0: // GPU command
@@ -947,8 +826,8 @@ void PCSX::GPU::BlitRamVram::processWrite(Buffer &buf, Logged::Origin origin, ui
[[fallthrough]];
case READ_XY:
value = buf.get();
x = signExtend<int, 11>(value & 0xffff);
y = signExtend<int, 11>(value >> 16);
x = value & 0xffff;
y = value >> 16;
m_state = READ_HW;
if (buf.isEmpty()) return;
[[fallthrough]];
@@ -1004,8 +883,8 @@ void PCSX::GPU::BlitVramRam::processWrite(Buffer &buf, Logged::Origin origin, ui
[[fallthrough]];
case READ_XY:
value = buf.get();
x = signExtend<int, 11>(value & 0xffff);
y = signExtend<int, 11>(value >> 16);
x = value & 0xffff;
y = value >> 16;
m_state = READ_HW;
if (buf.isEmpty()) return;
[[fallthrough]];
@@ -1046,31 +925,31 @@ PCSX::GPU::TPage::TPage(uint32_t value) {
}
PCSX::GPU::TWindow::TWindow(uint32_t value) {
x = value & 0x1f;
y = (value >> 5) & 0x1f;
w = (value >> 10) & 0x1f;
h = (value >> 15) & 0x1f;
x = value & c_TWindowFieldMask;
y = (value >> 5) & c_TWindowFieldMask;
w = (value >> 10) & c_TWindowFieldMask;
h = (value >> 15) & c_TWindowFieldMask;
raw = value;
}
PCSX::GPU::DrawingAreaStart::DrawingAreaStart(uint32_t value) {
x = value & 0x3ff;
y = (value >> 10) & 0x1ff;
x = value & c_Coord10Mask;
y = (value >> c_DrawingAreaYShift) & c_Coord9Mask;
raw = value;
}
PCSX::GPU::DrawingAreaEnd::DrawingAreaEnd(uint32_t value) {
x = value & 0x3ff;
y = (value >> 10) & 0x1ff;
x = value & c_Coord10Mask;
y = (value >> c_DrawingAreaYShift) & c_Coord9Mask;
raw = value;
}
PCSX::GPU::DrawingOffset::DrawingOffset(uint32_t value) {
int ux = value & 0x7ff;
int uy = (value >> 11) & 0x7ff;
int ux = value & c_Coord11Mask;
int uy = (value >> c_DrawingOffsetYShift) & c_Coord11Mask;
x = signExtend<int, 11>(ux);
y = signExtend<int, 11>(uy);
+55 -33
View File
@@ -22,7 +22,7 @@
#include <stdint.h>
#include <functional>
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include <memory>
#include <stdexcept>
#include <string>
@@ -43,6 +43,44 @@ namespace PCSX {
class UI;
struct SaveStateWrapper;
// GP0 / GP1 packet-layout constants. Each constant names the bit-field
// width that the PSX protocol assigns to a particular packet payload
// slot. The packet constructors and the GP0 command dispatcher decode
// these slots with `value & mask` / `(value >> shift) & mask` shapes;
// the constants below give those expressions hardware-anchored names
// without changing the constructor logic. The raw env-packet preserve
// sites (m_textureWindowRaw, m_drawingStartRaw, m_drawingEndRaw,
// m_drawingOffsetRaw) keep their inline literals because they are
// protocol-visible logging fields rather than decoded math.
// GP0 command words carry the command byte in bits 24..28 and the
// payload in bits 0..23. The 24-bit payload mask is the wire shape
// shared by every env-class packet constructor.
static constexpr uint32_t c_PacketInfoMask = 0x00ffffff;
// Coordinate field widths. PSX vertex coordinates pack as 11-bit
// signed values (sign-extended after extraction); draw-area positions
// pack as 10-bit X / 9-bit Y; horizontal display range as 12-bit X
// pairs; CLUT selectors as 6-bit X / 9-bit Y; texture-window fields
// as 5-bit selectors.
static constexpr uint32_t c_Coord9Mask = 0x1ff;
static constexpr uint32_t c_Coord10Mask = 0x3ff;
static constexpr uint32_t c_Coord11Mask = 0x7ff;
static constexpr uint32_t c_Coord12Mask = 0xfff;
static constexpr uint32_t c_ClutXFieldMask = 0x3f;
static constexpr uint32_t c_ClutYFieldMask = c_Coord9Mask;
static constexpr uint32_t c_TWindowFieldMask = 0x1f;
// Field shifts. The CLUT half-word packs X at bits 0..5 and Y at bits
// 6..14. The DrawingOffset packs X at bits 0..10 and Y at bits 11..21.
// The DrawingArea positions pack X at bits 0..9 and Y at bits 10..18.
// The horizontal display range packs x0 at bits 0..11 and x1 at bits
// 12..23.
static constexpr int c_ClutYShift = 6;
static constexpr int c_DrawingOffsetYShift = 11;
static constexpr int c_DrawingAreaYShift = 10;
static constexpr int c_HorizDisplayRangeX1Shift = 12;
class GPU {
public:
template <typename T, unsigned bits>
@@ -247,37 +285,19 @@ class GPU {
template <typename T, T wMax = 1024, T hMax = 512>
static bool clip(T &x, T &y, T &w, T &h) {
bool clipped = false;
if (x >= wMax || y >= hMax) {
x = wMax;
y = hMax;
if ((w != 0) || (h != 0)) {
w = h = 0;
return true;
}
return false;
}
if (x < 0) {
clipped = true;
w += x;
x = 0;
}
if (y < 0) {
clipped = true;
h += y;
y = 0;
}
if (w < 0 || h < 0) {
h = w = 0;
return true;
}
x %= wMax;
y %= hMax;
if (x + w > wMax) {
x = 0;
clipped = true;
w = wMax - x;
}
if (y + h > hMax) {
y = 0;
clipped = true;
h = hMax - y;
}
return clipped;
}
@@ -571,14 +591,14 @@ class GPU {
DrawingOffset offset;
TextureUnitType clutX() {
if constexpr (textured == Textured::Yes) {
return (clutraw & 0x3f) * 16;
return (clutraw & c_ClutXFieldMask) * 16;
} else {
return {};
}
}
TextureUnitType clutY() {
if constexpr (textured == Textured::Yes) {
return (clutraw >> 6) & 0x1ff;
return (clutraw >> c_ClutYShift) & c_ClutYFieldMask;
} else {
return {};
}
@@ -681,14 +701,14 @@ class GPU {
DrawingOffset offset;
TextureUnitType clutX() {
if constexpr (textured == Textured::Yes) {
return (clutraw & 0x3f) * 16;
return (clutraw & c_ClutXFieldMask) * 16;
} else {
return {};
}
}
TextureUnitType clutY() {
if constexpr (textured == Textured::Yes) {
return (clutraw >> 6) & 0x1ff;
return (clutraw >> c_ClutYShift) & c_ClutYFieldMask;
} else {
return {};
}
@@ -755,7 +775,7 @@ class GPU {
void generateStatsInfo() override {}
void cumulateStats(GPUStats *) override {}
void getVertices(AddTri &&, PixelOp) override {}
CtrlDisplayStart(uint32_t value) : x(value & 0x3ff), y((value >> 10) & 0x1ff) {}
CtrlDisplayStart(uint32_t value) : x(value & c_Coord10Mask), y((value >> c_DrawingAreaYShift) & c_Coord9Mask) {}
CtrlDisplayStart(const CtrlDisplayStart &other) = default;
CtrlDisplayStart(CtrlDisplayStart &&other) = default;
CtrlDisplayStart &operator=(const CtrlDisplayStart &other) = default;
@@ -768,7 +788,8 @@ class GPU {
void generateStatsInfo() override {}
void cumulateStats(GPUStats *) override {}
void getVertices(AddTri &&, PixelOp) override {}
CtrlHorizontalDisplayRange(uint32_t value) : x0(value & 0xfff), x1((value >> 12) & 0xfff) {}
CtrlHorizontalDisplayRange(uint32_t value)
: x0(value & c_Coord12Mask), x1((value >> c_HorizDisplayRangeX1Shift) & c_Coord12Mask) {}
CtrlHorizontalDisplayRange(const CtrlHorizontalDisplayRange &other) = default;
CtrlHorizontalDisplayRange(CtrlHorizontalDisplayRange &&other) = default;
CtrlHorizontalDisplayRange &operator=(const CtrlHorizontalDisplayRange &other) = default;
@@ -781,7 +802,8 @@ class GPU {
void generateStatsInfo() override {}
void cumulateStats(GPUStats *) override {}
void getVertices(AddTri &&, PixelOp) override {}
CtrlVerticalDisplayRange(uint32_t value) : y0(value & 0x3ff), y1((value >> 10) & 0x3ff) {}
CtrlVerticalDisplayRange(uint32_t value)
: y0(value & c_Coord10Mask), y1((value >> c_DrawingAreaYShift) & c_Coord10Mask) {}
CtrlVerticalDisplayRange(const CtrlVerticalDisplayRange &other) = default;
CtrlVerticalDisplayRange(CtrlVerticalDisplayRange &&other) = default;
CtrlVerticalDisplayRange &operator=(const CtrlVerticalDisplayRange &other) = default;
+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;
+385
View File
@@ -0,0 +1,385 @@
/***************************************************************************
* 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. *
***************************************************************************/
// GTE instruction implementations and public dispatch methods.
//
// Each instruction is implemented as a template parameterized on sf (shift
// factor) and lm (limit mode). The public methods decode these bits from
// the instruction encoding and dispatch to the right instantiation.
//
// MVMVA is further templatized on mx, v, and cv for full compile-time
// elimination of the matrix/vector selection branches.
#include "core/gte.h"
#include "core/gte-internal.h"
#include "core/pgxp_debug.h"
#include "core/pgxp_gte.h"
using namespace PCSX::GTEImpl;
// ============================================================================
// Template instruction implementations
// ============================================================================
// RTPS core: perspective transform for vertex v.
// When last=true, computes the depth queue interpolation at the end.
template <bool sf, bool lm, int v>
static void rtps(bool last) {
mac1() = A1<sf>(int44(trX() << 12) +
r11() * vertexX<v>() + r12() * vertexY<v>() + r13() * vertexZ<v>());
mac2() = A2<sf>(int44(trY() << 12) +
r21() * vertexX<v>() + r22() * vertexY<v>() + r23() * vertexZ<v>());
int64_t rawMac3;
mac3() = A3<sf>(int44(trZ() << 12) +
r31() * vertexX<v>() + r32() * vertexY<v>() + r33() * vertexZ<v>(), rawMac3);
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3sf<sf, lm>(rawMac3);
pushZ(limD<true>(rawMac3));
int32_t hOverSz3 = gteDivide(gteH(), sz3());
sxy0() = sxy1();
sxy1() = sxy2();
double widescreenFactor = PCSX::g_emulator->config().Widescreen ? 0.75 : 1.0;
// ir1()*hOverSz3 can exceed int32_t (hOverSz3 is up to 0x1FFFF), so widen ir first
sx2() = limG1(F(gteOFX() + (int64_t)ir1() * hOverSz3 * widescreenFactor) >> 16);
sy2() = limG2(F(gteOFY() + (int64_t)ir2() * hOverSz3) >> 16);
PGXP_pushSXYZ2s(limG1ia(gteOFX() + (int64_t)ir1() * hOverSz3 * widescreenFactor),
limG2ia(gteOFY() + (int64_t)ir2() * hOverSz3),
std::max((int)sz3(), gteH() / 2), sxy2());
if (last) {
int64_t rawMac0;
mac0() = F(gteDQB() + gteDQA() * hOverSz3, rawMac0);
ir0() = limH(rawMac0);
}
}
// OP: outer product using rotation matrix diagonal
template <bool sf, bool lm>
void PCSX::GTE::op(uint32_t op) {
gteFlag() = 0;
mac1() = A1<sf>(r22() * ir3() - r33() * ir2());
mac2() = A2<sf>(r33() * ir1() - r11() * ir3());
mac3() = A3<sf>(r11() * ir2() - r22() * ir1());
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
}
template <bool sf, bool lm>
void PCSX::GTE::dpcs(uint32_t op) {
gteFlag() = 0;
depthCue<sf, lm>(rgbR() << 16, rgbG() << 16, rgbB() << 16);
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::intpl(uint32_t op) {
gteFlag() = 0;
depthCue<sf, lm>(ir1() << 12, ir2() << 12, ir3() << 12);
pushColor();
}
// MVMVA: fully templatized wrapper for dispatch table
template <bool sf, bool lm, int mx, int v, int cv>
static void mvmvaImpl() {
gteFlag() = 0;
matrixVectorMultiply<sf, lm, mx, v, cv>();
}
// NCDS core: used by NCDS (v=0) and NCDT (v=0,1,2)
template <bool sf, bool lm, int v>
static void ncdsCore() {
lightTransform<sf, lm, v>();
colorMatrix<sf, lm>();
depthCueColor<sf, lm>();
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::cdp(uint32_t op) {
gteFlag() = 0;
colorMatrix<sf, lm>();
depthCueColor<sf, lm>();
pushColor();
}
// NCCS core: used by NCCS (v=0) and NCCT (v=0,1,2)
template <bool sf, bool lm, int v>
static void nccsCore() {
lightTransform<sf, lm, v>();
colorMatrix<sf, lm>();
colorApply<sf, lm>();
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::cc(uint32_t op) {
gteFlag() = 0;
colorMatrix<sf, lm>();
colorApply<sf, lm>();
pushColor();
}
// NCS core: used by NCS (v=0) and NCT (v=0,1,2)
template <bool sf, bool lm, int v>
static void ncsCore() {
lightTransform<sf, lm, v>();
colorMatrix<sf, lm>();
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::sqr(uint32_t op) {
gteFlag() = 0;
mac1() = A1<sf>(ir1() * ir1());
mac2() = A2<sf>(ir2() * ir2());
mac3() = A3<sf>(ir3() * ir3());
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
}
template <bool sf, bool lm>
void PCSX::GTE::dcpl(uint32_t op) {
gteFlag() = 0;
depthCueColor<sf, lm>();
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::dpct(uint32_t op) {
gteFlag() = 0;
for (int v = 0; v < 3; v++) {
depthCue<sf, lm>(rgb0R() << 16, rgb0G() << 16, rgb0B() << 16);
pushColor();
}
}
template <bool sf, bool lm>
void PCSX::GTE::gpf(uint32_t op) {
gteFlag() = 0;
mac1() = A1<sf>(ir0() * ir1());
mac2() = A2<sf>(ir0() * ir2());
mac3() = A3<sf>(ir0() * ir3());
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
pushColor();
}
template <bool sf, bool lm>
void PCSX::GTE::gpl(uint32_t op) {
gteFlag() = 0;
int64_t shiftedMac1, shiftedMac2, shiftedMac3;
if constexpr (sf) {
shiftedMac1 = (int64_t)mac1() << 12; // <<12 on int32_t overflows
shiftedMac2 = (int64_t)mac2() << 12;
shiftedMac3 = (int64_t)mac3() << 12;
} else {
shiftedMac1 = mac1();
shiftedMac2 = mac2();
shiftedMac3 = mac3();
}
mac1() = A1<sf>(shiftedMac1 + ir0() * ir1());
mac2() = A2<sf>(shiftedMac2 + ir0() * ir2());
mac3() = A3<sf>(shiftedMac3 + ir0() * ir3());
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
pushColor();
}
// ============================================================================
// MVMVA dispatch table (256 entries: sf * lm * mx * v * cv)
// ============================================================================
namespace {
template <bool sf, bool lm, int mx, int v, int cv>
struct MvmvaEntry {
static void fn() { mvmvaImpl<sf, lm, mx, v, cv>(); }
};
using MvmvaFn = void (*)();
constexpr auto mvmvaTable =
PCSX::GTEImpl::makeMvmvaTable<MvmvaFn, MvmvaEntry>(std::make_index_sequence<256>{});
} // anonymous namespace
// ============================================================================
// Public dispatch methods
// ============================================================================
#define GTE_DISPATCH_SF_LM(method, ...) \
do { \
uint32_t _op = code & 0x1ffffff; \
switch (sfLmIndex(_op)) { \
case 0: method<false, false>(_op, ##__VA_ARGS__); break; \
case 1: method<false, true>(_op, ##__VA_ARGS__); break; \
case 2: method<true, false>(_op, ##__VA_ARGS__); break; \
case 3: method<true, true>(_op, ##__VA_ARGS__); break; \
} \
} while (0)
void PCSX::GTE::RTPS(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: rtps<false, false, 0>(true); break;
case 1: rtps<false, true, 0>(true); break;
case 2: rtps<true, false, 0>(true); break;
case 3: rtps<true, true, 0>(true); break;
}
}
void PCSX::GTE::RTPT(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: rtps<false, false, 0>(false); rtps<false, false, 1>(false); rtps<false, false, 2>(true); break;
case 1: rtps<false, true, 0>(false); rtps<false, true, 1>(false); rtps<false, true, 2>(true); break;
case 2: rtps<true, false, 0>(false); rtps<true, false, 1>(false); rtps<true, false, 2>(true); break;
case 3: rtps<true, true, 0>(false); rtps<true, true, 1>(false); rtps<true, true, 2>(true); break;
}
}
void PCSX::GTE::NCLIP(uint32_t code) {
gteFlag() = 0;
if (PGXP_NLCIP_valid(sxy0(), sxy1(), sxy2()))
mac0() = F(PGXP_NCLIP());
else
mac0() = F((int64_t)sx0() * sy1() + sx1() * sy2() + sx2() * sy0() -
sx0() * sy2() - sx1() * sy0() - sx2() * sy1());
}
void PCSX::GTE::OP(uint32_t code) { GTE_DISPATCH_SF_LM(op); }
void PCSX::GTE::DPCS(uint32_t code) { GTE_DISPATCH_SF_LM(dpcs); }
void PCSX::GTE::INTPL(uint32_t code) { GTE_DISPATCH_SF_LM(intpl); }
void PCSX::GTE::MVMVA(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
unsigned sf = (_op >> 19) & 1;
unsigned lm = (_op >> 10) & 1;
unsigned mx = (_op >> 17) & 3;
unsigned v = (_op >> 15) & 3;
unsigned cv = (_op >> 13) & 3;
unsigned idx = (sf << 7) | (lm << 6) | (mx << 4) | (v << 2) | cv;
mvmvaTable[idx]();
}
void PCSX::GTE::NCDS(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: ncdsCore<false, false, 0>(); break;
case 1: ncdsCore<false, true, 0>(); break;
case 2: ncdsCore<true, false, 0>(); break;
case 3: ncdsCore<true, true, 0>(); break;
}
}
void PCSX::GTE::CDP(uint32_t code) { GTE_DISPATCH_SF_LM(cdp); }
void PCSX::GTE::NCDT(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: ncdsCore<false, false, 0>(); ncdsCore<false, false, 1>(); ncdsCore<false, false, 2>(); break;
case 1: ncdsCore<false, true, 0>(); ncdsCore<false, true, 1>(); ncdsCore<false, true, 2>(); break;
case 2: ncdsCore<true, false, 0>(); ncdsCore<true, false, 1>(); ncdsCore<true, false, 2>(); break;
case 3: ncdsCore<true, true, 0>(); ncdsCore<true, true, 1>(); ncdsCore<true, true, 2>(); break;
}
}
void PCSX::GTE::NCCS(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: nccsCore<false, false, 0>(); break;
case 1: nccsCore<false, true, 0>(); break;
case 2: nccsCore<true, false, 0>(); break;
case 3: nccsCore<true, true, 0>(); break;
}
}
void PCSX::GTE::CC(uint32_t code) { GTE_DISPATCH_SF_LM(cc); }
void PCSX::GTE::NCS(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: ncsCore<false, false, 0>(); break;
case 1: ncsCore<false, true, 0>(); break;
case 2: ncsCore<true, false, 0>(); break;
case 3: ncsCore<true, true, 0>(); break;
}
}
void PCSX::GTE::NCT(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: ncsCore<false, false, 0>(); ncsCore<false, false, 1>(); ncsCore<false, false, 2>(); break;
case 1: ncsCore<false, true, 0>(); ncsCore<false, true, 1>(); ncsCore<false, true, 2>(); break;
case 2: ncsCore<true, false, 0>(); ncsCore<true, false, 1>(); ncsCore<true, false, 2>(); break;
case 3: ncsCore<true, true, 0>(); ncsCore<true, true, 1>(); ncsCore<true, true, 2>(); break;
}
}
void PCSX::GTE::SQR(uint32_t code) { GTE_DISPATCH_SF_LM(sqr); }
void PCSX::GTE::DCPL(uint32_t code) { GTE_DISPATCH_SF_LM(dcpl); }
void PCSX::GTE::DPCT(uint32_t code) { GTE_DISPATCH_SF_LM(dpct); }
void PCSX::GTE::AVSZ3(uint32_t code) {
gteFlag() = 0;
int64_t rawMac0;
mac0() = F(gteZSF3() * sz1() + gteZSF3() * sz2() + gteZSF3() * sz3(), rawMac0);
otz() = limD<true>(rawMac0);
}
void PCSX::GTE::AVSZ4(uint32_t code) {
gteFlag() = 0;
int64_t rawMac0;
mac0() = F(gteZSF4() * sz0() + gteZSF4() * sz1() + gteZSF4() * sz2() + gteZSF4() * sz3(), rawMac0);
otz() = limD<true>(rawMac0);
}
void PCSX::GTE::GPF(uint32_t code) { GTE_DISPATCH_SF_LM(gpf); }
void PCSX::GTE::GPL(uint32_t code) { GTE_DISPATCH_SF_LM(gpl); }
void PCSX::GTE::NCCT(uint32_t code) {
uint32_t _op = code & 0x1ffffff;
gteFlag() = 0;
switch (sfLmIndex(_op)) {
case 0: nccsCore<false, false, 0>(); nccsCore<false, false, 1>(); nccsCore<false, false, 2>(); break;
case 1: nccsCore<false, true, 0>(); nccsCore<false, true, 1>(); nccsCore<false, true, 2>(); break;
case 2: nccsCore<true, false, 0>(); nccsCore<true, false, 1>(); nccsCore<true, false, 2>(); break;
case 3: nccsCore<true, true, 0>(); nccsCore<true, true, 1>(); nccsCore<true, true, 2>(); break;
}
}
#undef GTE_DISPATCH_SF_LM
+480
View File
@@ -0,0 +1,480 @@
/***************************************************************************
* 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. *
***************************************************************************/
// GTE internal implementation header.
//
// Shared by gte-transfer.cc and gte-instructions.cc. Not part of the public
// interface. Contains register accessors, arithmetic helpers, limiter functions,
// and pipeline stage templates - everything that the GTE instruction
// implementations need but callers of the GTE class do not.
#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include "core/gte.h"
#include "core/psxemulator.h"
#include "core/r3000a.h"
#include "support/table-generator.h"
namespace PCSX {
namespace GTEImpl {
// ============================================================================
// 44-bit accumulator with per-addition overflow tracking
// ============================================================================
class int44 {
public:
int44(int64_t value)
: m_value(value),
m_posOverflow(value > INT64_C(0x7ffffffffff)),
m_negOverflow(value < INT64_C(-0x80000000000)) {}
int44(int64_t value, bool posOverflow, bool negOverflow)
: m_value(value), m_posOverflow(posOverflow), m_negOverflow(negOverflow) {}
int44 operator+(int64_t rhs) const {
int64_t result = ((m_value + rhs) << 20) >> 20;
return int44(result, m_posOverflow || (result < 0 && m_value >= 0 && rhs >= 0),
m_negOverflow || (result >= 0 && m_value < 0 && rhs < 0));
}
bool positiveOverflow() const { return m_posOverflow; }
bool negativeOverflow() const { return m_negOverflow; }
int64_t value() const { return m_value; }
private:
int64_t m_value;
bool m_posOverflow;
bool m_negOverflow;
};
// ============================================================================
// FLAG register bit definitions
// ============================================================================
namespace Flag {
constexpr uint32_t GTE_ERROR = 1u << 31;
constexpr uint32_t MAC1_POS = GTE_ERROR | (1u << 30);
constexpr uint32_t MAC1_NEG = GTE_ERROR | (1u << 27);
constexpr uint32_t MAC2_POS = GTE_ERROR | (1u << 29);
constexpr uint32_t MAC2_NEG = GTE_ERROR | (1u << 26);
constexpr uint32_t MAC3_POS = GTE_ERROR | (1u << 28);
constexpr uint32_t MAC3_NEG = GTE_ERROR | (1u << 25);
constexpr uint32_t IR1_SAT = GTE_ERROR | (1u << 24);
constexpr uint32_t IR2_SAT = GTE_ERROR | (1u << 23);
constexpr uint32_t IR3_SAT = 1u << 22;
constexpr uint32_t COLOR_R_SAT = 1u << 21;
constexpr uint32_t COLOR_G_SAT = 1u << 20;
constexpr uint32_t COLOR_B_SAT = 1u << 19;
constexpr uint32_t SZ_SAT = GTE_ERROR | (1u << 18);
constexpr uint32_t DIV_OVER = GTE_ERROR | (1u << 17);
constexpr uint32_t MAC0_POS = GTE_ERROR | (1u << 16);
constexpr uint32_t MAC0_NEG = GTE_ERROR | (1u << 15);
constexpr uint32_t SX_SAT = GTE_ERROR | (1u << 14);
constexpr uint32_t SY_SAT = GTE_ERROR | (1u << 13);
constexpr uint32_t IR0_SAT = 1u << 12;
constexpr uint32_t ERROR_BITS = 0x7f87e000u;
} // namespace Flag
// ============================================================================
// Register access
// ============================================================================
inline PAIR* dataRegs() { return g_emulator->m_cpu->m_regs.CP2D.p; }
inline PAIR* ctrlRegs() { return g_emulator->m_cpu->m_regs.CP2C.p; }
// Vertex vectors: compile-time v selection
template <int v>
inline int16_t vertexX() {
if constexpr (v < 3) return dataRegs()[v * 2].sw.l;
else return dataRegs()[9].sw.l;
}
template <int v>
inline int16_t vertexY() {
if constexpr (v < 3) return dataRegs()[v * 2].sw.h;
else return dataRegs()[10].sw.l;
}
template <int v>
inline int16_t vertexZ() {
if constexpr (v < 3) return dataRegs()[v * 2 + 1].sw.l;
else return dataRegs()[11].sw.l;
}
// RGBC
inline uint8_t& rgbR() { return dataRegs()[6].b.l; }
inline uint8_t& rgbG() { return dataRegs()[6].b.h; }
inline uint8_t& rgbB() { return dataRegs()[6].b.h2; }
inline uint8_t& rgbCode() { return dataRegs()[6].b.h3; }
inline uint16_t& otz() { return dataRegs()[7].w.l; }
inline int16_t& ir0() { return dataRegs()[8].sw.l; }
inline int16_t& ir1() { return dataRegs()[9].sw.l; }
inline int16_t& ir2() { return dataRegs()[10].sw.l; }
inline int16_t& ir3() { return dataRegs()[11].sw.l; }
inline uint32_t& sxy0() { return dataRegs()[12].d; }
inline int16_t& sx0() { return dataRegs()[12].sw.l; }
inline int16_t& sy0() { return dataRegs()[12].sw.h; }
inline uint32_t& sxy1() { return dataRegs()[13].d; }
inline int16_t& sx1() { return dataRegs()[13].sw.l; }
inline int16_t& sy1() { return dataRegs()[13].sw.h; }
inline uint32_t& sxy2() { return dataRegs()[14].d; }
inline int16_t& sx2() { return dataRegs()[14].sw.l; }
inline int16_t& sy2() { return dataRegs()[14].sw.h; }
inline uint16_t& sz0() { return dataRegs()[16].w.l; }
inline uint16_t& sz1() { return dataRegs()[17].w.l; }
inline uint16_t& sz2() { return dataRegs()[18].w.l; }
inline uint16_t& sz3() { return dataRegs()[19].w.l; }
inline uint32_t& rgb0() { return dataRegs()[20].d; }
inline uint8_t& rgb0R() { return dataRegs()[20].b.l; }
inline uint8_t& rgb0G() { return dataRegs()[20].b.h; }
inline uint8_t& rgb0B() { return dataRegs()[20].b.h2; }
inline uint32_t& rgb1() { return dataRegs()[21].d; }
inline uint32_t& rgb2() { return dataRegs()[22].d; }
inline uint8_t& rgb2R() { return dataRegs()[22].b.l; }
inline uint8_t& rgb2G() { return dataRegs()[22].b.h; }
inline uint8_t& rgb2B() { return dataRegs()[22].b.h2; }
inline uint8_t& rgb2Cd() { return dataRegs()[22].b.h3; }
inline int32_t& mac0() { return dataRegs()[24].sd; }
inline int32_t& mac1() { return dataRegs()[25].sd; }
inline int32_t& mac2() { return dataRegs()[26].sd; }
inline int32_t& mac3() { return dataRegs()[27].sd; }
// Control registers - rotation matrix
inline int16_t r11() { return ctrlRegs()[0].sw.l; }
inline int16_t r12() { return ctrlRegs()[0].sw.h; }
inline int16_t r13() { return ctrlRegs()[1].sw.l; }
inline int16_t r21() { return ctrlRegs()[1].sw.h; }
inline int16_t r22() { return ctrlRegs()[2].sw.l; }
inline int16_t r23() { return ctrlRegs()[2].sw.h; }
inline int16_t r31() { return ctrlRegs()[3].sw.l; }
inline int16_t r32() { return ctrlRegs()[3].sw.h; }
inline int16_t r33() { return ctrlRegs()[4].sw.l; }
// Control registers used in 64-bit arithmetic return int64_t to avoid casts at every use site.
// The underlying storage is 32-bit or 16-bit; the widening happens here, once.
inline int64_t trX() { return ctrlRegs()[5].sd; }
inline int64_t trY() { return ctrlRegs()[6].sd; }
inline int64_t trZ() { return ctrlRegs()[7].sd; }
inline int64_t rbk() { return ctrlRegs()[13].sd; }
inline int64_t gbk() { return ctrlRegs()[14].sd; }
inline int64_t bbk() { return ctrlRegs()[15].sd; }
inline int64_t rfc() { return ctrlRegs()[21].sd; }
inline int64_t gfc() { return ctrlRegs()[22].sd; }
inline int64_t bfc() { return ctrlRegs()[23].sd; }
inline int64_t gteOFX() { return ctrlRegs()[24].sd; }
inline int64_t gteOFY() { return ctrlRegs()[25].sd; }
inline int16_t gteH() { return ctrlRegs()[26].sw.l; } // stays 16-bit for gteDivide signature
inline int64_t gteDQA() { return ctrlRegs()[27].sw.l; }
inline int64_t gteDQB() { return ctrlRegs()[28].sd; }
inline int64_t gteZSF3() { return ctrlRegs()[29].sw.l; }
inline int64_t gteZSF4() { return ctrlRegs()[30].sw.l; }
inline uint32_t& gteFlag() { return ctrlRegs()[31].d; }
// Matrix element access - compile-time (mx, row, col)
template <int mx, int row, int col>
inline int32_t matrixElement() {
if constexpr (mx < 3) {
constexpr int linear = row * 3 + col;
constexpr int regIdx = mx * 8 + linear / 2;
if constexpr (linear & 1) return ctrlRegs()[regIdx].sw.h;
else return ctrlRegs()[regIdx].sw.l;
} else {
// Garbage matrix: {-R<<4, R<<4, IR0, R13, R13, R13, R22, R22, R22}
constexpr int linear = row * 3 + col;
if constexpr (linear == 0) { return (-static_cast<int32_t>(dataRegs()[6].b.l)) << 4; }
else if constexpr (linear == 1) { return static_cast<int32_t>(dataRegs()[6].b.l) << 4; }
else if constexpr (linear == 2) { return ir0(); }
else if constexpr (linear <= 5) { return ctrlRegs()[1].sw.l; } // R13
else { return ctrlRegs()[2].sw.l; } // R22
}
}
// Control vector component - compile-time (cv, component)
template <int cv, int component>
inline int64_t controlVector() {
if constexpr (cv == 3) return 0;
else return ctrlRegs()[cv * 8 + 5 + component].sd;
}
// ============================================================================
// Division
// ============================================================================
// UNR reciprocal table generator for GTE division.
// Formula from hardware: unrTable[i] = max(0, ((0x40000 / (i + 0x100)) + 1) / 2 - 0x101)
struct UNRGenerator {
static consteval uint8_t calculateValue(size_t i) {
int val = ((0x40000 / (int)(i + 0x100)) + 1) / 2 - 0x101;
return static_cast<uint8_t>(val < 0 ? 0 : val);
}
};
inline constexpr auto unrTable = generateTable<257, UNRGenerator>();
inline uint32_t gteDivide(uint16_t numerator, uint16_t denominator) {
if (numerator >= denominator * 2) {
gteFlag() |= Flag::DIV_OVER;
return 0x1ffff;
}
int shift = GTE::countLeadingZeros16(denominator);
int r1 = (denominator << shift) & 0x7fff;
int r2 = unrTable[((r1 + 0x40) >> 7)] + 0x101;
int r3 = ((0x80 - (r2 * (r1 + 0x8000))) >> 8) & 0x1ffff;
uint32_t reciprocal = ((r2 * r3) + 0x80) >> 8;
uint32_t result = ((static_cast<uint64_t>(reciprocal) * (numerator << shift)) + 0x8000) >> 16;
return std::min<uint32_t>(0x1ffff, result);
}
// ============================================================================
// Limiter functions
// ============================================================================
inline int32_t lim(int32_t value, int32_t max, int32_t min, uint32_t flag) {
if (value > max) { gteFlag() |= flag; return max; }
if (value < min) { gteFlag() |= flag; return min; }
return value;
}
template <bool sf>
inline int64_t gteShift(int64_t a) {
if constexpr (sf) return a >> 12;
else return a;
}
template <bool sf>
inline int32_t bounds(int44 value, uint32_t posFlag, uint32_t negFlag) {
if (value.positiveOverflow()) gteFlag() |= posFlag;
if (value.negativeOverflow()) gteFlag() |= negFlag;
return static_cast<int32_t>(gteShift<sf>(value.value()));
}
template <bool sf>
inline int32_t A1(int44 a) { return bounds<sf>(a, Flag::MAC1_POS, Flag::MAC1_NEG); }
template <bool sf>
inline int32_t A2(int44 a) { return bounds<sf>(a, Flag::MAC2_POS, Flag::MAC2_NEG); }
template <bool sf>
inline int32_t A3(int44 a, int64_t& rawOut) {
rawOut = a.value();
return bounds<sf>(a, Flag::MAC3_POS, Flag::MAC3_NEG);
}
template <bool sf>
inline int32_t A3(int44 a) { return bounds<sf>(a, Flag::MAC3_POS, Flag::MAC3_NEG); }
inline int64_t F(int64_t a, int64_t& rawOut) {
rawOut = a;
if (a > INT64_C(0x7fffffff)) gteFlag() |= Flag::MAC0_POS;
if (a < INT64_C(-0x80000000)) gteFlag() |= Flag::MAC0_NEG;
return a;
}
inline int64_t F(int64_t a) {
if (a > INT64_C(0x7fffffff)) gteFlag() |= Flag::MAC0_POS;
if (a < INT64_C(-0x80000000)) gteFlag() |= Flag::MAC0_NEG;
return a;
}
template <bool lm> inline int32_t limB1(int32_t a) { return lim(a, 0x7fff, lm ? 0 : -0x8000, Flag::IR1_SAT); }
template <bool lm> inline int32_t limB2(int32_t a) { return lim(a, 0x7fff, lm ? 0 : -0x8000, Flag::IR2_SAT); }
template <bool lm> inline int32_t limB3(int32_t a) { return lim(a, 0x7fff, lm ? 0 : -0x8000, Flag::IR3_SAT); }
template <bool sf, bool lm>
inline int32_t limB3sf(int64_t rawMac3) {
int32_t valueSf = static_cast<int32_t>(gteShift<sf>(rawMac3));
int32_t value12 = static_cast<int32_t>(rawMac3 >> 12);
constexpr int32_t min = lm ? 0 : -0x8000;
if (value12 < -0x8000 || value12 > 0x7fff) gteFlag() |= Flag::IR3_SAT;
return std::clamp<int32_t>(valueSf, min, 0x7fff);
}
inline int32_t limC1(int32_t a) { return lim(a, 0xff, 0, Flag::COLOR_R_SAT); }
inline int32_t limC2(int32_t a) { return lim(a, 0xff, 0, Flag::COLOR_G_SAT); }
inline int32_t limC3(int32_t a) { return lim(a, 0xff, 0, Flag::COLOR_B_SAT); }
template <bool sf>
inline int32_t limD(int64_t a) { return lim(static_cast<int32_t>(gteShift<sf>(a)), 0xffff, 0, Flag::SZ_SAT); }
inline int32_t limG1(int64_t a) {
if (a > 0x3ff) { gteFlag() |= Flag::SX_SAT; return 0x3ff; }
if (a < -0x400) { gteFlag() |= Flag::SX_SAT; return -0x400; }
return static_cast<int32_t>(a);
}
inline int32_t limG2(int64_t a) {
if (a > 0x3ff) { gteFlag() |= Flag::SY_SAT; return 0x3ff; }
if (a < -0x400) { gteFlag() |= Flag::SY_SAT; return -0x400; }
return static_cast<int32_t>(a);
}
inline int32_t limG1ia(int64_t a) { return static_cast<int32_t>(std::clamp<int64_t>(a, -0x4000000, 0x3ffffff)); }
inline int32_t limG2ia(int64_t a) { return static_cast<int32_t>(std::clamp<int64_t>(a, -0x4000000, 0x3ffffff)); }
inline int32_t limH(int64_t rawMac0) {
int64_t valueSf = rawMac0 >> 12;
int32_t value12 = static_cast<int32_t>(rawMac0 >> 12);
if (valueSf < 0 || valueSf > 0x1000) gteFlag() |= Flag::IR0_SAT;
return std::clamp<int32_t>(value12, 0, 0x1000);
}
// ============================================================================
// FIFO operations
// ============================================================================
inline void pushZ(uint16_t z) {
sz0() = sz1(); sz1() = sz2(); sz2() = sz3(); sz3() = z;
}
inline void pushColor() {
rgb0() = rgb1(); rgb1() = rgb2();
rgb2Cd() = rgbCode();
rgb2R() = limC1(mac1() >> 4);
rgb2G() = limC2(mac2() >> 4);
rgb2B() = limC3(mac3() >> 4);
}
// ============================================================================
// Pipeline stage: matrix-vector multiply (fully templatized)
// ============================================================================
template <bool sf, bool lm, int mx, int v, int cv>
inline void matrixVectorMultiply(int64_t& rawMac3) {
if constexpr (cv == 2) {
// FC bug path: columns 1-2 first, then column 0 for FLAG only
mac1() = A1<sf>(int44(matrixElement<mx, 0, 1>() * vertexY<v>()) +
matrixElement<mx, 0, 2>() * vertexZ<v>());
mac2() = A2<sf>(int44(matrixElement<mx, 1, 1>() * vertexY<v>()) +
matrixElement<mx, 1, 2>() * vertexZ<v>());
mac3() = A3<sf>(int44(matrixElement<mx, 2, 1>() * vertexY<v>()) +
matrixElement<mx, 2, 2>() * vertexZ<v>(), rawMac3);
// Column 0: FLAG side effects only, results discarded
limB1<false>(A1<sf>(int44(controlVector<cv, 0>() << 12) +
matrixElement<mx, 0, 0>() * vertexX<v>()));
limB2<false>(A2<sf>(int44(controlVector<cv, 1>() << 12) +
matrixElement<mx, 1, 0>() * vertexX<v>()));
limB3<false>(A3<sf>(int44(controlVector<cv, 2>() << 12) +
matrixElement<mx, 2, 0>() * vertexX<v>()));
} else {
mac1() = A1<sf>(int44(controlVector<cv, 0>() << 12) +
matrixElement<mx, 0, 0>() * vertexX<v>() +
matrixElement<mx, 0, 1>() * vertexY<v>() +
matrixElement<mx, 0, 2>() * vertexZ<v>());
mac2() = A2<sf>(int44(controlVector<cv, 1>() << 12) +
matrixElement<mx, 1, 0>() * vertexX<v>() +
matrixElement<mx, 1, 1>() * vertexY<v>() +
matrixElement<mx, 1, 2>() * vertexZ<v>());
mac3() = A3<sf>(int44(controlVector<cv, 2>() << 12) +
matrixElement<mx, 2, 0>() * vertexX<v>() +
matrixElement<mx, 2, 1>() * vertexY<v>() +
matrixElement<mx, 2, 2>() * vertexZ<v>(), rawMac3);
}
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
}
template <bool sf, bool lm, int mx, int v, int cv>
inline void matrixVectorMultiply() {
int64_t unused;
matrixVectorMultiply<sf, lm, mx, v, cv>(unused);
}
// ============================================================================
// Pipeline stage: light transform - L * V(v) -> MAC/IR
// ============================================================================
template <bool sf, bool lm, int v>
inline void lightTransform() {
matrixVectorMultiply<sf, lm, 1, v, 3>();
}
// ============================================================================
// Pipeline stage: color matrix - BK + C * IR -> MAC/IR
// ============================================================================
template <bool sf, bool lm>
inline void colorMatrix() {
matrixVectorMultiply<sf, lm, 2, 3, 1>();
}
// ============================================================================
// Pipeline stage: depth cue interpolation
// ============================================================================
template <bool sf, bool lm>
inline void depthCue(int64_t inR, int64_t inG, int64_t inB) {
mac1() = A1<sf>(inR + ir0() * limB1<false>(A1<sf>((rfc() << 12) - inR)));
mac2() = A2<sf>(inG + ir0() * limB2<false>(A2<sf>((gfc() << 12) - inG)));
int64_t rawMac3;
mac3() = A3<sf>(inB + ir0() * limB3<false>(A3<sf>((bfc() << 12) - inB)), rawMac3);
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
}
template <bool sf, bool lm>
inline void depthCueColor() {
depthCue<sf, lm>((int64_t)(rgbR() << 4) * ir1(),
(int64_t)(rgbG() << 4) * ir2(),
(int64_t)(rgbB() << 4) * ir3());
}
// ============================================================================
// Pipeline stage: color apply - RGBC * IR -> MAC/IR
// ============================================================================
template <bool sf, bool lm>
inline void colorApply() {
mac1() = A1<sf>((int64_t)(rgbR() << 4) * ir1());
mac2() = A2<sf>((int64_t)(rgbG() << 4) * ir2());
mac3() = A3<sf>((int64_t)(rgbB() << 4) * ir3());
ir1() = limB1<lm>(mac1());
ir2() = limB2<lm>(mac2());
ir3() = limB3<lm>(mac3());
}
// ============================================================================
// Dispatch helpers
// ============================================================================
inline unsigned sfLmIndex(uint32_t op) {
return ((op >> 18) & 2) | ((op >> 10) & 1);
}
// Generate a 256-entry dispatch table for MVMVA (sf * lm * mx * v * cv).
// Index layout: [sf:1][lm:1][mx:2][v:2][cv:2]
template <typename Fn, template <bool, bool, int, int, int> class Impl, size_t... Is>
constexpr auto makeMvmvaTable(std::index_sequence<Is...>) {
return std::array<Fn, sizeof...(Is)>{
Impl<bool(Is >> 7), bool((Is >> 6) & 1), int((Is >> 4) & 3), int((Is >> 2) & 3), int(Is & 3)>::fn...};
}
} // namespace GTEImpl
} // namespace PCSX
+126
View File
@@ -0,0 +1,126 @@
/***************************************************************************
* 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. *
***************************************************************************/
// GTE COP2 data transfer operations: MFC2, MTC2, CFC2, CTC2, LWC2, SWC2.
#include "core/gte.h"
#include "core/gte-internal.h"
#include "core/psxmem.h"
using namespace PCSX::GTEImpl;
uint32_t PCSX::GTE::MFC2(uint32_t code) {
return MFC2(static_cast<int>(_Rd_));
}
uint32_t PCSX::GTE::MFC2(int reg) {
auto* d = dataRegs();
switch (reg) {
case 1: case 3: case 5:
case 8: case 9: case 10: case 11:
d[reg].d = static_cast<int32_t>(d[reg].sw.l);
break;
case 7: case 16: case 17: case 18: case 19:
d[reg].d = static_cast<uint32_t>(d[reg].w.l);
break;
case 15:
d[reg].d = sxy2();
break;
case 28: case 29:
d[reg].d = lim(ir1() >> 7, 0x1f, 0, 0) |
(lim(ir2() >> 7, 0x1f, 0, 0) << 5) |
(lim(ir3() >> 7, 0x1f, 0, 0) << 10);
break;
}
return d[reg].d;
}
uint32_t PCSX::GTE::CFC2(uint32_t code) {
return ctrlRegs()[_Rd_].d;
}
void PCSX::GTE::MTC2(uint32_t value, int reg) {
auto* d = dataRegs();
switch (reg) {
case 15:
sxy0() = sxy1();
sxy1() = sxy2();
sxy2() = value;
break;
case 28:
ir1() = (value & 0x1f) << 7;
ir2() = (value & 0x3e0) << 2;
ir3() = (value & 0x7c00) >> 3;
break;
case 30:
d[31].d = countLeadingBits(value);
break;
case 31:
return;
}
d[reg].d = value;
}
void PCSX::GTE::MTC2(uint32_t code) {
MTC2(g_emulator->m_cpu->m_regs.GPR.r[_Rt_], _Rd_);
}
void PCSX::GTE::CTC2(uint32_t value, int reg) {
switch (reg) {
case 4: case 12: case 20:
case 26: case 27: case 29: case 30:
value = static_cast<int32_t>(static_cast<int16_t>(value));
break;
case 31:
value = value & 0x7ffff000;
if (value & Flag::ERROR_BITS) value |= Flag::GTE_ERROR;
break;
}
ctrlRegs()[reg].d = value;
}
void PCSX::GTE::CTC2(uint32_t code) {
CTC2(g_emulator->m_cpu->m_regs.GPR.r[_Rt_], _Rd_);
}
void PCSX::GTE::LWC2(uint32_t code) {
uint32_t addr = g_emulator->m_cpu->m_regs.GPR.r[_Rs_] + _Imm_;
if (addr & 3) {
g_emulator->m_cpu->m_regs.pc -= 4;
g_system->log(LogClass::CPU, _("Unaligned address 0x%08x in LWC2 from 0x%08x\n"), addr,
g_emulator->m_cpu->m_regs.pc);
g_emulator->m_cpu->m_regs.CP0.n.BadVAddr = addr;
g_emulator->m_cpu->exception(R3000Acpu::Exception::LoadAddressError, g_emulator->m_cpu->m_inDelaySlot);
return;
}
MTC2(g_emulator->m_mem->read32(addr), _Rt_);
}
void PCSX::GTE::SWC2(uint32_t code) {
uint32_t addr = g_emulator->m_cpu->m_regs.GPR.r[_Rs_] + _Imm_;
if (addr & 3) {
g_emulator->m_cpu->m_regs.pc -= 4;
g_system->log(LogClass::CPU, _("Unaligned address 0x%08x in SWC2 from 0x%08x\n"), addr,
g_emulator->m_cpu->m_regs.pc);
g_emulator->m_cpu->m_regs.CP0.n.BadVAddr = addr;
g_emulator->m_cpu->exception(R3000Acpu::Exception::StoreAddressError, g_emulator->m_cpu->m_inDelaySlot);
return;
}
g_emulator->m_mem->write32(addr, MFC2(static_cast<int>(_Rt_)));
}
-1026
View File
File diff suppressed because it is too large Load Diff
+32 -94
View File
@@ -1,5 +1,5 @@
/***************************************************************************
* Copyright (C) 2007 Ryan Schultz, PCSX-df Team, PCSX team *
* 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 *
@@ -18,64 +18,32 @@
***************************************************************************/
#pragma once
#include <bit>
#include <cstdint>
#include "core/psxemulator.h"
#include "core/r3000a.h"
// WTF termios
// termios defines NCCS which collides with our method name
#undef NCCS
#define gteoB (PCSX::g_emulator->m_cpu->m_regs.GPR.r[_Rs_] + _Imm_)
#define gteop(instruction) ((instruction) & 0x1ffffff)
namespace PCSX {
class GTE {
public:
uint32_t MFC2(uint32_t code) {
// CPU[Rt] = GTE_D[Rd]
return MFC2_internal(_Rd_);
}
uint32_t MFC2(int reg) { return MFC2_internal(reg); }
uint32_t CFC2(uint32_t code) {
// CPU[Rt] = GTE_C[Rd]
return PCSX::g_emulator->m_cpu->m_regs.CP2C.p[_Rd_].d;
}
void CTC2(uint32_t value, int reg) { CTC2_internal(value, reg); }
void MTC2(uint32_t value, int reg) { MTC2_internal(value, reg); }
void MTC2(uint32_t code) { MTC2_internal(PCSX::g_emulator->m_cpu->m_regs.GPR.r[_Rt_], _Rd_); }
void CTC2(uint32_t code) { CTC2_internal(PCSX::g_emulator->m_cpu->m_regs.GPR.r[_Rt_], _Rd_); }
void LWC2(uint32_t code) {
if (gteoB & 3) {
PCSX::g_emulator->m_cpu->m_regs.pc -= 4;
PCSX::g_system->log(PCSX::LogClass::CPU, _("Unaligned address 0x%08x in LWC2 from 0x%08x\n"), gteoB,
PCSX::g_emulator->m_cpu->m_regs.pc);
PCSX::g_emulator->m_cpu->m_regs.CP0.n.BadVAddr = gteoB;
PCSX::g_emulator->m_cpu->exception(PCSX::R3000Acpu::Exception::LoadAddressError,
PCSX::g_emulator->m_cpu->m_inDelaySlot);
return;
}
MTC2_internal(PCSX::g_emulator->m_mem->read32(gteoB), _Rt_);
}
void SWC2(uint32_t code) {
if (gteoB & 3) {
PCSX::g_emulator->m_cpu->m_regs.pc -= 4;
PCSX::g_system->log(PCSX::LogClass::CPU, _("Unaligned address 0x%08x in SWC2 from 0x%08x\n"), gteoB,
PCSX::g_emulator->m_cpu->m_regs.pc);
PCSX::g_emulator->m_cpu->m_regs.CP0.n.BadVAddr = gteoB;
PCSX::g_emulator->m_cpu->exception(PCSX::R3000Acpu::Exception::StoreAddressError,
PCSX::g_emulator->m_cpu->m_inDelaySlot);
return;
}
PCSX::g_emulator->m_mem->write32(gteoB, MFC2_internal(_Rt_));
}
// COP2 data transfer operations
uint32_t MFC2(uint32_t code);
uint32_t MFC2(int reg);
uint32_t CFC2(uint32_t code);
void MTC2(uint32_t value, int reg);
void MTC2(uint32_t code);
void CTC2(uint32_t value, int reg);
void CTC2(uint32_t code);
void LWC2(uint32_t code);
void SWC2(uint32_t code);
// GTE function instructions (COP2 imm25)
void RTPS(uint32_t code);
void NCLIP(uint32_t code);
void OP(uint32_t code);
@@ -99,61 +67,31 @@ class GTE {
void GPL(uint32_t code);
void NCCT(uint32_t code);
// If MSB is set, return the number of leading ones, else return the number of leading zeroes
// For an input of 0, 32 is returned
// Count leading redundant sign bits. For positive: leading zeros. For negative: leading ones.
// Returns 32 for input of 0 or 0xffffffff.
static uint32_t countLeadingBits(uint32_t value) {
if (value & 0x80000000) {
value = ~value;
}
if (value & 0x80000000) value = ~value;
return std::countl_zero<uint32_t>(value);
}
// Count leading zeroes of a 16-bit value. For an input of 0, 16 is returned
// Count leading zeros of a 16-bit value. Returns 16 for input of 0.
static uint32_t countLeadingZeros16(uint16_t value) {
// Use a 32-bit CLZ as it's what's most commonly available and Clang/GCC fail to optimize 16-bit CLZ
const auto count = std::countl_zero<uint32_t>((uint32_t)value);
return count - 16;
return std::countl_zero<uint32_t>(static_cast<uint32_t>(value)) - 16;
}
private:
class int44 {
public:
int44(int64_t value)
: m_value(value), m_positive_overflow(value > 0x7ffffffffff), m_negative_overflow(value < -0x80000000000) {}
int44(int64_t value, bool positive_overflow, bool negative_overflow)
: m_value(value), m_positive_overflow(positive_overflow), m_negative_overflow(negative_overflow) {}
int44 operator+(int64_t rhs) {
int64_t value = ((m_value + rhs) << 20) >> 20;
return int44(value, m_positive_overflow || (value < 0 && m_value >= 0 && rhs >= 0),
m_negative_overflow || (value >= 0 && m_value < 0 && rhs < 0));
}
bool positiveOverflow() { return m_positive_overflow; }
bool negativeOverflow() { return m_negative_overflow; }
int64_t value() { return m_value; }
private:
int64_t m_value;
bool m_positive_overflow;
bool m_negative_overflow;
};
int s_sf;
int64_t s_mac0;
int64_t s_mac3;
int32_t BOUNDS(int44 value, int max_flag, int min_flag);
int32_t A1(int44 a);
int32_t A2(int44 a);
int32_t A3(int44 a);
int64_t F(int64_t a);
uint32_t MFC2_internal(int reg);
void MTC2_internal(uint32_t value, int reg);
void CTC2_internal(uint32_t value, int reg);
void pushZ(uint16_t z);
// Template instruction implementations, parameterized on sf (shift factor) and lm (limit mode).
// Defined in gte-instructions.cc. The public methods dispatch to these based on the encoding.
template <bool sf, bool lm> void op(uint32_t op);
template <bool sf, bool lm> void dpcs(uint32_t op);
template <bool sf, bool lm> void intpl(uint32_t op);
template <bool sf, bool lm> void cdp(uint32_t op);
template <bool sf, bool lm> void cc(uint32_t op);
template <bool sf, bool lm> void sqr(uint32_t op);
template <bool sf, bool lm> void dcpl(uint32_t op);
template <bool sf, bool lm> void dpct(uint32_t op);
template <bool sf, bool lm> void gpf(uint32_t op);
template <bool sf, bool lm> void gpl(uint32_t op);
};
} // namespace PCSX
+245 -18
View File
@@ -46,12 +46,98 @@ LuaFile* fileisoOpen(LuaIso* wrapper, uint32_t lba, uint32_t size, enum SectorMo
uint32_t getIsoTN(LuaIso* wrapper);
MSF getIsoTD(LuaIso* wrapper, uint32_t tn);
typedef struct { char opaque[?]; } DirEntries;
DirEntries* readerListDir(IsoReader* reader, const char* path);
void deleteDirEntries(DirEntries* entries);
uint32_t dirEntriesCount(DirEntries* entries);
const char* dirEntryName(DirEntries* entries, uint32_t index);
uint32_t dirEntryLBA(DirEntries* entries, uint32_t index);
uint32_t dirEntrySize(DirEntries* entries, uint32_t index);
bool dirEntryIsDir(DirEntries* entries, uint32_t index);
typedef struct { char opaque[?]; } GapList;
GapList* readerFindGaps(IsoReader* reader);
void deleteGapList(GapList* list);
uint32_t gapListCount(GapList* list);
uint32_t gapEntryLBA(GapList* list, uint32_t index);
uint32_t gapEntrySectors(GapList* list, uint32_t index);
typedef struct { char opaque[?]; } ISO9660Builder;
ISO9660Builder* createIsoBuilder(LuaFile* out);
void deleteIsoBuilder(ISO9660Builder* builder);
bool isoBuilderFailed(ISO9660Builder* builder);
void isoBuilderWriteLicense(ISO9660Builder* builder, LuaFile*);
void isoBuilderWriteSector(ISO9660Builder* builder, const uint8_t* sectorData, enum SectorMode mode);
void isoBuilderClose(ISO9660Builder* builder);
uint32_t isoBuilderWriteSector(ISO9660Builder* builder, const uint8_t* sectorData, uint32_t dataSize, enum SectorMode mode);
uint32_t isoBuilderWriteSectorAt(ISO9660Builder* builder, const uint8_t* sectorData, uint32_t dataSize, uint32_t lba, enum SectorMode mode);
uint32_t isoBuilderGetCurrentLBA(ISO9660Builder* builder);
void isoBuilderClose(ISO9660Builder* builder, uint32_t threadCount);
// High-level filesystem-aware ISO builder
typedef struct { char opaque[?]; } IsoDirTree;
// PVD field getters (copy trimmed string to caller buffer)
void hlPvdGetSystemIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetVolumeIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetVolSetIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetPublisherIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetDataPreparerIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetApplicationIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetCopyrightFileIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetAbstractFileIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
void hlPvdGetBibliographicFileIdent(ISO9660Builder* b, char* buf, uint32_t bufSize);
// PVD field setters (pad with spaces)
void hlPvdSetSystemIdent(ISO9660Builder* b, const char* val);
void hlPvdSetVolumeIdent(ISO9660Builder* b, const char* val);
void hlPvdSetVolSetIdent(ISO9660Builder* b, const char* val);
void hlPvdSetPublisherIdent(ISO9660Builder* b, const char* val);
void hlPvdSetDataPreparerIdent(ISO9660Builder* b, const char* val);
void hlPvdSetApplicationIdent(ISO9660Builder* b, const char* val);
void hlPvdSetCopyrightFileIdent(ISO9660Builder* b, const char* val);
void hlPvdSetAbstractFileIdent(ISO9660Builder* b, const char* val);
void hlPvdSetBibliographicFileIdent(ISO9660Builder* b, const char* val);
// DirTree creation
IsoDirTree* hlCreateRoot(ISO9660Builder* b, uint32_t sectorCount);
IsoDirTree* hlCreateDir(ISO9660Builder* b, IsoDirTree* parent, const char* name, uint32_t sectorCount);
IsoDirTree* hlCreateFile(ISO9660Builder* b, IsoDirTree* parent, const char* name, LuaFile* content);
// DirTree property access
const char* dirTreeGetName(IsoDirTree* node);
uint32_t dirTreeGetSize(IsoDirTree* node);
uint32_t dirTreeGetLBA(IsoDirTree* node);
bool dirTreeIsDir(IsoDirTree* node);
bool dirTreeIsHidden(IsoDirTree* node);
void dirTreeSetHidden(IsoDirTree* node, bool val);
bool dirTreeShouldSkip(IsoDirTree* node);
void dirTreeSetSkip(IsoDirTree* node, bool val);
bool dirTreeHasAnchorLBA(IsoDirTree* node);
uint32_t dirTreeGetAnchorLBA(IsoDirTree* node);
void dirTreeSetAnchorLBA(IsoDirTree* node, uint32_t lba);
void dirTreeClearAnchorLBA(IsoDirTree* node);
bool dirTreeHasDeclaredSize(IsoDirTree* node);
uint32_t dirTreeGetDeclaredSize(IsoDirTree* node);
void dirTreeSetDeclaredSize(IsoDirTree* node, uint32_t size);
void dirTreeClearDeclaredSize(IsoDirTree* node);
bool dirTreeHasXA(IsoDirTree* node);
void dirTreeSetHasXA(IsoDirTree* node, bool val);
void dirTreeSetSectorMode(IsoDirTree* node, enum SectorMode mode);
uint16_t dirTreeGetXAAttribs(IsoDirTree* node);
void dirTreeSetXAAttribs(IsoDirTree* node, uint16_t val);
uint8_t dirTreeGetXAFileNum(IsoDirTree* node);
void dirTreeSetXAFileNum(IsoDirTree* node, uint8_t val);
// DirTree navigation
IsoDirTree* dirTreeParent(IsoDirTree* node);
IsoDirTree* dirTreeFirstChild(IsoDirTree* node);
IsoDirTree* dirTreeNextSibling(IsoDirTree* node);
// DirTree date access
void dirTreeSetDate(IsoDirTree* node, uint8_t year, uint8_t month, uint8_t day,
uint8_t hour, uint8_t minute, uint8_t second, uint8_t offset);
uint8_t dirTreeGetDateYear(IsoDirTree* node);
uint8_t dirTreeGetDateMonth(IsoDirTree* node);
uint8_t dirTreeGetDateDay(IsoDirTree* node);
]]
@@ -61,6 +147,34 @@ local function createIsoReaderWrapper(isoReader)
local reader = {
_wrapper = ffi.gc(isoReader, C.deleteIsoReader),
open = function(self, fname) return Support.File._createFileWrapper(C.readerOpen(self._wrapper, fname)) end,
findGaps = function(self)
local gapList = ffi.gc(C.readerFindGaps(self._wrapper), C.deleteGapList)
local count = C.gapListCount(gapList)
local result = {}
for i = 0, count - 1 do
table.insert(result, {
lba = C.gapEntryLBA(gapList, i),
sectors = C.gapEntrySectors(gapList, i),
})
end
return result
end,
listDir = function(self, path)
if path == nil then path = '' end
local entries = ffi.gc(C.readerListDir(self._wrapper, path), C.deleteDirEntries)
local count = C.dirEntriesCount(entries)
local result = {}
for i = 0, count - 1 do
local name = ffi.string(C.dirEntryName(entries, i))
table.insert(result, {
name = name,
lba = C.dirEntryLBA(entries, i),
size = C.dirEntrySize(entries, i),
isDir = C.dirEntryIsDir(entries, i),
})
end
return result
end,
}
return reader
end
@@ -91,24 +205,137 @@ local function createIsoWrapper(wrapper)
return iso
end
local function createIsoBuilderWrapper(wrapper)
local iso = {
_wrapper = ffi.gc(wrapper, function(self)
C.isoBuilderClose(self)
C.deleteIsoBuilder(self)
end),
writeLicense = function(self, file)
if not file then file = Support.File.failedFile() end
C.isoBuilderWriteLicense(self._wrapper, file._wrapper)
-- High-level ISO builder (filesystem-aware)
local pvdBuf = ffi.new('char[?]', 513) -- max CString size (512 for ApplicationUse) + 1
local function createDirTreeWrapper(node)
if node == nil then return nil end
local wrapper = {
_node = node,
getName = function(self) return ffi.string(C.dirTreeGetName(self._node)) end,
getSize = function(self) return C.dirTreeGetSize(self._node) end,
getLBA = function(self) return C.dirTreeGetLBA(self._node) end,
isDir = function(self) return C.dirTreeIsDir(self._node) end,
isHidden = function(self) return C.dirTreeIsHidden(self._node) end,
setHidden = function(self, val) C.dirTreeSetHidden(self._node, val) end,
shouldSkip = function(self) return C.dirTreeShouldSkip(self._node) end,
setSkip = function(self, val) C.dirTreeSetSkip(self._node, val) end,
hasAnchorLBA = function(self) return C.dirTreeHasAnchorLBA(self._node) end,
getAnchorLBA = function(self) return C.dirTreeGetAnchorLBA(self._node) end,
setAnchorLBA = function(self, lba) C.dirTreeSetAnchorLBA(self._node, lba) end,
clearAnchorLBA = function(self) C.dirTreeClearAnchorLBA(self._node) end,
hasDeclaredSize = function(self) return C.dirTreeHasDeclaredSize(self._node) end,
getDeclaredSize = function(self) return C.dirTreeGetDeclaredSize(self._node) end,
setDeclaredSize = function(self, size) C.dirTreeSetDeclaredSize(self._node, size) end,
clearDeclaredSize = function(self) C.dirTreeClearDeclaredSize(self._node) end,
hasXA = function(self) return C.dirTreeHasXA(self._node) end,
setXA = function(self, val) C.dirTreeSetHasXA(self._node, val) end,
setSectorMode = function(self, mode) C.dirTreeSetSectorMode(self._node, mode) end,
getXAAttribs = function(self) return C.dirTreeGetXAAttribs(self._node) end,
setXAAttribs = function(self, val) C.dirTreeSetXAAttribs(self._node, val) end,
getXAFileNum = function(self) return C.dirTreeGetXAFileNum(self._node) end,
setXAFileNum = function(self, val) C.dirTreeSetXAFileNum(self._node, val) end,
parent = function(self) return createDirTreeWrapper(C.dirTreeParent(self._node)) end,
firstChild = function(self) return createDirTreeWrapper(C.dirTreeFirstChild(self._node)) end,
nextSibling = function(self) return createDirTreeWrapper(C.dirTreeNextSibling(self._node)) end,
setDate = function(self, year, month, day, hour, minute, second, offset)
C.dirTreeSetDate(self._node, year or 0, month or 0, day or 0,
hour or 0, minute or 0, second or 0, offset or 0)
end,
writeSector = function(self, sectorData, mode)
if not mode then mode = 'M2_FORM1' end
if Support.isLuaBuffer(sectorData) then sectorData = sectorData.data end
C.isoBuilderWriteSector(self._wrapper, sectorData, mode)
end,
close = function(self) C.isoBuilderClose(self._wrapper) end,
}
return iso
return wrapper
end
-- Amount of data a raw sector write consumes, per mode. Modes absent from this table can't be written.
local sectorDataSize = {
RAW = 2352,
M2_RAW = 2336,
M2_FORM1 = 2048,
M2_FORM2 = 2324,
}
-- Normalizes the (data[, size]) pair of the raw sector writers. Strings carry their own size; anything
-- else is a pointer and needs one spelled out.
local function sectorData(data, size, mode)
if size == nil then
if type(data) ~= 'string' then error('a size is required when writing a sector from a pointer') end
size = #data
end
local needed = sectorDataSize[mode]
if needed == nil then error('sector mode ' .. tostring(mode) .. ' cannot be written') end
if size < needed then
error('sector mode ' .. mode .. ' needs ' .. needed .. ' bytes of data, got ' .. size)
end
return ffi.cast('const uint8_t*', data), size
end
local function createIsoBuilderWrapper(wrapper)
local function pvdGetter(getter)
return function(self)
getter(self._wrapper, pvdBuf, 513)
return ffi.string(pvdBuf)
end
end
local builder = {
_wrapper = ffi.gc(wrapper, C.deleteIsoBuilder),
failed = function(self) return C.isoBuilderFailed(self._wrapper) end,
getCurrentLBA = function(self) return C.isoBuilderGetCurrentLBA(self._wrapper) end,
writeSector = function(self, data, a, b)
local size, mode
if type(a) == 'number' then size, mode = a, b else size, mode = nil, a end
if mode == nil then mode = 'M2_FORM1' end
local ptr, len = sectorData(data, size, mode)
return C.isoBuilderWriteSector(self._wrapper, ptr, len, mode)
end,
writeSectorAt = function(self, data, a, b, c)
local size, lba, mode
if type(b) == 'number' then size, lba, mode = a, b, c else size, lba, mode = nil, a, b end
if mode == nil then mode = 'M2_FORM1' end
local ptr, len = sectorData(data, size, mode)
return C.isoBuilderWriteSectorAt(self._wrapper, ptr, len, lba, mode)
end,
writeLicense = function(self, file)
if file then
C.isoBuilderWriteLicense(self._wrapper, file._wrapper)
else
C.isoBuilderWriteLicense(self._wrapper, nil)
end
end,
-- PVD getters
getSystemIdent = pvdGetter(C.hlPvdGetSystemIdent),
getVolumeIdent = pvdGetter(C.hlPvdGetVolumeIdent),
getVolSetIdent = pvdGetter(C.hlPvdGetVolSetIdent),
getPublisherIdent = pvdGetter(C.hlPvdGetPublisherIdent),
getDataPreparerIdent = pvdGetter(C.hlPvdGetDataPreparerIdent),
getApplicationIdent = pvdGetter(C.hlPvdGetApplicationIdent),
getCopyrightFileIdent = pvdGetter(C.hlPvdGetCopyrightFileIdent),
getAbstractFileIdent = pvdGetter(C.hlPvdGetAbstractFileIdent),
getBibliographicFileIdent = pvdGetter(C.hlPvdGetBibliographicFileIdent),
-- PVD setters
setSystemIdent = function(self, val) C.hlPvdSetSystemIdent(self._wrapper, val) end,
setVolumeIdent = function(self, val) C.hlPvdSetVolumeIdent(self._wrapper, val) end,
setVolSetIdent = function(self, val) C.hlPvdSetVolSetIdent(self._wrapper, val) end,
setPublisherIdent = function(self, val) C.hlPvdSetPublisherIdent(self._wrapper, val) end,
setDataPreparerIdent = function(self, val) C.hlPvdSetDataPreparerIdent(self._wrapper, val) end,
setApplicationIdent = function(self, val) C.hlPvdSetApplicationIdent(self._wrapper, val) end,
setCopyrightFileIdent = function(self, val) C.hlPvdSetCopyrightFileIdent(self._wrapper, val) end,
setAbstractFileIdent = function(self, val) C.hlPvdSetAbstractFileIdent(self._wrapper, val) end,
setBibliographicFileIdent = function(self, val) C.hlPvdSetBibliographicFileIdent(self._wrapper, val) end,
-- Tree building
createRoot = function(self, sectorCount)
return createDirTreeWrapper(C.hlCreateRoot(self._wrapper, sectorCount or 1))
end,
createDir = function(self, parent, name, sectorCount)
return createDirTreeWrapper(C.hlCreateDir(self._wrapper, parent._node, name, sectorCount or 1))
end,
createFile = function(self, parent, name, fileHandle)
return createDirTreeWrapper(C.hlCreateFile(self._wrapper, parent._node, name, fileHandle._wrapper))
end,
close = function(self, threadCount)
C.isoBuilderClose(self._wrapper, threadCount or 0)
end,
}
return builder
end
PCSX.getCurrentIso = function() return createIsoWrapper(C.getCurrentIso()) end
+1 -1
View File
@@ -19,7 +19,7 @@
#include "core/kernel.h"
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include <string>
#include "core/psxmem.h"
+1 -1
View File
@@ -19,7 +19,7 @@
#pragma once
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include <utility>
#include "core/system.h"
+421 -4
View File
@@ -19,7 +19,9 @@
#include "core/luaiso.h"
#include <algorithm>
#include <memory>
#include <unordered_set>
#include "cdrom/cdriso.h"
#include "cdrom/file.h"
@@ -27,6 +29,7 @@
#include "core/cdrom.h"
#include "lua/luafile.h"
#include "lua/luawrapper.h"
#include "support/strings-helpers.h"
#include "supportpsx/iec-60908b.h"
#include "supportpsx/iso9660-builder.h"
@@ -67,17 +70,357 @@ MSF getIsoTD(LuaIso* wrapper, uint32_t tn) {
return {ret.m, ret.s, ret.f};
}
struct DirEntries {
std::vector<PCSX::ISO9660Reader::FullDirEntry> entries;
};
// Drop ISO9660 "." (\0) and ".." (\1) sentinel entries from a listing.
static std::vector<PCSX::ISO9660Reader::FullDirEntry> stripSelfParent(
std::vector<PCSX::ISO9660Reader::FullDirEntry>&& entries) {
std::vector<PCSX::ISO9660Reader::FullDirEntry> out;
out.reserve(entries.size());
for (auto& e : entries) {
const auto& name = e.first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value;
if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue;
out.push_back(std::move(e));
}
return out;
}
DirEntries* readerListDir(PCSX::ISO9660Reader* reader, const char* path) {
if (reader->failed()) return new DirEntries{};
auto root = reader->getRootDirEntry();
if (path == nullptr || path[0] == '\0') {
return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(root))};
}
// Walk the path using listAllEntriesFrom. ISO9660 directory entries
// don't carry version suffixes (only files do), so exact match on each
// path component is correct for directory listing.
auto parts = PCSX::StringsHelpers::split(std::string_view(path), "/");
PCSX::ISO9660LowLevel::DirEntry current = root;
for (auto& part : parts) {
auto entries = reader->listAllEntriesFrom(current);
bool found = false;
for (auto& [entry, xa] : entries) {
if (entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value == part) {
current = entry;
found = true;
break;
}
}
if (!found) return new DirEntries{};
}
// Only return children if the target is actually a directory.
if ((current.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) == 0) {
return new DirEntries{};
}
return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(current))};
}
void deleteDirEntries(DirEntries* entries) { delete entries; }
uint32_t dirEntriesCount(DirEntries* entries) { return entries->entries.size(); }
const char* dirEntryName(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return "";
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value.c_str();
}
uint32_t dirEntryLBA(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return 0;
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
}
uint32_t dirEntrySize(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return 0;
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
}
bool dirEntryIsDir(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return false;
return (entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0;
}
struct GapEntry {
uint32_t lba;
uint32_t sectors;
};
struct GapList {
std::vector<GapEntry> gaps;
};
// XA attribute bits (CD-XA spec, stored big-endian in directory record).
// Bit 11 (0x0800): Mode 2 Form 1 data file
// Bit 12 (0x1000): Mode 2 Form 2 interleaved audio/video
static constexpr uint16_t XA_ATTR_FORM2 = 0x1000;
static void collectAllEntries(PCSX::ISO9660Reader* reader, const PCSX::ISO9660LowLevel::DirEntry& dir,
std::vector<std::pair<uint32_t, uint32_t>>& out,
std::unordered_set<uint32_t>& visitedDirs) {
auto entries = reader->listAllEntriesFrom(dir);
for (auto& [entry, xa] : entries) {
const auto& name = entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value;
if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue;
uint32_t lba = entry.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
uint32_t size = entry.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
uint16_t attribs = xa.get<PCSX::ISO9660LowLevel::DirEntry_XA_Attribs>();
uint32_t sectorSize = (attribs & XA_ATTR_FORM2) ? 2324 : 2048;
uint32_t sectors = (size + sectorSize - 1) / sectorSize;
// Skip zero-length extents: they don't consume sectors, and emitting
// them would confuse the gap aggregation pass.
if (sectors != 0) out.push_back({lba, sectors});
bool isDir = (entry.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0;
// Guard against malformed ISOs with directory cycles.
if (isDir && visitedDirs.insert(lba).second) {
collectAllEntries(reader, entry, out, visitedDirs);
}
}
}
GapList* readerFindGaps(PCSX::ISO9660Reader* reader) {
if (reader->failed()) return new GapList{};
std::vector<std::pair<uint32_t, uint32_t>> allFiles;
// Account for ISO9660 system structures
allFiles.push_back({0, 16}); // License/system area
auto& pvd = reader->getPVD();
uint32_t vdEnd = reader->getVDEnd();
allFiles.push_back({16, vdEnd > 16 ? vdEnd - 16 : 1}); // Volume descriptors including terminator
uint32_t lPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableLocation>();
uint32_t pathTableSize = pvd.get<PCSX::ISO9660LowLevel::PVD_PathTableSize>();
uint32_t pathTableSectors = (pathTableSize + 2047) / 2048;
allFiles.push_back({lPathLoc, pathTableSectors});
uint32_t lPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableOptLocation>();
if (lPathOptLoc != 0) allFiles.push_back({lPathOptLoc, pathTableSectors});
uint32_t mPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableLocation>();
allFiles.push_back({mPathLoc, pathTableSectors});
uint32_t mPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableOptLocation>();
if (mPathOptLoc != 0) allFiles.push_back({mPathOptLoc, pathTableSectors});
auto& rootDir = reader->getRootDirEntry();
uint32_t rootLBA = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
uint32_t rootSize = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
allFiles.push_back({rootLBA, (rootSize + 2047) / 2048});
std::unordered_set<uint32_t> visitedDirs;
visitedDirs.insert(rootLBA);
collectAllEntries(reader, reader->getRootDirEntry(), allFiles, visitedDirs);
std::sort(allFiles.begin(), allFiles.end());
auto* result = new GapList{};
uint32_t nextExpected = 0;
for (auto& [lba, sectors] : allFiles) {
if (lba > nextExpected) {
result->gaps.push_back({nextExpected, lba - nextExpected});
}
uint32_t end = lba + sectors;
if (end > nextExpected) nextExpected = end;
}
// Trailing gap: anything between the last occupied extent and the disc end.
uint32_t volumeSpaceSize = pvd.get<PCSX::ISO9660LowLevel::PVD_VolumeSpaceSize>();
if (volumeSpaceSize > nextExpected) {
result->gaps.push_back({nextExpected, volumeSpaceSize - nextExpected});
}
return result;
}
void deleteGapList(GapList* list) { delete list; }
uint32_t gapListCount(GapList* list) { return list->gaps.size(); }
uint32_t gapEntryLBA(GapList* list, uint32_t index) {
if (index >= list->gaps.size()) return 0;
return list->gaps[index].lba;
}
uint32_t gapEntrySectors(GapList* list, uint32_t index) {
if (index >= list->gaps.size()) return 0;
return list->gaps[index].sectors;
}
PCSX::ISO9660Builder* createIsoBuilder(PCSX::LuaFFI::LuaFile* wrapper) {
return new PCSX::ISO9660Builder(wrapper->file);
}
void deleteIsoBuilder(PCSX::ISO9660Builder* builder) { delete builder; }
bool isoBuilderFailed(PCSX::ISO9660Builder* builder) { return builder->failed(); }
void isoBuilderWriteLicense(PCSX::ISO9660Builder* builder, PCSX::LuaFFI::LuaFile* licenseWrapper) {
builder->writeLicense(licenseWrapper->file);
builder->writeLicense(licenseWrapper ? licenseWrapper->file : nullptr);
}
void isoBuilderWriteSector(PCSX::ISO9660Builder* builder, const uint8_t* sectorData, PCSX::IEC60908b::SectorMode mode) {
builder->writeSector(sectorData, mode);
// Sentinel returned to Lua when a sector write couldn't be performed.
static constexpr uint32_t c_badLBA = 0xffffffff;
// Amount of data writeSectorAt will read from the caller's buffer for a given mode. Modes that can't
// be written to return 0.
static uint32_t sectorDataSize(PCSX::IEC60908b::SectorMode mode) {
switch (mode) {
case PCSX::IEC60908b::SectorMode::RAW:
return PCSX::IEC60908b::FRAMESIZE_RAW;
case PCSX::IEC60908b::SectorMode::M2_RAW:
return 2336;
case PCSX::IEC60908b::SectorMode::M2_FORM1:
return 2048;
case PCSX::IEC60908b::SectorMode::M2_FORM2:
return 2324;
default:
return 0;
}
}
// The LBAs here are relative to the start of the image, matching the rest of the iso API, while the
// builder works in MSF. Sector 0 of the image is MSF 00:02:00.
static uint32_t msfToImageLBA(PCSX::IEC60908b::MSF msf) {
uint32_t lba = msf.toLBA();
return lba < 150 ? c_badLBA : lba - 150;
}
uint32_t isoBuilderWriteSectorAt(PCSX::ISO9660Builder* builder, const uint8_t* sectorData, uint32_t dataSize,
uint32_t lba, PCSX::IEC60908b::SectorMode mode) {
uint32_t needed = sectorDataSize(mode);
if ((needed == 0) || (dataSize < needed)) return c_badLBA;
return msfToImageLBA(builder->writeSectorAt(sectorData, PCSX::IEC60908b::MSF(lba + 150), mode));
}
uint32_t isoBuilderWriteSector(PCSX::ISO9660Builder* builder, const uint8_t* sectorData, uint32_t dataSize,
PCSX::IEC60908b::SectorMode mode) {
uint32_t needed = sectorDataSize(mode);
if ((needed == 0) || (dataSize < needed)) return c_badLBA;
return msfToImageLBA(builder->writeSector(sectorData, mode));
}
uint32_t isoBuilderGetCurrentLBA(PCSX::ISO9660Builder* builder) {
return msfToImageLBA(builder->getCurrentLocation());
}
void isoBuilderClose(PCSX::ISO9660Builder* builder, uint32_t threadCount) { builder->close(threadCount); }
// PVD string field helpers
static void trimCStringTo(const char* src, size_t maxLen, char* dst, uint32_t dstSize) {
size_t len = strnlen(src, maxLen);
// Trim trailing spaces and nulls.
while (len > 0 && (src[len - 1] == ' ' || src[len - 1] == '\0')) len--;
size_t toCopy = len < dstSize - 1 ? len : dstSize - 1;
memcpy(dst, src, toCopy);
dst[toCopy] = '\0';
}
// PVD getters
void hlPvdGetSystemIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_SystemIdent>().value, 32, buf, bufSize);
}
void hlPvdGetVolumeIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_VolumeIdent>().value, 32, buf, bufSize);
}
void hlPvdGetVolSetIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_VolSetIdent>().value, 128, buf, bufSize);
}
void hlPvdGetPublisherIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_PublisherIdent>().value, 128, buf, bufSize);
}
void hlPvdGetDataPreparerIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_DataPreparerIdent>().value, 128, buf, bufSize);
}
void hlPvdGetApplicationIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_ApplicationIdent>().value, 128, buf, bufSize);
}
void hlPvdGetCopyrightFileIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_CopyrightFileIdent>().value, 37, buf, bufSize);
}
void hlPvdGetAbstractFileIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_AbstractFileIdent>().value, 37, buf, bufSize);
}
void hlPvdGetBibliographicFileIdent(PCSX::ISO9660Builder* b, char* buf, uint32_t bufSize) {
trimCStringTo(b->getPVD().get<PCSX::ISO9660LowLevel::PVD_BibliographicFileIdent>().value, 37, buf, bufSize);
}
// PVD setters
void hlPvdSetSystemIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_SystemIdent>().set(val, ' ');
}
void hlPvdSetVolumeIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_VolumeIdent>().set(val, ' ');
}
void hlPvdSetVolSetIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_VolSetIdent>().set(val, ' ');
}
void hlPvdSetPublisherIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_PublisherIdent>().set(val, ' ');
}
void hlPvdSetDataPreparerIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_DataPreparerIdent>().set(val, ' ');
}
void hlPvdSetApplicationIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_ApplicationIdent>().set(val, ' ');
}
void hlPvdSetCopyrightFileIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_CopyrightFileIdent>().set(val, ' ');
}
void hlPvdSetAbstractFileIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_AbstractFileIdent>().set(val, ' ');
}
void hlPvdSetBibliographicFileIdent(PCSX::ISO9660Builder* b, const char* val) {
b->getPVD().get<PCSX::ISO9660LowLevel::PVD_BibliographicFileIdent>().set(val, ' ');
}
// DirTree wrappers
PCSX::ISO9660::DirTree* hlCreateRoot(PCSX::ISO9660Builder* b, uint32_t sectorCount) {
return b->createRoot(sectorCount);
}
PCSX::ISO9660::DirTree* hlCreateDir(PCSX::ISO9660Builder* b, PCSX::ISO9660::DirTree* parent, const char* name,
uint32_t sectorCount) {
return b->createDir(parent, name, sectorCount);
}
PCSX::ISO9660::DirTree* hlCreateFile(PCSX::ISO9660Builder* b, PCSX::ISO9660::DirTree* parent, const char* name,
PCSX::LuaFFI::LuaFile* content) {
return b->createFile(parent, name, content->file);
}
const char* dirTreeGetName(PCSX::ISO9660::DirTree* node) { return node->getName().data(); }
uint32_t dirTreeGetSize(PCSX::ISO9660::DirTree* node) { return node->getSize(); }
uint32_t dirTreeGetLBA(PCSX::ISO9660::DirTree* node) { return node->getLBA(); }
bool dirTreeIsDir(PCSX::ISO9660::DirTree* node) { return node->isDir(); }
bool dirTreeIsHidden(PCSX::ISO9660::DirTree* node) { return node->isHidden(); }
void dirTreeSetHidden(PCSX::ISO9660::DirTree* node, bool val) { node->setHidden(val); }
bool dirTreeShouldSkip(PCSX::ISO9660::DirTree* node) { return node->shouldSkip(); }
void dirTreeSetSkip(PCSX::ISO9660::DirTree* node, bool val) { node->setSkip(val); }
bool dirTreeHasAnchorLBA(PCSX::ISO9660::DirTree* node) { return node->hasAnchorLBA(); }
uint32_t dirTreeGetAnchorLBA(PCSX::ISO9660::DirTree* node) { return node->getAnchorLBA(); }
void dirTreeSetAnchorLBA(PCSX::ISO9660::DirTree* node, uint32_t lba) { node->setAnchorLBA(lba); }
void dirTreeClearAnchorLBA(PCSX::ISO9660::DirTree* node) { node->clearAnchorLBA(); }
bool dirTreeHasDeclaredSize(PCSX::ISO9660::DirTree* node) { return node->hasDeclaredSize(); }
uint32_t dirTreeGetDeclaredSize(PCSX::ISO9660::DirTree* node) { return node->getDeclaredSize(); }
void dirTreeSetDeclaredSize(PCSX::ISO9660::DirTree* node, uint32_t size) { node->setDeclaredSize(size); }
void dirTreeClearDeclaredSize(PCSX::ISO9660::DirTree* node) { node->clearDeclaredSize(); }
bool dirTreeHasXA(PCSX::ISO9660::DirTree* node) { return node->hasXA(); }
void dirTreeSetHasXA(PCSX::ISO9660::DirTree* node, bool val) { node->setHasXA(val); }
void dirTreeSetSectorMode(PCSX::ISO9660::DirTree* node, PCSX::IEC60908b::SectorMode mode) { node->setSectorMode(mode); }
uint16_t dirTreeGetXAAttribs(PCSX::ISO9660::DirTree* node) {
return node->getXA().get<PCSX::ISO9660LowLevel::DirEntry_XA_Attribs>().value;
}
void dirTreeSetXAAttribs(PCSX::ISO9660::DirTree* node, uint16_t val) {
node->getXA().get<PCSX::ISO9660LowLevel::DirEntry_XA_Attribs>().value = val;
}
uint8_t dirTreeGetXAFileNum(PCSX::ISO9660::DirTree* node) {
return node->getXA().get<PCSX::ISO9660LowLevel::DirEntry_XA_FileNum>().value;
}
void dirTreeSetXAFileNum(PCSX::ISO9660::DirTree* node, uint8_t val) {
node->getXA().get<PCSX::ISO9660LowLevel::DirEntry_XA_FileNum>().value = val;
}
// DirTree navigation
PCSX::ISO9660::DirTree* dirTreeParent(PCSX::ISO9660::DirTree* node) { return node->parent(); }
PCSX::ISO9660::DirTree* dirTreeFirstChild(PCSX::ISO9660::DirTree* node) { return node->firstChild(); }
PCSX::ISO9660::DirTree* dirTreeNextSibling(PCSX::ISO9660::DirTree* node) { return node->nextSibling(); }
// DirTree date access
void dirTreeSetDate(PCSX::ISO9660::DirTree* node, uint8_t year, uint8_t month, uint8_t day, uint8_t hour,
uint8_t minute, uint8_t second, uint8_t offset) {
auto& date = node->getDate();
date.get<PCSX::ISO9660LowLevel::ShortDate_Year>().value = year;
date.get<PCSX::ISO9660LowLevel::ShortDate_Month>().value = month;
date.get<PCSX::ISO9660LowLevel::ShortDate_Day>().value = day;
date.get<PCSX::ISO9660LowLevel::ShortDate_Hour>().value = hour;
date.get<PCSX::ISO9660LowLevel::ShortDate_Minute>().value = minute;
date.get<PCSX::ISO9660LowLevel::ShortDate_Second>().value = second;
date.get<PCSX::ISO9660LowLevel::ShortDate_Offset>().value = offset;
}
uint8_t dirTreeGetDateYear(PCSX::ISO9660::DirTree* node) {
return node->getDate().get<PCSX::ISO9660LowLevel::ShortDate_Year>().value;
}
uint8_t dirTreeGetDateMonth(PCSX::ISO9660::DirTree* node) {
return node->getDate().get<PCSX::ISO9660LowLevel::ShortDate_Month>().value;
}
uint8_t dirTreeGetDateDay(PCSX::ISO9660::DirTree* node) {
return node->getDate().get<PCSX::ISO9660LowLevel::ShortDate_Day>().value;
}
void isoBuilderClose(PCSX::ISO9660Builder* builder) { builder->close(); }
} // namespace
@@ -110,12 +453,86 @@ static void registerAllSymbols(PCSX::Lua L) {
REGISTER(L, readerOpen);
REGISTER(L, fileisoOpen);
REGISTER(L, readerListDir);
REGISTER(L, deleteDirEntries);
REGISTER(L, dirEntriesCount);
REGISTER(L, dirEntryName);
REGISTER(L, dirEntryLBA);
REGISTER(L, dirEntrySize);
REGISTER(L, dirEntryIsDir);
REGISTER(L, readerFindGaps);
REGISTER(L, deleteGapList);
REGISTER(L, gapListCount);
REGISTER(L, gapEntryLBA);
REGISTER(L, gapEntrySectors);
REGISTER(L, createIsoBuilder);
REGISTER(L, deleteIsoBuilder);
REGISTER(L, isoBuilderFailed);
REGISTER(L, isoBuilderWriteLicense);
REGISTER(L, isoBuilderWriteSector);
REGISTER(L, isoBuilderWriteSectorAt);
REGISTER(L, isoBuilderGetCurrentLBA);
REGISTER(L, isoBuilderClose);
// PVD getters
REGISTER(L, hlPvdGetSystemIdent);
REGISTER(L, hlPvdGetVolumeIdent);
REGISTER(L, hlPvdGetVolSetIdent);
REGISTER(L, hlPvdGetPublisherIdent);
REGISTER(L, hlPvdGetDataPreparerIdent);
REGISTER(L, hlPvdGetApplicationIdent);
REGISTER(L, hlPvdGetCopyrightFileIdent);
REGISTER(L, hlPvdGetAbstractFileIdent);
REGISTER(L, hlPvdGetBibliographicFileIdent);
// PVD setters
REGISTER(L, hlPvdSetSystemIdent);
REGISTER(L, hlPvdSetVolumeIdent);
REGISTER(L, hlPvdSetVolSetIdent);
REGISTER(L, hlPvdSetPublisherIdent);
REGISTER(L, hlPvdSetDataPreparerIdent);
REGISTER(L, hlPvdSetApplicationIdent);
REGISTER(L, hlPvdSetCopyrightFileIdent);
REGISTER(L, hlPvdSetAbstractFileIdent);
REGISTER(L, hlPvdSetBibliographicFileIdent);
// DirTree
REGISTER(L, hlCreateRoot);
REGISTER(L, hlCreateDir);
REGISTER(L, hlCreateFile);
REGISTER(L, dirTreeGetName);
REGISTER(L, dirTreeGetSize);
REGISTER(L, dirTreeGetLBA);
REGISTER(L, dirTreeIsDir);
REGISTER(L, dirTreeIsHidden);
REGISTER(L, dirTreeSetHidden);
REGISTER(L, dirTreeShouldSkip);
REGISTER(L, dirTreeSetSkip);
REGISTER(L, dirTreeHasAnchorLBA);
REGISTER(L, dirTreeGetAnchorLBA);
REGISTER(L, dirTreeSetAnchorLBA);
REGISTER(L, dirTreeClearAnchorLBA);
REGISTER(L, dirTreeHasDeclaredSize);
REGISTER(L, dirTreeGetDeclaredSize);
REGISTER(L, dirTreeSetDeclaredSize);
REGISTER(L, dirTreeClearDeclaredSize);
REGISTER(L, dirTreeHasXA);
REGISTER(L, dirTreeSetHasXA);
REGISTER(L, dirTreeSetSectorMode);
REGISTER(L, dirTreeGetXAAttribs);
REGISTER(L, dirTreeSetXAAttribs);
REGISTER(L, dirTreeGetXAFileNum);
REGISTER(L, dirTreeSetXAFileNum);
REGISTER(L, dirTreeParent);
REGISTER(L, dirTreeFirstChild);
REGISTER(L, dirTreeNextSibling);
REGISTER(L, dirTreeSetDate);
REGISTER(L, dirTreeGetDateYear);
REGISTER(L, dirTreeGetDateMonth);
REGISTER(L, dirTreeGetDateDay);
L.settable();
L.pop();
}
+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"
+300 -339
View File
@@ -17,17 +17,16 @@
* 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>
#include <array>
#include <cmath>
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "core/psxemulator.h"
#include "core/system.h"
@@ -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,14 +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;
}
@@ -594,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;
}
}
@@ -616,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);
@@ -666,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;
@@ -879,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;
}
}
@@ -937,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);
@@ -975,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();
@@ -1074,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;
}
@@ -1097,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;
}
@@ -1110,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();
}
}
+101 -25
View File
@@ -43,8 +43,18 @@ inline void PCSX::Counters::writeCounterInternal(uint32_t index, uint32_t value)
m_rcnts[index].cycleStart = PCSX::g_emulator->m_cpu->m_regs.cycle;
m_rcnts[index].cycleStart -= value * m_rcnts[index].rate;
// TODO: <=.
if (value < m_rcnts[index].target) {
// Hardware tested (SCPH-5501): counter reaches target value briefly before
// resetting. Fast reads can observe the target value; slow reads (printf) cannot.
// The comparison boundary needs more precise measurement to determine if
// reset happens on the same cycle as reaching target or the next cycle.
// TODO: determine exact reset timing with cycle-accurate measurement.
// Schedule next event: target hit first (for flag/IRQ), then overflow.
// If RcCountToTarget is not set, the counter doesn't reset at target,
// but bit 11 (RcCountEqTarget) is still set when the counter passes
// the target value. However, if target == 0xFFFF, the target and
// overflow events coincide - skip straight to overflow to avoid
// redundant processing.
if (value < m_rcnts[index].target && m_rcnts[index].target != 0xffff) {
m_rcnts[index].cycle = m_rcnts[index].target * m_rcnts[index].rate;
m_rcnts[index].counterState = CountToTarget;
} else {
@@ -132,7 +142,15 @@ void PCSX::Counters::reset(uint32_t index) {
m_rcnts[index].mode |= RcOverflow;
}
m_rcnts[index].mode |= RcIrqRequest;
// IRQ request flag (bit 10) behavior depends on bit 7 (toggle mode):
// Pulse mode (bit7=0): bit 10 resets to 1 after IRQ (short pulse low)
// Toggle mode (bit7=1): bit 10 toggles (XOR) on each IRQ
// Hardware verified: toggle mode produces different bit10 state than pulse mode.
if (m_rcnts[index].mode & RcIrqToggle) {
m_rcnts[index].mode ^= RcIrqRequest;
} else {
m_rcnts[index].mode |= RcIrqRequest;
}
set();
}
@@ -143,8 +161,11 @@ void PCSX::Counters::update() {
{
uint64_t prev = g_emulator->m_cpu->m_regs.previousCycles;
uint64_t diff = cycle - prev;
diff *= 4410000;
diff /= g_emulator->settings.get<Emulator::SettingScaler>();
// Map elapsed CPU cycles to the realtime (1:1 emulated:hardware) audio-frame target. Emulation
// speed is no longer controlled here: it lives entirely at the audio sink (SPU::Speed), which is
// the master clock both this counter and the SPU thread pace against. 44100 == 4410000 / 100, and
// the old Emulator::SettingScaler defaulted to 100, so this is byte-identical to the prior default.
diff *= 44100;
diff /= g_emulator->m_psxClockSpeed;
uint32_t target = m_audioFrames + diff;
uint32_t newFrames = g_emulator->m_spu->getCurrentFrames();
@@ -177,15 +198,23 @@ void PCSX::Counters::update() {
reset(3);
m_hSyncCount++;
m_spuSyncCountdown--;
// Update spu.
if (m_spuSyncCountdown <= 0) {
// Scanlines until next sync
const auto scanlines = SpuUpdInterval[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()];
m_spuSyncCountdown = scanlines;
PCSX::g_emulator->m_spu->async(scanlines * m_rcnts[3].target);
// Counter 0 gate: triggered by Hblank
if (isGateEnabled(0, m_rcnts[0].mode)) {
switch (gateSyncMode(m_rcnts[0].mode)) {
case 1: // Reset at Hblank
case 2: // Reset at Hblank + pause outside
writeCounterInternal(0, 0);
break;
case 3: // Pause until first Hblank, then free run
if (!m_rcnts[0].gateStarted) {
m_rcnts[0].gateStarted = 1;
recalculateRate(0);
writeCounterInternal(0, 0);
set();
}
break;
}
}
// SIO1 callback on hsync to process data
@@ -197,6 +226,24 @@ void PCSX::Counters::update() {
if (m_hSyncCount == VBlankStart[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()]) {
setIrq(0x01);
PCSX::g_emulator->vsync();
// Counter 1 gate: triggered by VBlank start
if (isGateEnabled(1, m_rcnts[1].mode)) {
switch (gateSyncMode(m_rcnts[1].mode)) {
case 1: // Reset at VBlank
case 2: // Reset at VBlank + pause outside
writeCounterInternal(1, 0);
break;
case 3: // Pause until first VBlank, then free run
if (!m_rcnts[1].gateStarted) {
m_rcnts[1].gateStarted = 1;
recalculateRate(1);
writeCounterInternal(1, 0);
set();
}
break;
}
}
}
if (m_hSyncCount >= m_HSyncTotal[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()]) {
@@ -213,17 +260,22 @@ void PCSX::Counters::writeCounter(uint32_t index, uint32_t value) {
set();
}
void PCSX::Counters::writeMode(uint32_t index, uint32_t value) {
verboseLog(1, "[RCNT %i] writeMode: %x\n", index, value);
update();
m_rcnts[index].mode = value;
m_rcnts[index].irqState = false;
void PCSX::Counters::recalculateRate(uint32_t index) {
uint16_t value = m_rcnts[index].mode;
switch (index) {
case 0:
if (value & Rc0PixelClock) {
m_rcnts[index].rate = 5;
// Dotclock rate depends on GPU horizontal resolution and video standard.
static constexpr uint32_t dotclockDividers[] = {10, 8, 5, 4, 7, 7};
auto hres = PCSX::g_emulator->m_gpu->m_display.info.hres;
auto videoMode = PCSX::g_emulator->m_gpu->m_display.info.mode;
uint32_t divider = dotclockDividers[static_cast<int>(hres)];
uint32_t videoCyclesPerScanline = (videoMode == GPU::CtrlDisplayMode::VM_PAL) ? 3406 : 3413;
uint32_t dotsPerScanline = videoCyclesPerScanline / divider;
uint32_t cpuCyclesPerScanline = (PCSX::g_emulator->m_psxClockSpeed /
(FrameRate[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()] *
m_HSyncTotal[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()]));
m_rcnts[index].rate = std::max<uint32_t>(cpuCyclesPerScanline / dotsPerScanline, 1);
} else {
m_rcnts[index].rate = 1;
}
@@ -243,13 +295,38 @@ void PCSX::Counters::writeMode(uint32_t index, uint32_t value) {
} else {
m_rcnts[index].rate = 1;
}
// TODO: wcount must work.
// Timer 2 sync modes: 0,3 = stop counter; 1,2 = free run
// Hardware verified (SCPH-5501): modes 0,3 produce delta=0, modes 1,2 run normally.
if (value & Rc2Disable) {
m_rcnts[index].rate = 0xffffffff;
uint8_t syncMode = gateSyncMode(value);
if (syncMode == 0 || syncMode == 3) {
m_rcnts[index].rate = 0xffffffff;
}
}
break;
}
}
void PCSX::Counters::writeMode(uint32_t index, uint32_t value) {
verboseLog(1, "[RCNT %i] writeMode: %x\n", index, value);
update();
m_rcnts[index].mode = value | RcIrqRequest; // Hardware sets bit 10 on mode write
m_rcnts[index].irqState = false;
recalculateRate(index);
// Initialize gate state
m_rcnts[index].gateStarted = 0;
if (isGateEnabled(index, value)) {
uint8_t syncMode = gateSyncMode(value);
if (syncMode == 2 || syncMode == 3) {
// Mode 2: pause outside blank. Mode 3: pause until first blank.
// Both start paused.
m_rcnts[index].rate = 0xffffffff;
}
}
writeCounterInternal(index, 0);
set();
@@ -358,7 +435,6 @@ void PCSX::Counters::init() {
}
m_hSyncCount = 0;
m_spuSyncCountdown = SpuUpdInterval[PCSX::g_emulator->settings.get<PCSX::Emulator::SettingVideo>()];
m_audioFrames = PCSX::g_emulator->m_spu->getCurrentFrames();
set();
}
+12 -9
View File
@@ -36,25 +36,30 @@ class Counters {
void set();
void reset(uint32_t index);
void calculateHsync();
void recalculateRate(uint32_t index);
struct Rcnt {
uint16_t mode, target;
uint32_t rate, irq, counterState, irqState;
uint64_t cycle, cycleStart;
uint32_t gateStarted; // Gate mode 3: first blank has occurred, counter is free running
};
// Gate mode helpers - derived from mode register, not stored
static bool isGateEnabled(uint32_t index, uint16_t mode) { return (index <= 1) && (mode & RcSyncEnable); }
static uint8_t gateSyncMode(uint16_t mode) { return (mode >> 1) & 3; }
enum {
Rc0Gate = 0x0001, // 0 not implemented
Rc1Gate = 0x0001, // 0 not implemented
Rc2Disable = 0x0001, // 0 partially implemented
RcUnknown1 = 0x0002, // 1 ?
RcUnknown2 = 0x0004, // 2 ?
RcSyncEnable = 0x0001, // 0 Sync/gate enable
Rc2Disable = 0x0001, // 0 Timer 2: modes 0,3 = stop counter
RcSyncMode0 = 0x0002, // 1 Sync mode bit 0
RcSyncMode1 = 0x0004, // 2 Sync mode bit 1
RcCountToTarget = 0x0008, // 3
RcIrqOnTarget = 0x0010, // 4
RcIrqOnOverflow = 0x0020, // 5
RcIrqRegenerate = 0x0040, // 6
RcUnknown7 = 0x0080, // 7 ?
Rc0PixelClock = 0x0100, // 8 fake implementation
RcIrqToggle = 0x0080, // 7 IRQ toggle mode (0=pulse, 1=toggle bit10)
Rc0PixelClock = 0x0100, // 8 dotclock (varies with GPU hres)
Rc1HSyncClock = 0x0100, // 8
Rc2Unknown8 = 0x0100, // 8 ?
Rc0Unknown9 = 0x0200, // 9 ?
@@ -74,7 +79,6 @@ class Counters {
static inline const uint32_t FrameRate[] = {60, 50};
static inline const uint32_t VBlankStart[] = {243, 256};
static inline const uint32_t SpuUpdInterval[] = {23, 22};
static const uint16_t JITTER_FLAGS = (Rc2OneEighthClock | RcIrqRegenerate | RcCountToTarget);
@@ -82,7 +86,6 @@ class Counters {
uint32_t m_hSyncCount = 0;
uint32_t m_audioFrames = 0;
int32_t m_spuSyncCountdown = 0;
uint32_t m_HSyncTotal[PCSX::Emulator::PSX_TYPE_PAL + 1]; // 2
public:
+4
View File
@@ -21,6 +21,7 @@
#include "core/callstacks.h"
#include "core/cdrom.h"
#include "core/cdromlogger.h"
#include "core/debug.h"
#include "core/eventslua.h"
#include "core/gdb-server.h"
@@ -34,6 +35,7 @@
#include "core/pcsxlua.h"
#include "core/pio-cart.h"
#include "core/r3000a.h"
#include "core/ramlogger.h"
#include "core/sio.h"
#include "core/sio1-server.h"
#include "core/sio1.h"
@@ -57,11 +59,13 @@ extern "C" int luaopen_lpeg(lua_State* L);
PCSX::Emulator::Emulator()
: m_callStacks(new PCSX::CallStacks),
m_cdrom(PCSX::CDRom::factory()),
m_cdromLogger(new PCSX::CDRomLogger()),
m_counters(new PCSX::Counters()),
m_debug(new PCSX::Debug()),
m_gdbServer(new PCSX::GdbServer()),
m_gpuLogger(new PCSX::GPULogger()),
m_gte(new PCSX::GTE()),
m_ramLogger(new PCSX::RAMLogger()),
m_hw(new PCSX::HW()),
m_lua(new PCSX::Lua()),
m_mdec(new PCSX::MDEC()),
+8 -4
View File
@@ -68,12 +68,14 @@ namespace PCSX {
class CallStacks;
class CDRom;
class CDRomLogger;
class Counters;
class Debug;
class GdbServer;
class GPU;
class GPULogger;
class GTE;
class RAMLogger;
class HW;
class Lua;
class MDEC;
@@ -132,6 +134,7 @@ class Emulator {
typedef Setting<uint32_t, TYPESTRING("KernelCallB0_20_3f"), 0xffffffff> KernelCallB0_20_3f;
typedef Setting<uint32_t, TYPESTRING("KernelCallB0_40_5f"), 0xffffffff> KernelCallB0_40_5f;
typedef Setting<uint32_t, TYPESTRING("KernelCallC0_00_1f"), 0xffffffff> KernelCallC0_00_1f;
typedef Setting<bool, TYPESTRING("DemangledSymbols"), true> DemangledSymbols;
typedef Setting<bool, TYPESTRING("PCdrv"), false> PCdrv;
typedef SettingPath<TYPESTRING("PCdrvBase")> PCdrvBase;
typedef Setting<bool, TYPESTRING("SIO1Server"), false> SIO1Server;
@@ -148,8 +151,8 @@ class Emulator {
GdbServer, GdbManifest, GdbLogSetting, GdbServerPort, GdbServerTrace, WebServer, WebServerPort,
KernelCallA0_00_1f, KernelCallA0_20_3f, KernelCallA0_40_5f, KernelCallA0_60_7f,
KernelCallA0_80_9f, KernelCallA0_a0_bf, KernelCallB0_00_1f, KernelCallB0_20_3f,
KernelCallB0_40_5f, KernelCallC0_00_1f, PCdrv, PCdrvBase, SIO1Server, SIO1ServerPort,
SIO1Client, SIO1ClientHost, SIO1ClientPort, SIO1ModeSetting>
KernelCallB0_40_5f, KernelCallC0_00_1f, DemangledSymbols, PCdrv, PCdrvBase, SIO1Server,
SIO1ServerPort, SIO1Client, SIO1ClientHost, SIO1ClientPort, SIO1ModeSetting>
type;
};
typedef SettingNested<TYPESTRING("Debug"), DebugSettings::type> SettingDebugSettings;
@@ -161,7 +164,6 @@ class Emulator {
typedef Setting<bool, TYPESTRING("Xa"), true> SettingXa;
typedef Setting<bool, TYPESTRING("SpuIrq")> SettingSpuIrq;
typedef Setting<bool, TYPESTRING("BnWMdec")> SettingBnWMdec;
typedef Setting<int, TYPESTRING("Scaler"), 100> SettingScaler;
typedef Setting<bool, TYPESTRING("AutoVideo"), true> SettingAutoVideo;
typedef Setting<VideoType, TYPESTRING("Video"), PSX_TYPE_NTSC> SettingVideo;
typedef Setting<bool, TYPESTRING("FastBoot"), false> SettingFastBoot;
@@ -194,7 +196,7 @@ class Emulator {
typedef SettingVector<std::string, TYPESTRING("OpenDialogFavorites")> SettingOpenDialogFavorites;
Settings<SettingMcd1, SettingMcd2, SettingBios, SettingPpfDir, SettingPsxExe, SettingXa, SettingSpuIrq,
SettingBnWMdec, SettingScaler, SettingAutoVideo, SettingVideo, SettingFastBoot, SettingDebugSettings,
SettingBnWMdec, SettingAutoVideo, SettingVideo, SettingFastBoot, SettingDebugSettings,
SettingRCntFix, SettingIsoPath, SettingLocale, SettingMcd1Inserted, SettingMcd2Inserted, SettingDynarec,
Setting8MB, SettingGUITheme, SettingDither, SettingCachedDithering, SettingGLErrorReporting,
SettingGLErrorReportingSeverity, SettingFullCaching, SettingHardwareRenderer, SettingShownAutoUpdateConfig,
@@ -256,12 +258,14 @@ class Emulator {
std::unique_ptr<CallStacks> m_callStacks;
std::unique_ptr<CDRom> m_cdrom;
std::unique_ptr<CDRomLogger> m_cdromLogger;
std::unique_ptr<Counters> m_counters;
std::unique_ptr<Debug> m_debug;
std::unique_ptr<GdbServer> m_gdbServer;
std::unique_ptr<GPU> m_gpu;
std::unique_ptr<GPULogger> m_gpuLogger;
std::unique_ptr<GTE> m_gte;
std::unique_ptr<RAMLogger> m_ramLogger;
std::unique_ptr<HW> m_hw;
std::unique_ptr<Lua> m_lua;
std::unique_ptr<MDEC> m_mdec;
+9
View File
@@ -480,6 +480,11 @@ void PCSX::HW::write8(uint32_t add, uint32_t rawvalue) {
g_emulator->m_mem->initMsan(value);
break;
default:
if ((hwadd >= 0x1f801c00) && (hwadd < 0x1f801e00)) {
g_emulator->m_spu->writeRegister(add, (uint16_t)rawvalue);
break;
}
if (addressInRegisterSpace(hwadd)) {
uint32_t *ptr = (uint32_t *)&g_emulator->m_mem->m_hard[hwadd & 0xffff];
*ptr = SWAP_LEu32(rawvalue);
@@ -826,6 +831,10 @@ void PCSX::HW::write32(uint32_t add, uint32_t value) {
g_emulator->m_mem->msanFree(value);
break;
}
case 0x1f8020a0: {
g_emulator->m_mem->m_psyqoHeapMetadata = value;
break;
}
case 0x1f802094: {
PSXAddress headerAddrInfo(value);
switch (headerAddrInfo.type) {
+35 -5
View File
@@ -109,9 +109,10 @@ int PCSX::Memory::init() {
m_exp1 = (uint8_t *)calloc(0x00800000, 1);
m_hard = (uint8_t *)calloc(0x00010000, 1);
m_bios = (uint8_t *)calloc(0x00080000, 1);
m_sram = (uint8_t *)calloc(0x00200000, 1);
if (m_readLUT == NULL || m_writeLUT == NULL || m_wram == NULL || m_exp1 == NULL || m_bios == NULL ||
m_hard == NULL) {
m_sram == NULL || m_hard == NULL) {
g_system->message("%s", _("Error allocating memory!"));
return -1;
}
@@ -129,6 +130,18 @@ int PCSX::Memory::init() {
memcpy(m_readLUT + 0x9fc0, m_readLUT + 0x1fc0, 0x08 * sizeof(void *));
memcpy(m_readLUT + 0xbfc0, m_readLUT + 0x1fc0, 0x08 * sizeof(void *));
// DTL-H2000 dev board BIOS SRAM: 2MB of writable static RAM at 0x1fa00000,
// mirrored across kuser/kseg0/kseg1. setLuts() only touches the main-RAM pages,
// so these entries are safe to set once here.
for (int i = 0; i < 0x20; i++) {
m_readLUT[i + 0x1fa0] = (uint8_t *)&m_sram[i << 16];
m_writeLUT[i + 0x1fa0] = (uint8_t *)&m_sram[i << 16];
}
memcpy(m_readLUT + 0x9fa0, m_readLUT + 0x1fa0, 0x20 * sizeof(void *));
memcpy(m_readLUT + 0xbfa0, m_readLUT + 0x1fa0, 0x20 * sizeof(void *));
memcpy(m_writeLUT + 0x9fa0, m_writeLUT + 0x1fa0, 0x20 * sizeof(void *));
memcpy(m_writeLUT + 0xbfa0, m_writeLUT + 0x1fa0, 0x20 * sizeof(void *));
setLuts();
m_memoryAsFile = new MemoryAsFile(this);
@@ -174,6 +187,8 @@ void PCSX::Memory::reset() {
memset(m_wram, 0, 0x00800000);
memset(m_exp1, 0xff, exp1_size);
memset(m_bios, 0, bios_size);
memset(m_sram, 0, 0x00200000);
m_psyqoHeapMetadata = 0;
static const uint32_t nobios[6] = {
Mips::Encoder::lui(Mips::Encoder::Reg::V0, 0xbfc0), // v0 = 0xbfc00000
Mips::Encoder::lui(Mips::Encoder::Reg::V1, 0x1f80), // v1 = 0x1f800000
@@ -259,6 +274,7 @@ void PCSX::Memory::shutdown() {
free(m_exp1);
free(m_hard);
free(m_bios);
free(m_sram);
free(m_readLUT);
free(m_writeLUT);
@@ -793,7 +809,7 @@ ssize_t PCSX::Memory::MemoryAsFile::writeAt(const void *src, size_t size, size_t
void PCSX::Memory::MemoryAsFile::readBlock(void *dest_, size_t size, size_t ptr) {
auto dest = reinterpret_cast<uint8_t *>(dest_);
auto block = m_memory->m_readLUT[ptr / c_blockSize];
auto block = m_memory->debugPointer(ptr / c_blockSize);
if (!block) {
memset(dest, 0, size);
if (m_memory->msanInitialized()) {
@@ -812,10 +828,25 @@ void PCSX::Memory::MemoryAsFile::readBlock(void *dest_, size_t size, size_t ptr)
}
void PCSX::Memory::MemoryAsFile::writeBlock(const void *src_, size_t size, size_t ptr) {
// Yes. That's not a bug nor a typo.
// The read LUT, on purpose, and not a typo. The write LUT describes what the guest's current
// BIU cache configuration lets a store reach, and it goes empty for main RAM whenever the
// cache is isolated. That's transient guest state; a write coming in from a debugger has no
// business being subject to it. The read LUT is the table that describes what memory is
// actually there.
auto src = reinterpret_cast<const uint8_t *>(src_);
auto block = m_memory->m_readLUT[ptr / c_blockSize];
const auto page = ptr / c_blockSize;
auto block = m_memory->m_readLUT[page];
auto offset = ptr % c_blockSize;
if (!block) {
// The scratchpad is plain memory and safe to poke. The hardware registers sharing its page
// are stateful, and writing to their shadow behind the hardware's back would only desync
// the two, so those are silently dropped.
if ((page == 0x1f80) || (page == 0x9f80) || (page == 0xbf80)) {
if (offset < c_scratchpadSize) {
memcpy(m_memory->m_hard + offset, src, std::min<size_t>(size, c_scratchpadSize - offset));
}
return;
}
if (m_memory->msanInitialized()) {
for (size_t i = 0; i < size; ++i) {
size_t msanPtr = ptr + i;
@@ -826,7 +857,6 @@ void PCSX::Memory::MemoryAsFile::writeBlock(const void *src_, size_t size, size_
}
return;
}
auto offset = ptr % c_blockSize;
auto toCopy = std::min(size, c_blockSize - offset);
memcpy(block + offset, src, toCopy);
}
+20 -1
View File
@@ -304,6 +304,20 @@ class Memory {
friend class MemoryAsFile;
IO<MemoryAsFile> m_memoryAsFile;
// The scratchpad and the hardware registers share a single 64kB page, and the guest path has
// to dispatch register accesses to the hardware itself for their side effects, so that page is
// deliberately absent from the read LUT. Both are shadowed by m_hard at the same offsets,
// which spans the whole page and is safe to read at any time, so debugger-style accesses -
// which want a faithful view of memory and none of the side effects - resolve through here
// instead of through the read LUT directly.
static constexpr uint32_t c_scratchpadSize = 0x400;
uint8_t *debugPointer(uint32_t page) const {
auto pointer = m_readLUT[page];
if (pointer != nullptr) return pointer;
if ((page == 0x1f80) || (page == 0x9f80) || (page == 0xbf80)) return m_hard;
return nullptr;
}
uint32_t m_biosCRC = 0;
// Shared memory wrappers, pointers below point to these where appropriate
@@ -315,8 +329,9 @@ class Memory {
// hopefully this should become private eventually, with only certain classes having direct access.
public:
uint8_t *m_wram = nullptr; // Kernel & User Memory (8 Meg)
uint8_t *m_exp1 = nullptr; // Expansion Region 1 (ROM/RAM) / Parallel Port (512K)
uint8_t *m_exp1 = nullptr; // Expansion Region 1 (ROM/RAM) / Parallel Port (8 Meg buffer; up to 256K loaded)
uint8_t *m_bios = nullptr; // BIOS ROM (512K)
uint8_t *m_sram = nullptr; // DTL-H2000 dev board BIOS SRAM (2 Meg) @ 0x1fa00000
uint8_t *m_hard = nullptr; // Scratch Pad (1K) & Hardware Registers (8K)
uint8_t **m_writeLUT = nullptr;
@@ -331,6 +346,10 @@ class Memory {
uint32_t m_msanPtr = 1024;
EventBus::Listener m_listener;
// Address of the psyqo heap metadata struct in guest memory,
// registered by the MIPS allocator via pcsxhw write to 0x1f8020a0.
uint32_t m_psyqoHeapMetadata = 0;
std::unordered_map<uint32_t, uint32_t> m_msanAllocs;
static constexpr uint32_t c_msanChainMarker = 0x7ffffd;
std::unordered_map<uint32_t, uint32_t> m_msanChainRegistry;
+1 -1
View File
@@ -23,7 +23,7 @@
#include "core/r3000a.h"
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include "core/cdrom.h"
#include "core/debug.h"
+106
View File
@@ -0,0 +1,106 @@
/***************************************************************************
* 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 "core/ramlogger.h"
#include "core/psxemulator.h"
#include "core/psxmem.h"
void PCSX::RAMLogger::recordAccess(uint32_t physAddr, unsigned width, AccessType type, uint32_t cycle) {
if (!m_enabled) return;
uint32_t* timestamps = (type == AccessType::Read) ? m_readTimestamps
: (type == AccessType::Write) ? m_writeTimestamps
: m_execTimestamps;
for (unsigned i = 0; i < width; i++) {
uint32_t addr = physAddr + i;
if (addr < c_maxBytes) {
timestamps[addr] = cycle;
}
}
}
void PCSX::RAMLogger::enable() {
if (m_hasResources) {
m_enabled = true;
return;
}
auto setupTex = [](OpenGL::Texture& tex) {
tex.bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
// Create heatmap textures (GL_R32UI - unsigned integer)
m_readHeatmapTex.create(c_width, c_maxHeight, GL_R32UI);
m_writeHeatmapTex.create(c_width, c_maxHeight, GL_R32UI);
m_execHeatmapTex.create(c_width, c_maxHeight, GL_R32UI);
if (!m_readHeatmapTex.exists() || !m_writeHeatmapTex.exists() || !m_execHeatmapTex.exists()) return;
setupTex(m_readHeatmapTex);
setupTex(m_writeHeatmapTex);
setupTex(m_execHeatmapTex);
// Create RAM data texture (GL_R8)
m_ramTexture.create(c_width, c_maxHeight, GL_R8);
if (!m_ramTexture.exists()) return;
setupTex(m_ramTexture);
// Clear timestamp arrays
memset(m_readTimestamps, 0, sizeof(m_readTimestamps));
memset(m_writeTimestamps, 0, sizeof(m_writeTimestamps));
memset(m_execTimestamps, 0, sizeof(m_execTimestamps));
m_hasResources = true;
m_enabled = true;
}
void PCSX::RAMLogger::disable() {
m_enabled = false;
}
void PCSX::RAMLogger::uploadRAM() {
if (!m_hasResources) return;
bool is8MB = g_emulator->settings.get<Emulator::Setting8MB>();
int height = is8MB ? c_maxHeight : (c_maxHeight / 4);
m_ramTexture.bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_width, height, GL_RED, GL_UNSIGNED_BYTE, g_emulator->m_mem->m_wram);
}
void PCSX::RAMLogger::uploadHeatmaps() {
if (!m_hasResources) return;
bool is8MB = g_emulator->settings.get<Emulator::Setting8MB>();
int height = is8MB ? c_maxHeight : (c_maxHeight / 4);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
m_readHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_width, height, GL_RED_INTEGER, GL_UNSIGNED_INT, m_readTimestamps);
m_writeHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_width, height, GL_RED_INTEGER, GL_UNSIGNED_INT, m_writeTimestamps);
m_execHeatmapTex.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c_width, height, GL_RED_INTEGER, GL_UNSIGNED_INT, m_execTimestamps);
}
+77
View File
@@ -0,0 +1,77 @@
/***************************************************************************
* 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. *
***************************************************************************/
#pragma once
#include <stdint.h>
#include <string.h>
#include "support/opengl.h"
namespace PCSX {
namespace Widgets {
class RAMViewer;
}
class RAMLogger {
public:
enum class AccessType { Read, Write, Execute };
static constexpr int c_width = 2048;
static constexpr int c_maxHeight = 4096; // 8MB / 2048
static constexpr size_t c_maxBytes = c_width * c_maxHeight; // 8MB
void recordAccess(uint32_t physAddr, unsigned width, AccessType type, uint32_t cycle);
void enable();
void disable();
bool isEnabled() const { return m_enabled; }
void uploadRAM();
void uploadHeatmaps();
void bindReadHeatmap() { m_readHeatmapTex.bind(); }
void bindWriteHeatmap() { m_writeHeatmapTex.bind(); }
void bindExecHeatmap() { m_execHeatmapTex.bind(); }
void bindRAMTexture() { m_ramTexture.bind(); }
GLuint getRAMTextureID() { return m_ramTexture.handle(); }
// Configurable decay half-life in cycles (how many cycles until intensity halves)
float m_decayHalfLife = 33868800.0f; // ~1 second at 33.8MHz
private:
bool m_enabled = false;
bool m_hasResources = false;
// Per-byte cycle timestamp arrays (low 32 bits of cycle counter)
uint32_t m_readTimestamps[c_maxBytes] = {};
uint32_t m_writeTimestamps[c_maxBytes] = {};
uint32_t m_execTimestamps[c_maxBytes] = {};
// Heatmap textures (GL_R32UI, 2048 x maxHeight)
OpenGL::Texture m_readHeatmapTex, m_writeHeatmapTex, m_execHeatmapTex;
// Raw RAM texture (GL_R8, 2048 x maxHeight)
OpenGL::Texture m_ramTexture;
friend class Widgets::RAMViewer;
};
} // namespace PCSX
+25 -52
View File
@@ -27,6 +27,7 @@
#include "core/memorycard.h"
#include "core/pad.h"
#include "mips/common/util/sjis-fullwidth-ascii.hh"
#include "support/sjis_conv.h"
#include "support/strings-helpers.h"
@@ -362,7 +363,7 @@ void PCSX::SIO::scheduledCallback() {
#endif
}
void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock& info) {
if (block < 1 || block > 15) {
throw std::runtime_error(_("Wrong block number"));
}
@@ -373,10 +374,10 @@ void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
info.number = block;
info.mcd = mcd;
char *data = getMcdData(mcd);
uint8_t *ptr = reinterpret_cast<uint8_t *>(data) + block * c_blockSize + 2;
auto &ta = info.titleAscii;
auto &ts = info.titleSjis;
char* data = getMcdData(mcd);
uint8_t* ptr = reinterpret_cast<uint8_t*>(data) + block * c_blockSize + 2;
auto& ta = info.titleAscii;
auto& ts = info.titleSjis;
info.iconCount = std::max(1, *ptr & 0x3);
ptr += 2;
@@ -393,56 +394,28 @@ void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
c |= b;
}
// Poor man's SJIS to ASCII conversion
if (c >= 0x8281 && c <= 0x829a) {
c = (c - 0x8281) + 'a';
} else if (c >= 0x824f && c <= 0x827a) {
c = (c - 0x824f) + '0';
} else if (c == 0x8140) {
c = ' ';
} else if (c == 0x8143) {
c = ',';
} else if (c == 0x8144) {
c = '.';
} else if (c == 0x8146) {
c = ':';
} else if (c == 0x8147) {
c = ';';
} else if (c == 0x8148) {
c = '?';
} else if (c == 0x8149) {
c = '!';
} else if (c == 0x815e) {
c = '/';
} else if (c == 0x8168) {
c = '"';
} else if (c == 0x8169) {
c = '(';
} else if (c == 0x816a) {
c = ')';
} else if (c == 0x816d) {
c = '[';
} else if (c == 0x816e) {
c = ']';
} else if (c == 0x817c) {
c = '-';
} else if (c > 0x7e) {
c = '?';
bool found = false;
for (int s = 0; s < sizeof(::Sjis::c_fullwidthAsciiToSjis) / sizeof(::Sjis::c_fullwidthAsciiToSjis[0]); s++) {
if (c == ::Sjis::c_fullwidthAsciiToSjis[s]) {
c = s + 0x20;
found = true;
break;
}
}
ta += c;
ta += found ? c : '?';
}
info.titleUtf8 = Sjis::toUtf8(ts);
// Read CLUT
ptr = reinterpret_cast<uint8_t *>(data) + block * c_blockSize + 0x60;
ptr = reinterpret_cast<uint8_t*>(data) + block * c_blockSize + 0x60;
std::memcpy(clut, ptr, 16 * sizeof(uint16_t));
// Icons can have 1 to 3 frames of animation
for (uint32_t i = 0; i < info.iconCount; i++) {
uint16_t *icon = &info.icon[i * 16 * 16];
ptr = reinterpret_cast<uint8_t *>(data) + block * c_blockSize + 128 + 128 * i; // icon data
uint16_t* icon = &info.icon[i * 16 * 16];
ptr = reinterpret_cast<uint8_t*>(data) + block * c_blockSize + 128 + 128 * i; // icon data
// Fetch each pixel, store it in the icon array in ABBBBBGGGGGRRRRR with the alpha bit set to 1
for (x = 0; x < 16 * 16; x++) {
@@ -454,7 +427,7 @@ void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
}
// Parse directory frame info
const auto directoryFrame = (uint8_t *)data + block * c_sectorSize;
const auto directoryFrame = (uint8_t*)data + block * c_sectorSize;
uint32_t allocState = 0;
allocState |= directoryFrame[0];
allocState |= directoryFrame[1] << 8;
@@ -464,10 +437,10 @@ void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
char tmp[17];
memset(tmp, 0, sizeof(tmp));
std::strncpy(tmp, (const char *)&directoryFrame[0xa], 12);
std::strncpy(tmp, (const char*)&directoryFrame[0xa], 12);
info.id = tmp;
memset(tmp, 0, sizeof(tmp));
std::strncpy(tmp, (const char *)&directoryFrame[0x16], 16);
std::strncpy(tmp, (const char*)&directoryFrame[0x16], 16);
info.name = tmp;
uint32_t fileSize = 0;
@@ -492,7 +465,7 @@ void PCSX::SIO::getMcdBlockInfo(int mcd, int block, McdBlock &info) {
}
}
char *PCSX::SIO::getMcdData(int mcd) {
char* PCSX::SIO::getMcdData(int mcd) {
switch (mcd) {
case 1:
return m_memoryCard[0].getMcdData();
@@ -507,15 +480,15 @@ char *PCSX::SIO::getMcdData(int mcd) {
// Erase a memory card block by clearing it with 0s
// mcd: The memory card we want to use (1 or 2)
void PCSX::SIO::eraseMcdFile(const McdBlock &block) {
char *data = getMcdData(block.mcd);
void PCSX::SIO::eraseMcdFile(const McdBlock& block) {
char* data = getMcdData(block.mcd);
// Set the block data to 0
const size_t offset = block.number * c_blockSize;
std::memset(data + offset, 0, c_blockSize);
// Fix up the corresponding directory frame in block 0.
const auto frame = (uint8_t *)data + block.number * c_sectorSize;
const auto frame = (uint8_t*)data + block.number * c_sectorSize;
frame[0] = 0xa0; // Code for a freshly formatted block
for (auto i = 1; i < 0x7f; i++) { // Zero the rest of the frame
frame[i] = 0;
@@ -609,7 +582,7 @@ bool PCSX::SIO::copyMcdFile(McdBlock block) {
// Fix up the corresponding directory frame in block 0.
if (prevBlock != -1) {
const auto frame = reinterpret_cast<uint8_t *>(otherData) + prevBlock * c_sectorSize;
const auto frame = reinterpret_cast<uint8_t*>(otherData) + prevBlock * c_sectorSize;
uint8_t crcFix = frame[8] ^ (dstBlock - 1);
frame[8] = dstBlock - 1;
frame[0x7f] ^= crcFix;
+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>
-1
View File
@@ -44,7 +44,6 @@ class SPUInterface {
virtual void writeRegister(uint32_t, uint16_t) = 0;
virtual void playCDDAchannel(int16_t *, int) = 0;
virtual void playADPCMchannel(xa_decode_t *) = 0;
virtual void async(uint32_t) = 0;
virtual void writeDMAMem(uint16_t *, int) = 0;
virtual void readDMAMem(uint16_t *, int) = 0;
virtual void lockSPURAM() = 0;
+8 -8
View File
@@ -42,6 +42,7 @@ PCSX::SaveStates::SaveState PCSX::SaveStates::constructSaveState() {
ROM{g_emulator->m_mem->m_bios},
EXP1{g_emulator->m_mem->m_exp1},
HardwareMemory{g_emulator->m_mem->m_hard},
SRAM{g_emulator->m_mem->m_sram},
},
Registers{
GPR{g_emulator->m_cpu->m_regs.GPR.r},
@@ -218,9 +219,9 @@ void PCSX::Counters::serialize(SaveStateWrapper* w) {
counters.get<Rcnts>().value[i].get<RcntIRQState>().value = m_rcnts[i].irqState;
counters.get<Rcnts>().value[i].get<RcntCycle>().value = m_rcnts[i].cycle;
counters.get<Rcnts>().value[i].get<RcntCycleStart>().value = m_rcnts[i].cycleStart;
counters.get<Rcnts>().value[i].get<RcntGateStarted>().value = m_rcnts[i].gateStarted;
}
counters.get<HSyncCount>().value = m_hSyncCount;
counters.get<SPUSyncCountdown>().value = m_spuSyncCountdown;
counters.get<PSXNextCounter>().value = m_psxNextCounter;
}
@@ -265,7 +266,7 @@ bool PCSX::SaveStates::load(std::string_view data) {
auto& left = xa.get<SaveStates::XAADPCMLeft>();
g_emulator->m_cdrom->m_xa.left.y0 = left.get<SaveStates::ADPCMDecodeY0>().value;
g_emulator->m_cdrom->m_xa.left.y1 = left.get<SaveStates::ADPCMDecodeY1>().value;
auto& right = xa.get<SaveStates::XAADPCMLeft>();
auto& right = xa.get<SaveStates::XAADPCMRight>();
g_emulator->m_cdrom->m_xa.right.y0 = right.get<SaveStates::ADPCMDecodeY0>().value;
g_emulator->m_cdrom->m_xa.right.y1 = right.get<SaveStates::ADPCMDecodeY1>().value;
xa.get<SaveStates::XAPCM>().copyTo(reinterpret_cast<uint8_t*>(g_emulator->m_cdrom->m_xa.pcm));
@@ -383,20 +384,19 @@ void PCSX::Counters::deserialize(const SaveStateWrapper* w) {
m_rcnts[i].irqState = counters.get<Rcnts>().value[i].get<RcntIRQState>().value;
m_rcnts[i].cycle = counters.get<Rcnts>().value[i].get<RcntCycle>().value;
m_rcnts[i].cycleStart = counters.get<Rcnts>().value[i].get<RcntCycleStart>().value;
m_rcnts[i].gateStarted = counters.get<Rcnts>().value[i].get<RcntGateStarted>().value;
}
m_hSyncCount = counters.get<HSyncCount>().value;
m_spuSyncCountdown = counters.get<SPUSyncCountdown>().value;
m_psxNextCounter = counters.get<PSXNextCounter>().value;
calculateHsync();
// iCB: recalculate target count in case overclock is changed
// Recalculate rates from mode registers (handles overclock changes and dotclock)
m_rcnts[3].target =
(g_emulator->m_psxClockSpeed / (FrameRate[g_emulator->settings.get<Emulator::SettingVideo>()] *
m_HSyncTotal[g_emulator->settings.get<Emulator::SettingVideo>()]));
if (m_rcnts[1].rate != 1)
m_rcnts[1].rate =
(g_emulator->m_psxClockSpeed / (FrameRate[g_emulator->settings.get<Emulator::SettingVideo>()] *
m_HSyncTotal[g_emulator->settings.get<Emulator::SettingVideo>()]));
for (unsigned i = 0; i < 3; i++) {
recalculateRate(i);
}
m_audioFrames = g_emulator->m_spu->getCurrentFrames();
}
+7 -4
View File
@@ -46,7 +46,8 @@ typedef Protobuf::FieldPtr<Protobuf::FixedBytes<0x00800000>, TYPESTRING("ram"),
typedef Protobuf::FieldPtr<Protobuf::FixedBytes<0x00080000>, TYPESTRING("rom"), 2> ROM;
typedef Protobuf::FieldPtr<Protobuf::FixedBytes<0x00800000>, TYPESTRING("exp1"), 3> EXP1;
typedef Protobuf::FieldPtr<Protobuf::FixedBytes<0x00010000>, TYPESTRING("hardware"), 4> HardwareMemory;
typedef Protobuf::Message<TYPESTRING("Memory"), RAM, ROM, EXP1, HardwareMemory> Memory;
typedef Protobuf::FieldPtr<Protobuf::FixedBytes<0x00200000>, TYPESTRING("sram"), 5> SRAM;
typedef Protobuf::Message<TYPESTRING("Memory"), RAM, ROM, EXP1, HardwareMemory, SRAM> Memory;
typedef Protobuf::MessageField<Memory, TYPESTRING("memory"), 3> MemoryField;
typedef Protobuf::RepeatedFieldRef<Protobuf::UInt32, 34, TYPESTRING("gpr"), 1> GPR;
@@ -188,14 +189,16 @@ typedef Protobuf::Field<Protobuf::UInt32, TYPESTRING("counter_state"), 5> RcntCo
typedef Protobuf::Field<Protobuf::UInt32, TYPESTRING("irq_state"), 6> RcntIRQState;
typedef Protobuf::Field<Protobuf::UInt64, TYPESTRING("cycle"), 7> RcntCycle;
typedef Protobuf::Field<Protobuf::UInt64, TYPESTRING("cycle_start"), 8> RcntCycleStart;
typedef Protobuf::Field<Protobuf::UInt32, TYPESTRING("gate_started"), 9> RcntGateStarted;
typedef Protobuf::Message<TYPESTRING("Rcnt"), RcntMode, RcntTarget, RcntRate, RcntIRQ, RcntCounterState, RcntIRQState,
RcntCycle, RcntCycleStart>
RcntCycle, RcntCycleStart, RcntGateStarted>
Rcnt;
typedef Protobuf::RepeatedField<Rcnt, 4, TYPESTRING("rcnts"), 1> Rcnts;
typedef Protobuf::Field<Protobuf::UInt32, TYPESTRING("hsync_count"), 2> HSyncCount;
typedef Protobuf::Field<Protobuf::Int32, TYPESTRING("spu_sync_countdown"), 3> SPUSyncCountdown;
// Field 3 was spu_sync_countdown, dropped when the SPU stopped being pumped on the
// CPU's scanline schedule. DO NOT REUSE FIELD NUMBER 3 in Counters.
typedef Protobuf::Field<Protobuf::UInt64, TYPESTRING("psx_next_counter"), 4> PSXNextCounter;
typedef Protobuf::Message<TYPESTRING("Counters"), Rcnts, HSyncCount, SPUSyncCountdown, PSXNextCounter> Counters;
typedef Protobuf::Message<TYPESTRING("Counters"), Rcnts, HSyncCount, PSXNextCounter> Counters;
typedef Protobuf::MessageField<Counters, TYPESTRING("counters"), 10> CountersField;
typedef Protobuf::Field<Protobuf::UInt32, TYPESTRING("reg0"), 1> MDECReg0;
+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",
+14 -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 {
@@ -112,6 +116,10 @@ struct VRAMClick {
float x, y;
VRAMMode vramMode;
};
struct RAMFocus {
uint32_t address;
uint32_t size;
};
} // namespace GUI
struct Keyboard {
int key, scancode, action, mods;
@@ -123,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;
@@ -251,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>
+5 -2
View File
@@ -17,13 +17,15 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
#include <uv.h>
#include "core/web-server.h"
#include <llhttp.h>
#include <multipart_parser.h>
#include <charconv>
#include <magic_enum_all.hpp>
#include <magic_enum/magic_enum_all.hpp>
#include <map>
#include <memory>
#include <string>
@@ -632,7 +634,8 @@ class StateExecutor : public PCSX::WebExecutor {
}
}
if (message.empty()) {
std::filesystem::path saveFilepath(PCSX::g_gui->buildSaveStateFilename(name));
std::filesystem::path saveFilepath =
PCSX::Widgets::NamedSaveStates::createSaveStatePath(PCSX::g_gui, name);
bool success = false;
if (path == "load") {
success = PCSX::g_gui->loadSaveState(saveFilepath);
+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")
+1 -1
View File
@@ -65,7 +65,7 @@ void PCSX::SoftGPU::impl::clearVRAM() {
GUI *gui = dynamic_cast<GUI *>(m_ui);
if (!gui) return;
const auto oldTex = OpenGL::getTex2D();
std::memset(m_allocatedVRAM, 0x00, (GPU_HEIGHT * 2) * 1024 + (1024 * 1024));
std::memset(m_allocatedVRAM, 0x00, (VRAM_HEIGHT * 2) * 1024 + (1024 * 1024));
glBindTexture(GL_TEXTURE_2D, m_vramTexture16);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1024, 512, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV, m_allocatedVRAM);
+144 -126
View File
@@ -23,20 +23,24 @@
#include "core/debug.h"
#include "core/psxemulator.h"
#include "gpu/soft/interface.h"
#include "gpu/soft/pixel-writer.h"
#include "gpu/soft/soft.h"
#include "gui/gui.h"
#include "imgui.h"
#include "support/imgui-helpers.h"
#include "tracy/Tracy.hpp"
#define GPUSTATUS_DMABITS 0x60000000
#define GPUSTATUS_READYFORCOMMANDS 0x10000000
#define GPUSTATUS_IDLE 0x04000000
#define GPUSTATUS_DISPLAYDISABLED 0x00800000
#define GPUSTATUS_INTERLACED 0x00400000
#define GPUSTATUS_RGB24 0x00200000
#define GPUSTATUS_PAL 0x00100000
#define GPUSTATUS_DOUBLEHEIGHT 0x00080000
#define GPUSTATUS_WIDTHBITS 0x00070000
// GP1 status-register bit definitions. Names are protocol-visible and
// must stay stable - logs and debugger expressions read them out.
static constexpr uint32_t GPUSTATUS_DMABITS = 0x60000000;
static constexpr uint32_t GPUSTATUS_READYFORCOMMANDS = 0x10000000;
static constexpr uint32_t GPUSTATUS_IDLE = 0x04000000;
static constexpr uint32_t GPUSTATUS_DISPLAYDISABLED = 0x00800000;
static constexpr uint32_t GPUSTATUS_INTERLACED = 0x00400000;
static constexpr uint32_t GPUSTATUS_RGB24 = 0x00200000;
static constexpr uint32_t GPUSTATUS_PAL = 0x00100000;
static constexpr uint32_t GPUSTATUS_DOUBLEHEIGHT = 0x00080000;
static constexpr uint32_t GPUSTATUS_WIDTHBITS = 0x00070000;
int32_t PCSX::SoftGPU::impl::initBackend(UI *ui) {
m_ui = ui;
@@ -44,7 +48,7 @@ int32_t PCSX::SoftGPU::impl::initBackend(UI *ui) {
initDisplay();
// always alloc one extra MB for soft drawing funcs security
m_allocatedVRAM = new uint8_t[(GPU_HEIGHT * 2) * 1024 + (1024 * 1024)]();
m_allocatedVRAM = new uint8_t[(VRAM_HEIGHT * 2) * 1024 + (1024 * 1024)]();
if (!m_allocatedVRAM) return -1;
//!!! ATTENTION !!!
@@ -83,6 +87,8 @@ int32_t PCSX::SoftGPU::impl::shutdown() {
std::unique_ptr<PCSX::GPU> PCSX::GPU::getSoft() { return std::unique_ptr<PCSX::GPU>(new PCSX::SoftGPU::impl()); }
void PCSX::SoftGPU::impl::updateDisplay(bool fromGui) {
GUI *gui = dynamic_cast<GUI *>(m_ui);
if (!gui) return;
if (m_softDisplay.Disabled) {
glClearColor(1, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
@@ -125,8 +131,11 @@ void PCSX::SoftGPU::impl::changeDispOffsetsX() {
m_previousDisplay.Range.x1 += (int16_t)(lx - l);
}
glClearColor(1, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
GUI *gui = dynamic_cast<GUI *>(m_ui);
if (gui) {
glClearColor(1, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
}
}
m_doVSyncUpdate = true;
@@ -136,9 +145,9 @@ void PCSX::SoftGPU::impl::changeDispOffsetsY() {
int iT, iO = m_previousDisplay.Range.y0;
int iOldYOffset = m_previousDisplay.DisplayModeNew.y;
if ((m_previousDisplay.DisplayModeNew.x + m_softDisplay.DisplayModeNew.y) > GPU_HEIGHT) {
int dy1 = GPU_HEIGHT - m_previousDisplay.DisplayModeNew.x;
int dy2 = (m_previousDisplay.DisplayModeNew.x + m_softDisplay.DisplayModeNew.y) - GPU_HEIGHT;
if ((m_previousDisplay.DisplayModeNew.x + m_softDisplay.DisplayModeNew.y) > VRAM_HEIGHT) {
int dy1 = VRAM_HEIGHT - m_previousDisplay.DisplayModeNew.x;
int dy2 = (m_previousDisplay.DisplayModeNew.x + m_softDisplay.DisplayModeNew.y) - VRAM_HEIGHT;
if (dy1 >= dy2) {
m_previousDisplay.DisplayModeNew.y = -dy2;
@@ -171,8 +180,11 @@ void PCSX::SoftGPU::impl::changeDispOffsetsY() {
}
if (iO != m_previousDisplay.Range.y0) {
glClearColor(1, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
GUI *gui = dynamic_cast<GUI *>(m_ui);
if (gui) {
glClearColor(1, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
}
}
}
@@ -277,9 +289,7 @@ void PCSX::SoftGPU::impl::debug() {
ImGui::End();
}
static constexpr inline uint16_t BGR24to16(uint32_t BGR) {
return (uint16_t)(((BGR >> 3) & 0x1f) | ((BGR & 0xf80000) >> 9) | ((BGR & 0xf800) >> 6));
}
static constexpr inline uint16_t BGR24to16(uint32_t BGR) { return PCSX::SoftGPU::Channel555::fromCommandColor(BGR); }
static constexpr inline uint16_t BGR24to16(PCSX::GPU::EmptyColor) { return 0; }
@@ -301,7 +311,7 @@ void PCSX::SoftGPU::impl::write0(FastFill *prim) {
sW += sX;
sH += sY;
fillSoftwareArea(sX, sY, sW, sH, BGR24to16(prim->color));
fillArea(sX, sY, sW, sH, BGR24to16(prim->color));
m_doVSyncUpdate = true;
}
@@ -309,20 +319,22 @@ void PCSX::SoftGPU::impl::write0(FastFill *prim) {
template <PCSX::GPU::Shading shading, PCSX::GPU::Shape shape, PCSX::GPU::Textured textured, PCSX::GPU::Blend blend,
PCSX::GPU::Modulation modulation>
void PCSX::SoftGPU::impl::polyExec(Poly<shading, shape, textured, blend, modulation> *prim) {
m_x0 = prim->x[0];
m_y0 = prim->y[0];
m_x1 = prim->x[1];
m_y1 = prim->y[1];
m_x2 = prim->x[2];
m_y2 = prim->y[2];
int16_t x0 = prim->x[0];
int16_t y0 = prim->y[0];
int16_t x1 = prim->x[1];
int16_t y1 = prim->y[1];
int16_t x2 = prim->x[2];
int16_t y2 = prim->y[2];
int16_t x3 = 0;
int16_t y3 = 0;
if constexpr (shape == Shape::Quad) {
m_x3 = prim->x[3];
m_y3 = prim->y[3];
if (checkCoord4()) return;
applyOffset4();
x3 = prim->x[3];
y3 = prim->y[3];
if (checkCoord4(x0, y0, x1, y1, x2, y2, x3, y3)) return;
applyOffset4(x0, y0, x1, y1, x2, y2, x3, y3);
} else {
if (checkCoord3()) return;
applyOffset3();
if (checkCoord3(x0, y0, x1, y1, x2, y2)) return;
applyOffset3(x0, y0, x1, y1, x2, y2);
}
m_drawSemiTrans = blend == Blend::Semi;
@@ -344,44 +356,61 @@ void PCSX::SoftGPU::impl::polyExec(Poly<shading, shape, textured, blend, modulat
}
texturePage(&prim->tpage);
if constexpr (shape == Shape::Quad) {
// Decompose into two triangles using the (1,3,2)+(0,1,2)
// PSX vertex split that drawPolyFlat4 and drawPoly4TG
// already use. drawPoly3T carries the same pixel-centre
// bias as the retired 4-vertex flat-textured walker, so
// this split is bit-equivalent to the old sweep for
// QFD-class geometry.
switch (m_globalTextTP) {
case GPU::TexDepth::Tex4Bits:
drawPoly4TEx4(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2],
prim->clutX(), prim->clutY());
drawPoly3T<TexMode::Clut4>(x1, y1, x3, y3, x2, y2, prim->u[1], prim->v[1], prim->u[3],
prim->v[3], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
drawPoly3T<TexMode::Clut4>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
break;
case GPU::TexDepth::Tex8Bits:
drawPoly4TEx8(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2],
prim->clutX(), prim->clutY());
drawPoly3T<TexMode::Clut8>(x1, y1, x3, y3, x2, y2, prim->u[1], prim->v[1], prim->u[3],
prim->v[3], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
drawPoly3T<TexMode::Clut8>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
break;
case GPU::TexDepth::Tex16Bits:
drawPoly4TD(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2]);
drawPoly3T<TexMode::Direct15>(x1, y1, x3, y3, x2, y2, prim->u[1], prim->v[1], prim->u[3],
prim->v[3], prim->u[2], prim->v[2], 0, 0);
drawPoly3T<TexMode::Direct15>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], 0, 0);
break;
}
} else {
switch (m_globalTextTP) {
case GPU::TexDepth::Tex4Bits:
drawPoly3TEx4(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(), prim->clutY());
drawPoly3T<TexMode::Clut4>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
break;
case GPU::TexDepth::Tex8Bits:
drawPoly3TEx8(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(), prim->clutY());
drawPoly3T<TexMode::Clut8>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY());
break;
case GPU::TexDepth::Tex16Bits:
drawPoly3TD(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2]);
drawPoly3T<TexMode::Direct15>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], 0, 0);
break;
}
}
}
} else {
if constexpr (shape == Shape::Quad) {
drawPolyFlat4(prim->colors[0]);
drawPoly3F(x1, y1, x3, y3, x2, y2, prim->colors[0]);
drawPoly3F(x0, y0, x1, y1, x2, y2, prim->colors[0]);
} else {
drawPolyFlat3(prim->colors[0]);
drawPoly3F(x0, y0, x1, y1, x2, y2, prim->colors[0]);
}
}
} else {
@@ -395,48 +424,52 @@ void PCSX::SoftGPU::impl::polyExec(Poly<shading, shape, textured, blend, modulat
if constexpr (shape == Shape::Quad) {
switch (m_globalTextTP) {
case GPU::TexDepth::Tex4Bits:
drawPoly4TGEx4(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2],
prim->clutX(), prim->clutY(), prim->colors[0], prim->colors[1],
prim->colors[2], prim->colors[3]);
drawPoly4TG<TexMode::Clut4>(x0, y0, x1, y1, x3, y3, x2, y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2],
prim->v[2], prim->clutX(), prim->clutY(), prim->colors[0],
prim->colors[1], prim->colors[2], prim->colors[3]);
break;
case GPU::TexDepth::Tex8Bits:
drawPoly4TGEx8(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2],
prim->clutX(), prim->clutY(), prim->colors[0], prim->colors[1],
prim->colors[2], prim->colors[3]);
drawPoly4TG<TexMode::Clut8>(x0, y0, x1, y1, x3, y3, x2, y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2],
prim->v[2], prim->clutX(), prim->clutY(), prim->colors[0],
prim->colors[1], prim->colors[2], prim->colors[3]);
break;
case GPU::TexDepth::Tex16Bits:
drawPoly4TGD(m_x0, m_y0, m_x1, m_y1, m_x3, m_y3, m_x2, m_y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2], prim->v[2],
prim->colors[0], prim->colors[1], prim->colors[2], prim->colors[3]);
drawPoly4TG<TexMode::Direct15>(x0, y0, x1, y1, x3, y3, x2, y2, prim->u[0], prim->v[0],
prim->u[1], prim->v[1], prim->u[3], prim->v[3], prim->u[2],
prim->v[2], 0, 0, prim->colors[0], prim->colors[1],
prim->colors[2], prim->colors[3]);
break;
}
} else {
switch (m_globalTextTP) {
case GPU::TexDepth::Tex4Bits:
drawPoly3TGEx4(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(), prim->clutY(),
prim->colors[0], prim->colors[1], prim->colors[2]);
drawPoly3TG<TexMode::Clut4>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY(), prim->colors[0], prim->colors[1],
prim->colors[2]);
break;
case GPU::TexDepth::Tex8Bits:
drawPoly3TGEx8(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(), prim->clutY(),
prim->colors[0], prim->colors[1], prim->colors[2]);
drawPoly3TG<TexMode::Clut8>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->clutX(),
prim->clutY(), prim->colors[0], prim->colors[1],
prim->colors[2]);
break;
case GPU::TexDepth::Tex16Bits:
drawPoly3TGD(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], prim->colors[0], prim->colors[1],
prim->colors[2]);
drawPoly3TG<TexMode::Direct15>(x0, y0, x1, y1, x2, y2, prim->u[0], prim->v[0], prim->u[1],
prim->v[1], prim->u[2], prim->v[2], 0, 0, prim->colors[0],
prim->colors[1], prim->colors[2]);
break;
}
}
}
} else {
if constexpr (shape == Shape::Quad) {
drawPolyShade4(prim->colors[0], prim->colors[1], prim->colors[2], prim->colors[3]);
drawPoly4G(x0, y0, x1, y1, x2, y2, x3, y3, prim->colors[0], prim->colors[1], prim->colors[2],
prim->colors[3]);
} else {
drawPolyShade3(prim->colors[0], prim->colors[1], prim->colors[2]);
drawPoly3G(x0, y0, x1, y1, x2, y2, prim->colors[0], prim->colors[1], prim->colors[2]);
}
}
}
@@ -446,21 +479,15 @@ void PCSX::SoftGPU::impl::polyExec(Poly<shading, shape, textured, blend, modulat
static constexpr int CHKMAX_X = 1024;
static constexpr int CHKMAX_Y = 512;
// Hardware drops lines whose endpoint delta exceeds the same per-edge bounds
// that gate polygons: |dx| <= 1023, |dy| <= 511 (verified on SCPH-5501 via
// gpu-raster-phase14 ct_line_dx_1024_drop / ct_line_dx_2047_drop).
static constexpr inline bool CheckCoordL(int16_t slx0, int16_t sly0, int16_t slx1, int16_t sly1) {
if (slx0 < 0) {
if ((slx1 - slx0) > CHKMAX_X) return true;
}
if (slx1 < 0) {
if ((slx0 - slx1) > CHKMAX_X) return true;
}
if (sly0 < 0) {
if ((sly1 - sly0) > CHKMAX_Y) return true;
}
if (sly1 < 0) {
if ((sly0 - sly1) > CHKMAX_Y) return true;
}
return false;
int dx = slx1 - slx0;
int dy = sly1 - sly0;
if (dx < 0) dx = -dx;
if (dy < 0) dy = -dy;
return dx >= CHKMAX_X || dy >= CHKMAX_Y;
}
template <PCSX::GPU::Shading shading, PCSX::GPU::LineType lineType, PCSX::GPU::Blend blend>
@@ -470,25 +497,17 @@ void PCSX::SoftGPU::impl::lineExec(Line<shading, lineType, blend> *prim) {
m_drawSemiTrans = blend == Blend::Semi;
for (unsigned i = 1; i < count; i++) {
auto x0 = prim->x[i - 1];
auto x1 = prim->x[i];
auto y0 = prim->y[i - 1];
auto y1 = prim->y[i];
int16_t x0 = prim->x[i - 1];
int16_t x1 = prim->x[i];
int16_t y0 = prim->y[i - 1];
int16_t y1 = prim->y[i];
auto c0 = prim->colors[i - 1];
auto c1 = prim->colors[i];
if (CheckCoordL(x0, y0, x1, y1)) continue;
m_y0 = y0;
m_x0 = x0;
m_y1 = y1;
m_x1 = x1;
applyOffset2();
if constexpr (shading == Shading::Gouraud) {
drawSoftwareLineShade(c0, c1);
} else {
drawSoftwareLineFlat(c0);
}
applyOffset2(x0, y0, x1, y1);
drawLine<shading>(x0, y0, x1, y1, c0, c1);
}
m_doVSyncUpdate = true;
}
@@ -497,12 +516,20 @@ template <PCSX::GPU::Size size, PCSX::GPU::Textured textured, PCSX::GPU::Blend b
void PCSX::SoftGPU::impl::rectExec(Rect<size, textured, blend, modulation> *prim) {
int16_t w, h;
m_x0 = prim->x;
m_y0 = prim->y;
int16_t x0 = prim->x;
int16_t y0 = prim->y;
int16_t x1, y1, x2, y2, x3, y3;
if constexpr (size == Size::Variable) {
w = prim->w;
h = prim->h;
// Hardware masks the variable-rect dimensions to 10 bits (width) and
// 9 bits (height); the effective extents are `w & 0x3FF` / `h & 0x1FF`,
// not the documented `((w-1) & mask) + 1` shape. A width of 1024 maps
// to 0 and silently drops the primitive on real silicon (verified via
// gpu-raster-phase14 ct_rect_w1024 / ct_rect_h512 against SCPH-5501),
// while 1025/513 fall through as 1-pixel wide/tall.
w = static_cast<int16_t>(prim->w & 0x3FF);
h = static_cast<int16_t>(prim->h & 0x1FF);
if (w == 0 || h == 0) return;
} else if constexpr (size == Size::S1) {
w = h = 1;
} else if constexpr (size == Size::S8) {
@@ -521,36 +548,27 @@ void PCSX::SoftGPU::impl::rectExec(Rect<size, textured, blend, modulation> *prim
m_m1 = m_m2 = m_m3 = 128;
}
m_x1 = m_x2 = m_x0 + w + m_softDisplay.DrawOffset.x;
m_x0 = m_x3 = m_x0 + m_softDisplay.DrawOffset.x;
m_y2 = m_y3 = m_y0 + h + m_softDisplay.DrawOffset.y;
m_y0 = m_y1 = m_y0 + m_softDisplay.DrawOffset.y;
x1 = x2 = x0 + w + m_softDisplay.DrawOffset.x;
x0 = x3 = x0 + m_softDisplay.DrawOffset.x;
y2 = y3 = y0 + h + m_softDisplay.DrawOffset.y;
y0 = y1 = y0 + m_softDisplay.DrawOffset.y;
if ((textured == Textured::Yes) && !m_disableTexturesInRectangles) {
if constexpr (textured == Textured::Yes) {
int16_t tx0, ty0, tx1, ty1, tx2, ty2, tx3, ty3;
tx0 = tx3 = prim->u;
tx1 = tx2 = tx0 + w;
ty0 = ty1 = prim->v;
ty2 = ty3 = ty0 + h;
switch (m_globalTextTP) {
case GPU::TexDepth::Tex4Bits:
drawPoly4TEx4_S(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, m_x3, m_y3, tx0, ty0, tx1, ty1, tx2, ty2, tx3,
ty3, prim->clutX(), prim->clutY());
drawSprite<TexMode::Clut4>(x0, y0, w, h, prim->u, prim->v, prim->clutX(), prim->clutY());
break;
case GPU::TexDepth::Tex8Bits:
drawPoly4TEx8_S(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, m_x3, m_y3, tx0, ty0, tx1, ty1, tx2, ty2, tx3,
ty3, prim->clutX(), prim->clutY());
drawSprite<TexMode::Clut8>(x0, y0, w, h, prim->u, prim->v, prim->clutX(), prim->clutY());
break;
case GPU::TexDepth::Tex16Bits:
drawPoly4TD_S(m_x0, m_y0, m_x1, m_y1, m_x2, m_y2, m_x3, m_y3, tx0, ty0, tx1, ty1, tx2, ty2, tx3,
ty3);
drawSprite<TexMode::Direct15>(x0, y0, w, h, prim->u, prim->v, 0, 0);
break;
}
}
} else {
fillSoftwareAreaTrans(m_x0, m_y0, m_x2, m_y2, BGR24to16(prim->color));
drawRect(x0, y0, x2, y2, BGR24to16(prim->color));
}
m_doVSyncUpdate = true;
@@ -570,13 +588,13 @@ void PCSX::SoftGPU::impl::write0(BlitVramVram *prim) {
if (imageSX <= 0) return;
if (imageSY <= 0) return;
if ((imageY0 + imageSY) > GPU_HEIGHT || (imageX0 + imageSX) > 1024 || (imageY1 + imageSY) > GPU_HEIGHT ||
if ((imageY0 + imageSY) > VRAM_HEIGHT || (imageX0 + imageSX) > 1024 || (imageY1 + imageSY) > VRAM_HEIGHT ||
(imageX1 + imageSX) > 1024) {
int i, j;
for (j = 0; j < imageSY; j++) {
for (i = 0; i < imageSX; i++) {
m_vram16[(1024 * ((imageY1 + j) & GPU_HEIGHT_MASK)) + ((imageX1 + i) & 0x3ff)] =
m_vram16[(1024 * ((imageY0 + j) & GPU_HEIGHT_MASK)) + ((imageX0 + i) & 0x3ff)];
m_vram16[(1024 * ((imageY1 + j) & VRAM_Y_MASK)) + ((imageX1 + i) & VRAM_X_MASK)] =
m_vram16[(1024 * ((imageY0 + j) & VRAM_Y_MASK)) + ((imageX0 + i) & VRAM_X_MASK)];
}
}
@@ -705,9 +723,9 @@ void PCSX::SoftGPU::impl::write1(CtrlDisplayStart *ctrl) {
// store the same val in some helper var, we need it on later compares
m_previousDisplay.DisplayModeNew.x = m_softDisplay.DisplayPosition.y;
if ((m_softDisplay.DisplayPosition.y + m_softDisplay.DisplayMode.y) > GPU_HEIGHT) {
int dy1 = GPU_HEIGHT - m_softDisplay.DisplayPosition.y;
int dy2 = (m_softDisplay.DisplayPosition.y + m_softDisplay.DisplayMode.y) - GPU_HEIGHT;
if ((m_softDisplay.DisplayPosition.y + m_softDisplay.DisplayMode.y) > VRAM_HEIGHT) {
int dy1 = VRAM_HEIGHT - m_softDisplay.DisplayPosition.y;
int dy2 = (m_softDisplay.DisplayPosition.y + m_softDisplay.DisplayMode.y) - VRAM_HEIGHT;
if (dy1 >= dy2) {
m_previousDisplay.DisplayModeNew.y = -dy2;
+1 -1
View File
@@ -28,7 +28,7 @@ class UI;
namespace SoftGPU {
class impl final : public GPU, public SoftRenderer {
class impl final : public GPU, private SoftRenderer {
int32_t initBackend(UI *) override;
int32_t shutdown() override;
uint32_t readStatusInternal() override;
+161
View File
@@ -0,0 +1,161 @@
/***************************************************************************
* 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 "gpu/soft/soft.h"
template <PCSX::SoftGPU::Line::Axis MajorAxis, PCSX::SoftGPU::Line::MajorSign MaSign,
PCSX::SoftGPU::Line::MinorSign MiSign, PCSX::SoftGPU::Line::Bias B, PCSX::GPU::Shading Shading>
void PCSX::SoftGPU::SoftRenderer::drawLineOctant(int x0, int y0, int x1, int y1, uint32_t rgb0, uint32_t rgb1) {
const auto drawX = m_drawX;
const auto drawY = m_drawY;
const auto drawH = m_drawH;
const auto drawW = m_drawW;
// Major-axis distance for the gouraud interpolation denominator.
int steps;
if constexpr (MajorAxis == Line::Axis::X) {
steps = x1 - x0;
} else if constexpr (MaSign == Line::MajorSign::Plus) {
steps = y1 - y0;
} else {
steps = y0 - y1;
}
LineStepper<MajorAxis, MaSign, MiSign, B> stepper(x0, y0, x1, y1);
LineColorWalker<Shading> walker(rgb0, rgb1, steps);
RasterState rs = makeBaseRasterState();
auto plot = [&](int x, int y) {
if ((x >= drawX) && (x < drawW) && (y >= drawY) && (y < drawH)) {
PixelWriter<false, GPU::Shading::Flat, WriteMode::Default>::scalar(rs, x, y, walker.current555());
}
};
plot(stepper.x(), stepper.y());
int stepIdx = 0;
while (stepper.more()) {
stepper.advance();
++stepIdx;
walker.advanceTo(stepIdx);
plot(stepper.x(), stepper.y());
}
}
template <PCSX::SoftGPU::Line::Axis Iter, PCSX::GPU::Shading Shading>
void PCSX::SoftGPU::SoftRenderer::drawAxisLine(int constCoord, int varStart, int varEnd, uint32_t rgb0, uint32_t rgb1) {
const int steps = varEnd - varStart;
const int varStartOrig = varStart;
LineColorWalker<Shading> walker(rgb0, rgb1, steps);
if constexpr (Iter == Line::Axis::X) {
if (varStart < m_drawX) varStart = m_drawX;
if (varEnd > m_drawW) varEnd = m_drawW;
} else {
if (varStart < m_drawY) varStart = m_drawY;
if (varEnd > m_drawH) varEnd = m_drawH;
}
RasterState rs = makeBaseRasterState();
for (int v = varStart; v <= varEnd; ++v) {
walker.advanceTo(v - varStartOrig);
if constexpr (Iter == Line::Axis::X) {
PixelWriter<false, GPU::Shading::Flat, WriteMode::Default>::scalar(rs, v, constCoord, walker.current555());
} else {
PixelWriter<false, GPU::Shading::Flat, WriteMode::Default>::scalar(rs, constCoord, v, walker.current555());
}
}
}
/* Bresenham Line drawing function */
template <PCSX::GPU::Shading Shading>
void PCSX::SoftGPU::SoftRenderer::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int32_t rgb0, int32_t rgb1) {
int16_t xt, yt;
double m, dy, dx;
if (x0 > m_drawW && x1 > m_drawW) return;
if (y0 > m_drawH && y1 > m_drawH) return;
if (x0 < m_drawX && x1 < m_drawX) return;
if (y0 < m_drawY && y1 < m_drawY) return;
if (m_drawY >= m_drawH) return;
if (m_drawX >= m_drawW) return;
dx = x1 - x0;
dy = y1 - y0;
if (dx == 0) {
if (dy == 0) {
// Zero-length line: hardware draws exactly one pixel at the vertex.
// The GP0 command color arrives as a 24-bit BGR888 word; convert
// to the BGR555 layout the writer expects before storing.
if ((x0 >= m_drawX) && (x0 < m_drawW) && (y0 >= m_drawY) && (y0 < m_drawH)) {
RasterState rs = makeBaseRasterState();
PixelWriter<false, GPU::Shading::Flat, WriteMode::Default>::scalar(
rs, x0, y0, Channel555::fromCommandColor(rgb0));
}
return;
} else if (dy > 0) {
drawAxisLine<Line::Axis::Y, Shading>(x0, y0, y1, rgb0, rgb1);
} else {
drawAxisLine<Line::Axis::Y, Shading>(x0, y1, y0, rgb0, rgb1);
}
} else if (dy == 0) {
if (dx > 0) {
drawAxisLine<Line::Axis::X, Shading>(y0, x0, x1, rgb0, rgb1);
} else {
drawAxisLine<Line::Axis::X, Shading>(y0, x1, x0, rgb0, rgb1);
}
} else {
if (dx < 0) {
xt = x0;
yt = y0;
x0 = x1;
y0 = y1;
x1 = xt;
y1 = yt;
dx = x1 - x0;
dy = y1 - y0;
}
m = dy / dx;
if (m >= 0) {
if (m > 1) {
drawLineOctant<Line::Axis::Y, Line::MajorSign::Plus, Line::MinorSign::Plus, Line::Bias::Steep, Shading>(
x0, y0, x1, y1, rgb0, rgb1);
} else {
drawLineOctant<Line::Axis::X, Line::MajorSign::Plus, Line::MinorSign::Plus, Line::Bias::Shallow,
Shading>(x0, y0, x1, y1, rgb0, rgb1);
}
} else if (m < -1) {
drawLineOctant<Line::Axis::Y, Line::MajorSign::Minus, Line::MinorSign::Plus, Line::Bias::Steep, Shading>(
x0, y0, x1, y1, rgb0, rgb1);
} else {
drawLineOctant<Line::Axis::X, Line::MajorSign::Plus, Line::MinorSign::Minus, Line::Bias::Shallow, Shading>(
x0, y0, x1, y1, rgb0, rgb1);
}
}
}
template void PCSX::SoftGPU::SoftRenderer::drawLine<PCSX::GPU::Shading::Flat>(int16_t x0, int16_t y0, int16_t x1,
int16_t y1, int32_t rgb0, int32_t rgb1);
template void PCSX::SoftGPU::SoftRenderer::drawLine<PCSX::GPU::Shading::Gouraud>(int16_t x0, int16_t y0, int16_t x1,
int16_t y1, int32_t rgb0,
int32_t rgb1);
+802
View File
@@ -0,0 +1,802 @@
/***************************************************************************
* 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. *
***************************************************************************/
#pragma once
#include <stdint.h>
#include "core/gpu.h"
#include "gpu/soft/raster-state.h"
namespace PCSX {
namespace SoftGPU {
// Compile-time write mode for the per-pixel writer policy. Picks which
// branches of the existing pixel-helper family the writer collapses to.
//
// - Solid: guaranteed !checkMask && !drawSemiTrans. The fast path
// writes through with mask/blend disabled. Zero-texel halves
// of a packed pair preserve the existing destination bits;
// the source's mask bit gets ORed into the result. Matches
// the legacy `getTextureTransCol*Solid` helpers.
// - Default: handles checkMask and drawSemiTrans at runtime by reading
// RasterState. Matches the legacy `getTextureTransCol*`
// helpers without the Solid suffix.
enum class WriteMode { Solid, Default };
// Saturating helper for packed-pair 5-bit channels.
//
// The packed writers represent one color channel as an int32_t whose two
// 16-bit halfwords each carry a 5-bit value in bits 0..4. The value
// arrives there post-modulate, typically as
//
// int32_t r = (((color & 0x001f001f) * factor) & 0xff80ff80) >> 7;
//
// which leaves each halfword in 0..0x3f. saturate5Pair clamps each
// halfword to 31: if any of bits 5..14 of a halfword are set, that
// halfword's low five bits become 0x1f. The opposite halfword is
// preserved bit-for-bit. The two halves are tested independently so a
// single helper covers both lanes; the carry behaviour is identical to
// the open-coded six-line pattern this replaces.
struct PackedPair555 {
static inline int32_t saturate5Pair(int32_t c) {
if (c & 0x7fe00000) c = 0x1f0000 | (c & 0xffff);
if (c & 0x7fe0) c = 0x1f | (c & 0xffff0000);
return c;
}
// Compose the final BGR555 packed pair from three right-aligned
// channels. Each input is a 32-bit value carrying one 5-bit channel
// value in each halfword (bits 0..4). The output is two consecutive
// BGR555 pixels: R in bits 0..4, B in bits 5..9, G in bits 10..14.
static inline uint32_t packBGR(uint32_t r, uint32_t b, uint32_t g) { return (g << 10) | (b << 5) | r; }
// Lane preservation: "preserveLowHalf(dest, val)" returns the
// composition that keeps `dest`'s low halfword and takes `val`'s
// high halfword. preserveHighHalf is the mirror. Used at the end of
// the packed pixel writers, where a zero source halfword (transparent
// texel) or a set destination mask bit means the corresponding
// destination halfword survives unchanged.
static inline uint32_t preserveLowHalf(uint32_t dest, uint32_t val) { return (dest & 0xffff) | (val & 0xffff0000); }
static inline uint32_t preserveHighHalf(uint32_t dest, uint32_t val) {
return (val & 0xffff) | (dest & 0xffff0000);
}
// Right-aligned channel extraction from a packed pair. Each helper
// returns a value whose two halfwords each carry the corresponding
// channel in bits 0..4. Replaces the legacy X32COL1/2/3 macros.
static inline uint32_t extractR(uint32_t c) { return c & 0x001f001f; }
static inline uint32_t extractB(uint32_t c) { return (c >> 5) & 0x001f001f; }
static inline uint32_t extractG(uint32_t c) { return (c >> 10) & 0x001f001f; }
// Right-aligned channel extract with bits 0..1 cleared per halfword.
// The HalfBackAndQuarter blend computes `(F >> 2)`; clearing the
// low two bits up front keeps the shift lossless. Replaces the
// legacy X32BCOL1/2/3 macros.
static inline uint32_t extractRForQuarter(uint32_t c) { return c & 0x001c001c; }
static inline uint32_t extractBForQuarter(uint32_t c) { return (c >> 5) & 0x001c001c; }
static inline uint32_t extractGForQuarter(uint32_t c) { return (c >> 10) & 0x001c001c; }
// Pre-aligned native-position channel for the HalfBackAndHalfFront
// carry-preserving blend reformulation. Each channel is shifted so
// the result lands at bits 7..11 of each halfword - the common
// position where the modulated source can be added in without
// losing the bit-0 carry that the historical `(B|F) & 0x7bde`
// shortcut dropped. Replaces the legacy X32TCOL1/2/3 macros.
static inline uint32_t alignRForHalfBlend(uint32_t c) { return (c & 0x001f001f) << 7; }
static inline uint32_t alignBForHalfBlend(uint32_t c) { return (c & 0x03e003e0) << 2; }
static inline uint32_t alignGForHalfBlend(uint32_t c) { return (c & 0x7c007c00) >> 3; }
// Texture modulation for packed-pair channels. The post-multiply mask is
// part of the lane contract: it prevents overflow from one halfword from
// bleeding into the other before the divide-by-128 shift.
static inline int32_t modulateExtracted(uint32_t extracted, int32_t factor) {
return static_cast<int32_t>(((extracted * factor) & 0xff80ff80) >> 7);
}
static inline int32_t modulateR(uint32_t c, int32_t factor) { return modulateExtracted(extractR(c), factor); }
static inline int32_t modulateB(uint32_t c, int32_t factor) { return modulateExtracted(extractB(c), factor); }
static inline int32_t modulateG(uint32_t c, int32_t factor) { return modulateExtracted(extractG(c), factor); }
static inline uint32_t scaledRForHalfBlend(uint32_t c, int32_t factor) { return extractR(c) * factor; }
static inline uint32_t scaledBForHalfBlend(uint32_t c, int32_t factor) { return extractB(c) * factor; }
static inline uint32_t scaledGForHalfBlend(uint32_t c, int32_t factor) { return extractG(c) * factor; }
static inline int32_t modulateRForQuarter(uint32_t c, int32_t factor) {
return modulateExtracted(extractRForQuarter(c) >> 2, factor);
}
static inline int32_t modulateBForQuarter(uint32_t c, int32_t factor) {
return modulateExtracted(extractBForQuarter(c) >> 2, factor);
}
static inline int32_t modulateGForQuarter(uint32_t c, int32_t factor) {
return modulateExtracted(extractGForQuarter(c) >> 2, factor);
}
static inline int32_t modulateRLowHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractR(c) * factor) & 0x0000ff80) >> 7);
}
static inline int32_t modulateBLowHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractB(c) * factor) & 0x0000ff80) >> 7);
}
static inline int32_t modulateGLowHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractG(c) * factor) & 0x0000ff80) >> 7);
}
static inline int32_t modulateRHighHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractR(c) * factor) & 0xff800000) >> 7);
}
static inline int32_t modulateBHighHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractB(c) * factor) & 0xff800000) >> 7);
}
static inline int32_t modulateGHighHalf(uint32_t c, int32_t factor) {
return static_cast<int32_t>(((extractG(c) * factor) & 0xff800000) >> 7);
}
};
// Native-position channel traits for the BGR555 layout.
//
// The PSX 16-bit pixel layout keeps each colour channel in a fixed
// position: R in bits 0..4, B in bits 5..9, G in bits 10..14, mask in
// bit 15. The scalar pixel writers do per-channel math entirely at
// native position - extract via `color & Mask`, multiply by a 0..255
// modulation factor, shift right by 7, then saturate. The saturate is
// the same shape across all three channels: probe whether any bit
// above the channel's native top is set, and clamp to the channel's
// max value if so.
//
// Channel555::R / B / G expose `mask` (native-position extract /
// max-value) and `saturateNative(c)` (post-multiply clamp). Use the
// channel type for both the extract and the saturate so the two halves
// of the round-trip stay consistent.
namespace Channel555 {
template <uint32_t Mask, uint32_t Overflow, int Shift>
struct Trait {
static constexpr uint32_t mask = Mask;
static constexpr uint32_t overflowMask = Overflow;
static constexpr int shift = Shift;
static inline int32_t saturateNative(int32_t c) {
if (c & Overflow) c = Mask;
return c;
}
// Native-position extract: returns the channel still at its source
// bit range (R: bits 0..4, B: 5..9, G: 10..14). Replaces legacy
// XCOL1/2/3.
static inline int32_t extractNative(uint32_t c) { return c & Mask; }
// Right-aligned extract: shifts the channel down to bits 0..4 so
// the result is in 0..0x1f regardless of which channel. Replaces
// legacy XCOL1D/2D/3D.
static inline int32_t extractRightAligned(uint32_t c) { return (c >> Shift) & 0x1f; }
static inline int32_t modulateNative(uint32_t c, int32_t factor) { return (extractNative(c) * factor) >> 7; }
static inline int32_t modulateRightAligned(uint32_t c, int32_t factor) {
return (extractRightAligned(c) * factor) >> 7;
}
static inline int32_t modulateQuarterNative(uint32_t c, int32_t factor) {
return ((extractNative(c) >> 2) * factor) >> 7;
}
};
using R = Trait<0x1f, 0x7fffffe0, 0>;
using B = Trait<0x3e0, 0x7ffffc00, 5>;
using G = Trait<0x7c00, 0x7fff8000, 10>;
// Pack three native-position channels into a BGR555 scalar pixel,
// masking each to its own range. Replaces the legacy XPSXCOL macro.
// Argument order matches PackedPair555::packBGR (r, b, g) - the macro
// took (r, g, b), so migrations swap the last two arguments.
inline uint16_t packBGRMasked(int32_t r, int32_t b, int32_t g) {
return static_cast<uint16_t>((g & 0x7c00) | (b & 0x3e0) | (r & 0x1f));
}
// Convert a GP0 command color word to the software renderer's 15-bit pixel
// layout. The high/middle/low command bytes land in the high/middle/low 5-bit
// pixel fields, matching the legacy soft GPU packing convention.
inline constexpr uint16_t fromCommandColor(uint32_t rgb) {
return static_cast<uint16_t>(((rgb & 0x00f80000) >> 9) | ((rgb & 0x0000f800) >> 6) | ((rgb & 0x000000f8) >> 3));
}
// Pack high-aligned interpolated channels used by the polygon/line edge
// walkers. R/G/B are kept in the historical 8.16-ish positions so the shifts
// below are part of the rasterizer's bit-exact contract.
inline constexpr uint16_t fromHighAlignedRGB(int32_t r, int32_t g, int32_t b) {
return static_cast<uint16_t>(((r >> 9) & 0x7c00) | ((g >> 14) & 0x03e0) | ((b >> 19) & 0x001f));
}
inline constexpr uint32_t fromHighAlignedRGBPair(int32_t r, int32_t g, int32_t b, int32_t dr, int32_t dg, int32_t db) {
return static_cast<uint32_t>(fromHighAlignedRGB(r, g, b)) |
(static_cast<uint32_t>(fromHighAlignedRGB(r + dr, g + dg, b + db)) << 16);
}
} // namespace Channel555
// Per-channel hardware blend operations for the four PSX ABR modes.
//
// Each helper expresses one of the GPU's semi-transparent blend
// functions for a single colour channel; the writer call-sites stamp
// the same expression out across R, B, G with channel-specific masks
// and shifts. Modulation is the caller's job (the F argument should
// already be modulated when the source path is textured), so this
// layer doesn't need to know whether the modulation came from
// rs.m1/m2/m3 or per-call m1/m2/m3 or wasn't applied at all.
//
// Two representations are in use:
// - "right-aligned": both B and F are 5-bit values sitting in bits
// 0..4 of an int32_t. F may carry one or two extra bits from the
// modulation product (post `>> 7`, peak around 62 for native or
// 124 for the sum of a paired channel). The helper result stays
// at the same alignment and the caller shifts it back to its
// native position.
// - "native-position": B and F sit at the channel's own bit range
// (R: 0..4, B: 5..9, G: 10..14). The helper result stays at the
// same range and the caller follows with `Channel555::*::
// saturateNative` for the overflow clamp.
//
// The runtime dispatch on rs.abr stays inside the writer specialisations;
// these helpers consolidate the blend math without templating the ABR
// axis. Templating ABR at primitive entry is plausible (the rasterizer
// stamps thousands of pixels under a single rs.abr value) but expands
// the code-size matrix to ABR x TexMode x WriteMode x Shading, which
// is a separate architectural decision left for later.
namespace BlendOp {
// HalfBackAndHalfFront: (B + F) / 2 per channel, with the bit-0 carry
// preserved. The historical ((B|F) & 0x7bde) >> 1 shortcut dropped
// that carry; hardware keeps it (SCPH-5501 via gpu-raster-phase12
// abr0_tri_b31_f31). Both inputs right-aligned in bits 0..6; result
// right-aligned in bits 0..5 (caller shifts to native).
inline int32_t halfBackHalfFront(int32_t B, int32_t F) { return (B + F) >> 1; }
// FullBackAndFullFront: B + F per channel; saturates at the channel
// max via the caller's Channel555 trait. Both inputs at native
// position; result lives at the same position and may overflow into
// the channel's overflow region.
inline int32_t fullBackFullFront(int32_t B, int32_t F) { return B + F; }
// FullBackSubFullFront: B - F per channel, clamped to zero on
// underflow. Both inputs at native position; result stays at native
// position with the sign-flip clamp folded in.
inline int32_t fullBackSubFullFront(int32_t B, int32_t F) {
int32_t v = B - F;
if (v & 0x80000000) v = 0;
return v;
}
// HalfBackAndQuarter: B + F/4 per channel. The caller pre-shifts F
// (either as a >> 2 of the raw channel or as a modulation against
// the bits-0..1-cleared extract from PackedPair555::extract*ForQuarter)
// so this layer is a plain addition. Both inputs at native position;
// shares the saturation contract with fullBackFullFront.
inline int32_t halfBackQuarter(int32_t B, int32_t Fquarter) { return B + Fquarter; }
// Packed-pair variant of fullBackSubFullFront. B_pair carries one
// channel's destination value in each halfword at its native bit
// range (e.g. R as `*pdest & 0x001f001f`). F_pair carries the same
// channel post-modulation with the per-halfword overflow bits 5..6
// still set (e.g. the `& 0xff80ff80) >> 7` output for R). The two
// halves are subtracted independently and each clamped to zero on
// underflow; result is the merged pair ready for the next channel
// or the BGR pack. Used in the textured packed semi-transparent
// path where the post-modulation F has both halves populated.
inline int32_t packedFullBackSubFullFront(int32_t B_pair, int32_t F_pair) {
int32_t hi = (B_pair & 0x001f0000) - (F_pair & 0x003f0000);
if (hi & 0x80000000) hi = 0;
int32_t lo = (B_pair & 0x0000001f) - (F_pair & 0x0000003f);
if (lo & 0x80000000) lo = 0;
return lo | hi;
}
} // namespace BlendOp
// Per-pixel writer policy. Each specialization owns both a scalar (one
// 16-bit pixel) and a packed (a 32-bit pair, low halfword first) entry
// point. The packed entry must produce bit-equivalent output to two
// successive scalar entries against the same destination, modulo the
// existing fast-path optimizations the legacy code already encodes.
//
// Specializations exist only for the (Textured, Shading, WriteMode)
// triples that the rasterizer currently exercises. Adding a new one is
// the migration path for the next legacy helper.
template <bool Textured, GPU::Shading Shading, WriteMode WM>
struct PixelWriter;
// Textured, flat-shaded, Solid (!checkMask && !drawSemiTrans).
//
// Matches the legacy `getTextureTransColShadeSolid` (scalar) and
// `getTextureTransColShade32Solid` (pair) member helpers bit for bit.
// Modulation::Off folds in via the neutral 128 coefficients in
// RasterState.m1/m2/m3 (same multiply path as Modulation::On).
template <>
struct PixelWriter<true, GPU::Shading::Flat, WriteMode::Solid> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color) {
if (color == 0) return;
uint16_t *pdest = rs.pixel16(x, y);
const uint16_t l = rs.setMask16 | (color & 0x8000);
int32_t r = Channel555::R::modulateNative(color, rs.m1);
int32_t b = Channel555::B::modulateNative(color, rs.m2);
int32_t g = Channel555::G::modulateNative(color, rs.m3);
r = Channel555::R::saturateNative(r);
b = Channel555::B::saturateNative(b);
g = Channel555::G::saturateNative(g);
*pdest = Channel555::packBGRMasked(r, b, g) | l;
}
static inline void packed(const RasterState &rs, int x, int y, uint32_t color) {
if (color == 0) return;
uint32_t *pdest = rs.pixelPair32(x, y);
// Channels are extracted in packed-pair form: bits 0..4 of each
// halfword for red, 5..9 for blue, 10..14 for green. Multiplying
// by m1/m2/m3 (0..255) keeps both halves of the int32 valid as
// long as we mask post-multiply with 0xff80ff80 before the >> 7
// (otherwise the high half's overflow would bleed into the low
// half's saturation check).
int32_t r = PackedPair555::modulateR(color, rs.m1);
int32_t b = PackedPair555::modulateB(color, rs.m2);
int32_t g = PackedPair555::modulateG(color, rs.m3);
r = PackedPair555::saturate5Pair(r);
b = PackedPair555::saturate5Pair(b);
g = PackedPair555::saturate5Pair(g);
const uint32_t flags = rs.setMask32 | (color & 0x80008000);
const uint32_t packed_rgb = PackedPair555::packBGR(r, b, g);
// Zero-texel-half preservation: an entirely-zero halfword of the
// source color means "transparent texel", and the destination
// halfword keeps its prior value.
if ((color & 0xffff) == 0) {
*pdest = PackedPair555::preserveLowHalf(*pdest, packed_rgb | flags);
return;
}
if ((color & 0xffff0000) == 0) {
*pdest = PackedPair555::preserveHighHalf(*pdest, packed_rgb | flags);
return;
}
*pdest = packed_rgb | flags;
}
};
// Textured, flat-shaded, Default (runtime checkMask + drawSemiTrans).
//
// Matches the legacy `getTextureTransColShade` (scalar) and
// `getTextureTransColShade32` (pair) member helpers bit for bit.
// Reads checkMask, drawSemiTrans, and abr from RasterState at runtime;
// handles all four BlendFunction cases (HalfBackAndHalfFront,
// FullBackAndFullFront, FullBackSubFullFront, HalfBackAndQuarter).
//
// Semantic differences vs Solid:
// - scalar honors checkMask via an early return when the destination
// mask bit is already set
// - both entry points blend with the destination color when the
// source mask bit is set and drawSemiTrans is true
// - packed handles checkMask after the math, with per-halfword
// destination preservation when the destination's mask bit is set
// or the corresponding source halfword is zero
template <>
struct PixelWriter<true, GPU::Shading::Flat, WriteMode::Default> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color) {
if (color == 0) return;
uint16_t *pdest = rs.pixel16(x, y);
if (rs.checkMask && *pdest & 0x8000) return;
const uint16_t l = rs.setMask16 | (color & 0x8000);
int32_t r, g, b;
if (rs.drawSemiTrans && (color & 0x8000)) {
if (rs.abr == GPU::BlendFunction::HalfBackAndHalfFront) {
const int32_t Br = Channel555::R::extractNative(*pdest);
const int32_t Bb = (*pdest >> 5) & 0x1f;
const int32_t Bg = (*pdest >> 10) & 0x1f;
const int32_t Fr = Channel555::R::modulateNative(color, rs.m1);
const int32_t Fb = Channel555::B::modulateRightAligned(color, rs.m2);
const int32_t Fg = Channel555::G::modulateRightAligned(color, rs.m3);
r = BlendOp::halfBackHalfFront(Br, Fr);
b = BlendOp::halfBackHalfFront(Bb, Fb) << 5;
g = BlendOp::halfBackHalfFront(Bg, Fg) << 10;
} else if (rs.abr == GPU::BlendFunction::FullBackAndFullFront) {
r = BlendOp::fullBackFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::modulateNative(color, rs.m1));
b = BlendOp::fullBackFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::modulateNative(color, rs.m2));
g = BlendOp::fullBackFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::modulateNative(color, rs.m3));
} else if (rs.abr == GPU::BlendFunction::FullBackSubFullFront) {
r = BlendOp::fullBackSubFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::modulateNative(color, rs.m1));
b = BlendOp::fullBackSubFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::modulateNative(color, rs.m2));
g = BlendOp::fullBackSubFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::modulateNative(color, rs.m3));
} else {
r = BlendOp::halfBackQuarter(Channel555::R::extractNative(*pdest),
Channel555::R::modulateQuarterNative(color, rs.m1));
b = BlendOp::halfBackQuarter(Channel555::B::extractNative(*pdest),
Channel555::B::modulateQuarterNative(color, rs.m2));
g = BlendOp::halfBackQuarter(Channel555::G::extractNative(*pdest),
Channel555::G::modulateQuarterNative(color, rs.m3));
}
} else {
r = Channel555::R::modulateNative(color, rs.m1);
b = Channel555::B::modulateNative(color, rs.m2);
g = Channel555::G::modulateNative(color, rs.m3);
}
r = Channel555::R::saturateNative(r);
b = Channel555::B::saturateNative(b);
g = Channel555::G::saturateNative(g);
*pdest = Channel555::packBGRMasked(r, b, g) | l;
}
static inline void packed(const RasterState &rs, int x, int y, uint32_t color) {
if (color == 0) return;
uint32_t *pdest = rs.pixelPair32(x, y);
const uint32_t l = rs.setMask32 | (color & 0x80008000);
int32_t r, g, b;
if (rs.drawSemiTrans && (color & 0x80008000)) {
if (rs.abr == GPU::BlendFunction::HalfBackAndHalfFront) {
// Each channel's destination contribution gets shifted to
// bits 7..11 of each halfword via alignXForHalfBlend so the
// modulated source's bit-7 carry survives the post-mask.
r = ((PackedPair555::alignRForHalfBlend(*pdest) + PackedPair555::scaledRForHalfBlend(color, rs.m1)) &
0xff00ff00) >>
8;
b = ((PackedPair555::alignBForHalfBlend(*pdest) + PackedPair555::scaledBForHalfBlend(color, rs.m2)) &
0xff00ff00) >>
8;
g = ((PackedPair555::alignGForHalfBlend(*pdest) + PackedPair555::scaledGForHalfBlend(color, rs.m3)) &
0xff00ff00) >>
8;
} else if (rs.abr == GPU::BlendFunction::FullBackAndFullFront) {
r = (*pdest & 0x001f001f) + (PackedPair555::modulateR(color, rs.m1));
b = ((*pdest >> 5) & 0x001f001f) + (PackedPair555::modulateB(color, rs.m2));
g = ((*pdest >> 10) & 0x001f001f) + (PackedPair555::modulateG(color, rs.m3));
} else if (rs.abr == GPU::BlendFunction::FullBackSubFullFront) {
r = BlendOp::packedFullBackSubFullFront(*pdest & 0x001f001f, PackedPair555::modulateR(color, rs.m1));
b = BlendOp::packedFullBackSubFullFront((*pdest >> 5) & 0x001f001f,
PackedPair555::modulateB(color, rs.m2));
g = BlendOp::packedFullBackSubFullFront((*pdest >> 10) & 0x001f001f,
PackedPair555::modulateG(color, rs.m3));
} else {
// HalfBackAndQuarter: X32BCOL1(color) is color & 0x001c001c
// (drops bits 0-1 so >> 2 stays lossless).
r = (*pdest & 0x001f001f) + PackedPair555::modulateRForQuarter(color, rs.m1);
b = ((*pdest >> 5) & 0x001f001f) + PackedPair555::modulateBForQuarter(color, rs.m2);
g = ((*pdest >> 10) & 0x001f001f) + PackedPair555::modulateGForQuarter(color, rs.m3);
}
// If only one half of the source has its mask bit set, the other
// half still needs to be multiplied as if it were a non-blended
// texel. Patch the corresponding halfword of r/b/g afterwards.
if (!(color & 0x8000)) {
r = PackedPair555::preserveHighHalf(r, PackedPair555::modulateRLowHalf(color, rs.m1));
b = PackedPair555::preserveHighHalf(b, PackedPair555::modulateBLowHalf(color, rs.m2));
g = PackedPair555::preserveHighHalf(g, PackedPair555::modulateGLowHalf(color, rs.m3));
}
if (!(color & 0x80000000)) {
r = PackedPair555::preserveLowHalf(r, PackedPair555::modulateRHighHalf(color, rs.m1));
b = PackedPair555::preserveLowHalf(b, PackedPair555::modulateBHighHalf(color, rs.m2));
g = PackedPair555::preserveLowHalf(g, PackedPair555::modulateGHighHalf(color, rs.m3));
}
} else {
r = PackedPair555::modulateR(color, rs.m1);
b = PackedPair555::modulateB(color, rs.m2);
g = PackedPair555::modulateG(color, rs.m3);
}
r = PackedPair555::saturate5Pair(r);
b = PackedPair555::saturate5Pair(b);
g = PackedPair555::saturate5Pair(g);
const uint32_t packed_rgb = PackedPair555::packBGR(r, b, g);
if (rs.checkMask) {
const uint32_t ma = *pdest;
*pdest = packed_rgb | l;
if ((color & 0xffff) == 0) *pdest = PackedPair555::preserveHighHalf(*pdest, ma);
if ((color & 0xffff0000) == 0) *pdest = PackedPair555::preserveLowHalf(*pdest, ma);
if (ma & 0x80000000) *pdest = PackedPair555::preserveLowHalf(*pdest, ma);
if (ma & 0x00008000) *pdest = PackedPair555::preserveHighHalf(*pdest, ma);
return;
}
if ((color & 0xffff) == 0) {
*pdest = PackedPair555::preserveLowHalf(*pdest, packed_rgb | l);
return;
}
if ((color & 0xffff0000) == 0) {
*pdest = PackedPair555::preserveHighHalf(*pdest, packed_rgb | l);
return;
}
*pdest = packed_rgb | l;
}
};
// Textured, gouraud-shaded, Solid (!checkMask && !drawSemiTrans && !ditherMode).
//
// Matches the legacy `getTextureTransColShadeXSolid` (scalar) and
// `getTextureTransColShadeX32Solid` (pair) member helpers bit for bit.
// The "X" suffix in the legacy names is the family marker for "modulation
// passed per-call as int16_t m1/m2/m3 args" rather than from
// RasterState.m1/m2/m3 - that's what makes this the Gouraud specialization.
//
// The packed entry takes m1/m2/m3 as int16_t parameters; callers pack the
// gouraud-interpolated integer parts of two consecutive pixels via
// `(c >> 16) | ((c + dif) & 0xff0000)`. Sign-extension during the int16_t
// promotion drops the high half, so both pixels of a packed pair end up
// using the first pixel's modulation - a documented PS1-emulation
// approximation also present in the legacy helpers.
template <>
struct PixelWriter<true, GPU::Shading::Gouraud, WriteMode::Solid> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color, int16_t m1, int16_t m2, int16_t m3) {
if (color == 0) return;
uint16_t *pdest = rs.pixel16(x, y);
int32_t r = Channel555::R::modulateNative(color, m1);
int32_t b = Channel555::B::modulateNative(color, m2);
int32_t g = Channel555::G::modulateNative(color, m3);
r = Channel555::R::saturateNative(r);
b = Channel555::B::saturateNative(b);
g = Channel555::G::saturateNative(g);
*pdest = Channel555::packBGRMasked(r, b, g) | rs.setMask16 | (color & 0x8000);
}
static inline void packed(const RasterState &rs, int x, int y, uint32_t color, int16_t m1, int16_t m2, int16_t m3) {
if (color == 0) return;
uint32_t *pdest = rs.pixelPair32(x, y);
int32_t r = PackedPair555::modulateR(color, m1);
int32_t b = PackedPair555::modulateB(color, m2);
int32_t g = PackedPair555::modulateG(color, m3);
r = PackedPair555::saturate5Pair(r);
b = PackedPair555::saturate5Pair(b);
g = PackedPair555::saturate5Pair(g);
const uint32_t flags = rs.setMask32 | (color & 0x80008000);
const uint32_t packed_rgb = PackedPair555::packBGR(r, b, g);
if ((color & 0xffff) == 0) {
*pdest = PackedPair555::preserveLowHalf(*pdest, packed_rgb | flags);
return;
}
if ((color & 0xffff0000) == 0) {
*pdest = PackedPair555::preserveHighHalf(*pdest, packed_rgb | flags);
return;
}
*pdest = packed_rgb | flags;
}
};
// Textured, gouraud-shaded, Default (runtime checkMask + drawSemiTrans, no
// dither). Matches the legacy `getTextureTransColShadeX` (scalar) member
// helper bit for bit. Only a scalar entry point is provided because the
// gouraud slow path iterates one pixel at a time (color interpolation
// changes per pixel, the packed-pair fast path is unavailable for it).
//
// The dithered variant (matching `getTextureTransColShadeXDither`) lives in
// a separate Dither specialization because it operates on a different
// channel representation (right-aligned 8-bit channels via XCOL1D/2D/3D vs
// the native-position channels here) and dispatches through
// applyDither/applyDitherCached for the final write.
template <>
struct PixelWriter<true, GPU::Shading::Gouraud, WriteMode::Default> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color, int16_t m1, int16_t m2, int16_t m3) {
if (color == 0) return;
uint16_t *pdest = rs.pixel16(x, y);
if (rs.checkMask && *pdest & 0x8000) return;
const uint16_t l = rs.setMask16 | (color & 0x8000);
int32_t r, g, b;
if (rs.drawSemiTrans && (color & 0x8000)) {
if (rs.abr == GPU::BlendFunction::HalfBackAndHalfFront) {
const int32_t Br = Channel555::R::extractNative(*pdest);
const int32_t Bb = (*pdest >> 5) & 0x1f;
const int32_t Bg = (*pdest >> 10) & 0x1f;
const int32_t Fr = Channel555::R::modulateNative(color, m1);
const int32_t Fb = Channel555::B::modulateRightAligned(color, m2);
const int32_t Fg = Channel555::G::modulateRightAligned(color, m3);
r = BlendOp::halfBackHalfFront(Br, Fr);
b = BlendOp::halfBackHalfFront(Bb, Fb) << 5;
g = BlendOp::halfBackHalfFront(Bg, Fg) << 10;
} else if (rs.abr == GPU::BlendFunction::FullBackAndFullFront) {
r = BlendOp::fullBackFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::modulateNative(color, m1));
b = BlendOp::fullBackFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::modulateNative(color, m2));
g = BlendOp::fullBackFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::modulateNative(color, m3));
} else if (rs.abr == GPU::BlendFunction::FullBackSubFullFront) {
r = BlendOp::fullBackSubFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::modulateNative(color, m1));
b = BlendOp::fullBackSubFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::modulateNative(color, m2));
g = BlendOp::fullBackSubFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::modulateNative(color, m3));
} else {
r = BlendOp::halfBackQuarter(Channel555::R::extractNative(*pdest),
Channel555::R::modulateQuarterNative(color, m1));
b = BlendOp::halfBackQuarter(Channel555::B::extractNative(*pdest),
Channel555::B::modulateQuarterNative(color, m2));
g = BlendOp::halfBackQuarter(Channel555::G::extractNative(*pdest),
Channel555::G::modulateQuarterNative(color, m3));
}
} else {
r = Channel555::R::modulateNative(color, m1);
b = Channel555::B::modulateNative(color, m2);
g = Channel555::G::modulateNative(color, m3);
}
r = Channel555::R::saturateNative(r);
b = Channel555::B::saturateNative(b);
g = Channel555::G::saturateNative(g);
*pdest = Channel555::packBGRMasked(r, b, g) | l;
}
};
// Untextured, Solid (!checkMask && !drawSemiTrans).
//
// The untextured fast paths already compute final BGR555 colors before
// calling the writer: flat primitives pass the primitive color, and gouraud
// primitives pass the interpolated color. Solid mode only ORs in the mask
// write bit and stores the scalar or packed pair.
template <GPU::Shading Shading>
struct PixelWriter<false, Shading, WriteMode::Solid> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color) {
*rs.pixel16(x, y) = color | rs.setMask16;
}
static inline void packed(const RasterState &rs, int x, int y, uint32_t color) {
*rs.pixelPair32(x, y) = color | rs.setMask32;
}
};
// Untextured, flat-shaded, Default (runtime checkMask + drawSemiTrans).
//
// Matches the legacy `getShadeTransCol` (scalar) and `getShadeTransCol32`
// (pair) member helpers bit for bit. Used by the untextured paths -
// drawPoly3F (flat triangle, slow path) and the line / fill / sprite
// helpers - where there is no texture sampler and `color` is the
// primitive's solid color, optionally blended with the destination on
// drawSemiTrans.
//
// No Modulation here: untextured writes the color through unchanged
// (modulation::On for an untextured primitive in the legacy code path
// is a no-op; the primitive's color word IS the final color). No
// modulation factors are consulted; the channel multiplies that the
// textured writers do are absent.
template <>
struct PixelWriter<false, GPU::Shading::Flat, WriteMode::Default> {
static inline void scalar(const RasterState &rs, int x, int y, uint16_t color) {
uint16_t *pdest = rs.pixel16(x, y);
if (rs.checkMask && *pdest & 0x8000) return;
if (rs.drawSemiTrans) {
int32_t r, g, b;
if (rs.abr == GPU::BlendFunction::HalfBackAndHalfFront) {
const int32_t Br = Channel555::R::extractNative(*pdest);
const int32_t Bb = (*pdest >> 5) & 0x1f;
const int32_t Bg = (*pdest >> 10) & 0x1f;
const int32_t Fr = Channel555::R::extractNative(color);
const int32_t Fb = (color >> 5) & 0x1f;
const int32_t Fg = (color >> 10) & 0x1f;
r = BlendOp::halfBackHalfFront(Br, Fr);
b = BlendOp::halfBackHalfFront(Bb, Fb) << 5;
g = BlendOp::halfBackHalfFront(Bg, Fg) << 10;
} else if (rs.abr == GPU::BlendFunction::FullBackAndFullFront) {
r = BlendOp::fullBackFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::extractNative(color));
b = BlendOp::fullBackFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::extractNative(color));
g = BlendOp::fullBackFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::extractNative(color));
} else if (rs.abr == GPU::BlendFunction::FullBackSubFullFront) {
r = BlendOp::fullBackSubFullFront(Channel555::R::extractNative(*pdest),
Channel555::R::extractNative(color));
b = BlendOp::fullBackSubFullFront(Channel555::B::extractNative(*pdest),
Channel555::B::extractNative(color));
g = BlendOp::fullBackSubFullFront(Channel555::G::extractNative(*pdest),
Channel555::G::extractNative(color));
} else {
r = BlendOp::halfBackQuarter(Channel555::R::extractNative(*pdest),
(Channel555::R::extractNative(color)) >> 2);
b = BlendOp::halfBackQuarter(Channel555::B::extractNative(*pdest),
(Channel555::B::extractNative(color)) >> 2);
g = BlendOp::halfBackQuarter(Channel555::G::extractNative(*pdest),
(Channel555::G::extractNative(color)) >> 2);
}
r = Channel555::R::saturateNative(r);
b = Channel555::B::saturateNative(b);
g = Channel555::G::saturateNative(g);
*pdest = Channel555::packBGRMasked(r, b, g) | rs.setMask16;
} else {
*pdest = color | rs.setMask16;
}
}
static inline void packed(const RasterState &rs, int x, int y, uint32_t color) {
uint32_t *pdest = rs.pixelPair32(x, y);
if (rs.drawSemiTrans) {
int32_t r, g, b;
if (rs.abr == GPU::BlendFunction::HalfBackAndHalfFront) {
// Per-channel (B + F) / 2 across the two packed pixels.
// Replaces the legacy `((p|color) & 0x7bde7bde) >> 1`
// shortcut, which dropped each channel's bit-0 carry.
// Hardware preserves it (phase-12 abr0_tri_b31_f31).
const uint32_t Br = PackedPair555::extractR(*pdest);
const uint32_t Bb = PackedPair555::extractB(*pdest);
const uint32_t Bg = PackedPair555::extractG(*pdest);
const uint32_t Fr = PackedPair555::extractR(color);
const uint32_t Fb = PackedPair555::extractB(color);
const uint32_t Fg = PackedPair555::extractG(color);
if (!rs.checkMask) {
const uint32_t hr = ((Br + Fr) >> 1) & 0x001f001f;
const uint32_t hb = ((Bb + Fb) >> 1) & 0x001f001f;
const uint32_t hg = ((Bg + Fg) >> 1) & 0x001f001f;
*pdest = PackedPair555::packBGR(hr, hb, hg) | rs.setMask32;
return;
}
r = ((Br + Fr) >> 1) & 0x001f001f;
b = ((Bb + Fb) >> 1) & 0x001f001f;
g = ((Bg + Fg) >> 1) & 0x001f001f;
} else if (rs.abr == GPU::BlendFunction::FullBackAndFullFront) {
r = PackedPair555::extractR(*pdest) + PackedPair555::extractR(color);
b = PackedPair555::extractB(*pdest) + PackedPair555::extractB(color);
g = PackedPair555::extractG(*pdest) + PackedPair555::extractG(color);
} else if (rs.abr == GPU::BlendFunction::FullBackSubFullFront) {
int32_t sr, sb, sg, src, sbc, sgc, c;
src = Channel555::R::extractNative(color);
sbc = Channel555::B::extractNative(color);
sgc = Channel555::G::extractNative(color);
c = (*pdest) >> 16;
sr = (Channel555::R::extractNative(c)) - src;
if (sr & 0x8000) sr = 0;
sb = (Channel555::B::extractNative(c)) - sbc;
if (sb & 0x8000) sb = 0;
sg = (Channel555::G::extractNative(c)) - sgc;
if (sg & 0x8000) sg = 0;
r = ((int32_t)sr) << 16;
b = ((int32_t)sb) << 11;
g = ((int32_t)sg) << 6;
c = (*pdest) & 0xffff;
sr = (Channel555::R::extractNative(c)) - src;
if (sr & 0x8000) sr = 0;
sb = (Channel555::B::extractNative(c)) - sbc;
if (sb & 0x8000) sb = 0;
sg = (Channel555::G::extractNative(c)) - sgc;
if (sg & 0x8000) sg = 0;
r |= sr;
b |= sb >> 5;
g |= sg >> 10;
} else {
// HalfBackAndQuarter: X32BCOL1 = (x & 0x001c001c). Drops bits 0-1 so >> 2 stays lossless.
r = PackedPair555::extractR(*pdest) + (PackedPair555::extractRForQuarter(color) >> 2);
b = PackedPair555::extractB(*pdest) + (PackedPair555::extractBForQuarter(color) >> 2);
g = PackedPair555::extractG(*pdest) + (PackedPair555::extractGForQuarter(color) >> 2);
}
r = PackedPair555::saturate5Pair(r);
b = PackedPair555::saturate5Pair(b);
g = PackedPair555::saturate5Pair(g);
const uint32_t packed_rgb = PackedPair555::packBGR(r, b, g);
if (rs.checkMask) {
const uint32_t ma = *pdest;
*pdest = packed_rgb | rs.setMask32;
if (ma & 0x80000000) *pdest = PackedPair555::preserveLowHalf(*pdest, ma);
if (ma & 0x00008000) *pdest = PackedPair555::preserveHighHalf(*pdest, ma);
return;
}
*pdest = packed_rgb | rs.setMask32;
} else {
if (rs.checkMask) {
const uint32_t ma = *pdest;
*pdest = color | rs.setMask32;
if (ma & 0x80000000) *pdest = PackedPair555::preserveLowHalf(*pdest, ma);
if (ma & 0x00008000) *pdest = PackedPair555::preserveHighHalf(*pdest, ma);
return;
}
*pdest = color | rs.setMask32;
}
}
};
} // namespace SoftGPU
} // namespace PCSX

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