mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-08-03 11:23:49 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc793d859 | ||
|
|
01d78b71fe | ||
|
|
4d79f19089 | ||
|
|
8fe4765f9e | ||
|
|
c71bb9fc9e | ||
|
|
f60f80b631 |
+158
-14
@@ -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,168 @@ 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
|
||||
|
||||
release:
|
||||
name: Release KytyPS5
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
needs: [windows, macos]
|
||||
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"
|
||||
test -d "$windows_dir"
|
||||
test -d "$macos_dir"
|
||||
chmod a+x \
|
||||
"$macos_dir/launcher" \
|
||||
"$macos_dir/kyty_emulator" \
|
||||
"$macos_dir/libMoltenVK.dylib"
|
||||
(
|
||||
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" .
|
||||
)
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
assets=(
|
||||
"$RELEASE_NAME-Windows-x64.zip"
|
||||
"$RELEASE_NAME-macOS-x86_64.zip"
|
||||
)
|
||||
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/
|
||||
@@ -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-linux.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, 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,10 @@ 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.
|
||||
Linux support is planned. Windows remains the primary supported platform.
|
||||
|
||||
macOS support is experimental. Compatibility with the same games on Windows and macOS has not yet
|
||||
been tested.
|
||||
|
||||
## Bugs and Issues
|
||||
|
||||
|
||||
+3
-1
@@ -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)
|
||||
@@ -129,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
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif defined(__APPLE__)
|
||||
#include <csignal>
|
||||
#include <sys/ucontext.h>
|
||||
#endif
|
||||
|
||||
// IWYU pragma: no_include <errhandlingapi.h>
|
||||
@@ -118,6 +121,98 @@ static LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS exception) {
|
||||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool InstallHandler(Handler handler) {
|
||||
@@ -140,6 +235,36 @@ bool InstallHandler(Handler handler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_install_state.store(2, std::memory_order_release);
|
||||
return true;
|
||||
#elif defined(__APPLE__)
|
||||
if (handler == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t expected_state = 0;
|
||||
if (!g_install_state.compare_exchange_strong(expected_state, 1, std::memory_order_acq_rel)) {
|
||||
return expected_state == 2 && g_handler.load(std::memory_order_acquire) == handler;
|
||||
}
|
||||
|
||||
g_handler.store(handler, std::memory_order_release);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
g_install_state.store(2, std::memory_order_release);
|
||||
return true;
|
||||
#else
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <utime.h>
|
||||
@@ -536,8 +537,26 @@ void SysFileFindFiles(const std::filesystem::path& /*path*/,
|
||||
EXIT("not implemented\n");
|
||||
}
|
||||
|
||||
void SysFileGetDents(const std::filesystem::path& /*path*/, std::vector<sys_dir_entry_t>& /*out*/) {
|
||||
EXIT("not implemented\n");
|
||||
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
|
||||
sys_dir_entry_t r {};
|
||||
r.name = entry->d_name;
|
||||
if (entry->d_type == DT_REG) {
|
||||
r.is_file = true;
|
||||
} else if (entry->d_type == DT_DIR) {
|
||||
r.is_file = false;
|
||||
} else {
|
||||
// DT_UNKNOWN / symlink: resolve with stat.
|
||||
struct stat st {};
|
||||
r.is_file = (stat((path / entry->d_name).c_str(), &st) == 0) ? S_ISREG(st.st_mode) : true;
|
||||
}
|
||||
out.push_back(std::move(r));
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
bool SysFileCopyFile(const std::filesystem::path& /*src*/, const std::filesystem::path& /*dst*/) {
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.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>
|
||||
// IWYU pragma: no_include <bits/pthread_types.h>
|
||||
@@ -31,8 +36,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
|
||||
@@ -129,6 +134,32 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
|
||||
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 +177,7 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +197,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 +242,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);
|
||||
@@ -261,6 +315,33 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
|
||||
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
|
||||
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 +359,7 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
|
||||
ret_addr = 0;
|
||||
ptr = MAP_FAILED;
|
||||
}
|
||||
#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,12 @@
|
||||
#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>
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
@@ -28,16 +34,55 @@ 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;
|
||||
}
|
||||
#endif
|
||||
|
||||
thread_local bool g_in_fault_resolution = false;
|
||||
@@ -78,6 +123,8 @@ 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()));
|
||||
#else
|
||||
FailFast();
|
||||
#endif
|
||||
@@ -145,6 +192,12 @@ 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");
|
||||
#endif
|
||||
@@ -225,6 +278,14 @@ 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");
|
||||
@@ -245,6 +306,14 @@ 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;
|
||||
@@ -265,6 +334,18 @@ 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;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <windows.h>
|
||||
#undef min
|
||||
#undef max
|
||||
#elif defined(__APPLE__)
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
namespace Libs::Graphics {
|
||||
@@ -49,6 +51,9 @@ 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()));
|
||||
#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]));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1049,7 +1067,17 @@ 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; }
|
||||
|
||||
|
||||
+37
-6
@@ -58,6 +58,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;
|
||||
@@ -795,9 +801,9 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
}
|
||||
|
||||
// The guest ABI expects the entry argument in rdi and a 16-byte aligned stack before call.
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
"pushq %%r14\n\t"
|
||||
"pushq %%r15\n\t"
|
||||
"movq %%gs:0x08, %%r14\n\t"
|
||||
@@ -805,7 +811,6 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
"xorq %%rcx, %%rcx\n\t"
|
||||
"movq %%rcx, %%gs:0x08\n\t"
|
||||
"movq %%rcx, %%gs:0x10\n\t"
|
||||
#endif
|
||||
"movq %%rsp, %%r12\n\t"
|
||||
"movq %%rbp, %%r13\n\t"
|
||||
"movq %[guest_rsp], %%rsp\n\t"
|
||||
@@ -813,12 +818,10 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
"callq *%%rsi\n\t"
|
||||
"movq %%r13, %%rbp\n\t"
|
||||
"movq %%r12, %%rsp\n\t"
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
"movq %%r14, %%gs:0x08\n\t"
|
||||
"movq %%r15, %%gs:0x10\n\t"
|
||||
"popq %%r15\n\t"
|
||||
"popq %%r14\n\t"
|
||||
#endif
|
||||
"popq %%r13\n\t"
|
||||
"popq %%r12\n\t"
|
||||
: "=a"(ret), "+D"(arg), "+S"(func)
|
||||
@@ -826,6 +829,29 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
|
||||
: "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
|
||||
// Pin the stack operands to registers the template never touches (r14/r15 are
|
||||
// SysV callee-saved, so guest code preserves them): plain "r" constraints may
|
||||
// allocate them into r12/r13, which the template overwrites before use.
|
||||
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");
|
||||
#endif
|
||||
|
||||
g_guest_entry_return_rsp = 0;
|
||||
if (g_pthread_self != nullptr) {
|
||||
@@ -1056,7 +1082,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 +2666,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,
|
||||
|
||||
@@ -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,20 @@
|
||||
|
||||
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
|
||||
#elif defined(__linux__)
|
||||
constexpr char GNOME[] = "gnome-terminal";
|
||||
constexpr char XTERM[] = "xterm";
|
||||
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 +229,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;
|
||||
}
|
||||
|
||||
@@ -286,7 +292,7 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
process->setProgram(GNOME);
|
||||
process->setArguments({"--", "bash", "-c", bash_file_name});
|
||||
}
|
||||
#else
|
||||
#elif defined(_WIN32)
|
||||
{
|
||||
process->setProgram(CMD_EXE);
|
||||
QStringList process_args;
|
||||
@@ -294,9 +300,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_
|
||||
@@ -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"
|
||||
@@ -344,28 +345,48 @@ 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"
|
||||
"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");
|
||||
#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
|
||||
|
||||
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");
|
||||
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
|
||||
|
||||
asm volatile("pushq %%r12\n\t"
|
||||
"pushq %%r13\n\t"
|
||||
"movq %%rbp, %%r12\n\t"
|
||||
@@ -375,8 +396,13 @@ 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");
|
||||
@@ -1326,7 +1352,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 +1372,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();
|
||||
|
||||
+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") {
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ function(include_what_you_use_with_mappings target dirs mappings)
|
||||
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 +41,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}")
|
||||
|
||||
Reference in New Issue
Block a user