diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml deleted file mode 100644 index 881c2b2..0000000 --- a/.github/workflows/build-linux.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ba6ee8..6e59910 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -198,10 +198,137 @@ jobs: 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: name: Release KytyPS5 - if: github.event_name == 'push' - needs: [windows, macos] + if: github.event_name == 'push' && github.repository == 'KytyPS5/KytyPS5' + needs: [windows, macos, linux] runs-on: ubuntu-latest permissions: contents: write @@ -222,12 +349,16 @@ jobs: 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" + "$macos_dir/libMoltenVK.dylib" \ + "$linux_dir/launcher" \ + "$linux_dir/kyty_emulator" ( cd "$windows_dir" zip -r "$GITHUB_WORKSPACE/$RELEASE_NAME-Windows-x64.zip" . @@ -236,6 +367,8 @@ jobs: 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 @@ -245,6 +378,7 @@ jobs: 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" "${assets[@]}" \ diff --git a/README.md b/README.md index c58acb0..b17b267 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # KytyPS5 [![Build KytyPS5 (Windows)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28Windows%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml) -[![Build KytyPS5 (Linux)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build-linux.yml/badge.svg)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build-linux.yml) +[![Build KytyPS5 (Linux)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28Linux%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml) [![Build KytyPS5 (macOS)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28macOS%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml) -[![Platform](https://img.shields.io/badge/platform-Windows%20x64%20%7C%20macOS%20x86__64-0078D4.svg)](#system-requirements) +[![Platform](https://img.shields.io/badge/platform-Windows%20x64%20%7C%20Linux%20x64%20%7C%20macOS%20x86__64-0078D4.svg)](#system-requirements) [![Status](https://img.shields.io/badge/status-early%20development-orange.svg)](#current-status) [![License](https://img.shields.io/badge/license-GPL--2.0-blue.svg)](LICENSE) -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 +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. @@ -24,7 +24,8 @@ 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. Windows remains the primary supported platform. +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. @@ -64,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 @@ -100,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 @@ -138,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: @@ -164,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. @@ -174,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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 450d200..99350ce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -251,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 @@ -394,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 $) add_test(NAME memory_tracker COMMAND $) @@ -410,23 +438,25 @@ if(BUILD_TESTING) add_test(NAME gpu_tiler COMMAND $ --gpu-tiler-only) + add_test(NAME texture_cache_layered_image + COMMAND $ --layered-image-only) + add_test(NAME texture_cache_image_views + COMMAND $ --image-view-cache-only) + add_test(NAME texture_cache_storage_sampled + COMMAND $ --storage-sampled-only) + add_test(NAME texture_cache_depth_readback + COMMAND $ --depth-readback-only) + add_test(NAME buffer_cache_dirty_gc + COMMAND $ --buffer-cache-gc-only) + if(WIN32) + # These tests still depend on the Windows multisample-depth path. add_test(NAME texture_cache_image_overlap COMMAND $ --image-overlap-only) add_test(NAME texture_cache_htile_clear COMMAND $ --htile-clear-only) - add_test(NAME texture_cache_layered_image - COMMAND $ --layered-image-only) - add_test(NAME texture_cache_image_views - COMMAND $ --image-view-cache-only) - add_test(NAME texture_cache_storage_sampled - COMMAND $ --storage-sampled-only) - add_test(NAME texture_cache_depth_readback - COMMAND $ --depth-readback-only) add_test(NAME buffer_cache_ranges COMMAND $ --buffer-cache-range-only) - add_test(NAME buffer_cache_dirty_gc - COMMAND $ --buffer-cache-gc-only) endif() endif() @@ -446,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}") diff --git a/src/common/hostException.cpp b/src/common/hostException.cpp index ef90344..86f093e 100644 --- a/src/common/hostException.cpp +++ b/src/common/hostException.cpp @@ -9,6 +9,11 @@ #elif defined(__APPLE__) #include #include +#else +#include +#include +#include // IWYU pragma: keep +#include #endif // IWYU pragma: no_include @@ -19,7 +24,7 @@ namespace Common::HostException { -#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +#if !defined(__APPLE__) static std::atomic g_handler {nullptr}; static std::atomic_uint32_t g_install_state {0}; @@ -33,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(EXCEPTION_NONCONTINUABLE_EXCEPTION)); +#endif std::_Exit(321); } @@ -51,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; @@ -109,14 +131,7 @@ 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 = g_handler.load(std::memory_order_acquire); - if (handler == nullptr) { - FailFast("host exception callback is null"); - } + const auto handler = LoadInstalledHandler(); return handler(info) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH; } @@ -213,10 +228,79 @@ static void SignalHandler(int sig, siginfo_t* si, void* uctx) { 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(native_context); + auto* gregs = context->uc_mcontext.gregs; + + ExceptionInfo info {}; + info.exception_address = static_cast(gregs[REG_RIP]); + info.native_code = static_cast(signal_number); + info.native_context = context; + + if (signal_number == SIGSEGV || signal_number == SIGBUS) { + info.type = ExceptionType::AccessViolation; + const auto error_code = static_cast(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(signal_info->si_addr); + } else if (signal_number == SIGILL) { + info.type = ExceptionType::IllegalInstruction; + } else { + ChainToDefault(signal_number); + return; + } + + info.rax = static_cast(gregs[REG_RAX]); + info.rbx = static_cast(gregs[REG_RBX]); + info.rcx = static_cast(gregs[REG_RCX]); + info.rdx = static_cast(gregs[REG_RDX]); + info.rsi = static_cast(gregs[REG_RSI]); + info.rdi = static_cast(gregs[REG_RDI]); + info.rbp = static_cast(gregs[REG_RBP]); + info.rsp = static_cast(gregs[REG_RSP]); + info.r8 = static_cast(gregs[REG_R8]); + info.r9 = static_cast(gregs[REG_R9]); + info.r10 = static_cast(gregs[REG_R10]); + info.r11 = static_cast(gregs[REG_R11]); + info.r12 = static_cast(gregs[REG_R12]); + info.r13 = static_cast(gregs[REG_R13]); + info.r14 = static_cast(gregs[REG_R14]); + info.r15 = static_cast(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; } @@ -228,27 +312,14 @@ 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; } - - 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; @@ -264,13 +335,25 @@ bool InstallHandler(Handler handler) { 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 diff --git a/src/common/platform/sysDbg.h b/src/common/platform/sysDbg.h index c881753..d9397af 100644 --- a/src/common/platform/sysDbg.h +++ b/src/common/platform/sysDbg.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 }; diff --git a/src/common/platform/sysLinuxDbg.cpp b/src/common/platform/sysLinuxDbg.cpp index 2d3a2d8..9f7b1f2 100644 --- a/src/common/platform/sysLinuxDbg.cpp +++ b/src/common/platform/sysLinuxDbg.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -15,8 +17,48 @@ #include // POSIX basename() lives here on macOS, not in #endif -void SysStackWalk(void** /*stack*/, int* depth) { - *depth = 0; +// Avoid unwinding a guest-owned stack. +static bool OnOwnStack() { + const char* probe = reinterpret_cast(&probe); + + pthread_attr_t attr {}; +#if defined(__APPLE__) + const auto* top = static_cast(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(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) { @@ -33,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(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(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(pid)); @@ -46,8 +115,6 @@ void SysStackUsage(sys_dbg_stack_info_t& s) { result = sprintf(str, "/proc/%d/maps", static_cast(pid)); - memset(&s, 0, sizeof(sys_dbg_stack_info_t)); - FILE* f = fopen(str, "r"); if (f == nullptr) { @@ -121,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 diff --git a/src/common/platform/sysLinuxFileIO.cpp b/src/common/platform/sysLinuxFileIO.cpp index 8903f74..13fc0ff 100644 --- a/src/common/platform/sysLinuxFileIO.cpp +++ b/src/common/platform/sysLinuxFileIO.cpp @@ -12,7 +12,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -40,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); @@ -137,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; @@ -151,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; @@ -181,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); @@ -195,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); @@ -215,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; @@ -240,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(size)); } if (f.type == SYS_FILE_MEMORY_STAT || f.type == SYS_FILE_MEMORY_DYN) { @@ -261,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(size)) == 0); + if (position >= 0) { + fseek(f.f, position, SEEK_SET); + } + } + return ok; } bool SysFileUnlink(sys_file_t& /*f*/, const std::filesystem::path& name) { @@ -383,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; @@ -401,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; @@ -420,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) { @@ -532,45 +615,110 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name, // } } -void SysFileFindFiles(const std::filesystem::path& /*path*/, - std::vector& /*out*/) { - EXIT("not implemented\n"); -} +// Recursively collect regular files. +void SysFileFindFiles(const std::filesystem::path& path, std::vector& out) { + auto real_path = get_internal_name(path); -void SysFileGetDents(const std::filesystem::path& path, std::vector& out) { - DIR* dir = opendir(path.c_str()); + DIR* dir = opendir(real_path.string().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; + + 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(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); } - out.push_back(std::move(r)); } + closedir(dir); } -bool SysFileCopyFile(const std::filesystem::path& /*src*/, const std::filesystem::path& /*dst*/) { - EXIT("not implemented\n"); - return false; +// Keep "." and ".." to match FindFirstFileW. +void SysFileGetDents(const std::filesystem::path& path, std::vector& 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 SysFileMoveFile(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; } -void SysFileRemoveReadonly(const std::filesystem::path& /*name*/) { - EXIT("not implemented\n"); +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) { + 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 diff --git a/src/common/platform/sysLinuxVirtual.cpp b/src/common/platform/sysLinuxVirtual.cpp index 1c5f856..b70572e 100644 --- a/src/common/platform/sysLinuxVirtual.cpp +++ b/src/common/platform/sysLinuxVirtual.cpp @@ -8,9 +8,11 @@ #include "common/platform/sysVirtual.h" #include "common/virtualMemory.h" +#include #include #include #include +#include #if defined(__APPLE__) #include @@ -81,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 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(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(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); @@ -88,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(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(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++) { @@ -122,16 +187,15 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory:: auto addr = static_cast(address); int protect = get_protection_flag(mode); - void* ptr = - mmap(reinterpret_cast(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(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); - ptr = mmap(reinterpret_cast(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(ptr); if (ptr != MAP_FAILED) { #if defined(__APPLE__) @@ -186,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++) { @@ -272,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++) { @@ -303,16 +367,15 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig auto addr = static_cast(address); - void* ptr = mmap(reinterpret_cast(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(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); - ptr = mmap(reinterpret_cast(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(ptr); if (ptr != MAP_FAILED) { #if defined(__APPLE__) @@ -368,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++) { @@ -406,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++) { @@ -421,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(sysconf(_SC_PAGESIZE)); + if (page_size != 0) { + // Do not discard pages outside the requested range. + const auto begin = (static_cast(address) + page_size - 1) & ~(page_size - 1); + const auto end = (static_cast(address) + size) & ~(page_size - 1); + if (end > begin) { + ::madvise(reinterpret_cast(begin), end - begin, RECLAIM_ADVICE); + } + } + } + + return true; } bool SysVirtualFree(uint64_t address) { @@ -473,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(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(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; } diff --git a/src/common/platform/sysTimer.h b/src/common/platform/sysTimer.h index 7c599ba..a53f761 100644 --- a/src/common/platform/sysTimer.h +++ b/src/common/platform/sysTimer.h @@ -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((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(-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((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(-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((now.tv_nsec / 1000000) % 1000); } inline void SysQueryPerformanceFrequency(uint64_t* freq) { diff --git a/src/common/threads.cpp b/src/common/threads.cpp index 4446cb8..4ffe8a2 100644 --- a/src/common/threads.cpp +++ b/src/common/threads.cpp @@ -7,6 +7,7 @@ #include #include // IWYU pragma: keep #include // IWYU pragma: keep +#include #include #include @@ -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 +#endif + #include #include #include @@ -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(deadline.tv_nsec) + nanos; + deadline.tv_sec += static_cast(target_nsec / NANOS_PER_SEC); + deadline.tv_nsec = static_cast(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(micros) * 10); +#elif defined(KYTY_POSIX_HIGH_RES_SLEEP) + SleepHighResolutionNanos(static_cast(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 diff --git a/src/graphics/host_gpu/pageManager.cpp b/src/graphics/host_gpu/pageManager.cpp index e669d06..2e490d1 100644 --- a/src/graphics/host_gpu/pageManager.cpp +++ b/src/graphics/host_gpu/pageManager.cpp @@ -25,6 +25,14 @@ #include #include #include +#else +#include +#include +#include +#include +#include +#include +#include #endif namespace Libs::Graphics { @@ -83,6 +91,9 @@ static uint32_t MachQueryPageProt(uint64_t vaddr) { } 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; @@ -101,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(std::size(frames))); + ::backtrace_symbols_fd(frames, frame_count, STDERR_FILENO); #endif std::fflush(stderr); #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS @@ -125,11 +140,134 @@ uint32_t CurrentThread() noexcept { return GetCurrentThreadId(); #elif defined(__APPLE__) return static_cast(pthread_mach_thread_np(pthread_self())); +#elif defined(__linux__) + static thread_local const uint32_t tid = [] { + const auto raw = static_cast(::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(c - '0'); + } else if (c >= 'a' && c <= 'f') { + digit = static_cast(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) { @@ -171,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; @@ -199,7 +341,10 @@ struct PageManager::Impl { static_cast(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(host_page_size) != PAGE_SIZE) { + Fatal("unsupported host page size %ld", static_cast(host_page_size)); + } #endif regions = std::make_unique[]>(REGION_COUNT); for (uint64_t i = 0; i < REGION_COUNT; i++) { @@ -267,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(static_cast(vaddr)), &info, @@ -287,12 +432,19 @@ struct PageManager::Impl { } 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(static_cast(vaddr)), &info, @@ -315,13 +467,24 @@ struct PageManager::Impl { 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(static_cast(vaddr)), PAGE_SIZE, @@ -347,10 +510,24 @@ struct PageManager::Impl { 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(static_cast(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 } @@ -480,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: @@ -507,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); @@ -540,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 } } @@ -661,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) { @@ -686,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; } @@ -713,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"); @@ -762,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; diff --git a/src/graphics/host_gpu/regionManager.h b/src/graphics/host_gpu/regionManager.h index 94fde40..f3371bb 100644 --- a/src/graphics/host_gpu/regionManager.h +++ b/src/graphics/host_gpu/regionManager.h @@ -18,6 +18,9 @@ #undef max #elif defined(__APPLE__) #include +#elif defined(__linux__) +#include +#include #endif namespace Libs::Graphics { @@ -54,6 +57,9 @@ private: #elif defined(__APPLE__) // mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel). return static_cast(pthread_mach_thread_np(pthread_self())); +#elif defined(__linux__) + static thread_local const uint32_t tid = static_cast(::syscall(SYS_gettid)); + return tid; #else EXIT("region tracking thread identity is unsupported on this platform\n"); #endif diff --git a/src/graphics/presentation/renderDoc.cpp b/src/graphics/presentation/renderDoc.cpp index 1c0f81c..ecc9d4b 100644 --- a/src/graphics/presentation/renderDoc.cpp +++ b/src/graphics/presentation/renderDoc.cpp @@ -19,12 +19,16 @@ #include #undef min #undef max +#else +#include + +// 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(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(::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(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(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( + static_cast(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(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 diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index 6f9a656..a643eeb 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include 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; diff --git a/src/kernel/memory.cpp b/src/kernel/memory.cpp index eda552a..eae359e 100644 --- a/src/kernel/memory.cpp +++ b/src/kernel/memory.cpp @@ -242,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 {}; @@ -2992,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 diff --git a/src/kernel/memoryAddressSpace.inc b/src/kernel/memoryAddressSpace.inc index fbeb8c8..3340202 100644 --- a/src/kernel/memoryAddressSpace.inc +++ b/src/kernel/memoryAddressSpace.inc @@ -385,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; } @@ -1079,7 +1082,10 @@ private: #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 diff --git a/src/kernel/pthread.cpp b/src/kernel/pthread.cpp index 9e278e4..f51c384 100644 --- a/src/kernel/pthread.cpp +++ b/src/kernel/pthread.cpp @@ -46,6 +46,17 @@ #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #include #include +#elif !defined(__APPLE__) +#include +#include +#endif + +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS +#include // pthread_kill is declared here, not in +#endif + +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && (defined(_M_X64) || defined(__x86_64__)) +#include #endif #ifdef pthread_attr_getguardsize @@ -99,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(::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(); @@ -109,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}; @@ -784,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; @@ -794,45 +822,14 @@ 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 KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS - asm volatile("pushq %%r12\n\t" - "pushq %%r13\n\t" - "pushq %%r14\n\t" - "pushq %%r15\n\t" - "movq %%gs:0x08, %%r14\n\t" - "movq %%gs:0x10, %%r15\n\t" - "xorq %%rcx, %%rcx\n\t" - "movq %%rcx, %%gs:0x08\n\t" - "movq %%rcx, %%gs:0x10\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" - "movq %%r14, %%gs:0x08\n\t" - "movq %%r15, %%gs:0x10\n\t" - "popq %%r15\n\t" - "popq %%r14\n\t" - "popq %%r13\n\t" - "popq %%r12\n\t" - : "=a"(ret), "+D"(arg), "+S"(func) - : [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp) - : "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. +#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" @@ -851,6 +848,45 @@ 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 + // 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" + "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" + "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" + "movq %[guest_rbp], %%rbp\n\t" + "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" +#endif + "popq %%r15\n\t" + "popq %%r14\n\t" + "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; @@ -886,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(static_cast(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 } @@ -926,6 +985,8 @@ void PthreadInitSelfForMainThread() { uint64_t os_thread_id = 0; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS os_thread_id = static_cast(GetCurrentThreadId()); +#else + os_thread_id = GetHostThreadId(); #endif g_pthread_self->host_thread_id = os_thread_id; g_pthread_main = g_pthread_self; @@ -3118,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; @@ -3215,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(GetCurrentThreadId()); +#else + os_thread_id = GetHostThreadId(); #endif thread->host_thread_id = os_thread_id; diff --git a/src/kernel/pthread.h b/src/kernel/pthread.h index 8b1942e..f8ffc97 100644 --- a/src/kernel/pthread.h +++ b/src/kernel/pthread.h @@ -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(); diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 46091be..ff4f378 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -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() diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index bec14b1..b873fe3 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -45,8 +45,6 @@ constexpr char EMULATOR_EXE[] = "kyty_emulator"; #if defined(_WIN32) constexpr char CMD_EXE[] = "cmd.exe"; #elif defined(__linux__) -constexpr char GNOME[] = "gnome-terminal"; -constexpr char XTERM[] = "xterm"; constexpr char KYTY_BASH_FILE[] = "kyty_run.sh"; #endif #if defined(_WIN32) @@ -265,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) { @@ -289,8 +338,16 @@ 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}); + } } #elif defined(_WIN32) { @@ -319,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); } diff --git a/src/libs/libC.cpp b/src/libs/libC.cpp index 9d4648f..25edfd2 100644 --- a/src/libs/libC.cpp +++ b/src/libs/libC.cpp @@ -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) { diff --git a/src/libs/libDbgAsan.cpp b/src/libs/libDbgAsan.cpp index 11000cb..dfa5e36 100644 --- a/src/libs/libDbgAsan.cpp +++ b/src/libs/libDbgAsan.cpp @@ -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(&s)); -#else + // Cover the full stack reservation. AllocShadow(s.reserved_addr, reinterpret_cast(&s)); -#endif LibKernel::Memory::RegisterCallbacks(KernelAlloc, KernelFree); } diff --git a/src/libs/libKernel.cpp b/src/libs/libKernel.cpp index f08fa6a..755f8c4 100644 --- a/src/libs/libKernel.cpp +++ b/src/libs/libKernel.cpp @@ -40,6 +40,9 @@ #define NOMINMAX #endif #include +#elif !defined(__APPLE__) +#include +#include #endif namespace Libs { @@ -630,6 +633,75 @@ static SignalUcontext CreateSignalUcontext( ctx.uc_mcontext.mc_rsp = reinterpret_cast(&stack_marker); ctx.uc_mcontext.mc_rbp = reinterpret_cast(__builtin_frame_address(0)); ctx.uc_mcontext.mc_rip = reinterpret_cast(__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(gregs[REG_RDI]); + ctx.uc_mcontext.mc_rsi = static_cast(gregs[REG_RSI]); + ctx.uc_mcontext.mc_rdx = static_cast(gregs[REG_RDX]); + ctx.uc_mcontext.mc_rcx = static_cast(gregs[REG_RCX]); + ctx.uc_mcontext.mc_r8 = static_cast(gregs[REG_R8]); + ctx.uc_mcontext.mc_r9 = static_cast(gregs[REG_R9]); + ctx.uc_mcontext.mc_rax = static_cast(gregs[REG_RAX]); + ctx.uc_mcontext.mc_rbx = static_cast(gregs[REG_RBX]); + ctx.uc_mcontext.mc_rbp = static_cast(gregs[REG_RBP]); + ctx.uc_mcontext.mc_r10 = static_cast(gregs[REG_R10]); + ctx.uc_mcontext.mc_r11 = static_cast(gregs[REG_R11]); + ctx.uc_mcontext.mc_r12 = static_cast(gregs[REG_R12]); + ctx.uc_mcontext.mc_r13 = static_cast(gregs[REG_R13]); + ctx.uc_mcontext.mc_r14 = static_cast(gregs[REG_R14]); + ctx.uc_mcontext.mc_r15 = static_cast(gregs[REG_R15]); + ctx.uc_mcontext.mc_rip = static_cast(gregs[REG_RIP]); + ctx.uc_mcontext.mc_rsp = static_cast(gregs[REG_RSP]); + ctx.uc_mcontext.mc_rflags = static_cast(gregs[REG_EFL]); + + // Linux packs cs/gs/fs into one greg. + const auto csgsfs = static_cast(gregs[REG_CSGSFS]); + ctx.uc_mcontext.mc_cs = csgsfs & 0xffffu; + ctx.uc_mcontext.mc_gs = static_cast((csgsfs >> 16u) & 0xffffu); + ctx.uc_mcontext.mc_fs = static_cast((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(src_ctx.uc_mcontext.mc_rdi); + gregs[REG_RSI] = static_cast(src_ctx.uc_mcontext.mc_rsi); + gregs[REG_RDX] = static_cast(src_ctx.uc_mcontext.mc_rdx); + gregs[REG_RCX] = static_cast(src_ctx.uc_mcontext.mc_rcx); + gregs[REG_R8] = static_cast(src_ctx.uc_mcontext.mc_r8); + gregs[REG_R9] = static_cast(src_ctx.uc_mcontext.mc_r9); + gregs[REG_RAX] = static_cast(src_ctx.uc_mcontext.mc_rax); + gregs[REG_RBX] = static_cast(src_ctx.uc_mcontext.mc_rbx); + gregs[REG_RBP] = static_cast(src_ctx.uc_mcontext.mc_rbp); + gregs[REG_R10] = static_cast(src_ctx.uc_mcontext.mc_r10); + gregs[REG_R11] = static_cast(src_ctx.uc_mcontext.mc_r11); + gregs[REG_R12] = static_cast(src_ctx.uc_mcontext.mc_r12); + gregs[REG_R13] = static_cast(src_ctx.uc_mcontext.mc_r13); + gregs[REG_R14] = static_cast(src_ctx.uc_mcontext.mc_r14); + gregs[REG_R15] = static_cast(src_ctx.uc_mcontext.mc_r15); + gregs[REG_RIP] = static_cast(src_ctx.uc_mcontext.mc_rip); + gregs[REG_RSP] = static_cast(src_ctx.uc_mcontext.mc_rsp); + gregs[REG_EFL] = static_cast(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(native_context); + + for (int signum = 0; signum < static_cast(std::size(g_exception_handlers)); signum++) { + if (!TakePendingSignal(current, signum)) { + continue; + } + + auto* handler = reinterpret_cast(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(__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); diff --git a/src/loader/runtimeLinker.cpp b/src/loader/runtimeLinker.cpp index 2d93a42..91f6ef6 100644 --- a/src/loader/runtimeLinker.cpp +++ b/src/loader/runtimeLinker.cpp @@ -36,6 +36,12 @@ #define NOMINMAX #endif #include +#else +#include +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) +#include +#include +#endif #endif namespace Libs::LibKernel { @@ -354,6 +360,7 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu 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" @@ -377,6 +384,22 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu : "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" + : + : [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", "r12", "r13", + "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", + "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"); +#endif return; } @@ -387,6 +410,7 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu register uintptr_t guest_rbp_reg asm("r14") = reinterpret_cast(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" @@ -406,6 +430,19 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu : "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(addr)(params, atexit_func); @@ -518,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(current), &mbi, sizeof(mbi)) == 0 || + mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { + return false; + } + const auto region_end = reinterpret_cast(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(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(current), sizeof(probe)}; + + if (process_vm_readv(getpid(), &local, 1, &remote, 1, 0) != + static_cast(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; @@ -563,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(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 @@ -586,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(*reinterpret_cast( - 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(dump_start); + LOGF("code-32:"); + for (uint32_t i = 0; i < 64; i++) { + LOGF(" %02" PRIx32, static_cast(dump_ptr[i])); + } + LOGF("\n"); + } else { + LOGF("code-32: unavailable\n"); } - LOGF("\n"); #endif } else { LOGF("code: unavailable\n"); @@ -609,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(current), &mbi, sizeof(mbi)) == 0 || - mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - return false; - } - const auto region_end = reinterpret_cast(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(info->rsp); LOGF("stack:"); for (uint64_t i = 0; i < 16; i++) { @@ -669,6 +752,9 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc #else auto* dump_ptr = reinterpret_cast(addr >= 16 ? addr - 16 : addr); const auto dump_size = 32u; + if (!IsReadableRange(reinterpret_cast(dump_ptr), dump_size)) { + return; + } #endif LOGF("%s code: addr=%016" PRIx64 ", off=%016" PRIx64 ", module=%s:", name, addr, @@ -684,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(info->rsp); for (uint64_t i = 0; i < 16; i++) { char name[32] {}; @@ -713,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; @@ -728,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; } @@ -753,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(info->rbx); dump_guest_qwords("vorbis obj", local[0]); dump_guest_qwords("vorbis len", info->rcx); diff --git a/src/loader/x64InstructionEmulator.cpp b/src/loader/x64InstructionEmulator.cpp index 7bb8aa2..874998b 100644 --- a/src/loader/x64InstructionEmulator.cpp +++ b/src/loader/x64InstructionEmulator.cpp @@ -4,20 +4,13 @@ #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #include // IWYU pragma: keep +#elif !defined(__APPLE__) +#include +#include #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(fpregs->_xmm[index].element); +} + +static uint64_t GetXmmLow(const uint32_t* xmm) { + return static_cast(xmm[0]) | (static_cast(xmm[1]) << 32u); +} + +static void SetXmmLow(uint32_t* xmm, uint64_t value) { + xmm[0] = static_cast(value); + xmm[1] = static_cast(value >> 32u); +} + +static void SetXmmHigh(uint32_t* xmm, uint64_t value) { + xmm[2] = static_cast(value); + xmm[3] = static_cast(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(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(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(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(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(native_context); return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context); +#elif !defined(__APPLE__) + auto* context = static_cast(native_context); + return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context); #else (void)native_context; return false; diff --git a/src/utils.cmake b/src/utils.cmake index a34cd71..d0f747a 100644 --- a/src/utils.cmake +++ b/src/utils.cmake @@ -1,20 +1,28 @@ +# 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() diff --git a/tests/MemoryTrackerTests.cpp b/tests/MemoryTrackerTests.cpp index adf44a0..d686108 100644 --- a/tests/MemoryTrackerTests.cpp +++ b/tests/MemoryTrackerTests.cpp @@ -18,6 +18,13 @@ #include #undef min #undef max +#else +#include +#include +#include +#include +#include +#include #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(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 &AllocationSizes() { + static std::map 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)) { + fd_ = static_cast(::syscall(SYS_memfd_create, "kyty-shared-page", 0u)); + Check(fd_ >= 0, "memfd_create failed"); + Check(::ftruncate(fd_, static_cast(size)) == 0, "ftruncate failed"); + guest = static_cast(::mmap(reinterpret_cast(address), size_, + PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_FIXED_NOREPLACE, fd_, 0)); + Check(guest == reinterpret_cast(address), "fixed shared view failed"); + backing = static_cast( + ::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(native_context); + const auto error_code = static_cast(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(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(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 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]); } diff --git a/tests/PageManagerTests.cpp b/tests/PageManagerTests.cpp index 48ec1a8..53b3124 100644 --- a/tests/PageManagerTests.cpp +++ b/tests/PageManagerTests.cpp @@ -17,6 +17,13 @@ #include #undef min #undef max +#else +#include +#include +#include +#include +#include +#include #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(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 &AllocationSizes() { + static std::map 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(native_context); + const auto error_code = static_cast(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(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(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( VirtualAlloc(reinterpret_cast(test_address), size, MEM_RESERVE | MEM_COMMIT, protection)); Check(memory == reinterpret_cast(test_address), "fixed low VirtualAlloc failed"); +#else + // Do not overwrite a leaked mapping from an earlier case. + void *raw = ::mmap(reinterpret_cast(test_address), size, ToHostProt(protection), + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + Check(raw == reinterpret_cast(test_address), "fixed low mmap failed"); + auto *memory = static_cast(raw); + AllocationSizes()[raw] = static_cast(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]); } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 6379786..239aca3 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -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 &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 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]); }