mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-22 11:56:58 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b9cba457e | |||
| 3dc793d859 | |||
| 01d78b71fe | |||
| 4d79f19089 | |||
| 8fe4765f9e | |||
| c71bb9fc9e | |||
| f60f80b631 | |||
| 31ea0081a9 | |||
| 7921269878 |
@@ -1,82 +0,0 @@
|
||||
name: Build KytyPS5 (Linux)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends --yes \
|
||||
clang \
|
||||
glslang-tools \
|
||||
libgl1-mesa-dev \
|
||||
libx11-dev \
|
||||
libxcursor-dev \
|
||||
libxext-dev \
|
||||
libxfixes-dev \
|
||||
libxi-dev \
|
||||
libxrandr-dev \
|
||||
libxss-dev \
|
||||
lld \
|
||||
ninja-build
|
||||
|
||||
- name: Install Qt 6.10.3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.10.3"
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
cache: true
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
git --version
|
||||
cmake --version
|
||||
ninja --version
|
||||
clang++ --version
|
||||
ld.lld --version
|
||||
glslangValidator --version
|
||||
echo "Qt6_DIR=$Qt6_DIR"
|
||||
|
||||
- name: Configure
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S src -B _Build/linux \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/linux --target launcher --parallel
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --install _Build/linux --prefix _Build/linux/install
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: KytyPS5-Linux
|
||||
path: _Build/linux/install/**
|
||||
if-no-files-found: error
|
||||
+293
-15
@@ -1,4 +1,4 @@
|
||||
name: Build KytyPS5
|
||||
name: Build and Release KytyPS5
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -7,7 +7,8 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
windows:
|
||||
name: Build KytyPS5 (Windows)
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
@@ -22,11 +23,31 @@ jobs:
|
||||
- name: Setup Ninja
|
||||
uses: seanmiddleditch/gha-setup-ninja@v5
|
||||
|
||||
- name: Install glslang
|
||||
- name: Locate vcpkg
|
||||
id: vcpkg
|
||||
shell: pwsh
|
||||
run: |
|
||||
$vcpkgRoot = Split-Path (Get-Command vcpkg).Source
|
||||
vcpkg install glslang[tools,opt]:x64-windows
|
||||
$portHash = (Get-FileHash "$vcpkgRoot\ports\glslang\vcpkg.json" -Algorithm SHA256).Hash
|
||||
"root=$vcpkgRoot" |
|
||||
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"port_hash=$($portHash.ToLowerInvariant())" |
|
||||
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
- name: Cache glslang
|
||||
id: glslang_cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.vcpkg.outputs.root }}\installed\x64-windows
|
||||
key: glslang-${{ runner.os }}-${{ runner.arch }}-${{ steps.vcpkg.outputs.port_hash }}-tools-opt
|
||||
|
||||
- name: Install glslang
|
||||
shell: pwsh
|
||||
run: |
|
||||
$vcpkgRoot = "${{ steps.vcpkg.outputs.root }}"
|
||||
if ("${{ steps.glslang_cache.outputs.cache-hit }}" -ne "true") {
|
||||
vcpkg install glslang[tools,opt]:x64-windows
|
||||
}
|
||||
"$vcpkgRoot\installed\x64-windows\tools\glslang" |
|
||||
Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
@@ -69,45 +90,302 @@ jobs:
|
||||
run: |
|
||||
cmake --install _Build/windows --prefix _Build/windows/install
|
||||
|
||||
- name: Upload Artifacts
|
||||
- name: Upload Windows artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: KytyPS5
|
||||
name: KytyPS5-Windows-x64
|
||||
path: _Build/windows/install/**
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
name: Build KytyPS5 (macOS)
|
||||
runs-on: macos-15
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Select Xcode 26
|
||||
shell: bash
|
||||
run: |
|
||||
xcode_path="$(ls -d /Applications/Xcode_26*.app | tail -n 1)"
|
||||
test -n "$xcode_path"
|
||||
sudo xcode-select --switch "$xcode_path/Contents/Developer"
|
||||
xcodebuild -version
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: brew install glslang ninja
|
||||
|
||||
- name: Install Qt 6.10.3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.10.3"
|
||||
host: mac
|
||||
target: desktop
|
||||
arch: clang_64
|
||||
cache: true
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
git --version
|
||||
cmake --version
|
||||
ninja --version
|
||||
clang++ --version
|
||||
glslangValidator --version
|
||||
echo "Host architecture: $(uname -m)"
|
||||
echo "QT_ROOT_DIR=$QT_ROOT_DIR"
|
||||
|
||||
- name: Configure
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S src -B _Build/macos \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_PREFIX_PATH="$QT_ROOT_DIR"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos --target launcher --parallel
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --install _Build/macos --prefix _Build/macos/install
|
||||
|
||||
- name: Bundle MoltenVK
|
||||
shell: bash
|
||||
env:
|
||||
MOLTENVK_VERSION: v1.4.2
|
||||
MOLTENVK_SHA256: f95765a6229cb7b915990a2890ce12ebe36a730b021545d3d52ae69ce4c4024e
|
||||
run: |
|
||||
archive="$RUNNER_TEMP/MoltenVK-macos.tar"
|
||||
package="$RUNNER_TEMP/MoltenVK"
|
||||
curl --fail --location --retry 3 \
|
||||
--output "$archive" \
|
||||
"https://github.com/KhronosGroup/MoltenVK/releases/download/$MOLTENVK_VERSION/MoltenVK-macos.tar"
|
||||
echo "$MOLTENVK_SHA256 $archive" | shasum -a 256 --check
|
||||
tar -xf "$archive" -C "$RUNNER_TEMP"
|
||||
install -m 755 \
|
||||
"$package/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" \
|
||||
_Build/macos/install/libMoltenVK.dylib
|
||||
install -m 644 "$package/LICENSE" _Build/macos/install/LICENSE.MoltenVK
|
||||
codesign --force --sign - --timestamp=none _Build/macos/install/libMoltenVK.dylib
|
||||
|
||||
- name: Verify artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
file _Build/macos/install/launcher
|
||||
file _Build/macos/install/kyty_emulator
|
||||
file _Build/macos/install/libMoltenVK.dylib
|
||||
lipo _Build/macos/install/launcher -verify_arch x86_64
|
||||
lipo _Build/macos/install/kyty_emulator -verify_arch x86_64
|
||||
lipo _Build/macos/install/libMoltenVK.dylib -verify_arch x86_64
|
||||
codesign --verify --strict _Build/macos/install/kyty_emulator
|
||||
codesign --verify --strict _Build/macos/install/libMoltenVK.dylib
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: KytyPS5-macOS-x86_64
|
||||
path: _Build/macos/install/**
|
||||
if-no-files-found: error
|
||||
|
||||
linux:
|
||||
name: Build KytyPS5 (Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends --yes \
|
||||
clang \
|
||||
glslang-tools \
|
||||
libasound2-dev \
|
||||
libdbus-1-dev \
|
||||
libgl1-mesa-dev \
|
||||
libpulse-dev \
|
||||
libudev-dev \
|
||||
libwayland-dev \
|
||||
libx11-dev \
|
||||
libxcursor-dev \
|
||||
libxext-dev \
|
||||
libxfixes-dev \
|
||||
libxi-dev \
|
||||
libxkbcommon-dev \
|
||||
libxrandr-dev \
|
||||
libxss-dev \
|
||||
lld \
|
||||
ninja-build \
|
||||
wayland-protocols
|
||||
|
||||
- name: Install Qt 6.10.3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.10.3"
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
cache: true
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
git --version
|
||||
cmake --version
|
||||
ninja --version
|
||||
clang++ --version
|
||||
ld.lld --version
|
||||
glslangValidator --version
|
||||
echo "Qt6_DIR=$Qt6_DIR"
|
||||
|
||||
- name: Configure
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p _Build
|
||||
cmake -S src -B _Build/linux \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR" 2>&1 | tee _Build/configure.log
|
||||
exit "${PIPESTATUS[0]}"
|
||||
|
||||
- name: Verify SDL2 backends
|
||||
shell: bash
|
||||
run: |
|
||||
status=0
|
||||
for feature in SDL_ALSA SDL_PULSEAUDIO SDL_WAYLAND SDL_X11 SDL_LIBUDEV SDL_DBUS; do
|
||||
if grep -qE "^-- ${feature} +\\(Wanted: ON\\): ON" _Build/configure.log; then
|
||||
echo "ok ${feature}"
|
||||
else
|
||||
echo "FAIL ${feature} is not enabled"
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(page_manager|memory_tracker)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --install _Build/linux --prefix _Build/linux/install
|
||||
|
||||
- name: Verify artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
file _Build/linux/install/launcher
|
||||
file _Build/linux/install/kyty_emulator
|
||||
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
|
||||
ldd _Build/linux/install/kyty_emulator > /dev/null
|
||||
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
while IFS= read -r -d '' binary; do
|
||||
while read -r dependency; do
|
||||
test -e "_Build/linux/install/lib/$dependency"
|
||||
done < <(
|
||||
readelf -d "$binary" |
|
||||
sed -n 's/.*Shared library: \[\(libQt6[^]]*\|libicu[^]]*\)\].*/\1/p'
|
||||
)
|
||||
done < <(
|
||||
find _Build/linux/install/launcher _Build/linux/install/plugins \
|
||||
-type f \( -name launcher -o -name '*.so' \) -print0
|
||||
)
|
||||
_Build/linux/install/kyty_emulator --help > /dev/null
|
||||
|
||||
- name: Upload Linux artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: KytyPS5-Linux-x86_64
|
||||
path: _Build/linux/install/**
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
name: Release KytyPS5
|
||||
if: github.event_name == 'push' && github.repository == 'KytyPS5/KytyPS5'
|
||||
needs: [windows, macos, linux]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download build
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: KytyPS5
|
||||
path: KytyPS5
|
||||
path: artifacts
|
||||
|
||||
- name: Set release name
|
||||
shell: bash
|
||||
run: |
|
||||
echo "RELEASE_NAME=KytyPS5-$(date -u +'%Y-%m-%d')-${GITHUB_SHA::7}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Package build
|
||||
- name: Package builds
|
||||
shell: bash
|
||||
run: zip -r "$RELEASE_NAME.zip" KytyPS5
|
||||
run: |
|
||||
windows_dir="artifacts/KytyPS5-Windows-x64"
|
||||
macos_dir="artifacts/KytyPS5-macOS-x86_64"
|
||||
linux_dir="artifacts/KytyPS5-Linux-x86_64"
|
||||
test -d "$windows_dir"
|
||||
test -d "$macos_dir"
|
||||
test -d "$linux_dir"
|
||||
chmod a+x \
|
||||
"$macos_dir/launcher" \
|
||||
"$macos_dir/kyty_emulator" \
|
||||
"$macos_dir/libMoltenVK.dylib" \
|
||||
"$linux_dir/launcher" \
|
||||
"$linux_dir/kyty_emulator"
|
||||
(
|
||||
cd "$windows_dir"
|
||||
zip -r "$GITHUB_WORKSPACE/$RELEASE_NAME-Windows-x64.zip" .
|
||||
)
|
||||
(
|
||||
cd "$macos_dir"
|
||||
zip -r "$GITHUB_WORKSPACE/$RELEASE_NAME-macOS-x86_64.zip" .
|
||||
)
|
||||
tar -C "$linux_dir" -czf \
|
||||
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
assets=(
|
||||
"$RELEASE_NAME-Windows-x64.zip"
|
||||
"$RELEASE_NAME-macOS-x86_64.zip"
|
||||
"$RELEASE_NAME-Linux-x86_64.tar.gz"
|
||||
)
|
||||
if gh release view "$RELEASE_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
|
||||
gh release upload "$RELEASE_NAME" "$RELEASE_NAME.zip" --clobber --repo "$GITHUB_REPOSITORY"
|
||||
gh release upload "$RELEASE_NAME" "${assets[@]}" \
|
||||
--clobber \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
else
|
||||
gh release create "$RELEASE_NAME" "$RELEASE_NAME.zip" \
|
||||
gh release create "$RELEASE_NAME" "${assets[@]}" \
|
||||
--generate-notes \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--target "$GITHUB_SHA" \
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@
|
||||
.vs/
|
||||
.idea/
|
||||
build/
|
||||
_Build/vscode-clang/
|
||||
_Build/vscode-clang/
|
||||
_Build/
|
||||
Vendored
+11
-1
@@ -44,7 +44,17 @@ set(SPDLOG_NO_EXCEPTIONS ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(spdlog EXCLUDE_FROM_ALL)
|
||||
|
||||
if (NOT TARGET FFmpeg::ffmpeg)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
# On macOS the target arch is driven by CMAKE_OSX_ARCHITECTURES, not the host
|
||||
# CMAKE_SYSTEM_PROCESSOR (which reports arm64 even for an x86_64/Rosetta build).
|
||||
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
if(CMAKE_OSX_ARCHITECTURES MATCHES "^(x86_64)$")
|
||||
set(ARCHITECTURE x86_64)
|
||||
elseif(CMAKE_OSX_ARCHITECTURES MATCHES "^(arm64)$")
|
||||
set(ARCHITECTURE arm64)
|
||||
else()
|
||||
set(ARCHITECTURE x86_64 arm64) # universal
|
||||
endif()
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(ARCHITECTURE arm64)
|
||||
else()
|
||||
set(ARCHITECTURE x86_64)
|
||||
|
||||
Vendored
+12
-3
@@ -5,7 +5,14 @@ set(cpuinfo_src
|
||||
deps/clog/src/clog.c
|
||||
)
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i[3-6]86|AMD64|x86(_64)?)$")
|
||||
# On Apple the target arch is driven by CMAKE_OSX_ARCHITECTURES, not the host
|
||||
# CMAKE_SYSTEM_PROCESSOR (which is arm64 even for an x86_64/Rosetta build).
|
||||
set(CPUINFO_TARGET_PROC "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(GET CMAKE_OSX_ARCHITECTURES 0 CPUINFO_TARGET_PROC)
|
||||
endif()
|
||||
|
||||
if(CPUINFO_TARGET_PROC MATCHES "^(i[3-6]86|AMD64|x86(_64)?)$")
|
||||
list(APPEND cpuinfo_src
|
||||
src/x86/init.c
|
||||
src/x86/info.c
|
||||
@@ -19,11 +26,13 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i[3-6]86|AMD64|x86(_64)?)$")
|
||||
src/x86/cache/deterministic.c)
|
||||
if(LINUX OR ANDROID)
|
||||
list(APPEND cpuinfo_src src/x86/linux/init.c src/x86/linux/cpuinfo.c)
|
||||
elseif(APPLE)
|
||||
list(APPEND cpuinfo_src src/x86/mach/init.c src/mach/topology.c)
|
||||
else()
|
||||
list(APPEND cpuinfo_src src/x86/windows/init.c)
|
||||
endif()
|
||||
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv[5-8].*|aarch64|arm64)$")
|
||||
|
||||
elseif(CPUINFO_TARGET_PROC MATCHES "^(armv[5-8].*|aarch64|arm64)$")
|
||||
list(APPEND cpuinfo_src src/arm/uarch.c src/arm/cache.c)
|
||||
|
||||
if(LINUX OR ANDROID)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# KytyPS5
|
||||
|
||||
[](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
|
||||
[](https://github.com/KytyPS5/KytyPS5/actions/workflows/build-linux.yml)
|
||||
[](#system-requirements)
|
||||
[](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
|
||||
[](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
|
||||
[](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
|
||||
[](#system-requirements)
|
||||
[](#current-status)
|
||||
[](LICENSE)
|
||||
|
||||
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows. It is based on
|
||||
a heavily modified version of [Kyty](https://github.com/InoriRus/Kyty). The project is in an early
|
||||
stage of development, so compatibility is limited and behavior may change significantly between
|
||||
builds.
|
||||
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows and Linux,
|
||||
with experimental macOS support. It is based on a heavily modified version of
|
||||
[Kyty](https://github.com/InoriRus/Kyty). The project is in an early stage of development, so
|
||||
compatibility is limited and behavior may change significantly between builds.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> KytyPS5 is not affiliated with Sony Interactive Entertainment or PlayStation. The project does
|
||||
@@ -23,7 +24,11 @@ KytyPS5 can boot 2D games and a selection of 3D games, including titles built wi
|
||||
|
||||
Development is focused on compatibility and boot reliability.
|
||||
|
||||
Linux support is planned, but Windows is the only supported platform at this time.
|
||||
Windows is the primary platform and receives the most testing. Linux builds and runs; see
|
||||
[Building on Linux](#building-on-linux).
|
||||
|
||||
macOS support is experimental. Compatibility with the same games on Windows and macOS has not yet
|
||||
been tested.
|
||||
|
||||
## Bugs and Issues
|
||||
|
||||
@@ -60,8 +65,9 @@ graphical glitches, low compatibility, and poor performance.
|
||||
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
|
||||
issues first, then use the **Game Emulation Bug Report** template and attach the complete log file.
|
||||
|
||||
Code contributions should be focused, build successfully on Windows, and include relevant tests
|
||||
where practical. Because KytyPS5 is still evolving quickly, consider opening an issue before
|
||||
Code contributions should be focused, build successfully on the platforms they touch, and include
|
||||
relevant tests where practical. Windows is the primary target, so a change that alters shared code
|
||||
should not regress it; changes confined to a platform's own code paths only need to build there. Because KytyPS5 is still evolving quickly, consider opening an issue before
|
||||
starting a large change.
|
||||
|
||||
### Formatting
|
||||
@@ -96,11 +102,11 @@ the Vulkan/SPIR-V validation rules.
|
||||
|
||||
### System requirements
|
||||
|
||||
- Windows 10 version 1803
|
||||
- Windows 10 version 1803, or a current Linux distribution
|
||||
- A 64-bit x86 processor
|
||||
- A Vulkan 1.3-capable GPU with current drivers
|
||||
|
||||
### Build requirements
|
||||
### Build requirements (Windows)
|
||||
|
||||
- Git
|
||||
- CMake 3.12 or newer
|
||||
@@ -134,11 +140,48 @@ cmake --install _Build/windows --prefix _Build/windows/install
|
||||
The finished application and its runtime dependencies will be placed in
|
||||
`_Build/windows/install`.
|
||||
|
||||
### Building on Linux
|
||||
|
||||
Install the toolchain and the libraries the bundled SDL2 needs. Without the audio, Wayland and
|
||||
udev development packages SDL2 quietly configures itself without those backends, and the resulting
|
||||
build has no working sound and no gamepad hotplug:
|
||||
|
||||
```bash
|
||||
sudo apt-get install --no-install-recommends \
|
||||
clang lld ninja-build cmake git glslang-tools \
|
||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxext-dev libxfixes-dev \
|
||||
libxi-dev libxrandr-dev libxss-dev libxkbcommon-dev \
|
||||
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
|
||||
```
|
||||
|
||||
Qt 6 (Concurrent, Network, Widgets) is also required — either the distribution packages
|
||||
(`qt6-base-dev`) or an official Qt installation.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
|
||||
cmake -S src -B _Build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR"
|
||||
|
||||
cmake --build _Build/linux --target launcher --parallel
|
||||
cmake --install _Build/linux --prefix _Build/linux/install
|
||||
```
|
||||
|
||||
The install step copies the Qt libraries and plugins next to the binaries, so
|
||||
`_Build/linux/install` runs without a matching system Qt.
|
||||
|
||||
As on Windows, the MSVC compiler is not used; Clang is required. `cl.exe` is rejected at configure
|
||||
time.
|
||||
|
||||
Note that the CMake source root is `src`, not the repository root.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
|
||||
Tools to build the project with Ninja and `clang-cl` and provides launch profiles for both
|
||||
`launcher.exe` and `kyty_emulator.exe`.
|
||||
`launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
compiler per platform, so on Linux configure from the command line as shown above.
|
||||
|
||||
Before using it:
|
||||
|
||||
@@ -160,6 +203,10 @@ To use the graphical launcher:
|
||||
.\_Build\windows\install\launcher.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
./_Build/linux/install/launcher
|
||||
```
|
||||
|
||||
On first launch, add one or more game folders in the global settings. The launcher searches those
|
||||
folders recursively for game directories containing `eboot.bin`. Select a detected game and run it
|
||||
from the game list.
|
||||
@@ -170,7 +217,11 @@ The emulator can also be started directly with a legally obtained game directory
|
||||
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame"
|
||||
```
|
||||
|
||||
Run `kyty_emulator.exe --help` to see the available graphics, logging, validation, profiling, and
|
||||
```bash
|
||||
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame"
|
||||
```
|
||||
|
||||
Run `kyty_emulator --help` to see the available graphics, logging, validation, profiling, and
|
||||
debugging options.
|
||||
|
||||
### AI Use
|
||||
|
||||
+64
-15
@@ -10,8 +10,8 @@ if(CMAKE_SYSTEM_NAME MATCHES ".*Linux")
|
||||
set(LINUX TRUE)
|
||||
endif()
|
||||
|
||||
if (NOT (WIN32 OR LINUX))
|
||||
message(FATAL_ERROR "only Windows and Linux builds are supported")
|
||||
if (NOT (WIN32 OR LINUX OR APPLE))
|
||||
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
@@ -22,6 +22,8 @@ set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
|
||||
include(utils.cmake)
|
||||
include(CTest)
|
||||
|
||||
option(KYTY_ENABLE_CLANG_TIDY "Run clang-tidy checks during builds" OFF)
|
||||
|
||||
set(KYTY_THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty")
|
||||
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
@@ -45,8 +47,9 @@ else()
|
||||
set(KYTY_BUILD KYTY_BUILD_RELEASE)
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
set(KYTY_PLATFORM KYTY_PLATFORM_LINUX)
|
||||
if(LINUX OR APPLE)
|
||||
# macOS rides the POSIX/Linux code paths until it gets a dedicated platform
|
||||
set(KYTY_PLATFORM KYTY_PLATFORM_LINUX)
|
||||
else()
|
||||
set(KYTY_PLATFORM KYTY_PLATFORM_WINDOWS)
|
||||
endif()
|
||||
@@ -63,6 +66,8 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^(Apple)?Clang$")
|
||||
if(WIN32 AND KYTY_CXX_COMPILER_NAME STREQUAL "clang-cl")
|
||||
set(KYTY_CLANG_CL TRUE)
|
||||
set(KYTY_LINKER LLD_LINK)
|
||||
elseif(APPLE)
|
||||
set(KYTY_LINKER LD64) # Apple's default linker; lld flags don't apply
|
||||
else()
|
||||
set(KYTY_LINKER LLD)
|
||||
endif()
|
||||
@@ -126,7 +131,7 @@ if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
|
||||
common
|
||||
#launcher
|
||||
)
|
||||
list(APPEND KYTY_CLANG_TYDY
|
||||
list(APPEND KYTY_CLANG_TIDY
|
||||
kyty_emulator
|
||||
#common
|
||||
launcher
|
||||
@@ -246,6 +251,26 @@ endif()
|
||||
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9)
|
||||
|
||||
# Linux system libraries required by the static FFmpeg archive.
|
||||
if(LINUX)
|
||||
find_package(Threads REQUIRED)
|
||||
list(APPEND kyty_emulator_link_libraries m ${CMAKE_DL_LIBS} Threads::Threads)
|
||||
|
||||
# Optional FFmpeg dependencies.
|
||||
find_package(ZLIB)
|
||||
if(ZLIB_FOUND)
|
||||
list(APPEND kyty_emulator_link_libraries ZLIB::ZLIB)
|
||||
endif()
|
||||
find_library(KYTY_BZ2_LIBRARY bz2)
|
||||
if(KYTY_BZ2_LIBRARY)
|
||||
list(APPEND kyty_emulator_link_libraries ${KYTY_BZ2_LIBRARY})
|
||||
endif()
|
||||
find_library(KYTY_LZMA_LIBRARY lzma)
|
||||
if(KYTY_LZMA_LIBRARY)
|
||||
list(APPEND kyty_emulator_link_libraries ${KYTY_LZMA_LIBRARY})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(inc_headers
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${KYTY_THIRD_PARTY_DIR}/SDL2/include
|
||||
@@ -389,6 +414,14 @@ add_kyty_full_emulator_test(virtual_memory_allocation_tests ../tests/VirtualMemo
|
||||
target_compile_definitions(virtual_memory_allocation_tests PRIVATE
|
||||
KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS=1)
|
||||
|
||||
# These tests use exceptions.
|
||||
if(NOT KYTY_CLANG_CL)
|
||||
foreach(kyty_exception_test scalar_provenance_tests resource_tracking_tests
|
||||
virtual_memory_allocation_tests)
|
||||
target_compile_options(${kyty_exception_test} PRIVATE -fexceptions)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
|
||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
|
||||
@@ -405,23 +438,25 @@ if(BUILD_TESTING)
|
||||
add_test(NAME gpu_tiler
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-tiler-only)
|
||||
|
||||
add_test(NAME texture_cache_layered_image
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --layered-image-only)
|
||||
add_test(NAME texture_cache_image_views
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-view-cache-only)
|
||||
add_test(NAME texture_cache_storage_sampled
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --storage-sampled-only)
|
||||
add_test(NAME texture_cache_depth_readback
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --depth-readback-only)
|
||||
add_test(NAME buffer_cache_dirty_gc
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-gc-only)
|
||||
|
||||
if(WIN32)
|
||||
# These tests still depend on the Windows multisample-depth path.
|
||||
add_test(NAME texture_cache_image_overlap
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
|
||||
add_test(NAME texture_cache_htile_clear
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
|
||||
add_test(NAME texture_cache_layered_image
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --layered-image-only)
|
||||
add_test(NAME texture_cache_image_views
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-view-cache-only)
|
||||
add_test(NAME texture_cache_storage_sampled
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --storage-sampled-only)
|
||||
add_test(NAME texture_cache_depth_readback
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --depth-readback-only)
|
||||
add_test(NAME buffer_cache_ranges
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
|
||||
add_test(NAME buffer_cache_dirty_gc
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-gc-only)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -441,6 +476,8 @@ endif()
|
||||
if (CLANG AND NOT KYTY_CLANG_CL)
|
||||
target_link_libraries(kyty_emulator pthread)
|
||||
endif()
|
||||
# dlopen/dlsym/dladdr for RenderDoc.
|
||||
target_link_libraries(kyty_emulator ${CMAKE_DL_LIBS})
|
||||
target_include_directories(kyty_emulator PRIVATE ${inc_headers})
|
||||
|
||||
clang_tidy_check(kyty_emulator "" "${check_headers}" "${inc_headers}")
|
||||
@@ -461,6 +498,18 @@ endif()
|
||||
|
||||
add_dependencies(kyty_emulator KytyGitVersion)
|
||||
|
||||
if(APPLE)
|
||||
# The emulator writes x86-64 trampolines/PLT stubs into memory and executes them, and
|
||||
# runs guest code under Rosetta. That requires the JIT / unsigned-executable-memory
|
||||
# entitlements, so re-sign the binary after every link (an unsigned/relinked binary
|
||||
# reverts to the hardened defaults and aborts when it first executes written code).
|
||||
add_custom_command(TARGET kyty_emulator POST_BUILD
|
||||
COMMAND codesign -s - --force --options runtime
|
||||
--entitlements "${CMAKE_CURRENT_SOURCE_DIR}/macos_jit.entitlements"
|
||||
$<TARGET_FILE:kyty_emulator>
|
||||
COMMENT "Codesign kyty_emulator with JIT entitlements (macOS)")
|
||||
endif()
|
||||
|
||||
install(TARGETS kyty_emulator DESTINATION .)
|
||||
if(KYTY_BUILD_LAUNCHER)
|
||||
add_subdirectory(launcher)
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif defined(__APPLE__)
|
||||
#include <csignal>
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <initializer_list>
|
||||
#include <ucontext.h> // IWYU pragma: keep
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// IWYU pragma: no_include <errhandlingapi.h>
|
||||
@@ -16,7 +24,7 @@
|
||||
|
||||
namespace Common::HostException {
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
static std::atomic<Handler> g_handler {nullptr};
|
||||
static std::atomic_uint32_t g_install_state {0};
|
||||
@@ -30,7 +38,9 @@ static_assert(decltype(g_install_state)::is_always_lock_free);
|
||||
std::fputs(reason != nullptr ? reason : "unspecified", stderr);
|
||||
std::fputc('\n', stderr);
|
||||
std::fflush(stderr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
TerminateProcess(GetCurrentProcess(), static_cast<UINT>(EXCEPTION_NONCONTINUABLE_EXCEPTION));
|
||||
#endif
|
||||
std::_Exit(321);
|
||||
}
|
||||
|
||||
@@ -48,6 +58,21 @@ public:
|
||||
KYTY_CLASS_NO_COPY(FilterScope);
|
||||
};
|
||||
|
||||
static Handler LoadInstalledHandler() noexcept {
|
||||
if (g_install_state.load(std::memory_order_acquire) == 0) {
|
||||
FailFast("host exception handler is not installed");
|
||||
}
|
||||
|
||||
const auto handler = g_handler.load(std::memory_order_acquire);
|
||||
if (handler == nullptr) {
|
||||
FailFast("host exception callback is null");
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS exception) {
|
||||
FilterScope filter_scope;
|
||||
|
||||
@@ -106,22 +131,176 @@ static LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS exception) {
|
||||
info.r14 = exception->ContextRecord->R14;
|
||||
info.r15 = exception->ContextRecord->R15;
|
||||
|
||||
if (g_install_state.load(std::memory_order_acquire) == 0) {
|
||||
FailFast("host exception handler is not installed");
|
||||
const auto handler = LoadInstalledHandler();
|
||||
|
||||
return handler(info) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
static std::atomic<Handler> g_handler {nullptr};
|
||||
static std::atomic_uint32_t g_install_state {0};
|
||||
static thread_local bool g_in_exception_filter = false;
|
||||
|
||||
static_assert(decltype(g_handler)::is_always_lock_free);
|
||||
static_assert(decltype(g_install_state)::is_always_lock_free);
|
||||
|
||||
[[noreturn]] static void FailFast(const char* reason) noexcept {
|
||||
std::fputs("HostException fail-fast: ", stderr);
|
||||
std::fputs(reason != nullptr ? reason : "unspecified", stderr);
|
||||
std::fputc('\n', stderr);
|
||||
std::fflush(stderr);
|
||||
std::_Exit(321);
|
||||
}
|
||||
|
||||
// Translate the x86-64 page-fault error code (mcontext __es.__err) into an access type.
|
||||
// bit 1 (0x2) = write, bit 4 (0x10) = instruction fetch, otherwise a read.
|
||||
static AccessViolationType DecodeAccess(uint64_t err) {
|
||||
if ((err & 0x10u) != 0) {
|
||||
return AccessViolationType::Execute;
|
||||
}
|
||||
if ((err & 0x2u) != 0) {
|
||||
return AccessViolationType::Write;
|
||||
}
|
||||
return AccessViolationType::Read;
|
||||
}
|
||||
|
||||
// POSIX signal handler that mirrors the Windows vectored handler: build an ExceptionInfo
|
||||
// from the mcontext and dispatch. A resolved fault (handler returns true) simply returns,
|
||||
// re-executing the faulting instruction against the now-fixed protection. An unresolved
|
||||
// fault restores the default disposition so the retry terminates the process.
|
||||
static void SignalHandler(int sig, siginfo_t* si, void* uctx) {
|
||||
if (g_in_exception_filter) {
|
||||
FailFast("nested exception while resolving a host fault");
|
||||
}
|
||||
g_in_exception_filter = true;
|
||||
|
||||
auto* uc = static_cast<ucontext_t*>(uctx);
|
||||
const auto* mc = uc->uc_mcontext;
|
||||
const auto& ss = mc->__ss;
|
||||
|
||||
ExceptionInfo info {};
|
||||
info.exception_address = ss.__rip;
|
||||
info.native_code = static_cast<uint32_t>(si->si_code);
|
||||
info.native_context = uctx;
|
||||
|
||||
if (sig == SIGILL) {
|
||||
info.type = ExceptionType::IllegalInstruction;
|
||||
} else {
|
||||
info.type = ExceptionType::AccessViolation;
|
||||
info.access_violation_type = DecodeAccess(mc->__es.__err);
|
||||
info.access_violation_vaddr = reinterpret_cast<uint64_t>(si->si_addr);
|
||||
}
|
||||
|
||||
info.rax = ss.__rax;
|
||||
info.rbx = ss.__rbx;
|
||||
info.rcx = ss.__rcx;
|
||||
info.rdx = ss.__rdx;
|
||||
info.rsi = ss.__rsi;
|
||||
info.rdi = ss.__rdi;
|
||||
info.rbp = ss.__rbp;
|
||||
info.rsp = ss.__rsp;
|
||||
info.r8 = ss.__r8;
|
||||
info.r9 = ss.__r9;
|
||||
info.r10 = ss.__r10;
|
||||
info.r11 = ss.__r11;
|
||||
info.r12 = ss.__r12;
|
||||
info.r13 = ss.__r13;
|
||||
info.r14 = ss.__r14;
|
||||
info.r15 = ss.__r15;
|
||||
|
||||
const auto handler = g_handler.load(std::memory_order_acquire);
|
||||
if (handler == nullptr) {
|
||||
FailFast("host exception callback is null");
|
||||
}
|
||||
|
||||
return handler(info) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
|
||||
const bool resolved = handler(info);
|
||||
g_in_exception_filter = false;
|
||||
|
||||
if (resolved) {
|
||||
return; // retry the faulting instruction against the fixed mapping
|
||||
}
|
||||
|
||||
// Unresolved: restore the default action so the re-executed instruction terminates.
|
||||
struct sigaction dfl {};
|
||||
dfl.sa_handler = SIG_DFL;
|
||||
sigemptyset(&dfl.sa_mask);
|
||||
sigaction(sig, &dfl, nullptr);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// x86-64 page-fault error bits.
|
||||
constexpr uint64_t PAGE_FAULT_ERROR_WRITE = 0x02;
|
||||
constexpr uint64_t PAGE_FAULT_ERROR_INSTRUCTION = 0x10;
|
||||
|
||||
// Let the kernel handle an unresolved fault on retry.
|
||||
static void ChainToDefault(int signal_number) noexcept {
|
||||
struct sigaction restore {};
|
||||
restore.sa_handler = SIG_DFL;
|
||||
sigemptyset(&restore.sa_mask);
|
||||
restore.sa_flags = 0;
|
||||
::sigaction(signal_number, &restore, nullptr);
|
||||
}
|
||||
|
||||
static void SignalHandler(int signal_number, siginfo_t* signal_info, void* native_context) {
|
||||
FilterScope filter_scope;
|
||||
|
||||
auto* context = static_cast<ucontext_t*>(native_context);
|
||||
auto* gregs = context->uc_mcontext.gregs;
|
||||
|
||||
ExceptionInfo info {};
|
||||
info.exception_address = static_cast<uint64_t>(gregs[REG_RIP]);
|
||||
info.native_code = static_cast<uint32_t>(signal_number);
|
||||
info.native_context = context;
|
||||
|
||||
if (signal_number == SIGSEGV || signal_number == SIGBUS) {
|
||||
info.type = ExceptionType::AccessViolation;
|
||||
const auto error_code = static_cast<uint64_t>(gregs[REG_ERR]);
|
||||
if ((error_code & PAGE_FAULT_ERROR_INSTRUCTION) != 0) {
|
||||
info.access_violation_type = AccessViolationType::Execute;
|
||||
} else if ((error_code & PAGE_FAULT_ERROR_WRITE) != 0) {
|
||||
info.access_violation_type = AccessViolationType::Write;
|
||||
} else {
|
||||
info.access_violation_type = AccessViolationType::Read;
|
||||
}
|
||||
info.access_violation_vaddr = reinterpret_cast<uint64_t>(signal_info->si_addr);
|
||||
} else if (signal_number == SIGILL) {
|
||||
info.type = ExceptionType::IllegalInstruction;
|
||||
} else {
|
||||
ChainToDefault(signal_number);
|
||||
return;
|
||||
}
|
||||
|
||||
info.rax = static_cast<uint64_t>(gregs[REG_RAX]);
|
||||
info.rbx = static_cast<uint64_t>(gregs[REG_RBX]);
|
||||
info.rcx = static_cast<uint64_t>(gregs[REG_RCX]);
|
||||
info.rdx = static_cast<uint64_t>(gregs[REG_RDX]);
|
||||
info.rsi = static_cast<uint64_t>(gregs[REG_RSI]);
|
||||
info.rdi = static_cast<uint64_t>(gregs[REG_RDI]);
|
||||
info.rbp = static_cast<uint64_t>(gregs[REG_RBP]);
|
||||
info.rsp = static_cast<uint64_t>(gregs[REG_RSP]);
|
||||
info.r8 = static_cast<uint64_t>(gregs[REG_R8]);
|
||||
info.r9 = static_cast<uint64_t>(gregs[REG_R9]);
|
||||
info.r10 = static_cast<uint64_t>(gregs[REG_R10]);
|
||||
info.r11 = static_cast<uint64_t>(gregs[REG_R11]);
|
||||
info.r12 = static_cast<uint64_t>(gregs[REG_R12]);
|
||||
info.r13 = static_cast<uint64_t>(gregs[REG_R13]);
|
||||
info.r14 = static_cast<uint64_t>(gregs[REG_R14]);
|
||||
info.r15 = static_cast<uint64_t>(gregs[REG_R15]);
|
||||
|
||||
const auto handler = LoadInstalledHandler();
|
||||
|
||||
if (handler(info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChainToDefault(signal_number);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool InstallHandler(Handler handler) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (handler == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -133,19 +312,48 @@ bool InstallHandler(Handler handler) {
|
||||
|
||||
g_handler.store(handler, std::memory_order_release);
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (AddVectoredExceptionHandler(1, ExceptionFilter) == nullptr) {
|
||||
g_handler.store(nullptr, std::memory_order_release);
|
||||
g_install_state.store(0, std::memory_order_release);
|
||||
printf("AddVectoredExceptionHandler() failed\n");
|
||||
return false;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
struct sigaction sa {};
|
||||
sa.sa_sigaction = SignalHandler;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
// macOS raises SIGBUS for protection faults on some paths and SIGSEGV on others;
|
||||
// SIGILL covers instructions the host cannot execute (routed to the x64 emulator).
|
||||
bool ok = sigaction(SIGSEGV, &sa, nullptr) == 0 && sigaction(SIGBUS, &sa, nullptr) == 0 &&
|
||||
sigaction(SIGILL, &sa, nullptr) == 0;
|
||||
if (!ok) {
|
||||
g_handler.store(nullptr, std::memory_order_release);
|
||||
g_install_state.store(0, std::memory_order_release);
|
||||
printf("sigaction() failed to install the host fault handler\n");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = SignalHandler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
// Fault resolution needs the normal thread stack.
|
||||
action.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
|
||||
for (const int signal_number: {SIGSEGV, SIGBUS, SIGILL}) {
|
||||
if (::sigaction(signal_number, &action, nullptr) != 0) {
|
||||
g_handler.store(nullptr, std::memory_order_release);
|
||||
g_install_state.store(0, std::memory_order_release);
|
||||
printf("sigaction(%d) failed\n", signal_number);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
g_install_state.store(2, std::memory_order_release);
|
||||
return true;
|
||||
#else
|
||||
(void)handler;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Common::HostException
|
||||
|
||||
@@ -178,6 +178,11 @@ Direction GetDirection() {
|
||||
return g_direction;
|
||||
}
|
||||
|
||||
bool IsSilent() {
|
||||
// Before init LOGF must keep writing to stdout, so report non-silent.
|
||||
return g_initialized && g_direction == Direction::Silent;
|
||||
}
|
||||
|
||||
void Write(std::string_view text) {
|
||||
WriteImpl(text);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ KYTY_SUBSYSTEM_DEFINE(Log);
|
||||
enum class Direction { Silent, Console, File };
|
||||
|
||||
Direction GetDirection();
|
||||
bool IsSilent();
|
||||
void Write(std::string_view text);
|
||||
void Write(fmt::text_style style, std::string_view text);
|
||||
void WriteFatal(std::string_view text);
|
||||
@@ -41,8 +42,18 @@ inline constexpr auto BrightWhite = fmt::fg(fmt::terminal_color::bright_white)
|
||||
} // namespace Log
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF(...) ::Log::Write(::fmt::sprintf(__VA_ARGS__))
|
||||
#define LOGF(...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write(::fmt::sprintf(__VA_ARGS__)); \
|
||||
} \
|
||||
} while (false)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF_COLOR(style, ...) ::Log::Write((style), ::fmt::sprintf(__VA_ARGS__))
|
||||
#define LOGF_COLOR(style, ...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write((style), ::fmt::sprintf(__VA_ARGS__)); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#endif /* KYTY_COMMON_LOGGING_LOG_H_ */
|
||||
|
||||
@@ -23,6 +23,10 @@ struct sys_dbg_stack_info_t {
|
||||
size_t commited_size;
|
||||
size_t total_size;
|
||||
size_t code_size;
|
||||
|
||||
// Full stack reservation reported by pthread.
|
||||
uintptr_t reserved_addr;
|
||||
size_t reserved_size;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -8,12 +8,57 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <execinfo.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__APPLE__)
|
||||
#include <libgen.h> // POSIX basename() lives here on macOS, not in <cstring>
|
||||
#endif
|
||||
|
||||
void SysStackWalk(void** /*stack*/, int* depth) {
|
||||
*depth = 0;
|
||||
// Avoid unwinding a guest-owned stack.
|
||||
static bool OnOwnStack() {
|
||||
const char* probe = reinterpret_cast<const char*>(&probe);
|
||||
|
||||
pthread_attr_t attr {};
|
||||
#if defined(__APPLE__)
|
||||
const auto* top = static_cast<const char*>(pthread_get_stackaddr_np(pthread_self()));
|
||||
const auto size = pthread_get_stacksize_np(pthread_self());
|
||||
(void)attr;
|
||||
return top != nullptr && size != 0 && probe < top && probe >= top - size;
|
||||
#else
|
||||
if (pthread_getattr_np(pthread_self(), &attr) != 0) {
|
||||
return false;
|
||||
}
|
||||
void* base = nullptr;
|
||||
size_t size = 0;
|
||||
const bool ok =
|
||||
pthread_attr_getstack(&attr, &base, &size) == 0 && base != nullptr && size != 0;
|
||||
pthread_attr_destroy(&attr);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
const auto* low = static_cast<const char*>(base);
|
||||
return probe >= low && probe < low + size;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SysStackWalk(void** stack, int* depth) {
|
||||
if (stack == nullptr || depth == nullptr || *depth <= 0) {
|
||||
if (depth != nullptr) {
|
||||
*depth = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OnOwnStack()) {
|
||||
*depth = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const int n = ::backtrace(stack, *depth);
|
||||
*depth = (n < 0 ? 0 : n);
|
||||
}
|
||||
|
||||
void SysStackUsagePrint(sys_dbg_stack_info_t& stack) {
|
||||
@@ -30,6 +75,33 @@ void SysStackUsage(sys_dbg_stack_info_t& s) {
|
||||
|
||||
[[maybe_unused]] int result = 0;
|
||||
|
||||
memset(&s, 0, sizeof(sys_dbg_stack_info_t));
|
||||
|
||||
// Record the reservation before the Linux /proc walk.
|
||||
{
|
||||
pthread_attr_t self_attr {};
|
||||
#if defined(__APPLE__)
|
||||
void* stack_top = pthread_get_stackaddr_np(pthread_self());
|
||||
const size_t stack_size = pthread_get_stacksize_np(pthread_self());
|
||||
if (stack_top != nullptr && stack_size != 0) {
|
||||
s.reserved_addr = reinterpret_cast<uintptr_t>(stack_top) - stack_size;
|
||||
s.reserved_size = stack_size;
|
||||
}
|
||||
(void)self_attr;
|
||||
#else
|
||||
if (pthread_getattr_np(pthread_self(), &self_attr) == 0) {
|
||||
void* stack_base = nullptr;
|
||||
size_t stack_size = 0;
|
||||
if (pthread_attr_getstack(&self_attr, &stack_base, &stack_size) == 0 &&
|
||||
stack_base != nullptr && stack_size != 0) {
|
||||
s.reserved_addr = reinterpret_cast<uintptr_t>(stack_base);
|
||||
s.reserved_size = stack_size;
|
||||
}
|
||||
pthread_attr_destroy(&self_attr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
char str[1024];
|
||||
char str2[1024];
|
||||
result = sprintf(str, "/proc/%d/exe", static_cast<int>(pid));
|
||||
@@ -43,8 +115,6 @@ void SysStackUsage(sys_dbg_stack_info_t& s) {
|
||||
|
||||
result = sprintf(str, "/proc/%d/maps", static_cast<int>(pid));
|
||||
|
||||
memset(&s, 0, sizeof(sys_dbg_stack_info_t));
|
||||
|
||||
FILE* f = fopen(str, "r");
|
||||
|
||||
if (f == nullptr) {
|
||||
@@ -118,6 +188,11 @@ void SysStackUsage(sys_dbg_stack_info_t& s) {
|
||||
}
|
||||
|
||||
result = fclose(f);
|
||||
|
||||
if (s.reserved_addr == 0) {
|
||||
s.reserved_addr = s.addr;
|
||||
s.reserved_size = s.total_size;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <sys/stat.h>
|
||||
#include <system_error>
|
||||
#include <unistd.h>
|
||||
#include <utime.h>
|
||||
|
||||
@@ -39,10 +43,43 @@ struct sys_file_t {
|
||||
};
|
||||
};
|
||||
|
||||
// Darwin uses BSD timestamp member names.
|
||||
#if defined(__APPLE__)
|
||||
#define KYTY_STAT_ATIME_NS(st) ((st).st_atimespec.tv_nsec)
|
||||
#define KYTY_STAT_MTIME_NS(st) ((st).st_mtimespec.tv_nsec)
|
||||
#else
|
||||
#define KYTY_STAT_ATIME_NS(st) ((st).st_atim.tv_nsec)
|
||||
#define KYTY_STAT_MTIME_NS(st) ((st).st_mtim.tv_nsec)
|
||||
#endif
|
||||
|
||||
static std::filesystem::path get_internal_name(const std::filesystem::path& name) {
|
||||
return name.is_absolute() ? name : (std::filesystem::path(".") / name);
|
||||
}
|
||||
|
||||
// Pass access-pattern hints to the host.
|
||||
static void apply_cache_hint(FILE* f, sys_file_cache_type_t cache_type) {
|
||||
if (f == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
int advice = POSIX_FADV_NORMAL;
|
||||
switch (cache_type) {
|
||||
case SYS_FILE_CACHE_RANDOM_ACCESS: advice = POSIX_FADV_RANDOM; break;
|
||||
case SYS_FILE_CACHE_SEQUENTIAL_SCAN: advice = POSIX_FADV_SEQUENTIAL; break;
|
||||
case SYS_FILE_CACHE_AUTO:
|
||||
default: return;
|
||||
}
|
||||
::posix_fadvise(fileno(f), 0, 0, advice);
|
||||
#else
|
||||
if (cache_type == SYS_FILE_CACHE_SEQUENTIAL_SCAN) {
|
||||
::fcntl(fileno(f), F_RDAHEAD, 1);
|
||||
} else if (cache_type == SYS_FILE_CACHE_RANDOM_ACCESS) {
|
||||
::fcntl(fileno(f), F_RDAHEAD, 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SysFileRead(void* data, uint32_t size, sys_file_t& f, uint32_t* bytes_read) {
|
||||
if (f.type == SYS_FILE_FILE) {
|
||||
size_t w = fread(data, 1, size, f.f);
|
||||
@@ -136,7 +173,7 @@ sys_file_t* SysFileCreate(const std::filesystem::path& file_name) {
|
||||
}
|
||||
|
||||
sys_file_t* SysFileOpenR(const std::filesystem::path& file_name,
|
||||
sys_file_cache_type_t /*cache_type*/) {
|
||||
sys_file_cache_type_t cache_type) {
|
||||
auto* ret = new sys_file_t;
|
||||
|
||||
ret->type = SYS_FILE_FILE;
|
||||
@@ -150,6 +187,8 @@ sys_file_t* SysFileOpenR(const std::filesystem::path& file_name,
|
||||
ret->type = SYS_FILE_ERROR;
|
||||
}
|
||||
|
||||
apply_cache_hint(f, cache_type);
|
||||
|
||||
ret->f = f;
|
||||
|
||||
return ret;
|
||||
@@ -180,7 +219,7 @@ sys_file_t* SysFileCreate() {
|
||||
}
|
||||
|
||||
sys_file_t* SysFileOpenW(const std::filesystem::path& file_name,
|
||||
sys_file_cache_type_t /*cache_type*/) {
|
||||
sys_file_cache_type_t cache_type) {
|
||||
auto* ret = new sys_file_t;
|
||||
|
||||
auto real_name = get_internal_name(file_name);
|
||||
@@ -194,13 +233,15 @@ sys_file_t* SysFileOpenW(const std::filesystem::path& file_name,
|
||||
ret->type = SYS_FILE_FILE;
|
||||
}
|
||||
|
||||
apply_cache_hint(f, cache_type);
|
||||
|
||||
ret->f = f;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
sys_file_t* SysFileOpenRw(const std::filesystem::path& file_name,
|
||||
sys_file_cache_type_t /*cache_type*/) {
|
||||
sys_file_cache_type_t cache_type) {
|
||||
auto* ret = new sys_file_t;
|
||||
|
||||
auto real_name = get_internal_name(file_name);
|
||||
@@ -214,6 +255,8 @@ sys_file_t* SysFileOpenRw(const std::filesystem::path& file_name,
|
||||
ret->type = SYS_FILE_FILE;
|
||||
}
|
||||
|
||||
apply_cache_hint(f, cache_type);
|
||||
|
||||
ret->f = f;
|
||||
|
||||
return ret;
|
||||
@@ -239,11 +282,17 @@ uint64_t SysFileSize(sys_file_t& f) {
|
||||
[[maybe_unused]] int result = 0;
|
||||
|
||||
if (f.type == SYS_FILE_FILE) {
|
||||
uint32_t pos = ftell(f.f);
|
||||
result = fseek(f.f, 0, SEEK_END);
|
||||
uint32_t size = ftell(f.f);
|
||||
result = fseek(f.f, pos, SEEK_SET);
|
||||
return size;
|
||||
// Preserve sizes above 4 GiB.
|
||||
const off_t pos = ftello(f.f);
|
||||
if (pos < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (fseeko(f.f, 0, SEEK_END) != 0) {
|
||||
return 0;
|
||||
}
|
||||
const off_t size = ftello(f.f);
|
||||
result = fseeko(f.f, pos, SEEK_SET);
|
||||
return (size < 0 ? 0 : static_cast<uint64_t>(size));
|
||||
}
|
||||
|
||||
if (f.type == SYS_FILE_MEMORY_STAT || f.type == SYS_FILE_MEMORY_DYN) {
|
||||
@@ -260,8 +309,18 @@ uint64_t SysFileSize(const std::filesystem::path& file_name) {
|
||||
return size;
|
||||
}
|
||||
|
||||
bool SysFileTruncate(sys_file_t& /*f*/, uint64_t /*size*/) {
|
||||
return false;
|
||||
bool SysFileTruncate(sys_file_t& f, uint64_t size) {
|
||||
bool ok = false;
|
||||
if (f.type == SYS_FILE_FILE) {
|
||||
// Flush before resizing and restore the caller's position.
|
||||
const auto position = ftell(f.f);
|
||||
fflush(f.f);
|
||||
ok = (ftruncate(fileno(f.f), static_cast<off_t>(size)) == 0);
|
||||
if (position >= 0) {
|
||||
fseek(f.f, position, SEEK_SET);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool SysFileUnlink(sys_file_t& /*f*/, const std::filesystem::path& name) {
|
||||
@@ -382,6 +441,7 @@ SysFileTimeStruct SysFileGetLastAccessTimeUtc(const std::filesystem::path& name)
|
||||
} else {
|
||||
r.is_invalid = false;
|
||||
r.time = s.st_atime;
|
||||
r.nanos = KYTY_STAT_ATIME_NS(s);
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -400,6 +460,7 @@ SysFileTimeStruct SysFileGetLastWriteTimeUtc(const std::filesystem::path& name)
|
||||
} else {
|
||||
r.is_invalid = false;
|
||||
r.time = s.st_mtime;
|
||||
r.nanos = KYTY_STAT_MTIME_NS(s);
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -419,13 +480,36 @@ void SysFileGetLastAccessAndWriteTimeUtc(const std::filesystem::path& name, SysF
|
||||
a.is_invalid = false;
|
||||
w.is_invalid = false;
|
||||
a.time = s.st_atime;
|
||||
a.nanos = KYTY_STAT_ATIME_NS(s);
|
||||
w.time = s.st_mtime;
|
||||
w.nanos = KYTY_STAT_MTIME_NS(s);
|
||||
}
|
||||
}
|
||||
|
||||
void SysFileGetLastAccessAndWriteTimeUtc(sys_file_t& /*f*/, SysFileTimeStruct& /*a*/,
|
||||
SysFileTimeStruct& /*w*/) {
|
||||
EXIT("not implemented\n");
|
||||
void SysFileGetLastAccessAndWriteTimeUtc(sys_file_t& f, SysFileTimeStruct& a,
|
||||
SysFileTimeStruct& w) {
|
||||
if (f.type == SYS_FILE_FILE) {
|
||||
struct stat s {};
|
||||
|
||||
const bool ok = (0 == fstat(fileno(f.f), &s));
|
||||
|
||||
a.is_invalid = w.is_invalid = !ok;
|
||||
|
||||
if (ok) {
|
||||
a.time = s.st_atime;
|
||||
a.nanos = KYTY_STAT_ATIME_NS(s);
|
||||
w.time = s.st_mtime;
|
||||
w.nanos = KYTY_STAT_MTIME_NS(s);
|
||||
}
|
||||
} else if (f.type == SYS_FILE_MEMORY_STAT || f.type == SYS_FILE_MEMORY_DYN) {
|
||||
// Memory-backed files use the current time.
|
||||
SysTimeStruct t {};
|
||||
SysGetSystemTimeUtc(t);
|
||||
SysSystemToFileTimeUtc(t, a);
|
||||
SysSystemToFileTimeUtc(t, w);
|
||||
} else {
|
||||
a.is_invalid = w.is_invalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool SysFileSetLastAccessTimeUtc(const std::filesystem::path& name, SysFileTimeStruct& access) {
|
||||
@@ -531,27 +615,110 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
|
||||
// }
|
||||
}
|
||||
|
||||
void SysFileFindFiles(const std::filesystem::path& /*path*/,
|
||||
std::vector<sys_file_find_t>& /*out*/) {
|
||||
EXIT("not implemented\n");
|
||||
// Recursively collect regular files.
|
||||
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out) {
|
||||
auto real_path = get_internal_name(path);
|
||||
|
||||
DIR* dir = opendir(real_path.string().c_str());
|
||||
if (dir == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
|
||||
const std::string file_name(entry->d_name);
|
||||
|
||||
if (file_name == "." || file_name == "..") {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto child = real_path / file_name;
|
||||
struct stat s {};
|
||||
|
||||
// lstat, so a symlink is never followed into a cycle during the recursive walk.
|
||||
if (0 != lstat(child.string().c_str(), &s)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(s.st_mode)) {
|
||||
SysFileFindFiles(child, out);
|
||||
} else if (S_ISREG(s.st_mode)) {
|
||||
sys_file_find_t r {};
|
||||
|
||||
r.path_with_name = child;
|
||||
r.size = static_cast<uint64_t>(s.st_size);
|
||||
r.last_access_time.is_invalid = false;
|
||||
r.last_access_time.time = s.st_atime;
|
||||
r.last_access_time.nanos = KYTY_STAT_ATIME_NS(s);
|
||||
r.last_write_time.is_invalid = false;
|
||||
r.last_write_time.time = s.st_mtime;
|
||||
r.last_write_time.nanos = KYTY_STAT_MTIME_NS(s);
|
||||
|
||||
out.push_back(r);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
void SysFileGetDents(const std::filesystem::path& /*path*/, std::vector<sys_dir_entry_t>& /*out*/) {
|
||||
EXIT("not implemented\n");
|
||||
// Keep "." and ".." to match FindFirstFileW.
|
||||
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
|
||||
auto real_path = get_internal_name(path);
|
||||
|
||||
DIR* dir = opendir(real_path.string().c_str());
|
||||
if (dir == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
|
||||
sys_dir_entry_t r {};
|
||||
|
||||
r.name = entry->d_name;
|
||||
|
||||
if (entry->d_type == DT_UNKNOWN) {
|
||||
// Some filesystems do not populate d_type.
|
||||
struct stat s {};
|
||||
r.is_file = 0 == lstat((real_path / r.name).string().c_str(), &s) && S_ISREG(s.st_mode);
|
||||
} else {
|
||||
r.is_file = entry->d_type != DT_DIR;
|
||||
}
|
||||
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
bool SysFileCopyFile(const std::filesystem::path& /*src*/, const std::filesystem::path& /*dst*/) {
|
||||
EXIT("not implemented\n");
|
||||
return false;
|
||||
bool SysFileCopyFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
|
||||
std::error_code error;
|
||||
return std::filesystem::copy_file(get_internal_name(src), get_internal_name(dst),
|
||||
std::filesystem::copy_options::overwrite_existing, error) &&
|
||||
!error;
|
||||
}
|
||||
|
||||
bool SysFileMoveFile(const std::filesystem::path& /*src*/, const std::filesystem::path& /*dst*/) {
|
||||
EXIT("not implemented\n");
|
||||
return false;
|
||||
bool SysFileMoveFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
|
||||
auto real_src = get_internal_name(src);
|
||||
auto real_dst = get_internal_name(dst);
|
||||
|
||||
// Match MoveFileW: fail when the destination exists.
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(real_dst, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 0 == rename(real_src.string().c_str(), real_dst.string().c_str());
|
||||
}
|
||||
|
||||
void SysFileRemoveReadonly(const std::filesystem::path& /*name*/) {
|
||||
EXIT("not implemented\n");
|
||||
void SysFileRemoveReadonly(const std::filesystem::path& name) {
|
||||
auto real_name = get_internal_name(name);
|
||||
auto real_name_str = real_name.string();
|
||||
|
||||
struct stat s {};
|
||||
|
||||
if (0 != stat(real_name_str.c_str(), &s)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chmod(real_name_str.c_str(), s.st_mode | S_IWUSR);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,9 +8,16 @@
|
||||
#include "common/platform/sysVirtual.h"
|
||||
#include "common/virtualMemory.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#endif
|
||||
|
||||
// IWYU pragma: no_include <asm/mman-common.h>
|
||||
// IWYU pragma: no_include <asm/mman.h>
|
||||
@@ -31,8 +38,8 @@ void SysVirtualInit() {
|
||||
pthread_mutexattr_t attr {};
|
||||
|
||||
pthread_mutexattr_init(&attr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_FAST_NP);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_FAST_NP); // glibc-only fast mutex
|
||||
#else
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
|
||||
#endif
|
||||
@@ -76,6 +83,70 @@ static VirtualMemory::Mode get_protection_flag(int mode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep automatic mappings inside the guest and GPU-addressable low window.
|
||||
#ifdef KYTY_FIXED_NOREPLACE
|
||||
static constexpr uintptr_t LOW_ARENA_LIMIT = 0x000000FC00000000ULL; // libc mspace window ceiling
|
||||
static constexpr uintptr_t LOW_ARENA_FLOOR = 0x000000A000000000ULL; // 640 GiB
|
||||
static constexpr uintptr_t LOW_ARENA_GRAIN = 0x0000000000010000ULL; // 64 KiB
|
||||
|
||||
static_assert(LOW_ARENA_LIMIT <= 0x0000010000000000ULL,
|
||||
"arena must stay inside the GPU page tracker's 1<<40 window");
|
||||
static_assert(LOW_ARENA_FLOOR < LOW_ARENA_LIMIT, "arena floor must sit below its ceiling");
|
||||
|
||||
static std::atomic<uintptr_t> g_low_arena_next {LOW_ARENA_LIMIT};
|
||||
#endif
|
||||
|
||||
// Caller holds g_virtual_mutex.
|
||||
static void record_alloc(uintptr_t addr, size_t size) {
|
||||
auto next = g_allocs->upper_bound(addr);
|
||||
if (next != g_allocs->begin()) {
|
||||
auto it = std::prev(next);
|
||||
const auto alloc_addr = it->first;
|
||||
const auto alloc_end = alloc_addr + it->second;
|
||||
if (alloc_addr <= addr && addr + size <= alloc_end) {
|
||||
g_allocs->erase(it);
|
||||
if (alloc_addr < addr) {
|
||||
(*g_allocs)[alloc_addr] = addr - alloc_addr;
|
||||
}
|
||||
if (addr + size < alloc_end) {
|
||||
(*g_allocs)[addr + size] = alloc_end - (addr + size);
|
||||
}
|
||||
}
|
||||
}
|
||||
(*g_allocs)[addr] = size;
|
||||
}
|
||||
|
||||
#ifdef KYTY_FIXED_NOREPLACE
|
||||
static uintptr_t align_up_to(uintptr_t addr, uint64_t alignment) {
|
||||
return (addr + alignment - 1) & ~(alignment - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Freed arena addresses are not reused while GPU caches remain keyed by address.
|
||||
static void* map_anonymous(uintptr_t addr, size_t size, int protect, int flags) {
|
||||
if (addr != 0) {
|
||||
return mmap(reinterpret_cast<void*>(addr), size, protect, flags, -1, 0); // NOLINT
|
||||
}
|
||||
|
||||
#ifdef KYTY_FIXED_NOREPLACE
|
||||
const auto step = align_up_to(size, LOW_ARENA_GRAIN);
|
||||
for (int attempt = 0; attempt < 256; attempt++) {
|
||||
const auto top = g_low_arena_next.fetch_sub(step, std::memory_order_relaxed);
|
||||
if (top < step || top - step < LOW_ARENA_FLOOR) {
|
||||
break;
|
||||
}
|
||||
const auto hint = (top - step) & ~(LOW_ARENA_GRAIN - 1);
|
||||
void* ptr = mmap(reinterpret_cast<void*>(hint), size, protect,
|
||||
flags | MAP_FIXED_NOREPLACE, -1, 0); // NOLINT
|
||||
if (ptr != MAP_FAILED) {
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return mmap(nullptr, size, protect, flags, -1, 0); // NOLINT
|
||||
}
|
||||
|
||||
uint64_t SysVirtualAlloc(uint64_t address, uint64_t size, VirtualMemory::Mode mode) {
|
||||
EXIT_IF(g_allocs == nullptr);
|
||||
|
||||
@@ -83,14 +154,13 @@ uint64_t SysVirtualAlloc(uint64_t address, uint64_t size, VirtualMemory::Mode mo
|
||||
|
||||
int protect = get_protection_flag(mode);
|
||||
|
||||
void* ptr =
|
||||
mmap(reinterpret_cast<void*>(addr), size, protect, MAP_PRIVATE | MAP_ANON, -1, 0); // NOLINT
|
||||
void* ptr = map_anonymous(addr, size, protect, MAP_PRIVATE | MAP_ANON);
|
||||
|
||||
auto ret_addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
|
||||
if (ptr != MAP_FAILED) {
|
||||
pthread_mutex_lock(&g_virtual_mutex);
|
||||
(*g_allocs)[ret_addr] = size;
|
||||
record_alloc(ret_addr, size);
|
||||
uintptr_t page_start = ret_addr >> 12u;
|
||||
uintptr_t page_end = (ret_addr + size - 1) >> 12u;
|
||||
for (uintptr_t page = page_start; page <= page_end; page++) {
|
||||
@@ -117,18 +187,43 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
auto addr = static_cast<uintptr_t>(address);
|
||||
int protect = get_protection_flag(mode);
|
||||
|
||||
void* ptr =
|
||||
mmap(reinterpret_cast<void*>(addr), size, protect, MAP_PRIVATE | MAP_ANON, -1, 0); // NOLINT
|
||||
void* ptr = map_anonymous(addr, size, protect, MAP_PRIVATE | MAP_ANON);
|
||||
|
||||
auto ret_addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
|
||||
if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) {
|
||||
munmap(ptr, size);
|
||||
|
||||
ptr = mmap(reinterpret_cast<void*>(addr), size + alignment, protect,
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
|
||||
ptr = map_anonymous(addr, size + alignment, protect,
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE);
|
||||
ret_addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
if (ptr != MAP_FAILED) {
|
||||
#if defined(__APPLE__)
|
||||
// Carve the aligned subrange out of the live mapping with MAP_FIXED (in-place
|
||||
// replacement) and trim the slack; never munmap the whole range first, or a
|
||||
// concurrent host mapping (dyld, Rosetta, Metal) could claim the hole and be
|
||||
// destroyed by the MAP_FIXED. Other platforms keep the original path below.
|
||||
auto aligned_addr = align_up(ret_addr, alignment);
|
||||
// NOLINTNEXTLINE
|
||||
void* fixed = mmap(reinterpret_cast<void*>(aligned_addr), size, protect,
|
||||
MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (fixed == MAP_FAILED) {
|
||||
munmap(ptr, size + alignment);
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
} else {
|
||||
if (aligned_addr > ret_addr) {
|
||||
munmap(reinterpret_cast<void*>(ret_addr), aligned_addr - ret_addr);
|
||||
}
|
||||
const uintptr_t tail_start = aligned_addr + size;
|
||||
const uintptr_t resv_end = ret_addr + size + alignment;
|
||||
if (resv_end > tail_start) {
|
||||
munmap(reinterpret_cast<void*>(tail_start), resv_end - tail_start);
|
||||
}
|
||||
ptr = fixed;
|
||||
ret_addr = aligned_addr;
|
||||
}
|
||||
#else
|
||||
munmap(ptr, size + alignment);
|
||||
auto aligned_addr = align_up(ret_addr, alignment);
|
||||
#ifdef KYTY_FIXED_NOREPLACE
|
||||
@@ -146,6 +241,7 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +250,7 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_virtual_mutex);
|
||||
(*g_allocs)[ret_addr] = size;
|
||||
record_alloc(ret_addr, size);
|
||||
uintptr_t page_start = ret_addr >> 12u;
|
||||
uintptr_t page_end = (ret_addr + size - 1) >> 12u;
|
||||
for (uintptr_t page = page_start; page <= page_end; page++) {
|
||||
@@ -165,6 +261,27 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
return ret_addr;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// macOS has no /proc/self/maps; query the Mach VM map directly. mach_vm_region returns
|
||||
// the first mapped region at or above `region_addr`; if it begins before the end of the
|
||||
// requested range, the range overlaps an existing mapping.
|
||||
static bool is_mapped(void* ptr, size_t length) {
|
||||
auto query_addr = reinterpret_cast<mach_vm_address_t>(ptr);
|
||||
mach_vm_address_t region_addr = query_addr;
|
||||
mach_vm_size_t region_size = 0;
|
||||
vm_region_basic_info_data_64_t info {};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object_name = MACH_PORT_NULL;
|
||||
|
||||
kern_return_t kr =
|
||||
mach_vm_region(mach_task_self(), ®ion_addr, ®ion_size, VM_REGION_BASIC_INFO_64,
|
||||
reinterpret_cast<vm_region_info_t>(&info), &count, &object_name);
|
||||
if (kr != KERN_SUCCESS) {
|
||||
return false; // no region at or above the address → unmapped
|
||||
}
|
||||
return region_addr < (query_addr + length);
|
||||
}
|
||||
#else
|
||||
static bool is_mapped(void* ptr, size_t length) {
|
||||
FILE* file = fopen("/proc/self/maps", "r");
|
||||
char line[1024];
|
||||
@@ -189,6 +306,7 @@ static bool is_mapped(void* ptr, size_t length) {
|
||||
fclose(file);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SysVirtualAllocFixed(uint64_t address, uint64_t size, VirtualMemory::Mode mode) {
|
||||
EXIT_IF(g_allocs == nullptr);
|
||||
@@ -218,7 +336,7 @@ bool SysVirtualAllocFixed(uint64_t address, uint64_t size, VirtualMemory::Mode m
|
||||
|
||||
if (ptr != MAP_FAILED) {
|
||||
pthread_mutex_lock(&g_virtual_mutex);
|
||||
(*g_allocs)[ret_addr] = size;
|
||||
record_alloc(ret_addr, size);
|
||||
uintptr_t page_start = ret_addr >> 12u;
|
||||
uintptr_t page_end = (ret_addr + size - 1) >> 12u;
|
||||
for (uintptr_t page = page_start; page <= page_end; page++) {
|
||||
@@ -249,18 +367,44 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
|
||||
|
||||
auto addr = static_cast<uintptr_t>(address);
|
||||
|
||||
void* ptr = mmap(reinterpret_cast<void*>(addr), size, PROT_NONE,
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
|
||||
void* ptr = map_anonymous(addr, size, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE);
|
||||
|
||||
auto ret_addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
|
||||
if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) {
|
||||
munmap(ptr, size);
|
||||
|
||||
ptr = mmap(reinterpret_cast<void*>(addr), size + alignment, PROT_NONE,
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
|
||||
ptr = map_anonymous(addr, size + alignment, PROT_NONE,
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE);
|
||||
ret_addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
if (ptr != MAP_FAILED) {
|
||||
#if defined(__APPLE__)
|
||||
// Carve the aligned subrange out of the live reservation with MAP_FIXED (an
|
||||
// in-place replacement), then trim the slack. The range must never be
|
||||
// returned to the OS in between: another thread (dyld, Rosetta, Metal,
|
||||
// malloc) could claim the hole, and the subsequent MAP_FIXED would silently
|
||||
// destroy its mapping. Other platforms keep the original path below.
|
||||
auto aligned_addr = align_up(ret_addr, alignment);
|
||||
// NOLINTNEXTLINE
|
||||
void* fixed = mmap(reinterpret_cast<void*>(aligned_addr), size, PROT_NONE,
|
||||
MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0);
|
||||
if (fixed == MAP_FAILED) {
|
||||
munmap(ptr, size + alignment);
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
} else {
|
||||
if (aligned_addr > ret_addr) {
|
||||
munmap(reinterpret_cast<void*>(ret_addr), aligned_addr - ret_addr);
|
||||
}
|
||||
const uintptr_t tail_start = aligned_addr + size;
|
||||
const uintptr_t resv_end = ret_addr + size + alignment;
|
||||
if (resv_end > tail_start) {
|
||||
munmap(reinterpret_cast<void*>(tail_start), resv_end - tail_start);
|
||||
}
|
||||
ptr = fixed;
|
||||
ret_addr = aligned_addr;
|
||||
}
|
||||
#else
|
||||
munmap(ptr, size + alignment);
|
||||
auto aligned_addr = align_up(ret_addr, alignment);
|
||||
#ifdef KYTY_FIXED_NOREPLACE
|
||||
@@ -278,6 +422,7 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +431,7 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_virtual_mutex);
|
||||
(*g_allocs)[ret_addr] = size;
|
||||
record_alloc(ret_addr, size);
|
||||
uintptr_t page_start = ret_addr >> 12u;
|
||||
uintptr_t page_end = (ret_addr + size - 1) >> 12u;
|
||||
for (uintptr_t page = page_start; page <= page_end; page++) {
|
||||
@@ -324,7 +469,7 @@ bool SysVirtualReserveFixed(uint64_t address, uint64_t size) {
|
||||
|
||||
if (ptr != MAP_FAILED) {
|
||||
pthread_mutex_lock(&g_virtual_mutex);
|
||||
(*g_allocs)[ret_addr] = size;
|
||||
record_alloc(ret_addr, size);
|
||||
uintptr_t page_start = ret_addr >> 12u;
|
||||
uintptr_t page_end = (ret_addr + size - 1) >> 12u;
|
||||
for (uintptr_t page = page_start; page <= page_end; page++) {
|
||||
@@ -339,7 +484,29 @@ bool SysVirtualReserveFixed(uint64_t address, uint64_t size) {
|
||||
}
|
||||
|
||||
bool SysVirtualDecommit(uint64_t address, uint64_t size) {
|
||||
return SysVirtualProtect(address, size, VirtualMemory::Mode::NoAccess);
|
||||
// Drop physical pages while preserving the reservation.
|
||||
if (!SysVirtualProtect(address, size, VirtualMemory::Mode::NoAccess)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (size != 0) {
|
||||
#if defined(__APPLE__)
|
||||
constexpr int RECLAIM_ADVICE = MADV_FREE;
|
||||
#else
|
||||
constexpr int RECLAIM_ADVICE = MADV_DONTNEED;
|
||||
#endif
|
||||
const auto page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
|
||||
if (page_size != 0) {
|
||||
// Do not discard pages outside the requested range.
|
||||
const auto begin = (static_cast<uintptr_t>(address) + page_size - 1) & ~(page_size - 1);
|
||||
const auto end = (static_cast<uintptr_t>(address) + size) & ~(page_size - 1);
|
||||
if (end > begin) {
|
||||
::madvise(reinterpret_cast<void*>(begin), end - begin, RECLAIM_ADVICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SysVirtualFree(uint64_t address) {
|
||||
@@ -391,15 +558,34 @@ bool SysVirtualFreeRange(uint64_t address, uint64_t size) {
|
||||
pthread_mutex_unlock(&g_virtual_mutex);
|
||||
return false;
|
||||
}
|
||||
auto it = std::prev(next);
|
||||
const auto alloc_addr = it->first;
|
||||
const auto alloc_end = alloc_addr + it->second;
|
||||
if (addr < alloc_addr || end > alloc_end || munmap(reinterpret_cast<void*>(addr), size) != 0) {
|
||||
|
||||
// A reservation may have been split into several adjacent records.
|
||||
auto first = std::prev(next);
|
||||
const auto alloc_addr = first->first;
|
||||
if (addr < alloc_addr || alloc_addr + first->second <= addr) {
|
||||
pthread_mutex_unlock(&g_virtual_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_allocs->erase(it);
|
||||
auto last = first;
|
||||
uintptr_t cursor = alloc_addr + first->second;
|
||||
while (cursor < end) {
|
||||
auto following = std::next(last);
|
||||
if (following == g_allocs->end() || following->first != cursor) {
|
||||
pthread_mutex_unlock(&g_virtual_mutex);
|
||||
return false;
|
||||
}
|
||||
last = following;
|
||||
cursor = following->first + following->second;
|
||||
}
|
||||
const auto alloc_end = cursor;
|
||||
|
||||
if (munmap(reinterpret_cast<void*>(addr), size) != 0) {
|
||||
pthread_mutex_unlock(&g_virtual_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_allocs->erase(first, std::next(last));
|
||||
if (alloc_addr < addr) {
|
||||
(*g_allocs)[alloc_addr] = addr - alloc_addr;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ struct SysFileTimeStruct {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
FILETIME time;
|
||||
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
// Nanoseconds preserve sub-second file timestamps.
|
||||
time_t time;
|
||||
long nanos;
|
||||
#endif
|
||||
bool is_invalid;
|
||||
};
|
||||
@@ -139,7 +141,7 @@ inline void SysFileToSystemTimeUtc(const SysFileTimeStruct& f, SysTimeStruct& t)
|
||||
t.Hour = i.tm_hour;
|
||||
t.Minute = i.tm_min;
|
||||
t.Second = (i.tm_sec == 60 ? 59 : i.tm_sec);
|
||||
t.Milliseconds = 0;
|
||||
t.Milliseconds = static_cast<uint16_t>((f.nanos / 1000000) % 1000);
|
||||
}
|
||||
|
||||
inline void SysTimeTToSystem(time_t t, SysTimeStruct& s) {
|
||||
@@ -168,10 +170,11 @@ inline void SysSystemToFileTimeUtc(const SysTimeStruct& f, SysFileTimeStruct& t)
|
||||
|
||||
// Retrieves the current local date and time.
|
||||
inline void SysGetSystemTime(SysTimeStruct& t) {
|
||||
time_t st {};
|
||||
// Preserve millisecond precision.
|
||||
timespec now {};
|
||||
struct tm i {};
|
||||
|
||||
if (time(&st) == static_cast<time_t>(-1) || localtime_r(&st, &i) == nullptr) {
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0 || localtime_r(&now.tv_sec, &i) == nullptr) {
|
||||
t.is_invalid = true;
|
||||
return;
|
||||
}
|
||||
@@ -183,15 +186,16 @@ inline void SysGetSystemTime(SysTimeStruct& t) {
|
||||
t.Hour = i.tm_hour;
|
||||
t.Minute = i.tm_min;
|
||||
t.Second = (i.tm_sec == 60 ? 59 : i.tm_sec);
|
||||
t.Milliseconds = 0;
|
||||
t.Milliseconds = static_cast<uint16_t>((now.tv_nsec / 1000000) % 1000);
|
||||
}
|
||||
|
||||
// Retrieves the current system date and time in Coordinated Universal Time (UTC).
|
||||
inline void SysGetSystemTimeUtc(SysTimeStruct& t) {
|
||||
time_t st {};
|
||||
// Preserve millisecond precision.
|
||||
timespec now {};
|
||||
struct tm i {};
|
||||
|
||||
if (time(&st) == static_cast<time_t>(-1) || gmtime_r(&st, &i) == nullptr) {
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0 || gmtime_r(&now.tv_sec, &i) == nullptr) {
|
||||
t.is_invalid = true;
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +207,7 @@ inline void SysGetSystemTimeUtc(SysTimeStruct& t) {
|
||||
t.Hour = i.tm_hour;
|
||||
t.Minute = i.tm_min;
|
||||
t.Second = (i.tm_sec == 60 ? 59 : i.tm_sec);
|
||||
t.Milliseconds = 0;
|
||||
t.Milliseconds = static_cast<uint16_t>((now.tv_nsec / 1000000) % 1000);
|
||||
}
|
||||
|
||||
inline void SysQueryPerformanceFrequency(uint64_t* freq) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <atomic>
|
||||
#include <chrono> // IWYU pragma: keep
|
||||
#include <condition_variable> // IWYU pragma: keep
|
||||
#include <cerrno>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
@@ -14,6 +15,12 @@
|
||||
#define KYTY_WIN_CS
|
||||
#endif
|
||||
|
||||
// macOS has no clock_nanosleep.
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__)
|
||||
#define KYTY_POSIX_HIGH_RES_SLEEP
|
||||
#include <ctime>
|
||||
#endif
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -121,6 +128,42 @@ static SleepConditionVariableCS_func_t ResolveSleepConditionVariableCS() {
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef KYTY_POSIX_HIGH_RES_SLEEP
|
||||
// Spin for very short waits; use an absolute deadline for longer waits.
|
||||
static void SleepHighResolutionNanos(uint64_t nanos) {
|
||||
if (nanos == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr uint64_t NANOS_PER_SEC = 1000000000;
|
||||
constexpr uint64_t SPIN_LIMIT_NS = 50000; // below this a context switch dominates
|
||||
|
||||
timespec deadline {};
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
|
||||
std::this_thread::sleep_for(std::chrono::nanoseconds(nanos));
|
||||
return;
|
||||
}
|
||||
|
||||
auto target_nsec = static_cast<uint64_t>(deadline.tv_nsec) + nanos;
|
||||
deadline.tv_sec += static_cast<time_t>(target_nsec / NANOS_PER_SEC);
|
||||
deadline.tv_nsec = static_cast<long>(target_nsec % NANOS_PER_SEC);
|
||||
|
||||
if (nanos <= SPIN_LIMIT_NS) {
|
||||
timespec now {};
|
||||
do {
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
|
||||
return;
|
||||
}
|
||||
} while (now.tv_sec < deadline.tv_sec ||
|
||||
(now.tv_sec == deadline.tv_sec && now.tv_nsec < deadline.tv_nsec));
|
||||
return;
|
||||
}
|
||||
|
||||
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, nullptr) == EINTR) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Common {
|
||||
|
||||
using thread_id_t = std::thread::id;
|
||||
@@ -249,6 +292,8 @@ void Thread::Sleep(uint32_t millis) {
|
||||
void Thread::SleepMicro(uint32_t micros) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
SleepHighResolution100ns(static_cast<uint64_t>(micros) * 10);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(static_cast<uint64_t>(micros) * 1000);
|
||||
#else
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(micros));
|
||||
#endif
|
||||
@@ -257,6 +302,8 @@ void Thread::SleepMicro(uint32_t micros) {
|
||||
void Thread::SleepNano(uint64_t nanos) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
SleepHighResolution100ns((nanos + 99) / 100);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(nanos);
|
||||
#else
|
||||
std::this_thread::sleep_for(std::chrono::nanoseconds(nanos));
|
||||
#endif
|
||||
|
||||
+6
-5
@@ -159,13 +159,14 @@ static void LoadElf(const std::filesystem::path& elf, bool dbg_print_reloc = fal
|
||||
}
|
||||
}
|
||||
|
||||
static void Execute() {
|
||||
static void Execute(const std::filesystem::path& game_patch) {
|
||||
auto patch_path = game_patch;
|
||||
Common::Thread guest_thread(
|
||||
[](void* /*unused*/) {
|
||||
[](void* param) {
|
||||
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||
rt->Execute();
|
||||
rt->Execute(*static_cast<const std::filesystem::path*>(param));
|
||||
},
|
||||
nullptr);
|
||||
&patch_path);
|
||||
Libs::Graphics::WindowRun();
|
||||
std::quick_exit(0);
|
||||
}
|
||||
@@ -207,7 +208,7 @@ void Run(const RunOptions& options) {
|
||||
|
||||
LoadElf(options.elf);
|
||||
|
||||
Execute();
|
||||
Execute(options.game_patch);
|
||||
}
|
||||
|
||||
} // namespace Emulator
|
||||
|
||||
@@ -12,6 +12,7 @@ struct RunOptions {
|
||||
Config::ConfigOptions config;
|
||||
std::filesystem::path app0_dir;
|
||||
std::filesystem::path elf;
|
||||
std::filesystem::path game_patch;
|
||||
};
|
||||
|
||||
void Run(const RunOptions& options);
|
||||
|
||||
@@ -19,6 +19,20 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <execinfo.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
@@ -28,16 +42,58 @@ constexpr uint64_t PAGE_SIZE = TRACKER_PAGE_SIZE;
|
||||
constexpr uint64_t REGION_SIZE = TRACKER_REGION_SIZE;
|
||||
constexpr uint64_t ADDRESS_SIZE = TRACKER_ADDRESS_SIZE;
|
||||
constexpr uint64_t REGION_COUNT = ADDRESS_SIZE / REGION_SIZE;
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// The tracker reuses Win32 memory-protection tags as internal page-state values (on
|
||||
// Windows they come from <windows.h> and are what VirtualQuery returns). Mirror the
|
||||
// canonical Win32 numeric values so the shared state-machine logic is identical.
|
||||
constexpr uint32_t PAGE_NOACCESS = 0x01;
|
||||
constexpr uint32_t PAGE_READONLY = 0x02;
|
||||
constexpr uint32_t PAGE_READWRITE = 0x04;
|
||||
#endif
|
||||
constexpr uint64_t REGION_PAGES = REGION_SIZE / PAGE_SIZE;
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS;
|
||||
constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY;
|
||||
constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS;
|
||||
constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY;
|
||||
constexpr uint32_t READ_WRITE_PROTECTION = PAGE_READWRITE;
|
||||
#else
|
||||
constexpr uint32_t NO_ACCESS_PROTECTION = 0;
|
||||
constexpr uint32_t READ_ONLY_PROTECTION = 1;
|
||||
constexpr uint32_t READ_WRITE_PROTECTION = 2;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Map the tracker's Win32-style protection tags to POSIX mprotect flags.
|
||||
static int PageProtToPosix(uint32_t protection) {
|
||||
switch (protection) {
|
||||
case PAGE_NOACCESS: return PROT_NONE;
|
||||
case PAGE_READONLY: return PROT_READ;
|
||||
case PAGE_READWRITE: return PROT_READ | PROT_WRITE;
|
||||
default: return PROT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// Query the current protection of the page containing vaddr via the Mach VM map and
|
||||
// collapse it to the tracker's read/write tags (execute is irrelevant to write tracking).
|
||||
static uint32_t MachQueryPageProt(uint64_t vaddr) {
|
||||
auto region_addr = static_cast<mach_vm_address_t>(vaddr);
|
||||
mach_vm_size_t region_size = 0;
|
||||
vm_region_basic_info_data_64_t info {};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object_name = MACH_PORT_NULL;
|
||||
|
||||
kern_return_t kr =
|
||||
mach_vm_region(mach_task_self(), ®ion_addr, ®ion_size, VM_REGION_BASIC_INFO_64,
|
||||
reinterpret_cast<vm_region_info_t>(&info), &count, &object_name);
|
||||
if (kr != KERN_SUCCESS || region_addr > vaddr) {
|
||||
return PAGE_NOACCESS; // no region covering vaddr
|
||||
}
|
||||
if ((info.protection & VM_PROT_WRITE) != 0) {
|
||||
return PAGE_READWRITE;
|
||||
}
|
||||
if ((info.protection & VM_PROT_READ) != 0) {
|
||||
return PAGE_READONLY;
|
||||
}
|
||||
return PAGE_NOACCESS;
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
// Zero is the unknown protection sentinel.
|
||||
constexpr uint32_t UNKNOWN_PROTECTION = 0;
|
||||
#endif
|
||||
|
||||
thread_local bool g_in_fault_resolution = false;
|
||||
@@ -56,6 +112,10 @@ thread_local bool g_in_fault_resolution = false;
|
||||
std::fprintf(stderr, " frame[%u]=0x%016" PRIxPTR " image_rva=0x%016" PRIxPTR "\n", i,
|
||||
address, address >= image_base ? address - image_base : 0);
|
||||
}
|
||||
#elif !defined(__APPLE__)
|
||||
void* frames[16] {};
|
||||
const int frame_count = ::backtrace(frames, static_cast<int>(std::size(frames)));
|
||||
::backtrace_symbols_fd(frames, frame_count, STDERR_FILENO);
|
||||
#endif
|
||||
std::fflush(stderr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
@@ -78,11 +138,136 @@ thread_local bool g_in_fault_resolution = false;
|
||||
uint32_t CurrentThread() noexcept {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return GetCurrentThreadId();
|
||||
#elif defined(__APPLE__)
|
||||
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
|
||||
#elif defined(__linux__)
|
||||
static thread_local const uint32_t tid = [] {
|
||||
const auto raw = static_cast<uint32_t>(::syscall(SYS_gettid));
|
||||
if (raw == 0) {
|
||||
FailFast("gettid returned the reserved zero owner token");
|
||||
}
|
||||
return raw;
|
||||
}();
|
||||
return tid;
|
||||
#else
|
||||
FailFast();
|
||||
FailFast("page tracking thread identity is unsupported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
int ToHostProtection(uint32_t protection) {
|
||||
switch (protection) {
|
||||
case NO_ACCESS_PROTECTION: return PROT_NONE;
|
||||
case READ_ONLY_PROTECTION: return PROT_READ;
|
||||
case READ_WRITE_PROTECTION: return PROT_READ | PROT_WRITE;
|
||||
default: Fatal("unmappable protection 0x%08" PRIx32, protection);
|
||||
}
|
||||
}
|
||||
|
||||
// Async-signal-safe lookup in the address-ordered /proc/self/maps.
|
||||
uint32_t QueryHostProtection(uint64_t vaddr) noexcept {
|
||||
int fd = ::open("/proc/self/maps", O_RDONLY | O_CLOEXEC); // NOLINT
|
||||
if (fd < 0) {
|
||||
return UNKNOWN_PROTECTION;
|
||||
}
|
||||
|
||||
enum class Field { Start, End, Perms, Rest };
|
||||
|
||||
uint32_t result = UNKNOWN_PROTECTION;
|
||||
auto field = Field::Start;
|
||||
uint64_t start = 0;
|
||||
uint64_t end = 0;
|
||||
char perms[4] = {};
|
||||
uint32_t perms_len = 0;
|
||||
bool line_valid = true;
|
||||
|
||||
char buffer[8192];
|
||||
|
||||
for (bool done = false; !done;) {
|
||||
const auto got = ::read(fd, buffer, sizeof(buffer));
|
||||
if (got < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (got == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (ssize_t i = 0; i < got && !done; i++) {
|
||||
const char c = buffer[i];
|
||||
|
||||
if (c == '\n') {
|
||||
field = Field::Start;
|
||||
start = 0;
|
||||
end = 0;
|
||||
perms_len = 0;
|
||||
line_valid = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line_valid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (field) {
|
||||
case Field::Start:
|
||||
case Field::End: {
|
||||
uint64_t digit = 0;
|
||||
if (c >= '0' && c <= '9') {
|
||||
digit = static_cast<uint64_t>(c - '0');
|
||||
} else if (c >= 'a' && c <= 'f') {
|
||||
digit = static_cast<uint64_t>(c - 'a') + 10;
|
||||
} else if (c == '-' && field == Field::Start) {
|
||||
field = Field::End;
|
||||
break;
|
||||
} else if (c == ' ' && field == Field::End) {
|
||||
field = Field::Perms;
|
||||
perms_len = 0;
|
||||
break;
|
||||
} else {
|
||||
line_valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
auto& value = (field == Field::Start ? start : end);
|
||||
value = (value << 4u) | digit;
|
||||
break;
|
||||
}
|
||||
|
||||
case Field::Perms: {
|
||||
if (c != ' ') {
|
||||
if (perms_len < sizeof(perms)) {
|
||||
perms[perms_len] = c;
|
||||
}
|
||||
perms_len++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (vaddr < start) {
|
||||
done = true;
|
||||
} else if (vaddr < end && perms_len >= 2) {
|
||||
result = perms[1] == 'w' ? READ_WRITE_PROTECTION
|
||||
: perms[0] == 'r' ? READ_ONLY_PROTECTION
|
||||
: NO_ACCESS_PROTECTION;
|
||||
done = true;
|
||||
} else {
|
||||
field = Field::Rest;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Field::Rest: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::close(fd);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
class SpinGuard final {
|
||||
public:
|
||||
explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) {
|
||||
@@ -124,6 +309,10 @@ struct PageManager::Impl {
|
||||
uint32_t access_watchers = 0;
|
||||
uint32_t original_protection = 0;
|
||||
uint32_t backing_writer = 0;
|
||||
#if defined(__linux__)
|
||||
// Shadow the protection applied through Protect().
|
||||
uint32_t current_protection = UNKNOWN_PROTECTION;
|
||||
#endif
|
||||
bool resolving = false;
|
||||
bool resolving_read_write = false;
|
||||
bool late_read_pending = false;
|
||||
@@ -145,8 +334,17 @@ struct PageManager::Impl {
|
||||
Fatal("unsupported host page size 0x%08" PRIx32,
|
||||
static_cast<uint32_t>(info.dwPageSize));
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// Under Rosetta the host page size is 4 KB, matching TRACKER_PAGE_SIZE.
|
||||
if (static_cast<uint64_t>(getpagesize()) != PAGE_SIZE) {
|
||||
Fatal("unsupported host page size 0x%08" PRIx32,
|
||||
static_cast<uint32_t>(getpagesize()));
|
||||
}
|
||||
#else
|
||||
Fatal("page-fault invalidation is not implemented on this platform");
|
||||
const auto host_page_size = ::sysconf(_SC_PAGESIZE);
|
||||
if (host_page_size < 0 || static_cast<uint64_t>(host_page_size) != PAGE_SIZE) {
|
||||
Fatal("unsupported host page size %ld", static_cast<long>(host_page_size));
|
||||
}
|
||||
#endif
|
||||
regions = std::make_unique<std::atomic<Region*>[]>(REGION_COUNT);
|
||||
for (uint64_t i = 0; i < REGION_COUNT; i++) {
|
||||
@@ -214,7 +412,7 @@ struct PageManager::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t QueryProtection(uint64_t vaddr) {
|
||||
static uint32_t QueryProtection([[maybe_unused]] PageState& page, uint64_t vaddr) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MEMORY_BASIC_INFORMATION info {};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(static_cast<uintptr_t>(vaddr)), &info,
|
||||
@@ -225,13 +423,28 @@ struct PageManager::Impl {
|
||||
vaddr, static_cast<uint32_t>(info.State), static_cast<uint32_t>(info.Protect));
|
||||
}
|
||||
return info.Protect;
|
||||
#elif defined(__APPLE__)
|
||||
const uint32_t protection = MachQueryPageProt(vaddr);
|
||||
if (protection != PAGE_READWRITE) {
|
||||
Fatal("basic path requires PAGE_READWRITE at 0x%016" PRIx64 " (protection=0x%08" PRIx32
|
||||
")",
|
||||
vaddr, protection);
|
||||
}
|
||||
return protection;
|
||||
#else
|
||||
(void)vaddr;
|
||||
Fatal("page query is unsupported on this platform");
|
||||
const auto host_protection = QueryHostProtection(vaddr);
|
||||
if (host_protection != READ_WRITE_PROTECTION) {
|
||||
Fatal("basic path requires a read/write mapping at 0x%016" PRIx64
|
||||
" (protection=0x%08" PRIx32 ")",
|
||||
vaddr, host_protection);
|
||||
}
|
||||
page.current_protection = host_protection;
|
||||
return host_protection;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool AllowsAccess(uint64_t vaddr, PageFaultAccess access) noexcept {
|
||||
static bool AllowsAccess([[maybe_unused]] const PageState& page, uint64_t vaddr,
|
||||
PageFaultAccess access) noexcept {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MEMORY_BASIC_INFORMATION info {};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(static_cast<uintptr_t>(vaddr)), &info,
|
||||
@@ -245,14 +458,33 @@ struct PageManager::Impl {
|
||||
case PageFaultAccess::Write: return info.Protect == PAGE_READWRITE;
|
||||
default: return false;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
const uint32_t protection = MachQueryPageProt(vaddr);
|
||||
switch (access) {
|
||||
case PageFaultAccess::Read:
|
||||
return protection == PAGE_READONLY || protection == PAGE_READWRITE;
|
||||
case PageFaultAccess::Write: return protection == PAGE_READWRITE;
|
||||
default: return false;
|
||||
}
|
||||
#else
|
||||
(void)vaddr;
|
||||
return false;
|
||||
const auto permitted = [](uint32_t protection, PageFaultAccess wanted) {
|
||||
switch (wanted) {
|
||||
case PageFaultAccess::Read:
|
||||
return protection == READ_ONLY_PROTECTION || protection == READ_WRITE_PROTECTION;
|
||||
case PageFaultAccess::Write: return protection == READ_WRITE_PROTECTION;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!permitted(page.current_protection, access)) {
|
||||
return false;
|
||||
}
|
||||
return permitted(QueryHostProtection(vaddr), access);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void Protect(uint64_t vaddr, uint32_t protection, uint32_t expected_old,
|
||||
bool fault_path) noexcept {
|
||||
static void Protect([[maybe_unused]] PageState& page, uint64_t vaddr, uint32_t protection,
|
||||
uint32_t expected_old, bool fault_path) noexcept {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
DWORD old_protection = 0;
|
||||
if (VirtualProtect(reinterpret_cast<void*>(static_cast<uintptr_t>(vaddr)), PAGE_SIZE,
|
||||
@@ -265,11 +497,37 @@ struct PageManager::Impl {
|
||||
", expected=0x%08" PRIx32 ", new=0x%08" PRIx32,
|
||||
vaddr, static_cast<uint32_t>(old_protection), expected_old, protection);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// mprotect cannot report the previous protection, so the expected_old comparison
|
||||
// is dropped; the tracker is the sole mutator of these pages and drives the
|
||||
// transition from its own shadow state.
|
||||
(void)expected_old;
|
||||
if (mprotect(reinterpret_cast<void*>(static_cast<uintptr_t>(vaddr)), PAGE_SIZE,
|
||||
PageProtToPosix(protection)) != 0) {
|
||||
if (fault_path) {
|
||||
FailFast("mprotect fault transition failed");
|
||||
}
|
||||
Fatal("mprotect failed at 0x%016" PRIx64 ", new=0x%08" PRIx32, vaddr, protection);
|
||||
}
|
||||
#else
|
||||
(void)vaddr;
|
||||
(void)protection;
|
||||
(void)fault_path;
|
||||
FailFast("page protection is unsupported on this platform");
|
||||
if (page.current_protection != UNKNOWN_PROTECTION &&
|
||||
page.current_protection != expected_old) {
|
||||
if (fault_path) {
|
||||
FailFast("mprotect fault transition did not match expected protection");
|
||||
}
|
||||
Fatal("invalid protection transition at 0x%016" PRIx64 ", old=0x%08" PRIx32
|
||||
", expected=0x%08" PRIx32 ", new=0x%08" PRIx32,
|
||||
vaddr, page.current_protection, expected_old, protection);
|
||||
}
|
||||
if (::mprotect(reinterpret_cast<void*>(static_cast<uintptr_t>(vaddr)), PAGE_SIZE,
|
||||
ToHostProtection(protection)) != 0) {
|
||||
if (fault_path) {
|
||||
FailFast("mprotect failed on the fault path");
|
||||
}
|
||||
Fatal("mprotect failed at 0x%016" PRIx64 ", new=0x%08" PRIx32 " (%s)", vaddr,
|
||||
protection, std::strerror(errno));
|
||||
}
|
||||
page.current_protection = protection;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -399,13 +657,13 @@ void PageManager::UpdatePageWatchers(bool track, uint64_t vaddr, uint64_t size,
|
||||
}
|
||||
const bool first_watcher = page.write_watchers == 0 && page.access_watchers == 0;
|
||||
if (first_watcher) {
|
||||
page.original_protection = Impl::QueryProtection(page_vaddr);
|
||||
page.original_protection = Impl::QueryProtection(page, page_vaddr);
|
||||
}
|
||||
const auto old_protection = Impl::WatcherProtection(page);
|
||||
watchers++;
|
||||
const auto new_protection = Impl::WatcherProtection(page);
|
||||
if (new_protection != old_protection) {
|
||||
Impl::Protect(page_vaddr, new_protection, old_protection, false);
|
||||
Impl::Protect(page, page_vaddr, new_protection, old_protection, false);
|
||||
}
|
||||
switch (new_protection) {
|
||||
case NO_ACCESS_PROTECTION:
|
||||
@@ -426,7 +684,7 @@ void PageManager::UpdatePageWatchers(bool track, uint64_t vaddr, uint64_t size,
|
||||
watchers--;
|
||||
const auto new_protection = Impl::WatcherProtection(page);
|
||||
if (page.backing_writer == 0 && new_protection != old_protection) {
|
||||
Impl::Protect(page_vaddr, new_protection, old_protection, false);
|
||||
Impl::Protect(page, page_vaddr, new_protection, old_protection, false);
|
||||
}
|
||||
if (page.backing_writer == 0) {
|
||||
Impl::PublishDelayedFaults(page, old_protection, new_protection);
|
||||
@@ -459,6 +717,12 @@ void PageManager::OnGpuMap(uint64_t vaddr, uint64_t size, GpuAccess access) {
|
||||
page.mappings++;
|
||||
page.gpu_read_mappings += gpu_read ? 1u : 0u;
|
||||
page.gpu_write_mappings += gpu_write ? 1u : 0u;
|
||||
#if defined(__linux__)
|
||||
// New guest mappings start read/write.
|
||||
if (page.current_protection == UNKNOWN_PROTECTION) {
|
||||
page.current_protection = READ_WRITE_PROTECTION;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +844,7 @@ void PageManager::EndBackingWrite(uint64_t vaddr, uint64_t size) noexcept {
|
||||
const auto old_protection = NO_ACCESS_PROTECTION;
|
||||
const auto new_protection = Impl::WatcherProtection(page);
|
||||
if (new_protection != old_protection) {
|
||||
Impl::Protect(address, new_protection, old_protection, false);
|
||||
Impl::Protect(page, address, new_protection, old_protection, false);
|
||||
}
|
||||
Impl::PublishDelayedFaults(page, old_protection, new_protection);
|
||||
if (page.write_watchers == 0 && page.access_watchers == 0) {
|
||||
@@ -605,12 +869,12 @@ bool PageManager::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noex
|
||||
while (true) {
|
||||
SpinGuard lock(page.lock);
|
||||
if (access == PageFaultAccess::Read && page.late_read_pending &&
|
||||
Impl::AllowsAccess(fault_vaddr, access)) {
|
||||
Impl::AllowsAccess(page, fault_vaddr, access)) {
|
||||
page.late_read_pending = false;
|
||||
return true;
|
||||
}
|
||||
if (access == PageFaultAccess::Write && page.late_write_pending &&
|
||||
Impl::AllowsAccess(fault_vaddr, access)) {
|
||||
Impl::AllowsAccess(page, fault_vaddr, access)) {
|
||||
page.late_write_pending = false;
|
||||
return true;
|
||||
}
|
||||
@@ -632,7 +896,7 @@ bool PageManager::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noex
|
||||
}
|
||||
bool& pending = (access == PageFaultAccess::Read ? page.late_read_pending
|
||||
: page.late_write_pending);
|
||||
const bool allowed = Impl::AllowsAccess(fault_vaddr, access);
|
||||
const bool allowed = Impl::AllowsAccess(page, fault_vaddr, access);
|
||||
pending = false;
|
||||
if (waited && !allowed) {
|
||||
FailFast("page remained inaccessible after waiting for its resolver");
|
||||
@@ -681,12 +945,12 @@ bool PageManager::HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noex
|
||||
page.write_watchers = 0;
|
||||
}
|
||||
const auto restored_protection = Impl::WatcherProtection(page);
|
||||
Impl::Protect(PageStart(fault_vaddr), restored_protection, old_protection, true);
|
||||
Impl::Protect(page, PageStart(fault_vaddr), restored_protection, old_protection, true);
|
||||
if (page.write_watchers == 0) {
|
||||
page.original_protection = 0;
|
||||
}
|
||||
Impl::PublishDelayedFaults(page, old_protection, restored_protection);
|
||||
} else if (!Impl::AllowsAccess(fault_vaddr, access)) {
|
||||
} else if (!Impl::AllowsAccess(page, fault_vaddr, access)) {
|
||||
FailFast("fault completion left the page inaccessible");
|
||||
}
|
||||
page.resolving = false;
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#elif defined(__APPLE__)
|
||||
#include <pthread.h>
|
||||
#elif defined(__linux__)
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
@@ -49,6 +54,12 @@ private:
|
||||
static uint32_t CurrentThread() noexcept {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return GetCurrentThreadId();
|
||||
#elif defined(__APPLE__)
|
||||
// mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel).
|
||||
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
|
||||
#elif defined(__linux__)
|
||||
static thread_local const uint32_t tid = static_cast<uint32_t>(::syscall(SYS_gettid));
|
||||
return tid;
|
||||
#else
|
||||
EXIT("region tracking thread identity is unsupported on this platform\n");
|
||||
#endif
|
||||
|
||||
@@ -395,6 +395,10 @@ static void SetDynamicParams(const RenderCommandBuffer& buffer, vk::CommandBuffe
|
||||
dynamic_params.stencil_back.reference);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// MoltenVK has no VK_EXT_color_write_enable; the pipeline is created without the
|
||||
// eColorWriteEnableEXT dynamic state and relies on the static colorWriteMask instead.
|
||||
#else
|
||||
vk::Bool32 enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
for (uint32_t i = 0; i < dynamic_params.color_write_count; i++) {
|
||||
enable[i] = (dynamic_params.color_write_enable[i] ? VK_TRUE : VK_FALSE);
|
||||
@@ -402,6 +406,7 @@ static void SetDynamicParams(const RenderCommandBuffer& buffer, vk::CommandBuffe
|
||||
if (dynamic_params.color_write_count != 0) {
|
||||
vk_buffer.setColorWriteEnableEXT(dynamic_params.color_write_count, enable);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
|
||||
|
||||
@@ -729,7 +729,13 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
|
||||
|
||||
vk::PipelineRasterizationStateCreateInfo rasterizer {};
|
||||
rasterizer.sType = vk::StructureType::ePipelineRasterizationStateCreateInfo;
|
||||
// MoltenVK lacks VK_EXT_depth_clip_enable; omit the depth-clip struct on macOS and accept
|
||||
// Vulkan's default depth clipping (enabled) instead of the PS5's clamp behavior.
|
||||
#if defined(__APPLE__)
|
||||
rasterizer.pNext = nullptr;
|
||||
#else
|
||||
rasterizer.pNext = &clip_ext;
|
||||
#endif
|
||||
rasterizer.flags = {};
|
||||
rasterizer.depthClampEnable = VK_FALSE;
|
||||
rasterizer.rasterizerDiscardEnable = VK_FALSE;
|
||||
@@ -807,7 +813,13 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
|
||||
|
||||
vk::PipelineColorBlendStateCreateInfo color_blending {};
|
||||
color_blending.sType = vk::StructureType::ePipelineColorBlendStateCreateInfo;
|
||||
// MoltenVK lacks VK_EXT_color_write_enable; drop the dynamic color-write struct on macOS
|
||||
// and rely on each attachment's static colorWriteMask (all channels enabled by default).
|
||||
#if defined(__APPLE__)
|
||||
color_blending.pNext = nullptr;
|
||||
#else
|
||||
color_blending.pNext = &color_write;
|
||||
#endif
|
||||
color_blending.flags = {};
|
||||
color_blending.logicOpEnable = VK_FALSE;
|
||||
color_blending.logicOp = vk::LogicOp::eCopy;
|
||||
@@ -873,7 +885,11 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
|
||||
depth_stencil_info.depthWriteEnable = (static_params.depth_write_enable ? VK_TRUE : VK_FALSE);
|
||||
depth_stencil_info.depthCompareOp = static_params.depth_compare_op;
|
||||
depth_stencil_info.depthBoundsTestEnable =
|
||||
#if defined(__APPLE__)
|
||||
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
|
||||
#else
|
||||
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
#endif
|
||||
depth_stencil_info.stencilTestEnable = (static_params.stencil_test_enable ? VK_TRUE : VK_FALSE);
|
||||
depth_stencil_info.front.failOp = static_params.stencil_front.failOp;
|
||||
depth_stencil_info.front.passOp = static_params.stencil_front.passOp;
|
||||
@@ -893,7 +909,9 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
|
||||
vk::DynamicState::eStencilCompareMask,
|
||||
vk::DynamicState::eStencilReference,
|
||||
vk::DynamicState::eStencilWriteMask,
|
||||
vk::DynamicState::eColorWriteEnableEXT,
|
||||
#if !defined(__APPLE__)
|
||||
vk::DynamicState::eColorWriteEnableEXT, // unsupported by MoltenVK; static mask instead
|
||||
#endif
|
||||
};
|
||||
const auto dynamic_states_count =
|
||||
static_cast<uint32_t>(sizeof(dynamic_states) / sizeof(dynamic_states[0]));
|
||||
|
||||
@@ -19,12 +19,16 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
|
||||
// RenderDoc uses Windows-style names in its cross-platform API.
|
||||
#define __cdecl
|
||||
using HMODULE = void*;
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
using RenderDocDevicePointer = void*;
|
||||
using RenderDocWindowHandle = void*;
|
||||
|
||||
@@ -107,6 +111,8 @@ static RenderDocDevicePointer GetRenderDocDevicePointer(vk::Instance instance) {
|
||||
return VulkanHandleToPointer(instance);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static bool BindRenderDocApi(HMODULE module) {
|
||||
auto* get_api = reinterpret_cast<pRENDERDOC_GetAPI>(GetProcAddress(module, "RENDERDOC_GetAPI"));
|
||||
if (get_api == nullptr) {
|
||||
@@ -147,10 +153,84 @@ static RenderDocWindowHandle GetRenderDocWindowHandle(SDL_Window* window) {
|
||||
return info.info.win.window;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static bool BindRenderDocApi(HMODULE module) {
|
||||
auto* get_api = reinterpret_cast<pRENDERDOC_GetAPI>(::dlsym(module, "RENDERDOC_GetAPI"));
|
||||
if (get_api == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void* api = nullptr;
|
||||
if (get_api(eRENDERDOC_API_Version_1_4_2, &api) == 0 || api == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_module = module;
|
||||
g_api = static_cast<RenderDocApi*>(api);
|
||||
|
||||
g_api->SetCaptureFilePathTemplate("_RenderDoc/kyty");
|
||||
static RenderDocInputButton capture_keys[] = {eRENDERDOC_Key_F1};
|
||||
g_api->SetCaptureKeys(capture_keys, 1);
|
||||
g_api->UnloadCrashHandler();
|
||||
|
||||
Dl_info info {};
|
||||
if (::dladdr(reinterpret_cast<void*>(get_api), &info) != 0 && info.dli_fname != nullptr) {
|
||||
LOGF("RenderDoc: bound API from %s\n", info.dli_fname);
|
||||
} else {
|
||||
LOGF("RenderDoc: bound API\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static RenderDocWindowHandle GetRenderDocWindowHandle(SDL_Window* window) {
|
||||
if (window == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_SysWMinfo info {};
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#if defined(SDL_VIDEO_DRIVER_X11)
|
||||
if (info.subsystem == SDL_SYSWM_X11) {
|
||||
// RenderDoc takes the raw xlib Window id in the pointer slot, not a Display*.
|
||||
return reinterpret_cast<RenderDocWindowHandle>(
|
||||
static_cast<uintptr_t>(info.info.x11.window));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Wayland capture works without an active-window handle.
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true)) {
|
||||
LOGF("RenderDoc: no native window handle for SDL subsystem %d (Wayland?); the in-app "
|
||||
"overlay is unavailable, but --rd captures still work\n",
|
||||
static_cast<int>(info.subsystem));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static bool IsAvailable() {
|
||||
return g_api != nullptr && g_device != nullptr && g_window != nullptr;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static bool IsAvailable() {
|
||||
return g_api != nullptr && g_device != nullptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
void RenderDocInit() {
|
||||
bool expected = false;
|
||||
if (!g_init_done.compare_exchange_strong(expected, true)) {
|
||||
@@ -187,6 +267,33 @@ void RenderDocInit() {
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void RenderDocInit() {
|
||||
bool expected = false;
|
||||
if (!g_init_done.compare_exchange_strong(expected, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer an injected RenderDoc instance.
|
||||
auto* module = ::dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (module == nullptr) {
|
||||
module = ::dlopen("librenderdoc.so", RTLD_NOW);
|
||||
}
|
||||
if (module == nullptr) {
|
||||
LOGF("RenderDoc: librenderdoc.so was not found; in-app capture disabled\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!BindRenderDocApi(module)) {
|
||||
LOGF("RenderDoc: API 1.4.2 is not available; in-app capture disabled\n");
|
||||
::dlclose(module);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void RenderDocSetActiveWindow(vk::Instance instance, SDL_Window* window) {
|
||||
if (g_api == nullptr) {
|
||||
return;
|
||||
@@ -274,13 +381,4 @@ void RenderDocOnPresent() {
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void RenderDocInit() {}
|
||||
void RenderDocSetActiveWindow(vk::Instance /*instance*/, SDL_Window* /*window*/) {}
|
||||
void RenderDocRequestCapture() {}
|
||||
void RenderDocOnPresent() {}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -586,7 +586,13 @@ void Swapchain::RefreshSurfaceSize() {
|
||||
void Swapchain::Recreate(bool surface_lost) {
|
||||
Destroy();
|
||||
if (surface_lost) {
|
||||
#if defined(__APPLE__)
|
||||
// Surface recreation goes through SDL_Vulkan_CreateSurface, which touches the
|
||||
// window's view/layer and must run on the main thread on macOS.
|
||||
m_window.RunOnMainThread([this] { m_window.RecreateSurface(); }, true);
|
||||
#else
|
||||
m_window.RecreateSurface();
|
||||
#endif
|
||||
}
|
||||
RefreshSurfaceSize();
|
||||
Create();
|
||||
@@ -797,9 +803,20 @@ void Presenter::Present(Frame& frame, bool reuse) {
|
||||
|
||||
auto& window = m_impl->window;
|
||||
if (window.window_hidden) {
|
||||
#if defined(__APPLE__)
|
||||
// AppKit traps if a window is shown off the main thread; marshal and wait so the
|
||||
// swapchain below is recreated against a visible window.
|
||||
window.RunOnMainThread(
|
||||
[&window] {
|
||||
window.UpdateIcon();
|
||||
SDL_ShowWindow(window.window);
|
||||
},
|
||||
true);
|
||||
#else
|
||||
window.UpdateIcon();
|
||||
|
||||
SDL_ShowWindow(window.window);
|
||||
#endif
|
||||
|
||||
window.window_hidden = false;
|
||||
m_impl->RecoverSwapchain(Swapchain::Status::Recreate);
|
||||
|
||||
@@ -217,7 +217,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
|
||||
if (color_write_ext.colorWriteEnable != VK_TRUE) {
|
||||
LOGF("colorWriteEnable is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (depth_clip_control.depthClipControl != VK_TRUE) {
|
||||
@@ -226,7 +228,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
}
|
||||
if (depth_clip_enable.depthClipEnable != VK_TRUE) {
|
||||
LOGF("depthClipEnable is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (features12.samplerMirrorClampToEdge != VK_TRUE) {
|
||||
@@ -271,7 +275,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
}
|
||||
if (device_features2.features.depthBounds != VK_TRUE) {
|
||||
LOGF("depthBounds is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
#endif
|
||||
}
|
||||
if (device_features2.features.shaderStorageImageWriteWithoutFormat != VK_TRUE) {
|
||||
LOGF("shaderStorageImageWriteWithoutFormat is not supported\n");
|
||||
@@ -494,7 +500,14 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
|
||||
|
||||
vk::PhysicalDeviceDepthClipControlFeaturesEXT depth_clip_control {};
|
||||
depth_clip_control.sType = vk::StructureType::ePhysicalDeviceDepthClipControlFeaturesEXT;
|
||||
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable, so drop those
|
||||
// feature structs from the chain on macOS (the renderer falls back to default depth
|
||||
// clipping and static color-write masks).
|
||||
#if defined(__APPLE__)
|
||||
depth_clip_control.pNext = nullptr;
|
||||
#else
|
||||
depth_clip_control.pNext = &depth_clip_enable;
|
||||
#endif
|
||||
depth_clip_control.depthClipControl = VK_TRUE;
|
||||
|
||||
vk::PhysicalDeviceVulkan12Features features12 {};
|
||||
@@ -541,7 +554,9 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
|
||||
device_features.fragmentStoresAndAtomics = VK_TRUE;
|
||||
device_features.samplerAnisotropy = VK_TRUE;
|
||||
device_features.robustBufferAccess = VK_TRUE;
|
||||
device_features.depthBounds = VK_TRUE;
|
||||
#if !defined(__APPLE__)
|
||||
device_features.depthBounds = VK_TRUE; // unsupported by MoltenVK
|
||||
#endif
|
||||
device_features.shaderStorageImageWriteWithoutFormat = VK_TRUE;
|
||||
device_features.shaderStorageImageReadWithoutFormat = VK_TRUE;
|
||||
device_features.shaderImageGatherExtended = VK_TRUE;
|
||||
@@ -894,10 +909,20 @@ void WindowContext::CreateVulkan() {
|
||||
}
|
||||
surface = native_surface;
|
||||
|
||||
std::vector<const char*> device_extensions = {
|
||||
VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME,
|
||||
VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME, VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME,
|
||||
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, "VK_KHR_maintenance1"};
|
||||
std::vector<const char*> device_extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME,
|
||||
VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME,
|
||||
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
|
||||
"VK_KHR_maintenance1"};
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable; the renderer
|
||||
// falls back to default depth clipping and static color-write masks on macOS. It also
|
||||
// requires VK_KHR_portability_subset per the Vulkan portability spec.
|
||||
device_extensions.push_back("VK_KHR_portability_subset");
|
||||
#else
|
||||
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
|
||||
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
|
||||
#endif
|
||||
|
||||
#ifdef KYTY_ENABLE_DEBUG_PRINTF
|
||||
if (Config::SpirvDebugPrintfEnabled()) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "graphics/presentation/window.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "SDL.h"
|
||||
#include "SDL_error.h"
|
||||
#include "SDL_events.h"
|
||||
@@ -697,6 +699,52 @@ void WindowContext::ProcessEvent(double time_s) {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
void WindowContext::RunOnMainThread(std::function<void()> task, bool wait) {
|
||||
if (Common::Thread::IsMainThread()) {
|
||||
task();
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t ticket = 0;
|
||||
{
|
||||
Common::LockGuard lock(main_task_mutex);
|
||||
main_tasks.push_back(std::move(task));
|
||||
ticket = ++main_tasks_queued;
|
||||
}
|
||||
|
||||
// Wake the main loop in case it is blocked in SDL_WaitEvent.
|
||||
SDL_Event event {};
|
||||
event.type = SDL_USEREVENT;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
if (!wait) {
|
||||
return;
|
||||
}
|
||||
Common::LockGuard lock(main_task_mutex);
|
||||
while (main_tasks_run < ticket) {
|
||||
main_task_done.Wait(&main_task_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowContext::DrainMainThreadTasks() {
|
||||
std::vector<std::function<void()>> tasks;
|
||||
{
|
||||
Common::LockGuard lock(main_task_mutex);
|
||||
tasks.swap(main_tasks);
|
||||
}
|
||||
if (tasks.empty()) {
|
||||
return;
|
||||
}
|
||||
for (auto& task: tasks) {
|
||||
task();
|
||||
}
|
||||
Common::LockGuard lock(main_task_mutex);
|
||||
main_tasks_run += tasks.size();
|
||||
main_task_done.SignalAll();
|
||||
}
|
||||
#endif
|
||||
|
||||
void WindowContext::Run() {
|
||||
Common::Timer timer;
|
||||
timer.Start();
|
||||
@@ -706,6 +754,9 @@ void WindowContext::Run() {
|
||||
loop.paused.store(false, std::memory_order_release);
|
||||
|
||||
while (!loop.need_exit) {
|
||||
#if defined(__APPLE__)
|
||||
DrainMainThreadTasks();
|
||||
#endif
|
||||
if (SDL_PollEvent(&loop.event) != 0) {
|
||||
ProcessEvent(timer.GetTimeS());
|
||||
continue;
|
||||
@@ -747,9 +798,18 @@ static void WindowCreate(WindowContext& context) {
|
||||
|
||||
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
|
||||
|
||||
uint32_t window_flags = KYTY_SDL_WINDOW_FLAGS;
|
||||
#if defined(__APPLE__)
|
||||
// macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed
|
||||
// throwing NSExceptions under Rosetta during the first CATransaction commit. A
|
||||
// borderless window skips that machinery entirely.
|
||||
if (std::getenv("KYTY_BORDERLESS") != nullptr) {
|
||||
window_flags |= static_cast<uint32_t>(SDL_WINDOW_BORDERLESS);
|
||||
}
|
||||
#endif
|
||||
context.window =
|
||||
SDL_CreateWindow(KYTY_SDL_WINDOW_CAPTION, KYTY_SDL_WINDOWPOS_CENTERED,
|
||||
KYTY_SDL_WINDOWPOS_CENTERED, width, height, KYTY_SDL_WINDOW_FLAGS);
|
||||
KYTY_SDL_WINDOWPOS_CENTERED, width, height, window_flags);
|
||||
|
||||
context.window_hidden = true;
|
||||
|
||||
@@ -896,7 +956,13 @@ void WindowContext::UpdateTitle() {
|
||||
(has_app_ver ? " " : ""), device_name, processor_name,
|
||||
frame_num, current_fps);
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing.
|
||||
RunOnMainThread([this, fps = std::move(fps)] { SDL_SetWindowTitle(window, fps.c_str()); },
|
||||
false);
|
||||
#else
|
||||
SDL_SetWindowTitle(window, fps.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <functional>
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
class Presenter;
|
||||
@@ -47,6 +51,14 @@ struct WindowContext {
|
||||
void ProcessEvent(double time_seconds);
|
||||
void Run();
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// AppKit only allows window operations (show, icon, title, view/layer changes) on the
|
||||
// main thread; the present thread marshals them through the SDL main loop with these.
|
||||
// wait=true blocks until the task has run on the main thread.
|
||||
void RunOnMainThread(std::function<void()> task, bool wait);
|
||||
void DrainMainThreadTasks();
|
||||
#endif
|
||||
|
||||
GraphicContext graphic_ctx;
|
||||
SDL_Window* window = nullptr;
|
||||
bool window_hidden = true;
|
||||
@@ -60,6 +72,14 @@ struct WindowContext {
|
||||
char processor_name[64] = {0};
|
||||
|
||||
Common::Mutex mutex;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
Common::Mutex main_task_mutex;
|
||||
Common::CondVar main_task_done;
|
||||
std::vector<std::function<void()>> main_tasks; // guarded by main_task_mutex
|
||||
uint64_t main_tasks_queued = 0; // guarded by main_task_mutex
|
||||
uint64_t main_tasks_run = 0; // guarded by main_task_mutex
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::LibKernel::FileSystem {
|
||||
@@ -153,6 +154,11 @@ void FileDescriptors::DeleteDescriptor(int d) {
|
||||
EXIT_IF(m_files[index] == nullptr);
|
||||
EXIT_IF(m_files[index]->opened);
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// Close host files opened before a failed descriptor setup.
|
||||
m_files[index]->f.Close();
|
||||
#endif
|
||||
|
||||
delete m_files[index];
|
||||
m_files[index] = nullptr;
|
||||
}
|
||||
@@ -223,6 +229,52 @@ void MountPoints::Umount(const std::string& folder_or_point) {
|
||||
}
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// Resolve guest paths case-insensitively on case-sensitive hosts.
|
||||
static std::filesystem::path ResolvePathIgnoringCase(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Preserve unmatched components for the caller's ENOENT path.
|
||||
std::filesystem::path resolved = path.has_root_path() ? path.root_path() : std::filesystem::path(".");
|
||||
bool matched = true;
|
||||
|
||||
for (const auto& component: path.relative_path()) {
|
||||
if (component.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto candidate = resolved / component;
|
||||
if (!matched || std::filesystem::exists(candidate, ec)) {
|
||||
resolved = std::move(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (std::filesystem::directory_iterator entry(resolved, ec), end; entry != end;
|
||||
entry.increment(ec)) {
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
if (Common::EqualNoCase(entry->path().filename().string(), component.string())) {
|
||||
resolved = entry->path();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
resolved = std::move(candidate);
|
||||
matched = false;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_file_name) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
@@ -239,7 +291,11 @@ std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_fi
|
||||
while (Common::StartsWith(rel_path, '/')) {
|
||||
rel_path = Common::RemoveFirst(rel_path, 1);
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return p.dir / rel_path;
|
||||
#else
|
||||
return ResolvePathIgnoringCase(p.dir / rel_path);
|
||||
#endif
|
||||
}
|
||||
|
||||
return mounted_file_name;
|
||||
@@ -260,7 +316,11 @@ std::filesystem::path MountPoints::GetRealDirectory(const std::string& mounted_d
|
||||
while (Common::StartsWith(rel_path, '/')) {
|
||||
rel_path = Common::RemoveFirst(rel_path, 1);
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return p.dir / rel_path;
|
||||
#else
|
||||
return ResolvePathIgnoringCase(p.dir / rel_path);
|
||||
#endif
|
||||
}
|
||||
|
||||
return mounted_directory;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#endif
|
||||
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
@@ -241,6 +242,13 @@ static bool VirtualRangesOverlap(uint64_t left_start, uint64_t left_size, uint64
|
||||
static bool CommitFixedHostRange(uint64_t start, uint64_t size, VirtualMemory::Mode mode) {
|
||||
constexpr uint64_t PAGE_SIZE = 0x4000;
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// Prefer one syscall, then fall back to the per-page path.
|
||||
if (size > PAGE_SIZE && VirtualMemory::AllocFixed(start, size, mode)) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
for (uint64_t addr = start; addr < start + size; addr += PAGE_SIZE) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MEMORY_BASIC_INFORMATION info {};
|
||||
@@ -2991,6 +2999,13 @@ static bool ReserveFixedHostRange(uint64_t start, uint64_t size) {
|
||||
g_test_host_reservation_pages_before_failure = UINT32_MAX;
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// Prefer one syscall, then fall back to the per-page path.
|
||||
if (size > PAGE_SIZE && VirtualMemory::ReserveFixed(start, size)) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool host_mutated = false;
|
||||
for (uint64_t addr = start; addr < start + size; addr += PAGE_SIZE) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
@@ -54,9 +54,21 @@ public:
|
||||
m_handle = nullptr;
|
||||
}
|
||||
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
char shm_name[64];
|
||||
snprintf(shm_name, sizeof(shm_name), "/kyty-dm-%d-%llx", static_cast<int>(getpid()),
|
||||
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(this)));
|
||||
m_fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600);
|
||||
if (m_fd >= 0) {
|
||||
shm_unlink(shm_name);
|
||||
}
|
||||
}
|
||||
#else
|
||||
m_fd = static_cast<int>(syscall(SYS_memfd_create, "KytyDirectMemory", 0));
|
||||
#endif
|
||||
if (m_fd < 0) {
|
||||
LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: memfd_create failed: %d\n",
|
||||
LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: shared fd creation failed: %d\n",
|
||||
errno);
|
||||
return;
|
||||
}
|
||||
@@ -179,9 +191,15 @@ public:
|
||||
SetFailure(failure_reason, FailureReason::ReserveAlignedFailed);
|
||||
return 0;
|
||||
}
|
||||
#if !defined(__APPLE__)
|
||||
// On macOS the PROT_NONE reservation is left in place: MapView's MAP_FIXED
|
||||
// replaces it atomically, so the range never becomes a hole that dyld,
|
||||
// Rosetta, or Metal background threads could claim (which the subsequent
|
||||
// MAP_FIXED would then silently destroy).
|
||||
if (!ReleaseReservedProbe(reserved, failure_reason)) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (MapFixed(reserved, size, backing_offset, mode, failure_reason)) {
|
||||
return reserved;
|
||||
}
|
||||
@@ -367,9 +385,12 @@ public:
|
||||
m_maps.emplace(old.vaddr, old);
|
||||
return false;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (preserve_placeholder && placeholder_preserved != nullptr) {
|
||||
*placeholder_preserved = true;
|
||||
}
|
||||
#endif
|
||||
// Only Windows preserves placeholders.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1049,9 +1070,22 @@ private:
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static bool UnmapView(void* vaddr, uint64_t size) { return munmap(vaddr, size) == 0; }
|
||||
static bool UnmapView(void* vaddr, uint64_t size) {
|
||||
#if defined(__APPLE__)
|
||||
// Re-reserve instead of freeing: the guest may MAP_FIXED-remap this range
|
||||
// later, and that must never destroy a host mapping (dyld, Rosetta, Metal)
|
||||
// that moved into the hole in the meantime.
|
||||
return mmap(vaddr, size, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE,
|
||||
-1, 0) != MAP_FAILED;
|
||||
#else
|
||||
return munmap(vaddr, size) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool UnmapViewPreservePlaceholder(void* /*vaddr*/, uint64_t /*size*/) { return false; }
|
||||
// POSIX has no placeholder equivalent.
|
||||
static bool UnmapViewPreservePlaceholder(void* vaddr, uint64_t size) {
|
||||
return munmap(vaddr, size) == 0;
|
||||
}
|
||||
|
||||
int m_fd = -1;
|
||||
#endif
|
||||
|
||||
+112
-7
@@ -46,6 +46,17 @@
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <fmt/format.h>
|
||||
#include <pthread_time.h>
|
||||
#elif !defined(__APPLE__)
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
#include <csignal> // pthread_kill is declared here, not in <pthread.h>
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && (defined(_M_X64) || defined(__x86_64__))
|
||||
#include <x86intrin.h>
|
||||
#endif
|
||||
|
||||
#ifdef pthread_attr_getguardsize
|
||||
@@ -58,6 +69,12 @@ namespace LibKernel {
|
||||
|
||||
LIB_NAME("libkernel", "libkernel");
|
||||
|
||||
// macOS's <pthread.h>/<limits.h> define PTHREAD_STACK_MIN as a macro; the emulator
|
||||
// wants its own guest-side value with the same name, so drop the host macro here.
|
||||
#ifdef PTHREAD_STACK_MIN
|
||||
#undef PTHREAD_STACK_MIN
|
||||
#endif
|
||||
|
||||
constexpr int KEYS_MAX = 256;
|
||||
constexpr int DESTRUCTOR_ITERATIONS = 4;
|
||||
constexpr size_t PTHREAD_STACK_DEFAULT = 0x100000;
|
||||
@@ -93,8 +110,21 @@ static constexpr int KERNEL_PTHREAD_MUTEX_RECURSIVE = 2;
|
||||
static constexpr int KERNEL_PTHREAD_MUTEX_NORMAL = 3;
|
||||
static constexpr int KERNEL_PTHREAD_MUTEX_ADAPTIVE = 4;
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// OS-level thread id reported to the guest.
|
||||
static uint64_t GetHostThreadId() {
|
||||
#if defined(__APPLE__)
|
||||
uint64_t tid = 0;
|
||||
pthread_threadid_np(nullptr, &tid);
|
||||
return tid;
|
||||
#else
|
||||
return static_cast<uint64_t>(::syscall(SYS_gettid));
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint64_t KernelReadTscNative() {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && (defined(_M_X64) || defined(__x86_64__))
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
return __rdtsc();
|
||||
#else
|
||||
return Common::Timer::QueryPerformanceCounter();
|
||||
@@ -103,7 +133,7 @@ static uint64_t KernelReadTscNative() {
|
||||
|
||||
static uint64_t KernelGetTscFrequencyNative() {
|
||||
static const uint64_t frequency = [] {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && (defined(_M_X64) || defined(__x86_64__))
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto host_frequency = Common::Timer::QueryPerformanceFrequency();
|
||||
if (host_frequency == 0) {
|
||||
return uint64_t {1000000000};
|
||||
@@ -778,7 +808,11 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
|
||||
if (g_pthread_self != nullptr) {
|
||||
g_pthread_self->guest_host_rbx = host_rbx;
|
||||
#if defined(__APPLE__)
|
||||
g_pthread_self->guest_host_rsp = host_rsp - (2u * sizeof(uint64_t));
|
||||
#else
|
||||
g_pthread_self->guest_host_rsp = host_rsp - (4u * sizeof(uint64_t));
|
||||
#endif
|
||||
g_pthread_self->guest_host_rbp = host_rbp;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
uintptr_t host_gs8 = 0;
|
||||
@@ -788,18 +822,39 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
: "=r"(host_gs8), "=r"(host_gs10)
|
||||
:
|
||||
: "memory");
|
||||
g_pthread_self->guest_host_rsp -= 2u * sizeof(uint64_t);
|
||||
g_pthread_self->guest_host_gs8 = host_gs8;
|
||||
g_pthread_self->guest_host_gs10 = host_gs10;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The guest ABI expects the entry argument in rdi and a 16-byte aligned stack before call.
|
||||
#if defined(__APPLE__)
|
||||
// Keep inputs out of r12/r13.
|
||||
register uintptr_t guest_rsp_reg asm("r14") = guest_rsp;
|
||||
register uintptr_t guest_rbp_reg asm("r15") = guest_rbp;
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
"movq %%rsp, %%r12\n\t"
|
||||
"movq %%rbp, %%r13\n\t"
|
||||
"movq %[guest_rsp], %%rsp\n\t"
|
||||
"movq %[guest_rbp], %%rbp\n\t"
|
||||
"callq *%%rsi\n\t"
|
||||
"movq %%r13, %%rbp\n\t"
|
||||
"movq %%r12, %%rsp\n\t"
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
: "=a"(ret), "+D"(arg), "+S"(func)
|
||||
: [guest_rsp] "r"(guest_rsp_reg), [guest_rbp] "r"(guest_rbp_reg)
|
||||
: "cc", "memory", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
|
||||
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
|
||||
"xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#else
|
||||
// PthreadExit resumes at this frame, so all four saved registers stay on the host stack.
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
"pushq %%r14\n\t"
|
||||
"pushq %%r15\n\t"
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
"movq %%gs:0x08, %%r14\n\t"
|
||||
"movq %%gs:0x10, %%r15\n\t"
|
||||
"xorq %%rcx, %%rcx\n\t"
|
||||
@@ -816,16 +871,23 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
"movq %%r14, %%gs:0x08\n\t"
|
||||
"movq %%r15, %%gs:0x10\n\t"
|
||||
#endif
|
||||
"popq %%r15\n\t"
|
||||
"popq %%r14\n\t"
|
||||
#endif
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
: "=a"(ret), "+D"(arg), "+S"(func)
|
||||
: [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp)
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
: "cc", "memory", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
|
||||
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
|
||||
"xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#else
|
||||
: "cc", "memory", "rcx", "rdx", "r8", "r9", "r10", "r11", "r12", "r13", "xmm0",
|
||||
"xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10",
|
||||
"xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
g_guest_entry_return_rsp = 0;
|
||||
if (g_pthread_self != nullptr) {
|
||||
@@ -860,6 +922,29 @@ static void UpdateCurrentThreadStackAttr(PthreadAttr* attr) {
|
||||
(*attr)->stack_size = high - low;
|
||||
(*attr)->stack_user = true;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// macOS reports a stack top and size.
|
||||
void* top = pthread_get_stackaddr_np(pthread_self());
|
||||
const size_t size = pthread_get_stacksize_np(pthread_self());
|
||||
|
||||
if (top != nullptr && size != 0) {
|
||||
(*attr)->stack_addr = static_cast<void*>(static_cast<uint8_t*>(top) - size);
|
||||
(*attr)->stack_size = size;
|
||||
(*attr)->stack_user = true;
|
||||
}
|
||||
#else
|
||||
// Record the main thread's stack bounds.
|
||||
pthread_attr_t self_attr {};
|
||||
if (pthread_getattr_np(pthread_self(), &self_attr) == 0) {
|
||||
void* base = nullptr;
|
||||
size_t size = 0;
|
||||
if (pthread_attr_getstack(&self_attr, &base, &size) == 0 && base != nullptr && size != 0) {
|
||||
(*attr)->stack_addr = base;
|
||||
(*attr)->stack_size = size;
|
||||
(*attr)->stack_user = true;
|
||||
}
|
||||
pthread_attr_destroy(&self_attr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -900,6 +985,8 @@ void PthreadInitSelfForMainThread() {
|
||||
uint64_t os_thread_id = 0;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
os_thread_id = static_cast<uint64_t>(GetCurrentThreadId());
|
||||
#else
|
||||
os_thread_id = GetHostThreadId();
|
||||
#endif
|
||||
g_pthread_self->host_thread_id = os_thread_id;
|
||||
g_pthread_main = g_pthread_self;
|
||||
@@ -1056,7 +1143,7 @@ static void PthreadAttrDbgPrint(const PthreadAttr* src) {
|
||||
"\tstack_addr = 0x%016" PRIx64 "\n"
|
||||
"\tstack_size = %" PRIu64 "\n",
|
||||
mask, state, guard_size, inherit_sched, param.sched_priority, policy, solosched,
|
||||
reinterpret_cast<uint64_t>(stack_addr), reinterpret_cast<uint64_t>(stack_size));
|
||||
reinterpret_cast<uint64_t>(stack_addr), static_cast<uint64_t>(stack_size));
|
||||
}
|
||||
|
||||
static constexpr int32_t DST_NONE = 0;
|
||||
@@ -2640,7 +2727,12 @@ int KYTY_SYSV_ABI PthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid c
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
int result = pthread_condattr_setclock(&(*attr)->p, pclock_id);
|
||||
#if defined(__APPLE__)
|
||||
(void)pclock_id;
|
||||
int result = 0;
|
||||
#else
|
||||
int result = pthread_condattr_setclock(&(*attr)->p, pclock_id);
|
||||
#endif
|
||||
(*attr)->clock_id = clock_id;
|
||||
|
||||
LOGF("\tcondattr setclock: clock_id = %d, native = %d, result = %d\n", clock_id,
|
||||
@@ -3087,6 +3179,17 @@ uint64_t PthreadGetHostThreadId(Pthread thread) {
|
||||
return thread != nullptr ? thread->host_thread_id : 0;
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// Raise a host signal on another guest thread.
|
||||
bool PthreadKillHost(Pthread thread, int host_signal) {
|
||||
if (thread == nullptr || thread->free) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ::pthread_kill(thread->p, host_signal) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void PthreadQueuePendingSignal(Pthread thread, int signum) {
|
||||
if (thread == nullptr || signum < 0 || signum >= 64) {
|
||||
return;
|
||||
@@ -3184,6 +3287,8 @@ static void* RunThread(void* arg) {
|
||||
uint64_t os_thread_id = 0;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
os_thread_id = static_cast<uint64_t>(GetCurrentThreadId());
|
||||
#else
|
||||
os_thread_id = GetHostThreadId();
|
||||
#endif
|
||||
thread->host_thread_id = os_thread_id;
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ void PthreadQueuePendingSignal(Pthread thread, int signum);
|
||||
bool PthreadHasPendingSignal(Pthread thread, int signum);
|
||||
bool PthreadTakePendingSignal(Pthread thread, int signum);
|
||||
bool PthreadGetGuestStack(Pthread thread, uint64_t* stack_addr, uint64_t* stack_size);
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
bool PthreadKillHost(Pthread thread, int host_signal);
|
||||
#endif
|
||||
int PthreadGetPriorityForKernel(Pthread thread);
|
||||
int PthreadGetCurrentPriorityForKernel();
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ set_target_properties(launcher PROPERTIES AUTOUIC ON AUTOMOC ON AUTORCC ON)
|
||||
|
||||
if(WIN32 AND NOT KYTY_CLANG_CL)
|
||||
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
|
||||
elseif(LINUX)
|
||||
# Use the same linker as kyty_emulator.
|
||||
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
|
||||
endif()
|
||||
|
||||
add_dependencies(launcher KytyGitVersion)
|
||||
@@ -79,6 +82,62 @@ if(WIN32)
|
||||
--no-translations
|
||||
)
|
||||
")
|
||||
elseif(LINUX)
|
||||
# Bundle the Qt runtime and plugins.
|
||||
set_target_properties(launcher PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/lib"
|
||||
BUILD_WITH_INSTALL_RPATH FALSE
|
||||
LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,--disable-new-dtags")
|
||||
|
||||
# Support distribution and official Qt layouts.
|
||||
get_target_property(kyty_qt_core_lib Qt6::Core IMPORTED_LOCATION_RELEASE)
|
||||
if(NOT kyty_qt_core_lib)
|
||||
get_target_property(kyty_qt_core_lib Qt6::Core IMPORTED_LOCATION)
|
||||
endif()
|
||||
get_filename_component(kyty_qt_libdir "${kyty_qt_core_lib}" DIRECTORY)
|
||||
|
||||
set(kyty_qt_plugins_dir "")
|
||||
foreach(kyty_candidate
|
||||
"${kyty_qt_libdir}/qt6/plugins"
|
||||
"${kyty_qt_libdir}/../plugins"
|
||||
"${Qt6_DIR}/../../../plugins")
|
||||
if(IS_DIRECTORY "${kyty_candidate}/platforms")
|
||||
get_filename_component(kyty_qt_plugins_dir "${kyty_candidate}" ABSOLUTE)
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(kyty_qt_plugins_dir)
|
||||
message(STATUS "Qt plugins for launcher deployment: ${kyty_qt_plugins_dir}")
|
||||
set(kyty_qt_plugin_files "")
|
||||
foreach(kyty_qt_plugin platforms tls imageformats xcbglintegrations wayland-shell-integration)
|
||||
if(IS_DIRECTORY "${kyty_qt_plugins_dir}/${kyty_qt_plugin}")
|
||||
file(GLOB kyty_qt_plugin_group
|
||||
"${kyty_qt_plugins_dir}/${kyty_qt_plugin}/*.so"
|
||||
"${kyty_qt_plugins_dir}/${kyty_qt_plugin}/*.so.*")
|
||||
list(APPEND kyty_qt_plugin_files ${kyty_qt_plugin_group})
|
||||
install(DIRECTORY "${kyty_qt_plugins_dir}/${kyty_qt_plugin}" DESTINATION "plugins")
|
||||
endif()
|
||||
endforeach()
|
||||
install(CODE "
|
||||
set(kyty_plugin_files \"${kyty_qt_plugin_files}\")
|
||||
file(GET_RUNTIME_DEPENDENCIES
|
||||
EXECUTABLES \"${CMAKE_CURRENT_BINARY_DIR}/${launcher_name}\"
|
||||
LIBRARIES \${kyty_plugin_files}
|
||||
RESOLVED_DEPENDENCIES_VAR kyty_resolved
|
||||
POST_INCLUDE_REGEXES \"libQt6\" \"libicu\"
|
||||
POST_EXCLUDE_REGEXES \".*\")
|
||||
foreach(kyty_dep \${kyty_resolved})
|
||||
file(INSTALL DESTINATION \"\${CMAKE_INSTALL_PREFIX}/lib\"
|
||||
TYPE SHARED_LIBRARY FOLLOW_SYMLINK_CHAIN FILES \"\${kyty_dep}\")
|
||||
endforeach()
|
||||
")
|
||||
install(CODE "
|
||||
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/qt.conf\" \"[Paths]\\nPlugins = plugins\\n\")
|
||||
")
|
||||
else()
|
||||
message(WARNING "Qt plugin directory not found; the installed launcher will need a system Qt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
@@ -183,6 +183,11 @@
|
||||
<string>Serial</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Firmware Version</string>
|
||||
|
||||
@@ -73,6 +73,7 @@ public:
|
||||
|
||||
QString name;
|
||||
QString title_id; /* Serial / title id from sce_sys/param.json */
|
||||
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
|
||||
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
||||
QString basedir; /* Game base directory */
|
||||
QString game_path; /* Launcher-unique game path */
|
||||
@@ -117,6 +118,7 @@ public:
|
||||
void CopyFrom(const Configuration& other) {
|
||||
name = other.name;
|
||||
title_id = other.title_id;
|
||||
gameVersion = other.gameVersion;
|
||||
firmwareVer = other.firmwareVer;
|
||||
basedir = other.basedir;
|
||||
game_path = other.game_path;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef PATCHES_DIALOG_H
|
||||
#define PATCHES_DIALOG_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class Configuration;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class PatchesDialog final: public QDialog {
|
||||
public:
|
||||
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
||||
~PatchesDialog() override = default;
|
||||
|
||||
KYTY_QT_CLASS_NO_COPY(PatchesDialog);
|
||||
|
||||
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
|
||||
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
|
||||
|
||||
private:
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
QString m_title_id;
|
||||
QListWidget* m_patches = nullptr;
|
||||
QLabel* m_status = nullptr;
|
||||
QPushButton* m_apply = nullptr;
|
||||
};
|
||||
|
||||
#endif // PATCHES_DIALOG_H
|
||||
@@ -24,6 +24,7 @@ namespace {
|
||||
enum Column {
|
||||
NameColumn,
|
||||
SerialColumn,
|
||||
GameVersionColumn,
|
||||
FirmwareVersionColumn,
|
||||
PathColumn,
|
||||
StatusColumn,
|
||||
@@ -85,6 +86,9 @@ QString GetDisplayText(const Configuration& info) {
|
||||
if (!info.title_id.isEmpty()) {
|
||||
lines.append(QStringLiteral("Serial ID: %1").arg(info.title_id));
|
||||
}
|
||||
if (!info.gameVersion.isEmpty()) {
|
||||
lines.append(QStringLiteral("Game version: %1").arg(info.gameVersion));
|
||||
}
|
||||
if (!info.firmwareVer.isEmpty()) {
|
||||
lines.append(QStringLiteral("Firmware version: %1").arg(info.firmwareVer));
|
||||
}
|
||||
@@ -169,6 +173,8 @@ void ConfigurationItem::Update() {
|
||||
|
||||
setText(NameColumn, m_info->name);
|
||||
setText(SerialColumn, m_info->title_id);
|
||||
setText(GameVersionColumn,
|
||||
m_info->gameVersion.isEmpty() ? QStringLiteral("\u2014") : m_info->gameVersion);
|
||||
setText(FirmwareVersionColumn,
|
||||
m_info->firmwareVer.isEmpty() ? QStringLiteral("\u2014") : m_info->firmwareVer);
|
||||
setText(PathColumn, path);
|
||||
@@ -198,9 +204,13 @@ bool ConfigurationItem::operator<(const QTreeWidgetItem& other) const {
|
||||
case StatusColumn:
|
||||
return GetStatusText(m_info->game_status) <
|
||||
GetStatusText(other_item->m_info->game_status);
|
||||
case GameVersionColumn:
|
||||
case FirmwareVersionColumn: {
|
||||
const auto& version = m_info->firmwareVer;
|
||||
const auto& other_version = other_item->m_info->firmwareVer;
|
||||
const auto& version = column == GameVersionColumn ? m_info->gameVersion
|
||||
: m_info->firmwareVer;
|
||||
const auto& other_version = column == GameVersionColumn
|
||||
? other_item->m_info->gameVersion
|
||||
: other_item->m_info->firmwareVer;
|
||||
if (version.isEmpty() || other_version.isEmpty()) {
|
||||
return version.isEmpty() && !other_version.isEmpty();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "patchesDialog.h"
|
||||
#include "common.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
@@ -55,10 +56,11 @@ constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
||||
|
||||
constexpr int GAME_NAME_COLUMN = 0;
|
||||
constexpr int GAME_SERIAL_COLUMN = 1;
|
||||
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 2;
|
||||
constexpr int GAME_PATH_COLUMN = 3;
|
||||
constexpr int GAME_STATUS_COLUMN = 4;
|
||||
constexpr int GAME_COMMENT_COLUMN = 5;
|
||||
constexpr int GAME_VERSION_COLUMN = 2;
|
||||
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 3;
|
||||
constexpr int GAME_PATH_COLUMN = 4;
|
||||
constexpr int GAME_STATUS_COLUMN = 5;
|
||||
constexpr int GAME_COMMENT_COLUMN = 6;
|
||||
|
||||
static QString NormalizeGameDirectory(const QString& dir) {
|
||||
const auto trimmed = dir.trimmed();
|
||||
@@ -239,6 +241,7 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
m_ui->cfgs_list->setSortingEnabled(true);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_SERIAL_COLUMN, 110);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_VERSION_COLUMN, 120);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_FIRMWARE_VERSION_COLUMN, 150);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
|
||||
@@ -403,6 +406,7 @@ static Configuration* CloneConfiguration(const Configuration& source) {
|
||||
struct GameMetadata {
|
||||
QString title_name;
|
||||
QString title_id;
|
||||
QString gameVersion;
|
||||
QString firmwareVer;
|
||||
};
|
||||
|
||||
@@ -488,6 +492,10 @@ static GameMetadata GetGameMetadata(const QString& param_file, const QString& fa
|
||||
}
|
||||
|
||||
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
|
||||
ret.gameVersion = GetJsonString(root, QStringLiteral("appVersion"));
|
||||
if (ret.gameVersion.isEmpty()) {
|
||||
ret.gameVersion = GetJsonString(root, QStringLiteral("contentVersion"));
|
||||
}
|
||||
ret.firmwareVer = GetFirmwareVersion(root);
|
||||
|
||||
return ret;
|
||||
@@ -501,6 +509,7 @@ static void SetGameFiles(Configuration& info, const QString& game_dir, const QSt
|
||||
info.basedir = game.absolutePath();
|
||||
info.name = metadata.title_name;
|
||||
info.title_id = metadata.title_id;
|
||||
info.gameVersion = metadata.gameVersion;
|
||||
info.firmwareVer = metadata.firmwareVer;
|
||||
|
||||
if (info.name.isEmpty()) {
|
||||
@@ -866,6 +875,15 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
|
||||
connect(action_view_trophies, &QAction::triggered, this,
|
||||
&ConfigurationListWidget::ViewTrophies);
|
||||
QAction* action_patches = menu.addAction(tr("Patches (experimental)..."));
|
||||
connect(action_patches, &QAction::triggered, this, [this, item]() {
|
||||
if (item != nullptr) {
|
||||
auto* dialog = new PatchesDialog(item->GetInfo(), this);
|
||||
dialog->show();
|
||||
}
|
||||
});
|
||||
action_patches->setVisible(item != nullptr &&
|
||||
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
|
||||
QAction* action_remove_save_data =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "patchesDialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
#include "ui_main_dialog.h"
|
||||
|
||||
#ifndef __linux__
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#endif
|
||||
|
||||
@@ -35,20 +36,18 @@
|
||||
|
||||
class QWidget;
|
||||
|
||||
#ifdef __linux__
|
||||
constexpr char EMULATOR_EXE[] = "kyty_emulator";
|
||||
#else
|
||||
#if defined(_WIN32)
|
||||
constexpr char EMULATOR_EXE[] = "kyty_emulator.exe";
|
||||
#else
|
||||
constexpr char EMULATOR_EXE[] = "kyty_emulator";
|
||||
#endif
|
||||
|
||||
#ifndef __linux__
|
||||
#if defined(_WIN32)
|
||||
constexpr char CMD_EXE[] = "cmd.exe";
|
||||
#else
|
||||
constexpr char GNOME[] = "gnome-terminal";
|
||||
constexpr char XTERM[] = "xterm";
|
||||
#elif defined(__linux__)
|
||||
constexpr char KYTY_BASH_FILE[] = "kyty_run.sh";
|
||||
#endif
|
||||
#ifndef __linux__
|
||||
#if defined(_WIN32)
|
||||
constexpr DWORD CMD_X_CHARS = 175;
|
||||
constexpr DWORD CMD_Y_CHARS = 1000;
|
||||
#endif
|
||||
@@ -228,6 +227,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
args << "--game" << game;
|
||||
|
||||
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
|
||||
if (QFileInfo::exists(patch_plan)) {
|
||||
args << "--game-patch" << patch_plan;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -259,6 +263,57 @@ static bool CreateBashScript(const QString& interpreter, const QStringList& args
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find a terminal and its command separator.
|
||||
static bool FindTerminal(QString* program, QStringList* prefix) {
|
||||
struct TerminalSpec {
|
||||
const char* executable;
|
||||
const char* separator; // nullptr when the command follows immediately
|
||||
};
|
||||
|
||||
static const TerminalSpec candidates[] = {
|
||||
{"x-terminal-emulator", "-e"}, {"gnome-terminal", "--"}, {"konsole", "-e"},
|
||||
{"xfce4-terminal", "-x"}, {"mate-terminal", "--"}, {"tilix", "-e"},
|
||||
{"alacritty", "-e"}, {"kitty", nullptr}, {"foot", nullptr},
|
||||
{"wezterm", "-e"}, {"urxvt", "-e"}, {"xterm", "-e"},
|
||||
};
|
||||
|
||||
const auto try_candidate = [program, prefix](const QString& executable, const char* separator) {
|
||||
const auto resolved = QStandardPaths::findExecutable(executable);
|
||||
if (resolved.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
*program = resolved;
|
||||
prefix->clear();
|
||||
if (separator != nullptr) {
|
||||
*prefix << QString::fromLatin1(separator);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (const auto from_env = qEnvironmentVariable("TERMINAL"); !from_env.isEmpty()) {
|
||||
// Reuse the known separator for an explicit terminal.
|
||||
const auto env_name = QFileInfo(from_env).fileName();
|
||||
const char* separator = "-e";
|
||||
for (const auto& candidate: candidates) {
|
||||
if (env_name == QLatin1String(candidate.executable)) {
|
||||
separator = candidate.separator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (try_candidate(from_env, separator)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& candidate: candidates) {
|
||||
if (try_candidate(QString::fromLatin1(candidate.executable), candidate.separator)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
@@ -283,10 +338,18 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
}
|
||||
|
||||
{
|
||||
process->setProgram(GNOME);
|
||||
process->setArguments({"--", "bash", "-c", bash_file_name});
|
||||
QString terminal;
|
||||
QStringList terminal_prefix;
|
||||
if (FindTerminal(&terminal, &terminal_prefix)) {
|
||||
process->setProgram(terminal);
|
||||
process->setArguments(terminal_prefix + QStringList {"bash", "-c", bash_file_name});
|
||||
} else {
|
||||
// Run without a terminal as a fallback.
|
||||
process->setProgram(QStringLiteral("bash"));
|
||||
process->setArguments({QStringLiteral("-c"), bash_file_name});
|
||||
}
|
||||
}
|
||||
#else
|
||||
#elif defined(_WIN32)
|
||||
{
|
||||
process->setProgram(CMD_EXE);
|
||||
QStringList process_args;
|
||||
@@ -294,9 +357,12 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
process_args += args;
|
||||
process->setArguments(process_args);
|
||||
}
|
||||
#else
|
||||
process->setProgram(interpreter);
|
||||
process->setArguments(args);
|
||||
#endif
|
||||
process->setWorkingDirectory(dir.path());
|
||||
#ifndef __linux__
|
||||
#if defined(_WIN32)
|
||||
process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) {
|
||||
args->flags |= static_cast<uint32_t>(CREATE_NEW_CONSOLE);
|
||||
args->startupInfo->dwFlags &= ~static_cast<DWORD>(STARTF_USESTDHANDLES);
|
||||
@@ -310,6 +376,15 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
});
|
||||
#endif
|
||||
process->start();
|
||||
#if !defined(_WIN32)
|
||||
// Report immediate launch failures.
|
||||
if (!process->waitForStarted(5000)) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to start:\n%1\n\n%2")
|
||||
.arg(process->program(), process->errorString()));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
process->waitForFinished(100);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "patchesDialog.h"
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QSaveFile>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(tr("Patches (Experimental) - %1").arg(game.name));
|
||||
resize(640, 480);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
m_patches = new QListWidget(this);
|
||||
m_status = new QLabel(this);
|
||||
m_apply = new QPushButton(tr("Apply selection"), this);
|
||||
auto* close = new QPushButton(tr("Close"), this);
|
||||
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_patches);
|
||||
layout->addWidget(m_status);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(this);
|
||||
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
|
||||
buttons->addButton(close, QDialogButtonBox::RejectRole);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
|
||||
connect(close, &QPushButton::clicked, this, &QDialog::close);
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
|
||||
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
|
||||
}
|
||||
|
||||
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
|
||||
return QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
||||
}
|
||||
|
||||
void PatchesDialog::Load() {
|
||||
QFile file(PatchPlanPath(m_title_id));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
m_status->setText(tr("No local patch file: %1").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto patches = QJsonDocument::fromJson(file.readAll())
|
||||
.object()
|
||||
.value(QStringLiteral("patches"))
|
||||
.toArray();
|
||||
for (const auto& value: patches) {
|
||||
const auto patch = value.toObject();
|
||||
auto* item = new QListWidgetItem(patch.value(QStringLiteral("name")).toString(), m_patches);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(patch.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
|
||||
: Qt::Unchecked);
|
||||
}
|
||||
|
||||
m_apply->setEnabled(!patches.isEmpty());
|
||||
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
|
||||
}
|
||||
|
||||
void PatchesDialog::Save() {
|
||||
const auto path = PatchPlanPath(m_title_id);
|
||||
QFile input(path);
|
||||
if (!input.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto document = QJsonDocument::fromJson(input.readAll());
|
||||
input.close();
|
||||
auto root = document.object();
|
||||
auto patches = root.value(QStringLiteral("patches")).toArray();
|
||||
for (int index = 0; index < patches.size(); index++) {
|
||||
auto patch = patches[index].toObject();
|
||||
patch.insert(QStringLiteral("enabled"),
|
||||
m_patches->item(index)->checkState() == Qt::Checked);
|
||||
patches[index] = patch;
|
||||
}
|
||||
root.insert(QStringLiteral("patches"), patches);
|
||||
document.setObject(root);
|
||||
|
||||
QSaveFile output(path);
|
||||
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
|
||||
output.commit()) {
|
||||
m_status->setText(tr("Patch selection saved."));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1919,7 +1919,7 @@ int KYTY_SYSV_ABI Ngs2SystemCreateWithAllocator(const Ngs2SystemOption* optio
|
||||
option->sample_rate, option->max_voice_channels,
|
||||
reinterpret_cast<uint64_t>(allocator->alloc_handler),
|
||||
reinterpret_cast<uint64_t>(allocator->free_handler),
|
||||
reinterpret_cast<uint64_t>(allocator->user_data));
|
||||
static_cast<uint64_t>(allocator->user_data));
|
||||
|
||||
Ngs2ContextBufferInfo buf {};
|
||||
buf.host_buffer = nullptr;
|
||||
@@ -2002,7 +2002,7 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
|
||||
option->max_input_delay_blocks, option->max_matrices, option->max_ports,
|
||||
option->max_voice_channels, option->max_output_channels,
|
||||
reinterpret_cast<uint64_t>(buffer_info->host_buffer),
|
||||
reinterpret_cast<uint64_t>(buffer_info->host_buffer_size));
|
||||
static_cast<uint64_t>(buffer_info->host_buffer_size));
|
||||
|
||||
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
|
||||
auto* rack = static_cast<Ngs2RackInternal*>(buffer_info->host_buffer);
|
||||
@@ -2106,7 +2106,7 @@ int KYTY_SYSV_ABI Ngs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t
|
||||
option->max_voice_channels, option->max_output_channels,
|
||||
reinterpret_cast<uint64_t>(allocator->alloc_handler),
|
||||
reinterpret_cast<uint64_t>(allocator->free_handler),
|
||||
reinterpret_cast<uint64_t>(allocator->user_data));
|
||||
static_cast<uint64_t>(allocator->user_data));
|
||||
|
||||
Ngs2ContextBufferInfo buf {};
|
||||
buf.host_buffer = nullptr;
|
||||
|
||||
+10
-1
@@ -192,7 +192,8 @@ static void PrintAbortPointerArrayCandidate(const char* name, uint64_t addr) {
|
||||
|
||||
uint64_t rbp = 0;
|
||||
uint64_t rsp = 0;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
// This assembly is x86-64-specific, not Windows-specific.
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
asm volatile("movq %%rbp, %0" : "=r"(rbp));
|
||||
asm volatile("movq %%rsp, %0" : "=r"(rsp));
|
||||
#endif
|
||||
@@ -419,7 +420,15 @@ static KYTY_SYSV_ABI size_t libc_strftime(char* str, size_t count, const char* f
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
|
||||
// Guest-created tm values do not initialize glibc's tm_zone pointer.
|
||||
std::tm sanitized = *timeptr;
|
||||
sanitized.tm_zone = nullptr;
|
||||
|
||||
return std::strftime(str, count, format, &sanitized);
|
||||
#else
|
||||
return std::strftime(str, count, format, timeptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI void catchReturnFromMain(int status) {
|
||||
|
||||
@@ -135,11 +135,8 @@ static void KYTY_SYSV_ABI asan_init() {
|
||||
sys_dbg_stack_info_t s {};
|
||||
SysStackUsage(s);
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
AllocShadow(s.addr, reinterpret_cast<uintptr_t>(&s));
|
||||
#else
|
||||
// Cover the full stack reservation.
|
||||
AllocShadow(s.reserved_addr, reinterpret_cast<uintptr_t>(&s));
|
||||
#endif
|
||||
|
||||
LibKernel::Memory::RegisterCallbacks(KernelAlloc, KernelFree);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif !defined(__APPLE__)
|
||||
#include <csignal>
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
|
||||
namespace Libs {
|
||||
@@ -630,6 +633,75 @@ static SignalUcontext CreateSignalUcontext(
|
||||
ctx.uc_mcontext.mc_rsp = reinterpret_cast<uint64_t>(&stack_marker);
|
||||
ctx.uc_mcontext.mc_rbp = reinterpret_cast<uint64_t>(__builtin_frame_address(0));
|
||||
ctx.uc_mcontext.mc_rip = reinterpret_cast<uint64_t>(__builtin_return_address(0));
|
||||
|
||||
#if defined(__x86_64__)
|
||||
// Capture the remaining guest-visible registers.
|
||||
uint64_t rax = 0;
|
||||
uint64_t rbx = 0;
|
||||
uint64_t rcx = 0;
|
||||
uint64_t rdx = 0;
|
||||
uint64_t rsi = 0;
|
||||
uint64_t rdi = 0;
|
||||
asm volatile("movq %%rax, %0\n\t"
|
||||
"movq %%rbx, %1\n\t"
|
||||
"movq %%rcx, %2\n\t"
|
||||
"movq %%rdx, %3\n\t"
|
||||
"movq %%rsi, %4\n\t"
|
||||
"movq %%rdi, %5\n\t"
|
||||
: "=m"(rax), "=m"(rbx), "=m"(rcx), "=m"(rdx), "=m"(rsi), "=m"(rdi)
|
||||
:
|
||||
: "memory");
|
||||
|
||||
uint64_t r8 = 0;
|
||||
uint64_t r9 = 0;
|
||||
uint64_t r10 = 0;
|
||||
uint64_t r11 = 0;
|
||||
uint64_t r12 = 0;
|
||||
uint64_t r13 = 0;
|
||||
uint64_t r14 = 0;
|
||||
uint64_t r15 = 0;
|
||||
asm volatile("movq %%r8, %0\n\t"
|
||||
"movq %%r9, %1\n\t"
|
||||
"movq %%r10, %2\n\t"
|
||||
"movq %%r11, %3\n\t"
|
||||
"movq %%r12, %4\n\t"
|
||||
"movq %%r13, %5\n\t"
|
||||
"movq %%r14, %6\n\t"
|
||||
"movq %%r15, %7\n\t"
|
||||
: "=m"(r8), "=m"(r9), "=m"(r10), "=m"(r11), "=m"(r12), "=m"(r13), "=m"(r14),
|
||||
"=m"(r15)
|
||||
:
|
||||
: "memory");
|
||||
|
||||
uint64_t rflags = 0;
|
||||
uint64_t cs = 0;
|
||||
uint64_t ss = 0;
|
||||
asm volatile("pushfq\n\t"
|
||||
"popq %0\n\t"
|
||||
"movq %%cs, %1\n\t"
|
||||
"movq %%ss, %2\n\t"
|
||||
: "=r"(rflags), "=r"(cs), "=r"(ss)
|
||||
:
|
||||
: "memory");
|
||||
|
||||
ctx.uc_mcontext.mc_rax = rax;
|
||||
ctx.uc_mcontext.mc_rbx = rbx;
|
||||
ctx.uc_mcontext.mc_rcx = rcx;
|
||||
ctx.uc_mcontext.mc_rdx = rdx;
|
||||
ctx.uc_mcontext.mc_rsi = rsi;
|
||||
ctx.uc_mcontext.mc_rdi = rdi;
|
||||
ctx.uc_mcontext.mc_r8 = r8;
|
||||
ctx.uc_mcontext.mc_r9 = r9;
|
||||
ctx.uc_mcontext.mc_r10 = r10;
|
||||
ctx.uc_mcontext.mc_r11 = r11;
|
||||
ctx.uc_mcontext.mc_r12 = r12;
|
||||
ctx.uc_mcontext.mc_r13 = r13;
|
||||
ctx.uc_mcontext.mc_r14 = r14;
|
||||
ctx.uc_mcontext.mc_r15 = r15;
|
||||
ctx.uc_mcontext.mc_rflags = rflags;
|
||||
ctx.uc_mcontext.mc_cs = cs;
|
||||
ctx.uc_mcontext.mc_ss = ss;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return ctx;
|
||||
@@ -765,6 +837,123 @@ static void ApplySignalUcontext(CONTEXT* dst_ctx, const SignalUcontext& src_ctx)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__) && defined(__x86_64__)
|
||||
|
||||
static SignalUcontext CreateSignalUcontextFromHost(const ucontext_t* host_ctx) {
|
||||
SignalUcontext ctx = {};
|
||||
if (host_ctx == nullptr) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const auto* gregs = host_ctx->uc_mcontext.gregs;
|
||||
|
||||
ctx.uc_mcontext.mc_rdi = static_cast<uint64_t>(gregs[REG_RDI]);
|
||||
ctx.uc_mcontext.mc_rsi = static_cast<uint64_t>(gregs[REG_RSI]);
|
||||
ctx.uc_mcontext.mc_rdx = static_cast<uint64_t>(gregs[REG_RDX]);
|
||||
ctx.uc_mcontext.mc_rcx = static_cast<uint64_t>(gregs[REG_RCX]);
|
||||
ctx.uc_mcontext.mc_r8 = static_cast<uint64_t>(gregs[REG_R8]);
|
||||
ctx.uc_mcontext.mc_r9 = static_cast<uint64_t>(gregs[REG_R9]);
|
||||
ctx.uc_mcontext.mc_rax = static_cast<uint64_t>(gregs[REG_RAX]);
|
||||
ctx.uc_mcontext.mc_rbx = static_cast<uint64_t>(gregs[REG_RBX]);
|
||||
ctx.uc_mcontext.mc_rbp = static_cast<uint64_t>(gregs[REG_RBP]);
|
||||
ctx.uc_mcontext.mc_r10 = static_cast<uint64_t>(gregs[REG_R10]);
|
||||
ctx.uc_mcontext.mc_r11 = static_cast<uint64_t>(gregs[REG_R11]);
|
||||
ctx.uc_mcontext.mc_r12 = static_cast<uint64_t>(gregs[REG_R12]);
|
||||
ctx.uc_mcontext.mc_r13 = static_cast<uint64_t>(gregs[REG_R13]);
|
||||
ctx.uc_mcontext.mc_r14 = static_cast<uint64_t>(gregs[REG_R14]);
|
||||
ctx.uc_mcontext.mc_r15 = static_cast<uint64_t>(gregs[REG_R15]);
|
||||
ctx.uc_mcontext.mc_rip = static_cast<uint64_t>(gregs[REG_RIP]);
|
||||
ctx.uc_mcontext.mc_rsp = static_cast<uint64_t>(gregs[REG_RSP]);
|
||||
ctx.uc_mcontext.mc_rflags = static_cast<uint64_t>(gregs[REG_EFL]);
|
||||
|
||||
// Linux packs cs/gs/fs into one greg.
|
||||
const auto csgsfs = static_cast<uint64_t>(gregs[REG_CSGSFS]);
|
||||
ctx.uc_mcontext.mc_cs = csgsfs & 0xffffu;
|
||||
ctx.uc_mcontext.mc_gs = static_cast<uint16_t>((csgsfs >> 16u) & 0xffffu);
|
||||
ctx.uc_mcontext.mc_fs = static_cast<uint16_t>((csgsfs >> 32u) & 0xffffu);
|
||||
ctx.uc_mcontext.mc_len = sizeof(SignalMcontext);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static void ApplySignalUcontextToHost(ucontext_t* dst_ctx, const SignalUcontext& src_ctx) {
|
||||
if (dst_ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* gregs = dst_ctx->uc_mcontext.gregs;
|
||||
|
||||
gregs[REG_RDI] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rdi);
|
||||
gregs[REG_RSI] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rsi);
|
||||
gregs[REG_RDX] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rdx);
|
||||
gregs[REG_RCX] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rcx);
|
||||
gregs[REG_R8] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r8);
|
||||
gregs[REG_R9] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r9);
|
||||
gregs[REG_RAX] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rax);
|
||||
gregs[REG_RBX] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rbx);
|
||||
gregs[REG_RBP] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rbp);
|
||||
gregs[REG_R10] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r10);
|
||||
gregs[REG_R11] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r11);
|
||||
gregs[REG_R12] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r12);
|
||||
gregs[REG_R13] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r13);
|
||||
gregs[REG_R14] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r14);
|
||||
gregs[REG_R15] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_r15);
|
||||
gregs[REG_RIP] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rip);
|
||||
gregs[REG_RSP] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rsp);
|
||||
gregs[REG_EFL] = static_cast<greg_t>(src_ctx.uc_mcontext.mc_rflags);
|
||||
|
||||
// The kernel validates packed segment selectors on sigreturn.
|
||||
}
|
||||
|
||||
static int SignalDispatchHostSignal() {
|
||||
static const int host_signal = SIGRTMIN + 3;
|
||||
return host_signal;
|
||||
}
|
||||
|
||||
static void HostSignalDispatchHandler(int /*host_signal*/, siginfo_t* /*info*/,
|
||||
void* native_context) {
|
||||
Pthread current = PthreadSelfOrNull();
|
||||
if (current == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* host_ctx = static_cast<ucontext_t*>(native_context);
|
||||
|
||||
for (int signum = 0; signum < static_cast<int>(std::size(g_exception_handlers)); signum++) {
|
||||
if (!TakePendingSignal(current, signum)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* handler = reinterpret_cast<exception_handler_func_t>(g_exception_handlers[signum]);
|
||||
if (handler != nullptr) {
|
||||
SignalDispatchScope scope;
|
||||
auto ctx = CreateSignalUcontextFromHost(host_ctx);
|
||||
if (IsGuestCodeAddress(ctx.uc_mcontext.mc_rip)) {
|
||||
handler(signum, &ctx);
|
||||
ApplySignalUcontextToHost(host_ctx, ctx);
|
||||
} else {
|
||||
// Do not expose or restore a host frame.
|
||||
SanitizeNonGuestSignalUcontext(&ctx, current);
|
||||
handler(signum, &ctx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static bool EnsureHostSignalDispatchInstalled() {
|
||||
static const bool installed = [] {
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = HostSignalDispatchHandler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
return ::sigaction(SignalDispatchHostSignal(), &action, nullptr) == 0;
|
||||
}();
|
||||
return installed;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
enum KytyQueueUserApcFlags : ULONG_PTR {
|
||||
KytyQueueUserApcFlagsNone = 0,
|
||||
@@ -985,6 +1174,31 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
}
|
||||
CloseHandle(target_thread);
|
||||
return OK;
|
||||
#elif !defined(__APPLE__) && defined(__x86_64__)
|
||||
// Deliver on the target thread.
|
||||
if (thread == PthreadSelfOrNull()) {
|
||||
SignalDispatchScope scope;
|
||||
auto ctx = CreateCurrentGuestCallSignalUcontext(
|
||||
reinterpret_cast<uint64_t>(__builtin_return_address(0)));
|
||||
handler(signum, &ctx);
|
||||
return OK;
|
||||
}
|
||||
|
||||
if (!EnsureHostSignalDispatchInstalled()) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
QueuePendingSignal(thread, signum);
|
||||
if (!PthreadKillHost(thread, SignalDispatchHostSignal())) {
|
||||
TakePendingSignal(thread, signum);
|
||||
LOGF("\t pthread_kill failed for target thread\n");
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
Common::CondVar::SignalThread(PthreadGetUniqueId(thread));
|
||||
PthreadWakeForSignal(thread);
|
||||
WaitForSignalDispatch(thread, signum);
|
||||
return OK;
|
||||
#else
|
||||
auto ctx = CreateSignalUcontext();
|
||||
handler(signum, &ctx);
|
||||
@@ -1293,6 +1507,11 @@ static bool DecodeEhFramePointer(const uint8_t* data, const uint8_t* end, uint8_
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kyty does not create guest signal-return trampoline frames.
|
||||
int KYTY_SYSV_ABI KernelIsSignalReturn(uint64_t /*pc*/) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags,
|
||||
ModuleInfoForUnwind* info) {
|
||||
if (flags >= 3) {
|
||||
@@ -3044,6 +3263,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("AqBioC2vF3I", LibKernel::read);
|
||||
LIB_FUNC("f7KBOafysXo", LibKernel::KernelGetModuleInfoFromAddr);
|
||||
LIB_FUNC("RpQJJVKTiFM", LibKernel::KernelGetModuleInfoForUnwind);
|
||||
LIB_FUNC("crb5j7mkk1c", LibKernel::KernelIsSignalReturn); // _is_signal_return
|
||||
LIB_FUNC("Fjc4-n1+y2g", LibKernel::elf_phdr_match_addr);
|
||||
LIB_FUNC("FxVZqBAA7ks", LibKernel::write);
|
||||
LIB_FUNC("FJmglmTMdr4", LibKernel::getargv);
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
#endif
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// POSIX uses plain int file descriptors for sockets; provide the Winsock spellings
|
||||
// the shared (non-guarded) code paths reference.
|
||||
using SOCKET = int;
|
||||
static constexpr SOCKET INVALID_SOCKET = -1;
|
||||
#endif
|
||||
|
||||
#include "common/assert.h"
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "loader/gamePatch.h"
|
||||
|
||||
#include "common/stringUtils.h"
|
||||
#include "common/virtualMemory.h"
|
||||
#include "loader/elf.h"
|
||||
#include "loader/runtimeLinker.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace Loader::GamePatch {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Write {
|
||||
std::string patch_name;
|
||||
uint64_t address = 0;
|
||||
std::vector<uint8_t> expected;
|
||||
std::vector<uint8_t> replacement;
|
||||
};
|
||||
|
||||
struct Plan {
|
||||
std::string title_id;
|
||||
std::string game_version;
|
||||
std::string process;
|
||||
std::vector<Write> writes;
|
||||
std::vector<std::string> patch_names;
|
||||
};
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
bool Fail(std::string* error, std::string message) {
|
||||
*error = std::move(message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Json::string_t* StringField(const Json& object, const char* name) {
|
||||
const auto value = object.find(name);
|
||||
return value == object.end() ? nullptr : value->get_ptr<const Json::string_t*>();
|
||||
}
|
||||
|
||||
const Json::array_t* ArrayField(const Json& object, const char* name) {
|
||||
const auto value = object.find(name);
|
||||
return value == object.end() ? nullptr : value->get_ptr<const Json::array_t*>();
|
||||
}
|
||||
|
||||
bool ParseBytes(std::string_view text, std::vector<uint8_t>* bytes) {
|
||||
if (text.empty() || (text.size() % 2) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes->resize(text.size() / 2);
|
||||
for (size_t index = 0; index < bytes->size(); index++) {
|
||||
unsigned int value = 0;
|
||||
const char* begin = text.data() + index * 2;
|
||||
const auto [end, error] = std::from_chars(begin, begin + 2, value, 16);
|
||||
if (error != std::errc {} || end != begin + 2) {
|
||||
return false;
|
||||
}
|
||||
(*bytes)[index] = static_cast<uint8_t>(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadJson(const std::filesystem::path& path, Json* root, std::string* error) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return Fail(error, "could not read patch plan");
|
||||
}
|
||||
|
||||
*root = Json::parse(file, nullptr, false);
|
||||
return root->is_object() || Fail(error, "patch plan is not valid JSON");
|
||||
}
|
||||
|
||||
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
|
||||
Json root;
|
||||
if (!ReadJson(path, &root, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* title_id = StringField(root, "title_id");
|
||||
const auto* game_version = StringField(root, "game_version");
|
||||
const auto* process = StringField(root, "process");
|
||||
const auto* patches = ArrayField(root, "patches");
|
||||
if (title_id == nullptr || game_version == nullptr || process == nullptr ||
|
||||
patches == nullptr) {
|
||||
return Fail(error, "invalid patch plan");
|
||||
}
|
||||
|
||||
plan->title_id = *title_id;
|
||||
plan->game_version = *game_version;
|
||||
plan->process = *process;
|
||||
|
||||
for (const auto& patch_json: *patches) {
|
||||
const auto enabled = patch_json.find("enabled");
|
||||
if (enabled != patch_json.end() && enabled->is_boolean() && !enabled->get<bool>()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* name = StringField(patch_json, "name");
|
||||
const auto* writes = ArrayField(patch_json, "writes");
|
||||
if (name == nullptr || writes == nullptr) {
|
||||
return Fail(error, "invalid patch entry");
|
||||
}
|
||||
|
||||
plan->patch_names.push_back(*name);
|
||||
for (const auto& write_json: *writes) {
|
||||
const auto* expected = StringField(write_json, "expected");
|
||||
const auto* replacement = StringField(write_json, "replacement");
|
||||
if (expected == nullptr || replacement == nullptr) {
|
||||
return Fail(error, "invalid patch write");
|
||||
}
|
||||
|
||||
Write write {
|
||||
.patch_name = *name,
|
||||
};
|
||||
if (!ParseBytes(*expected, &write.expected) ||
|
||||
!ParseBytes(*replacement, &write.replacement) ||
|
||||
write.expected.size() != write.replacement.size()) {
|
||||
return Fail(error, "invalid patch bytes");
|
||||
}
|
||||
|
||||
plan->writes.push_back(std::move(write));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValidateTarget(const Plan& plan, const Program* program, std::string* error) {
|
||||
if (program == nullptr || program->elf == nullptr || program->base_vaddr == 0) {
|
||||
return Fail(error, "main executable is not loaded");
|
||||
}
|
||||
|
||||
std::string title_id;
|
||||
std::string game_version;
|
||||
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
|
||||
!SystemContentParamSfoGetString("APP_VER", &game_version)) {
|
||||
return Fail(error, "game metadata is incomplete");
|
||||
}
|
||||
if (!Common::EqualNoCase(program->file_name.filename().string(), plan.process) ||
|
||||
!Common::EqualNoCase(title_id, plan.title_id) || game_version != plan.game_version) {
|
||||
return Fail(error, "patch plan does not match the loaded game");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Common::VirtualMemory::Mode ReadableMode(Elf64_Word flags) {
|
||||
const bool executable = (flags & PF_X) != 0;
|
||||
const bool writable = (flags & PF_W) != 0;
|
||||
if (executable && writable) {
|
||||
return Common::VirtualMemory::Mode::ExecuteReadWrite;
|
||||
}
|
||||
if (executable) {
|
||||
return Common::VirtualMemory::Mode::ExecuteRead;
|
||||
}
|
||||
return writable ? Common::VirtualMemory::Mode::ReadWrite : Common::VirtualMemory::Mode::Read;
|
||||
}
|
||||
|
||||
bool ResolveWrite(const Program& program, Write* write, std::string* error) {
|
||||
const auto* ehdr = program.elf->GetEhdr();
|
||||
const auto* phdr = program.elf->GetPhdr();
|
||||
|
||||
uint64_t match = 0;
|
||||
size_t match_count = 0;
|
||||
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
|
||||
const auto& segment = phdr[index];
|
||||
const bool loaded = segment.p_type == PT_LOAD || segment.p_type == PT_OS_RELRO;
|
||||
if (!loaded || segment.p_filesz < write->expected.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto segment_address = program.base_vaddr + segment.p_vaddr;
|
||||
const bool add_read = (segment.p_flags & PF_R) == 0;
|
||||
Common::VirtualMemory::Mode old_mode {};
|
||||
if (add_read && !Common::VirtualMemory::Protect(segment_address, segment.p_memsz,
|
||||
ReadableMode(segment.p_flags), &old_mode)) {
|
||||
return Fail(error, "could not read a loaded executable segment");
|
||||
}
|
||||
|
||||
const auto* begin = reinterpret_cast<const uint8_t*>(segment_address);
|
||||
const auto* end = begin + segment.p_filesz;
|
||||
for (auto* current = begin; current < end;) {
|
||||
const auto* found =
|
||||
std::search(current, end, write->expected.begin(), write->expected.end());
|
||||
if (found == end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto address = reinterpret_cast<uint64_t>(found);
|
||||
if (match == 0) {
|
||||
match = address;
|
||||
}
|
||||
match_count++;
|
||||
current = found + 1;
|
||||
}
|
||||
|
||||
if (add_read &&
|
||||
!Common::VirtualMemory::Protect(segment_address, segment.p_memsz, old_mode)) {
|
||||
return Fail(error, "could not restore executable segment protection");
|
||||
}
|
||||
}
|
||||
|
||||
::printf("Game patch: found %zu entries for '%s'\n", match_count,
|
||||
write->patch_name.c_str());
|
||||
if (match == 0) {
|
||||
return Fail(error, "original bytes not found for '" + write->patch_name + "'");
|
||||
}
|
||||
write->address = match;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareWrites(Plan* plan, const Program& program, std::string* error) {
|
||||
for (auto& write: plan->writes) {
|
||||
if (!ResolveWrite(program, &write, error)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApplyWrites(Plan* plan, std::string* error) {
|
||||
for (auto& write: plan->writes) {
|
||||
Common::VirtualMemory::Mode old_mode {};
|
||||
const auto size = write.replacement.size();
|
||||
if (!Common::VirtualMemory::Protect(
|
||||
write.address, size, Common::VirtualMemory::Mode::ExecuteReadWrite, &old_mode)) {
|
||||
return Fail(error, "could not make patch memory writable");
|
||||
}
|
||||
|
||||
std::memcpy(reinterpret_cast<void*>(write.address), write.replacement.data(), size);
|
||||
if (!Common::VirtualMemory::Protect(write.address, size, old_mode) ||
|
||||
!Common::VirtualMemory::FlushInstructionCache(write.address, size)) {
|
||||
return Fail(error, "could not finalize patch");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Apply(const std::filesystem::path& plan_path, Program* program) {
|
||||
Plan plan;
|
||||
std::string error;
|
||||
if (LoadPlan(plan_path, &plan, &error) && ValidateTarget(plan, program, &error) &&
|
||||
PrepareWrites(&plan, *program, &error) && ApplyWrites(&plan, &error)) {
|
||||
for (const auto& name: plan.patch_names) {
|
||||
::printf("Successfully applied patch: %s\n", name.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
::printf("Game patch error: %s\n", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Loader::GamePatch
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef KYTY_LOADER_GAME_PATCH_H_
|
||||
#define KYTY_LOADER_GAME_PATCH_H_
|
||||
|
||||
#include "common/common.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace Loader {
|
||||
|
||||
struct Program;
|
||||
|
||||
namespace GamePatch {
|
||||
|
||||
bool Apply(const std::filesystem::path& plan_path, Program* program);
|
||||
|
||||
} // namespace GamePatch
|
||||
|
||||
} // namespace Loader
|
||||
|
||||
#endif // KYTY_LOADER_GAME_PATCH_H_
|
||||
+164
-48
@@ -17,6 +17,7 @@
|
||||
#include "kernel/memory.h"
|
||||
#include "kernel/pthread.h"
|
||||
#include "loader/elf.h"
|
||||
#include "loader/gamePatch.h"
|
||||
#include "loader/jit.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
#include "loader/x64InstructionEmulator.h"
|
||||
@@ -35,6 +36,12 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
@@ -344,28 +351,66 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu
|
||||
guest_root_frame[0] = 0;
|
||||
guest_root_frame[1] = 0;
|
||||
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
"movq %%rsp, %%r12\n\t"
|
||||
#if defined(__APPLE__)
|
||||
// Clang on macOS can allocate plain "r" inputs to r12/r13, which the template
|
||||
// clobbers before consuming them. Pin the inputs to registers the SysV guest
|
||||
// preserves without changing register allocation on Windows or Linux.
|
||||
register entry_func_t func_reg asm("rbx") = func;
|
||||
register uintptr_t guest_rsp_reg asm("r14") = guest_rsp;
|
||||
register uintptr_t guest_rbp_reg asm("r15") = guest_rbp;
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
asm volatile(
|
||||
"pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
"movq %%rsp, %%r12\n\t"
|
||||
"movq %%rbp, %%r13\n\t"
|
||||
"movq %[guest_rsp], %%rsp\n\t"
|
||||
"movq %[guest_rbp], %%rbp\n\t"
|
||||
"callq *%[func]\n\t"
|
||||
"movq %%r13, %%rbp\n\t"
|
||||
"movq %%r12, %%rsp\n\t"
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
:
|
||||
#if defined(__APPLE__)
|
||||
: [func] "r"(func_reg), "D"(params),
|
||||
"S"(atexit_func), [guest_rsp] "r"(guest_rsp_reg), [guest_rbp] "r"(guest_rbp_reg)
|
||||
#else
|
||||
: [func] "r"(func), "D"(params),
|
||||
"S"(atexit_func), [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp)
|
||||
#endif
|
||||
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
|
||||
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12",
|
||||
"xmm13", "xmm14", "xmm15");
|
||||
#else
|
||||
// Clobbers prevent inputs from being allocated to r12/r13.
|
||||
asm volatile("movq %%rsp, %%r12\n\t"
|
||||
"movq %%rbp, %%r13\n\t"
|
||||
"movq %[guest_rsp], %%rsp\n\t"
|
||||
"movq %[guest_rbp], %%rbp\n\t"
|
||||
"callq *%[func]\n\t"
|
||||
"movq %%r13, %%rbp\n\t"
|
||||
"movq %%r12, %%rsp\n\t"
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
:
|
||||
: [func] "r"(func), "D"(params),
|
||||
"S"(atexit_func), [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp)
|
||||
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0",
|
||||
"xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9",
|
||||
"xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
|
||||
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "r12", "r13",
|
||||
"xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8",
|
||||
"xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
uintptr_t guest_root_frame[2] = {};
|
||||
|
||||
#if defined(__APPLE__)
|
||||
register entry_func_t func_reg asm("rbx") = func;
|
||||
register uintptr_t guest_rbp_reg asm("r14") = reinterpret_cast<uintptr_t>(guest_root_frame);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
"movq %%rbp, %%r12\n\t"
|
||||
@@ -375,11 +420,29 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
:
|
||||
#if defined(__APPLE__)
|
||||
: [func] "r"(func_reg), "D"(params),
|
||||
"S"(atexit_func), [guest_rbp] "r"(guest_rbp_reg)
|
||||
#else
|
||||
: [func] "r"(func), "D"(params),
|
||||
"S"(atexit_func), [guest_rbp] "r"(guest_root_frame)
|
||||
#endif
|
||||
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1",
|
||||
"xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
|
||||
"xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#else
|
||||
// Keep inputs out of r12.
|
||||
asm volatile("movq %%rbp, %%r12\n\t"
|
||||
"movq %[guest_rbp], %%rbp\n\t"
|
||||
"callq *%[func]\n\t"
|
||||
"movq %%r12, %%rbp\n\t"
|
||||
:
|
||||
: [func] "r"(func), "D"(params),
|
||||
"S"(atexit_func), [guest_rbp] "r"(guest_root_frame)
|
||||
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "r12", "xmm0",
|
||||
"xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10",
|
||||
"xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
|
||||
#endif
|
||||
#else
|
||||
(void)stack_top;
|
||||
reinterpret_cast<entry_func_t>(addr)(params, atexit_func);
|
||||
@@ -492,6 +555,69 @@ void KYTY_SYSV_ABI SysStackWalkX86(uint64_t rbp, void** stack, int* depth) {
|
||||
SysStackWalkX86(rbp, rbp, stack, depth);
|
||||
}
|
||||
|
||||
// Probe diagnostic ranges without raising another fault.
|
||||
static bool IsReadableRange(uint64_t addr, uint64_t size) {
|
||||
if (addr == 0 || size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t end = addr + size;
|
||||
if (end < addr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
uint64_t current = addr;
|
||||
while (current < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi {};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(current), &mbi, sizeof(mbi)) == 0 ||
|
||||
mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
|
||||
return false;
|
||||
}
|
||||
const auto region_end = reinterpret_cast<uint64_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
if (region_end <= current) {
|
||||
return false;
|
||||
}
|
||||
current = std::min(region_end, end);
|
||||
}
|
||||
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
|
||||
const auto page_size = static_cast<uint64_t>(sysconf(_SC_PAGESIZE));
|
||||
if (page_size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t current = addr; current < end;) {
|
||||
uint8_t probe = 0;
|
||||
|
||||
iovec local {&probe, sizeof(probe)};
|
||||
iovec remote {reinterpret_cast<void*>(current), sizeof(probe)};
|
||||
|
||||
if (process_vm_readv(getpid(), &local, 1, &remote, 1, 0) !=
|
||||
static_cast<ssize_t>(sizeof(probe))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t next = (current & ~(page_size - 1)) + page_size;
|
||||
if (next <= current) { // wrapped at the top of the address space
|
||||
break;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
#else
|
||||
(void)end;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsDumpableRange(uint64_t addr, uint64_t size) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
|
||||
return IsReadableRange(addr, size);
|
||||
#else
|
||||
(void)size;
|
||||
return addr != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exception_info) {
|
||||
const auto* info = &exception_info;
|
||||
|
||||
@@ -537,6 +663,12 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
LOGF("exception module: %s\n", module_name);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Dl_info module_info {};
|
||||
if (::dladdr(reinterpret_cast<void*>(info->exception_address), &module_info) != 0 &&
|
||||
module_info.dli_fname != nullptr) {
|
||||
LOGF("exception module: %s\n", module_info.dli_fname);
|
||||
}
|
||||
#endif
|
||||
if (info->exception_address != 0) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
@@ -560,12 +692,18 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
LOGF("code-32: unavailable\n");
|
||||
}
|
||||
#else
|
||||
LOGF("code-32:");
|
||||
for (uint64_t i = 0; i < 64; i++) {
|
||||
LOGF(" %02" PRIx32, static_cast<uint32_t>(*reinterpret_cast<const uint8_t*>(
|
||||
info->exception_address + i - 32)));
|
||||
const auto fault_addr = info->exception_address;
|
||||
const auto dump_start = (fault_addr >= 32 ? fault_addr - 32 : fault_addr);
|
||||
if (IsReadableRange(dump_start, 64)) {
|
||||
auto* dump_ptr = reinterpret_cast<const uint8_t*>(dump_start);
|
||||
LOGF("code-32:");
|
||||
for (uint32_t i = 0; i < 64; i++) {
|
||||
LOGF(" %02" PRIx32, static_cast<uint32_t>(dump_ptr[i]));
|
||||
}
|
||||
LOGF("\n");
|
||||
} else {
|
||||
LOGF("code-32: unavailable\n");
|
||||
}
|
||||
LOGF("\n");
|
||||
#endif
|
||||
} else {
|
||||
LOGF("code: unavailable\n");
|
||||
@@ -583,36 +721,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
LOGF("regs: r12=%016" PRIx64 " r13=%016" PRIx64 " r14=%016" PRIx64 " r15=%016" PRIx64 "\n",
|
||||
info->r12, info->r13, info->r14, info->r15);
|
||||
|
||||
auto is_readable_range = [](uint64_t addr, uint64_t size) {
|
||||
if (addr == 0 || size == 0) {
|
||||
return false;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
uint64_t current = addr;
|
||||
uint64_t end = addr + size;
|
||||
if (end < addr) {
|
||||
return false;
|
||||
}
|
||||
while (current < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi {};
|
||||
if (VirtualQuery(reinterpret_cast<const void*>(current), &mbi, sizeof(mbi)) == 0 ||
|
||||
mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
|
||||
return false;
|
||||
}
|
||||
const auto region_end = reinterpret_cast<uint64_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
if (region_end <= current) {
|
||||
return false;
|
||||
}
|
||||
current = std::min(region_end, end);
|
||||
}
|
||||
#else
|
||||
(void)addr;
|
||||
(void)size;
|
||||
#endif
|
||||
return true;
|
||||
};
|
||||
|
||||
if (is_readable_range(info->rsp, 16u * sizeof(uint64_t))) {
|
||||
if (IsReadableRange(info->rsp, 16u * sizeof(uint64_t))) {
|
||||
auto* stack = reinterpret_cast<const uint64_t*>(info->rsp);
|
||||
LOGF("stack:");
|
||||
for (uint64_t i = 0; i < 16; i++) {
|
||||
@@ -643,6 +752,9 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
#else
|
||||
auto* dump_ptr = reinterpret_cast<const uint8_t*>(addr >= 16 ? addr - 16 : addr);
|
||||
const auto dump_size = 32u;
|
||||
if (!IsReadableRange(reinterpret_cast<uint64_t>(dump_ptr), dump_size)) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOGF("%s code: addr=%016" PRIx64 ", off=%016" PRIx64 ", module=%s:", name, addr,
|
||||
@@ -658,7 +770,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
dump_guest_code("guest rbx[0]", info->rbx);
|
||||
dump_guest_code("guest rcx[0]", info->rcx);
|
||||
dump_guest_code("guest rsi[0]", info->rsi);
|
||||
if (info->rsp != 0) {
|
||||
if (IsDumpableRange(info->rsp, 16u * sizeof(uint64_t))) {
|
||||
auto* stack = reinterpret_cast<const uint64_t*>(info->rsp);
|
||||
for (uint64_t i = 0; i < 16; i++) {
|
||||
char name[32] {};
|
||||
@@ -687,7 +799,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
}
|
||||
}
|
||||
|
||||
auto dump_guest_qwords = [&is_readable_range](const char* name, uint64_t addr) {
|
||||
auto dump_guest_qwords = [](const char* name, uint64_t addr) {
|
||||
if (addr == 0) {
|
||||
LOGF("%s = 0\n", name);
|
||||
return;
|
||||
@@ -702,7 +814,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!is_readable_range(addr, 8u * sizeof(uint64_t))) {
|
||||
if (!IsReadableRange(addr, 8u * sizeof(uint64_t))) {
|
||||
LOGF("%s = %016" PRIx64 " (unmapped)\n", name, addr);
|
||||
return;
|
||||
}
|
||||
@@ -727,7 +839,8 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
dump_guest_qwords("guest r14", info->r14);
|
||||
dump_guest_qwords("guest r15", info->r15);
|
||||
|
||||
if (info->exception_address == 0x000000090064364e && info->rbx != 0) {
|
||||
if (info->exception_address == 0x000000090064364e &&
|
||||
IsDumpableRange(info->rbx, sizeof(uint64_t))) {
|
||||
auto* local = reinterpret_cast<const uint64_t*>(info->rbx);
|
||||
dump_guest_qwords("vorbis obj", local[0]);
|
||||
dump_guest_qwords("vorbis len", info->rcx);
|
||||
@@ -1326,7 +1439,7 @@ void RuntimeLinker::SaveProgram(Program* program, const std::filesystem::path& e
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLinker::Execute() {
|
||||
void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
|
||||
KYTY_PROFILER_THREAD("Thread_Main");
|
||||
|
||||
Libs::LibKernel::PthreadInitSelfForMainThread();
|
||||
@@ -1346,6 +1459,9 @@ void RuntimeLinker::Execute() {
|
||||
|
||||
PreloadAdjacentPrograms();
|
||||
RelocateAll();
|
||||
if (!game_patch.empty()) {
|
||||
GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front());
|
||||
}
|
||||
StartAllModules();
|
||||
|
||||
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
|
||||
|
||||
@@ -154,7 +154,7 @@ public:
|
||||
void RelocateAll();
|
||||
void RelocateProgram(Program* program);
|
||||
|
||||
void Execute();
|
||||
void Execute(const std::filesystem::path& game_patch = {});
|
||||
int StartModule(Program* program, size_t args, const void* argp, module_func_t func);
|
||||
int StopModule(Program* program, size_t args, const void* argp, module_func_t func);
|
||||
void StartAllModules();
|
||||
|
||||
@@ -4,20 +4,13 @@
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif !defined(__APPLE__)
|
||||
#include <sched.h>
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
|
||||
namespace Loader::X64InstructionEmulator {
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static M128A* GetContextXmm(PCONTEXT context, uint8_t index) {
|
||||
if (context == nullptr || index >= 16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &context->Xmm0 + index;
|
||||
}
|
||||
|
||||
static uint64_t ExtractBitField(uint64_t value, uint32_t length, uint32_t index) {
|
||||
length &= 0x3fu;
|
||||
index &= 0x3fu;
|
||||
@@ -63,6 +56,16 @@ static uint64_t InsertBitField(uint64_t dst, uint64_t src, uint32_t length, uint
|
||||
return (dst & ~shifted) | src_shifted;
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static M128A* GetContextXmm(PCONTEXT context, uint8_t index) {
|
||||
if (context == nullptr || index >= 16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &context->Xmm0 + index;
|
||||
}
|
||||
|
||||
static bool TryEmulateSse4a(PCONTEXT context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
@@ -140,12 +143,125 @@ static bool TryEmulateMonitorxMwaitx(PCONTEXT context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#elif !defined(__APPLE__)
|
||||
|
||||
// Linux signal contexts expose registers through ucontext_t.
|
||||
|
||||
static uint32_t* GetContextXmm(ucontext_t* context, uint8_t index) {
|
||||
if (context == nullptr || index >= 16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* fpregs = context->uc_mcontext.fpregs;
|
||||
if (fpregs == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t*>(fpregs->_xmm[index].element);
|
||||
}
|
||||
|
||||
static uint64_t GetXmmLow(const uint32_t* xmm) {
|
||||
return static_cast<uint64_t>(xmm[0]) | (static_cast<uint64_t>(xmm[1]) << 32u);
|
||||
}
|
||||
|
||||
static void SetXmmLow(uint32_t* xmm, uint64_t value) {
|
||||
xmm[0] = static_cast<uint32_t>(value);
|
||||
xmm[1] = static_cast<uint32_t>(value >> 32u);
|
||||
}
|
||||
|
||||
static void SetXmmHigh(uint32_t* xmm, uint64_t value) {
|
||||
xmm[2] = static_cast<uint32_t>(value);
|
||||
xmm[3] = static_cast<uint32_t>(value >> 32u);
|
||||
}
|
||||
|
||||
static bool TryEmulateSse4a(ucontext_t* context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& rip_reg = context->uc_mcontext.gregs[REG_RIP];
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(rip_reg);
|
||||
|
||||
const uint8_t prefix = rip[0];
|
||||
if (prefix != 0x66 && prefix != 0xf2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t offset = 1;
|
||||
uint8_t rex = 0;
|
||||
if ((rip[offset] & 0xf0u) == 0x40u) {
|
||||
rex = rip[offset];
|
||||
offset++;
|
||||
}
|
||||
|
||||
if (rip[offset] != 0x0f || rip[offset + 1] != 0x78) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto modrm = rip[offset + 2];
|
||||
if ((modrm & 0xc0u) != 0xc0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t reg = ((modrm >> 3u) & 0x07u) | ((rex & 0x04u) << 1u);
|
||||
const uint8_t rm = (modrm & 0x07u) | ((rex & 0x01u) << 3u);
|
||||
const uint8_t length = rip[offset + 3];
|
||||
const uint8_t index = rip[offset + 4];
|
||||
|
||||
// AMD SSE4a immediate-form EXTRQ/INSERTQ.
|
||||
if (prefix == 0x66) {
|
||||
auto* dst = GetContextXmm(context, rm);
|
||||
if (dst == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetXmmLow(dst, ExtractBitField(GetXmmLow(dst), length, index));
|
||||
SetXmmHigh(dst, 0);
|
||||
rip_reg += static_cast<greg_t>(offset + 5);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* dst = GetContextXmm(context, reg);
|
||||
auto* src = GetContextXmm(context, rm);
|
||||
if (dst == nullptr || src == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetXmmLow(dst, InsertBitField(GetXmmLow(dst), GetXmmLow(src), length, index));
|
||||
rip_reg += static_cast<greg_t>(offset + 5);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(ucontext_t* context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& rip_reg = context->uc_mcontext.gregs[REG_RIP];
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(rip_reg);
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Approximate AMD MONITORX/MWAITX as no-op/yield.
|
||||
if (rip[2] == 0xfb) {
|
||||
::sched_yield();
|
||||
}
|
||||
rip_reg += 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool TryEmulate(void* native_context) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
auto* context = static_cast<PCONTEXT>(native_context);
|
||||
return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context);
|
||||
#elif !defined(__APPLE__)
|
||||
auto* context = static_cast<ucontext_t*>(native_context);
|
||||
return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context);
|
||||
#else
|
||||
(void)native_context;
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+15
-1
@@ -42,6 +42,8 @@ static void PrintUsage() {
|
||||
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
|
||||
::printf("Options:\n");
|
||||
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
|
||||
::printf(
|
||||
" --game-patch <json> Validated patch plan to apply before entry.\n");
|
||||
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
||||
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
||||
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
||||
@@ -59,7 +61,8 @@ static void PrintUsage() {
|
||||
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
|
||||
::printf(" --ngg-rectlist-draw <true|false> Draw rect-list auto draws using the NGG "
|
||||
"4-vertex path.\n");
|
||||
::printf(" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
||||
::printf(
|
||||
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
||||
::printf(" --rd Enable RenderDoc capture.\n");
|
||||
}
|
||||
|
||||
@@ -147,6 +150,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("--game must point to an existing directory or ELF: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--game-patch") {
|
||||
if (!options.game_patch.empty()) {
|
||||
::printf("--game-patch can only be specified once\n");
|
||||
return false;
|
||||
}
|
||||
value = Common::FixFilenameSlash(value);
|
||||
if (!Common::File::IsFileExisting(value)) {
|
||||
::printf("--game-patch must point to an existing file: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
options.game_patch = value;
|
||||
} else if (arg == "--screen-width") {
|
||||
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
|
||||
} else if (arg == "--screen-height") {
|
||||
|
||||
+23
-7
@@ -1,26 +1,34 @@
|
||||
# Keep the existing Linux IWYU warnings non-fatal.
|
||||
set(KYTY_IWYU_COMMON "-Xiwyu;--cxx17ns;-Qunused-arguments")
|
||||
if (LINUX)
|
||||
set(KYTY_IWYU_STRICT "")
|
||||
else()
|
||||
set(KYTY_IWYU_STRICT ";-Werror")
|
||||
endif()
|
||||
|
||||
function(include_what_you_use target dirs)
|
||||
if (NOT LINUX AND CLANG AND ("${target}" IN_LIST KYTY_IWYU))
|
||||
if (CLANG AND ("${target}" IN_LIST KYTY_IWYU))
|
||||
find_program (CLANG_IWYU_EXE NAMES "include-what-you-use")
|
||||
if (CLANG_IWYU_EXE)
|
||||
set_target_properties(${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE "${CLANG_IWYU_EXE};-Xiwyu;--cxx17ns;-Qunused-arguments;-Werror")
|
||||
set_target_properties(${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE "${CLANG_IWYU_EXE};${KYTY_IWYU_COMMON}${KYTY_IWYU_STRICT}")
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(include_what_you_use_with_mappings target dirs mappings)
|
||||
if (NOT LINUX AND CLANG AND ("${target}" IN_LIST KYTY_IWYU))
|
||||
if (CLANG AND ("${target}" IN_LIST KYTY_IWYU))
|
||||
find_program (CLANG_IWYU_EXE NAMES "include-what-you-use")
|
||||
if (CLANG_IWYU_EXE)
|
||||
foreach(map ${mappings})
|
||||
list(APPEND mapdirs ";-Xiwyu;--mapping_file=${map}")
|
||||
endforeach()
|
||||
set_target_properties(${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE "${CLANG_IWYU_EXE};${mapdirs};-Xiwyu;--cxx17ns;-Qunused-arguments;-Werror")
|
||||
set_target_properties(${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE "${CLANG_IWYU_EXE};${mapdirs};${KYTY_IWYU_COMMON}${KYTY_IWYU_STRICT}")
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(clang_tidy_check target config headers dirs)
|
||||
if (CLANG AND ("${target}" IN_LIST KYTY_CLANG_TYDY) AND NOT KYTY_CLANG_CL)
|
||||
if (KYTY_ENABLE_CLANG_TIDY AND CLANG AND ("${target}" IN_LIST KYTY_CLANG_TIDY) AND NOT KYTY_CLANG_CL)
|
||||
find_program (CLANG_TIDY_EXE NAMES "clang-tidy")
|
||||
if (CLANG_TIDY_EXE)
|
||||
set(std_arg "-extra-arg=-std=c++${CMAKE_CXX_STANDARD}")
|
||||
@@ -41,7 +49,7 @@ function(clang_tidy_check target config headers dirs)
|
||||
endfunction()
|
||||
|
||||
function(clang_tidy_fix target config headers dirs)
|
||||
if (CLANG AND ("${target}" IN_LIST KYTY_CLANG_TYDY))
|
||||
if (KYTY_ENABLE_CLANG_TIDY AND CLANG AND ("${target}" IN_LIST KYTY_CLANG_TIDY))
|
||||
find_program (CLANG_TIDY_EXE NAMES "clang-tidy")
|
||||
if (CLANG_TIDY_EXE)
|
||||
set(std_arg "-extra-arg=-std=c++${CMAKE_CXX_STANDARD}")
|
||||
@@ -68,7 +76,15 @@ set(KYTY_WARNINGS_ARE_ERRORS OFF)
|
||||
set(KYTY_C_FLAGS "")
|
||||
set(KYTY_CPP_FLAGS "")
|
||||
|
||||
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
|
||||
# Apple's /usr/bin/ar does not understand @response-file syntax, and macOS has a
|
||||
# large ARG_MAX so response files aren't needed. Note: the Ninja generator forces a
|
||||
# response file whenever CMAKE_NINJA_FORCE_RESPONSE_FILE is *defined* (it tests
|
||||
# definedness, not the value), so on Apple it must be fully unset, not set to 0.
|
||||
if(APPLE)
|
||||
unset(CMAKE_NINJA_FORCE_RESPONSE_FILE CACHE)
|
||||
else()
|
||||
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
|
||||
endif()
|
||||
|
||||
if(KYTY_CLANG_CL)
|
||||
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <map>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/wait.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
@@ -39,6 +46,123 @@ void Check(bool value, const char *text) {
|
||||
}
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// POSIX shims for the shared test body.
|
||||
using DWORD = uint32_t;
|
||||
constexpr uint32_t PAGE_NOACCESS = 1;
|
||||
constexpr uint32_t PAGE_READONLY = 2;
|
||||
constexpr uint32_t PAGE_READWRITE = 3;
|
||||
constexpr uint32_t MEM_RESERVE = 0;
|
||||
constexpr uint32_t MEM_COMMIT = 0;
|
||||
constexpr uint32_t MEM_RELEASE = 0;
|
||||
|
||||
int ToHostProt(uint32_t protection) {
|
||||
switch (protection) {
|
||||
case PAGE_NOACCESS:
|
||||
return PROT_NONE;
|
||||
case PAGE_READONLY:
|
||||
return PROT_READ;
|
||||
default:
|
||||
return PROT_READ | PROT_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Protection(const void *address) {
|
||||
const auto addr = reinterpret_cast<uintptr_t>(address);
|
||||
std::FILE *maps = std::fopen("/proc/self/maps", "r");
|
||||
Check(maps != nullptr, "open /proc/self/maps failed");
|
||||
char line[512];
|
||||
uint32_t result = 0; // 0 => not mapped at all
|
||||
while (std::fgets(line, sizeof(line), maps) != nullptr) {
|
||||
unsigned long start = 0;
|
||||
unsigned long end = 0;
|
||||
char perms[8]{};
|
||||
if (std::sscanf(line, "%lx-%lx %7s", &start, &end, perms) != 3) {
|
||||
continue;
|
||||
}
|
||||
if (addr >= start && addr < end) {
|
||||
result = perms[1] == 'w' ? PAGE_READWRITE
|
||||
: perms[0] == 'r' ? PAGE_READONLY
|
||||
: PAGE_NOACCESS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::fclose(maps);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsWritable(const void *address) { return Protection(address) == PAGE_READWRITE; }
|
||||
|
||||
// munmap needs a length where VirtualFree's callers pass 0, so sizes are remembered here.
|
||||
std::map<void *, size_t> &AllocationSizes() {
|
||||
static std::map<void *, size_t> sizes;
|
||||
return sizes;
|
||||
}
|
||||
|
||||
void *VirtualAlloc(void *address, size_t size, DWORD /*type*/, uint32_t protection) {
|
||||
const int extra = address != nullptr ? MAP_FIXED_NOREPLACE : 0;
|
||||
void *raw = ::mmap(address, size, ToHostProt(protection),
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | extra, -1, 0);
|
||||
if (raw == MAP_FAILED) {
|
||||
return nullptr;
|
||||
}
|
||||
AllocationSizes()[raw] = size;
|
||||
return raw;
|
||||
}
|
||||
|
||||
int VirtualFree(void *address, size_t /*size*/, DWORD /*type*/) {
|
||||
auto &sizes = AllocationSizes();
|
||||
auto it = sizes.find(address);
|
||||
if (it == sizes.end()) {
|
||||
return 0;
|
||||
}
|
||||
const int ok = ::munmap(address, it->second) == 0 ? 1 : 0;
|
||||
sizes.erase(it);
|
||||
return ok;
|
||||
}
|
||||
|
||||
[[maybe_unused]] int VirtualProtect(void *address, size_t size, uint32_t protection,
|
||||
DWORD *old_protection) {
|
||||
if (old_protection != nullptr) {
|
||||
*old_protection = Protection(address);
|
||||
}
|
||||
return ::mprotect(address, size, ToHostProt(protection)) == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
// Two views of one anonymous shared object.
|
||||
class SharedPage final {
|
||||
public:
|
||||
SharedPage(uintptr_t address, uint64_t size) : size_(static_cast<size_t>(size)) {
|
||||
fd_ = static_cast<int>(::syscall(SYS_memfd_create, "kyty-shared-page", 0u));
|
||||
Check(fd_ >= 0, "memfd_create failed");
|
||||
Check(::ftruncate(fd_, static_cast<off_t>(size)) == 0, "ftruncate failed");
|
||||
guest = static_cast<uint8_t *>(::mmap(reinterpret_cast<void *>(address), size_,
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_FIXED_NOREPLACE, fd_, 0));
|
||||
Check(guest == reinterpret_cast<void *>(address), "fixed shared view failed");
|
||||
backing = static_cast<uint8_t *>(
|
||||
::mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
|
||||
Check(backing != MAP_FAILED && backing != nullptr, "shared backing view failed");
|
||||
}
|
||||
|
||||
~SharedPage() {
|
||||
Check(::munmap(backing, size_) == 0, "shared backing unmap failed");
|
||||
Check(::munmap(guest, size_) == 0, "shared guest unmap failed");
|
||||
Check(::close(fd_) == 0, "shared mapping close failed");
|
||||
}
|
||||
|
||||
SharedPage(const SharedPage &) = delete;
|
||||
SharedPage &operator=(const SharedPage &) = delete;
|
||||
|
||||
uint8_t *guest = nullptr;
|
||||
uint8_t *backing = nullptr;
|
||||
|
||||
private:
|
||||
size_t size_ = 0;
|
||||
int fd_ = -1;
|
||||
};
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool IsWritable(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
@@ -84,6 +208,8 @@ class SharedPage final {
|
||||
private:
|
||||
HANDLE mapping_ = nullptr;
|
||||
};
|
||||
#endif
|
||||
#if 1
|
||||
|
||||
bool DummyFault(void *, PageFaultAccess, uint64_t, uint64_t, PageFaultPhase) noexcept {
|
||||
return true;
|
||||
@@ -246,6 +372,7 @@ void UnmapContended() noexcept {
|
||||
g_unmap_contended.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LONG CALLBACK NativeTrackerFaultHandler(EXCEPTION_POINTERS *exception) {
|
||||
if (exception == nullptr || exception->ExceptionRecord == nullptr ||
|
||||
exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
|
||||
@@ -266,6 +393,45 @@ LONG CALLBACK NativeTrackerFaultHandler(EXCEPTION_POINTERS *exception) {
|
||||
? EXCEPTION_CONTINUE_EXECUTION
|
||||
: EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
#else
|
||||
// SIGSEGV stands in for the vectored exception handler.
|
||||
void NativeTrackerFaultHandler(int signal_number, siginfo_t *info, void *native_context) {
|
||||
auto *context = static_cast<ucontext_t *>(native_context);
|
||||
const auto error_code = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_ERR]);
|
||||
const auto access = (error_code & 0x10u) != 0 ? PageFaultAccess::Execute
|
||||
: (error_code & 0x02u) != 0 ? PageFaultAccess::Write
|
||||
: PageFaultAccess::Read;
|
||||
auto *page_manager = g_native_page_manager.load(std::memory_order_acquire);
|
||||
if (page_manager != nullptr) {
|
||||
g_native_fault_entered.store(true, std::memory_order_release);
|
||||
if (page_manager->HandleFault(access, reinterpret_cast<uint64_t>(info->si_addr))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
struct sigaction restore {};
|
||||
restore.sa_handler = SIG_DFL;
|
||||
sigemptyset(&restore.sa_mask);
|
||||
::sigaction(signal_number, &restore, nullptr);
|
||||
}
|
||||
|
||||
struct sigaction g_saved_segv_action {};
|
||||
|
||||
void *AddVectoredExceptionHandler(unsigned long /*first*/,
|
||||
void (*handler)(int, siginfo_t *, void *)) {
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = handler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
if (::sigaction(SIGSEGV, &action, &g_saved_segv_action) != 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<void *>(handler);
|
||||
}
|
||||
|
||||
int RemoveVectoredExceptionHandler(void * /*token*/) {
|
||||
return ::sigaction(SIGSEGV, &g_saved_segv_action, nullptr) == 0 ? 1 : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void TestPendingFaultBlocksUploadConsumption() {
|
||||
constexpr uintptr_t base = 0x0000000200010000ull;
|
||||
@@ -978,14 +1144,17 @@ void TestCrossRegionUpload() {
|
||||
}
|
||||
|
||||
void TestFatalPaths() {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
char path[MAX_PATH]{};
|
||||
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0,
|
||||
"GetModuleFileName failed");
|
||||
#endif
|
||||
for (const char *name : {"gpu-dirty-fault", "gpu-dirty-read", "virtual-gpu-read",
|
||||
"gpu-dirty-explicit-cpu",
|
||||
"unmapped", "reentrant-upload",
|
||||
"writable-upload-race", "gpu-dirty-unmap-race",
|
||||
"missing-download-bytes"}) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::string command = std::string("\"") + path + "\" --death " + name;
|
||||
std::vector<char> mutable_command(command.begin(), command.end());
|
||||
mutable_command.push_back('\0');
|
||||
@@ -1004,6 +1173,21 @@ void TestFatalPaths() {
|
||||
"MemoryTracker death path used the wrong exit");
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
#else
|
||||
const pid_t pid = ::fork();
|
||||
Check(pid >= 0, "fork failed");
|
||||
if (pid == 0) {
|
||||
::execl("/proc/self/exe", "MemoryTrackerTests", "--death", name, nullptr);
|
||||
std::_Exit(0x7e);
|
||||
}
|
||||
int status = 0;
|
||||
Check(::waitpid(pid, &status, 0) == pid, "waitpid failed");
|
||||
// Exit status carries only the low 8 bits.
|
||||
const bool fatal_exit = WIFEXITED(status) && (WEXITSTATUS(status) == (321 & 0xff) ||
|
||||
WEXITSTATUS(status) == (322 & 0xff));
|
||||
Check(fatal_exit || WIFSIGNALED(status),
|
||||
"MemoryTracker death path used the wrong exit");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1011,7 +1195,7 @@ void TestFatalPaths() {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#if 1
|
||||
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
|
||||
RunDeathCase(argv[2]);
|
||||
}
|
||||
|
||||
+150
-1
@@ -17,6 +17,13 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <map>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/wait.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
@@ -32,6 +39,76 @@ void Check(bool value, const char *text) {
|
||||
}
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
// POSIX shims for the shared test body.
|
||||
using DWORD = uint32_t;
|
||||
constexpr uint32_t PAGE_NOACCESS = 1;
|
||||
constexpr uint32_t PAGE_READONLY = 2;
|
||||
constexpr uint32_t PAGE_READWRITE = 3;
|
||||
constexpr uint32_t MEM_RELEASE = 0;
|
||||
|
||||
int ToHostProt(uint32_t protection) {
|
||||
switch (protection) {
|
||||
case PAGE_NOACCESS:
|
||||
return PROT_NONE;
|
||||
case PAGE_READONLY:
|
||||
return PROT_READ;
|
||||
default:
|
||||
return PROT_READ | PROT_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Protection(const void *address) {
|
||||
const auto addr = reinterpret_cast<uintptr_t>(address);
|
||||
std::FILE *maps = std::fopen("/proc/self/maps", "r");
|
||||
Check(maps != nullptr, "open /proc/self/maps failed");
|
||||
char line[512];
|
||||
uint32_t result = 0; // 0 => not mapped at all
|
||||
while (std::fgets(line, sizeof(line), maps) != nullptr) {
|
||||
unsigned long start = 0;
|
||||
unsigned long end = 0;
|
||||
char perms[8]{};
|
||||
if (std::sscanf(line, "%lx-%lx %7s", &start, &end, perms) != 3) {
|
||||
continue;
|
||||
}
|
||||
if (addr >= start && addr < end) {
|
||||
result = perms[1] == 'w' ? PAGE_READWRITE
|
||||
: perms[0] == 'r' ? PAGE_READONLY
|
||||
: PAGE_NOACCESS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::fclose(maps);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsWritable(const void *address) { return Protection(address) == PAGE_READWRITE; }
|
||||
|
||||
// munmap needs the length that VirtualFree's callers pass as 0, so sizes are remembered here.
|
||||
std::map<void *, size_t> &AllocationSizes() {
|
||||
static std::map<void *, size_t> sizes;
|
||||
return sizes;
|
||||
}
|
||||
|
||||
int VirtualFree(void *address, size_t /*size*/, DWORD /*type*/) {
|
||||
auto &sizes = AllocationSizes();
|
||||
auto it = sizes.find(address);
|
||||
if (it == sizes.end()) {
|
||||
return 0;
|
||||
}
|
||||
const int ok = ::munmap(address, it->second) == 0 ? 1 : 0;
|
||||
sizes.erase(it);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int VirtualProtect(void *address, size_t size, uint32_t protection, DWORD *old_protection) {
|
||||
if (old_protection != nullptr) {
|
||||
*old_protection = Protection(address);
|
||||
}
|
||||
return ::mprotect(address, size, ToHostProt(protection)) == 0 ? 1 : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool IsWritable(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
@@ -44,6 +121,8 @@ uint32_t Protection(const void *address) {
|
||||
Check(VirtualQuery(address, &info, sizeof(info)) != 0, "VirtualQuery failed");
|
||||
return info.Protect;
|
||||
}
|
||||
#endif
|
||||
#if 1
|
||||
|
||||
struct FaultContext {
|
||||
PageManager *manager = nullptr;
|
||||
@@ -61,6 +140,7 @@ std::atomic_bool g_delay_native_fault{false};
|
||||
std::atomic_bool g_native_fault_entered{false};
|
||||
std::atomic_bool g_release_native_fault{false};
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LONG CALLBACK NativeFaultHandler(EXCEPTION_POINTERS *exception) {
|
||||
if (exception == nullptr || exception->ExceptionRecord == nullptr ||
|
||||
exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
|
||||
@@ -86,6 +166,50 @@ LONG CALLBACK NativeFaultHandler(EXCEPTION_POINTERS *exception) {
|
||||
? EXCEPTION_CONTINUE_EXECUTION
|
||||
: EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
#else
|
||||
// SIGSEGV stands in for the vectored exception handler.
|
||||
void NativeFaultHandler(int signal_number, siginfo_t *info, void *native_context) {
|
||||
auto *context = static_cast<ucontext_t *>(native_context);
|
||||
const auto error_code = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_ERR]);
|
||||
const auto access = (error_code & 0x10u) != 0 ? PageFaultAccess::Execute
|
||||
: (error_code & 0x02u) != 0 ? PageFaultAccess::Write
|
||||
: PageFaultAccess::Read;
|
||||
auto *manager = g_native_fault_manager.load(std::memory_order_acquire);
|
||||
if (manager != nullptr) {
|
||||
if (g_delay_native_fault.load(std::memory_order_acquire)) {
|
||||
g_native_fault_entered.store(true, std::memory_order_release);
|
||||
while (!g_release_native_fault.load(std::memory_order_acquire)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
if (manager->HandleFault(access, reinterpret_cast<uint64_t>(info->si_addr))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
struct sigaction restore {};
|
||||
restore.sa_handler = SIG_DFL;
|
||||
sigemptyset(&restore.sa_mask);
|
||||
::sigaction(signal_number, &restore, nullptr);
|
||||
}
|
||||
|
||||
struct sigaction g_saved_segv_action {};
|
||||
|
||||
void *AddVectoredExceptionHandler(unsigned long /*first*/,
|
||||
void (*handler)(int, siginfo_t *, void *)) {
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = handler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
if (::sigaction(SIGSEGV, &action, &g_saved_segv_action) != 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<void *>(handler);
|
||||
}
|
||||
|
||||
int RemoveVectoredExceptionHandler(void * /*token*/) {
|
||||
return ::sigaction(SIGSEGV, &g_saved_segv_action, nullptr) == 0 ? 1 : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool InvalidateFault(void *context, Libs::Graphics::PageFaultAccess, uint64_t vaddr, uint64_t size,
|
||||
Libs::Graphics::PageFaultPhase phase) noexcept {
|
||||
@@ -112,11 +236,20 @@ bool InvalidateFault(void *context, Libs::Graphics::PageFaultAccess, uint64_t va
|
||||
|
||||
uint8_t *Allocate(uint64_t size, uint32_t protection = PAGE_READWRITE) {
|
||||
constexpr uintptr_t test_address = 0x0000000200010000ull;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(test_address), size,
|
||||
MEM_RESERVE | MEM_COMMIT, protection));
|
||||
Check(memory == reinterpret_cast<void *>(test_address),
|
||||
"fixed low VirtualAlloc failed");
|
||||
#else
|
||||
// Do not overwrite a leaked mapping from an earlier case.
|
||||
void *raw = ::mmap(reinterpret_cast<void *>(test_address), size, ToHostProt(protection),
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
Check(raw == reinterpret_cast<void *>(test_address), "fixed low mmap failed");
|
||||
auto *memory = static_cast<uint8_t *>(raw);
|
||||
AllocationSizes()[raw] = static_cast<size_t>(size);
|
||||
#endif
|
||||
return memory;
|
||||
}
|
||||
|
||||
@@ -497,6 +630,7 @@ void TestCrossRegionRange() {
|
||||
}
|
||||
|
||||
void CheckDeathCase(const char *name) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
char path[MAX_PATH]{};
|
||||
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0,
|
||||
"GetModuleFileName failed");
|
||||
@@ -518,6 +652,21 @@ void CheckDeathCase(const char *name) {
|
||||
"death case did not use the PageManager fatal exit");
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
#else
|
||||
const pid_t pid = ::fork();
|
||||
Check(pid >= 0, "fork failed");
|
||||
if (pid == 0) {
|
||||
::execl("/proc/self/exe", "PageManagerTests", "--death", name, nullptr);
|
||||
std::_Exit(0x7e);
|
||||
}
|
||||
int status = 0;
|
||||
Check(::waitpid(pid, &status, 0) == pid, "waitpid failed");
|
||||
// Exit status carries only the low 8 bits.
|
||||
const bool fatal_exit = WIFEXITED(status) && WEXITSTATUS(status) == (322 & 0xff);
|
||||
const bool fatal_signal = WIFSIGNALED(status);
|
||||
Check(fatal_exit || fatal_signal,
|
||||
"death case did not use the PageManager fatal exit");
|
||||
#endif
|
||||
}
|
||||
|
||||
void TestFatalPaths() {
|
||||
@@ -631,7 +780,7 @@ void TestGpuAccessPermissions() {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#if 1
|
||||
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
|
||||
RunDeathCase(argv[2]);
|
||||
}
|
||||
|
||||
@@ -1507,7 +1507,7 @@ public:
|
||||
ordered_finished.load(),
|
||||
"submission barrier did not drain prior PM4 work");
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
constexpr uintptr_t fault_base = 0x0000000200500000ull;
|
||||
constexpr uint64_t fault_size = 0x10000;
|
||||
int64_t fault_direct_offset = -1;
|
||||
@@ -1689,7 +1689,7 @@ public:
|
||||
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
|
||||
fault_direct_offset, fault_size) == 0,
|
||||
"processor-fault direct-memory allocation release failed");
|
||||
#endif
|
||||
|
||||
|
||||
std::binary_semaphore command_entered{0};
|
||||
std::binary_semaphore release_command{0};
|
||||
@@ -1896,7 +1896,7 @@ public:
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
void CheckBufferCacheDirtyGarbageCollection() {
|
||||
constexpr const char *name = "BufferCacheDirtyGarbageCollection";
|
||||
constexpr uintptr_t base = 0x0000000200700000ull;
|
||||
@@ -6117,7 +6117,7 @@ public:
|
||||
"descriptor discovery direct-memory allocation release failed");
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Buffer CreateStorageBuffer(const char *shader_name,
|
||||
const std::vector<u32> &initial,
|
||||
@@ -15071,7 +15071,6 @@ void CheckEmbeddedFetchVertexOffset() {
|
||||
std::printf("[host] %-32s ok\n", "EmbeddedFetchVertexOffset");
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
struct CacheFaultContext {
|
||||
TextureCache *texture = nullptr;
|
||||
};
|
||||
@@ -15088,6 +15087,7 @@ bool CacheFault(void *opaque, PageFaultAccess access, uint64_t vaddr,
|
||||
std::_Exit(0x7f);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
void CheckReverseRenderTargetFormatContract() {
|
||||
const auto format = TextureGetRenderTargetFormat(12u, 7u, 2u);
|
||||
Require("ReverseRenderTarget", "exact format",
|
||||
@@ -15129,6 +15129,7 @@ void CheckReverseRenderTargetFormatContract() {
|
||||
"adjacent unproven render-target tuple did not retain the fatal guard");
|
||||
std::printf("[host] %-32s ok\n", "ReverseRenderTargetFormat");
|
||||
}
|
||||
#endif
|
||||
|
||||
[[noreturn]] void RunImageViewDeathCase(const char *kind) {
|
||||
if (std::strcmp(kind, "sampled-invalid-selector") == 0) {
|
||||
@@ -15211,6 +15212,7 @@ void CheckReverseRenderTargetFormatContract() {
|
||||
std::_Exit(0x7f);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
void CheckSampledColorViews() {
|
||||
Require("SampledColorViews", "identity",
|
||||
SelectSampledColorView(vk::Format::eR8G8B8A8Unorm,
|
||||
@@ -15446,6 +15448,7 @@ void CheckSampledColorViews() {
|
||||
}
|
||||
std::printf("[host] %-32s ok\n", "SampledColorRenderTargetViews");
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckSampledDepthResource() {
|
||||
ShaderRecompiler::IR::ImageResource resource{};
|
||||
@@ -16209,6 +16212,7 @@ ShaderTextureResource BasicUintVolumeStorageTextureDescriptor() {
|
||||
std::_Exit(0x7f);
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
void CheckBasicStorageTextureDescriptor() {
|
||||
const auto descriptor = BasicStorageTextureDescriptor();
|
||||
Require("BasicStorageTexture", "descriptor",
|
||||
@@ -16567,6 +16571,7 @@ void CheckBasicStorageTextureDescriptor() {
|
||||
}
|
||||
std::printf("[host] %-32s ok\n", "BasicStorageTextureDescriptor");
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckStorageTextureLinearUploadLayout() {
|
||||
constexpr uint32_t format =
|
||||
@@ -16835,6 +16840,7 @@ void CheckStandard64RenderTargetTileRoundTrip() {
|
||||
std::printf("[host] %-32s ok\n", "Standard64RenderTarget");
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
void CheckStorageTextureGpuOwnedRebindState() {
|
||||
constexpr uintptr_t base = 0x0000000200200000ull;
|
||||
constexpr uint64_t size = 0x10000;
|
||||
@@ -16904,6 +16910,7 @@ void CheckStorageTextureGpuOwnedRebindState() {
|
||||
VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
|
||||
std::printf("[host] %-32s ok\n", "StorageTextureGpuOwnedRebind");
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckNativeMsaaState() {
|
||||
Require("NativeMsaaState", "sample encoding",
|
||||
@@ -17407,7 +17414,7 @@ void CheckSharedFenceResourceLifetime() {
|
||||
std::printf("[host] %-32s ok\n", "SharedFenceResourceLifetime");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
void CheckEmbeddedFetchLaneSpill() {
|
||||
std::vector<u32> code;
|
||||
@@ -17683,18 +17690,6 @@ int main(int argc, char **argv) {
|
||||
vulkan.CheckGpuCommandLane();
|
||||
return 0;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-death") == 0) {
|
||||
RunReverseRenderTargetDeathCase();
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-only") == 0) {
|
||||
CheckReverseRenderTargetFormatContract();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--standard64-rt-only") == 0) {
|
||||
CheckStandard64RenderTargetTileRoundTrip();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-overlap-only") == 0) {
|
||||
CheckDepthAttachmentWrites();
|
||||
CheckDynamicRenderingState();
|
||||
@@ -17714,17 +17709,48 @@ int main(int argc, char **argv) {
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-view-cache-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--storage-sampled-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--depth-readback-only") == 0) {
|
||||
CheckDepthTargetFootprints();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-range-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedTextureCacheFlow();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-gc-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckBufferCacheDirtyGarbageCollection();
|
||||
return 0;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-death") == 0) {
|
||||
RunReverseRenderTargetDeathCase();
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-only") == 0) {
|
||||
CheckReverseRenderTargetFormatContract();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--standard64-rt-only") == 0) {
|
||||
CheckStandard64RenderTargetTileRoundTrip();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-view-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
CheckSampledColorViews();
|
||||
CheckSampledVideoOutView(vulkan.RuntimeRenderer());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-view-cache-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-transition-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
CheckImageTransitionState(vulkan.RuntimeRenderer());
|
||||
@@ -17736,16 +17762,6 @@ int main(int argc, char **argv) {
|
||||
CheckSampledDepthDescriptor(vulkan.RuntimeRenderer());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-range-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedTextureCacheFlow();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-gc-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckBufferCacheDirtyGarbageCollection();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--storage-bgra-only") == 0) {
|
||||
CheckSampledColorViews();
|
||||
CheckBasicStorageTextureDescriptor();
|
||||
@@ -17762,15 +17778,6 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, ImageStoreYzwxUsesInverseSwizzle());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--storage-sampled-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--depth-readback-only") == 0) {
|
||||
CheckDepthTargetFootprints();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 3 && std::strcmp(argv[1], "--image-view-death") == 0) {
|
||||
RunImageViewDeathCase(argv[2]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user