mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-27 01:02:47 +01:00
SpeedBreaker v0.1.9 Windows beta 1
Co-authored-by: rafidwayne <117944293+rafidwayne@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57b0c69334
commit
61365bd067
99 files changed
+7457
-431
No files matched your search
+43
-3
@@ -5,6 +5,9 @@
|
||||
# -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++
|
||||
# ninja -C build/main -j6
|
||||
#
|
||||
# Windows (clang-cl, lld-link, the static C runtime; see CMakePresets.json):
|
||||
# cmake --preset windows-clang -B <build dir> -DCMAKE_PREFIX_PATH=<deps> -DFFMPEG_XMA=<deps>
|
||||
#
|
||||
# Needs ppc/ (XenonRecomp output from your own default.xex) and tools/
|
||||
# (scripts/setup_tools.sh). -DSB_TESTS=ON adds the unit tests (tests/; run
|
||||
# them with ctest from the build folder).
|
||||
@@ -71,15 +74,52 @@ project(SpeedBreaker VERSION 0.1.9 LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# clang-cl (the MSVC front end): its spellings of the flags the other
|
||||
# platforms pass GCC-style.
|
||||
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
set(SB_MSVC_FRONTEND ON)
|
||||
else()
|
||||
set(SB_MSVC_FRONTEND OFF)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
if(NOT SB_MSVC_FRONTEND OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(FATAL_ERROR "Windows builds need clang-cl (the generated code is Clang-only): see CMakePresets.json")
|
||||
endif()
|
||||
# One self-contained exe: the static C/C++ runtime (/MT) in every
|
||||
# configuration, which every dependency must match (lld-link refuses a
|
||||
# mix).
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded)
|
||||
# xbox.h includes <windows.h> in every translation unit.
|
||||
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN _CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS)
|
||||
# C++ exceptions with /EHs, not CMake's /EHsc: the c lets the compiler
|
||||
# assume extern "C" functions never throw, and every recompiled function
|
||||
# is extern "C" (PPC_FUNC_IMPL) with GuestThreadExit thrown through it
|
||||
# (ExTerminateThread). Under /EHsc that throw ends in std::terminate.
|
||||
string(REPLACE "/EHsc" "/EHs" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
if(NOT CMAKE_CXX_FLAGS MATCHES "/EHs")
|
||||
string(APPEND CMAKE_CXX_FLAGS " /EHs")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Binaries name their sources from the repository root (assert messages,
|
||||
# debug information), never by the folders they were built in: a published
|
||||
# build carries no one's home folder (a build folder inside the checkout
|
||||
# reads ./build/...). The real path too, for a checkout reached through a
|
||||
# symlink; both map to the same name, so their order doesn't matter.
|
||||
# clang-cl takes the option through /clang:, and only C and C++ get it there
|
||||
# (the resource compiler, platform/win32/speedbreaker.rc, doesn't know it).
|
||||
file(REAL_PATH "${CMAKE_SOURCE_DIR}" SB_SOURCE_REAL)
|
||||
add_compile_options("-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
|
||||
if(NOT SB_SOURCE_REAL STREQUAL CMAKE_SOURCE_DIR)
|
||||
add_compile_options("-ffile-prefix-map=${SB_SOURCE_REAL}=.")
|
||||
if(SB_MSVC_FRONTEND)
|
||||
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:/clang:-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.>")
|
||||
if(NOT SB_SOURCE_REAL STREQUAL CMAKE_SOURCE_DIR)
|
||||
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:/clang:-ffile-prefix-map=${SB_SOURCE_REAL}=.>")
|
||||
endif()
|
||||
else()
|
||||
add_compile_options("-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
|
||||
if(NOT SB_SOURCE_REAL STREQUAL CMAKE_SOURCE_DIR)
|
||||
add_compile_options("-ffile-prefix-map=${SB_SOURCE_REAL}=.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# macOS: a call to an API newer than the deployment target is an error unless
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 3,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 21,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-clang",
|
||||
"displayName": "Windows x64: clang-cl, lld-link, static CRT, Release",
|
||||
"description": "From a vcvars64 environment (MSVC's headers and libraries, the Windows SDK, rc.exe) with LLVM's clang-cl 19 or newer on PATH; CMake takes lld-link and llvm-lib from clang-cl's folder. Pass the dependency prefix (SDL3, glslang, FFmpeg-XMA, all /MT): -DCMAKE_PREFIX_PATH=<prefix> -DFFMPEG_XMA=<prefix>.",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/windows",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"CMAKE_C_COMPILER": "clang-cl",
|
||||
"CMAKE_CXX_COMPILER": "clang-cl"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Windows"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-clang-tester",
|
||||
"inherits": "windows-clang",
|
||||
"displayName": "Windows x64 beta for testers: no console window, labelled",
|
||||
"description": "windows-clang for builds that go to testers (scripts/windows/make_zip.py): linked for the Windows subsystem (no console window; the session log has everything) and labelled, so logs, Settings > Advanced and bug-report names say 'SpeedBreaker v<version> Windows beta <date> (<commit>)'.",
|
||||
"binaryDir": "${sourceDir}/build/windows-tester",
|
||||
"cacheVariables": {
|
||||
"SB_WINDOWS_GUI": "ON",
|
||||
"SB_BUILD_LABEL": "Windows beta"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -124,7 +124,7 @@ belongs to its owners.</sub>
|
||||
| **Linux** (x86-64) | [Download][dl-steamos-x86_64] the x86-64 SteamOS build, `SpeedBreaker-steamos-x86_64.tar.xz`, or [build from source](#building-from-source) | Tested here on SteamOS. Players reported v0.1.1 working on Fedora 44 (GeForce GTX 1080 Ti) and Ubuntu 24.04 (Intel Arc 140T). Other setups: reports welcome. |
|
||||
| **Mac** (Apple silicon) | [Download][dl-macos]<br>`SpeedBreaker-macos.dmg` | MacBook Pro (M1 Pro) on macOS 27 and Mac Studio (M4 Max) on macOS 26; macOS 15.6 (Sequoia) in a virtual machine. There is no Intel Mac build. |
|
||||
| **iPhone** and **iPad** | [Sideload the IPA](#iphone-and-ipad) (experimental), `SpeedBreaker-<version>-ios.ipa` on the [release page][releases], or [build it yourself](#iphone-and-ipad) with Xcode | iPhone 15 Pro Max and iPhone 18 Pro Max (iOS 27). iPad Pro 12.9-inch (M2, iPadOS 27) with a Backbone Pro. Not on the App Store. Those were Xcode-signed builds: the IPA itself hasn't been installed on a device yet. |
|
||||
| **Windows** | Coming soon | |
|
||||
| **Windows** (beta) | [Download][all-releases] `SpeedBreaker-0.1.9-windows-x64-beta.zip` from the Releases page; see [Windows (beta)](#windows-beta) | A laptop with Intel UHD Graphics (60 Hz). NVIDIA, AMD and recent Intel Arc graphics and 120/144 Hz monitors haven't been tested yet: [reports welcome](#reporting-a-bug). |
|
||||
| **Android** | Coming soon | |
|
||||
|
||||
Each link downloads the newest release. Every file and its `.sha256` checksum file are on the
|
||||
@@ -151,6 +151,8 @@ should say OK. What changed in each release is in [CHANGELOG.md](CHANGELOG.md).
|
||||
- **Steam Frame** (the arm64 SteamOS download): SteamOS on the Steam Frame, kept up to date
|
||||
(v0.1.2 needs the September 2026 release or later). The arm64 download is for the Frame only;
|
||||
it hasn't been tested on any other arm64 Linux device.
|
||||
- **Windows (beta):** Windows 10 or 11, 64-bit; a CPU with AVX2 (Intel Haswell from 2013 or
|
||||
newer, or any AMD Ryzen); a Vulkan 1.2 graphics card with a current driver.
|
||||
- **Mac:** Apple silicon and **macOS 15** (Sequoia) or later. The app won't open on older
|
||||
versions of macOS, and there is no Intel Mac build.
|
||||
- **iPhone and iPad:** iOS or iPadOS 17 or later, on an A15-class chip or newer (iPhone 13 and
|
||||
@@ -164,7 +166,7 @@ should say OK. What changed in each release is in [CHANGELOG.md](CHANGELOG.md).
|
||||
the extracted disc files. No title update is needed. SpeedBreaker doesn't provide game files
|
||||
or disc images, or link to them.
|
||||
2. **Get SpeedBreaker.** [Download it](#download) for Steam Deck, Steam Machine and Steam Frame
|
||||
(SteamOS), Linux or Mac. On iPhone and iPad you sideload the experimental IPA or build it
|
||||
(SteamOS), Linux, Mac or Windows (beta). On iPhone and iPad you sideload the experimental IPA or build it
|
||||
yourself. Then follow the
|
||||
[setup for your platform](#setup-by-platform).
|
||||
3. **Start it and install.** On first run, SpeedBreaker looks for your disc image, checks that
|
||||
@@ -181,6 +183,7 @@ should say OK. What changed in each release is in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
- **Mac:** drag the new SpeedBreaker into Applications, choose **Replace**, then allow it once more with **Open Anyway** (see [Mac setup](#mac)).
|
||||
- **Steam Deck, Steam Machine, Steam Frame and Linux:** unpack the new download over the old folder (`~/Games/SpeedBreaker`); your Steam shortcut keeps working.
|
||||
- **Windows (beta):** extract the new zip into a new folder, run it from there, then delete the old folder.
|
||||
- **Everywhere:** your saves, settings and the installed game are kept, and no disc image is needed. Settings > Advanced > **Check for Updates** says when there's a new release, and so does GitHub's **Watch > Custom > Releases**.
|
||||
|
||||
## Setup by platform
|
||||
@@ -296,6 +299,23 @@ yet: the log's `[video] Vulkan devices:` line says which GPU it chose and why, a
|
||||
that line from yours would help. To pick another, start it with `NFSMW_GPU=<its number or part of
|
||||
its name>`, e.g. `NFSMW_GPU=intel %command%` in Steam's launch options.
|
||||
|
||||
### Windows (beta)
|
||||
|
||||
The Windows version is a **beta**: so far it has been tested on one laptop with Intel UHD
|
||||
Graphics and a 60 Hz screen. Download `SpeedBreaker-0.1.9-windows-x64-beta.zip` from the
|
||||
[Releases page][all-releases], right-click it, choose **Extract All**, and double-click
|
||||
`SpeedBreaker.exe` in the extracted folder (not inside the zip). SpeedBreaker isn't signed, so
|
||||
Windows may say **"Windows protected your PC"**: click **More info**, then **Run anyway**.
|
||||
(Smart App Control, where it's on, offers no Run anyway.) The installer then works as on the
|
||||
other platforms: it looks for your `.iso` in your user folder, Downloads, Desktop, Documents and
|
||||
Games and on your other drives, or takes one (or an extracted disc folder) you browse for, and
|
||||
puts the game in `%LOCALAPPDATA%\SpeedBreaker`, where your saves, settings and logs go too.
|
||||
`README.txt` in the zip has the details. **Check for Updates** isn't available on Windows yet.
|
||||
|
||||
Please [report](#reporting-a-bug) how it runs, especially on NVIDIA, AMD and Intel Arc
|
||||
graphics and on 120 or 144 Hz monitors, with "Windows beta" in the issue's title. Windows
|
||||
builds made by others aren't supported here.
|
||||
|
||||
### Mac
|
||||
|
||||
1. **Download and install.** Download [`SpeedBreaker-macos.dmg`][dl-macos], open it, and drag
|
||||
@@ -627,7 +647,7 @@ pull requests are handled.
|
||||
|
||||
## What's next
|
||||
|
||||
- **Windows:** coming soon.
|
||||
- **Windows:** in [beta](#windows-beta) now; a full release once it has run on more PCs.
|
||||
- **Android:** coming soon.
|
||||
|
||||
**One more thing… SpeedBreaker: Carbon, coming soon.** It's planned, not available yet.
|
||||
@@ -711,6 +731,12 @@ The supported executable is 3,936,256 bytes, with SHA-256
|
||||
|
||||
- **Other Linux distributions:** your distribution's equivalents of the package list above
|
||||
(Clang 18 or newer). Untested so far. The GPU needs Vulkan 1.2 and BC (DXT) texture support.
|
||||
- **Windows (beta):** the Visual Studio Build Tools (MSVC's libraries, the Windows SDK) with
|
||||
LLVM's `clang-cl` 19 or newer, CMake and Ninja, and SDL3, glslang and FFmpeg's XMA decoder built
|
||||
as static libraries with the static C runtime (`/MT`) into one folder. FFmpeg's configure
|
||||
options are `scripts/build_ffmpeg_xma.sh`'s, with FFmpeg's MSVC toolchain; the Windows
|
||||
third-party source archive on the release page has the exact build script. Step 2 runs on a
|
||||
Mac or Linux; copy `ppc/` and `tools/XenonRecomp` to the Windows PC.
|
||||
- `gh`, logged in, is optional: it fetches Xenia's instruction tests.
|
||||
|
||||
#### 2. Toolchain, your game files, recompilation
|
||||
@@ -752,6 +778,17 @@ cmake -S . -B build/main -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
ninja -C build/main SpeedBreaker
|
||||
```
|
||||
|
||||
On Windows (beta), from an x64 Visual Studio developer prompt with `clang-cl` on `PATH`:
|
||||
|
||||
```bat
|
||||
cmake -S . --preset windows-clang -DCMAKE_PREFIX_PATH=<deps folder> -DFFMPEG_XMA=<deps folder>
|
||||
ninja -C build\windows SpeedBreaker
|
||||
```
|
||||
|
||||
`--preset windows-clang-tester` instead links it without a console window and labels it, as the
|
||||
download is; `python scripts\windows\make_zip.py --public ...` (its header lists the options)
|
||||
packages it with the license notices.
|
||||
|
||||
Run `build/main/runtime/SpeedBreaker` from the repository root. With the whole disc extracted in
|
||||
`game/files`, it plays from there. Otherwise the first-run installer asks for your disc image.
|
||||
|
||||
@@ -868,6 +905,7 @@ SpeedBreaker is provided "as is", without warranty of any kind, as set out in th
|
||||
Public License.
|
||||
|
||||
[releases]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest
|
||||
[all-releases]: https://github.com/SpeedBreakerProject/speedbreaker/releases
|
||||
[dl-steamos-x86_64]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-steamos-x86_64.tar.xz
|
||||
[dl-steamos-arm64]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-steamos-arm64.tar.xz
|
||||
[dl-macos]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-macos.dmg
|
||||
|
||||
+25
-10
@@ -14,6 +14,12 @@ from you.
|
||||
Resolution to **Native**.
|
||||
- **From source:** the README's "Building from source", or
|
||||
[scripts/steamos/README.md](../scripts/steamos/README.md) for SteamOS.
|
||||
- **Windows (a beta build for testers):** unzip the kit somewhere (not
|
||||
inside the zip) and run `SpeedBreaker.exe`; its `README-TESTER.txt` has
|
||||
the rest. It is made with the `windows-clang-tester` CMake preset (no
|
||||
console window: everything goes to the session log; labelled "Windows
|
||||
beta") and packed with `scripts/windows/make_zip.py`, which checks that no
|
||||
game file, user name or user-folder path goes in.
|
||||
|
||||
## The menu
|
||||
|
||||
@@ -31,10 +37,13 @@ from you.
|
||||
1. Open the menu, go to the **Advanced** page (LB/RB, or Q/E) and choose
|
||||
**Save Bug Report** (A, or Enter).
|
||||
2. A notice at the bottom of the screen says where the file went:
|
||||
`speedbreaker-bug-report-v<version>-<date>_<time>.zip` on your **Desktop**
|
||||
(on SteamOS, `/home/deck/Desktop`: switch to Desktop Mode to pick it up;
|
||||
on iPhone and iPad, the Files app's SpeedBreaker folder), or, if there is
|
||||
no Desktop, in the game's data folder (below).
|
||||
`speedbreaker-bug-report-v<version>-<date>_<time>.zip` (a Windows beta adds
|
||||
its label: `...-windows-beta-<build date>-<commit>.zip`) on your
|
||||
**Desktop** (on SteamOS, `/home/deck/Desktop`: switch to Desktop Mode to
|
||||
pick it up; on iPhone and iPad, the Files app's SpeedBreaker folder; on
|
||||
Windows, the Desktop Explorer shows, OneDrive's if it backs that up), or,
|
||||
if there is no Desktop (or it can't take the file), in the game's data
|
||||
folder (below).
|
||||
3. Send that .zip with a few words: what you were doing, what you expected,
|
||||
what happened, and roughly how long into the session (the logs count
|
||||
seconds from launch).
|
||||
@@ -54,8 +63,9 @@ close the window), launch again and save one then: the hang report is kept.
|
||||
A bug report holds the last five sessions' logs, any crash and hang
|
||||
reports, your settings.toml, a summary of the machine (OS, CPU, memory, GPU
|
||||
and driver, display, the settings in effect) and a screenshot of the last
|
||||
game frame without the menu over it. Paths in your home folder show as `~`.
|
||||
Nothing is sent anywhere by the game: you send the file.
|
||||
game frame without the menu over it. Paths in your home folder show as `~`
|
||||
(on Windows, your user folder shows as `%USERPROFILE%`). Nothing is sent
|
||||
anywhere by the game: you send the file.
|
||||
|
||||
## Where the logs live
|
||||
|
||||
@@ -67,13 +77,18 @@ reports:
|
||||
(`$XDG_DATA_HOME/speedbreaker/logs/` if that is set; in portable mode,
|
||||
`data/logs/` in the game's folder, next to `portable.txt`)
|
||||
- macOS: `~/Library/Application Support/SpeedBreaker/logs/`
|
||||
- Windows: `%LOCALAPPDATA%\SpeedBreaker\logs\` (paste it into Explorer's
|
||||
address bar; in portable mode, `data\logs\` next to `SpeedBreaker.exe`
|
||||
and `portable.txt`). Keep `SpeedBreaker.pdb` next to `SpeedBreaker.exe`:
|
||||
crash and hang reports take the names of the game's functions from it.
|
||||
|
||||
On SteamOS the launcher also leaves the latest session's output in
|
||||
`last-run.log`, in the game's folder (`~/Games/SpeedBreaker/` for the
|
||||
download).
|
||||
|
||||
The first lines of every log say which build it is
|
||||
(`[build] SpeedBreaker v<version> (<commit>), built ...`, also shown at the
|
||||
bottom of Settings > Advanced), whether this version is new since the last
|
||||
run (`[version] updated from v<a> to v<b>`, or `first run`), and describe the
|
||||
machine (`[system] ...`).
|
||||
(`[build] SpeedBreaker v<version> (<commit>), built ...`, or for a Windows
|
||||
beta `[build] SpeedBreaker v<version> Windows beta <date> (<commit>): built
|
||||
...`, also shown at the bottom of Settings > Advanced), whether this version
|
||||
is new since the last run (`[version] updated from v<a> to v<b>`, or `first
|
||||
run`), and describe the machine (`[system] ...`).
|
||||
+22
-7
@@ -16,14 +16,29 @@ target_include_directories(NfsmwRecompLib PUBLIC
|
||||
${ROOT}/tools/XenonRecomp/thirdparty/simde)
|
||||
target_compile_options(NfsmwRecompLib PRIVATE
|
||||
-Wno-unused-label -Wno-unused-variable -Wno-unused-but-set-variable
|
||||
-fno-strict-aliasing
|
||||
# Checked indirect calls (runtime/cpu/indirect.cpp). SHELL: keeps each
|
||||
# -include with its file (CMake drops a repeated plain -include).
|
||||
"SHELL:-include \"${ROOT}/runtime/cpu/indirect.h\""
|
||||
# mftb reads the game's clock, which stands still while it's suspended.
|
||||
"SHELL:-include \"${ROOT}/runtime/cpu/guest_time.h\"")
|
||||
-fno-strict-aliasing)
|
||||
if(SB_MSVC_FRONTEND)
|
||||
# clang-cl has no -include: /FI, in the same order. The third, on
|
||||
# Windows only, aligns the guest's jmp_bufs for the CRT's setjmp
|
||||
# (runtime/platform/win32/guest_setjmp.h).
|
||||
target_compile_options(NfsmwRecompLib PRIVATE
|
||||
"/FI${ROOT}/runtime/cpu/indirect.h"
|
||||
"/FI${ROOT}/runtime/cpu/guest_time.h"
|
||||
"/FI${ROOT}/runtime/platform/win32/guest_setjmp.h")
|
||||
else()
|
||||
target_compile_options(NfsmwRecompLib PRIVATE
|
||||
# Checked indirect calls (runtime/cpu/indirect.cpp). SHELL: keeps each
|
||||
# -include with its file (CMake drops a repeated plain -include).
|
||||
"SHELL:-include \"${ROOT}/runtime/cpu/indirect.h\""
|
||||
# mftb reads the game's clock, which stands still while it's suspended.
|
||||
"SHELL:-include \"${ROOT}/runtime/cpu/guest_time.h\"")
|
||||
endif()
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
target_compile_options(NfsmwRecompLib PRIVATE -march=x86-64-v3)
|
||||
if(SB_MSVC_FRONTEND)
|
||||
target_compile_options(NfsmwRecompLib PRIVATE /clang:-march=x86-64-v3)
|
||||
else()
|
||||
target_compile_options(NfsmwRecompLib PRIVATE -march=x86-64-v3)
|
||||
endif()
|
||||
endif()
|
||||
if(IOS)
|
||||
target_compile_options(NfsmwRecompLib PRIVATE -mcpu=apple-a15)
|
||||
|
||||
+72
-3
@@ -13,10 +13,23 @@ add_library(XenonUtilsLite STATIC
|
||||
target_include_directories(XenonUtilsLite PUBLIC ${XR}/XenonUtils ${XR}/thirdparty/simde
|
||||
PRIVATE ${XR}/thirdparty/libmspack/libmspack/mspack ${XR}/thirdparty/tiny-AES-c ${XR}/thirdparty/TinySHA1)
|
||||
target_compile_definitions(XenonUtilsLite PRIVATE NOMINMAX)
|
||||
target_compile_options(XenonUtilsLite PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-include cstdlib>)
|
||||
if(SB_MSVC_FRONTEND)
|
||||
target_compile_options(XenonUtilsLite PRIVATE $<$<COMPILE_LANGUAGE:CXX>:/FIcstdlib>)
|
||||
else()
|
||||
target_compile_options(XenonUtilsLite PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-include cstdlib>)
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE RUNTIME_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
|
||||
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/thirdparty/")
|
||||
# The platform layer (platform/*.h): Windows' implementation in
|
||||
# platform/win32, everyone else's in platform/posix.
|
||||
if(WIN32)
|
||||
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/platform/posix/")
|
||||
# Built without -march, below (SpeedBreakerCpuCheck).
|
||||
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/platform/win32/cpu_check\\.cpp$")
|
||||
else()
|
||||
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/platform/win32/")
|
||||
endif()
|
||||
# Nayuki's QR Code generator (thirdparty/qrcodegen, vendored, C): the
|
||||
# download page's code in Check for Updates. Part of the runtime itself, not
|
||||
# a library of its own, so the iOS app (scripts/ios/build.sh) links it too.
|
||||
@@ -143,9 +156,17 @@ endif()
|
||||
# logs, crash reports and Settings > Advanced: generated/build_info.inc,
|
||||
# checked on every build but rewritten only when it changes
|
||||
# (report/build_info.cmake), so a no-op build stays one.
|
||||
# -DSB_BUILD_LABEL="Windows beta" names a build that goes to other people:
|
||||
# its build line starts "SpeedBreaker v<version> Windows beta <date>
|
||||
# (<commit>)" (logs, Settings > Advanced, reports), and its bug reports' file
|
||||
# names carry it too (scripts/windows/make_zip.py packs it).
|
||||
set(SB_BUILD_LABEL "" CACHE STRING "A label for a build that goes to other people (\"Windows beta\"); empty for none")
|
||||
if(NOT SB_BUILD_LABEL STREQUAL "" AND NOT SB_BUILD_LABEL MATCHES "^[A-Za-z0-9]+( [A-Za-z0-9]+)*$")
|
||||
message(FATAL_ERROR "SB_BUILD_LABEL: letters, digits and single spaces only (it goes into file names): \"${SB_BUILD_LABEL}\"")
|
||||
endif()
|
||||
add_custom_target(nfsmw_build_info
|
||||
COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/generated/build_info.inc
|
||||
-DVERSION=${CMAKE_PROJECT_VERSION} -DBUILD_TYPE=${CMAKE_BUILD_TYPE} -P ${CMAKE_CURRENT_SOURCE_DIR}/report/build_info.cmake
|
||||
-DVERSION=${CMAKE_PROJECT_VERSION} -DBUILD_TYPE=${CMAKE_BUILD_TYPE} "-DLABEL=${SB_BUILD_LABEL}" -P ${CMAKE_CURRENT_SOURCE_DIR}/report/build_info.cmake
|
||||
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/generated/build_info.inc
|
||||
VERBATIM)
|
||||
add_dependencies(SpeedBreaker nfsmw_build_info)
|
||||
@@ -167,7 +188,55 @@ target_include_directories(SpeedBreaker SYSTEM PRIVATE ${FFMPEG_INCLUDE_DIR} ${X
|
||||
# cases with assert(), and those must fail loudly, never fall through.
|
||||
target_compile_options(SpeedBreaker PRIVATE -Wno-unused-label -Wno-unused-variable -fno-strict-aliasing -UNDEBUG)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
target_compile_options(SpeedBreaker PRIVATE -march=x86-64-v3)
|
||||
if(SB_MSVC_FRONTEND)
|
||||
target_compile_options(SpeedBreaker PRIVATE /clang:-march=x86-64-v3)
|
||||
else()
|
||||
target_compile_options(SpeedBreaker PRIVATE -march=x86-64-v3)
|
||||
endif()
|
||||
endif()
|
||||
if(WIN32)
|
||||
enable_language(RC)
|
||||
# Debug information for the runtime (crash reports, debuggers), in the
|
||||
# objects (/Z7) and then the PDB; the recompiled code goes without.
|
||||
target_compile_options(SpeedBreaker PRIVATE /Z7)
|
||||
# The CPU check runs before any static initializer of the x86-64-v3
|
||||
# code, so it is built without -march, as an object library (a static
|
||||
# library's lone constructor would never be linked in).
|
||||
add_library(SpeedBreakerCpuCheck OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/cpu_check.cpp)
|
||||
target_link_libraries(SpeedBreaker PRIVATE SpeedBreakerCpuCheck)
|
||||
# The icon, the version and the manifest (UTF-8 code page, per-monitor
|
||||
# DPI awareness, long paths, Windows 10 and 11).
|
||||
target_sources(SpeedBreaker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/speedbreaker.rc)
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/speedbreaker.rc PROPERTIES
|
||||
OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/speedbreaker.manifest;${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/speedbreaker.ico")
|
||||
# /STACK: std::thread threads run guest code (the command processor),
|
||||
# and Windows reserves 1 MB by default; 8 MB is Linux's.
|
||||
# /DEBUG always, and with it /OPT:NOICF: without /DEBUG lld-link folds
|
||||
# identical functions, and thousands of one-line guest bodies (blr)
|
||||
# would share one address, aliasing PPCFuncMappings' host pointers.
|
||||
# /MANIFEST:NO: the manifest is in the .rc. /MAP: the link map, where
|
||||
# the runtime's overrides of generated functions can be checked.
|
||||
# /PDBALTPATH: the debug record names SpeedBreaker.pdb alone, not the
|
||||
# folder it was built in.
|
||||
target_link_options(SpeedBreaker PRIVATE /STACK:8388608 /DEBUG /PDBALTPATH:SpeedBreaker.pdb /OPT:REF /OPT:NOICF /INCREMENTAL:NO /MANIFEST:NO
|
||||
/MAP:$<TARGET_FILE_DIR:SpeedBreaker>/SpeedBreaker.map)
|
||||
# Console for development builds; -DSB_WINDOWS_GUI=ON for a release (no
|
||||
# console window; main() stays the entry point, and
|
||||
# platform::PrepareProcess gives stdin, stdout and stderr a terminal's
|
||||
# console or NUL, so the session log still gets everything: preset
|
||||
# windows-clang-tester).
|
||||
option(SB_WINDOWS_GUI "Link SpeedBreaker.exe for the Windows subsystem (release builds)" OFF)
|
||||
if(SB_WINDOWS_GUI)
|
||||
target_link_options(SpeedBreaker PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
|
||||
# main.cpp: a message box when there's no window (nothing else to see).
|
||||
target_compile_definitions(SpeedBreaker PRIVATE SB_WINDOWS_GUI=1)
|
||||
else()
|
||||
target_link_options(SpeedBreaker PRIVATE /SUBSYSTEM:CONSOLE)
|
||||
endif()
|
||||
# The timer resolution (winmm), crash reports (dbghelp), WaitOnAddress
|
||||
# (synchronization), and the shell's known folders (shell32, ole32);
|
||||
# FFmpeg's own (avcodec: ole32, user32; avutil: user32, bcrypt).
|
||||
target_link_libraries(SpeedBreaker PRIVATE winmm dbghelp synchronization bcrypt user32 shell32 ole32 advapi32)
|
||||
endif()
|
||||
if(IOS)
|
||||
# The CPU level both the iPad Pro (M2) and the iPhone 15 Pro (A17 Pro)
|
||||
|
||||
+32
-1
@@ -33,6 +33,7 @@
|
||||
#include <kernel/function.h>
|
||||
#include <cpu/guest_thread.h>
|
||||
#include <cpu/guest_time.h>
|
||||
#include <platform/timer.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#ifdef __APPLE__
|
||||
#include <pthread.h>
|
||||
@@ -148,6 +149,19 @@ namespace
|
||||
#endif
|
||||
GuestThreadContext ctx(0);
|
||||
SetHostThreadName("nfsmw-audio");
|
||||
// Windows (from rafidwayne's port, PR #39): MMCSS for this thread, and
|
||||
// its ticks on a high-resolution waitable timer (platform::SleepUntil):
|
||||
// sleep_until there wakes on the system tick, 1 ms only while
|
||||
// timeBeginPeriod(1) is honoured, which Windows 11 stops guaranteeing
|
||||
// while the window is occluded or minimized; at 15.6 ms every tick is
|
||||
// late and the catch-up bursts play silence.
|
||||
if (const char* priority = platform::RaiseAudioThreadPriority())
|
||||
fprintf(stderr, "[audio] pacing: a high-resolution timer, %s\n", priority);
|
||||
// Ticks that woke a whole interval or more late (a catch-up burst
|
||||
// follows), the worst, logged at most every 10 s.
|
||||
uint32_t lateTicks = 0;
|
||||
double worstLateMs = 0;
|
||||
auto lateLogged = steady_clock::now();
|
||||
constexpr auto INTERVAL = nanoseconds(1000000000ull * SAMPLES_PER_FRAME / SAMPLE_RATE);
|
||||
// The time suspended that `next` allows for. Both taken while running
|
||||
// (XAudio can start this thread in a suspension): `next` is a host
|
||||
@@ -176,7 +190,24 @@ namespace
|
||||
const auto started = steady_clock::now();
|
||||
while (s_running)
|
||||
{
|
||||
std::this_thread::sleep_until(next);
|
||||
platform::SleepUntil(duration_cast<nanoseconds>(next.time_since_epoch()).count());
|
||||
{
|
||||
const auto woke = steady_clock::now();
|
||||
const double lateMs = duration<double, std::milli>(woke - next).count();
|
||||
if (lateMs >= duration<double, std::milli>(INTERVAL).count())
|
||||
{
|
||||
lateTicks++;
|
||||
worstLateMs = std::max(worstLateMs, lateMs);
|
||||
}
|
||||
if (lateTicks && woke - lateLogged >= seconds(10))
|
||||
{
|
||||
fprintf(stderr, "[audio] pacing: %u ticks woke an interval or more late in %.0f s (worst %.1f ms)\n", lateTicks,
|
||||
duration<double>(woke - lateLogged).count(), worstLateMs);
|
||||
lateTicks = 0;
|
||||
worstLateMs = 0;
|
||||
lateLogged = woke;
|
||||
}
|
||||
}
|
||||
if (stallAt >= 0 && stallState < 2 && s_stream)
|
||||
{
|
||||
const double t = duration<double>(steady_clock::now() - started).count();
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#include <cpu/ppc_context.h>
|
||||
#include <report/report.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
|
||||
constexpr size_t PCR_SIZE = 0xAB0;
|
||||
constexpr size_t TLS_SIZE = 0x100;
|
||||
@@ -86,6 +88,10 @@ static size_t GetStackSize()
|
||||
{
|
||||
// 8 MiB is a typical default.
|
||||
constexpr auto defaultSize = 8 * 1024 * 1024;
|
||||
#ifdef _WIN32
|
||||
// A reservation, committed as it grows (platform/thread.h).
|
||||
stackSize = defaultSize;
|
||||
#else
|
||||
struct rlimit lim;
|
||||
const auto ret = getrlimit(RLIMIT_STACK, &lim);
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
@@ -103,19 +109,14 @@ static size_t GetStackSize()
|
||||
{
|
||||
stackSize = defaultSize;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return stackSize;
|
||||
}
|
||||
|
||||
void SetHostThreadName(const char* name)
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
pthread_setname_np(name);
|
||||
#elif defined(__linux__)
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
platform::NameCurrentThread(name);
|
||||
report::RegisterThread(name); // hang reports sample named threads
|
||||
}
|
||||
|
||||
@@ -151,16 +152,17 @@ GuestThreadHandle::GuestThreadHandle(const GuestThreadParams& params)
|
||||
: params(params), id(s_nextThreadId++), suspended((params.flags & 0x1) != 0)
|
||||
#ifdef USE_PTHREAD
|
||||
{
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, GetStackSize());
|
||||
refs = 2; // SpeedBreaker: the handle's reference + the thread's own
|
||||
const auto ret = pthread_create(&thread, &attr, GuestThreadFunc, this);
|
||||
const auto ret = platform::StartThread(&thread, GetStackSize(), GuestThreadFunc, this);
|
||||
if (ret != 0) {
|
||||
#ifdef _WIN32
|
||||
fprintf(stderr, "_beginthreadex failed with error code 0x%X.\n", ret);
|
||||
#else
|
||||
fprintf(stderr, "pthread_create failed with error code 0x%X.\n", ret);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
pthread_detach(thread);
|
||||
platform::DetachThread(thread);
|
||||
}
|
||||
#else
|
||||
, thread(GuestThreadFunc, this)
|
||||
@@ -231,34 +233,3 @@ void GuestThread::SetLastError(uint32_t error)
|
||||
// TEB + 0x160 : Win32LastError
|
||||
*(uint32_t*)(thread + TEB_OFFSET + 0x160) = ByteSwap(error);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void GuestThread::SetThreadName(uint32_t threadId, const char* name)
|
||||
{
|
||||
#pragma pack(push,8)
|
||||
const DWORD MS_VC_EXCEPTION = 0x406D1388;
|
||||
|
||||
typedef struct tagTHREADNAME_INFO
|
||||
{
|
||||
DWORD dwType; // Must be 0x1000.
|
||||
LPCSTR szName; // Pointer to name (in user addr space).
|
||||
DWORD dwThreadID; // Thread ID (-1=caller thread).
|
||||
DWORD dwFlags; // Reserved for future use, must be zero.
|
||||
} THREADNAME_INFO;
|
||||
#pragma pack(pop)
|
||||
|
||||
THREADNAME_INFO info;
|
||||
info.dwType = 0x1000;
|
||||
info.szName = name;
|
||||
info.dwThreadID = threadId;
|
||||
info.dwFlags = 0;
|
||||
|
||||
__try
|
||||
{
|
||||
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -5,12 +5,13 @@
|
||||
#include <kernel/xdm.h>
|
||||
#include <kernel/dispatcher.h>
|
||||
|
||||
// SpeedBreaker: pthreads on every platform (macOS and Linux), for the stack
|
||||
// size and so thread handles share one detached, reference-counted path.
|
||||
// SpeedBreaker: pthreads on every platform (macOS and Linux; Windows:
|
||||
// _beginthreadex, platform/thread.h), for the stack size and so thread
|
||||
// handles share one detached, reference-counted path.
|
||||
#define USE_PTHREAD 1
|
||||
|
||||
#ifdef USE_PTHREAD
|
||||
#include <pthread.h>
|
||||
#include <platform/thread.h>
|
||||
#endif
|
||||
|
||||
#define CURRENT_THREAD_HANDLE uint32_t(-2)
|
||||
@@ -46,7 +47,7 @@ struct GuestThreadHandle : Waitable
|
||||
std::atomic<bool> suspended;
|
||||
bool finished = false; // SpeedBreaker: guarded by dispatcher::Lock()
|
||||
#ifdef USE_PTHREAD
|
||||
pthread_t thread;
|
||||
platform::Thread thread{};
|
||||
#else
|
||||
std::thread thread;
|
||||
#endif
|
||||
@@ -70,10 +71,6 @@ struct GuestThread
|
||||
static void SetDefaultStackSize(size_t size);
|
||||
// The current thread's kernel object (for the 0xFFFFFFFE pseudo-handle).
|
||||
static KernelObject* GetCurrentThreadObject();
|
||||
|
||||
#ifdef _WIN32
|
||||
static void SetThreadName(uint32_t threadId, const char* name);
|
||||
#endif
|
||||
};
|
||||
|
||||
// Names the calling host thread, for top -H, perf and debuggers (at most 15
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace gpu::clock
|
||||
std::map<uint64_t, uint64_t> ReadTimes(const std::filesystem::path& file)
|
||||
{
|
||||
std::map<uint64_t, uint64_t> times;
|
||||
FILE* f = fopen(file.c_str(), "r");
|
||||
FILE* f = fopen(file.string().c_str(), "r");
|
||||
if (!f)
|
||||
return times;
|
||||
char line[1024];
|
||||
|
||||
@@ -30,6 +30,32 @@
|
||||
// shared mapping, then a copy in uncached system memory; ring in VRAM), the
|
||||
// Steam Frame and other unified-memory devices (a copy in their one
|
||||
// host-visible device memory), and Apple's (imported).
|
||||
//
|
||||
// Windows. NVIDIA's Windows driver exposes CPU-mapped VRAM as its own
|
||||
// heap, a 256 MB window (214-256 MB) unless resizable BAR is on in the
|
||||
// firmware and the driver's profile for the game turns it on, which it does
|
||||
// only for games it knows: the window kit 1 (the first Windows tester
|
||||
// build) put the 512 MB copy in, so it quit at startup on such a card. The
|
||||
// plan is the same on Windows; what differs is the import. Guest physical
|
||||
// memory there is a view of one pagefile-backed section, mapped at each of
|
||||
// its three windows into placeholders (platform/win32/memory.cpp:
|
||||
// VirtualAlloc2, MapViewOfFile3), and the import is of the 0xA0000000 view.
|
||||
// Intel's Windows driver imports it, and on the ThinkPad that tested the
|
||||
// port the write watch's VirtualProtect (read-only and no-access pages, in
|
||||
// all three views) leaves the imported memory working both ways: the
|
||||
// laptop.md P0 probe, item f, and every run since. That is the failure
|
||||
// ANV's import has (ImportRefusal) and Windows' can't: Windows gives drivers
|
||||
// no MMU notifier, so a driver that maps a process's pages for its device
|
||||
// locks them for as long as it uses them, and a protection change reaches
|
||||
// only the CPU's page tables. Whether NVIDIA's and AMD's Windows drivers take
|
||||
// a file mapping's view is unknown. If one doesn't, it refuses at once
|
||||
// (vkGetMemoryHostPointerPropertiesEXT or vkAllocateMemory), the renderer
|
||||
// logs that, and the copy in system memory comes next. So a discrete GPU
|
||||
// without resizable BAR imports first on Windows too: the copy in system
|
||||
// memory would put guest memory in the same place (system RAM the GPU
|
||||
// reads over PCIe) and add the CPU's page copies (writewatch::SyncShadow)
|
||||
// every frame, and copies back of what resolves wrote when the CPU reads
|
||||
// it. The copy goes first only where it lives in VRAM.
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
+466
-47
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <kernel/vmem.h>
|
||||
#include <kernel/write_watch.h>
|
||||
#include <report/report.h>
|
||||
#include <video/presenter.h>
|
||||
#include <video/picture_fit.h>
|
||||
#include <video/scale_budget.h>
|
||||
@@ -46,12 +47,79 @@
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#ifndef _WIN32
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
// swapWord(v, e) in the compute shaders: a guest word in Xenos endianness e
|
||||
// (0 none, 1 8in16, 2 8in32, 3 16in32; the resolve's 4 and up: 8in32) as a
|
||||
// host uint. On Windows as selects, like the vertex and pixel shaders'
|
||||
// gpuSwap (shader_translator.cpp): Intel's Windows driver compiled the
|
||||
// early-return chain wrongly. In the compute shaders it showed in races: a
|
||||
// black starburst over the middle of the picture. Elsewhere as before.
|
||||
// The select forms equal the chains for every e (the plain swapWord leaves
|
||||
// 4 and up unswapped, as its chain does).
|
||||
//
|
||||
// The same shape elsewhere in the compute shaders gets the same treatment
|
||||
// on Windows, whether or not it has shown yet: the resolve's bppLog2 (a run
|
||||
// of early returns) and the front image's swap (an else-if chain on
|
||||
// p.endian). Intel's 101.2135 merged one branch's value into the default;
|
||||
// nothing here shows whether it does so for any other chain.
|
||||
#ifdef _WIN32
|
||||
#define GLSL_SWAP_WORD R"(uint swapWord(uint v, uint e)
|
||||
{
|
||||
uint s8 = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
v = (e == 1u || e == 2u) ? s8 : v;
|
||||
return (e == 2u || e == 3u) ? (v >> 16) | (v << 16) : v;
|
||||
})"
|
||||
#define GLSL_SWAP_WORD_RESOLVE R"(uint swapWord(uint v, uint e)
|
||||
{
|
||||
uint s8 = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
v = (e == 1u || e == 2u || e >= 4u) ? s8 : v;
|
||||
return e >= 2u ? (v >> 16) | (v << 16) : v;
|
||||
})"
|
||||
#define GLSL_BPP_LOG2 R"(uint bppLog2(uint f)
|
||||
{
|
||||
uint b = (f == 26u || f == 32u || f == 34u || f == 37u) ? 3u : 2u; // 64, else 32
|
||||
b = (f == 3u || f == 4u || f == 10u || f == 15u || f == 24u || f == 30u) ? 1u : b;
|
||||
return f == 2u ? 0u : b; // 8
|
||||
})"
|
||||
#define GLSL_FRONT_SWAP R"( uint s8 = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
v = (p.endian == 1u || p.endian == 2u) ? s8 : v;
|
||||
v = (p.endian == 2u || p.endian == 3u) ? (v >> 16) | (v << 16) : v;)"
|
||||
#else
|
||||
#define GLSL_BPP_LOG2 R"(uint bppLog2(uint f)
|
||||
{
|
||||
if (f == 2u) return 0u; // 8
|
||||
if (f == 3u || f == 4u || f == 10u || f == 15u || f == 24u || f == 30u) return 1u;
|
||||
if (f == 26u || f == 32u || f == 34u || f == 37u) return 3u;
|
||||
return 2u;
|
||||
})"
|
||||
#define GLSL_FRONT_SWAP R"( if (p.endian == 1u) v = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
else if (p.endian == 2u) v = (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
else if (p.endian == 3u) v = (v >> 16) | (v << 16);)"
|
||||
#define GLSL_SWAP_WORD R"(uint swapWord(uint v, uint e)
|
||||
{
|
||||
if (e == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (e == 2u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (e == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
})"
|
||||
#define GLSL_SWAP_WORD_RESOLVE R"(uint swapWord(uint v, uint e)
|
||||
{
|
||||
if (e == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (e == 2u || e >= 4u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (e == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
})"
|
||||
#endif
|
||||
|
||||
namespace gpu::renderer
|
||||
{
|
||||
namespace
|
||||
@@ -76,7 +144,16 @@ namespace gpu::renderer
|
||||
// Lighter passes (NFSMW_UNTILED_AREA, NFSMW_PASS_BARRIER): Apple only.
|
||||
// On the Frame the smaller area cost +0.3 ms (Turnip chooses tiled
|
||||
// or direct rendering by the area) and the barrier changed nothing.
|
||||
// Each variable overrides.
|
||||
// Windows: both on an Intel integrated GPU, chosen by the device in
|
||||
// Initialize (PassDefaultsForDevice), but for the pass barrier (see
|
||||
// s_passBarrierLevel: the untiled area alone there). On a UHD 620
|
||||
// (Intel's driver), GPU-bound at ~20 fps in a race at 1x: lighter
|
||||
// +1.15 fps (3 of 3 runs; the barrier's share of it measured later at
|
||||
// ~0.1), fewer +0.55 (+0.10 to +0.81), together with occlusion
|
||||
// counting +1.8 (-4.0 ms GPU); no artifact seen in the parked race's
|
||||
// captures (by eye: they aren't the same game frame). AMD's APUs
|
||||
// and discrete GPUs there are unmeasured: off. Each variable
|
||||
// overrides.
|
||||
#if defined(__APPLE__) || (defined(__linux__) && defined(__aarch64__))
|
||||
constexpr bool kFewerPasses = true;
|
||||
#else
|
||||
@@ -86,6 +163,14 @@ namespace gpu::renderer
|
||||
constexpr bool kLighterPasses = true;
|
||||
#else
|
||||
constexpr bool kLighterPasses = false;
|
||||
#endif
|
||||
// What follows kFewerPasses and kLighterPasses: constants of the
|
||||
// build, but on Windows PassDefaultsForDevice sets them once, before
|
||||
// the first frame, where their variables are unset.
|
||||
#ifdef _WIN32
|
||||
#define PASS_CONST
|
||||
#else
|
||||
#define PASS_CONST const
|
||||
#endif
|
||||
|
||||
// Where a frame's time went, for hitch reports (renderer::HitchReport).
|
||||
@@ -617,6 +702,14 @@ namespace gpu::renderer
|
||||
// here, harvested at completion, then the END reports, in order.
|
||||
std::vector<ZpdSegment> zpdSegments;
|
||||
std::vector<ZpdEnd> zpdEnds;
|
||||
#ifdef _WIN32
|
||||
// NFSMW_BLACK_CHECK=1: the present whose front image this
|
||||
// submission probes (its entry in s_black.host, else -1).
|
||||
int32_t blackEntry = -1;
|
||||
uint64_t blackFrame = 0;
|
||||
double blackSec = 0;
|
||||
uint32_t blackDraws = 0, blackWaiting = 0, blackResolves = 0; // the game's work for that frame
|
||||
#endif
|
||||
};
|
||||
// NFSMW_FENCE_EVENTS=0 disables mid-buffer fence publication.
|
||||
const bool s_fenceEvents = [] { const char* v = std::getenv("NFSMW_FENCE_EVENTS"); return !v || v[0] != '0'; }();
|
||||
@@ -970,6 +1063,66 @@ namespace gpu::renderer
|
||||
(unsigned long long)raw, count, ms, lost ? std::format(" ({} segments lost)", lost).c_str() : "");
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// NFSMW_BLACK_CHECK=1: a black-frame count over every present, not a
|
||||
// capture now and then (P3's black frames came from Intel's driver
|
||||
// and hit a third of all frames; captures cost ~0.5 s each here).
|
||||
// After each present's front image is written a compute pass counts
|
||||
// its texels with any red, green or blue (RecordBlackProbe); the
|
||||
// completion thread reads the count once that submission is done and
|
||||
// logs every present whose front image is all zero, with a summary
|
||||
// every 600 presents. The game's own cuts to black count too (the
|
||||
// intro cinematic has some). Each line gives the frame, the time and
|
||||
// the game's work for that frame (draws since the last present, how
|
||||
// many waited for a pipeline still compiling, resolves): a cut the
|
||||
// game makes has the same counts on the Mac at the same point, while
|
||||
// a high draw count alone proves nothing (a fade from black draws the
|
||||
// whole scene under it). Only whole black frames are seen, not a pass
|
||||
// missing under the HUD.
|
||||
struct BlackCheck
|
||||
{
|
||||
bool on = false;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
const volatile uint32_t* host = nullptr;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
|
||||
std::chrono::steady_clock::time_point start;
|
||||
uint32_t next = 0; // command processor
|
||||
uint64_t drawsAt = 0, resolvesAt = 0; // command processor: at the last present
|
||||
uint64_t presents = 0, black = 0, windowBlack = 0, logged = 0; // completion thread
|
||||
};
|
||||
BlackCheck s_black;
|
||||
// Entries are reused in turn. One outlives its submission: more
|
||||
// entries than slots, and a slot's next recording waits for its last
|
||||
// submission to be retired, harvest included.
|
||||
constexpr uint32_t kBlackEntries = 4 * kSlots;
|
||||
|
||||
// Completion thread: `sub` (which carries a probe) is done.
|
||||
void BlackHarvest(Submission& sub)
|
||||
{
|
||||
uint32_t lit = s_black.host[sub.blackEntry];
|
||||
s_black.presents++;
|
||||
if (lit == 0)
|
||||
{
|
||||
s_black.black++;
|
||||
s_black.windowBlack++;
|
||||
if (s_black.logged++ < 2000)
|
||||
fprintf(stderr, "[black] frame %llu, %.2f s: the front image is all zero; the game's frame: %u draws (%u waiting for a "
|
||||
"pipeline), %u resolves\n", (unsigned long long)sub.blackFrame, sub.blackSec, sub.blackDraws, sub.blackWaiting,
|
||||
sub.blackResolves);
|
||||
}
|
||||
if (s_black.presents % 600 == 0)
|
||||
{
|
||||
fprintf(stderr, "[black] %llu of the last 600 presents black, %llu of %llu since the first\n",
|
||||
(unsigned long long)s_black.windowBlack, (unsigned long long)s_black.black, (unsigned long long)s_black.presents);
|
||||
s_black.windowBlack = 0;
|
||||
}
|
||||
sub.blackEntry = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void CompletionThread()
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
@@ -1068,6 +1221,10 @@ namespace gpu::renderer
|
||||
}
|
||||
if (!sub.zpdSegments.empty())
|
||||
ZpdHarvest(slot, sub);
|
||||
#ifdef _WIN32
|
||||
if (sub.blackEntry >= 0)
|
||||
BlackHarvest(sub);
|
||||
#endif
|
||||
{
|
||||
// Writes run under the lock so QueueWrite can't append to
|
||||
// a submission that is being retired.
|
||||
@@ -1160,7 +1317,18 @@ namespace gpu::renderer
|
||||
// [perf] window, off in the rest: one run, the same content in both
|
||||
// arms. Each window's [barrier] line names the level it ran at.
|
||||
// Default 2 with kLighterPasses (Apple: iPad Pro M2 GPU-bound at 2x,
|
||||
// same-run windows: executing -0.5 ms), else 0.
|
||||
// same-run windows: executing -0.5 ms), else 0. Windows on an Intel
|
||||
// integrated GPU keeps 0 with the rest of the lighter passes: on a UHD
|
||||
// 620, GPU-bound at ~23 fps in a race, level 2 against 0 in the same
|
||||
// runs (the ALT above, 15 race window pairs in 2 runs) was -0.10 ms
|
||||
// of GPU busy (median -0.30), within noise; and no scene on any
|
||||
// device has needed a vertex barrier, so the validation layer has
|
||||
// never seen the path that keeps level 2 safe where a vertex shader
|
||||
// reads what a resolve wrote. Rechecked with Windows' Best
|
||||
// performance power mode (the chip at ~25 W, ~27 fps in the race):
|
||||
// the ALT's 18 race window pairs -0.04 ms, and in whole runs (2
|
||||
// against 2) level 2 was no faster (race -1.3 fps, cinematic -0.5,
|
||||
// inside that mode's run-to-run spread of about 1 fps).
|
||||
const uint32_t s_passBarrierLevel = [] { const char* v = std::getenv("NFSMW_PASS_BARRIER"); return v ? uint32_t(std::clamp(std::atoi(v), 0, 3)) : kLighterPasses ? 2u : 0u; }();
|
||||
const bool s_passBarrierAlt = [] { const char* v = std::getenv("NFSMW_PASS_BARRIER_ALT"); return v && v[0] == '1'; }() && s_passBarrierLevel;
|
||||
uint32_t s_passBarrier = s_passBarrierAlt ? 0u : s_passBarrierLevel;
|
||||
@@ -1475,7 +1643,7 @@ namespace gpu::renderer
|
||||
if (!s_shaderCacheEnabled)
|
||||
return CompileGlslUncached(source, stage, log, preamble);
|
||||
std::filesystem::path file = ShaderCacheFile(source, stage, preamble);
|
||||
if (FILE* f = fopen(file.c_str(), "rb"))
|
||||
if (FILE* f = fopen(file.string().c_str(), "rb"))
|
||||
{
|
||||
std::vector<uint32_t> spirv;
|
||||
fseek(f, 0, SEEK_END);
|
||||
@@ -1502,7 +1670,7 @@ namespace gpu::renderer
|
||||
// Unique per thread: two workers may compile the same source.
|
||||
std::filesystem::path temp = file;
|
||||
temp += std::format(".{}.tmp", std::hash<std::thread::id>{}(std::this_thread::get_id()));
|
||||
if (FILE* f = fopen(temp.c_str(), "wb"))
|
||||
if (FILE* f = fopen(temp.string().c_str(), "wb"))
|
||||
{
|
||||
bool ok = fwrite(spirv.data(), 4, spirv.size(), f) == spirv.size();
|
||||
ok = fclose(f) == 0 && ok;
|
||||
@@ -1567,7 +1735,7 @@ namespace gpu::renderer
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
if (s_shaderCacheEnabled)
|
||||
if (FILE* f = fopen((GetCachePath() / "pipelines.bin").c_str(), "rb"))
|
||||
if (FILE* f = fopen((GetCachePath() / "pipelines.bin").string().c_str(), "rb"))
|
||||
{
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
@@ -1641,7 +1809,7 @@ namespace gpu::renderer
|
||||
std::filesystem::create_directories(GetCachePath(), ec);
|
||||
std::filesystem::path file = GetCachePath() / "pipelines.bin", temp = file;
|
||||
temp += ".tmp";
|
||||
if (FILE* f = fopen(temp.c_str(), "wb"))
|
||||
if (FILE* f = fopen(temp.string().c_str(), "wb"))
|
||||
{
|
||||
bool ok = fwrite(data.data(), 1, size, f) == size;
|
||||
ok = fclose(f) == 0 && ok;
|
||||
@@ -1716,6 +1884,25 @@ namespace gpu::renderer
|
||||
VkDescriptorSetLayout s_set0Layout = VK_NULL_HANDLE;
|
||||
VkDescriptorPool s_descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet s_set0 = VK_NULL_HANDLE;
|
||||
#ifdef _WIN32
|
||||
// Intel's Windows driver (UHD 620, 101.2135) reads zeros through a
|
||||
// dynamic uniform or storage buffer descriptor wherever the dynamic
|
||||
// offset plus the read's place in the range reaches 128 MB (2^27);
|
||||
// a descriptor whose own offset is that far in reads right (P3's
|
||||
// uborepro). The draws' constants are a dynamic uniform buffer in
|
||||
// the 192 MB upload ring: draws whose constants landed in slots 8-11
|
||||
// read zeros, their vertices collapsed to w = 0, and every primitive
|
||||
// was clipped. A third of all frames were black (the title, menus,
|
||||
// races) or lost whole passes (a black world under the HUD). Here
|
||||
// set 0 is one set per ring slot with the constants at the slot's
|
||||
// start, so the dynamic offset is the offset in the slot (under 16
|
||||
// MB). Elsewhere one set, as before. NFSMW_SLOT_SETS=0 binds the one
|
||||
// set instead (to see whether a newer driver still has the bug:
|
||||
// NFSMW_BLACK_CHECK=1 counts the black frames it brings back).
|
||||
VkDescriptorPool s_slotDescriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet s_slotSet0[kSlots] = {};
|
||||
bool s_slotSets = true;
|
||||
#endif
|
||||
VkSampler s_sampler = VK_NULL_HANDLE;
|
||||
Image s_dummy[3]; // 2D, 3D, cube: until the texture cache (M4)
|
||||
Image s_dummyStorage; // R32_UINT 1x1: a resolve's texture binding when it stores into none
|
||||
@@ -1876,7 +2063,7 @@ namespace gpu::renderer
|
||||
// and each shader pair keeping the whole target, once.
|
||||
// On by default with kLighterPasses (Apple: iPad Pro M2 GPU-bound at
|
||||
// 2x, same-run windows: executing -1.0 ms), else off; =0 turns it off.
|
||||
const uint32_t s_untiledArea = [] {
|
||||
PASS_CONST uint32_t s_untiledArea = [] {
|
||||
const char* v = std::getenv("NFSMW_UNTILED_AREA");
|
||||
return !v ? (kLighterPasses ? 1u : 0u) : v[0] == '1' ? 1u : strcmp(v, "alt") == 0 ? 2u : 0u;
|
||||
}();
|
||||
@@ -2001,6 +2188,14 @@ namespace gpu::renderer
|
||||
// Settings > Video > Internal Resolution; NFSMW_RENDER_SCALE
|
||||
// (<n> or <sx>x<sy>, developer override) wins.
|
||||
s_scaleBudget = props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 6 : 4;
|
||||
#ifdef _WIN32
|
||||
// An Intel integrated GPU on Windows renders at 1x, as iOS does:
|
||||
// a UHD 620 runs a race at 1x at ~20 fps, GPU-bound, and Auto
|
||||
// would pick 2x2 for a maximized window or a 1080p screen
|
||||
// (presenter.cpp, IntegratedGpuDefaults).
|
||||
if (s_vk->integratedGpuDefaults)
|
||||
s_scaleBudget = 1;
|
||||
#endif
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
// iPhones and iPads: chip by chip (video/scale_budget), more than
|
||||
// 1x1 only where a chip was measured to hold a race at 60 fps with
|
||||
@@ -2072,6 +2267,29 @@ namespace gpu::renderer
|
||||
struct RecentTarget { uint64_t key = ~0ull; RenderTarget* target = nullptr; };
|
||||
RecentTarget s_recentTargets[16];
|
||||
|
||||
#ifdef _WIN32
|
||||
// EDRAM is one memory: a depth target the game first uses at a base
|
||||
// where it drew with another layout holds, on the console, what that
|
||||
// EDRAM was last cleared to (the game clears depth as it resolves).
|
||||
// The race's first frame does this (depth at base 1312 with pitch
|
||||
// 1280, after pitch 640 with 2x MSAA in the cinematic) and draws with
|
||||
// LEQUAL before any clear of its own: from zero every draw failed and
|
||||
// the frame was black (P3: in every laptop run where that target was
|
||||
// first made at the race start, and whenever it was made anew there
|
||||
// on purpose). So a new depth target starts from the last depth clear
|
||||
// at its base (ClearRect records it), or from zero where there was
|
||||
// none, as before. Limits: the same base only (other overlapping
|
||||
// layouts aren't modelled), and a new target only: one that exists
|
||||
// keeps its own contents after another layout's newer clear at its
|
||||
// base. Every such reuse the review counted (drive and long scripts)
|
||||
// found the target's own last clear, the same 1.0, nothing drawn
|
||||
// since. Windows only for now, though the cause is this renderer, not
|
||||
// a driver: Linux (same pass defaults) can hit it whenever its timing
|
||||
// makes that target at the race start. No Mac run made it there: with
|
||||
// the Mac's defaults it was never made, with Windows defaults in the
|
||||
// cut before the cinematic or not at all.
|
||||
std::unordered_map<uint32_t, VkClearDepthStencilValue> s_lastDepthClear;
|
||||
#endif
|
||||
// A new target, at the current scale if its pitch is a scaled one,
|
||||
// cleared to zero (new EDRAM contents).
|
||||
std::unique_ptr<RenderTarget> CreateTarget(uint32_t base, uint32_t format, bool depth, uint32_t pitch, bool wide)
|
||||
@@ -2126,6 +2344,14 @@ namespace gpu::renderer
|
||||
if (depth)
|
||||
{
|
||||
VkClearDepthStencilValue v{ 0.0f, 0 };
|
||||
#ifdef _WIN32
|
||||
if (auto it = s_lastDepthClear.find(base); it != s_lastDepthClear.end())
|
||||
{
|
||||
v = it->second;
|
||||
fprintf(stderr, "[renderer] EDRAM target at base %u (pitch %u) starts at the depth last cleared there: %g, stencil %u\n", base,
|
||||
pitch, v.depth, v.stencil);
|
||||
}
|
||||
#endif
|
||||
vkCmdClearDepthStencilImage(s_cmd, rt->image.image, VK_IMAGE_LAYOUT_GENERAL, &v, 1, &range);
|
||||
}
|
||||
else
|
||||
@@ -2179,7 +2405,7 @@ namespace gpu::renderer
|
||||
// view's pass in three: 10 of the 46 passes of an iPad race frame's
|
||||
// profile. Expected ~0.4-0.5 ms of its GPU time (bit 1 ~0.2, bit 2
|
||||
// ~0.15-0.25: the glow and flare draws still run).
|
||||
const uint32_t s_passMerge = [] { const char* v = std::getenv("NFSMW_PASS_MERGE"); return v ? uint32_t(std::atoi(v)) : kFewerPasses ? 3u : 0u; }();
|
||||
PASS_CONST uint32_t s_passMerge = [] { const char* v = std::getenv("NFSMW_PASS_MERGE"); return v ? uint32_t(std::atoi(v)) : kFewerPasses ? 3u : 0u; }();
|
||||
|
||||
void EndPass()
|
||||
{
|
||||
@@ -2278,7 +2504,7 @@ namespace gpu::renderer
|
||||
// tile bands, ~0.2-0.3 ms of the iPad's GPU time. The clears kept
|
||||
// wait for the next pass on their target, which must hold them
|
||||
// (BeginPass logs one that reaches past its pass).
|
||||
const bool s_clearFlushRect = [] { const char* v = std::getenv("NFSMW_CLEAR_FLUSH_RECT"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
PASS_CONST bool s_clearFlushRect = [] { const char* v = std::getenv("NFSMW_CLEAR_FLUSH_RECT"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
|
||||
void BeginPass(RenderTarget* const color[4], RenderTarget* depth, VkRect2D area)
|
||||
{
|
||||
@@ -2834,6 +3060,9 @@ namespace gpu::renderer
|
||||
auto build = [job, key, layout, vs, ps] {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
VkPipeline pipeline = BuildPipeline(key, layout, *vs, *ps);
|
||||
// Progress for the hang watchdog: a cold cache's compiles
|
||||
// can hold a frame for 20 s on a slow driver (Windows).
|
||||
report::g_pipelinesBuilt.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(job->mutex);
|
||||
job->pipeline = pipeline;
|
||||
@@ -3573,13 +3802,7 @@ uint tiled2D(uint x, uint y, uint pitch, uint bppLog2)
|
||||
(((oi >> 5) & 7u) << 8) | ((oi >> 8) << 12);
|
||||
}
|
||||
|
||||
uint swapWord(uint v, uint e)
|
||||
{
|
||||
if (e == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (e == 2u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (e == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
)" GLSL_SWAP_WORD R"(
|
||||
|
||||
uint addr(uint bx, uint by)
|
||||
{
|
||||
@@ -3667,7 +3890,7 @@ void main()
|
||||
// untiles, six copies and a split render pass a frame (two in heavy
|
||||
// scenes). A device that turns the image or a view down keeps the
|
||||
// staging path for that format.
|
||||
const bool s_directCube = [] { const char* v = std::getenv("NFSMW_DIRECT_CUBE"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
PASS_CONST bool s_directCube = [] { const char* v = std::getenv("NFSMW_DIRECT_CUBE"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
// fi.vk -> a storable R32_UINT view is allowed (2D; cube compatible, with six layers)
|
||||
std::unordered_map<VkFormat, bool> s_directFormats, s_directCubeFormats;
|
||||
|
||||
@@ -3803,7 +4026,7 @@ void main()
|
||||
// (1280x720) are each resolved in three strips, a predicated tile
|
||||
// each, and were untiled again between passes every frame (3.7 MB
|
||||
// each), on every platform.
|
||||
const bool s_fusePartial = [] { const char* v = std::getenv("NFSMW_FUSE_PARTIAL"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
PASS_CONST bool s_fusePartial = [] { const char* v = std::getenv("NFSMW_FUSE_PARTIAL"); return v ? v[0] == '1' : kFewerPasses; }();
|
||||
// Direct textures by their first guest byte (the latest made there),
|
||||
// for resolves into part of one (Resolve): every one with
|
||||
// NFSMW_FUSE_PARTIAL, the cubes with NFSMW_DIRECT_CUBE alone; and the
|
||||
@@ -4817,7 +5040,7 @@ void main()
|
||||
// The constants pushed to the resolve shader: without NFSMW_FUSE_PARTIAL
|
||||
// and NFSMW_DIRECT_CUBE, those before texX, as before them, and the
|
||||
// shader is built without TEX_OFFSET (the same code and push).
|
||||
const uint32_t s_resolvePush = s_fusePartial || s_directCube ? offsetof(ResolveConstants, hostWrite) : offsetof(ResolveConstants, texX);
|
||||
PASS_CONST uint32_t s_resolvePush = s_fusePartial || s_directCube ? offsetof(ResolveConstants, hostWrite) : offsetof(ResolveConstants, texX);
|
||||
|
||||
const char* kResolveGlsl = R"(#version 460
|
||||
#extension GL_EXT_samplerless_texture_functions : require
|
||||
@@ -4900,21 +5123,9 @@ uint tiled2D(uint x, uint y, uint pitch, uint bppLog2)
|
||||
(((oi >> 5) & 7u) << 8) | ((oi >> 8) << 12);
|
||||
}
|
||||
|
||||
uint swapWord(uint v, uint e)
|
||||
{
|
||||
if (e == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (e == 2u || e >= 4u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (e == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
)" GLSL_SWAP_WORD_RESOLVE R"(
|
||||
|
||||
uint bppLog2(uint f)
|
||||
{
|
||||
if (f == 2u) return 0u; // 8
|
||||
if (f == 3u || f == 4u || f == 10u || f == 15u || f == 24u || f == 30u) return 1u;
|
||||
if (f == 26u || f == 32u || f == 34u || f == 37u) return 3u;
|
||||
return 2u;
|
||||
}
|
||||
)" GLSL_BPP_LOG2 R"(
|
||||
|
||||
// px() at 1.5x (a den of 2): the host pixels the guest pixel covers, each
|
||||
// weighted by how much of it lies inside (gpu/render_scale.h's box filter:
|
||||
@@ -5263,6 +5474,9 @@ void main()
|
||||
depth = float(v >> 8) / 16777215.0f;
|
||||
}
|
||||
ca.clearValue.depthStencil = { std::clamp(depth, 0.0f, 1.0f), v & 0xFF };
|
||||
#ifdef _WIN32
|
||||
s_lastDepthClear[rt->base] = ca.clearValue.depthStencil; // (see s_lastDepthClear)
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -6263,13 +6477,7 @@ uint tiled2D(uint x, uint y, uint pitch, uint bppLog2)
|
||||
(((oi >> 5) & 7u) << 8) | ((oi >> 8) << 12);
|
||||
}
|
||||
|
||||
uint swapWord(uint v, uint e)
|
||||
{
|
||||
if (e == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (e == 2u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (e == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
)" GLSL_SWAP_WORD R"(
|
||||
|
||||
void main()
|
||||
{
|
||||
@@ -6703,9 +6911,7 @@ void main()
|
||||
uint addr = p.base + (p.tiled != 0u ? tiled2D(g.x, g.y, p.pitch, 2u) : (g.y * p.pitch + g.x) * 4u);
|
||||
v = memLoad((addr & 0x1FFFFFFFu) >> 2);
|
||||
}
|
||||
if (p.endian == 1u) v = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
else if (p.endian == 2u) v = (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
else if (p.endian == 3u) v = (v >> 16) | (v << 16);
|
||||
)" GLSL_FRONT_SWAP R"(
|
||||
float c[6];
|
||||
if (p.format == 7u || p.format == 54u)
|
||||
{
|
||||
@@ -6754,6 +6960,93 @@ void main()
|
||||
width = height = 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// (See BlackCheck.) Per 8x8 group, then one atomic add per group that
|
||||
// saw any light.
|
||||
const char* kBlackProbeGlsl = R"(#version 460
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(set = 0, binding = 0, rgba8) readonly uniform image2D frontImage;
|
||||
layout(std430, set = 0, binding = 1) buffer Lit { uint lit[]; };
|
||||
layout(push_constant) uniform P { uint width, height, entry; } p;
|
||||
shared uint s_lit;
|
||||
void main()
|
||||
{
|
||||
if (gl_LocalInvocationIndex == 0u) s_lit = 0u;
|
||||
barrier();
|
||||
uvec2 g = gl_GlobalInvocationID.xy;
|
||||
if (g.x < p.width && g.y < p.height && any(greaterThan(imageLoad(frontImage, ivec2(g)).rgb, vec3(0.0))))
|
||||
atomicAdd(s_lit, 1u);
|
||||
barrier();
|
||||
if (gl_LocalInvocationIndex == 0u && s_lit != 0u)
|
||||
atomicAdd(lit[p.entry], s_lit);
|
||||
}
|
||||
)";
|
||||
|
||||
void CreateBlackCheck()
|
||||
{
|
||||
const char* v = std::getenv("NFSMW_BLACK_CHECK");
|
||||
if (!v || v[0] != '1')
|
||||
return;
|
||||
VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||||
bci.size = kBlackEntries * sizeof(uint32_t);
|
||||
bci.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
Check(vkCreateBuffer(s_dev, &bci, nullptr, &s_black.buffer), "vkCreateBuffer(black check)");
|
||||
VkMemoryRequirements req;
|
||||
vkGetBufferMemoryRequirements(s_dev, s_black.buffer, &req);
|
||||
VkMemoryAllocateInfo mai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||||
mai.allocationSize = req.size;
|
||||
mai.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
Check(vkAllocateMemory(s_dev, &mai, nullptr, &s_black.memory), "vkAllocateMemory(black check)");
|
||||
vkBindBufferMemory(s_dev, s_black.buffer, s_black.memory, 0);
|
||||
void* p;
|
||||
Check(vkMapMemory(s_dev, s_black.memory, 0, VK_WHOLE_SIZE, 0, &p), "vkMapMemory(black check)");
|
||||
s_black.host = static_cast<const volatile uint32_t*>(p);
|
||||
VkDescriptorSetLayoutBinding b[2] = {
|
||||
{ 0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
|
||||
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
|
||||
};
|
||||
CreateComputePipeline(kBlackProbeGlsl, b, 2, 3 * sizeof(uint32_t), s_black.setLayout, s_black.layout, s_black.pipeline);
|
||||
s_black.start = std::chrono::steady_clock::now();
|
||||
s_black.on = true;
|
||||
fprintf(stderr, "[black] NFSMW_BLACK_CHECK: every present's front image is checked; an all-zero one is logged as [black]\n");
|
||||
}
|
||||
|
||||
// After the front pass wrote `front` (and a full barrier), before the
|
||||
// present's submit: count its lit texels into the next entry.
|
||||
void RecordBlackProbe(const Image& front, uint32_t width, uint32_t height)
|
||||
{
|
||||
uint32_t entry = s_black.next++ % kBlackEntries;
|
||||
vkCmdFillBuffer(s_cmd, s_black.buffer, VkDeviceSize(entry) * sizeof(uint32_t), sizeof(uint32_t), 0);
|
||||
VkMemoryBarrier mb{ VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT };
|
||||
vkCmdPipelineBarrier(s_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
VkDescriptorImageInfo ii{ VK_NULL_HANDLE, front.view, VK_IMAGE_LAYOUT_GENERAL };
|
||||
VkDescriptorBufferInfo bi{ s_black.buffer, 0, VK_WHOLE_SIZE };
|
||||
VkWriteDescriptorSet w[2] = {
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &ii, nullptr, nullptr },
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 1, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &bi, nullptr },
|
||||
};
|
||||
uint32_t pc[3] = { width, height, entry };
|
||||
s_stateEpoch++;
|
||||
vkCmdBindPipeline(s_cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s_black.pipeline);
|
||||
s_pushDescriptorSet(s_cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s_black.layout, 0, 2, w);
|
||||
vkCmdPushConstants(s_cmd, s_black.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), pc);
|
||||
vkCmdDispatch(s_cmd, (width + 7) / 8, (height + 7) / 8, 1);
|
||||
mb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
|
||||
mb.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(s_cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
Submission& sub = s_submissions[s_slot];
|
||||
sub.blackEntry = int32_t(entry);
|
||||
sub.blackFrame = s_frame;
|
||||
sub.blackSec = std::chrono::duration<double>(std::chrono::steady_clock::now() - s_black.start).count();
|
||||
sub.blackDraws = uint32_t(s_stats.draws - s_black.drawsAt);
|
||||
sub.blackWaiting = s_pipelineSkips; // (this frame's: OnSwap, which resets it, comes after the present)
|
||||
sub.blackResolves = uint32_t(s_stats.resolves - s_black.resolvesAt);
|
||||
s_black.drawsAt = s_stats.draws;
|
||||
s_black.resolvesAt = s_stats.resolves;
|
||||
}
|
||||
#endif
|
||||
|
||||
void CreateUntilePipeline()
|
||||
{
|
||||
VkDescriptorSetLayoutBinding b[2] = {
|
||||
@@ -6982,9 +7275,16 @@ void main()
|
||||
return false;
|
||||
}
|
||||
VkMemoryHostPointerPropertiesEXT hpp{ VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT };
|
||||
if (s_vk->getMemoryHostPointerProperties(s_dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT,
|
||||
s_sharedHost, &hpp) != VK_SUCCESS)
|
||||
if (VkResult r = s_vk->getMemoryHostPointerProperties(s_dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT,
|
||||
s_sharedHost, &hpp); r != VK_SUCCESS)
|
||||
{
|
||||
// (A driver can refuse the pointer itself: on Windows guest
|
||||
// memory is a view of a file mapping, which only Intel's
|
||||
// driver is known to import. Logged, so a bug report tells
|
||||
// this from a refused allocation.)
|
||||
fprintf(stderr, "[renderer] importing guest memory failed: the driver doesn't take its host pointer (%d)\n", int(r));
|
||||
return false;
|
||||
}
|
||||
|
||||
VkExternalMemoryBufferCreateInfo embci{ VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO };
|
||||
embci.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT;
|
||||
@@ -7094,6 +7394,37 @@ void main()
|
||||
return failed.empty() ? std::string() : " after " + failed + " failed";
|
||||
}
|
||||
|
||||
// The driver refused every place the plan had for guest memory or the
|
||||
// upload ring (the line before this says which). Elsewhere abort(),
|
||||
// as before. Windows: the game ends with a message, not abort(),
|
||||
// whose SIGABRT handler writes a crash report (and Windows Error
|
||||
// Reporting may add a dump) for what is the player's card or driver,
|
||||
// not a bug in the game; the log has every place tried and the
|
||||
// device's memory (the [renderer] memory heaps line). The tester
|
||||
// build (no console) shows a box, as for a window that can't open
|
||||
// (main.cpp); the watchdog stands down while it waits, so no hang
|
||||
// report either. Called on the command processor's thread.
|
||||
[[noreturn]] void EndWithoutMemory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
report::g_endingWithMessage.store(true);
|
||||
fprintf(stderr, "[renderer] the graphics driver gave the game none of the memory it needs: the game ends here\n");
|
||||
report::FlushLog();
|
||||
#ifdef SB_WINDOWS_GUI
|
||||
int answer = MessageBoxA(nullptr,
|
||||
"SpeedBreaker couldn't set up its memory on your graphics card: the graphics driver refused every place the "
|
||||
"game tried (it needs 512 MB for the game's memory and 192 MB for drawing).\n\nUpdate your graphics driver and "
|
||||
"try again. If it still happens, please send the game's log; it is in %LOCALAPPDATA%\\SpeedBreaker\\logs.",
|
||||
"SpeedBreaker", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
|
||||
fprintf(stderr, "[renderer] message box closed (%d)\n", answer);
|
||||
#endif
|
||||
report::FlushLog();
|
||||
std::_Exit(1);
|
||||
#else
|
||||
abort();
|
||||
#endif
|
||||
}
|
||||
|
||||
void CreateSharedMemory()
|
||||
{
|
||||
s_sharedHost = static_cast<uint8_t*>(g_memory.Translate(vmem::Physical().Base()));
|
||||
@@ -7154,7 +7485,7 @@ void main()
|
||||
if (!placed)
|
||||
{
|
||||
fprintf(stderr, "[renderer] guest memory: no place could hold it (%s failed; %s)\n", failed.c_str(), plan.why.c_str());
|
||||
abort();
|
||||
EndWithoutMemory();
|
||||
}
|
||||
if (placed->place == memplan::Place::kImport)
|
||||
{
|
||||
@@ -7176,6 +7507,19 @@ void main()
|
||||
VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||||
bci.size = kRingSize;
|
||||
bci.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
#ifdef _WIN32
|
||||
// Texture uploads copy the untiled texels from the ring into
|
||||
// their images (vkCmdCopyBufferToImage), which needs TRANSFER_SRC:
|
||||
// without it the validation layer reports
|
||||
// VUID-vkCmdCopyBufferToImage-srcBuffer-00174 on every upload.
|
||||
// NFSMW_CHECK_FRONT(_SEC) copies the front image back into the
|
||||
// ring (vkCmdCopyImageToBuffer), which needs TRANSFER_DST
|
||||
// (VUID-vkCmdCopyImageToBuffer-dstBuffer-00191).
|
||||
// The drivers met so far copy anyway; a new driver needn't.
|
||||
// Windows only for now, so the other platforms stay as they are;
|
||||
// the spec wants it everywhere.
|
||||
bci.usage |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
#endif
|
||||
Check(vkCreateBuffer(s_dev, &bci, nullptr, &s_ring), "vkCreateBuffer(ring)");
|
||||
VkMemoryRequirements req;
|
||||
vkGetBufferMemoryRequirements(s_dev, s_ring, &req);
|
||||
@@ -7222,7 +7566,7 @@ void main()
|
||||
if (!placed)
|
||||
{
|
||||
fprintf(stderr, "[renderer] upload ring: no place could hold it (%s failed; %s)\n", failed.c_str(), plan.why.c_str());
|
||||
abort();
|
||||
EndWithoutMemory();
|
||||
}
|
||||
// The choice and why, in one line.
|
||||
fprintf(stderr, "[renderer] memory: guest memory %s; upload ring in %s, memory type %u (%s)%s\n", s_guestChoice.c_str(),
|
||||
@@ -7266,6 +7610,30 @@ void main()
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, s_set0, 2, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &ring, nullptr },
|
||||
};
|
||||
vkUpdateDescriptorSets(s_dev, AddSharedParts(w, 3, s_set0), w, 0, nullptr);
|
||||
#ifdef _WIN32
|
||||
// (See s_slotSet0.) The same bindings, the constants at each slot.
|
||||
VkDescriptorPoolSize slotSizes[2] = { { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, (s_splitMemory ? 5u : 2u) * kSlots },
|
||||
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, kSlots } };
|
||||
dpci.maxSets = kSlots;
|
||||
dpci.pPoolSizes = slotSizes;
|
||||
Check(vkCreateDescriptorPool(s_dev, &dpci, nullptr, &s_slotDescriptorPool), "slot descriptor pool");
|
||||
for (uint32_t k = 0; k < kSlots; k++)
|
||||
{
|
||||
dsai.descriptorPool = s_slotDescriptorPool;
|
||||
Check(vkAllocateDescriptorSets(s_dev, &dsai, &s_slotSet0[k]), "slot set 0");
|
||||
VkDescriptorBufferInfo slotConstants{ s_ring, VkDeviceSize(k) * kSlotSize, kConstantsSize };
|
||||
VkWriteDescriptorSet sw[6] = {
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, s_slotSet0[k], 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &s_sharedInfo[0], nullptr },
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, s_slotSet0[k], 1, 0, 1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, nullptr, &slotConstants, nullptr },
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, s_slotSet0[k], 2, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &ring, nullptr },
|
||||
};
|
||||
vkUpdateDescriptorSets(s_dev, AddSharedParts(sw, 3, s_slotSet0[k]), sw, 0, nullptr);
|
||||
}
|
||||
const char* slotSets = std::getenv("NFSMW_SLOT_SETS");
|
||||
s_slotSets = !slotSets || slotSets[0] != '0';
|
||||
fprintf(stderr, "[renderer] set 0: %s\n", s_slotSets ? "one per ring slot (dynamic offsets under 16 MB)"
|
||||
: "one for the whole ring (NFSMW_SLOT_SETS=0: dynamic offsets up to 192 MB)");
|
||||
#endif
|
||||
}
|
||||
|
||||
void CreateSamplerAndDummies()
|
||||
@@ -7543,6 +7911,34 @@ void main()
|
||||
last = c;
|
||||
lastStores = stores;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Fewer and lighter passes on an Intel integrated GPU (see
|
||||
// kFewerPasses): the values the initializers above give with both on,
|
||||
// for each variable not set, and what is derived from them; but the
|
||||
// pass barrier stays at level 0 there (see s_passBarrierLevel).
|
||||
void PassDefaultsForDevice()
|
||||
{
|
||||
if (!s_vk->integratedGpuDefaults)
|
||||
return;
|
||||
auto unset = [](const char* name) { return !std::getenv(name); };
|
||||
if (unset("NFSMW_PASS_MERGE"))
|
||||
s_passMerge = 3;
|
||||
if (unset("NFSMW_CLEAR_FLUSH_RECT"))
|
||||
s_clearFlushRect = true;
|
||||
if (unset("NFSMW_DIRECT_CUBE"))
|
||||
s_directCube = true;
|
||||
if (unset("NFSMW_FUSE_PARTIAL"))
|
||||
s_fusePartial = true;
|
||||
if (unset("NFSMW_UNTILED_AREA"))
|
||||
s_untiledArea = 1;
|
||||
s_resolvePush = s_fusePartial || s_directCube ? offsetof(ResolveConstants, hostWrite) : offsetof(ResolveConstants, texX);
|
||||
fprintf(stderr, "[renderer] passes: merge %u, clear flush rect %s, direct cubes %s, fused partial resolves %s, "
|
||||
"untiled area %s, barrier level %u (an Intel integrated GPU; NFSMW_* variables override)\n",
|
||||
s_passMerge, s_clearFlushRect ? "on" : "off", s_directCube ? "on" : "off", s_fusePartial ? "on" : "off",
|
||||
s_untiledArea ? "on" : "off", s_passBarrierLevel);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Initialize(const uint32_t* regs)
|
||||
@@ -7552,6 +7948,9 @@ void main()
|
||||
return false;
|
||||
s_dev = s_vk->device;
|
||||
s_regs = regs;
|
||||
#ifdef _WIN32
|
||||
PassDefaultsForDevice(); // before anything reads them
|
||||
#endif
|
||||
{
|
||||
VkPhysicalDeviceProperties props;
|
||||
vkGetPhysicalDeviceProperties(s_vk->physical, &props);
|
||||
@@ -7679,6 +8078,9 @@ void main()
|
||||
CreateComputePipeline(kFrontGlsl, bindings.data(), uint32_t(bindings.size()), 14 * sizeof(uint32_t), s_frontSetLayout,
|
||||
s_frontLayout, s_frontPipeline);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
CreateBlackCheck();
|
||||
#endif
|
||||
Flush();
|
||||
fprintf(stderr, "[renderer] ready: guest physical memory %s (%llu MB)\n",
|
||||
!s_shadowMode ? "imported" : s_shadowInVram ? "copied for the GPU, in VRAM" : "copied for the GPU, in host memory",
|
||||
@@ -9668,12 +10070,25 @@ void main()
|
||||
memcpy(bound.bias, bias, sizeof(bias));
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The constants' slot's set and their offset in it (see s_slotSet0).
|
||||
// `bound` keeps the whole ring offset, which names both, so a skip
|
||||
// never leaves another slot's set bound.
|
||||
VkDescriptorSet set0 = s_slotSets ? s_slotSet0[constantsOffset / kSlotSize] : s_set0;
|
||||
uint32_t dynamicOffset = uint32_t(s_slotSets ? constantsOffset % kSlotSize : constantsOffset);
|
||||
if (fresh || bound.setOffset != uint32_t(constantsOffset))
|
||||
{
|
||||
vkCmdBindDescriptorSets(s_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout.pipeline, 0, 1, &set0, 1, &dynamicOffset);
|
||||
bound.setOffset = uint32_t(constantsOffset);
|
||||
}
|
||||
#else
|
||||
uint32_t dynamicOffset = uint32_t(constantsOffset);
|
||||
if (fresh || bound.setOffset != dynamicOffset)
|
||||
{
|
||||
vkCmdBindDescriptorSets(s_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout.pipeline, 0, 1, &s_set0, 1, &dynamicOffset);
|
||||
bound.setOffset = dynamicOffset;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkWriteDescriptorSet writes[96];
|
||||
uint32_t writeCount = 0;
|
||||
@@ -10112,6 +10527,10 @@ void main()
|
||||
vkCmdDispatch(s_cmd, (outW + 7) / 8, (outH + 7) / 8, 1);
|
||||
GuardGpu(pc.base, frontBytes, false, "front buffer");
|
||||
FullBarrier();
|
||||
#ifdef _WIN32
|
||||
if (s_black.on)
|
||||
RecordBlackProbe(front, outW, outH);
|
||||
#endif
|
||||
SubmitRecorded(7); // the presenter's blit comes later in queue order
|
||||
video::SubmitFrontImage(front.image, outW, outH, width, height, between, shift, target);
|
||||
// Back at 1x (a live change): the host-resolution image is no longer
|
||||
|
||||
@@ -14,6 +14,50 @@
|
||||
#include <format>
|
||||
#include <set>
|
||||
|
||||
// gpuSwap(v, endian): a guest word in the given Xenos endianness (0 none,
|
||||
// 1 8in16, 2 8in32, 3 16in32) as a host uint. On Windows as selects: Intel's
|
||||
// Windows Vulkan driver (UHD 620, 101.2135) compiled the early-return chain
|
||||
// below wrongly in vertex shaders, returning the swapped word OR'd with the
|
||||
// unswapped one for 8in32 (1.0f read back as 1.0039f, 0x3F80803F). Vertex
|
||||
// positions and colours came out wrong: the front end's layers at z = 1 lay
|
||||
// beyond the far plane and were clipped, so the title screen and every menu
|
||||
// were black while movies played. The same function (for every endian value,
|
||||
// 4 and up left as they are, as the chain does); the other platforms keep
|
||||
// the chain for now (the same SPIR-V and caches as before).
|
||||
//
|
||||
// cubeDirection(c) below, a run of early returns on the cube face, is written
|
||||
// as selects on Windows for the same reason, though it hasn't shown yet.
|
||||
#ifdef _WIN32
|
||||
#define GLSL_GPU_SWAP R"(uint gpuSwap(uint v, uint endian)
|
||||
{
|
||||
uint s8 = ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
v = (endian == 1u || endian == 2u) ? s8 : v;
|
||||
return (endian == 2u || endian == 3u) ? (v >> 16) | (v << 16) : v;
|
||||
}
|
||||
)"
|
||||
#define GLSL_CUBE_FACE_DIRECTION R"( vec3 d = vec3(-sc * 2.0, -tc * 2.0, -1.0);
|
||||
d = face == 4 ? vec3(sc * 2.0, -tc * 2.0, 1.0) : d;
|
||||
d = face == 3 ? vec3(sc * 2.0, -1.0, -tc * 2.0) : d;
|
||||
d = face == 2 ? vec3(sc * 2.0, 1.0, tc * 2.0) : d;
|
||||
d = face == 1 ? vec3(-1.0, -tc * 2.0, sc * 2.0) : d;
|
||||
return face == 0 ? vec3(1.0, -tc * 2.0, -sc * 2.0) : d;)"
|
||||
#else
|
||||
#define GLSL_CUBE_FACE_DIRECTION R"( if (face == 0) return vec3(1.0, -tc * 2.0, -sc * 2.0);
|
||||
if (face == 1) return vec3(-1.0, -tc * 2.0, sc * 2.0);
|
||||
if (face == 2) return vec3(sc * 2.0, 1.0, tc * 2.0);
|
||||
if (face == 3) return vec3(sc * 2.0, -1.0, -tc * 2.0);
|
||||
if (face == 4) return vec3(sc * 2.0, -tc * 2.0, 1.0);
|
||||
return vec3(-sc * 2.0, -tc * 2.0, -1.0);)"
|
||||
#define GLSL_GPU_SWAP R"(uint gpuSwap(uint v, uint endian)
|
||||
{
|
||||
if (endian == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (endian == 2u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (endian == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
)"
|
||||
#endif
|
||||
|
||||
namespace gpu
|
||||
{
|
||||
using namespace xe::gpu;
|
||||
@@ -75,14 +119,7 @@ float fetchLodBias(uint slot)
|
||||
return float(bitfieldExtract(int(fetchDword(slot, 4u)), 12, 10)) / 32.0;
|
||||
}
|
||||
|
||||
uint gpuSwap(uint v, uint endian)
|
||||
{
|
||||
if (endian == 1u) return ((v << 8) & 0xFF00FF00u) | ((v >> 8) & 0x00FF00FFu);
|
||||
if (endian == 2u) return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
if (endian == 3u) return (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
|
||||
)" GLSL_GPU_SWAP R"(
|
||||
// Guest memory words are big-endian; the buffer holds them as host uints.
|
||||
uint memWord(uint byteAddress)
|
||||
{
|
||||
@@ -1020,12 +1057,7 @@ vec3 cubeDirection(vec3 c)
|
||||
// degrees). Back to a direction, inverting cubeop().
|
||||
float sc = c.x - 1.5, tc = c.y - 1.5;
|
||||
int face = int(c.z + 0.5);
|
||||
if (face == 0) return vec3(1.0, -tc * 2.0, -sc * 2.0);
|
||||
if (face == 1) return vec3(-1.0, -tc * 2.0, sc * 2.0);
|
||||
if (face == 2) return vec3(sc * 2.0, 1.0, tc * 2.0);
|
||||
if (face == 3) return vec3(sc * 2.0, -1.0, -tc * 2.0);
|
||||
if (face == 4) return vec3(sc * 2.0, -tc * 2.0, 1.0);
|
||||
return vec3(-sc * 2.0, -tc * 2.0, -1.0);
|
||||
)" GLSL_CUBE_FACE_DIRECTION R"(
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ static_assert(settings::keynames::Code("a") == SDL_SCANCODE_A && settings::keyna
|
||||
|
||||
namespace
|
||||
{
|
||||
#ifndef ERROR_SUCCESS // (<windows.h> has these, with the same values)
|
||||
constexpr uint32_t ERROR_SUCCESS = 0;
|
||||
constexpr uint32_t ERROR_DEVICE_NOT_CONNECTED = 0x48F;
|
||||
#endif
|
||||
|
||||
struct Pad
|
||||
{
|
||||
@@ -1398,7 +1400,9 @@ namespace
|
||||
};
|
||||
static_assert(sizeof(Keystroke) == 8);
|
||||
|
||||
#ifndef ERROR_EMPTY
|
||||
constexpr uint32_t ERROR_EMPTY = 0x10D2;
|
||||
#endif
|
||||
|
||||
uint32_t NextKeystroke(Keystroke* out)
|
||||
{
|
||||
|
||||
@@ -29,14 +29,29 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
|
||||
namespace install
|
||||
{
|
||||
namespace
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
#ifndef _WIN32
|
||||
static_assert(sizeof(off_t) == 8, "large file offsets are required");
|
||||
#endif
|
||||
// Opened for one front-to-back pass (Windows says so at the open;
|
||||
// elsewhere AdviseSequential does).
|
||||
#ifdef _WIN32
|
||||
constexpr int kReadOnce = O_RDONLY | O_CLOEXEC | _O_SEQUENTIAL;
|
||||
#else
|
||||
constexpr int kReadOnce = O_RDONLY | O_CLOEXEC;
|
||||
#endif
|
||||
|
||||
constexpr uint64_t kSectorSize = 2048;
|
||||
// Where the game partition starts: plain XISO (0), older rips
|
||||
@@ -105,7 +120,7 @@ namespace install
|
||||
size_t done = 0;
|
||||
while (done < size)
|
||||
{
|
||||
ssize_t n = pread(fd, p + done, std::min<size_t>(size - done, 1u << 30), off_t(offset + done));
|
||||
int64_t n = platform::ReadAt(fd, p + done, std::min<size_t>(size - done, 1u << 30), offset + done);
|
||||
if (n < 0)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
@@ -385,13 +400,13 @@ namespace install
|
||||
Result OpenImage(const fs::path& path, std::unique_ptr<DiscSource>& source, const OpenOptions& options)
|
||||
{
|
||||
auto image = std::make_shared<ImageFile>();
|
||||
image->fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
image->fd = platform::OpenFile(path, kReadOnce);
|
||||
if (image->fd < 0)
|
||||
return Fail(Error::NotFound, std::format("Cannot open {}: {}.", path.string(), strerror(errno)), path.string());
|
||||
struct stat st;
|
||||
if (fstat(image->fd, &st) != 0)
|
||||
platform::FileInfo st;
|
||||
if (platform::InfoOf(image->fd, st) != 0)
|
||||
return Fail(Error::ReadError, std::format("Cannot read {}: {}.", path.string(), strerror(errno)), path.string());
|
||||
image->size = uint64_t(st.st_size);
|
||||
image->size = st.size;
|
||||
AdviseSequential(image->fd);
|
||||
|
||||
// Try each partition offset with the magic, first to last, and keep
|
||||
@@ -500,7 +515,7 @@ namespace install
|
||||
std::unique_ptr<FileReader> Open(const DiscFile& file, std::string& error) const override
|
||||
{
|
||||
fs::path host = path / fs::path(file.path);
|
||||
int fd = open(host.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
int fd = platform::OpenFile(host, kReadOnce);
|
||||
if (fd < 0)
|
||||
{
|
||||
error = strerror(errno);
|
||||
@@ -570,8 +585,8 @@ namespace install
|
||||
std::set<std::pair<uint64_t, uint64_t>> entered;
|
||||
auto firstEntry = [&](const fs::path& dir)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(dir.c_str(), &st) == 0 && entered.insert({ uint64_t(st.st_dev), uint64_t(st.st_ino) }).second;
|
||||
platform::FileInfo st;
|
||||
return platform::InfoOf(dir, st) == 0 && entered.insert({ st.device, st.inode }).second;
|
||||
};
|
||||
firstEntry(root);
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace install
|
||||
{
|
||||
@@ -43,22 +47,40 @@ namespace install
|
||||
InterruptGuard()
|
||||
{
|
||||
s_cancel.store(false);
|
||||
#ifdef _WIN32
|
||||
// The C runtime's SIGINT is Ctrl-C (and Ctrl-Break), through
|
||||
// a console control handler, and it resets itself as
|
||||
// SA_RESETHAND does.
|
||||
previousInt = signal(SIGINT, OnInterrupt);
|
||||
previousTerm = signal(SIGTERM, OnInterrupt);
|
||||
#else
|
||||
struct sigaction sa{};
|
||||
sa.sa_handler = OnInterrupt;
|
||||
sa.sa_flags = SA_RESETHAND;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT, &sa, &previousInt);
|
||||
sigaction(SIGTERM, &sa, &previousTerm);
|
||||
#endif
|
||||
}
|
||||
|
||||
~InterruptGuard()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
signal(SIGINT, previousInt);
|
||||
signal(SIGTERM, previousTerm);
|
||||
#else
|
||||
sigaction(SIGINT, &previousInt, nullptr);
|
||||
sigaction(SIGTERM, &previousTerm, nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
void (*previousInt)(int) = SIG_DFL;
|
||||
void (*previousTerm)(int) = SIG_DFL;
|
||||
#else
|
||||
struct sigaction previousInt{}, previousTerm{};
|
||||
#endif
|
||||
};
|
||||
|
||||
// One rewritten status line on a terminal; a line per file otherwise
|
||||
@@ -133,7 +155,7 @@ namespace install
|
||||
Result r = OpenDiscSource(path, source);
|
||||
if (!r.Ok())
|
||||
return r;
|
||||
printf("Source: %s (%s, %zu files)\n", source->Path().c_str(),
|
||||
printf("Source: %s (%s, %zu files)\n", source->Path().string().c_str(),
|
||||
source->Kind() == SourceKind::Image ? "disc image" : "folder", source->Files().size());
|
||||
return {};
|
||||
}
|
||||
@@ -205,7 +227,7 @@ namespace install
|
||||
std::vector<DiscImageCandidate> images = FindDiscImages(where, 1ull << 30, &s_cancel);
|
||||
for (const DiscImageCandidate& image : images)
|
||||
{
|
||||
printf(" %-6s %s (%s)\n", image.status.Ok() ? "ready" : "no", image.path.c_str(),
|
||||
printf(" %-6s %s (%s)\n", image.status.Ok() ? "ready" : "no", image.path.string().c_str(),
|
||||
FormatSize(image.size).c_str());
|
||||
if (!image.status.Ok())
|
||||
printf(" %s: %s\n", ErrorName(image.status.error), image.status.message.c_str());
|
||||
@@ -220,10 +242,10 @@ namespace install
|
||||
{
|
||||
const portable::Layout& layout = portable::Current();
|
||||
if (layout.Active())
|
||||
printf("Portable mode (%s in %s): data in %s, caches in %s\n", portable::kMarker, layout.root.c_str(),
|
||||
GetUserPath().c_str(), GetCachePath().c_str());
|
||||
printf("Portable mode (%s in %s): data in %s, caches in %s\n", portable::kMarker, layout.root.string().c_str(),
|
||||
GetUserPath().string().c_str(), GetCachePath().string().c_str());
|
||||
else
|
||||
printf("Data in %s, caches in %s\n", GetUserPath().c_str(), GetCachePath().c_str());
|
||||
printf("Data in %s, caches in %s\n", GetUserPath().string().c_str(), GetCachePath().string().c_str());
|
||||
}
|
||||
|
||||
int RunWhere()
|
||||
@@ -246,7 +268,7 @@ namespace install
|
||||
}
|
||||
const char* origin = game->origin == InstallOrigin::Environment ? "NFSMW_GAME_DIR"
|
||||
: game->origin == InstallOrigin::Development ? "development checkout" : "installed";
|
||||
printf("%s (%s%s)\n", game->path.c_str(), origin, game->marker ? ", verified by the installer" : "");
|
||||
printf("%s (%s%s)\n", game->path.string().c_str(), origin, game->marker ? ", verified by the installer" : "");
|
||||
PrintDataFolders();
|
||||
return 0;
|
||||
}
|
||||
@@ -267,7 +289,7 @@ namespace install
|
||||
std::unique_ptr<DiscSource> source;
|
||||
if (Result r = OpenDiscSource(from, source); !r.Ok())
|
||||
return r;
|
||||
printf("Copying the game from %s (%s), checking every file:\n", from.c_str(),
|
||||
printf("Copying the game from %s (%s), checking every file:\n", from.string().c_str(),
|
||||
FormatSize(GameManifest().totalBytes).c_str());
|
||||
ProgressPrinter printer;
|
||||
Result r = Install(*source, to, std::ref(printer), &s_cancel);
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
#include <set>
|
||||
|
||||
#include <fcntl.h>
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
|
||||
namespace install
|
||||
{
|
||||
@@ -97,12 +103,7 @@ namespace install
|
||||
|
||||
void SyncDirectory(const fs::path& dir)
|
||||
{
|
||||
int fd = open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd >= 0)
|
||||
{
|
||||
fsync(fd);
|
||||
close(fd);
|
||||
}
|
||||
platform::SyncDirectory(dir);
|
||||
}
|
||||
|
||||
// Anything at `path`, a dangling link included.
|
||||
@@ -236,9 +237,9 @@ namespace install
|
||||
// The top of a mounted drive can't be renamed, and its siblings
|
||||
// are on the drive above it: the copy would fill the wrong disk
|
||||
// (on SteamOS, /run/media is in RAM) and then fail to move over.
|
||||
struct stat destStat, parentStat;
|
||||
if (stat(dest.c_str(), &destStat) == 0 && S_ISDIR(destStat.st_mode)
|
||||
&& stat(layout.parent.c_str(), &parentStat) == 0 && destStat.st_dev != parentStat.st_dev)
|
||||
platform::FileInfo destStat, parentStat;
|
||||
if (platform::InfoOf(dest, destStat) == 0 && destStat.directory
|
||||
&& platform::InfoOf(layout.parent, parentStat) == 0 && destStat.device != parentStat.device)
|
||||
return Fail(Error::DestinationNotWritable, std::format("{} is a whole drive. Choose a folder on it instead, "
|
||||
"such as {}.", dest.string(), (dest / kUserFolderName).string()), dest.string());
|
||||
return {};
|
||||
@@ -331,9 +332,11 @@ namespace install
|
||||
? std::format("Cannot install to {}.", dest.string())
|
||||
: std::format("Cannot install to {}: {} is a file, not a folder.", dest.string(), file.string()),
|
||||
dest.string());
|
||||
if (access(folder.c_str(), W_OK | X_OK) != 0)
|
||||
// What Install makes there: folders (the missing ones up to the
|
||||
// parent, then <dest>.partial); every file goes inside those.
|
||||
if (int error = platform::CheckWritableFolder(folder, platform::NewEntry::Folder); error != 0)
|
||||
return Fail(Error::DestinationNotWritable, std::format("Cannot write to {}: {}. Choose another folder.",
|
||||
folder.string(), strerror(errno)), folder.string());
|
||||
folder.string(), strerror(error)), folder.string());
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -360,10 +363,7 @@ namespace install
|
||||
if (std::optional<uint64_t> important = platform::ios::AvailableCapacityForImportantUsage(folder))
|
||||
return important;
|
||||
#endif
|
||||
struct statvfs vfs;
|
||||
if (statvfs(folder.c_str(), &vfs) != 0)
|
||||
return std::nullopt;
|
||||
return uint64_t(vfs.f_bavail) * vfs.f_frsize;
|
||||
return platform::FreeSpace(folder);
|
||||
}
|
||||
|
||||
// `reclaimable`: bytes of the installer's own leftover .partial,
|
||||
@@ -394,19 +394,12 @@ namespace install
|
||||
public:
|
||||
explicit FolderLock(const fs::path& dir)
|
||||
{
|
||||
fd = open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd >= 0 && flock(fd, LOCK_EX | LOCK_NB) != 0)
|
||||
{
|
||||
busy = errno == EWOULDBLOCK;
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
lock = platform::LockFolder(dir, busy);
|
||||
}
|
||||
|
||||
~FolderLock()
|
||||
{
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
platform::UnlockFolder(lock);
|
||||
}
|
||||
|
||||
FolderLock(const FolderLock&) = delete;
|
||||
@@ -415,7 +408,7 @@ namespace install
|
||||
bool Busy() const { return busy; }
|
||||
|
||||
private:
|
||||
int fd = -1;
|
||||
intptr_t lock = -1;
|
||||
bool busy = false;
|
||||
};
|
||||
|
||||
@@ -447,7 +440,7 @@ namespace install
|
||||
entries.push_back(*it);
|
||||
if (ec)
|
||||
{
|
||||
fprintf(stderr, "[install] cannot list %s: %s\n", from.c_str(), ec.message().c_str());
|
||||
fprintf(stderr, "[install] cannot list %s: %s\n", from.string().c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -470,15 +463,15 @@ namespace install
|
||||
fs::path target = to / name;
|
||||
if (Present(target))
|
||||
{
|
||||
fprintf(stderr, "[install] cannot move %s: the new install already has %s\n", entry.path().c_str(),
|
||||
target.c_str());
|
||||
fprintf(stderr, "[install] cannot move %s: the new install already has %s\n", entry.path().string().c_str(),
|
||||
target.string().c_str());
|
||||
complete = false;
|
||||
continue;
|
||||
}
|
||||
fs::rename(entry.path(), target, typeEc);
|
||||
if (typeEc)
|
||||
{
|
||||
fprintf(stderr, "[install] cannot move %s into the new install: %s\n", entry.path().c_str(),
|
||||
fprintf(stderr, "[install] cannot move %s into the new install: %s\n", entry.path().string().c_str(),
|
||||
typeEc.message().c_str());
|
||||
complete = false;
|
||||
}
|
||||
@@ -494,13 +487,13 @@ namespace install
|
||||
if (!CarryOver(layout.previous, layout.destination, "", manifest))
|
||||
{
|
||||
fprintf(stderr, "[install] kept the previous install at %s: it still holds files that could not be moved "
|
||||
"into %s\n", layout.previous.c_str(), layout.destination.c_str());
|
||||
"into %s\n", layout.previous.string().c_str(), layout.destination.string().c_str());
|
||||
return;
|
||||
}
|
||||
std::error_code ec;
|
||||
fs::remove_all(layout.previous, ec);
|
||||
if (ec)
|
||||
fprintf(stderr, "[install] could not delete the previous install at %s: %s\n", layout.previous.c_str(),
|
||||
fprintf(stderr, "[install] could not delete the previous install at %s: %s\n", layout.previous.string().c_str(),
|
||||
ec.message().c_str());
|
||||
}
|
||||
|
||||
@@ -516,7 +509,7 @@ namespace install
|
||||
// Nothing took its place: put it back (a rename replaces an
|
||||
// empty folder).
|
||||
fs::rename(layout.previous, layout.destination, ec);
|
||||
fprintf(stderr, "[install] restored %s from an interrupted reinstall%s%s\n", layout.destination.c_str(),
|
||||
fprintf(stderr, "[install] restored %s from an interrupted reinstall%s%s\n", layout.destination.string().c_str(),
|
||||
ec ? ": " : "", ec ? ec.message().c_str() : "");
|
||||
}
|
||||
else if (ReadInstallMarker(layout.destination))
|
||||
@@ -540,7 +533,7 @@ namespace install
|
||||
"bytes = {}\n",
|
||||
manifest.version, manifest.Xex().sha256, manifest.files.size(), manifest.totalBytes);
|
||||
fs::path path = dir / kInstallMarker;
|
||||
int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
int fd = platform::OpenFile(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
if (fd < 0)
|
||||
return errno;
|
||||
int error = 0;
|
||||
@@ -566,6 +559,11 @@ namespace install
|
||||
return Fail(Error::WriteError, std::format("Cannot move the previous install at {} aside: {}.",
|
||||
layout.destination.string(), ec.message()), layout.destination.string());
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// rename() replaces an empty folder; Windows' MoveFileExW doesn't.
|
||||
if (!replacing && fs::is_directory(layout.destination, ec))
|
||||
fs::remove(layout.destination, ec);
|
||||
#endif
|
||||
fs::rename(layout.partial, layout.destination, ec);
|
||||
if (ec)
|
||||
{
|
||||
@@ -754,7 +752,7 @@ namespace install
|
||||
fs::remove_all(layout.partial, removeEc);
|
||||
return result;
|
||||
};
|
||||
int tag = open((layout.partial / kPartialTag).c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
int tag = platform::OpenFile(layout.partial / kPartialTag, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
if (tag < 0)
|
||||
return failed(WriteFailure(errno, layout.partial / kPartialTag));
|
||||
close(tag);
|
||||
@@ -781,7 +779,7 @@ namespace install
|
||||
p.currentFile = mf->path;
|
||||
Report(progress, p);
|
||||
|
||||
int fd = open(target.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
int fd = platform::OpenFile(target, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
if (fd < 0)
|
||||
return failed(WriteFailure(errno, target));
|
||||
r = StreamFile(source, *file, *mf, fd, target, buffer, p, progress, cancel);
|
||||
@@ -817,7 +815,7 @@ namespace install
|
||||
platform::ios::ExcludeFromBackup(layout.destination);
|
||||
#endif
|
||||
fprintf(stderr, "[install] installed %zu files (%s) to %s\n", manifest.files.size(),
|
||||
FormatSize(manifest.totalBytes).c_str(), layout.destination.c_str());
|
||||
FormatSize(manifest.totalBytes).c_str(), layout.destination.string().c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#include <format>
|
||||
#include <set>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/folders.h>
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <pwd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
@@ -18,6 +22,9 @@
|
||||
#include <sys/mount.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
|
||||
namespace install
|
||||
{
|
||||
@@ -31,7 +38,7 @@ namespace install
|
||||
return !name.empty() && name[0] == '.';
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
#if !defined(__APPLE__) && !defined(_WIN32)
|
||||
std::string UserName()
|
||||
{
|
||||
if (const char* user = std::getenv("USER"); user && *user)
|
||||
@@ -92,6 +99,11 @@ namespace install
|
||||
std::string extension = file.extension().string();
|
||||
if (!PathEqualsIgnoreCase(extension, ".iso"))
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
// Only in the cloud (OneDrive): opening it would download 7 GB.
|
||||
if (platform::IsCloudOnly(file))
|
||||
return;
|
||||
#endif
|
||||
std::error_code ec;
|
||||
uint64_t size = fs::file_size(file, ec);
|
||||
if (ec || size < minSize)
|
||||
@@ -164,7 +176,7 @@ namespace install
|
||||
{
|
||||
std::optional<std::string> marker = ReadInstallMarker(user);
|
||||
fprintf(stderr, "[install] %s is not a current install (marker %s, manifest %.*s): reinstall\n",
|
||||
user.c_str(), marker ? marker->c_str() : "missing", int(manifest.version.size()), manifest.version.data());
|
||||
user.string().c_str(), marker ? marker->c_str() : "missing", int(manifest.version.size()), manifest.version.data());
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -176,6 +188,7 @@ namespace install
|
||||
return GetUserPath() / "game-location";
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// A folder that is the root of a mounted file system.
|
||||
bool IsMountPoint(const fs::path& dir)
|
||||
{
|
||||
@@ -184,11 +197,12 @@ namespace install
|
||||
return false;
|
||||
return S_ISDIR(self.st_mode) && self.st_dev != parent.st_dev;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> ReadInstallRecord(const std::filesystem::path& file, const portable::Layout& layout)
|
||||
{
|
||||
FILE* f = fopen(file.c_str(), "rb");
|
||||
FILE* f = platform::OpenStdioFile(file, "rb");
|
||||
if (!f)
|
||||
return std::nullopt;
|
||||
char buffer[4096];
|
||||
@@ -205,7 +219,7 @@ namespace install
|
||||
fs::create_directories(file.parent_path(), ec);
|
||||
fs::path temp = file;
|
||||
temp += ".tmp";
|
||||
FILE* f = fopen(temp.c_str(), "wb");
|
||||
FILE* f = platform::OpenStdioFile(temp, "wb");
|
||||
if (!f)
|
||||
return false;
|
||||
std::string line = text + "\n";
|
||||
@@ -252,6 +266,7 @@ namespace install
|
||||
return out;
|
||||
}
|
||||
out.push_back({ "Internal storage", DefaultInstallPath(), false });
|
||||
#ifndef _WIN32
|
||||
auto addVolume = [&](const fs::path& volume)
|
||||
{
|
||||
if (Hidden(volume) || !IsMountPoint(volume))
|
||||
@@ -269,8 +284,14 @@ namespace install
|
||||
std::string label = volume.filename().string();
|
||||
out.push_back({ label.empty() ? volume.string() : label, volume / kUserFolderName / "game", true });
|
||||
};
|
||||
#endif
|
||||
std::error_code ec;
|
||||
#ifdef __APPLE__
|
||||
#ifdef _WIN32
|
||||
// Every other local drive that takes files: <drive>:\SpeedBreaker\game.
|
||||
for (const platform::Drive& drive : platform::Drives())
|
||||
if (!drive.system && !drive.readOnly)
|
||||
out.push_back({ drive.label, drive.root / kUserFolderName / "game", drive.removable });
|
||||
#elif defined(__APPLE__)
|
||||
for (fs::directory_iterator it("/Volumes", ec), end; !ec && it != end; it.increment(ec))
|
||||
{
|
||||
std::error_code sameEc;
|
||||
@@ -301,6 +322,22 @@ namespace install
|
||||
if (fs::is_directory(dir, ec) && std::find(folders.begin(), folders.end(), dir) == folders.end())
|
||||
folders.push_back(dir);
|
||||
};
|
||||
#ifdef _WIN32
|
||||
// The profile folder (never HOME: Git Bash and MSYS2 set it), the
|
||||
// shell's Downloads, Desktop and Documents (wherever OneDrive or the
|
||||
// player moved them), Games, and the root of every other local drive.
|
||||
fs::path profile = platform::GetKnownFolder(platform::KnownFolder::Profile);
|
||||
if (!profile.empty())
|
||||
add(profile);
|
||||
for (platform::KnownFolder known : { platform::KnownFolder::Downloads, platform::KnownFolder::Desktop, platform::KnownFolder::Documents })
|
||||
if (fs::path dir = platform::GetKnownFolder(known); !dir.empty())
|
||||
add(dir);
|
||||
if (!profile.empty())
|
||||
add(profile / "Games");
|
||||
for (const platform::Drive& drive : platform::Drives())
|
||||
if (!drive.system)
|
||||
add(drive.root);
|
||||
#else
|
||||
if (const char* home = std::getenv("HOME"); home && *home)
|
||||
{
|
||||
fs::path h = home;
|
||||
@@ -308,7 +345,9 @@ namespace install
|
||||
for (const char* sub : { "Downloads", "Desktop", "Documents", "Games" })
|
||||
add(h / sub);
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#elif defined(__APPLE__)
|
||||
// /Volumes also lists the startup disk (a link to /): skip it, a scan of
|
||||
// the whole system disk is what the folders above avoid.
|
||||
std::error_code ec;
|
||||
@@ -347,8 +386,10 @@ namespace install
|
||||
std::vector<DiscImageCandidate> found;
|
||||
std::set<fs::path> seenFiles, listedDirs;
|
||||
fs::path library;
|
||||
#ifndef _WIN32
|
||||
if (const char* home = std::getenv("HOME"); home && *home)
|
||||
library = fs::path(home) / "Library";
|
||||
#endif
|
||||
auto cancelled = [&] { return cancel && cancel->load(std::memory_order_relaxed); };
|
||||
|
||||
// Lists `dir`'s .iso files (once per directory, however it is reached)
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -38,8 +42,10 @@ namespace
|
||||
constexpr uint32_t FILE_NON_DIRECTORY_FILE = 0x40;
|
||||
// Access.
|
||||
constexpr uint32_t GENERIC_WRITE_ = 0x40000000;
|
||||
#ifndef FILE_WRITE_DATA // (<windows.h> has these two, with the same values)
|
||||
constexpr uint32_t FILE_WRITE_DATA = 0x0002;
|
||||
constexpr uint32_t FILE_APPEND_DATA = 0x0004;
|
||||
#endif
|
||||
// Attributes.
|
||||
constexpr uint32_t ATTR_READONLY = 0x01;
|
||||
constexpr uint32_t ATTR_DIRECTORY = 0x10;
|
||||
@@ -56,17 +62,13 @@ namespace
|
||||
|
||||
std::optional<HostAttributes> Stat(const std::filesystem::path& path, bool readOnly)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) != 0)
|
||||
platform::FileInfo st;
|
||||
if (platform::InfoOf(path, st) != 0)
|
||||
return std::nullopt;
|
||||
HostAttributes a;
|
||||
bool dir = S_ISDIR(st.st_mode);
|
||||
a.size = dir ? 0 : uint64_t(st.st_size);
|
||||
#ifdef __APPLE__
|
||||
int64_t sec = st.st_mtimespec.tv_sec, nsec = st.st_mtimespec.tv_nsec;
|
||||
#else
|
||||
int64_t sec = st.st_mtim.tv_sec, nsec = st.st_mtim.tv_nsec;
|
||||
#endif
|
||||
bool dir = st.directory;
|
||||
a.size = dir ? 0 : st.size;
|
||||
int64_t sec = st.modifiedSeconds, nsec = st.modifiedNanoseconds;
|
||||
a.writeTime = uint64_t(sec * 10000000 + nsec / 100 + FILETIME_EPOCH_DIFFERENCE);
|
||||
a.attributes = dir ? ATTR_DIRECTORY : ATTR_NORMAL;
|
||||
if (readOnly)
|
||||
@@ -280,7 +282,7 @@ uint32_t NtCreateFile(be<uint32_t>* handleOut, uint32_t desiredAccess, XOBJECT_A
|
||||
flags |= O_TRUNC | O_RDWR;
|
||||
action = disposition == FILE_SUPERSEDE ? FILE_SUPERSEDED : FILE_OVERWRITTEN;
|
||||
}
|
||||
file->fd = open(resolved->host.c_str(), flags, 0644);
|
||||
file->fd = platform::OpenFile(resolved->host, flags, 0644);
|
||||
if (file->fd < 0)
|
||||
{
|
||||
DestroyKernelObject(file);
|
||||
@@ -289,7 +291,7 @@ uint32_t NtCreateFile(be<uint32_t>* handleOut, uint32_t desiredAccess, XOBJECT_A
|
||||
}
|
||||
|
||||
*handleOut = GetKernelHandle(file);
|
||||
fprintf(stderr, "[file] open \"%s\" -> %s%s\n", guestPath->c_str(), resolved->host.c_str(),
|
||||
fprintf(stderr, "[file] open \"%s\" -> %s%s\n", guestPath->c_str(), resolved->host.string().c_str(),
|
||||
action == FILE_CREATED ? " (created)" : "");
|
||||
Complete(iosb, STATUS_SUCCESS, action);
|
||||
return STATUS_SUCCESS;
|
||||
@@ -328,13 +330,13 @@ uint32_t NtReadFile(uint32_t fileHandle, uint32_t eventHandle, uint32_t apcRouti
|
||||
ssize_t n = 0;
|
||||
while (uint32_t(n) < length)
|
||||
{
|
||||
ssize_t got = pread(file->fd, dst + n, length - uint32_t(n), off_t(pos + uint64_t(n)));
|
||||
int64_t got = platform::ReadAt(file->fd, dst + n, length - uint32_t(n), pos + uint64_t(n));
|
||||
if (got < 0 && errno == EFAULT)
|
||||
{
|
||||
static thread_local std::vector<uint8_t> bounce;
|
||||
bounce.resize(length - uint32_t(n));
|
||||
do
|
||||
got = pread(file->fd, bounce.data(), bounce.size(), off_t(pos + uint64_t(n)));
|
||||
got = platform::ReadAt(file->fd, bounce.data(), bounce.size(), pos + uint64_t(n));
|
||||
while (got < 0 && errno == EINTR);
|
||||
if (got > 0)
|
||||
memcpy(dst + n, bounce.data(), size_t(got));
|
||||
@@ -381,7 +383,7 @@ uint32_t NtWriteFile(uint32_t fileHandle, uint32_t eventHandle, uint32_t apcRout
|
||||
|
||||
int64_t offset = byteOffset ? int64_t(*byteOffset) : -1;
|
||||
uint64_t pos = offset >= 0 ? uint64_t(offset) : file->position;
|
||||
ssize_t n = pwrite(file->fd, buffer, length, off_t(pos));
|
||||
int64_t n = platform::WriteAt(file->fd, buffer, length, pos);
|
||||
if (n < 0)
|
||||
{
|
||||
Complete(iosb, STATUS_UNSUCCESSFUL, 0);
|
||||
@@ -499,7 +501,7 @@ uint32_t NtSetInformationFile(uint32_t fileHandle, XIO_STATUS_BLOCK* iosb, uint8
|
||||
if (length < 8) return STATUS_INFO_LENGTH_MISMATCH;
|
||||
if (file->readOnly || file->fd < 0)
|
||||
status = STATUS_ACCESS_DENIED;
|
||||
else if (ftruncate(file->fd, off_t(uint64_t(*reinterpret_cast<be<uint64_t>*>(info)))) != 0)
|
||||
else if (platform::Truncate(file->fd, uint64_t(*reinterpret_cast<be<uint64_t>*>(info))) != 0)
|
||||
status = STATUS_UNSUCCESSFUL;
|
||||
written = 8;
|
||||
break;
|
||||
|
||||
@@ -17,7 +17,9 @@ uint32_t NtClose(uint32_t handle);
|
||||
|
||||
constexpr uint32_t CURRENT_PROCESS_PSEUDO = 0xFFFFFFFF;
|
||||
constexpr uint32_t CURRENT_THREAD_PSEUDO = 0xFFFFFFFE;
|
||||
#ifndef DUPLICATE_CLOSE_SOURCE // (<windows.h> has it, with the same value)
|
||||
constexpr uint32_t DUPLICATE_CLOSE_SOURCE = 0x1;
|
||||
#endif
|
||||
|
||||
uint32_t NtDuplicateObject(uint32_t sourceHandle, be<uint32_t>* targetHandle, uint32_t options)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
#include <stdafx.h>
|
||||
#include "function.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// <windows.h> declares Windows' own RtlCompareMemory: the guest's gets its
|
||||
// own name, or the hook below can't tell which one it means.
|
||||
#define RtlCompareMemory GuestRtlCompareMemory
|
||||
#endif
|
||||
|
||||
uint32_t RtlCompareMemory(const uint8_t* a, const uint8_t* b, uint32_t length)
|
||||
{
|
||||
uint32_t n = 0;
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
#include "xdm.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <ntstatus.h>
|
||||
// SpeedBreaker: Windows has these NT values only in <ntstatus.h>, which
|
||||
// clashes with <windows.h> (already in); elsewhere xdm.h defines them.
|
||||
#ifndef STATUS_FAIL_CHECK
|
||||
#define STATUS_FAIL_CHECK 0xC0000229
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
@@ -223,20 +227,10 @@ uint32_t RtlUnicodeToMultiByteN(char* MultiByteString, uint32_t MaxBytesInMultiB
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
// SpeedBreaker: a no-op on every platform (Unleashed set the host thread's
|
||||
// priority on Windows only); thread priorities are PLAN.md P4's.
|
||||
void KeSetBasePriorityThread(GuestThreadHandle* hThread, int priority)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (priority == 16)
|
||||
{
|
||||
priority = 15;
|
||||
}
|
||||
else if (priority == -16)
|
||||
{
|
||||
priority = -15;
|
||||
}
|
||||
|
||||
SetThreadPriority(hThread == GetKernelObject(CURRENT_THREAD_HANDLE) ? GetCurrentThread() : hThread->thread.native_handle(), priority);
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t ObReferenceObjectByHandle(uint32_t handle, uint32_t objectType, be<uint32_t>* object)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// GPL-3.0-or-later. Modified for SpeedBreaker.
|
||||
#include "../stdafx.h"
|
||||
#include "memory.h"
|
||||
#ifdef _WIN32
|
||||
#include <platform/memory.h>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <cerrno>
|
||||
@@ -97,16 +100,13 @@ bool Memory::MapPhysicalMirrors()
|
||||
Memory::Memory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
base = (uint8_t*)VirtualAlloc((void*)0x100000000ull, PPC_MEMORY_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
|
||||
if (base == nullptr)
|
||||
base = (uint8_t*)VirtualAlloc(nullptr, PPC_MEMORY_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
|
||||
// SpeedBreaker: the same layout through placeholders: the physical
|
||||
// windows alias one section, the rest is private (platform/memory.h).
|
||||
// A view must fill its placeholder, so the 0xE window is unshifted.
|
||||
base = platform::MapGuestSpace();
|
||||
if (base == nullptr)
|
||||
return;
|
||||
|
||||
DWORD oldProtect;
|
||||
VirtualProtect(base, 4096, PAGE_NOACCESS, &oldProtect);
|
||||
eWindowShift = 0;
|
||||
#else
|
||||
// NFSMW_TEST_RESERVE_FAIL=1|2: fail the reservation or the mirrors as an
|
||||
// iOS without room would (memory_status.h), to see the startup message
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
#include "../stdafx.h"
|
||||
#include "vmem.h"
|
||||
#ifdef _WIN32
|
||||
#include <platform/memory.h>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace vmem
|
||||
{
|
||||
bool RemapAnon(void* host, size_t bytes)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Windows: decommit and commit again (platform/memory.h). Its pages
|
||||
// are 4 KB, a guest page, so every range is whole host pages.
|
||||
return platform::ResetPrivatePages(host, bytes);
|
||||
#else
|
||||
return mmap(host, bytes, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0) != MAP_FAILED;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ResetHostRange(void* host, size_t bytes, size_t hostPage, RemapFn remap)
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace vmem
|
||||
// Remaps [host, host+bytes) to fresh zero-filled pages: whole host pages,
|
||||
// an address and length aligned to them. False when it failed.
|
||||
using RemapFn = bool (*)(void* host, size_t bytes);
|
||||
bool RemapAnon(void* host, size_t bytes); // mmap(MAP_FIXED | MAP_ANON | MAP_PRIVATE)
|
||||
bool RemapAnon(void* host, size_t bytes); // mmap(MAP_FIXED | MAP_ANON | MAP_PRIVATE); Windows: decommit + commit
|
||||
// Returns [host, host+bytes) to zero: the whole host pages (`hostPage`, a
|
||||
// power of two) inside it remapped (lazy and cheap even for hundreds of
|
||||
// megabytes; cleared in place if that fails), the partial head and tail
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
#include "memory.h"
|
||||
|
||||
#include <cpu/guest_time.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace writewatch
|
||||
{
|
||||
@@ -753,8 +757,12 @@ namespace writewatch
|
||||
if (s_logGuard)
|
||||
{
|
||||
static std::atomic<uint32_t> logged{ 0 };
|
||||
#ifdef _WIN32
|
||||
struct { const char* dli_sname = nullptr; } dl; // host names on Windows: DbgHelp (PLAN.md P3)
|
||||
#else
|
||||
Dl_info dl{};
|
||||
dladdr(t_faultPc, &dl);
|
||||
#endif
|
||||
if (std::chrono::steady_clock::now() >= s_logGuardFrom && logged++ < 400)
|
||||
fprintf(stderr, "[guard] stall %llu us (%s %08X): %s %08X+%X (submission %llu%s) in %s\n", (unsigned long long)us,
|
||||
t_faultWrite ? "write" : "read", t_faultPhysical, s_gpuTag[sub].load(), s_gpuRange[0][sub].load(),
|
||||
@@ -850,9 +858,16 @@ namespace writewatch
|
||||
s_lastWriter[i].store(0);
|
||||
s_lastWriterPc[i].store(0);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
HMODULE module = nullptr;
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&Initialize), &module))
|
||||
s_imageBase = reinterpret_cast<const uint8_t*>(module);
|
||||
#else
|
||||
Dl_info dl{};
|
||||
if (dladdr(reinterpret_cast<const void*>(&Initialize), &dl))
|
||||
s_imageBase = static_cast<const uint8_t*>(dl.dli_fbase);
|
||||
#endif
|
||||
}
|
||||
if (s_logGuard)
|
||||
{
|
||||
|
||||
+17
-35
@@ -235,42 +235,24 @@ uint32_t XamShowMessageBoxUI(uint32_t dwUserIndex, be<uint16_t>* wszTitle, be<ui
|
||||
#if _DEBUG
|
||||
assert("XamShowMessageBoxUI encountered!" && false);
|
||||
#elif _WIN32
|
||||
// This code is Win32-only as it'll most likely crash, misbehave or
|
||||
// cause corruption due to using a different type of memory than what
|
||||
// wchar_t is on Linux. Windows uses 2 bytes while Linux uses 4 bytes.
|
||||
std::vector<std::wstring> texts{};
|
||||
|
||||
texts.emplace_back(reinterpret_cast<wchar_t*>(wszTitle));
|
||||
texts.emplace_back(reinterpret_cast<wchar_t*>(wszText));
|
||||
|
||||
// SpeedBreaker: the 360 shows a message box (no UI for one yet): log it.
|
||||
// Its strings are big-endian UTF-16, which Windows' wchar_t holds after a
|
||||
// swap (elsewhere wchar_t is 4 bytes, so only Windows logs them).
|
||||
auto text = [](const be<uint16_t>* guest) {
|
||||
std::wstring wide;
|
||||
for (; guest && guest->get() != 0; guest++)
|
||||
wide.push_back(wchar_t(guest->get()));
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), int(wide.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string out(size_t(std::max(n, 0)), '\0');
|
||||
if (n > 0)
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), int(wide.size()), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
};
|
||||
std::string buttons;
|
||||
for (size_t i = 0; i < cButtons; i++)
|
||||
texts.emplace_back(reinterpret_cast<wchar_t*>(pwszButtons[i].get()));
|
||||
|
||||
for (auto& text : texts)
|
||||
{
|
||||
for (size_t i = 0; i < text.size(); i++)
|
||||
ByteSwapInplace(text[i]);
|
||||
}
|
||||
|
||||
wprintf(L"[XamShowMessageBoxUI] !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
|
||||
wprintf(L"[XamShowMessageBoxUI] If you are encountering this message and the game has ceased functioning,\n");
|
||||
wprintf(L"[XamShowMessageBoxUI] please report it at https://github.com/SpeedBreakerProject/speedbreaker/issues.\n");
|
||||
wprintf(L"[XamShowMessageBoxUI] !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
|
||||
wprintf(L"[XamShowMessageBoxUI] %ls\n", texts[0].c_str());
|
||||
wprintf(L"[XamShowMessageBoxUI] %ls\n", texts[1].c_str());
|
||||
wprintf(L"[XamShowMessageBoxUI] ");
|
||||
|
||||
for (size_t i = 0; i < cButtons; i++)
|
||||
{
|
||||
wprintf(L"%ls", texts[2 + i].c_str());
|
||||
|
||||
if (i != cButtons - 1)
|
||||
wprintf(L" | ");
|
||||
}
|
||||
|
||||
wprintf(L"\n");
|
||||
wprintf(L"[XamShowMessageBoxUI] Defaulted to button: %d\n", pResult->get());
|
||||
wprintf(L"[XamShowMessageBoxUI] !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
|
||||
buttons += (i ? " | " : "") + text(pwszButtons[i].get());
|
||||
fprintf(stderr, "[xam] the game showed a message box, answered with button %u: \"%s\": \"%s\" [%s]\n",
|
||||
uint32_t(pResult->get()), text(wszTitle).c_str(), text(wszText).c_str(), buttons.c_str());
|
||||
#endif
|
||||
|
||||
XamNotifyEnqueueEvent(9, 0);
|
||||
|
||||
+45
-9
@@ -43,13 +43,19 @@
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <map>
|
||||
#include <pthread.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
#include <unordered_map>
|
||||
|
||||
#include <platform/fault.h>
|
||||
#include <platform/startup.h>
|
||||
#include <platform/thread.h>
|
||||
#include <platform/timer.h>
|
||||
|
||||
#include <file.h>
|
||||
#include <image.h>
|
||||
#include <xex.h>
|
||||
@@ -212,6 +218,7 @@ namespace
|
||||
return exact;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
#if defined(__linux__) && defined(__aarch64__)
|
||||
// Whether a data abort was a write: ESR_EL1's ISS bit 6 (WnR), which the
|
||||
// kernel passes in an esr_context record among those after the
|
||||
@@ -271,10 +278,16 @@ namespace
|
||||
report::WriteCrashReport(sig, info, context);
|
||||
_exit(128 + sig);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Windows: no drive-error dialogs, UTF-8 in the console
|
||||
// (platform/startup.h), and 1 ms sleeps instead of the 15.6 ms tick
|
||||
// (platform/timer.h).
|
||||
platform::PrepareProcess();
|
||||
platform::RaiseTimerResolution();
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
// iOS starts an app in /, which it can't write. Relative paths (test
|
||||
// output in build/, a ./game/files development copy) mean the app's
|
||||
@@ -359,14 +372,20 @@ int main(int argc, char** argv)
|
||||
guesttime::SetResumeHook(dispatcher::WakeAfterResume);
|
||||
|
||||
report::PrepareCrashReports();
|
||||
#ifdef _WIN32
|
||||
// A vectored handler for the write watch, the crash report for the rest.
|
||||
platform::InstallFaultHandlers();
|
||||
#else
|
||||
struct sigaction sa{};
|
||||
sa.sa_sigaction = CrashHandler;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
for (int sig : { SIGSEGV, SIGBUS, SIGILL, SIGTRAP, SIGFPE, SIGABRT })
|
||||
sigaction(sig, &sa, nullptr);
|
||||
#endif
|
||||
// SIGTERM, SIGINT and SIGHUP quit the game as its window's close button
|
||||
// does (platform/quit_signals.h): before SDL starts, which leaves them to
|
||||
// us.
|
||||
// does (platform/quit_signals.h; Windows: SIGINT and SIGBREAK, Ctrl+C and
|
||||
// Ctrl+Break in a console build's console): before SDL starts, which
|
||||
// leaves them to us.
|
||||
quitsignals::Install();
|
||||
// From here on a return from main() is a clean end too (an unknown
|
||||
// option, a cancelled install, a XEX that can't be read): the session's
|
||||
@@ -423,6 +442,24 @@ int main(int argc, char** argv)
|
||||
fprintf(stderr, "[version] v%s (the last version isn't known: settings.toml didn't load)\n", report::Version());
|
||||
// The window must live on the main thread (macOS); the game gets its own.
|
||||
bool windowed = video::Initialize();
|
||||
#if defined(_WIN32) && defined(SB_WINDOWS_GUI)
|
||||
// The Windows-subsystem build (the download) has no console: without
|
||||
// a window there would be nothing to see at all, the game running on
|
||||
// unseen (the headless mode, meant for tests) or ending without a word.
|
||||
// Say so in a box instead; the log has why.
|
||||
if (const char* headless = std::getenv("NFSMW_HEADLESS"); !windowed && !(headless && headless[0] == '1'))
|
||||
{
|
||||
fprintf(stderr, "[runtime] no window: a message box says so, then the game ends\n");
|
||||
report::FlushLog();
|
||||
// Windows' own box: SDL's may need the video driver that just failed.
|
||||
int answer = MessageBoxA(nullptr,
|
||||
"SpeedBreaker couldn't start its graphics. It needs a graphics card with Vulkan 1.2 and an up-to-date "
|
||||
"driver: update your graphics driver and try again.\n\nThe game's log is in %LOCALAPPDATA%\\SpeedBreaker\\logs.",
|
||||
"SpeedBreaker", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
|
||||
fprintf(stderr, "[runtime] message box closed (%d)\n", answer);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
if (!settingsLoaded)
|
||||
ui::Toast(settings::LastError(), 12.0); // a mistake in settings.toml: it was kept as .bad
|
||||
if (portableError)
|
||||
@@ -444,6 +481,7 @@ int main(int argc, char** argv)
|
||||
report::NoticePreviousCrash(!lastVersion.empty() && version::Compare(lastVersion, report::Version()) >= 0);
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
// NFSMW_LIFELINE_PID (scripts/steamos/speedbreaker.sh): the launcher's PID. The
|
||||
// game runs in a container, not as the launcher's child, so Steam
|
||||
// stopping the launcher never reaches it: exit once the launcher is gone.
|
||||
@@ -460,6 +498,7 @@ int main(int argc, char** argv)
|
||||
report::FlushLog();
|
||||
std::_Exit(0);
|
||||
}).detach();
|
||||
#endif
|
||||
|
||||
std::filesystem::path xexPath;
|
||||
std::string gameOrigin; // for the system summary
|
||||
@@ -552,11 +591,8 @@ int main(int argc, char** argv)
|
||||
if (auto* stack = static_cast<const be<uint32_t>*>(getOptHeaderPtr(file.data(), XEX_HEADER_DEFAULT_STACK_SIZE)))
|
||||
params.stackSize = std::max<uint32_t>(*stack, DEFAULT_GUEST_STACK);
|
||||
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, HOST_STACK);
|
||||
pthread_t thread;
|
||||
pthread_create(&thread, &attr, GuestMain, ¶ms);
|
||||
platform::Thread thread{};
|
||||
platform::StartThread(&thread, HOST_STACK, GuestMain, ¶ms);
|
||||
report::StartWatchdog(); // hang reports (and the NFSMW_TEST_* switches)
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -582,7 +618,7 @@ int main(int argc, char** argv)
|
||||
report::FlushLog();
|
||||
std::_Exit(0);
|
||||
}
|
||||
pthread_join(thread, nullptr);
|
||||
platform::JoinThread(thread);
|
||||
report::FlushLog();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Faults on Windows (main.cpp installs POSIX's signal handlers itself): a
|
||||
// vectored exception handler, first in line, gives access violations to
|
||||
// the write watch (writewatch::HandleFault: a store to a watched texture
|
||||
// page, or an access to one the GPU still uses), which unprotects the page
|
||||
// so the access runs again. Anything else goes on to the usual handlers (a
|
||||
// C++ throw, GuestThreadExit included, passes untouched), and what nothing
|
||||
// handles writes the crash report (report::WriteCrashReport) and ends the
|
||||
// process. abort() (a failed assert) reports too, and never shows a dialog.
|
||||
#pragma once
|
||||
|
||||
namespace platform
|
||||
{
|
||||
#ifdef _WIN32
|
||||
void InstallFaultHandlers();
|
||||
// The crash report's filter installed again, in case something loaded
|
||||
// since (a driver, an overlay) put its own in its place: false if it had.
|
||||
bool KeepCrashFilter();
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Files as the runtime uses them, where Windows differs from POSIX. Files
|
||||
// are C runtime descriptors everywhere (read, write, close and fsync work on
|
||||
// them as they are), but on Windows they are opened through CreateFileW:
|
||||
// wide paths (std::filesystem::path::c_str() is wchar_t there), binary,
|
||||
// and shared for reading, writing and deleting as POSIX files are. Offsets
|
||||
// and sizes are 64-bit (MSVC's off_t and struct stat sizes are 32-bit, and
|
||||
// a disc image is 7.8 GB). Failures set errno as POSIX would (Windows'
|
||||
// errors mapped: ERROR_NOACCESS, a buffer the kernel can't write, is EFAULT
|
||||
// as on Linux), so strerror(errno) explains them on every platform.
|
||||
//
|
||||
// On macOS, iOS and Linux each call is the POSIX call the runtime made
|
||||
// before (posix/file_io.cpp), unchanged.
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// open(2): O_RDONLY/O_WRONLY/O_RDWR with O_CREAT, O_EXCL, O_TRUNC,
|
||||
// O_APPEND, O_CLOEXEC (and on Windows _O_SEQUENTIAL for a front-to-back
|
||||
// read). A descriptor, or -1 with errno.
|
||||
int OpenFile(const std::filesystem::path& path, int flags, int mode = 0644);
|
||||
|
||||
// fopen(3) of a path (Windows: _wfopen, so any file name works).
|
||||
std::FILE* OpenStdioFile(const std::filesystem::path& path, const char* mode);
|
||||
|
||||
// pread/pwrite at a 64-bit offset: the bytes moved (a read returns fewer
|
||||
// only at the end of the file, 0 there), or -1 with errno.
|
||||
int64_t ReadAt(int fd, void* buffer, size_t size, uint64_t offset);
|
||||
int64_t WriteAt(int fd, const void* buffer, size_t size, uint64_t offset);
|
||||
// ftruncate to a 64-bit size: 0, or -1 with errno.
|
||||
int Truncate(int fd, uint64_t size);
|
||||
|
||||
// fstat / stat (links followed), with 64-bit sizes. `device` and `inode`
|
||||
// name the file for loop detection (Windows: the volume serial number
|
||||
// and the file index).
|
||||
struct FileInfo
|
||||
{
|
||||
uint64_t size = 0;
|
||||
bool directory = false;
|
||||
bool regular = false;
|
||||
int64_t modifiedSeconds = 0; // since 1970
|
||||
int64_t modifiedNanoseconds = 0;
|
||||
uint64_t device = 0;
|
||||
uint64_t inode = 0;
|
||||
};
|
||||
int InfoOf(int fd, FileInfo& info);
|
||||
int InfoOf(const std::filesystem::path& path, FileInfo& info);
|
||||
|
||||
// rename(2): `target` becomes `source` in one step (Windows: MoveFileExW,
|
||||
// replacing an existing file, written through). 0, or errno.
|
||||
int AtomicReplace(const std::filesystem::path& source, const std::filesystem::path& target);
|
||||
// unlink(2): 0, or -1 with errno.
|
||||
int RemoveFile(const std::filesystem::path& path);
|
||||
// fsync of a folder, so a rename in it survives a power cut (some file
|
||||
// systems refuse it: best effort). Windows: NTFS logs the rename itself,
|
||||
// so nothing.
|
||||
void SyncDirectory(const std::filesystem::path& dir);
|
||||
|
||||
// Bytes free for this user on the drive holding `folder`; nullopt when
|
||||
// it can't be told.
|
||||
std::optional<uint64_t> FreeSpace(const std::filesystem::path& folder);
|
||||
|
||||
// Whether a new file, or a new folder, can be made in `folder`: 0, or
|
||||
// errno. Ask for what the caller will really make. POSIX: access(W_OK |
|
||||
// X_OK), the one permission that covers both. Windows: one of them is
|
||||
// made and deleted again (under a name no other check uses at the same
|
||||
// time), since access() there ignores the folder's permissions (ACLs),
|
||||
// and there the two are separate rights: the root of an NTFS drive (C:\,
|
||||
// and a new data drive the same) lets an ordinary user add folders but
|
||||
// not files. (An elevated process, such as an SSH session, may do both.)
|
||||
enum class NewEntry
|
||||
{
|
||||
File, // a bug report on the Desktop
|
||||
Folder, // the installer's <dest>.partial beside its destination
|
||||
};
|
||||
int CheckWritableFolder(const std::filesystem::path& folder, NewEntry what);
|
||||
|
||||
// An exclusive lock on a folder, held until UnlockFolder (or the process
|
||||
// ends), for one install at a time per folder. A lock (>= 0); -1 if it
|
||||
// can't be locked, with `busy` set when another process holds it.
|
||||
// POSIX: flock on the folder, for the whole system. Windows: a named
|
||||
// mutex for the folder's volume and file index, in the Global namespace
|
||||
// so that every logon session (an SSH session is session 0; each
|
||||
// desktop user has their own) sees the same one, and openable by every
|
||||
// signed-in user (no file is left in the folder).
|
||||
intptr_t LockFolder(const std::filesystem::path& folder, bool& busy);
|
||||
void UnlockFolder(intptr_t lock);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Windows' own places for things (Windows only): the shell's known folders,
|
||||
// the drives, and OneDrive's cloud-only files. (macOS and Linux build these
|
||||
// from HOME, /Volumes and /run/media in user/paths.h and install/locate.cpp.)
|
||||
#pragma once
|
||||
#ifdef _WIN32
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
enum class KnownFolder
|
||||
{
|
||||
LocalAppData, // %LOCALAPPDATA%: per user, never roams
|
||||
Profile, // %USERPROFILE%
|
||||
Downloads,
|
||||
Desktop, // follows OneDrive's folder backup where that's on
|
||||
Documents,
|
||||
};
|
||||
// Where the shell keeps it (SHGetKnownFolderPath); empty if it can't say.
|
||||
std::filesystem::path GetKnownFolder(KnownFolder folder);
|
||||
|
||||
struct Drive
|
||||
{
|
||||
std::filesystem::path root; // "D:\"
|
||||
std::string label; // the volume's name, or "D:"
|
||||
bool removable = false; // a USB stick or an SD card
|
||||
bool readOnly = false;
|
||||
bool system = false; // the drive Windows runs from
|
||||
};
|
||||
// The local drives with a file system: fixed and removable ones (no
|
||||
// optical drives, network shares or RAM disks).
|
||||
std::vector<Drive> Drives();
|
||||
|
||||
// A file that is only in the cloud (OneDrive's Files On-Demand and the
|
||||
// like): opening it would start a download.
|
||||
bool IsCloudOnly(const std::filesystem::path& file);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The guest's 4 GB address space where the operating system needs its own
|
||||
// way of making it (kernel/memory.cpp keeps the POSIX one: an anonymous 4 GB
|
||||
// mapping with one shared object mapped over the physical windows).
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// The 4 GB space (at 0x100000000 if it's free) as one placeholder
|
||||
// reservation, split at 0xA0000000 and 0xC0000000 into four: [0,
|
||||
// 0xA0000000) committed private memory, and the three physical windows
|
||||
// 0xA, 0xC and 0xE, each a full 512 MB view of one pagefile-backed
|
||||
// section, so they alias. A view must fill its placeholder exactly (the
|
||||
// P0 probe: offset 0x1000 or a view 4 KB short both fail), so the 0xE
|
||||
// window is unshifted, as on Apple (Memory::eWindowShift 0). Page 0 is
|
||||
// left inaccessible (null guest pointers fault). About 3 GB of commit:
|
||||
// 2.5 GB private plus the 512 MB section. nullptr when it fails (logged).
|
||||
uint8_t* MapGuestSpace();
|
||||
|
||||
// Fresh zero-filled pages for [address, address + size) in the private
|
||||
// part (below 0xA0000000): decommitted, then committed again, which also
|
||||
// drops any protection. False if the decommit failed (nothing changed;
|
||||
// the caller zeroes them by hand). A commit that fails after it (the
|
||||
// system at its commit limit) is retried for about 2 s, then aborts.
|
||||
bool ResetPrivatePages(void* address, size_t size);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The operating system's own headers, chosen in one place (stdafx.h). On
|
||||
// macOS, iOS and Linux they are the POSIX ones the runtime always used. On
|
||||
// Windows, platform/win32/posix.h brings <windows.h> and the handful of
|
||||
// POSIX names the shared code calls (mprotect, sysconf, nanosleep, ...),
|
||||
// each a thin wrapper over the Windows call; anything with real semantics
|
||||
// of its own (files, threads, timers, faults, folders) is in the platform
|
||||
// layer's headers next to this one.
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/win32/posix.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
@@ -0,0 +1,129 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/file_io.h.
|
||||
// macOS, iOS and Linux: the POSIX calls the runtime always made.
|
||||
#include <platform/file_io.h>
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void Fill(const struct stat& st, FileInfo& info)
|
||||
{
|
||||
info.size = uint64_t(st.st_size);
|
||||
info.directory = S_ISDIR(st.st_mode);
|
||||
info.regular = S_ISREG(st.st_mode);
|
||||
#ifdef __APPLE__
|
||||
info.modifiedSeconds = st.st_mtimespec.tv_sec;
|
||||
info.modifiedNanoseconds = st.st_mtimespec.tv_nsec;
|
||||
#else
|
||||
info.modifiedSeconds = st.st_mtim.tv_sec;
|
||||
info.modifiedNanoseconds = st.st_mtim.tv_nsec;
|
||||
#endif
|
||||
info.device = uint64_t(st.st_dev);
|
||||
info.inode = uint64_t(st.st_ino);
|
||||
}
|
||||
}
|
||||
|
||||
int OpenFile(const std::filesystem::path& path, int flags, int mode)
|
||||
{
|
||||
return open(path.c_str(), flags, mode);
|
||||
}
|
||||
|
||||
std::FILE* OpenStdioFile(const std::filesystem::path& path, const char* mode)
|
||||
{
|
||||
return fopen(path.c_str(), mode);
|
||||
}
|
||||
|
||||
int64_t ReadAt(int fd, void* buffer, size_t size, uint64_t offset)
|
||||
{
|
||||
static_assert(sizeof(off_t) == 8, "large file offsets are required");
|
||||
return pread(fd, buffer, size, off_t(offset));
|
||||
}
|
||||
|
||||
int64_t WriteAt(int fd, const void* buffer, size_t size, uint64_t offset)
|
||||
{
|
||||
return pwrite(fd, buffer, size, off_t(offset));
|
||||
}
|
||||
|
||||
int Truncate(int fd, uint64_t size)
|
||||
{
|
||||
return ftruncate(fd, off_t(size));
|
||||
}
|
||||
|
||||
int InfoOf(int fd, FileInfo& info)
|
||||
{
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0)
|
||||
return -1;
|
||||
Fill(st, info);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int InfoOf(const std::filesystem::path& path, FileInfo& info)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) != 0)
|
||||
return -1;
|
||||
Fill(st, info);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int AtomicReplace(const std::filesystem::path& source, const std::filesystem::path& target)
|
||||
{
|
||||
return rename(source.c_str(), target.c_str()) == 0 ? 0 : errno;
|
||||
}
|
||||
|
||||
int RemoveFile(const std::filesystem::path& path)
|
||||
{
|
||||
return unlink(path.c_str());
|
||||
}
|
||||
|
||||
void SyncDirectory(const std::filesystem::path& dir)
|
||||
{
|
||||
int fd = open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd >= 0)
|
||||
{
|
||||
fsync(fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint64_t> FreeSpace(const std::filesystem::path& folder)
|
||||
{
|
||||
struct statvfs vfs;
|
||||
if (statvfs(folder.c_str(), &vfs) != 0)
|
||||
return std::nullopt;
|
||||
return uint64_t(vfs.f_bavail) * vfs.f_frsize;
|
||||
}
|
||||
|
||||
int CheckWritableFolder(const std::filesystem::path& folder, NewEntry)
|
||||
{
|
||||
return access(folder.c_str(), W_OK | X_OK) == 0 ? 0 : errno;
|
||||
}
|
||||
|
||||
intptr_t LockFolder(const std::filesystem::path& folder, bool& busy)
|
||||
{
|
||||
busy = false;
|
||||
int fd = open(folder.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd >= 0 && flock(fd, LOCK_EX | LOCK_NB) != 0)
|
||||
{
|
||||
busy = errno == EWOULDBLOCK;
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
void UnlockFolder(intptr_t lock)
|
||||
{
|
||||
if (lock >= 0)
|
||||
close(int(lock));
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,15 @@
|
||||
// keeps its crash marker, and the next launch logs that it ended without
|
||||
// quitting.
|
||||
//
|
||||
// Header only (tests/clean_exit_test.cpp).
|
||||
// Windows has no sigaction and no SIGHUP, and nothing there sends SIGTERM:
|
||||
// its C runtime turns Ctrl+C in the console into SIGINT and Ctrl+Break into
|
||||
// SIGBREAK, which a console build (the development one) catches here. The
|
||||
// Windows-subsystem build (the testers') has no console, so its clean ends
|
||||
// are the window's: the close button, Alt+F4 and Quit Game (SDL's quit
|
||||
// event). A session Windows ends without asking it, or Task Manager's End
|
||||
// task, keeps its crash marker, as SIGKILL's does.
|
||||
//
|
||||
// Header only (tests/clean_exit_test.cpp, POSIX only).
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
@@ -40,9 +48,29 @@ namespace quitsignals
|
||||
{
|
||||
int none = 0;
|
||||
g_requested.compare_exchange_strong(none, sig, std::memory_order_relaxed);
|
||||
#ifdef _WIN32
|
||||
// The C runtime resets a signal to its default before calling
|
||||
// the handler: another Ctrl+C while quitting changes nothing, as
|
||||
// elsewhere.
|
||||
std::signal(sig, Handle);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The signals a player ends a console build with (the C runtime's).
|
||||
inline constexpr int kSignals[] = { SIGINT, SIGBREAK };
|
||||
|
||||
// Main thread, before SDL starts (video::Initialize).
|
||||
inline void Install()
|
||||
{
|
||||
for (int sig : kSignals)
|
||||
{
|
||||
if (std::signal(sig, detail::Handle) == SIG_IGN)
|
||||
std::signal(sig, SIG_IGN); // ignored at the start: it stays so
|
||||
}
|
||||
}
|
||||
#else
|
||||
// The signals a player or the system ends the game with.
|
||||
inline constexpr int kSignals[] = { SIGTERM, SIGINT, SIGHUP };
|
||||
|
||||
@@ -63,6 +91,7 @@ namespace quitsignals
|
||||
sigaction(sig, &sa, nullptr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// The signal that asked the game to quit, 0 if none has. Any thread.
|
||||
inline int Requested()
|
||||
@@ -77,7 +106,11 @@ namespace quitsignals
|
||||
{
|
||||
case SIGTERM: return "SIGTERM";
|
||||
case SIGINT: return "SIGINT";
|
||||
#ifdef _WIN32
|
||||
case SIGBREAK: return "SIGBREAK";
|
||||
#else
|
||||
case SIGHUP: return "SIGHUP";
|
||||
#endif
|
||||
default: return "a signal";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Stacks and the names in them, on Windows (Windows only; report/crash.cpp
|
||||
// and report/watchdog.cpp use it for crash and hang reports). macOS and
|
||||
// Linux take theirs from backtrace(), dladdr and the executable's .symtab.
|
||||
//
|
||||
// - Names come from DbgHelp and SpeedBreaker.pdb, the PDB next to the .exe
|
||||
// (found by its name in the .exe's folder only: never the build machine's
|
||||
// path the .exe records, a symbol server or _NT_SYMBOL_PATH, so nothing
|
||||
// is fetched from anywhere). The runtime's functions have full debug
|
||||
// information; the recompiled code has only public symbols (__imp__sub_...
|
||||
// and the sub_... wrappers), which name it just as well. Without the PDB,
|
||||
// only the system DLLs' exports have names. DbgHelp loads it on a thread of
|
||||
// its own at startup (about 0.1 s and 25-35 MB on the ThinkPad, for a 49 MB
|
||||
// PDB), so a crash finds it ready.
|
||||
// - Stacks are walked with StackWalk64 from a thread's registers, using the
|
||||
// unwind tables every x64 function has (RtlLookupFunctionEntry: any module,
|
||||
// loaded with DbgHelp or not).
|
||||
// - Another thread's stack (a hang report): Capture suspends it, copies its
|
||||
// registers and the top of its stack into a buffer allocated beforehand,
|
||||
// and resumes it, calling nothing that allocates or takes a lock while it
|
||||
// is suspended (it may hold the heap's lock); the walk then reads the
|
||||
// copy, so the thread is stopped for a moment only.
|
||||
// - DbgHelp is single-threaded: every call here takes one lock. The crash
|
||||
// handler keeps it once it has it (EnterCrash) and gives up on DbgHelp if
|
||||
// it can't have it soon, or if the crash happened inside DbgHelp; a fault
|
||||
// inside DbgHelp during a crash is caught, and the frame keeps a plainer
|
||||
// name.
|
||||
#pragma once
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace platform::stack
|
||||
{
|
||||
// Loads the symbols (blocking: call it on a thread of its own). Each of
|
||||
// `warm` (code addresses in the program, may be null) is looked up once,
|
||||
// so the PDB's tables the crash handler would need are read now.
|
||||
void LoadSymbols(const void* const* warm, int warmCount);
|
||||
// LoadSymbols has finished (whether or not it found the PDB).
|
||||
bool SymbolsReady();
|
||||
// What LoadSymbols found, for reports: "SpeedBreaker.pdb", or why there
|
||||
// are no names ("no SpeedBreaker.pdb next to SpeedBreaker.exe"; DbgHelp
|
||||
// didn't start, failed in an earlier walk, or wasn't to be had in a crash).
|
||||
const char* SymbolsState();
|
||||
|
||||
// A thread's registers and the top of its stack, as Capture found them.
|
||||
struct Snapshot
|
||||
{
|
||||
CONTEXT context;
|
||||
uint8_t* buffer = nullptr; // allocated by the caller
|
||||
size_t capacity = 0;
|
||||
uintptr_t low = 0; // the stack address of buffer[0] (its rsp)
|
||||
size_t size = 0; // bytes copied
|
||||
};
|
||||
// Suspends `thread` (a handle with THREAD_SUSPEND_RESUME and
|
||||
// THREAD_GET_CONTEXT), takes its registers and its stack from rsp up to
|
||||
// `stackHigh` (at most `capacity` bytes), and resumes it. False if it
|
||||
// couldn't be suspended or read (it has ended).
|
||||
bool Capture(HANDLE thread, uintptr_t stackHigh, Snapshot& snapshot);
|
||||
|
||||
// The stack from `context`: frames[0] is the context's rip, then the
|
||||
// return addresses, innermost first. With `snapshot`, stack memory comes
|
||||
// from its copy where it has it. 0 if DbgHelp isn't usable (not loaded
|
||||
// yet, busy past the crash handler's wait, or broken by the crash).
|
||||
int Walk(const CONTEXT& context, void** frames, int max, const Snapshot* snapshot = nullptr);
|
||||
// The same walk without DbgHelp, for when Walk can't (a hang report's
|
||||
// threads): RtlVirtualUnwind and the unwind tables over the snapshot's
|
||||
// copy of the stack, never the live stack. Ends where the copy does.
|
||||
int Unwind(const Snapshot& snapshot, void** frames, int max);
|
||||
|
||||
// The function `pc` is in: its name (undecorated, cut to fit `size`), its
|
||||
// start, and its size (0 for a public symbol or an export: only its start
|
||||
// is known). False if there is no name, or DbgHelp isn't usable. Outside a
|
||||
// crash, a module DbgHelp hasn't seen yet (a driver's DLL) is loaded first.
|
||||
bool Name(const void* pc, char* name, size_t size, uintptr_t& start, uint32_t& length);
|
||||
|
||||
// The crash handler, before it walks or names anything: DbgHelp for this
|
||||
// thread from now on, waiting up to `waitMs` for another thread to finish
|
||||
// with it. False (and DbgHelp unused from then on) if it isn't to be had,
|
||||
// the crash happened inside it on this thread, or it was unusable already;
|
||||
// SymbolsState then says which.
|
||||
bool EnterCrash(unsigned waitMs);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,70 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Settings for the whole process, made once, first thing in main. Windows
|
||||
// only (elsewhere nothing):
|
||||
//
|
||||
// - No system dialogs for drive errors (SetErrorMode). Asking an empty card
|
||||
// reader or a USB drive that isn't ready for its name (the drive list, the
|
||||
// image search, the installer's destinations) would otherwise show "There
|
||||
// is no disk in the drive" and wait for a click; the call fails instead,
|
||||
// and the drive is skipped. Crashes still go to the crash report.
|
||||
//
|
||||
// - UTF-8 in the console. The runtime's text is UTF-8 (the manifest makes it
|
||||
// the process code page: paths, the environment), but a console turns the
|
||||
// bytes it is given into characters with its own output code page, the
|
||||
// OEM one (437 on an English system): C:\Users\José printed as Jos├⌐. The
|
||||
// console's code page is UTF-8 while the program runs. A console shared
|
||||
// with the terminal it was started from gets its own back when main
|
||||
// returns or exit() is called; std::_Exit (the game's own exits) and a
|
||||
// crash leave it at UTF-8, which Windows 10 and 11 consoles show fine.
|
||||
//
|
||||
// - C runtime calls given an invalid argument (close(-1), a write to a
|
||||
// descriptor that isn't open, fclose(nullptr)) fail with an error, as on
|
||||
// POSIX, which the shared code was written for, and are logged. By
|
||||
// default the C runtime ends the process there and then (__fastfail),
|
||||
// with no crash report and no log line. (A /GS stack-cookie failure
|
||||
// still ends that way, in Windows Error Reporting: macOS checks the same
|
||||
// cookies, but its failure is an abort, which the crash report catches.)
|
||||
//
|
||||
// - No QuickEdit in a console window of the program's own (started from
|
||||
// Explorer, a shortcut or a scheduled task, not from a terminal): there a
|
||||
// click in the window starts a selection, and while it lasts the console
|
||||
// accepts no output, so the log's writer stops, and with it every thread
|
||||
// that logs (the game freezes). A terminal it shares keeps its settings.
|
||||
//
|
||||
// - Standard streams that work without a console. A build linked for the
|
||||
// Windows subsystem (SB_WINDOWS_GUI: the download, no console window)
|
||||
// and started from Explorer has no stdin, stdout or stderr: the C runtime
|
||||
// gives each a placeholder without a handle (fileno -2), and every write
|
||||
// to them fails, so the session log's tee (report/log.cpp), which makes
|
||||
// fd 2 its pipe, would get none of stderr's lines. Each stream without a
|
||||
// handle is reopened on its own number: on the console of the terminal it
|
||||
// was started from, if there is one (AttachConsole: --where, --install and
|
||||
// the log show there), else on NUL (the log has everything). stdin is
|
||||
// always NUL: the terminal's shell reads the console's input. Streams the
|
||||
// parent redirected (a pipe, a file) stay as they are.
|
||||
//
|
||||
// - The discrete GPU on a PC with two (a laptop's integrated one beside an
|
||||
// NVIDIA or AMD one): the exe exports NvOptimusEnablement and
|
||||
// AmdPowerXpressRequestHighPerformance, which those drivers read at start.
|
||||
// (The Vulkan device itself is the presenter's choice, which prefers a
|
||||
// discrete GPU too: video/gpu_choice.h.)
|
||||
#pragma once
|
||||
|
||||
namespace platform
|
||||
{
|
||||
void PrepareProcess();
|
||||
|
||||
#ifdef _WIN32
|
||||
// What PrepareProcess did with the standard streams, for the session
|
||||
// log: "" when they came from the parent (a console, or redirects),
|
||||
// else a sentence.
|
||||
const char* StandardStreamsNote();
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
inline void PrepareProcess()
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Host threads with a chosen stack size: the guest's threads (each guest
|
||||
// call is a host call, so guest code needs a deep host stack) and the
|
||||
// game's main thread. pthreads on macOS, iOS and Linux, exactly as the
|
||||
// runtime always made them; _beginthreadex on Windows, where the size is a
|
||||
// reservation (STACK_SIZE_PARAM_IS_A_RESERVATION: committed as it grows),
|
||||
// so 64 MB costs address space, not memory. std::thread can't do either:
|
||||
// it has no stack size, and its handle can't outlive a running thread.
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
namespace platform
|
||||
{
|
||||
#ifdef _WIN32
|
||||
using Thread = void*; // the thread's HANDLE
|
||||
#else
|
||||
using Thread = pthread_t;
|
||||
#endif
|
||||
|
||||
// Starts fn(arg) on a new thread with `stackSize` bytes of stack.
|
||||
// Returns 0, or the error (pthread_create's; Windows: GetLastError()).
|
||||
int StartThread(Thread* thread, size_t stackSize, void* (*fn)(void*), void* arg);
|
||||
// Lets the thread run on its own: nothing joins it, its resources go
|
||||
// when it ends.
|
||||
void DetachThread(Thread thread);
|
||||
// Waits for the thread to end (then its handle is gone).
|
||||
void JoinThread(Thread thread);
|
||||
// The calling thread's name, for debuggers and profilers (Linux keeps
|
||||
// 15 characters; Windows: SetThreadDescription, where it exists).
|
||||
void NameCurrentThread(const char* name);
|
||||
|
||||
#ifndef _WIN32
|
||||
inline int StartThread(Thread* thread, size_t stackSize, void* (*fn)(void*), void* arg)
|
||||
{
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, stackSize);
|
||||
return pthread_create(thread, &attr, fn, arg);
|
||||
}
|
||||
|
||||
inline void DetachThread(Thread thread)
|
||||
{
|
||||
pthread_detach(thread);
|
||||
}
|
||||
|
||||
inline void JoinThread(Thread thread)
|
||||
{
|
||||
pthread_join(thread, nullptr);
|
||||
}
|
||||
|
||||
inline void NameCurrentThread(const char* name)
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
pthread_setname_np(name);
|
||||
#elif defined(__linux__)
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Timing the operating system decides. Windows schedules sleeps on a timer
|
||||
// that ticks every 15.6 ms unless a process asks for more: there Sleep(1),
|
||||
// a 100 us sleep_for and a 5.33 ms sleep_until all took about 16 ms (the
|
||||
// P0 probe, laptop.md item d), and after timeBeginPeriod(1) about 2 ms.
|
||||
// macOS and Linux sleep to the microsecond as it is: nothing to do there.
|
||||
// (Sub-millisecond waits on Windows, a high-resolution waitable timer plus a
|
||||
// short spin for the vblank, audio and tick cadences, come in PLAN.md P4.)
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// Once, first thing in main: the finest timer resolution (Windows: 1 ms,
|
||||
// for the life of the process). Elsewhere nothing.
|
||||
void RaiseTimerResolution();
|
||||
|
||||
// Sleeps until `steadyNs` (std::chrono::steady_clock's time since its
|
||||
// epoch, in ns), never returning before it: the presenter's hold of a new
|
||||
// frame for its refresh (video/vblank_lock.h's PresentAt). Windows: a
|
||||
// high-resolution waitable timer (Windows 10 1803 and later; a 1 ms
|
||||
// sleep_until, about 1.3 ms late there, where it can't be made), which
|
||||
// wakes within a few tenths of a millisecond, with the rest of the way
|
||||
// yielded; at 240 Hz a refresh is 4.2 ms. Also the audio thread's 5.33 ms
|
||||
// ticks (apu/audio.cpp). Elsewhere sleep_until.
|
||||
void SleepUntil(int64_t steadyNs);
|
||||
|
||||
// For the audio thread (apu/audio.cpp), on it: Windows: the thread joins
|
||||
// MMCSS's "Pro Audio" task (else "Audio", else the highest normal
|
||||
// priority), as WASAPI's own threads do, so a busy core doesn't hold its
|
||||
// 5.33 ms ticks back. Returns what it got, for the log. Elsewhere nothing
|
||||
// (Apple sets its QoS class in audio.cpp) and nullptr.
|
||||
const char* RaiseAudioThreadPriority();
|
||||
|
||||
#ifndef _WIN32
|
||||
inline const char* RaiseAudioThreadPriority()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline void RaiseTimerResolution()
|
||||
{
|
||||
}
|
||||
|
||||
inline void SleepUntil(int64_t steadyNs)
|
||||
{
|
||||
std::this_thread::sleep_until(std::chrono::steady_clock::time_point(std::chrono::nanoseconds(steadyNs)));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The CPU check on Windows (PLAN.md 1.3, Unleashed's pattern raised to
|
||||
// x86-64-v3). Everything else is built with -march=x86-64-v3, so on an
|
||||
// older CPU the first AVX2, BMI or MOVBE instruction would crash before
|
||||
// main. This runs first: built without -march (runtime/CMakeLists.txt) and
|
||||
// using no templates, so no v3 copy of anything can be picked for it, at
|
||||
// constructor priority 101 (".CRT$XCA00101", before every ordinary static
|
||||
// initializer in .CRT$XCU). Without the instruction sets it says so in a
|
||||
// message box and on stderr, and exits.
|
||||
#include <windows.h>
|
||||
#include <intrin.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool HasX86_64_V3(char* missing, size_t size)
|
||||
{
|
||||
int r[4];
|
||||
__cpuid(r, 0);
|
||||
int maxLeaf = r[0];
|
||||
__cpuid(r, 1);
|
||||
unsigned ecx1 = unsigned(r[2]);
|
||||
unsigned ebx7 = 0;
|
||||
if (maxLeaf >= 7)
|
||||
{
|
||||
__cpuidex(r, 7, 0);
|
||||
ebx7 = unsigned(r[1]);
|
||||
}
|
||||
__cpuid(r, int(0x80000000));
|
||||
unsigned ecxExt = 0;
|
||||
if (unsigned(r[0]) >= 0x80000001u)
|
||||
{
|
||||
__cpuid(r, int(0x80000001));
|
||||
ecxExt = unsigned(r[2]);
|
||||
}
|
||||
// The OS must save the YMM registers: OSXSAVE, then XCR0 bits 1-2.
|
||||
bool osAvx = (ecx1 & (1u << 27)) && (_xgetbv(0) & 6) == 6;
|
||||
struct Feature { const char* name; bool present; };
|
||||
const Feature features[] = {
|
||||
{ "AVX", (ecx1 & (1u << 28)) && osAvx },
|
||||
{ "AVX2", (ebx7 & (1u << 5)) && osAvx },
|
||||
{ "FMA", (ecx1 & (1u << 12)) != 0 },
|
||||
{ "F16C", (ecx1 & (1u << 29)) != 0 },
|
||||
{ "MOVBE", (ecx1 & (1u << 22)) != 0 },
|
||||
{ "BMI1", (ebx7 & (1u << 3)) != 0 },
|
||||
{ "BMI2", (ebx7 & (1u << 8)) != 0 },
|
||||
{ "LZCNT", (ecxExt & (1u << 5)) != 0 },
|
||||
};
|
||||
missing[0] = 0;
|
||||
for (const Feature& f : features)
|
||||
if (!f.present)
|
||||
{
|
||||
if (missing[0])
|
||||
strncat_s(missing, size, ", ", _TRUNCATE);
|
||||
strncat_s(missing, size, f.name, _TRUNCATE);
|
||||
}
|
||||
return missing[0] == 0;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((constructor(101))) static void CheckCpu()
|
||||
{
|
||||
char missing[128];
|
||||
if (HasX86_64_V3(missing, sizeof(missing)))
|
||||
return;
|
||||
char text[512];
|
||||
_snprintf_s(text, sizeof(text), _TRUNCATE,
|
||||
"SpeedBreaker needs a processor with the x86-64-v3 instruction sets (Intel Haswell or AMD Excavator, "
|
||||
"2013-2015, and newer). This one lacks: %s.", missing);
|
||||
fprintf(stderr, "[runtime] %s\n", text);
|
||||
fflush(stderr);
|
||||
MessageBoxA(nullptr, text, "SpeedBreaker", MB_OK | MB_ICONERROR);
|
||||
ExitProcess(1);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
// Windows' error codes as the errno values POSIX code expects (Windows only).
|
||||
#pragma once
|
||||
#include <platform/os.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
inline int ErrnoFromWin32(DWORD error)
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case ERROR_FILE_NOT_FOUND:
|
||||
case ERROR_PATH_NOT_FOUND:
|
||||
case ERROR_INVALID_DRIVE:
|
||||
case ERROR_BAD_NETPATH:
|
||||
case ERROR_BAD_NET_NAME:
|
||||
case ERROR_INVALID_NAME:
|
||||
case ERROR_BAD_PATHNAME:
|
||||
case ERROR_DIRECTORY:
|
||||
return ENOENT;
|
||||
case ERROR_FILE_EXISTS:
|
||||
case ERROR_ALREADY_EXISTS:
|
||||
return EEXIST;
|
||||
case ERROR_ACCESS_DENIED:
|
||||
case ERROR_SHARING_VIOLATION:
|
||||
case ERROR_LOCK_VIOLATION:
|
||||
case ERROR_CURRENT_DIRECTORY:
|
||||
return EACCES;
|
||||
case ERROR_WRITE_PROTECT:
|
||||
return EROFS;
|
||||
case ERROR_DISK_FULL:
|
||||
case ERROR_HANDLE_DISK_FULL:
|
||||
return ENOSPC;
|
||||
case ERROR_NOACCESS:
|
||||
case ERROR_INVALID_USER_BUFFER:
|
||||
return EFAULT;
|
||||
case ERROR_NOT_ENOUGH_MEMORY:
|
||||
case ERROR_OUTOFMEMORY:
|
||||
case ERROR_COMMITMENT_LIMIT:
|
||||
return ENOMEM;
|
||||
case ERROR_TOO_MANY_OPEN_FILES:
|
||||
return EMFILE;
|
||||
case ERROR_INVALID_HANDLE:
|
||||
return EBADF;
|
||||
case ERROR_DIR_NOT_EMPTY:
|
||||
return ENOTEMPTY;
|
||||
case ERROR_NOT_SAME_DEVICE:
|
||||
return EXDEV;
|
||||
case ERROR_BROKEN_PIPE:
|
||||
case ERROR_NO_DATA:
|
||||
return EPIPE;
|
||||
case ERROR_INVALID_PARAMETER:
|
||||
return EINVAL;
|
||||
case ERROR_FILENAME_EXCED_RANGE:
|
||||
return ENAMETOOLONG;
|
||||
default:
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/fault.h.
|
||||
#include <stdafx.h>
|
||||
#include <platform/fault.h>
|
||||
#include <kernel/write_watch.h>
|
||||
#include <report/report.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace
|
||||
{
|
||||
LONG CALLBACK OnException(EXCEPTION_POINTERS* info)
|
||||
{
|
||||
const EXCEPTION_RECORD* record = info->ExceptionRecord;
|
||||
if (record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && record->NumberParameters >= 2)
|
||||
{
|
||||
// ExceptionInformation[0]: 0 a read, 1 a write, 8 an execution
|
||||
// (DEP); [1]: the address. The probe saw both exact.
|
||||
bool isWrite = record->ExceptionInformation[0] == 1;
|
||||
void* address = reinterpret_cast<void*>(record->ExceptionInformation[1]);
|
||||
writewatch::t_faultPc = record->ExceptionAddress;
|
||||
if (writewatch::HandleFault(address, isWrite))
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
LONG WINAPI OnUnhandled(EXCEPTION_POINTERS* info)
|
||||
{
|
||||
report::WriteCrashReport(info);
|
||||
TerminateProcess(GetCurrentProcess(), info->ExceptionRecord->ExceptionCode);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
void OnAbort(int)
|
||||
{
|
||||
// abort() raises SIGABRT, not an exception: a record of our own,
|
||||
// with this thread's registers, so the report has a stack. It starts
|
||||
// where this handler was called (the C runtime's raise, in abort), as
|
||||
// a POSIX report's does: this function's own frame is unwound.
|
||||
static CONTEXT context;
|
||||
static EXCEPTION_RECORD record;
|
||||
RtlCaptureContext(&context);
|
||||
DWORD64 imageBase = 0;
|
||||
if (PRUNTIME_FUNCTION function = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr))
|
||||
{
|
||||
void* handlerData = nullptr;
|
||||
DWORD64 establisher = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, function, &context, &handlerData, &establisher, nullptr);
|
||||
}
|
||||
record = {};
|
||||
record.ExceptionCode = 0x40000015; // STATUS_FATAL_APP_EXIT, what Windows calls an abort
|
||||
record.ExceptionAddress = reinterpret_cast<void*>(context.Rip);
|
||||
EXCEPTION_POINTERS pointers{ &record, &context };
|
||||
report::WriteCrashReport(&pointers);
|
||||
_exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
bool KeepCrashFilter()
|
||||
{
|
||||
LPTOP_LEVEL_EXCEPTION_FILTER previous = SetUnhandledExceptionFilter(OnUnhandled);
|
||||
return previous == OnUnhandled;
|
||||
}
|
||||
|
||||
void InstallFaultHandlers()
|
||||
{
|
||||
AddVectoredExceptionHandler(1, OnException);
|
||||
SetUnhandledExceptionFilter(OnUnhandled);
|
||||
// Assertions stay on in every build: their message goes to stderr
|
||||
// (the log), never a dialog box, and abort() neither asks Windows
|
||||
// Error Reporting nor prints its own line.
|
||||
_set_error_mode(_OUT_TO_STDERR);
|
||||
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
|
||||
signal(SIGABRT, OnAbort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/file_io.h.
|
||||
#include <platform/file_io.h>
|
||||
#include <platform/win32/errors.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <sddl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
HANDLE HandleOf(int fd)
|
||||
{
|
||||
return reinterpret_cast<HANDLE>(_get_osfhandle(fd));
|
||||
}
|
||||
|
||||
int64_t ToUnixNs(const FILETIME& ft)
|
||||
{
|
||||
constexpr int64_t kEpochDelta = 116444736000000000; // 1601 to 1970 in 100 ns
|
||||
return ((int64_t(ft.dwHighDateTime) << 32 | ft.dwLowDateTime) - kEpochDelta) * 100;
|
||||
}
|
||||
|
||||
void Fill(const BY_HANDLE_FILE_INFORMATION& b, DWORD fileType, FileInfo& info)
|
||||
{
|
||||
info.directory = (b.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
info.regular = !info.directory && fileType == FILE_TYPE_DISK;
|
||||
info.size = info.directory ? 0 : uint64_t(b.nFileSizeHigh) << 32 | b.nFileSizeLow;
|
||||
int64_t ns = ToUnixNs(b.ftLastWriteTime);
|
||||
info.modifiedSeconds = ns / 1000000000;
|
||||
info.modifiedNanoseconds = ns % 1000000000;
|
||||
info.device = b.dwVolumeSerialNumber;
|
||||
info.inode = uint64_t(b.nFileIndexHigh) << 32 | b.nFileIndexLow;
|
||||
}
|
||||
|
||||
int Fail(DWORD error)
|
||||
{
|
||||
errno = ErrnoFromWin32(error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int OpenFile(const std::filesystem::path& path, int flags, int mode)
|
||||
{
|
||||
int accessMode = flags & (_O_RDONLY | _O_WRONLY | _O_RDWR);
|
||||
DWORD access = accessMode == _O_WRONLY ? GENERIC_WRITE : accessMode == _O_RDWR ? GENERIC_READ | GENERIC_WRITE : GENERIC_READ;
|
||||
DWORD disposition = OPEN_EXISTING;
|
||||
if (flags & _O_CREAT)
|
||||
disposition = (flags & _O_EXCL) ? CREATE_NEW : (flags & _O_TRUNC) ? CREATE_ALWAYS : OPEN_ALWAYS;
|
||||
else if (flags & _O_TRUNC)
|
||||
disposition = TRUNCATE_EXISTING;
|
||||
DWORD attributes = FILE_ATTRIBUTE_NORMAL;
|
||||
if ((flags & _O_CREAT) && !(mode & 0200))
|
||||
attributes = FILE_ATTRIBUTE_READONLY;
|
||||
if (flags & _O_SEQUENTIAL)
|
||||
attributes |= FILE_FLAG_SEQUENTIAL_SCAN;
|
||||
// A folder: what open(2) says about one opened for writing (for
|
||||
// reading, POSIX opens it; nothing here reads folders that way).
|
||||
DWORD existing = GetFileAttributesW(path.c_str());
|
||||
if (existing != INVALID_FILE_ATTRIBUTES && (existing & FILE_ATTRIBUTE_DIRECTORY))
|
||||
{
|
||||
errno = (flags & _O_CREAT) && (flags & _O_EXCL) ? EEXIST : EISDIR;
|
||||
return -1;
|
||||
}
|
||||
SECURITY_ATTRIBUTES security{ sizeof(SECURITY_ATTRIBUTES), nullptr, (flags & _O_NOINHERIT) ? FALSE : TRUE };
|
||||
HANDLE h = CreateFileW(path.c_str(), access, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security,
|
||||
disposition, attributes, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return Fail(GetLastError());
|
||||
int fd = _open_osfhandle(reinterpret_cast<intptr_t>(h), accessMode | (flags & _O_APPEND) | (flags & _O_NOINHERIT) | _O_BINARY);
|
||||
if (fd < 0)
|
||||
{
|
||||
CloseHandle(h);
|
||||
errno = EMFILE;
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
std::FILE* OpenStdioFile(const std::filesystem::path& path, const char* mode)
|
||||
{
|
||||
wchar_t wideMode[16];
|
||||
size_t n = 0;
|
||||
for (; mode[n] && n + 1 < std::size(wideMode); n++)
|
||||
wideMode[n] = wchar_t(mode[n]);
|
||||
wideMode[n] = 0;
|
||||
return _wfopen(path.c_str(), wideMode);
|
||||
}
|
||||
|
||||
int64_t ReadAt(int fd, void* buffer, size_t size, uint64_t offset)
|
||||
{
|
||||
HANDLE h = HandleOf(fd);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
OVERLAPPED at{};
|
||||
at.Offset = DWORD(offset);
|
||||
at.OffsetHigh = DWORD(offset >> 32);
|
||||
DWORD got = 0;
|
||||
if (!ReadFile(h, buffer, DWORD(std::min<size_t>(size, 1u << 30)), &got, &at))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
return error == ERROR_HANDLE_EOF ? 0 : Fail(error);
|
||||
}
|
||||
return int64_t(got);
|
||||
}
|
||||
|
||||
int64_t WriteAt(int fd, const void* buffer, size_t size, uint64_t offset)
|
||||
{
|
||||
HANDLE h = HandleOf(fd);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
OVERLAPPED at{};
|
||||
at.Offset = DWORD(offset);
|
||||
at.OffsetHigh = DWORD(offset >> 32);
|
||||
DWORD put = 0;
|
||||
if (!WriteFile(h, buffer, DWORD(std::min<size_t>(size, 1u << 30)), &put, &at))
|
||||
return Fail(GetLastError());
|
||||
return int64_t(put);
|
||||
}
|
||||
|
||||
int Truncate(int fd, uint64_t size)
|
||||
{
|
||||
HANDLE h = HandleOf(fd);
|
||||
FILE_END_OF_FILE_INFO end{};
|
||||
end.EndOfFile.QuadPart = LONGLONG(size);
|
||||
if (h == INVALID_HANDLE_VALUE || !SetFileInformationByHandle(h, FileEndOfFileInfo, &end, sizeof(end)))
|
||||
return Fail(h == INVALID_HANDLE_VALUE ? ERROR_INVALID_HANDLE : GetLastError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int InfoOf(int fd, FileInfo& info)
|
||||
{
|
||||
HANDLE h = HandleOf(fd);
|
||||
BY_HANDLE_FILE_INFORMATION b;
|
||||
if (h == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(h, &b))
|
||||
return Fail(h == INVALID_HANDLE_VALUE ? ERROR_INVALID_HANDLE : GetLastError());
|
||||
Fill(b, GetFileType(h), info);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int InfoOf(const std::filesystem::path& path, FileInfo& info)
|
||||
{
|
||||
// FILE_FLAG_BACKUP_SEMANTICS opens folders too; links (reparse
|
||||
// points) are followed, as stat(2) follows symlinks.
|
||||
HANDLE h = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
||||
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return Fail(GetLastError());
|
||||
BY_HANDLE_FILE_INFORMATION b;
|
||||
bool ok = GetFileInformationByHandle(h, &b);
|
||||
DWORD error = GetLastError();
|
||||
if (ok)
|
||||
Fill(b, GetFileType(h), info);
|
||||
CloseHandle(h);
|
||||
return ok ? 0 : Fail(error);
|
||||
}
|
||||
|
||||
int AtomicReplace(const std::filesystem::path& source, const std::filesystem::path& target)
|
||||
{
|
||||
if (MoveFileExW(source.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
return 0;
|
||||
return ErrnoFromWin32(GetLastError());
|
||||
}
|
||||
|
||||
int RemoveFile(const std::filesystem::path& path)
|
||||
{
|
||||
return DeleteFileW(path.c_str()) ? 0 : Fail(GetLastError());
|
||||
}
|
||||
|
||||
void SyncDirectory(const std::filesystem::path&)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<uint64_t> FreeSpace(const std::filesystem::path& folder)
|
||||
{
|
||||
ULARGE_INTEGER available;
|
||||
if (!GetDiskFreeSpaceExW(folder.c_str(), &available, nullptr, nullptr))
|
||||
return std::nullopt;
|
||||
return uint64_t(available.QuadPart);
|
||||
}
|
||||
|
||||
int CheckWritableFolder(const std::filesystem::path& folder, NewEntry what)
|
||||
{
|
||||
// This process, this thread and a count: the installer screen's
|
||||
// check and a running install may look at one folder at once.
|
||||
static std::atomic<uint32_t> s_count{ 0 };
|
||||
DWORD error = ERROR_ALREADY_EXISTS;
|
||||
// Another name only if this one is taken (left by a process that
|
||||
// ended between making a folder and deleting it).
|
||||
for (int attempt = 0; attempt < 4 && (error == ERROR_ALREADY_EXISTS || error == ERROR_FILE_EXISTS); attempt++)
|
||||
{
|
||||
std::filesystem::path probe = folder / std::format(L".speedbreaker-write-test-{}-{}-{}", GetCurrentProcessId(),
|
||||
GetCurrentThreadId(), s_count.fetch_add(1, std::memory_order_relaxed));
|
||||
if (what == NewEntry::File)
|
||||
{
|
||||
HANDLE h = CreateFileW(probe.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
|
||||
FILE_ATTRIBUTE_TEMPORARY | FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||
if (h != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(h);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (CreateDirectoryW(probe.c_str(), nullptr))
|
||||
{
|
||||
// Deleted at once. A scanner (Defender, the search indexer)
|
||||
// looking at the new folder can hold the deletion up for a
|
||||
// moment: wait for it a little.
|
||||
for (int i = 0; !RemoveDirectoryW(probe.c_str()); i++)
|
||||
{
|
||||
if (i == 10)
|
||||
{
|
||||
fprintf(stderr, "[install] could not delete the empty test folder %s: error %lu\n",
|
||||
probe.string().c_str(), GetLastError());
|
||||
break;
|
||||
}
|
||||
Sleep(20);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
error = GetLastError();
|
||||
}
|
||||
return ErrnoFromWin32(error);
|
||||
}
|
||||
|
||||
intptr_t LockFolder(const std::filesystem::path& folder, bool& busy)
|
||||
{
|
||||
busy = false;
|
||||
FileInfo info;
|
||||
if (InfoOf(folder, info) != 0 || !info.directory)
|
||||
return -1;
|
||||
// Global\: the whole machine, not this logon session (Local\ would
|
||||
// let an install over SSH and one at the desktop, or two users'
|
||||
// installs into one shared folder, run at once). Mutexes need no
|
||||
// privilege there (file mappings would). Every signed-in user may
|
||||
// wait on it and release it; nobody else may change or delete it.
|
||||
std::wstring name = std::format(L"Global\\SpeedBreaker-folder-{:08X}-{:016X}", uint32_t(info.device), info.inode);
|
||||
constexpr DWORD kAccess = SYNCHRONIZE | MUTEX_MODIFY_STATE;
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:(A;;0x001F0001;;;SY)(A;;0x001F0001;;;BA)(A;;0x001F0001;;;OW)"
|
||||
L"(A;;0x00100001;;;AU)", SDDL_REVISION_1, &descriptor, nullptr);
|
||||
SECURITY_ATTRIBUTES security{ sizeof(SECURITY_ATTRIBUTES), descriptor, FALSE };
|
||||
HANDLE mutex = CreateMutexExW(descriptor ? &security : nullptr, name.c_str(), 0, kAccess);
|
||||
DWORD error = GetLastError();
|
||||
LocalFree(descriptor);
|
||||
if (!mutex)
|
||||
{
|
||||
// There, but not ours to open: it exists only while an install
|
||||
// holds it (or is checking it), so another install is running.
|
||||
busy = error == ERROR_ACCESS_DENIED;
|
||||
return -1;
|
||||
}
|
||||
DWORD wait = WaitForSingleObject(mutex, 0);
|
||||
if (wait == WAIT_OBJECT_0 || wait == WAIT_ABANDONED) // abandoned: its holder ended without letting go
|
||||
return reinterpret_cast<intptr_t>(mutex);
|
||||
busy = wait == WAIT_TIMEOUT;
|
||||
CloseHandle(mutex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void UnlockFolder(intptr_t lock)
|
||||
{
|
||||
if (lock < 0)
|
||||
return;
|
||||
ReleaseMutex(reinterpret_cast<HANDLE>(lock));
|
||||
CloseHandle(reinterpret_cast<HANDLE>(lock));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/folders.h.
|
||||
#include <platform/folders.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <knownfolders.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string Utf8(const wchar_t* wide)
|
||||
{
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (n <= 1)
|
||||
return {};
|
||||
std::string out(size_t(n - 1), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide, -1, out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path GetKnownFolder(KnownFolder folder)
|
||||
{
|
||||
const KNOWNFOLDERID* id = nullptr;
|
||||
switch (folder)
|
||||
{
|
||||
case KnownFolder::LocalAppData: id = &FOLDERID_LocalAppData; break;
|
||||
case KnownFolder::Profile: id = &FOLDERID_Profile; break;
|
||||
case KnownFolder::Downloads: id = &FOLDERID_Downloads; break;
|
||||
case KnownFolder::Desktop: id = &FOLDERID_Desktop; break;
|
||||
case KnownFolder::Documents: id = &FOLDERID_Documents; break;
|
||||
}
|
||||
PWSTR path = nullptr;
|
||||
std::filesystem::path out;
|
||||
// KF_FLAG_DONT_VERIFY: just the location; nothing is created.
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(*id, KF_FLAG_DONT_VERIFY, nullptr, &path)) && path)
|
||||
out = path;
|
||||
CoTaskMemFree(path);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<Drive> Drives()
|
||||
{
|
||||
std::vector<Drive> drives;
|
||||
wchar_t systemDir[MAX_PATH] = L"";
|
||||
GetSystemDirectoryW(systemDir, MAX_PATH);
|
||||
DWORD mask = GetLogicalDrives();
|
||||
for (int i = 0; i < 26; i++)
|
||||
{
|
||||
if (!(mask & (1u << i)))
|
||||
continue;
|
||||
wchar_t root[4] = { wchar_t(L'A' + i), L':', L'\\', 0 };
|
||||
UINT type = GetDriveTypeW(root);
|
||||
if (type != DRIVE_FIXED && type != DRIVE_REMOVABLE)
|
||||
continue;
|
||||
wchar_t name[MAX_PATH + 1] = L"";
|
||||
DWORD flags = 0;
|
||||
// No medium in a card reader: no volume information; skip it.
|
||||
if (!GetVolumeInformationW(root, name, MAX_PATH + 1, nullptr, nullptr, &flags, nullptr, 0))
|
||||
continue;
|
||||
Drive d;
|
||||
d.root = root;
|
||||
d.label = name[0] ? Utf8(name) : Utf8(std::wstring(root, 2).c_str());
|
||||
d.removable = type == DRIVE_REMOVABLE;
|
||||
d.readOnly = (flags & FILE_READ_ONLY_VOLUME) != 0;
|
||||
d.system = towupper(systemDir[0]) == root[0];
|
||||
drives.push_back(std::move(d));
|
||||
}
|
||||
return drives;
|
||||
}
|
||||
|
||||
bool IsCloudOnly(const std::filesystem::path& file)
|
||||
{
|
||||
DWORD attributes = GetFileAttributesW(file.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & (FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_OFFLINE)) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Force-included into the recompiled code on Windows only (after indirect.h
|
||||
// and guest_time.h; recomp/CMakeLists.txt). The game's setjmp and longjmp
|
||||
// become the host's, with the host jmp_buf inside the guest's (XenonRecomp:
|
||||
// setjmp(*reinterpret_cast<jmp_buf*>(base + r3))). The CRT's _setjmp on x64
|
||||
// stores XMM6-15 with aligned moves, so a buffer that is only 8-byte aligned
|
||||
// crashes in _setjmp itself (the P0 probe, laptop.md item f). Four of the
|
||||
// game's seven setjmp sites take buffers of unknown alignment
|
||||
// (ppc_recomp.184.cpp, ppc_recomp.186.cpp), so both calls use the guest
|
||||
// buffer's first 16-byte boundary instead: setjmp and longjmp get the same
|
||||
// guest pointer, so they agree. It fits, as the guest's own setjmp
|
||||
// (sub_826BF180) fills 316 bytes or more and the host's jmp_buf is 256:
|
||||
// 15 + 256 <= 316. __builtin_setjmp works at any alignment but doesn't keep
|
||||
// XMM6-15, which Win64 code expects to survive the call.
|
||||
#pragma once
|
||||
|
||||
// Everything the generated code includes comes first, so the macros below
|
||||
// reach only the generated code itself.
|
||||
#include <ppc_context.h>
|
||||
|
||||
#include <csetjmp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace sb_win32
|
||||
{
|
||||
inline void* AlignJmpBuf(void* guestBuffer)
|
||||
{
|
||||
return reinterpret_cast<void*>((reinterpret_cast<uintptr_t>(guestBuffer) + 15) & ~uintptr_t(15));
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(sizeof(jmp_buf) + 15 <= 316, "the host jmp_buf must fit in the guest's after alignment");
|
||||
|
||||
#undef setjmp
|
||||
#define setjmp(buffer) _setjmp(*reinterpret_cast<jmp_buf*>(::sb_win32::AlignJmpBuf(&(buffer))))
|
||||
#define longjmp(buffer, value) ::longjmp(*reinterpret_cast<jmp_buf*>(::sb_win32::AlignJmpBuf(&(buffer))), (value))
|
||||
@@ -0,0 +1,125 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/memory.h.
|
||||
//
|
||||
// VirtualAlloc2 and MapViewOfFile3 (Windows 10 1803 and newer) come from
|
||||
// kernelbase.dll by GetProcAddress, not onecore.lib: no umbrella import
|
||||
// library mixed with kernel32.lib, and older Windows fails here with a
|
||||
// message instead of refusing to start. Everything here was tried on the
|
||||
// target laptop first (laptop.md, P0 probe item b).
|
||||
#include <platform/memory.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint64_t kGuestSize = 0x100000000ull;
|
||||
constexpr uint64_t kPhysicalSize = 0x20000000ull; // 512 MB
|
||||
constexpr uint32_t kWindows[3] = { 0xA0000000u, 0xC0000000u, 0xE0000000u };
|
||||
|
||||
using VirtualAlloc2Fn = PVOID(WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
|
||||
using MapViewOfFile3Fn = PVOID(WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
uint8_t* MapGuestSpace()
|
||||
{
|
||||
HMODULE kernelBase = GetModuleHandleW(L"kernelbase.dll");
|
||||
auto virtualAlloc2 = kernelBase ? reinterpret_cast<VirtualAlloc2Fn>(GetProcAddress(kernelBase, "VirtualAlloc2")) : nullptr;
|
||||
auto mapViewOfFile3 = kernelBase ? reinterpret_cast<MapViewOfFile3Fn>(GetProcAddress(kernelBase, "MapViewOfFile3")) : nullptr;
|
||||
if (!virtualAlloc2 || !mapViewOfFile3)
|
||||
{
|
||||
fprintf(stderr, "[memory] VirtualAlloc2 and MapViewOfFile3 are missing: Windows 10 (1803) or newer is needed\n");
|
||||
return nullptr;
|
||||
}
|
||||
HANDLE process = GetCurrentProcess();
|
||||
|
||||
// 1. The 4 GB placeholder, at the address the other platforms ask for.
|
||||
auto* base = static_cast<uint8_t*>(virtualAlloc2(process, reinterpret_cast<void*>(0x100000000ull), kGuestSize,
|
||||
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0));
|
||||
if (!base)
|
||||
base = static_cast<uint8_t*>(virtualAlloc2(process, nullptr, kGuestSize, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER,
|
||||
PAGE_NOACCESS, nullptr, 0));
|
||||
if (!base)
|
||||
{
|
||||
fprintf(stderr, "[memory] reserving 4 GB of address space failed: error %lu\n", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 2. Split it: [0, A) [A, C) [C, E) [E, 4G). (On a failure from here
|
||||
// on the reservation is left as it is: the caller exits.)
|
||||
if (!VirtualFree(base + kWindows[0], kPhysicalSize, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) ||
|
||||
!VirtualFree(base + kWindows[1], kPhysicalSize, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
|
||||
{
|
||||
fprintf(stderr, "[memory] splitting the guest space at the physical windows failed: error %lu\n", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 3. Physical memory: one 512 MB pagefile-backed section, viewed at
|
||||
// each window (the views keep it alive once its handle is closed).
|
||||
HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE | SEC_COMMIT, 0, DWORD(kPhysicalSize), nullptr);
|
||||
if (!section)
|
||||
{
|
||||
fprintf(stderr, "[memory] creating 512 MB of physical memory failed: error %lu\n", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
bool viewsOk = true;
|
||||
for (uint32_t window : kWindows)
|
||||
{
|
||||
void* view = mapViewOfFile3(section, process, base + window, 0, kPhysicalSize, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE,
|
||||
nullptr, 0);
|
||||
if (view != base + window)
|
||||
{
|
||||
fprintf(stderr, "[memory] mapping the physical window at %08X failed: error %lu\n", window, GetLastError());
|
||||
viewsOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CloseHandle(section);
|
||||
if (!viewsOk)
|
||||
return nullptr;
|
||||
|
||||
// 4. The private part, [0, 0xA0000000), committed in one call.
|
||||
void* low = virtualAlloc2(process, base, kWindows[0], MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE,
|
||||
nullptr, 0);
|
||||
if (low != base)
|
||||
{
|
||||
// Two steps: replace the placeholder with a reservation, then commit.
|
||||
low = virtualAlloc2(process, base, kWindows[0], MEM_RESERVE | MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0);
|
||||
if (low == base)
|
||||
low = VirtualAlloc(base, kWindows[0], MEM_COMMIT, PAGE_READWRITE);
|
||||
}
|
||||
if (low != base)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
fprintf(stderr, "[memory] committing 2.5 GB of guest memory failed: error %lu%s\n", error,
|
||||
error == ERROR_COMMITMENT_LIMIT ? " (the page file is too small, or too much else is running)" : "");
|
||||
return nullptr;
|
||||
}
|
||||
DWORD old;
|
||||
VirtualProtect(base, 4096, PAGE_NOACCESS, &old);
|
||||
return base;
|
||||
}
|
||||
|
||||
bool ResetPrivatePages(void* address, size_t size)
|
||||
{
|
||||
if (!VirtualFree(address, size, MEM_DECOMMIT))
|
||||
return false;
|
||||
// The decommit handed the pages' commit charge back. At the system's
|
||||
// commit limit another program may take it before the commit below:
|
||||
// wait for it to come back (a program ending, the page file
|
||||
// growing), for about 2 s. After that the guest memory the game
|
||||
// owns is gone, and the game can't go on.
|
||||
for (int attempt = 0; VirtualAlloc(address, size, MEM_COMMIT, PAGE_READWRITE) != address; attempt++)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
if (attempt == 0 || attempt == 40)
|
||||
fprintf(stderr, "[memory] recommitting %zu bytes at %p failed: error %lu%s\n", size, address, error,
|
||||
attempt == 0 ? " (trying again)" : "; giving up");
|
||||
if (attempt == 40)
|
||||
abort();
|
||||
Sleep(50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
// The POSIX calls platform/win32/posix.h declares, on Windows.
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
|
||||
int mprotect(void* address, size_t length, int prot)
|
||||
{
|
||||
DWORD protect = (prot & PROT_WRITE) ? PAGE_READWRITE : (prot & PROT_READ) ? PAGE_READONLY : PAGE_NOACCESS;
|
||||
DWORD old;
|
||||
if (VirtualProtect(address, length, protect, &old))
|
||||
return 0;
|
||||
// The write watch protects each mirror separately because a range that
|
||||
// crosses into another view fails (487, ERROR_INVALID_ADDRESS): a
|
||||
// failure here means a page stays as it was, so say so (a few times).
|
||||
static std::atomic<int> logged{ 0 };
|
||||
if (logged.fetch_add(1, std::memory_order_relaxed) < 20)
|
||||
fprintf(stderr, "[memory] VirtualProtect(%p, 0x%zX, %d) failed: error %lu\n", address, length, prot, GetLastError());
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
long sysconf(int name)
|
||||
{
|
||||
if (name != _SC_PAGESIZE)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
SYSTEM_INFO info;
|
||||
GetSystemInfo(&info);
|
||||
return long(info.dwPageSize);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
int64_t QpcFrequency()
|
||||
{
|
||||
static const int64_t frequency = [] {
|
||||
LARGE_INTEGER f;
|
||||
QueryPerformanceFrequency(&f);
|
||||
return int64_t(f.QuadPart);
|
||||
}();
|
||||
return frequency;
|
||||
}
|
||||
|
||||
int64_t NowNs()
|
||||
{
|
||||
LARGE_INTEGER t;
|
||||
QueryPerformanceCounter(&t);
|
||||
int64_t f = QpcFrequency();
|
||||
return int64_t(t.QuadPart / f) * 1000000000 + int64_t(t.QuadPart % f) * 1000000000 / f;
|
||||
}
|
||||
}
|
||||
|
||||
int nanosleep(const struct timespec* request, struct timespec* remaining)
|
||||
{
|
||||
if (!request || request->tv_nsec < 0 || request->tv_nsec >= 1000000000 || request->tv_sec < 0)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
int64_t ns = int64_t(request->tv_sec) * 1000000000 + request->tv_nsec;
|
||||
int64_t deadline = NowNs() + ns;
|
||||
if (ns >= 1000000)
|
||||
Sleep(DWORD(std::min<int64_t>(ns / 1000000, INFINITE - 1)));
|
||||
while (NowNs() < deadline)
|
||||
Sleep(0); // gives the core to a ready thread, if any
|
||||
if (remaining)
|
||||
*remaining = {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
int clock_gettime(int clock, struct timespec* time)
|
||||
{
|
||||
int64_t ns;
|
||||
if (clock == CLOCK_MONOTONIC)
|
||||
ns = NowNs();
|
||||
else if (clock == CLOCK_REALTIME)
|
||||
{
|
||||
FILETIME ft;
|
||||
GetSystemTimePreciseAsFileTime(&ft);
|
||||
constexpr int64_t kEpochDelta = 116444736000000000; // 1601 to 1970 in 100 ns
|
||||
ns = ((int64_t(ft.dwHighDateTime) << 32 | ft.dwLowDateTime) - kEpochDelta) * 100;
|
||||
}
|
||||
else if (clock == CLOCK_THREAD_CPUTIME_ID)
|
||||
{
|
||||
FILETIME created, exited, kernel, user;
|
||||
if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user))
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
ns = ((int64_t(kernel.dwHighDateTime) << 32 | kernel.dwLowDateTime) + (int64_t(user.dwHighDateTime) << 32 | user.dwLowDateTime)) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
time->tv_sec = time_t(ns / 1000000000);
|
||||
time->tv_nsec = long(ns % 1000000000);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Windows only (platform/os.h): <windows.h>, the C runtime's POSIX-named
|
||||
// headers (<io.h>: read, write, close, dup, dup2, isatty, fileno; <process.h>:
|
||||
// getpid), and the few POSIX calls the shared code makes that the C runtime
|
||||
// lacks, each a thin wrapper (platform/win32/os.cpp). Descriptors are the C
|
||||
// runtime's; platform/file_io.h opens files (binary, shared, wide paths).
|
||||
#pragma once
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
#include <direct.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <process.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
|
||||
using ssize_t = ptrdiff_t;
|
||||
using pid_t = int;
|
||||
|
||||
// open() flags. O_CLOEXEC: not inherited by child processes.
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC _O_NOINHERIT
|
||||
#endif
|
||||
|
||||
// No FIFOs in Windows' file systems, so nothing for O_NONBLOCK to guard.
|
||||
#ifndef O_NONBLOCK
|
||||
#define O_NONBLOCK 0
|
||||
#endif
|
||||
|
||||
// Quotas end a write as a full disk does.
|
||||
#ifndef EDQUOT
|
||||
#define EDQUOT ENOSPC
|
||||
#endif
|
||||
|
||||
#ifndef STDIN_FILENO
|
||||
#define STDIN_FILENO 0
|
||||
#define STDOUT_FILENO 1
|
||||
#define STDERR_FILENO 2
|
||||
#endif
|
||||
|
||||
#ifndef S_ISDIR
|
||||
#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR)
|
||||
#define S_ISREG(mode) (((mode) & _S_IFMT) == _S_IFREG)
|
||||
#endif
|
||||
|
||||
// Page protection: mprotect's values, applied with VirtualProtect. The
|
||||
// result is checked: a failure (a range that crosses a mapped view: error
|
||||
// 487) is logged, rate-limited, and returns -1 with errno EACCES.
|
||||
#define PROT_NONE 0
|
||||
#define PROT_READ 1
|
||||
#define PROT_WRITE 2
|
||||
int mprotect(void* address, size_t length, int prot);
|
||||
|
||||
// sysconf(_SC_PAGESIZE): the page size (4096 on x64).
|
||||
#define _SC_PAGESIZE 30
|
||||
long sysconf(int name);
|
||||
|
||||
// nanosleep: Sleep() for whole milliseconds; shorter waits spin with yields
|
||||
// (Sleep(0)), since even after timeBeginPeriod(1) a 100 us sleep takes
|
||||
// about 2 ms here (laptop.md, item d). Never interrupted: 0.
|
||||
int nanosleep(const struct timespec* request, struct timespec* remaining);
|
||||
|
||||
// clock_gettime: CLOCK_MONOTONIC (QueryPerformanceCounter), CLOCK_REALTIME
|
||||
// (the system time, 100 ns), CLOCK_THREAD_CPUTIME_ID (the thread's user and
|
||||
// kernel time, 100 ns units).
|
||||
#define CLOCK_REALTIME 0
|
||||
#define CLOCK_MONOTONIC 1
|
||||
#define CLOCK_THREAD_CPUTIME_ID 3
|
||||
int clock_gettime(int clock, struct timespec* time);
|
||||
|
||||
inline struct tm* localtime_r(const time_t* time, struct tm* out)
|
||||
{
|
||||
return localtime_s(out, time) == 0 ? out : nullptr;
|
||||
}
|
||||
|
||||
inline struct tm* gmtime_r(const time_t* time, struct tm* out)
|
||||
{
|
||||
return gmtime_s(out, time) == 0 ? out : nullptr;
|
||||
}
|
||||
|
||||
inline int strcasecmp(const char* a, const char* b)
|
||||
{
|
||||
return _stricmp(a, b);
|
||||
}
|
||||
|
||||
inline int strncasecmp(const char* a, const char* b, size_t n)
|
||||
{
|
||||
return _strnicmp(a, b, n);
|
||||
}
|
||||
|
||||
inline int setenv(const char* name, const char* value, int overwrite)
|
||||
{
|
||||
if (!overwrite && std::getenv(name))
|
||||
return 0;
|
||||
return _putenv_s(name, value) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
// FlushFileBuffers on the descriptor's handle.
|
||||
inline int fsync(int fd)
|
||||
{
|
||||
return _commit(fd);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!-- SpeedBreaker's application manifest (speedbreaker.rc). GPL-3.0-or-later (see COPYING).
|
||||
activeCodePage UTF-8: the narrow C runtime (argv, getenv, fopen,
|
||||
std::filesystem's string()) speaks UTF-8, as on macOS and Linux.
|
||||
PerMonitorV2: the window gets real pixels at every display scale.
|
||||
longPathAware: paths past 260 characters work (where the system allows).
|
||||
supportedOS: Windows 10 and 11 (one GUID for both). -->
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity type="win32" name="SpeedBreaker" version="1.0.0.0" processorArchitecture="amd64"/>
|
||||
<description>SpeedBreaker</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,38 @@
|
||||
// SpeedBreaker's Windows resources. GPL-3.0-or-later (see COPYING).
|
||||
// The icon (scripts/windows/make_ico.py), the version block, and the
|
||||
// manifest (linked with /MANIFEST:NO, so this is the only one).
|
||||
#include <winver.h>
|
||||
|
||||
1 ICON "speedbreaker.ico"
|
||||
|
||||
// CREATEPROCESS_MANIFEST_RESOURCE_ID (1), RT_MANIFEST (24).
|
||||
1 24 "speedbreaker.manifest"
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,0,0,0
|
||||
PRODUCTVERSION 0,0,0,0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "SpeedBreaker"
|
||||
VALUE "FileDescription", "SpeedBreaker"
|
||||
VALUE "FileVersion", "0.0.0.0"
|
||||
VALUE "InternalName", "SpeedBreaker"
|
||||
VALUE "LegalCopyright", "GPL-3.0-or-later"
|
||||
VALUE "OriginalFilename", "SpeedBreaker.exe"
|
||||
VALUE "ProductName", "SpeedBreaker"
|
||||
VALUE "ProductVersion", "0.0.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,472 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/stack.h.
|
||||
#include <platform/stack.h>
|
||||
|
||||
#include <dbghelp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
// Newer SDKs name it: no guessing that a folder in the path is a symbol store.
|
||||
#ifndef SYMOPT_DISABLE_SYMSRV_AUTODETECT
|
||||
#define SYMOPT_DISABLE_SYMSRV_AUTODETECT 0x02000000
|
||||
#endif
|
||||
|
||||
namespace platform::stack
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// How long anyone but the crash handler waits for DbgHelp (a crash
|
||||
// handler holding it never lets go).
|
||||
constexpr DWORD kWaitMs = 10000;
|
||||
|
||||
std::atomic<bool> s_ready{ false };
|
||||
// DbgHelp isn't to be used: it failed to start, faulted, or a crash
|
||||
// couldn't have it.
|
||||
std::atomic<bool> s_unusable{ false };
|
||||
std::atomic<bool> s_crashing{ false }; // EnterCrash was called
|
||||
char s_state[256] = "not loaded yet";
|
||||
|
||||
// `text` (a literal) as the state, copied a byte at a time: a crash
|
||||
// may call this.
|
||||
void SetState(const char* text)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (; text[i] && i + 1 < sizeof(s_state); i++)
|
||||
s_state[i] = text[i];
|
||||
s_state[i] = 0;
|
||||
}
|
||||
|
||||
// A fault inside DbgHelp: unusable from now on. Outside a crash the
|
||||
// state says so, for every later report's "names" line (a crash's
|
||||
// own report has printed its line already).
|
||||
void MarkFaulted()
|
||||
{
|
||||
s_unusable.store(true, std::memory_order_relaxed);
|
||||
if (!s_crashing.load(std::memory_order_relaxed))
|
||||
SetState("DbgHelp failed in an earlier stack walk: only recompiled functions are named");
|
||||
}
|
||||
|
||||
// DbgHelp's lock: the owning thread, and its depth (the owner's own).
|
||||
std::atomic<DWORD> s_owner{ 0 };
|
||||
int s_depth = 0;
|
||||
const Snapshot* s_walking = nullptr; // under the lock: Walk's copy of the stack
|
||||
|
||||
bool Lock(DWORD waitMs)
|
||||
{
|
||||
const DWORD self = GetCurrentThreadId();
|
||||
if (s_owner.load(std::memory_order_acquire) == self)
|
||||
{
|
||||
s_depth++;
|
||||
return true;
|
||||
}
|
||||
const ULONGLONG deadline = GetTickCount64() + waitMs;
|
||||
while (true)
|
||||
{
|
||||
DWORD expected = 0;
|
||||
if (s_owner.compare_exchange_strong(expected, self, std::memory_order_acquire))
|
||||
{
|
||||
s_depth = 1;
|
||||
return true;
|
||||
}
|
||||
if (GetTickCount64() >= deadline)
|
||||
return false;
|
||||
Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
void Unlock()
|
||||
{
|
||||
if (--s_depth == 0)
|
||||
s_owner.store(0, std::memory_order_release);
|
||||
}
|
||||
|
||||
// StackWalk64's memory: the copy of the stack where it has the bytes,
|
||||
// else the process's own memory (the code's unwind tables; in a crash,
|
||||
// the stack itself). ReadProcessMemory fails, rather than faults, on
|
||||
// an address that isn't readable.
|
||||
BOOL CALLBACK ReadMemory(HANDLE, DWORD64 address, PVOID buffer, DWORD size, LPDWORD read)
|
||||
{
|
||||
if (const Snapshot* s = s_walking; s && address >= s->low && address - s->low <= s->size && size <= s->size - (address - s->low))
|
||||
{
|
||||
memcpy(buffer, s->buffer + (address - s->low), size);
|
||||
*read = size;
|
||||
return TRUE;
|
||||
}
|
||||
SIZE_T got = 0;
|
||||
BOOL ok = ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<LPCVOID>(address), buffer, size, &got);
|
||||
*read = DWORD(got);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Unwind data from the loaded image itself: any module, whether or
|
||||
// not DbgHelp has loaded it.
|
||||
PVOID CALLBACK FunctionTable(HANDLE, DWORD64 pc)
|
||||
{
|
||||
DWORD64 base = 0;
|
||||
return RtlLookupFunctionEntry(pc, &base, nullptr);
|
||||
}
|
||||
|
||||
DWORD64 CALLBACK ModuleBase(HANDLE, DWORD64 pc)
|
||||
{
|
||||
DWORD64 base = 0;
|
||||
if (RtlLookupFunctionEntry(pc, &base, nullptr))
|
||||
return base;
|
||||
PVOID header = nullptr;
|
||||
return reinterpret_cast<DWORD64>(RtlPcToFileHeader(reinterpret_cast<PVOID>(pc), &header));
|
||||
}
|
||||
|
||||
// DbgHelp's calls, with a fault inside them caught (it may meet a heap
|
||||
// or a stack that the crash left broken): DbgHelp is unusable after.
|
||||
BOOL GuardedStackWalk(STACKFRAME64* frame, CONTEXT* context)
|
||||
{
|
||||
__try
|
||||
{
|
||||
return StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), frame, context, ReadMemory,
|
||||
FunctionTable, ModuleBase, nullptr);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
MarkFaulted();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
DWORD GuardedUnDecorate(const char* name, char* out, DWORD size)
|
||||
{
|
||||
__try
|
||||
{
|
||||
return UnDecorateSymbolName(name, out, size, UNDNAME_NAME_ONLY);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
MarkFaulted();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL GuardedSymFromAddr(DWORD64 address, DWORD64* displacement, SYMBOL_INFO* symbol)
|
||||
{
|
||||
__try
|
||||
{
|
||||
return SymFromAddr(GetCurrentProcess(), address, displacement, symbol);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
MarkFaulted();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
// One RtlVirtualUnwind step (Unwind: over a copy of the stack). A
|
||||
// read past the copy that faults ends the walk.
|
||||
bool GuardedUnwind(DWORD64 imageBase, PRUNTIME_FUNCTION function, CONTEXT* context)
|
||||
{
|
||||
__try
|
||||
{
|
||||
void* handlerData = nullptr;
|
||||
DWORD64 establisher = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context->Rip, function, context, &handlerData, &establisher, nullptr);
|
||||
return true;
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t ImageSize(HMODULE module)
|
||||
{
|
||||
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(module);
|
||||
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(reinterpret_cast<const uint8_t*>(module) + dos->e_lfanew);
|
||||
return nt->OptionalHeader.SizeOfImage;
|
||||
}
|
||||
|
||||
// Under the lock. A module DbgHelp doesn't have yet: its PDB if one is
|
||||
// in the search path (only ours is), else its exports.
|
||||
DWORD64 LoadModule(HMODULE module)
|
||||
{
|
||||
wchar_t path[2048];
|
||||
DWORD n = GetModuleFileNameW(module, path, DWORD(std::size(path)));
|
||||
if (!module || n == 0 || n >= std::size(path))
|
||||
return 0;
|
||||
return SymLoadModuleExW(GetCurrentProcess(), nullptr, path, nullptr, reinterpret_cast<DWORD64>(module), DWORD(ImageSize(module)),
|
||||
nullptr, 0);
|
||||
}
|
||||
|
||||
void Utf8(const wchar_t* wide, char* out, size_t size)
|
||||
{
|
||||
if (size == 0)
|
||||
return;
|
||||
out[0] = 0;
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, wide, -1, out, int(size), nullptr, nullptr) == 0)
|
||||
out[size - 1] = 0;
|
||||
}
|
||||
|
||||
const wchar_t* BaseName(const wchar_t* path)
|
||||
{
|
||||
const wchar_t* base = path;
|
||||
for (const wchar_t* p = path; *p; p++)
|
||||
if (*p == L'\\' || *p == L'/')
|
||||
base = p + 1;
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
void LoadSymbols(const void* const* warm, int warmCount)
|
||||
{
|
||||
if (!Lock(INFINITE))
|
||||
return;
|
||||
HANDLE process = GetCurrentProcess();
|
||||
wchar_t exe[2048];
|
||||
DWORD n = GetModuleFileNameW(nullptr, exe, DWORD(std::size(exe)));
|
||||
wchar_t folder[2048];
|
||||
if (n == 0 || n >= std::size(exe))
|
||||
exe[0] = 0;
|
||||
wcscpy_s(folder, exe);
|
||||
if (wchar_t* slash = wcsrchr(folder, L'\\'))
|
||||
*slash = 0;
|
||||
// Nothing but the .exe's folder searched (not the PDB path the .exe
|
||||
// recorded where it was built, nor a symbol server); no dialogs, ever.
|
||||
// Public symbols' names as they are (Name undecorates C++ ones):
|
||||
// SYMOPT_UNDNAME would also drop a C name's first underscore, as if
|
||||
// it were 32-bit cdecl's (__imp___xstart read _imp___xstart).
|
||||
SymSetOptions(SYMOPT_AUTO_PUBLICS | SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT_NO_PROMPTS | SYMOPT_IGNORE_CVREC |
|
||||
SYMOPT_DISABLE_SYMSRV_AUTODETECT);
|
||||
if (!exe[0] || !SymInitializeW(process, folder, FALSE))
|
||||
{
|
||||
snprintf(s_state, sizeof(s_state), "DbgHelp didn't start (error %lu): only recompiled functions are named", GetLastError());
|
||||
s_unusable.store(true);
|
||||
s_ready.store(true, std::memory_order_release);
|
||||
Unlock();
|
||||
return;
|
||||
}
|
||||
HMODULE self = GetModuleHandleW(nullptr);
|
||||
DWORD64 base = LoadModule(self);
|
||||
IMAGEHLP_MODULEW64 info{};
|
||||
info.SizeOfStruct = sizeof(info);
|
||||
char exeName[256], pdbName[256];
|
||||
Utf8(BaseName(exe), exeName, sizeof(exeName));
|
||||
if (base && SymGetModuleInfoW64(process, base, &info) && info.SymType == SymPdb)
|
||||
{
|
||||
Utf8(BaseName(info.LoadedPdbName), pdbName, sizeof(pdbName));
|
||||
snprintf(s_state, sizeof(s_state), "%s", pdbName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The PDB the .exe asks for, by name (else the .exe's own name
|
||||
// with .pdb): missing, or another build's.
|
||||
wchar_t stem[512];
|
||||
wcscpy_s(stem, BaseName(exe));
|
||||
if (wchar_t* dot = wcsrchr(stem, L'.'))
|
||||
*dot = 0;
|
||||
wchar_t wanted[2048];
|
||||
if (info.CVData[0])
|
||||
swprintf(wanted, std::size(wanted), L"%ls\\%ls", folder, BaseName(info.CVData));
|
||||
else
|
||||
swprintf(wanted, std::size(wanted), L"%ls\\%ls.pdb", folder, stem);
|
||||
Utf8(BaseName(wanted), pdbName, sizeof(pdbName));
|
||||
if (GetFileAttributesW(wanted) == INVALID_FILE_ATTRIBUTES)
|
||||
snprintf(s_state, sizeof(s_state), "no %s next to %s: only recompiled functions are named", pdbName, exeName);
|
||||
else
|
||||
snprintf(s_state, sizeof(s_state), "%s next to %s is another build's: only recompiled functions are named", pdbName, exeName);
|
||||
}
|
||||
// The system's own frames (a thread waiting, the threads' start).
|
||||
for (const wchar_t* dll : { L"ntdll.dll", L"kernelbase.dll", L"kernel32.dll" })
|
||||
if (HMODULE module = GetModuleHandleW(dll))
|
||||
LoadModule(module);
|
||||
// The first lookups read the PDB's tables: now, not in a crash.
|
||||
for (int i = 0; i < warmCount; i++)
|
||||
if (warm && warm[i])
|
||||
{
|
||||
alignas(SYMBOL_INFO) char storage[sizeof(SYMBOL_INFO) + 64];
|
||||
auto* symbol = reinterpret_cast<SYMBOL_INFO*>(storage);
|
||||
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbol->MaxNameLen = 64;
|
||||
DWORD64 displacement = 0;
|
||||
GuardedSymFromAddr(reinterpret_cast<DWORD64>(warm[i]), &displacement, symbol);
|
||||
}
|
||||
s_ready.store(true, std::memory_order_release);
|
||||
Unlock();
|
||||
}
|
||||
|
||||
bool SymbolsReady()
|
||||
{
|
||||
return s_ready.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
const char* SymbolsState()
|
||||
{
|
||||
return s_ready.load(std::memory_order_acquire) ? s_state : "DbgHelp hadn't loaded the symbols yet: only recompiled functions are named";
|
||||
}
|
||||
|
||||
bool Capture(HANDLE thread, uintptr_t stackHigh, Snapshot& snapshot)
|
||||
{
|
||||
snapshot.low = 0;
|
||||
snapshot.size = 0;
|
||||
if (SuspendThread(thread) == DWORD(-1))
|
||||
return false;
|
||||
// Until ResumeThread: nothing that allocates or takes a lock (the
|
||||
// thread may hold the heap's). GetThreadContext also waits for the
|
||||
// suspension to take effect; ReadProcessMemory is a plain system
|
||||
// call that fails on memory it can't read.
|
||||
snapshot.context.ContextFlags = CONTEXT_FULL;
|
||||
const bool ok = GetThreadContext(thread, &snapshot.context) != 0;
|
||||
if (ok && snapshot.buffer && snapshot.capacity && snapshot.context.Rsp < stackHigh)
|
||||
{
|
||||
const uintptr_t sp = snapshot.context.Rsp;
|
||||
SIZE_T got = 0;
|
||||
ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<LPCVOID>(sp), snapshot.buffer,
|
||||
std::min<size_t>(stackHigh - sp, snapshot.capacity), &got); // a part copied counts
|
||||
snapshot.low = sp;
|
||||
snapshot.size = got;
|
||||
}
|
||||
ResumeThread(thread);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int Walk(const CONTEXT& context, void** frames, int max, const Snapshot* snapshot)
|
||||
{
|
||||
if (max <= 0 || !s_ready.load(std::memory_order_acquire) || s_unusable.load(std::memory_order_relaxed))
|
||||
return 0;
|
||||
if (!Lock(s_crashing.load(std::memory_order_relaxed) ? 0 : kWaitMs))
|
||||
return 0;
|
||||
// One walk at a time (the lock): static, so a stack overflow's crash
|
||||
// report doesn't need 1.3 KB more of the stack it ran out of.
|
||||
static CONTEXT c;
|
||||
static STACKFRAME64 frame;
|
||||
c = context;
|
||||
frame = {};
|
||||
frame.AddrPC.Offset = c.Rip;
|
||||
frame.AddrPC.Mode = AddrModeFlat;
|
||||
frame.AddrStack.Offset = c.Rsp;
|
||||
frame.AddrStack.Mode = AddrModeFlat;
|
||||
frame.AddrFrame.Offset = c.Rbp;
|
||||
frame.AddrFrame.Mode = AddrModeFlat;
|
||||
s_walking = snapshot;
|
||||
int n = 0;
|
||||
DWORD64 lastSp = 0, lastPc = 0;
|
||||
while (n < max && !s_unusable.load(std::memory_order_relaxed) && GuardedStackWalk(&frame, &c))
|
||||
{
|
||||
const DWORD64 pc = frame.AddrPC.Offset, sp = frame.AddrStack.Offset;
|
||||
// The end, or a stack that runs backwards or in place (broken).
|
||||
if (pc == 0 || (n > 0 && (sp < lastSp || (sp == lastSp && pc == lastPc))))
|
||||
break;
|
||||
frames[n++] = reinterpret_cast<void*>(pc);
|
||||
lastSp = sp;
|
||||
lastPc = pc;
|
||||
}
|
||||
s_walking = nullptr;
|
||||
Unlock();
|
||||
return n;
|
||||
}
|
||||
|
||||
int Unwind(const Snapshot& snapshot, void** frames, int max)
|
||||
{
|
||||
if (max <= 0)
|
||||
return 0;
|
||||
// The copied part of the stack at its own addresses, and where the
|
||||
// copy is. Every register that points into that part (rsp, and a
|
||||
// frame pointer) is moved into the copy for each step, and back.
|
||||
const uintptr_t low = snapshot.low, high = snapshot.low + snapshot.size;
|
||||
const uintptr_t copy = reinterpret_cast<uintptr_t>(snapshot.buffer);
|
||||
CONTEXT c = snapshot.context;
|
||||
DWORD64* const registers[] = { &c.Rax, &c.Rcx, &c.Rdx, &c.Rbx, &c.Rsp, &c.Rbp, &c.Rsi, &c.Rdi, &c.R8, &c.R9, &c.R10,
|
||||
&c.R11, &c.R12, &c.R13, &c.R14, &c.R15 };
|
||||
int n = 0;
|
||||
while (n < max && c.Rip)
|
||||
{
|
||||
frames[n++] = reinterpret_cast<void*>(c.Rip);
|
||||
const DWORD64 sp = c.Rsp;
|
||||
if (!snapshot.buffer || sp < low || sp + 8 > high)
|
||||
break;
|
||||
DWORD64 imageBase = 0;
|
||||
if (PRUNTIME_FUNCTION function = RtlLookupFunctionEntry(c.Rip, &imageBase, nullptr))
|
||||
{
|
||||
for (DWORD64* r : registers)
|
||||
if (*r >= low && *r < high)
|
||||
*r = *r - low + copy;
|
||||
const bool ok = GuardedUnwind(imageBase, function, &c);
|
||||
for (DWORD64* r : registers)
|
||||
if (*r >= copy && *r < copy + snapshot.size)
|
||||
*r = *r - copy + low;
|
||||
if (!ok)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A leaf function (no unwind data): its return address is at rsp.
|
||||
memcpy(&c.Rip, snapshot.buffer + (sp - low), sizeof(c.Rip));
|
||||
c.Rsp = sp + 8;
|
||||
}
|
||||
// A stack that runs backwards or in place (broken), or a return
|
||||
// address read from beyond the copy.
|
||||
if (c.Rsp <= sp || c.Rsp > high)
|
||||
break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool Name(const void* pc, char* name, size_t size, uintptr_t& start, uint32_t& length)
|
||||
{
|
||||
if (size == 0 || !s_ready.load(std::memory_order_acquire) || s_unusable.load(std::memory_order_relaxed))
|
||||
return false;
|
||||
const bool crashing = s_crashing.load(std::memory_order_relaxed);
|
||||
if (!Lock(crashing ? 0 : kWaitMs))
|
||||
return false;
|
||||
const DWORD64 address = reinterpret_cast<DWORD64>(pc);
|
||||
if (!crashing && SymGetModuleBase64(GetCurrentProcess(), address) == 0)
|
||||
{
|
||||
HMODULE module = nullptr;
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
static_cast<LPCWSTR>(pc), &module))
|
||||
LoadModule(module);
|
||||
}
|
||||
// (Static under the lock, as in Walk.)
|
||||
alignas(SYMBOL_INFO) static char storage[sizeof(SYMBOL_INFO) + 512];
|
||||
auto* symbol = reinterpret_cast<SYMBOL_INFO*>(storage);
|
||||
memset(storage, 0, sizeof(storage));
|
||||
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbol->MaxNameLen = 512;
|
||||
DWORD64 displacement = 0;
|
||||
const bool found = GuardedSymFromAddr(address, &displacement, symbol) && symbol->Name[0];
|
||||
if (found)
|
||||
{
|
||||
// A decorated C++ name (a public symbol: no debug information, as
|
||||
// the recompiled code and the static libraries have) undecorated:
|
||||
// ?sub_82441190@@YAXAEIAUPPCContext@@PEAE@Z is sub_82441190.
|
||||
// Debug information's own names come undecorated already.
|
||||
static char undecorated[512];
|
||||
const char* shown = symbol->Name;
|
||||
if (shown[0] == '?' && GuardedUnDecorate(shown, undecorated, sizeof(undecorated)))
|
||||
shown = undecorated;
|
||||
size_t n = std::min<size_t>(strnlen(shown, 511), size - 1);
|
||||
memcpy(name, shown, n);
|
||||
name[n] = 0;
|
||||
start = uintptr_t(symbol->Address);
|
||||
length = symbol->Size;
|
||||
}
|
||||
Unlock();
|
||||
return found;
|
||||
}
|
||||
|
||||
bool EnterCrash(unsigned waitMs)
|
||||
{
|
||||
s_crashing.store(true);
|
||||
// Unusable before the crash: the state says why (it didn't start, or
|
||||
// failed in an earlier walk).
|
||||
if (s_unusable.load())
|
||||
return false;
|
||||
// A crash inside DbgHelp: it can't be trusted, nor its lock waited for.
|
||||
if (s_owner.load(std::memory_order_acquire) == GetCurrentThreadId() || !Lock(waitMs))
|
||||
{
|
||||
s_unusable.store(true);
|
||||
SetState("DbgHelp unavailable in this crash: only recompiled functions are named");
|
||||
return false;
|
||||
}
|
||||
// Kept until the process ends.
|
||||
return !s_unusable.load();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/startup.h.
|
||||
#include <platform/startup.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <io.h>
|
||||
|
||||
// Read by NVIDIA's (Optimus) and AMD's (PowerXpress) drivers from the exe
|
||||
// when it starts: run on the discrete GPU (platform/startup.h).
|
||||
extern "C"
|
||||
{
|
||||
__declspec(dllexport) DWORD NvOptimusEnablement = 1;
|
||||
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
UINT s_terminalCodePage = 0;
|
||||
const char* s_streamsNote = "";
|
||||
|
||||
// Standard streams without a handle (the Windows subsystem, started
|
||||
// without a console or redirects): see platform/startup.h.
|
||||
void BindStandardStreams()
|
||||
{
|
||||
if (_fileno(stdin) >= 0 && _fileno(stdout) >= 0 && _fileno(stderr) >= 0)
|
||||
return;
|
||||
const bool console = AttachConsole(ATTACH_PARENT_PROCESS) != 0;
|
||||
struct Standard
|
||||
{
|
||||
FILE* stream;
|
||||
int fd;
|
||||
DWORD id;
|
||||
const char* mode;
|
||||
const char* device;
|
||||
};
|
||||
const Standard streams[] = {
|
||||
{ stdin, 0, STD_INPUT_HANDLE, "r", "NUL" },
|
||||
{ stdout, 1, STD_OUTPUT_HANDLE, "w", console ? "CONOUT$" : "NUL" },
|
||||
{ stderr, 2, STD_ERROR_HANDLE, "w", console ? "CONOUT$" : "NUL" },
|
||||
};
|
||||
bool moved = false;
|
||||
for (const Standard& s : streams)
|
||||
{
|
||||
if (_fileno(s.stream) >= 0)
|
||||
continue;
|
||||
// The placeholder holds the number: free it, so the reopen
|
||||
// (which takes the lowest free one) lands on it. fd 2 must
|
||||
// be stderr's, as the log's tee replaces fd 2.
|
||||
_close(s.fd);
|
||||
FILE* reopened = nullptr;
|
||||
if (freopen_s(&reopened, s.device, s.mode, s.stream) != 0)
|
||||
freopen_s(&reopened, "NUL", s.mode, s.stream);
|
||||
if (_fileno(s.stream) == s.fd)
|
||||
SetStdHandle(s.id, reinterpret_cast<HANDLE>(_get_osfhandle(s.fd)));
|
||||
else
|
||||
moved = true;
|
||||
}
|
||||
s_streamsNote = moved ? "no console, and stdout or stderr couldn't be reopened on its own number: lines may be "
|
||||
"missing from this log"
|
||||
: console ? "no console of its own (the Windows subsystem): output also goes to the console of the "
|
||||
"terminal it was started from"
|
||||
: "no console (the Windows subsystem, started from Explorer or a shortcut): output goes to "
|
||||
"this log only";
|
||||
}
|
||||
|
||||
void RestoreConsoleCodePage()
|
||||
{
|
||||
SetConsoleOutputCP(s_terminalCodePage);
|
||||
}
|
||||
|
||||
// The C runtime's report of a call it refused. Written straight to
|
||||
// the stderr handle (the log, once it runs), not through stdio: the
|
||||
// refused call may be stdio's own. Return addresses in the program
|
||||
// are offsets into it (exe+...), for its .map or .pdb; the C runtime
|
||||
// is linked in (/MT), so the first few are its own.
|
||||
void OnInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, uintptr_t)
|
||||
{
|
||||
static std::atomic<int> s_reported{ 0 };
|
||||
if (s_reported.fetch_add(1, std::memory_order_relaxed) >= 10)
|
||||
return;
|
||||
void* frames[8];
|
||||
USHORT count = RtlCaptureStackBackTrace(1, 8, frames, nullptr);
|
||||
HMODULE exe = GetModuleHandleW(nullptr);
|
||||
char line[512];
|
||||
int size = snprintf(line, sizeof(line), "[crt] the C runtime refused a call (an invalid argument); it failed "
|
||||
"with an error, as on POSIX. Called from");
|
||||
for (USHORT i = 0; i < count && size > 0 && size < int(sizeof(line)) - 24; i++)
|
||||
{
|
||||
HMODULE module = nullptr;
|
||||
bool inExe = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
static_cast<LPCWSTR>(frames[i]), &module) && module == exe;
|
||||
size += inExe ? snprintf(line + size, sizeof(line) - size, " exe+0x%llX", static_cast<unsigned long long>(
|
||||
reinterpret_cast<uintptr_t>(frames[i]) - reinterpret_cast<uintptr_t>(exe)))
|
||||
: snprintf(line + size, sizeof(line) - size, " %p", frames[i]);
|
||||
}
|
||||
if (size > 0 && size < int(sizeof(line)) - 1)
|
||||
{
|
||||
line[size++] = '\n';
|
||||
DWORD written = 0;
|
||||
WriteFile(GetStdHandle(STD_ERROR_HANDLE), line, DWORD(size), &written, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareProcess()
|
||||
{
|
||||
SetErrorMode(GetErrorMode() | SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
|
||||
_set_invalid_parameter_handler(OnInvalidParameter);
|
||||
BindStandardStreams();
|
||||
|
||||
UINT codePage = GetConsoleOutputCP();
|
||||
if (codePage == 0)
|
||||
return; // no console (the release build, started from Explorer)
|
||||
// Only this process in the console: a window of its own.
|
||||
DWORD processes[2];
|
||||
bool ownConsole = GetConsoleProcessList(processes, 2) == 1;
|
||||
if (codePage != CP_UTF8 && SetConsoleOutputCP(CP_UTF8) && !ownConsole)
|
||||
{
|
||||
s_terminalCodePage = codePage;
|
||||
atexit(RestoreConsoleCodePage);
|
||||
}
|
||||
HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
|
||||
DWORD mode = 0;
|
||||
if (ownConsole && input && input != INVALID_HANDLE_VALUE && GetConsoleMode(input, &mode) && (mode & ENABLE_QUICK_EDIT_MODE))
|
||||
SetConsoleMode(input, (mode & ~DWORD(ENABLE_QUICK_EDIT_MODE)) | ENABLE_EXTENDED_FLAGS);
|
||||
}
|
||||
|
||||
const char* StandardStreamsNote()
|
||||
{
|
||||
return s_streamsNote;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/thread.h.
|
||||
#include <platform/thread.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <process.h>
|
||||
|
||||
#include <iterator>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Start
|
||||
{
|
||||
void* (*fn)(void*);
|
||||
void* arg;
|
||||
};
|
||||
|
||||
unsigned __stdcall Entry(void* p)
|
||||
{
|
||||
Start start = *static_cast<Start*>(p);
|
||||
delete static_cast<Start*>(p);
|
||||
start.fn(start.arg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
using SetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PCWSTR);
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
int StartThread(Thread* thread, size_t stackSize, void* (*fn)(void*), void* arg)
|
||||
{
|
||||
auto* start = new Start{ fn, arg };
|
||||
// _beginthreadex, not CreateThread: the C runtime's per-thread state
|
||||
// is set up (and freed) with the thread.
|
||||
uintptr_t handle = _beginthreadex(nullptr, unsigned(stackSize), Entry, start, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr);
|
||||
if (handle == 0)
|
||||
{
|
||||
int error = int(GetLastError());
|
||||
delete start;
|
||||
return error ? error : -1;
|
||||
}
|
||||
*thread = reinterpret_cast<Thread>(handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DetachThread(Thread thread)
|
||||
{
|
||||
CloseHandle(thread);
|
||||
}
|
||||
|
||||
void JoinThread(Thread thread)
|
||||
{
|
||||
WaitForSingleObject(thread, INFINITE);
|
||||
CloseHandle(thread);
|
||||
}
|
||||
|
||||
void NameCurrentThread(const char* name)
|
||||
{
|
||||
// Windows 10 1607 and newer: looked up, so older ones still start.
|
||||
static const auto setDescription = reinterpret_cast<SetThreadDescriptionFn>(
|
||||
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetThreadDescription"));
|
||||
if (!setDescription)
|
||||
return;
|
||||
wchar_t wide[64];
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, name, -1, wide, int(std::size(wide))) > 0)
|
||||
setDescription(GetCurrentThread(), wide);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See platform/timer.h.
|
||||
#include <platform/timer.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <timeapi.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
|
||||
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
|
||||
#endif
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
int64_t SteadyNs()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
}
|
||||
|
||||
void SleepUntil(int64_t steadyNs)
|
||||
{
|
||||
// One timer per thread that waits (the main thread's holds and the
|
||||
// audio thread's ticks), made at its first wait; INVALID: the system can't make one.
|
||||
thread_local HANDLE timer = [] {
|
||||
HANDLE h = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!h)
|
||||
fprintf(stderr, "[timer] no high-resolution waitable timer (error %lu): holds sleep to the 1 ms tick\n", GetLastError());
|
||||
return h ? h : INVALID_HANDLE_VALUE;
|
||||
}();
|
||||
constexpr int64_t kYieldNs = 50'000; // the last stretch, yielded (the timer may wake a little early on the QPC clock)
|
||||
for (int64_t left = steadyNs - SteadyNs(); left > 0; left = steadyNs - SteadyNs())
|
||||
{
|
||||
if (left <= kYieldNs)
|
||||
std::this_thread::yield();
|
||||
else if (timer == INVALID_HANDLE_VALUE)
|
||||
std::this_thread::sleep_until(std::chrono::steady_clock::time_point(std::chrono::nanoseconds(steadyNs)));
|
||||
else
|
||||
{
|
||||
LARGE_INTEGER due;
|
||||
due.QuadPart = -((left - kYieldNs / 2) / 100); // relative, in 100 ns units
|
||||
if (!SetWaitableTimerEx(timer, &due, 0, nullptr, nullptr, nullptr, 0) ||
|
||||
WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0)
|
||||
std::this_thread::sleep_until(std::chrono::steady_clock::time_point(std::chrono::nanoseconds(steadyNs)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* RaiseAudioThreadPriority()
|
||||
{
|
||||
// avrt.dll loaded, not linked (the exe needn't import it).
|
||||
if (HMODULE avrt = LoadLibraryW(L"avrt.dll"))
|
||||
{
|
||||
using SetFn = HANDLE(WINAPI*)(LPCWSTR, LPDWORD);
|
||||
if (auto set = reinterpret_cast<SetFn>(reinterpret_cast<void*>(GetProcAddress(avrt, "AvSetMmThreadCharacteristicsW"))))
|
||||
{
|
||||
DWORD task = 0;
|
||||
if (set(L"Pro Audio", &task))
|
||||
return "MMCSS Pro Audio";
|
||||
task = 0;
|
||||
if (set(L"Audio", &task))
|
||||
return "MMCSS Audio";
|
||||
}
|
||||
}
|
||||
if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST))
|
||||
return "thread priority highest (no MMCSS)";
|
||||
return "normal thread priority (no MMCSS)";
|
||||
}
|
||||
|
||||
void RaiseTimerResolution()
|
||||
{
|
||||
// Honoured even in a windowless process over SSH (the probe measured
|
||||
// the global resolution at 1.0 ms afterwards).
|
||||
if (timeBeginPeriod(1) != TIMERR_NOERROR)
|
||||
fprintf(stderr, "[timer] timeBeginPeriod(1) failed: sleeps are rounded to the 15.6 ms tick\n");
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,16 @@
|
||||
// Desktop Mode's file manager shows it), else the user data folder; on iOS
|
||||
// to the app's Documents, which the Files app shows (On My iPhone/iPad >
|
||||
// SpeedBreaker). NFSMW_BUG_REPORT_DIR=<dir> puts it there instead (tests).
|
||||
// Its name carries the version: speedbreaker-bug-report-v0.1.0-<date>_<time>.zip.
|
||||
// On Windows the Desktop is the shell's (FOLDERID_Desktop, which follows
|
||||
// OneDrive's folder backup); NFSMW_TEST_DESKTOP=<dir> stands in for it in
|
||||
// tests, where the real one can't be swapped for a sandbox's without
|
||||
// changing the registry. Its name carries the version:
|
||||
// speedbreaker-bug-report-v0.1.0-<date>_<time>.zip (a labelled build's, its
|
||||
// label and commit after that).
|
||||
#include <stdafx.h>
|
||||
#include "report.h"
|
||||
#include "internal.h"
|
||||
#include "scrub.h"
|
||||
#include "zip.h"
|
||||
|
||||
#include <cpu/guest_thread.h>
|
||||
@@ -25,7 +31,13 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <platform/folders.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
|
||||
namespace report
|
||||
{
|
||||
@@ -44,6 +56,17 @@ namespace report
|
||||
|
||||
std::filesystem::path DesktopFolder()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// The shell's Desktop (it follows OneDrive's folder backup), if
|
||||
// new files can go there (Controlled Folder Access may say no).
|
||||
const char* test = std::getenv("NFSMW_TEST_DESKTOP");
|
||||
std::filesystem::path desktop = test && *test ? std::filesystem::path(test)
|
||||
: platform::GetKnownFolder(platform::KnownFolder::Desktop);
|
||||
std::error_code ec;
|
||||
if (!desktop.empty() && std::filesystem::is_directory(desktop, ec) && platform::CheckWritableFolder(desktop, platform::NewEntry::File) == 0)
|
||||
return desktop;
|
||||
return {};
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home || !*home)
|
||||
return {};
|
||||
@@ -69,6 +92,7 @@ namespace report
|
||||
if (std::filesystem::is_directory(desktop, ec) && access(desktop.c_str(), W_OK) == 0)
|
||||
return desktop;
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
// iOS: the app's Documents folder (HOME/Documents), the one the
|
||||
@@ -123,8 +147,8 @@ namespace report
|
||||
|
||||
time_t ModifiedTime(const std::filesystem::path& path)
|
||||
{
|
||||
struct stat st{};
|
||||
return stat(path.c_str(), &st) == 0 ? st.st_mtime : time(nullptr);
|
||||
platform::FileInfo st;
|
||||
return platform::InfoOf(path, st) == 0 ? time_t(st.modifiedSeconds) : time(nullptr);
|
||||
}
|
||||
|
||||
// Halved (2x2 box filter) while wider than 2560: a 3440x1440 frame
|
||||
@@ -284,8 +308,13 @@ namespace report
|
||||
stamp, SessionName(), SessionSeconds(), BuildString());
|
||||
for (const std::string& note : notes)
|
||||
readme += " " + note + "\n";
|
||||
#ifdef _WIN32
|
||||
readme += "\nPaths in your user folder are shown as %USERPROFILE%. Nothing was sent anywhere: send\n"
|
||||
"this file with a few words on what you were doing and what went wrong.\n";
|
||||
#else
|
||||
readme += "\nPaths in your home folder are shown as ~. Nothing was sent anywhere: send this file\n"
|
||||
"with a few words on what you were doing and what went wrong.\n";
|
||||
#endif
|
||||
|
||||
ZipWriter zip;
|
||||
bool ok = true;
|
||||
@@ -309,7 +338,7 @@ namespace report
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(folder, ec);
|
||||
// write(2) rather than a stream, for an errno that says why.
|
||||
int fd = ::open(partial.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
int fd = platform::OpenFile(partial, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
if (fd < 0)
|
||||
return strerror(errno);
|
||||
int error = 0;
|
||||
@@ -323,8 +352,8 @@ namespace report
|
||||
}
|
||||
if (::close(fd) != 0 && !error)
|
||||
error = errno;
|
||||
if (!error && ::rename(partial.c_str(), path.c_str()) != 0)
|
||||
error = errno;
|
||||
if (!error)
|
||||
error = platform::AtomicReplace(partial, path);
|
||||
if (error)
|
||||
{
|
||||
std::filesystem::remove(partial, ec);
|
||||
@@ -333,13 +362,17 @@ namespace report
|
||||
return {};
|
||||
};
|
||||
std::filesystem::path folder = OutputFolder();
|
||||
std::filesystem::path path = folder / std::format("speedbreaker-bug-report-v{}-{}.zip", Version(), stamp);
|
||||
// A labelled build's reports say which build in their names too:
|
||||
// speedbreaker-bug-report-v0.1.3-<stamp>-windows-beta-2026-10-11-59d1ef3.zip.
|
||||
std::string tag = ReleaseFileTag();
|
||||
std::filesystem::path path = folder / std::format("speedbreaker-bug-report-v{}-{}{}.zip", Version(), stamp,
|
||||
tag.empty() ? "" : "-" + tag);
|
||||
std::string failed = saveTo(folder, path);
|
||||
// The Desktop refused it (full, or macOS's privacy prompt said
|
||||
// no): the data folder, where the logs are, is ours.
|
||||
if (!failed.empty() && !std::getenv("NFSMW_BUG_REPORT_DIR") && folder != GetUserPath())
|
||||
{
|
||||
fprintf(stderr, "[bugreport] couldn't save in %s (%s); trying the data folder\n", folder.c_str(), failed.c_str());
|
||||
fprintf(stderr, "[bugreport] couldn't save in %s (%s); trying the data folder\n", folder.string().c_str(), failed.c_str());
|
||||
std::filesystem::path fallback = GetUserPath();
|
||||
std::filesystem::path fallbackPath = fallback / path.filename();
|
||||
if (std::string again = saveTo(fallback, fallbackPath); again.empty())
|
||||
@@ -361,7 +394,7 @@ namespace report
|
||||
double seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
||||
std::string size = archive.size() < (1 << 20) ? std::format("{} KB", (archive.size() + 1023) >> 10)
|
||||
: std::format("{:.1f} MB", double(archive.size()) / (1 << 20));
|
||||
fprintf(stderr, "[bugreport] saved %s (%zu files, %s, %.1f s)\n", path.c_str(), zip.Entries(), size.c_str(), seconds);
|
||||
fprintf(stderr, "[bugreport] saved %s (%zu files, %s, %.1f s)\n", path.string().c_str(), zip.Entries(), size.c_str(), seconds);
|
||||
std::string where = Describe(path);
|
||||
SetMessage("Saved to " + where);
|
||||
s_state.store(BugReportState::Saved);
|
||||
@@ -404,40 +437,49 @@ namespace report
|
||||
|
||||
std::string ScrubHome(std::string text)
|
||||
{
|
||||
std::vector<std::string> homes;
|
||||
#ifdef _WIN32
|
||||
// The user's profile folder (C:\Users\<name>) as %USERPROFILE%, a
|
||||
// name Explorer's address bar takes: as the environment and the shell
|
||||
// spell it, and in its short 8.3 form (C:\Users\JOSEMA~1, which TEMP
|
||||
// can hold). Letters in either case, \ or / (or \\, escaped).
|
||||
std::vector<detail::FolderName> folders;
|
||||
auto utf8 = [](const std::wstring& wide) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), int(wide.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string out(size_t(std::max(n, 0)), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), int(wide.size()), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
};
|
||||
auto add = [&](std::wstring path) {
|
||||
while (path.size() > 3 && (path.back() == L'\\' || path.back() == L'/'))
|
||||
path.pop_back();
|
||||
if (path.size() <= 3) // never a whole drive
|
||||
return;
|
||||
folders.push_back({ utf8(path), "%USERPROFILE%" });
|
||||
wchar_t shortPath[1024];
|
||||
DWORD n = GetShortPathNameW(path.c_str(), shortPath, DWORD(std::size(shortPath)));
|
||||
if (n > 0 && n < std::size(shortPath) && _wcsicmp(shortPath, path.c_str()) != 0)
|
||||
folders.push_back({ utf8(std::wstring(shortPath, n)), "%USERPROFILE%" });
|
||||
};
|
||||
if (const wchar_t* profile = _wgetenv(L"USERPROFILE"); profile && *profile)
|
||||
add(profile);
|
||||
add(platform::GetKnownFolder(platform::KnownFolder::Profile).wstring());
|
||||
return detail::ReplaceFolders(std::move(text), std::move(folders), true);
|
||||
#else
|
||||
std::vector<detail::FolderName> homes;
|
||||
if (const char* h = std::getenv("HOME"); h && strlen(h) > 1)
|
||||
{
|
||||
std::string home = h;
|
||||
while (home.size() > 1 && home.back() == '/')
|
||||
home.pop_back();
|
||||
homes.push_back(home);
|
||||
homes.push_back({ home, "~" });
|
||||
std::error_code ec;
|
||||
std::string real = std::filesystem::weakly_canonical(home, ec).string();
|
||||
while (real.size() > 1 && real.back() == '/')
|
||||
real.pop_back();
|
||||
if (!ec && real.size() > 1 && real != home)
|
||||
homes.push_back(real);
|
||||
homes.push_back({ real, "~" });
|
||||
}
|
||||
std::sort(homes.begin(), homes.end(), [](const std::string& a, const std::string& b) { return a.size() > b.size(); });
|
||||
auto pathChar = [](char c) { return std::isalnum(uint8_t(c)) || c == '.' || c == '_' || c == '-'; };
|
||||
for (const std::string& home : homes)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
size_t from = 0;
|
||||
for (size_t at; (at = text.find(home, from)) != std::string::npos;)
|
||||
{
|
||||
size_t end = at + home.size();
|
||||
// Only the whole folder (/home/deck, not /home/deck2) and not
|
||||
// the tail of a longer path (/mnt/home/deck).
|
||||
bool whole = (end == text.size() || !pathChar(text[end])) && (at == 0 || !pathChar(text[at - 1]));
|
||||
out.append(text, from, at - from);
|
||||
out += whole ? "~" : home;
|
||||
from = end;
|
||||
}
|
||||
out.append(text, from, std::string::npos);
|
||||
text.swap(out);
|
||||
}
|
||||
return text;
|
||||
return detail::ReplaceFolders(std::move(text), std::move(homes), false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
# SpeedBreaker. GPL-3.0-or-later (see COPYING).
|
||||
#
|
||||
# cmake -DSOURCE_DIR=<repo> -DOUTPUT=<file> -DVERSION=<x.y.z> [-DBUILD_TYPE=Release] -P build_info.cmake
|
||||
# cmake -DSOURCE_DIR=<repo> -DOUTPUT=<file> -DVERSION=<x.y.z> [-DBUILD_TYPE=Release] [-DLABEL=<label>] -P build_info.cmake
|
||||
#
|
||||
# Writes the build's identity for report/build_info.cpp: the version (the
|
||||
# top-level project(), passed in), the git commit, whether tracked files
|
||||
# differ from it, and when it was built. It runs on
|
||||
# every build (runtime/CMakeLists.txt) but rewrites OUTPUT, which bumps the
|
||||
# build date and relinks, only when the identity changed (another commit,
|
||||
# another diff, another build type, another version): a no-op build stays a
|
||||
# no-op.
|
||||
# another diff, another build type, another version, another label): a no-op
|
||||
# build stays a no-op. LABEL (SB_BUILD_LABEL, e.g. "Windows beta") names a
|
||||
# build that goes to other people: "<label> <the build machine's local
|
||||
# date>", which the runtime prints after the version as "SpeedBreaker v0.1.3
|
||||
# Windows beta 2026-10-11 (59d1ef3)". Without it the file is as the release's.
|
||||
if(NOT VERSION)
|
||||
set(VERSION "0.0.0")
|
||||
endif()
|
||||
@@ -36,6 +39,9 @@ if(GIT_FOUND)
|
||||
endif()
|
||||
|
||||
set(identity "${VERSION} ${commit} ${dirty} ${diff_hash} ${BUILD_TYPE}")
|
||||
if(LABEL)
|
||||
set(identity "${identity} ${LABEL}")
|
||||
endif()
|
||||
if(EXISTS ${OUTPUT})
|
||||
file(STRINGS ${OUTPUT} previous REGEX "^// identity: ")
|
||||
if(previous STREQUAL "// identity: ${identity}")
|
||||
@@ -44,6 +50,11 @@ if(EXISTS ${OUTPUT})
|
||||
endif()
|
||||
|
||||
string(TIMESTAMP date "%Y-%m-%d %H:%M UTC" UTC)
|
||||
set(release "")
|
||||
if(LABEL)
|
||||
string(TIMESTAMP day "%Y-%m-%d")
|
||||
set(release "#define NFSMW_BUILD_RELEASE \"${LABEL} ${day}\"\n")
|
||||
endif()
|
||||
file(WRITE ${OUTPUT}
|
||||
"// Generated by runtime/report/build_info.cmake at build time. Do not edit.
|
||||
// identity: ${identity}
|
||||
@@ -52,4 +63,4 @@ file(WRITE ${OUTPUT}
|
||||
#define NFSMW_BUILD_DIRTY ${dirty}
|
||||
#define NFSMW_BUILD_DATE \"${date}\"
|
||||
#define NFSMW_BUILD_TYPE \"${BUILD_TYPE}\"
|
||||
")
|
||||
${release}")
|
||||
@@ -4,7 +4,8 @@
|
||||
// came from: the version (project() in the top-level CMakeLists.txt) and the
|
||||
// commit. build_info.inc is written at build time by build_info.cmake
|
||||
// (runtime/CMakeLists.txt); a build without it (a hand-run compiler) still
|
||||
// works and says "unknown".
|
||||
// works and says "unknown". A labelled build (SB_BUILD_LABEL: the Windows
|
||||
// beta) also defines NFSMW_BUILD_RELEASE, "Windows beta 2026-10-11".
|
||||
#include <stdafx.h>
|
||||
#include "report.h"
|
||||
|
||||
@@ -40,6 +41,46 @@ namespace report
|
||||
return id.c_str();
|
||||
}
|
||||
|
||||
#ifdef NFSMW_BUILD_RELEASE
|
||||
const char* ReleaseName()
|
||||
{
|
||||
static const std::string name = std::format("v{} {} ({})", Version(), NFSMW_BUILD_RELEASE, BuildId());
|
||||
return name.c_str();
|
||||
}
|
||||
|
||||
const char* ReleaseFileTag()
|
||||
{
|
||||
// "Windows beta 2026-10-11 (59d1ef3)" -> "windows-beta-2026-10-11-59d1ef3"
|
||||
// (the names it goes into carry the version already).
|
||||
static const std::string tag = [] {
|
||||
std::string text;
|
||||
const std::string name = std::format("{} ({})", NFSMW_BUILD_RELEASE, BuildId());
|
||||
for (const char* p = name.c_str(); *p; p++)
|
||||
{
|
||||
char c = (*p >= 'A' && *p <= 'Z') ? char(*p - 'A' + 'a') : *p;
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
|
||||
text += c;
|
||||
else if (!text.empty() && text.back() != '-')
|
||||
text += '-';
|
||||
}
|
||||
while (!text.empty() && text.back() == '-')
|
||||
text.pop_back();
|
||||
return text;
|
||||
}();
|
||||
return tag.c_str();
|
||||
}
|
||||
#else
|
||||
const char* ReleaseName()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* ReleaseFileTag()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* BuildString()
|
||||
{
|
||||
static const std::string text = [] {
|
||||
@@ -73,8 +114,14 @@ namespace report
|
||||
const char* arch = "an unknown CPU";
|
||||
#endif
|
||||
std::string type = NFSMW_BUILD_TYPE;
|
||||
#ifdef NFSMW_BUILD_RELEASE
|
||||
// The name the testers quote first (with the version), then the details.
|
||||
return std::format("{}: built {}, {}, {} {}{}{}", ReleaseName(), NFSMW_BUILD_DATE, compiler, os, arch,
|
||||
type.empty() ? "" : ", ", type);
|
||||
#else
|
||||
return std::format("{}, built {}, {}, {} {}{}{}", VersionString(), NFSMW_BUILD_DATE, compiler, os, arch,
|
||||
type.empty() ? "" : ", ", type);
|
||||
#endif
|
||||
}();
|
||||
return text.c_str();
|
||||
}
|
||||
|
||||
+298
-21
@@ -18,20 +18,40 @@
|
||||
// /proc/self/exe at startup (functions with sizes and names, ~5 MB) and the
|
||||
// handler looks addresses up there. A stripped build falls back to
|
||||
// PPCFuncMappings, which names the recompiled functions.
|
||||
//
|
||||
// Windows: the unhandled-exception filter (platform/win32/fault.cpp) calls
|
||||
// the handler on the crashing thread, not in a signal handler, but the same
|
||||
// care holds: nothing it calls allocates or takes the C runtime's locks
|
||||
// (WriteFile on handles opened at startup, a buffer of its own). The stack
|
||||
// is walked with StackWalk64 from the exception's registers and named from
|
||||
// SpeedBreaker.pdb by DbgHelp (platform/stack.h), loaded at startup on a
|
||||
// thread of its own; DbgHelp is the one exception, as dladdr is above (its
|
||||
// own heap, its faults caught). Without the PDB, or if DbgHelp is busy or
|
||||
// the crash broke it, PPCFuncMappings names the recompiled functions and the
|
||||
// RtlVirtualUnwind walk below gives the frames.
|
||||
#include <stdafx.h>
|
||||
#include "report.h"
|
||||
#include "internal.h"
|
||||
#include "crash_marker.h"
|
||||
|
||||
#include "symbol_names.h"
|
||||
|
||||
#include <gpu/command_processor.h>
|
||||
#include <ui/ui.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <cxxabi.h>
|
||||
#include <dlfcn.h>
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include <fstream>
|
||||
#ifdef _WIN32
|
||||
#include <platform/file_io.h>
|
||||
#include <platform/stack.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
@@ -163,12 +183,39 @@ namespace report
|
||||
std::atomic<int> s_reportFd{ -1 }; // -1 once the session has ended cleanly (ClearCrashMarker)
|
||||
char s_pendingPath[1024];
|
||||
char s_finalPath[1024];
|
||||
#ifdef _WIN32
|
||||
wchar_t s_pendingPathW[1024]; // the same, for MoveFileExW
|
||||
wchar_t s_finalPathW[1024];
|
||||
#endif
|
||||
char s_header[2048];
|
||||
size_t s_headerSize = 0;
|
||||
struct timespec s_started{};
|
||||
std::atomic<bool> s_crashing{ false };
|
||||
thread_local bool t_reporting = false; // this thread is writing the report
|
||||
|
||||
#ifdef _WIN32
|
||||
const char* ExceptionName(DWORD code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION (invalid memory access)";
|
||||
case EXCEPTION_IN_PAGE_ERROR: return "EXCEPTION_IN_PAGE_ERROR (a mapped file couldn't be read)";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION (illegal instruction)";
|
||||
case EXCEPTION_PRIV_INSTRUCTION: return "EXCEPTION_PRIV_INSTRUCTION (privileged instruction)";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO (arithmetic error)";
|
||||
case EXCEPTION_INT_OVERFLOW: return "EXCEPTION_INT_OVERFLOW (arithmetic error)";
|
||||
case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW (stack overflow)";
|
||||
case EXCEPTION_BREAKPOINT: return "EXCEPTION_BREAKPOINT (trap)";
|
||||
case 0x40000015: return "abort (a failed check)";
|
||||
// Only as a raised exception: a real __fastfail (a /GS stack
|
||||
// cookie, the C runtime's own) skips every handler, this one too,
|
||||
// and goes to Windows Error Reporting.
|
||||
case 0xC0000409: return "STATUS_STACK_BUFFER_OVERRUN (fail fast)";
|
||||
case 0xE06D7363: return "an uncaught C++ exception";
|
||||
default: return "an exception";
|
||||
}
|
||||
}
|
||||
#else
|
||||
const char* SignalName(int sig)
|
||||
{
|
||||
switch (sig)
|
||||
@@ -182,6 +229,7 @@ namespace report
|
||||
default: return "a signal";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// The target of an unconditional jump at `code`, or `code`.
|
||||
uintptr_t FollowJump(uintptr_t code)
|
||||
@@ -232,28 +280,15 @@ namespace report
|
||||
return true;
|
||||
}
|
||||
|
||||
// A recompiled function's own name for a symbol that is one: its body
|
||||
// (__imp__sub_825987E0) or its C++ wrapper or hook
|
||||
// (_Z12sub_82441190R10PPCContextPh, where the compiler put the body
|
||||
// inline). Async-signal-safe; `buffer` holds 13 bytes.
|
||||
const char* GuestName(const char* name, char* buffer)
|
||||
{
|
||||
if (strncmp(name, "__imp__sub_", 11) == 0)
|
||||
return name + 7;
|
||||
if (strncmp(name, "_Z12sub_", 8) == 0 && strncmp(name + 16, "R10PPCContextPh", 15) == 0)
|
||||
{
|
||||
memcpy(buffer, name + 4, 12);
|
||||
buffer[12] = 0;
|
||||
return buffer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* BaseName(const char* path)
|
||||
{
|
||||
const char* base = path;
|
||||
for (const char* p = path; p && *p; p++)
|
||||
#ifdef _WIN32
|
||||
if (*p == '/' || *p == '\\')
|
||||
#else
|
||||
if (*p == '/')
|
||||
#endif
|
||||
base = p + 1;
|
||||
return base;
|
||||
}
|
||||
@@ -306,6 +341,20 @@ namespace report
|
||||
s_symbols = new GuestSymbol[symbols.size()];
|
||||
std::copy(symbols.begin(), symbols.end(), s_symbols);
|
||||
s_symbolCount = symbols.size();
|
||||
#ifdef _WIN32
|
||||
// DbgHelp and SpeedBreaker.pdb, as Linux's .symtab: on a thread of
|
||||
// its own. Its first lookups, a runtime function and a recompiled
|
||||
// body, read the tables a crash would need.
|
||||
static const void* warm[2] = { reinterpret_cast<const void*>(&TestCrash), reinterpret_cast<const void*>(s_regionBegin) };
|
||||
std::thread([] {
|
||||
auto started = std::chrono::steady_clock::now();
|
||||
platform::stack::LoadSymbols(warm, 2);
|
||||
// The log says whether reports will have names (a tester's
|
||||
// folder without SpeedBreaker.pdb, another build's).
|
||||
fprintf(stderr, "[crash] names for crash and hang reports: %s (%.2f s)\n", platform::stack::SymbolsState(),
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count());
|
||||
}).detach();
|
||||
#endif
|
||||
}
|
||||
|
||||
void* ContextPc(void* context)
|
||||
@@ -316,6 +365,8 @@ namespace report
|
||||
return reinterpret_cast<void*>(static_cast<ucontext_t*>(context)->uc_mcontext.gregs[REG_RIP]);
|
||||
#elif defined(__linux__) && defined(__aarch64__)
|
||||
return reinterpret_cast<void*>(static_cast<ucontext_t*>(context)->uc_mcontext.pc);
|
||||
#elif defined(_WIN32) && defined(_M_X64)
|
||||
return reinterpret_cast<void*>(static_cast<CONTEXT*>(context)->Rip);
|
||||
#else
|
||||
(void)context;
|
||||
return nullptr;
|
||||
@@ -324,6 +375,13 @@ namespace report
|
||||
|
||||
int SignalFrames(void* const* frames, int count)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// No signal frames: Windows' stacks come from the exception's
|
||||
// own registers (WalkStack).
|
||||
(void)frames;
|
||||
(void)count;
|
||||
return 0;
|
||||
#else
|
||||
for (int i = 0; i < count && i < 6; i++)
|
||||
{
|
||||
Dl_info info{};
|
||||
@@ -340,6 +398,7 @@ namespace report
|
||||
#endif
|
||||
}
|
||||
return std::min(count, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FormatFrame(SafeBuf& out, const void* pc, bool returnAddress, bool demangle)
|
||||
@@ -347,6 +406,36 @@ namespace report
|
||||
const uintptr_t shown = reinterpret_cast<uintptr_t>(pc);
|
||||
const uintptr_t address = returnAddress && shown ? shown - 1 : shown;
|
||||
out.Str("0x").Hex(shown, 12).Str(" ");
|
||||
#ifdef _WIN32
|
||||
// SpeedBreaker.pdb's name (undecorated: platform::stack::Name), the
|
||||
// recompiled ones as sub_<address>; else the generated table's.
|
||||
// A public symbol, which has no size, that starts before the
|
||||
// recompiled function the table puts there is another function.
|
||||
(void)demangle;
|
||||
uint32_t guest, length = 0;
|
||||
uintptr_t start = 0, guestStart = 0;
|
||||
char name[256], guestName[13];
|
||||
const bool isGuest = FindGuest(address, guest, guestStart);
|
||||
const char* recompiled = nullptr;
|
||||
bool named = platform::stack::Name(reinterpret_cast<const void*>(address), name, sizeof(name), start, length);
|
||||
if (named)
|
||||
{
|
||||
recompiled = detail::GuestName(name, guestName);
|
||||
if (!recompiled && isGuest && length == 0 && guestStart > start)
|
||||
named = false;
|
||||
}
|
||||
if (named)
|
||||
out.Str(recompiled ? recompiled : name).Str("+0x").Hex(shown - start);
|
||||
else if (isGuest)
|
||||
out.Str("sub_").Hex(guest, 8, true).Str("+0x").Hex(shown - guestStart);
|
||||
else
|
||||
out.Str("?");
|
||||
HMODULE module = nullptr;
|
||||
char file[MAX_PATH];
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCSTR>(address), &module) && GetModuleFileNameA(module, file, sizeof(file)))
|
||||
out.Str(" (").Str(BaseName(file)).Str("+0x").Hex(shown - reinterpret_cast<uintptr_t>(module)).Char(')');
|
||||
#else
|
||||
Dl_info info{};
|
||||
bool found = dladdr(reinterpret_cast<const void*>(address), &info) != 0;
|
||||
uint32_t guest;
|
||||
@@ -358,7 +447,7 @@ namespace report
|
||||
char* readable = nullptr;
|
||||
int status = -1;
|
||||
char guestName[13];
|
||||
if (const char* recompiled = GuestName(name, guestName))
|
||||
if (const char* recompiled = detail::GuestName(name, guestName))
|
||||
name = recompiled;
|
||||
else if (demangle && (readable = abi::__cxa_demangle(name, nullptr, nullptr, &status)) && status == 0)
|
||||
name = readable;
|
||||
@@ -381,7 +470,7 @@ namespace report
|
||||
int status = -1;
|
||||
char guestName[13];
|
||||
const char* name = info.dli_sname;
|
||||
if (const char* recompiled = GuestName(name, guestName))
|
||||
if (const char* recompiled = detail::GuestName(name, guestName))
|
||||
name = recompiled; // one the table didn't name
|
||||
else if (demangle && (readable = abi::__cxa_demangle(name, nullptr, nullptr, &status)) && status == 0)
|
||||
name = readable;
|
||||
@@ -393,14 +482,17 @@ namespace report
|
||||
out.Str("?");
|
||||
if (found && info.dli_fname && info.dli_fbase)
|
||||
out.Str(" (").Str(BaseName(info.dli_fname)).Str("+0x").Hex(shown - reinterpret_cast<uintptr_t>(info.dli_fbase)).Char(')');
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareCrashReports()
|
||||
{
|
||||
#ifndef _WIN32
|
||||
// Loads glibc's unwinder now: the first backtrace() dlopens it.
|
||||
void* frames[4];
|
||||
backtrace(frames, 4);
|
||||
#endif
|
||||
detail::BuildSymbols();
|
||||
clock_gettime(CLOCK_MONOTONIC, &s_started);
|
||||
|
||||
@@ -413,10 +505,21 @@ namespace report
|
||||
{
|
||||
memcpy(s_pendingPath, pending.c_str(), pending.size() + 1);
|
||||
memcpy(s_finalPath, finalPath.c_str(), finalPath.size() + 1);
|
||||
#ifdef _WIN32
|
||||
std::wstring pendingW = (dir / (std::string(".crash-pending-") + SessionName())).wstring();
|
||||
std::wstring finalW = (dir / (std::string("crash-") + SessionName() + ".txt")).wstring();
|
||||
if (pendingW.size() < std::size(s_pendingPathW) && finalW.size() < std::size(s_finalPathW))
|
||||
{
|
||||
wmemcpy(s_pendingPathW, pendingW.c_str(), pendingW.size() + 1);
|
||||
wmemcpy(s_finalPathW, finalW.c_str(), finalW.size() + 1);
|
||||
s_reportFd = marker::Arm(s_pendingPathW);
|
||||
}
|
||||
#else
|
||||
s_reportFd = marker::Arm(s_pendingPath);
|
||||
#endif
|
||||
}
|
||||
if (s_reportFd < 0)
|
||||
fprintf(stderr, "[crash] can't prepare a crash report file in %s: %s\n", dir.c_str(), strerror(errno));
|
||||
fprintf(stderr, "[crash] can't prepare a crash report file in %s: %s\n", dir.string().c_str(), strerror(errno));
|
||||
|
||||
std::string header = std::format(
|
||||
"SpeedBreaker (NFS: Most Wanted, Xbox 360): crash report\n"
|
||||
@@ -427,6 +530,142 @@ namespace report
|
||||
memcpy(s_header, header.data(), s_headerSize);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace
|
||||
{
|
||||
// The stack from the exception's registers, a frame at a time
|
||||
// (RtlVirtualUnwind, from the unwind tables every x64 function has;
|
||||
// a leaf function has none, and its return address is at rsp).
|
||||
// Allocates nothing. Stops at the edge of this thread's stack.
|
||||
int WalkStack(CONTEXT context, void** frames, int max)
|
||||
{
|
||||
ULONG_PTR low = 0, high = 0;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
int n = 0;
|
||||
while (n < max && context.Rip)
|
||||
{
|
||||
frames[n++] = reinterpret_cast<void*>(context.Rip);
|
||||
DWORD64 imageBase = 0;
|
||||
if (PRUNTIME_FUNCTION function = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr))
|
||||
{
|
||||
void* handlerData = nullptr;
|
||||
DWORD64 establisher = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, function, &context, &handlerData, &establisher, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.Rsp < low || context.Rsp + 8 > high)
|
||||
break;
|
||||
context.Rip = *reinterpret_cast<const DWORD64*>(context.Rsp);
|
||||
context.Rsp += 8;
|
||||
}
|
||||
if (context.Rsp < low || context.Rsp >= high)
|
||||
break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteCrashReport(_EXCEPTION_POINTERS* info)
|
||||
{
|
||||
if (s_crashing.exchange(true))
|
||||
{
|
||||
// As on POSIX: another thread's report is being written; wait for
|
||||
// it to end the process (or, on the reporting thread, return).
|
||||
if (!t_reporting)
|
||||
Sleep(10000);
|
||||
return;
|
||||
}
|
||||
t_reporting = true;
|
||||
// What was written just before (an assert's message) reaches the tail.
|
||||
FlushLog();
|
||||
// DbgHelp for this thread from here on, if another one (its loader,
|
||||
// the watchdog naming a hang report's frames) lets go of it soon.
|
||||
const bool dbghelp = platform::stack::EnterCrash(2000);
|
||||
|
||||
static char text[24 * 1024];
|
||||
detail::SafeBuf b{ text, sizeof(text) - 1 };
|
||||
const EXCEPTION_RECORD* record = info->ExceptionRecord;
|
||||
const CONTEXT* context = info->ContextRecord;
|
||||
b.Str("\n[crash] ").Str(ExceptionName(record->ExceptionCode)).Str(", exception 0x").Hex(record->ExceptionCode, 8, true);
|
||||
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION || record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
|
||||
record->NumberParameters >= 2)
|
||||
{
|
||||
auto* fault = reinterpret_cast<uint8_t*>(record->ExceptionInformation[1]);
|
||||
ULONG_PTR kind = record->ExceptionInformation[0];
|
||||
b.Str(kind == 1 ? " (a write)" : kind == 8 ? " (an execution)" : " (a read)");
|
||||
if (g_memory.IsInMemoryRange(fault))
|
||||
b.Str(" at guest address ").Hex(uint32_t(fault - g_memory.base), 8, true);
|
||||
else
|
||||
b.Str(" at host address 0x").Hex(reinterpret_cast<uintptr_t>(fault));
|
||||
}
|
||||
const char* thread = detail::CurrentThreadName();
|
||||
b.Str(" in thread ").Str(*thread ? thread : "(unnamed)");
|
||||
b.Str(", ");
|
||||
AppendElapsed(b);
|
||||
b.Str(" into the session, ").Dec(int64_t(gpu::FrameCount())).Str(" game frames\n");
|
||||
if (PPCContext* ctx = GetPPCContext())
|
||||
{
|
||||
b.Str("[crash] guest registers: lr=").Hex(uint32_t(ctx->lr), 8, true).Str(" r1=").Hex(ctx->r1.u32, 8, true)
|
||||
.Str(" r3=").Hex(ctx->r3.u32, 8, true).Str(" r4=").Hex(ctx->r4.u32, 8, true).Str(" r5=").Hex(ctx->r5.u32, 8, true)
|
||||
.Str(" r31=").Hex(ctx->r31.u32, 8, true).Char('\n');
|
||||
}
|
||||
// As Linux's line, then the rest of the integer registers.
|
||||
b.Str("[crash] host registers: rip=0x").Hex(context->Rip).Str(" rsp=0x").Hex(context->Rsp)
|
||||
.Str(" rbp=0x").Hex(context->Rbp).Char('\n');
|
||||
const struct
|
||||
{
|
||||
const char* name;
|
||||
DWORD64 value;
|
||||
} registers[] = { { "rax", context->Rax }, { "rbx", context->Rbx }, { "rcx", context->Rcx }, { "rdx", context->Rdx },
|
||||
{ "rsi", context->Rsi }, { "rdi", context->Rdi }, { "r8", context->R8 }, { "r9", context->R9 }, { "r10", context->R10 },
|
||||
{ "r11", context->R11 }, { "r12", context->R12 }, { "r13", context->R13 }, { "r14", context->R14 }, { "r15", context->R15 },
|
||||
{ "eflags", context->EFlags } };
|
||||
for (size_t i = 0; i < std::size(registers); i++)
|
||||
{
|
||||
b.Str(i % 4 == 0 ? "[crash] " : " ").Str(registers[i].name).Str("=0x").Hex(registers[i].value);
|
||||
if (i % 4 == 3 || i + 1 == std::size(registers))
|
||||
b.Char('\n');
|
||||
}
|
||||
// Where the names come from: SpeedBreaker.pdb, or why not (without
|
||||
// DbgHelp, why: it didn't start, failed in an earlier walk, or isn't
|
||||
// to be had in this crash).
|
||||
b.Str("[crash] names: ").Str(platform::stack::SymbolsState()).Char('\n');
|
||||
// Innermost first: the faulting code, then its callers.
|
||||
b.Str("[crash] host stack (recompiled guest functions are sub_<guest address>):\n");
|
||||
void* frames[64];
|
||||
int count = dbghelp ? platform::stack::Walk(*context, frames, 64) : 0;
|
||||
if (count == 0)
|
||||
count = WalkStack(*context, frames, 64);
|
||||
for (int n = 0; n < count; n++)
|
||||
{
|
||||
b.Str(" #").Dec(n).Str(n < 10 ? " " : " ");
|
||||
detail::FormatFrame(b, frames[n], n > 0);
|
||||
b.Char('\n');
|
||||
}
|
||||
detail::WriteDirect(text, b.size);
|
||||
|
||||
if (const int fd = s_reportFd.load(); fd >= 0)
|
||||
{
|
||||
detail::WriteAll(fd, s_header, s_headerSize);
|
||||
detail::WriteAll(fd, text + 1, b.size - 1); // (without the leading blank line)
|
||||
static const char recent[] = "\nThe log's last lines:\n";
|
||||
detail::WriteAll(fd, recent, sizeof(recent) - 1);
|
||||
detail::WriteRecentOutput(fd, 300);
|
||||
static const char system[] = "\nSystem (at startup):\n";
|
||||
detail::WriteAll(fd, system, sizeof(system) - 1);
|
||||
const char* summary = detail::LoggedSystemSummary();
|
||||
detail::WriteAll(fd, summary, strlen(summary));
|
||||
// The handle itself (the C runtime's _commit would take its lock).
|
||||
FlushFileBuffers(reinterpret_cast<HANDLE>(_get_osfhandle(fd)));
|
||||
// (Shared for deleting, so it moves while still open.)
|
||||
bool renamed = MoveFileExW(s_pendingPathW, s_finalPathW, MOVEFILE_REPLACE_EXISTING) != 0;
|
||||
b.size = 0;
|
||||
b.Str("[crash] crash report: ").Str(renamed ? s_finalPath : s_pendingPath).Char('\n');
|
||||
detail::WriteDirect(text, b.size);
|
||||
}
|
||||
}
|
||||
#else
|
||||
void WriteCrashReport(int sig, siginfo_t* info, void* context)
|
||||
{
|
||||
if (s_crashing.exchange(true))
|
||||
@@ -523,6 +762,7 @@ namespace report
|
||||
detail::WriteDirect(text, b.size);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ClearCrashMarker()
|
||||
{
|
||||
@@ -530,7 +770,11 @@ namespace report
|
||||
if (s_crashing.load())
|
||||
return;
|
||||
if (const int fd = s_reportFd.exchange(-1); fd >= 0)
|
||||
#ifdef _WIN32
|
||||
marker::Clear(fd, s_pendingPathW);
|
||||
#else
|
||||
marker::Clear(fd, s_pendingPath);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NoticePreviousCrash(bool lastClearsMarker)
|
||||
@@ -550,8 +794,14 @@ namespace report
|
||||
std::filesystem::remove(left.path, fileError);
|
||||
// A version before v0.1.6 left one after every session.
|
||||
if (lastClearsMarker)
|
||||
#ifdef _WIN32
|
||||
fprintf(stderr, "[crash] an earlier session (%s) ended without quitting: it was stopped from outside (Task Manager's End "
|
||||
"task, Windows ending it, the power going) or crashed too hard to write a report; there is no report to send\n",
|
||||
left.session.c_str());
|
||||
#else
|
||||
fprintf(stderr, "[crash] an earlier session (%s) ended without quitting: it was stopped from outside (SIGKILL: a forced "
|
||||
"quit, the power going) or crashed too hard to write a report; there is no report to send\n", left.session.c_str());
|
||||
#endif
|
||||
}
|
||||
detail::KeepNewest(dir.string(), "crash-", ".txt", 5);
|
||||
|
||||
@@ -572,12 +822,29 @@ namespace report
|
||||
}
|
||||
if (seen == newest)
|
||||
return;
|
||||
fprintf(stderr, "[crash] an earlier session crashed: %s\n", (dir / newest).c_str());
|
||||
fprintf(stderr, "[crash] an earlier session crashed: %s\n", (dir / newest).string().c_str());
|
||||
ui::Toast(std::format("The game crashed last time and saved a crash report. To send it: {}.", detail::kSaveBugReportHint), 15.0);
|
||||
std::ofstream out(seenPath, std::ios::trunc);
|
||||
out << newest << "\n";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace
|
||||
{
|
||||
// NFSMW_TEST_CRASH=<s>:overflow: recursion until the stack runs out,
|
||||
// each call a little deeper (not a tail call): the fault comes with
|
||||
// the stack pointer at the very end of the stack, the hardest case
|
||||
// for the report (a frame of a page or more stops short of it, in
|
||||
// __chkstk).
|
||||
__declspec(noinline) int Overflow(int depth)
|
||||
{
|
||||
volatile char frame[192];
|
||||
frame[depth % 192] = char(depth);
|
||||
return depth >= 0 ? Overflow(depth + 1) + frame[0] : 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[[noreturn]] void TestCrash()
|
||||
{
|
||||
const char* v = std::getenv("NFSMW_TEST_CRASH");
|
||||
@@ -586,6 +853,16 @@ namespace report
|
||||
fprintf(stderr, "[test] NFSMW_TEST_CRASH: a failed check, then abort()\n");
|
||||
abort();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// Windows only: a crash report needs room on the stack that ran out
|
||||
// (SetThreadStackGuarantee, watchdog.cpp). Elsewhere the crash
|
||||
// handler has no stack of its own (no sigaltstack) to report one.
|
||||
if (v && strstr(v, ":overflow"))
|
||||
{
|
||||
fprintf(stderr, "[test] NFSMW_TEST_CRASH: a stack overflow\n");
|
||||
Overflow(0);
|
||||
}
|
||||
#endif
|
||||
fprintf(stderr, "[test] NFSMW_TEST_CRASH: a write to host address 0x10\n");
|
||||
static volatile uintptr_t address = 0x10;
|
||||
*reinterpret_cast<volatile int*>(address) = 1;
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
// the game had gone (a Steam Frame session its player closed from Steam,
|
||||
// 2026-10-15) said nothing about how it ended.
|
||||
//
|
||||
// POSIX file calls only. Header only (tests/clean_exit_test.cpp).
|
||||
// POSIX file calls only (Windows: the platform layer's, on wide paths, so a
|
||||
// user folder of any name works; a session there that Task Manager ends, or
|
||||
// that Windows ends without asking it, keeps its marker as SIGKILL's does).
|
||||
// Header only (tests/clean_exit_test.cpp, POSIX only).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -22,8 +25,13 @@
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/file_io.h>
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace report::marker
|
||||
{
|
||||
@@ -36,18 +44,34 @@ namespace report::marker
|
||||
|
||||
// Startup: the session's empty marker, open for the crash handler (-1:
|
||||
// it couldn't be made).
|
||||
#ifdef _WIN32
|
||||
inline int Arm(const wchar_t* path)
|
||||
{
|
||||
return platform::OpenFile(path, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
}
|
||||
#else
|
||||
inline int Arm(const char* path)
|
||||
{
|
||||
return open(path, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
}
|
||||
#endif
|
||||
|
||||
// The session ended cleanly: the marker closed and removed.
|
||||
#ifdef _WIN32
|
||||
inline void Clear(int fd, const wchar_t* path)
|
||||
{
|
||||
if (fd >= 0)
|
||||
_close(fd);
|
||||
platform::RemoveFile(path);
|
||||
}
|
||||
#else
|
||||
inline void Clear(int fd, const char* path)
|
||||
{
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
unlink(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
// An earlier session's marker, as a launch finds it.
|
||||
enum class Ended
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace report::detail
|
||||
};
|
||||
|
||||
// write(2) until done (EINTR, short writes). Async-signal-safe.
|
||||
// Windows: WriteFile on the descriptor's handle.
|
||||
void WriteAll(int fd, const void* data, size_t size);
|
||||
|
||||
// The original stderr and this session's log file (-1: none), for
|
||||
@@ -55,9 +56,11 @@ namespace report::detail
|
||||
|
||||
// Host code addresses as names (crash.cpp): on Linux from the
|
||||
// executable's .symtab, read by a thread at startup (dladdr sees no
|
||||
// symbols of an executable there); else the recompiled functions from
|
||||
// the generated table (PPCFuncMappings: guest address -> host function)
|
||||
// and the rest through dladdr. Built once at startup (BuildSymbols).
|
||||
// symbols of an executable there); on Windows from SpeedBreaker.pdb,
|
||||
// which DbgHelp loads on a thread at startup (platform/stack.h); else the
|
||||
// recompiled functions from the generated table (PPCFuncMappings: guest
|
||||
// address -> host function) and the rest through dladdr. Built once at
|
||||
// startup (BuildSymbols).
|
||||
void BuildSymbols();
|
||||
// "0x0000555555a0b3c4 sub_825A5B20+0x64 (SpeedBreaker+0xa0b3c4)".
|
||||
// Async-signal-safe (dladdr aside, which glibc and macOS implement
|
||||
|
||||
+145
-7
@@ -20,9 +20,16 @@
|
||||
#include <ctime>
|
||||
|
||||
#include <fcntl.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
#ifdef _WIN32
|
||||
#include <platform/startup.h>
|
||||
#endif
|
||||
|
||||
namespace report
|
||||
{
|
||||
@@ -43,7 +50,80 @@ namespace report
|
||||
std::atomic<bool> s_busy{ false }; // the writer holds a chunk it hasn't finished with
|
||||
std::atomic<uint64_t> s_chunks{ 0 }; // chunks finished
|
||||
std::atomic<bool> s_writerStarted{ false };
|
||||
#ifdef _WIN32
|
||||
DWORD s_writer = 0; // its thread id
|
||||
#else
|
||||
pthread_t s_writer{};
|
||||
#endif
|
||||
|
||||
// fcntl(fd, F_DUPFD_CLOEXEC, 3): a copy of `fd` numbered 3 or more.
|
||||
int DupAbove2(int fd)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// The C runtime's _dup takes the lowest free number: hold the low
|
||||
// ones until a copy lands above them.
|
||||
int held[3], heldCount = 0;
|
||||
int copy = _dup(fd);
|
||||
while (copy >= 0 && copy <= 2 && heldCount < 3)
|
||||
{
|
||||
held[heldCount++] = copy;
|
||||
copy = _dup(fd);
|
||||
}
|
||||
for (int i = 0; i < heldCount; i++)
|
||||
_close(held[i]);
|
||||
return copy;
|
||||
#else
|
||||
return fcntl(fd, F_DUPFD_CLOEXEC, 3);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The pipe, as a named pipe whose reading end is overlapped: FlushLog
|
||||
// asks how much is waiting (PeekNamedPipe) while the writer thread's
|
||||
// read is pending, and on a synchronous handle Windows queues the
|
||||
// question behind that read, which waits for output that the asking
|
||||
// thread was about to write (a hang at exit). Both ends are C runtime
|
||||
// descriptors (the writing one becomes fd 2), not inherited.
|
||||
int MakeLogPipe(int fds[2])
|
||||
{
|
||||
static std::atomic<int> serial{ 0 };
|
||||
wchar_t name[96];
|
||||
swprintf(name, std::size(name), L"\\\\.\\pipe\\SpeedBreaker-log-%lu-%d", GetCurrentProcessId(), serial++);
|
||||
HANDLE reader = CreateNamedPipeW(name, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE,
|
||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1, 0, 1 << 20, 0, nullptr);
|
||||
if (reader == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
errno = EMFILE;
|
||||
return -1;
|
||||
}
|
||||
HANDLE writer = CreateFileW(name, GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (writer == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(reader);
|
||||
errno = EMFILE;
|
||||
return -1;
|
||||
}
|
||||
fds[0] = _open_osfhandle(reinterpret_cast<intptr_t>(reader), _O_RDONLY | _O_NOINHERIT);
|
||||
fds[1] = _open_osfhandle(reinterpret_cast<intptr_t>(writer), _O_NOINHERIT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// read(2) on the pipe's overlapped end: waits for data; 0 once every
|
||||
// writer has gone.
|
||||
ssize_t ReadPipe(int fd, void* buffer, size_t size)
|
||||
{
|
||||
static HANDLE done = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
HANDLE pipe = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
|
||||
OVERLAPPED at{};
|
||||
at.hEvent = done;
|
||||
DWORD got = 0;
|
||||
if (!ReadFile(pipe, buffer, DWORD(size), nullptr, &at) && GetLastError() != ERROR_IO_PENDING)
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
if (!GetOverlappedResult(pipe, &at, &got, TRUE))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
return ssize_t(got);
|
||||
}
|
||||
#endif
|
||||
|
||||
char s_sessionName[64] = "";
|
||||
std::filesystem::path s_logPath;
|
||||
@@ -95,18 +175,25 @@ namespace report
|
||||
void Writer()
|
||||
{
|
||||
SetHostThreadName("nfsmw-log");
|
||||
#ifndef _WIN32
|
||||
// A terminal that went away must not kill the game: with SIGPIPE
|
||||
// blocked here, the write fails with EPIPE instead.
|
||||
// blocked here, the write fails with EPIPE instead. (Windows has
|
||||
// no SIGPIPE: the write just fails.)
|
||||
sigset_t set;
|
||||
sigemptyset(&set);
|
||||
sigaddset(&set, SIGPIPE);
|
||||
pthread_sigmask(SIG_BLOCK, &set, nullptr);
|
||||
#endif
|
||||
static char buffer[16384];
|
||||
static char stamped[sizeof(buffer) * 13]; // room for a prefix on every byte
|
||||
bool lineStart = true;
|
||||
while (true)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
ssize_t n = ReadPipe(s_pipeRead, buffer, sizeof(buffer));
|
||||
#else
|
||||
ssize_t n = read(s_pipeRead, buffer, sizeof(buffer));
|
||||
#endif
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (n <= 0)
|
||||
@@ -235,6 +322,23 @@ namespace report
|
||||
if (fd < 0)
|
||||
return;
|
||||
const char* p = static_cast<const char*>(data);
|
||||
#ifdef _WIN32
|
||||
// WriteFile on the descriptor's handle: the C runtime's write()
|
||||
// takes the descriptor's lock, which a crash may have left held.
|
||||
// (These descriptors are binary, and the log file is only ever
|
||||
// appended to through this one handle, so its position is its end.)
|
||||
HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
|
||||
if (handle == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
while (size > 0)
|
||||
{
|
||||
DWORD written = 0;
|
||||
if (!WriteFile(handle, p, DWORD(std::min<size_t>(size, 1u << 30)), &written, nullptr) || written == 0)
|
||||
return;
|
||||
p += written;
|
||||
size -= written;
|
||||
}
|
||||
#else
|
||||
while (size > 0)
|
||||
{
|
||||
ssize_t n = write(fd, p, size);
|
||||
@@ -245,6 +349,7 @@ namespace report
|
||||
p += n;
|
||||
size -= size_t(n);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int TerminalFd()
|
||||
@@ -339,7 +444,7 @@ namespace report
|
||||
{
|
||||
std::string name = attempt == 1 ? stamp : std::format("{}-{}", stamp, attempt);
|
||||
std::filesystem::path path = dir / (name + ".log");
|
||||
file = open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_APPEND | O_CLOEXEC, 0644);
|
||||
file = platform::OpenFile(path, O_WRONLY | O_CREAT | O_EXCL | O_APPEND | O_CLOEXEC, 0644);
|
||||
if (file >= 0)
|
||||
{
|
||||
snprintf(s_sessionName, sizeof(s_sessionName), "%s", name.c_str());
|
||||
@@ -349,21 +454,27 @@ namespace report
|
||||
break;
|
||||
}
|
||||
if (file < 0)
|
||||
fprintf(stderr, "[log] can't create a session log in %s: %s\n", dir.c_str(), strerror(errno));
|
||||
fprintf(stderr, "[log] can't create a session log in %s: %s\n", dir.string().c_str(), strerror(errno));
|
||||
else if (file <= 2)
|
||||
{
|
||||
// Launched with stderr (or stdin, stdout) closed, open() hands
|
||||
// out its number: as fd 2 the file would be replaced by the pipe
|
||||
// below, and the copying thread would feed the pipe to itself
|
||||
// forever (one core busy, the log growing without its cap).
|
||||
int moved = fcntl(file, F_DUPFD_CLOEXEC, 3);
|
||||
int moved = DupAbove2(file);
|
||||
close(file);
|
||||
file = moved;
|
||||
}
|
||||
|
||||
// fd 2 becomes the pipe; the writer copies it on (StartLogWriter).
|
||||
int fds[2];
|
||||
#ifdef _WIN32
|
||||
// Binary, not inherited, 1 MB: room for a burst (and for startup,
|
||||
// before the writer runs).
|
||||
if (MakeLogPipe(fds) != 0)
|
||||
#else
|
||||
if (pipe(fds) != 0)
|
||||
#endif
|
||||
{
|
||||
fprintf(stderr, "[build] SpeedBreaker %s\n", BuildString());
|
||||
fprintf(stderr, "[log] pipe: %s (no session log)\n", strerror(errno));
|
||||
@@ -376,7 +487,7 @@ namespace report
|
||||
for (int& fd : fds)
|
||||
if (fd <= 2)
|
||||
{
|
||||
int moved = fcntl(fd, F_DUPFD_CLOEXEC, 3);
|
||||
int moved = DupAbove2(fd);
|
||||
close(fd);
|
||||
fd = moved;
|
||||
}
|
||||
@@ -388,13 +499,15 @@ namespace report
|
||||
close(file);
|
||||
return;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
fcntl(fds[0], F_SETFD, FD_CLOEXEC);
|
||||
#endif
|
||||
#ifdef F_SETPIPE_SZ
|
||||
// Room for a burst (and for startup, before the writer runs).
|
||||
fcntl(fds[1], F_SETPIPE_SZ, 1 << 20);
|
||||
#endif
|
||||
fflush(stderr);
|
||||
int terminal = fcntl(2, F_DUPFD_CLOEXEC, 3);
|
||||
int terminal = DupAbove2(2);
|
||||
if (dup2(fds[1], 2) < 0)
|
||||
{
|
||||
fprintf(stderr, "[build] SpeedBreaker %s\n", BuildString());
|
||||
@@ -408,6 +521,11 @@ namespace report
|
||||
return;
|
||||
}
|
||||
close(fds[1]);
|
||||
#ifdef _WIN32
|
||||
// Code that writes to the process's standard error handle rather
|
||||
// than to fd 2 (the C runtime's assert, a driver) reaches the pipe too.
|
||||
SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(2)));
|
||||
#endif
|
||||
// A write that failed before (stderr closed at launch: the
|
||||
// NFSMW_INPUT_SCRIPT line, any message before this) leaves the
|
||||
// stream's error flag set, and macOS's fprintf then writes nothing,
|
||||
@@ -421,7 +539,12 @@ namespace report
|
||||
// The first lines of the file.
|
||||
fprintf(stderr, "[build] SpeedBreaker %s\n", BuildString());
|
||||
if (file >= 0)
|
||||
fprintf(stderr, "[log] session %s (pid %d): %s\n", s_sessionName, int(getpid()), s_logPath.c_str());
|
||||
fprintf(stderr, "[log] session %s (pid %d): %s\n", s_sessionName, int(getpid()), s_logPath.string().c_str());
|
||||
#ifdef _WIN32
|
||||
// The download (the Windows subsystem): where the console went.
|
||||
if (const char* note = platform::StandardStreamsNote(); note[0])
|
||||
fprintf(stderr, "[log] %s\n", note);
|
||||
#endif
|
||||
}
|
||||
|
||||
void StartLogWriter()
|
||||
@@ -429,7 +552,11 @@ namespace report
|
||||
if (s_pipeRead < 0 || s_writerStarted.load())
|
||||
return;
|
||||
std::thread writer(Writer);
|
||||
#ifdef _WIN32
|
||||
s_writer = GetThreadId(writer.native_handle());
|
||||
#else
|
||||
s_writer = writer.native_handle();
|
||||
#endif
|
||||
writer.detach();
|
||||
s_writerStarted.store(true, std::memory_order_release);
|
||||
// A return from main() or an exit() (a usage message, "the game
|
||||
@@ -472,8 +599,13 @@ namespace report
|
||||
|
||||
void FlushLog()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (s_pipeRead < 0 || !s_writerStarted.load(std::memory_order_acquire) || GetCurrentThreadId() == s_writer)
|
||||
return;
|
||||
#else
|
||||
if (s_pipeRead < 0 || !s_writerStarted.load(std::memory_order_acquire) || pthread_equal(pthread_self(), s_writer))
|
||||
return;
|
||||
#endif
|
||||
// Empty pipe and an idle writer, twice a millisecond apart with no
|
||||
// chunk finished in between (it may have just taken the last one).
|
||||
int quiet = 0;
|
||||
@@ -481,7 +613,13 @@ namespace report
|
||||
for (int i = 0; i < 250 && quiet < 2; i++)
|
||||
{
|
||||
int pending = 0;
|
||||
#ifdef _WIN32
|
||||
DWORD available = 0;
|
||||
if (PeekNamedPipe(reinterpret_cast<HANDLE>(_get_osfhandle(s_pipeRead)), nullptr, 0, nullptr, &available, nullptr))
|
||||
pending = int(std::min<DWORD>(available, INT_MAX));
|
||||
#else
|
||||
ioctl(s_pipeRead, FIONREAD, &pending);
|
||||
#endif
|
||||
uint64_t now = s_chunks.load(std::memory_order_acquire);
|
||||
quiet = (pending == 0 && !s_busy.load(std::memory_order_acquire) && now == chunks) ? quiet + 1 : 0;
|
||||
chunks = now;
|
||||
|
||||
+30
-2
@@ -18,6 +18,8 @@
|
||||
// - Crash reports (crash.cpp): the crash handler writes
|
||||
// logs/crash-<session>.txt with async-signal-safe calls only; what it
|
||||
// needs is prepared at startup. The next launch tells the player once.
|
||||
// (Windows: from the unhandled-exception filter, with names from
|
||||
// SpeedBreaker.pdb through DbgHelp: platform/stack.h.)
|
||||
// - Hang reports (watchdog.cpp): a watchdog notices the game has stopped
|
||||
// (no new frame, or the command processor in one WAIT_REG_MEM, for far
|
||||
// longer than any loading screen or movie) and writes
|
||||
@@ -26,14 +28,18 @@
|
||||
// - Bug reports (bug_report.cpp): one .zip on the Desktop (else the user
|
||||
// data folder; on iOS the Documents folder the Files app shows) with all
|
||||
// of that, settings.toml and a screenshot. Home-folder paths in it read
|
||||
// "~".
|
||||
// "~" (Windows: the user's folder reads "%USERPROFILE%").
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
struct _EXCEPTION_POINTERS;
|
||||
#else
|
||||
#include <signal.h>
|
||||
#endif
|
||||
|
||||
struct PPCContext;
|
||||
|
||||
@@ -75,8 +81,15 @@ namespace report
|
||||
const char* VersionString();
|
||||
// One line: the version and id, when it was built, compiler, platform,
|
||||
// build type: "v0.1.0 (fae4bd1), built 2026-10-07 21:30 UTC, clang
|
||||
// 21.1.0, macOS arm64, Release" (the log's first line).
|
||||
// 21.1.0, macOS arm64, Release" (the log's first line). A labelled
|
||||
// build's starts with its ReleaseName instead of the version and id.
|
||||
const char* BuildString();
|
||||
// A labelled build (SB_BUILD_LABEL, runtime/CMakeLists.txt): "v0.1.3
|
||||
// Windows beta 2026-10-11 (59d1ef3)", and the label and commit for file
|
||||
// names, which carry the version already: "windows-beta-2026-10-11-59d1ef3".
|
||||
// "" for every other build.
|
||||
const char* ReleaseName();
|
||||
const char* ReleaseFileTag();
|
||||
|
||||
// --- System summary (system_info.cpp) ---
|
||||
|
||||
@@ -97,7 +110,14 @@ namespace report
|
||||
void PrepareCrashReports();
|
||||
// The crash handler (a signal handler; async-signal-safe): logs the
|
||||
// crash and writes the report. The caller exits afterwards.
|
||||
#ifdef _WIN32
|
||||
// Windows: from the unhandled-exception filter or SIGABRT's handler
|
||||
// (platform/win32/fault.cpp), with the exception and its registers;
|
||||
// nothing allocates or takes the C runtime's locks but DbgHelp's lookups.
|
||||
void WriteCrashReport(_EXCEPTION_POINTERS* info);
|
||||
#else
|
||||
void WriteCrashReport(int sig, siginfo_t* info, void* context);
|
||||
#endif
|
||||
// The session is ending cleanly (the quit path, any return from main()):
|
||||
// its crash marker is removed (crash_marker.h), so one the next launch
|
||||
// finds is a session that didn't. Last, just before the exit: a crash
|
||||
@@ -121,6 +141,7 @@ namespace report
|
||||
// NFSMW_TEST_CRASH=<s>[:abort] crash in VdSwap (the game's render
|
||||
// thread) <s> seconds after launch;
|
||||
// ":abort" aborts after a message instead
|
||||
// (Windows also ":overflow": a stack overflow)
|
||||
// NFSMW_TEST_HANG=<s>[+<d>] from <s> seconds, the command processor's
|
||||
// waits never pass (for <d> seconds, or for good)
|
||||
// NFSMW_TEST_SUSPEND=<s>+<d>[b][,...]
|
||||
@@ -138,8 +159,13 @@ namespace report
|
||||
|
||||
// Fed by hot paths (relaxed; costs nothing that shows).
|
||||
inline std::atomic<uint64_t> g_fileBytesRead{ 0 }; // NtReadFile
|
||||
inline std::atomic<uint64_t> g_pipelinesBuilt{ 0 }; // pipeline compiles finished (renderer.cpp)
|
||||
inline std::atomic<bool> g_testCrash{ false }; // VdSwap calls TestCrash()
|
||||
inline std::atomic<bool> g_testHang{ false }; // WAIT_REG_MEM never passes
|
||||
// A startup failure ends the game with a message (Windows: the renderer
|
||||
// finding no memory the driver accepts): while the message waits for the
|
||||
// player no frames come, and the watchdog must not call that a hang.
|
||||
inline std::atomic<bool> g_endingWithMessage{ false };
|
||||
[[noreturn]] void TestCrash();
|
||||
|
||||
// --- Bug reports (bug_report.cpp) ---
|
||||
@@ -159,5 +185,7 @@ namespace report
|
||||
std::string BugReportMessage();
|
||||
|
||||
// HOME (and its real path) as "~", for text that leaves the machine.
|
||||
// Windows: the user's profile folder (USERPROFILE, the shell's, and its
|
||||
// 8.3 name) as "%USERPROFILE%", in either case and with either slash.
|
||||
std::string ScrubHome(std::string text);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See report.h (ScrubHome).
|
||||
//
|
||||
// A folder's path replaced in text that leaves the machine (a bug report):
|
||||
// the home folder as "~", or on Windows the user's profile as
|
||||
// "%USERPROFILE%". Header only, for tests/scrub_test.cpp.
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace report::detail
|
||||
{
|
||||
struct FolderName
|
||||
{
|
||||
std::string path; // no trailing separator
|
||||
std::string replacement; // "~", "%USERPROFILE%"
|
||||
};
|
||||
|
||||
namespace scrub
|
||||
{
|
||||
inline bool PathChar(char c)
|
||||
{
|
||||
return std::isalnum(uint8_t(c)) || c == '.' || c == '_' || c == '-';
|
||||
}
|
||||
|
||||
// How many bytes of `text` from `at` spell `path`, or 0. Windows
|
||||
// paths: ASCII letters in either case, and a separator as \, / or \\
|
||||
// (a backslash escaped, as in TOML or JSON). Elsewhere byte for byte.
|
||||
inline size_t MatchAt(std::string_view text, size_t at, std::string_view path, bool windows)
|
||||
{
|
||||
if (!windows)
|
||||
return text.compare(at, path.size(), path) == 0 ? path.size() : 0;
|
||||
size_t t = at;
|
||||
for (char p : path)
|
||||
{
|
||||
if (t >= text.size())
|
||||
return 0;
|
||||
if (p == '\\' || p == '/')
|
||||
{
|
||||
if (text[t] == '/')
|
||||
t++;
|
||||
else if (text[t] == '\\')
|
||||
t += (t + 1 < text.size() && text[t + 1] == '\\') ? 2 : 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
else if (std::tolower(uint8_t(text[t])) == std::tolower(uint8_t(p)))
|
||||
t++;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
return t - at;
|
||||
}
|
||||
}
|
||||
|
||||
// Each folder's path in `text` as its replacement, where it is the whole
|
||||
// folder (/home/deck, not /home/deck2) and not the tail of a longer path
|
||||
// (/mnt/home/deck); the longest paths first. `windows`: see MatchAt.
|
||||
inline std::string ReplaceFolders(std::string text, std::vector<FolderName> folders, bool windows)
|
||||
{
|
||||
std::sort(folders.begin(), folders.end(), [](const FolderName& a, const FolderName& b) { return a.path.size() > b.path.size(); });
|
||||
for (const FolderName& folder : folders)
|
||||
{
|
||||
if (folder.path.empty())
|
||||
continue;
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
size_t from = 0;
|
||||
if (!windows)
|
||||
{
|
||||
for (size_t at; (at = text.find(folder.path, from)) != std::string::npos;)
|
||||
{
|
||||
size_t end = at + folder.path.size();
|
||||
bool whole = (end == text.size() || !scrub::PathChar(text[end])) && (at == 0 || !scrub::PathChar(text[at - 1]));
|
||||
out.append(text, from, at - from);
|
||||
out += whole ? folder.replacement : folder.path;
|
||||
from = end;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const char first = char(std::tolower(uint8_t(folder.path[0])));
|
||||
for (size_t at = 0; at < text.size();)
|
||||
{
|
||||
size_t length = 0;
|
||||
if (char(std::tolower(uint8_t(text[at]))) == first && (at == 0 || !scrub::PathChar(text[at - 1])))
|
||||
length = scrub::MatchAt(text, at, folder.path, true);
|
||||
size_t end = at + length;
|
||||
if (length == 0 || (end < text.size() && scrub::PathChar(text[end])))
|
||||
{
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
out.append(text, from, at - from);
|
||||
out += folder.replacement;
|
||||
from = at = end;
|
||||
}
|
||||
}
|
||||
out.append(text, from, std::string::npos);
|
||||
text.swap(out);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See report.h.
|
||||
//
|
||||
// Recompiled functions' names as symbol tables spell them (report/crash.cpp
|
||||
// names stack frames with it). XenonRecomp emits each guest function's body
|
||||
// as `extern "C" __imp__sub_<address>` and a weak C++ wrapper
|
||||
// `sub_<address>(PPCContext&, uint8_t*)` (a hook in the runtime replaces the
|
||||
// wrapper). Header only, for tests/symbol_names_test.cpp.
|
||||
#pragma once
|
||||
#include <cstring>
|
||||
|
||||
namespace report::detail
|
||||
{
|
||||
namespace symbol_names
|
||||
{
|
||||
inline bool IsGuestAddress(const char* hex)
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (!((hex[i] >= '0' && hex[i] <= '9') || (hex[i] >= 'A' && hex[i] <= 'F')))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// "sub_" and 8 uppercase hex digits at `p`.
|
||||
inline bool IsSub(const char* p)
|
||||
{
|
||||
return strncmp(p, "sub_", 4) == 0 && strnlen(p, 12) == 12 && IsGuestAddress(p + 4);
|
||||
}
|
||||
}
|
||||
|
||||
// A recompiled function's own name ("sub_825987E0") for a symbol that
|
||||
// is one, else nullptr. Async-signal-safe; `buffer` holds 13 bytes.
|
||||
// ELF and Mach-O (dladdr, .symtab):
|
||||
// __imp__sub_825987E0 the body
|
||||
// _Z12sub_82441190R10PPCContextPh the C++ wrapper or a hook (the
|
||||
// compiler may put the body inline)
|
||||
// A PDB, through DbgHelp (Windows; platform/stack.h):
|
||||
// __imp__sub_825987E0 the body (a public symbol)
|
||||
// sub_82441190 a hook's own debug symbol, or the
|
||||
// wrapper's public one undecorated
|
||||
// ?sub_82441190@@YAXAEIAUPPCContext@@PEAE@Z the wrapper, decorated
|
||||
// .weak.?sub_82441190@@YAXAEI... lld-link's alias for a weak
|
||||
// wrapper
|
||||
// and the same as DbgHelp's SYMOPT_UNDNAME would spell them (the
|
||||
// ThinkPad's first look, 92306bf): _imp__sub_825987E0 (a C name's first
|
||||
// underscore dropped), weak.?sub_82441190YAXAEI...
|
||||
inline const char* GuestName(const char* name, char* buffer)
|
||||
{
|
||||
using namespace symbol_names;
|
||||
if (strncmp(name, "__imp__sub_", 11) == 0)
|
||||
return name + 7;
|
||||
if (strncmp(name, "_Z12sub_", 8) == 0 && strncmp(name + 16, "R10PPCContextPh", 15) == 0)
|
||||
{
|
||||
memcpy(buffer, name + 4, 12);
|
||||
buffer[12] = 0;
|
||||
return buffer;
|
||||
}
|
||||
if (strncmp(name, "_imp__sub_", 10) == 0 && IsSub(name + 6))
|
||||
return name + 6;
|
||||
if (IsSub(name) && name[12] == 0)
|
||||
return name;
|
||||
const char* decorated = nullptr;
|
||||
if (name[0] == '?')
|
||||
decorated = name + 1;
|
||||
else if (strncmp(name, "weak.?", 6) == 0)
|
||||
decorated = name + 6;
|
||||
else if (strncmp(name, ".weak.?", 7) == 0)
|
||||
decorated = name + 7;
|
||||
if (decorated && IsSub(decorated))
|
||||
{
|
||||
memcpy(buffer, decorated, 12);
|
||||
buffer[12] = 0;
|
||||
return buffer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,19 @@
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <intrin.h>
|
||||
#else
|
||||
#include <sys/utsname.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
namespace report
|
||||
{
|
||||
@@ -111,6 +117,137 @@ namespace report
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string Utf8(const wchar_t* wide)
|
||||
{
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (n <= 1)
|
||||
return {};
|
||||
std::string out(size_t(n - 1), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide, -1, out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string RegistryString(const wchar_t* key, const wchar_t* value)
|
||||
{
|
||||
wchar_t buffer[256];
|
||||
DWORD size = sizeof(buffer);
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE, key, value, RRF_RT_REG_SZ, nullptr, buffer, &size) != ERROR_SUCCESS)
|
||||
return {};
|
||||
return Trim(Utf8(buffer));
|
||||
}
|
||||
|
||||
DWORD RegistryNumber(const wchar_t* key, const wchar_t* value)
|
||||
{
|
||||
DWORD number = 0, size = sizeof(number);
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE, key, value, RRF_RT_REG_DWORD, nullptr, &number, &size) != ERROR_SUCCESS)
|
||||
return 0;
|
||||
return number;
|
||||
}
|
||||
|
||||
// Windows 10 and 11 both report 10.0: the build tells them apart.
|
||||
std::string OsText()
|
||||
{
|
||||
RTL_OSVERSIONINFOW version{};
|
||||
version.dwOSVersionInfoSize = sizeof(version);
|
||||
using RtlGetVersionFn = LONG(WINAPI*)(RTL_OSVERSIONINFOW*);
|
||||
if (auto get = reinterpret_cast<RtlGetVersionFn>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion")))
|
||||
get(&version);
|
||||
const wchar_t* key = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
|
||||
std::string display = RegistryString(key, L"DisplayVersion");
|
||||
std::string edition = RegistryString(key, L"EditionID");
|
||||
return std::format("Windows {}{}{}{} (build {}.{}), x64", version.dwBuildNumber >= 22000 ? 11 : 10,
|
||||
edition.empty() ? "" : " ", edition, display.empty() ? "" : " " + display, version.dwBuildNumber,
|
||||
RegistryNumber(key, L"UBR"));
|
||||
}
|
||||
|
||||
std::string MachineText()
|
||||
{
|
||||
// Lenovo keeps the model's name in SystemVersion ("ThinkPad T14
|
||||
// Gen 1") and its type number in SystemProductName.
|
||||
const wchar_t* key = L"HARDWARE\\DESCRIPTION\\System\\BIOS";
|
||||
std::string text = Trim(RegistryString(key, L"SystemManufacturer") + " " + RegistryString(key, L"SystemProductName"));
|
||||
std::string version = RegistryString(key, L"SystemVersion");
|
||||
if (!version.empty() && version != "None" && text.find(version) == std::string::npos)
|
||||
text += " (" + version + ")";
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string CpuText()
|
||||
{
|
||||
int r[4];
|
||||
char brand[49] = "";
|
||||
__cpuid(r, int(0x80000000));
|
||||
if (unsigned(r[0]) >= 0x80000004u)
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
__cpuid(r, int(0x80000002 + i));
|
||||
memcpy(brand + 16 * i, r, 16);
|
||||
}
|
||||
DWORD bytes = 0;
|
||||
GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &bytes);
|
||||
std::vector<uint8_t> buffer(bytes);
|
||||
size_t cores = 0;
|
||||
if (bytes && GetLogicalProcessorInformationEx(RelationProcessorCore,
|
||||
reinterpret_cast<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*>(buffer.data()), &bytes))
|
||||
for (DWORD at = 0; at < bytes;)
|
||||
{
|
||||
auto* info = reinterpret_cast<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*>(buffer.data() + at);
|
||||
cores++;
|
||||
at += info->Size;
|
||||
}
|
||||
std::string model = Trim(brand);
|
||||
return std::format("{}, {} cores / {} threads", model.empty() ? "unknown CPU" : model, cores, std::thread::hardware_concurrency());
|
||||
}
|
||||
|
||||
std::string MemoryText()
|
||||
{
|
||||
constexpr double kGiB = 1024.0 * 1024.0 * 1024.0;
|
||||
MEMORYSTATUSEX m{};
|
||||
m.dwLength = sizeof(m);
|
||||
GlobalMemoryStatusEx(&m);
|
||||
return std::format("{:.1f} GB ({:.1f} GB available at startup), commit limit {:.1f} GB", double(m.ullTotalPhys) / kGiB,
|
||||
double(m.ullAvailPhys) / kGiB, double(m.ullTotalPageFile) / kGiB);
|
||||
}
|
||||
|
||||
// Mains or battery, and Windows' power mode (Settings > System >
|
||||
// Power): on the ThinkPad, Best performance ran a race 25% faster
|
||||
// than Balanced, on the same settings.
|
||||
std::string PowerText()
|
||||
{
|
||||
std::string text = "power source unknown";
|
||||
SYSTEM_POWER_STATUS status{};
|
||||
if (GetSystemPowerStatus(&status))
|
||||
{
|
||||
text = status.ACLineStatus == 1 ? "on AC power" : status.ACLineStatus == 0 ? "on battery" : "power source unknown";
|
||||
if (status.BatteryFlag != 128 && status.BatteryFlag != 255 && status.BatteryLifePercent <= 100)
|
||||
text += std::format(", battery {}%", status.BatteryLifePercent);
|
||||
if (status.SystemStatusFlag == 1)
|
||||
text += ", battery saver on";
|
||||
}
|
||||
// powrprof's PowerGetEffectiveOverlayScheme, looked up so that a
|
||||
// Windows without it still starts.
|
||||
using OverlayFn = DWORD(WINAPI*)(GUID*);
|
||||
if (HMODULE powrprof = LoadLibraryExW(L"powrprof.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))
|
||||
{
|
||||
GUID overlay{};
|
||||
auto get = reinterpret_cast<OverlayFn>(GetProcAddress(powrprof, "PowerGetEffectiveOverlayScheme"));
|
||||
if (get && get(&overlay) == ERROR_SUCCESS)
|
||||
{
|
||||
std::string id = std::format("{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", overlay.Data1,
|
||||
overlay.Data2, overlay.Data3, overlay.Data4[0], overlay.Data4[1], overlay.Data4[2], overlay.Data4[3],
|
||||
overlay.Data4[4], overlay.Data4[5], overlay.Data4[6], overlay.Data4[7]);
|
||||
const char* mode = id == "00000000-0000-0000-0000-000000000000" ? "Balanced"
|
||||
: id == "ded574b5-45a0-4f42-8737-46345c09c238" ? "Best performance"
|
||||
: id == "961cc777-2547-4f9d-8174-7d86181b8a7a" ? "Best power efficiency"
|
||||
: nullptr;
|
||||
text += mode ? std::format("; power mode {}", mode) : std::format("; power mode overlay {}", id);
|
||||
}
|
||||
FreeLibrary(powrprof);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
#else
|
||||
std::string OsText()
|
||||
{
|
||||
struct utsname u{};
|
||||
@@ -205,6 +342,7 @@ namespace report
|
||||
std::max(swap, 0.0) / kGiB);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// How it was launched: Game Mode, from Steam, in a container.
|
||||
std::string SessionText()
|
||||
@@ -228,8 +366,16 @@ namespace report
|
||||
parts.push_back("SteamDeck=1");
|
||||
if (!env("CONTAINER_ID").empty())
|
||||
parts.push_back("container " + env("CONTAINER_ID"));
|
||||
#ifndef _WIN32
|
||||
else if (access("/run/.containerenv", F_OK) == 0 || access("/.dockerenv", F_OK) == 0)
|
||||
parts.push_back("in a container");
|
||||
#else
|
||||
DWORD sessionId = 0;
|
||||
if (ProcessIdToSessionId(GetCurrentProcessId(), &sessionId) && sessionId == 0)
|
||||
parts.push_back("session 0 (no desktop: a service or SSH)");
|
||||
if (GetSystemMetrics(SM_REMOTESESSION))
|
||||
parts.push_back("Remote Desktop");
|
||||
#endif
|
||||
if (!env("NFSMW_LIFELINE_PID").empty())
|
||||
parts.push_back("scripts/steamos/speedbreaker.sh");
|
||||
std::string text;
|
||||
@@ -241,9 +387,19 @@ namespace report
|
||||
std::string EnvironmentText()
|
||||
{
|
||||
std::vector<std::string> vars;
|
||||
#ifdef _WIN32
|
||||
if (wchar_t* block = GetEnvironmentStringsW())
|
||||
{
|
||||
for (const wchar_t* e = block; *e; e += wcslen(e) + 1)
|
||||
if (wcsncmp(e, L"NFSMW_", 6) == 0)
|
||||
vars.push_back(Utf8(e));
|
||||
FreeEnvironmentStringsW(block);
|
||||
}
|
||||
#else
|
||||
for (char** e = environ; e && *e; e++)
|
||||
if (strncmp(*e, "NFSMW_", 6) == 0)
|
||||
vars.push_back(*e);
|
||||
#endif
|
||||
std::sort(vars.begin(), vars.end());
|
||||
std::string text;
|
||||
for (const std::string& v : vars)
|
||||
@@ -291,6 +447,9 @@ namespace report
|
||||
text += "Machine: " + machine + "\n";
|
||||
text += "CPU: " + CpuText() + "\n";
|
||||
text += "RAM: " + MemoryText() + "\n";
|
||||
#ifdef _WIN32
|
||||
text += "Power: " + PowerText() + "\n";
|
||||
#endif
|
||||
{
|
||||
std::lock_guard lock(s_mutex);
|
||||
for (const auto& [key, value] : s_items)
|
||||
|
||||
+172
-7
@@ -16,7 +16,11 @@
|
||||
// Each runtime thread's host stack is taken where it stands: the watchdog
|
||||
// signals it (SIGURG, otherwise unused here) and its handler records
|
||||
// backtrace() into the thread's slot. The recompiled game's calls are host
|
||||
// calls, so the stack of a guest thread is its guest call chain.
|
||||
// calls, so the stack of a guest thread is its guest call chain. Windows has
|
||||
// no signals to send: the watchdog suspends each thread in turn, copies its
|
||||
// registers and the top of its stack, resumes it, and walks the copy
|
||||
// (platform/stack.h), through a handle each thread opens on itself as it
|
||||
// registers.
|
||||
#include <stdafx.h>
|
||||
#include "report.h"
|
||||
#include "internal.h"
|
||||
@@ -29,10 +33,17 @@
|
||||
#include <video/presenter.h>
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#ifndef _WIN32
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
#include <fstream>
|
||||
#ifdef _WIN32
|
||||
#include <platform/fault.h>
|
||||
#include <platform/stack.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace report
|
||||
{
|
||||
@@ -40,12 +51,19 @@ namespace report
|
||||
{
|
||||
constexpr int kMaxThreads = 256;
|
||||
constexpr int kMaxFrames = 48;
|
||||
#ifndef _WIN32
|
||||
constexpr int kSampleSignal = SIGURG;
|
||||
#endif
|
||||
|
||||
struct ThreadSlot
|
||||
{
|
||||
std::atomic<bool> used{ false };
|
||||
#ifdef _WIN32
|
||||
HANDLE handle = nullptr; // the thread's own, to suspend it and read its registers
|
||||
uintptr_t stackHigh = 0; // its stack's top (the stack grows down from it)
|
||||
#else
|
||||
pthread_t thread{};
|
||||
#endif
|
||||
char name[32] = "";
|
||||
PPCContext* guest = nullptr; // under s_threadsMutex
|
||||
std::atomic<uint32_t> request{ 0 }; // the sample asked for
|
||||
@@ -72,12 +90,18 @@ namespace report
|
||||
return;
|
||||
std::lock_guard lock(s_threadsMutex);
|
||||
s_threads[t_slot].guest = nullptr;
|
||||
#ifdef _WIN32
|
||||
if (s_threads[t_slot].handle)
|
||||
CloseHandle(s_threads[t_slot].handle);
|
||||
s_threads[t_slot].handle = nullptr;
|
||||
#endif
|
||||
s_threads[t_slot].used.store(false, std::memory_order_release);
|
||||
t_slot = -1;
|
||||
}
|
||||
};
|
||||
thread_local Unregister t_unregister;
|
||||
|
||||
#ifndef _WIN32
|
||||
void SampleHandler(int, siginfo_t*, void* context)
|
||||
{
|
||||
int saved = errno;
|
||||
@@ -90,6 +114,7 @@ namespace report
|
||||
}
|
||||
errno = saved;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Under s_threadsMutex.
|
||||
void RegisterLocked(const char* name)
|
||||
@@ -107,7 +132,23 @@ namespace report
|
||||
t_unregister.armed = true;
|
||||
}
|
||||
ThreadSlot& s = s_threads[t_slot];
|
||||
#ifdef _WIN32
|
||||
if (!s.handle)
|
||||
{
|
||||
s.handle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_LIMITED_INFORMATION, FALSE,
|
||||
GetCurrentThreadId());
|
||||
ULONG_PTR low = 0, high = 0;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
s.stackHigh = high;
|
||||
// A stack overflow's crash report runs on the stack that ran
|
||||
// out: 64 KB kept back for it (the report, StackWalk64 and
|
||||
// DbgHelp's lookups), where Windows keeps a few KB.
|
||||
ULONG guarantee = 64 * 1024;
|
||||
SetThreadStackGuarantee(&guarantee);
|
||||
}
|
||||
#else
|
||||
s.thread = pthread_self();
|
||||
#endif
|
||||
snprintf(s.name, sizeof(s.name), "%s", name);
|
||||
s.guest = nullptr;
|
||||
s.used.store(true, std::memory_order_release);
|
||||
@@ -123,8 +164,59 @@ namespace report
|
||||
std::vector<void*> frames;
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
int s_unwound = 0; // the last sample's threads walked without DbgHelp
|
||||
#endif
|
||||
|
||||
std::vector<ThreadSample> SampleThreads()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Each thread in turn: suspended only while its registers and the
|
||||
// top of its stack are copied into memory allocated before
|
||||
// (platform::stack::Capture), then walked from the copy.
|
||||
static std::vector<uint8_t> copy(1 << 20);
|
||||
static platform::stack::Snapshot snapshot;
|
||||
snapshot.buffer = copy.data();
|
||||
snapshot.capacity = copy.size();
|
||||
std::vector<ThreadSample> samples;
|
||||
s_unwound = 0;
|
||||
std::lock_guard lock(s_threadsMutex);
|
||||
for (int i = 0; i < kMaxThreads; i++)
|
||||
{
|
||||
ThreadSlot& s = s_threads[i];
|
||||
if (!s.used.load(std::memory_order_acquire) || i == t_slot)
|
||||
continue;
|
||||
ThreadSample sample;
|
||||
sample.name = s.name;
|
||||
if (s.guest)
|
||||
{
|
||||
// Racy, as on POSIX (the thread runs on), but the context
|
||||
// stays: it goes away only through SetThreadGuestContext,
|
||||
// which waits for this lock.
|
||||
sample.hasGuest = true;
|
||||
sample.lr = uint32_t(s.guest->lr);
|
||||
sample.r1 = s.guest->r1.u32;
|
||||
}
|
||||
sample.frames.reserve(kMaxFrames);
|
||||
sample.answered = s.handle && platform::stack::Capture(s.handle, s.stackHigh, snapshot);
|
||||
if (sample.answered)
|
||||
{
|
||||
void* frames[kMaxFrames];
|
||||
int count = platform::stack::Walk(snapshot.context, frames, kMaxFrames, &snapshot);
|
||||
if (count == 0)
|
||||
{
|
||||
// DbgHelp unusable (it didn't start, or failed in an
|
||||
// earlier walk) or busy: the unwind tables alone.
|
||||
count = platform::stack::Unwind(snapshot, frames, kMaxFrames);
|
||||
s_unwound++;
|
||||
}
|
||||
sample.pc = reinterpret_cast<void*>(snapshot.context.Rip);
|
||||
sample.frames.assign(frames, frames + count);
|
||||
}
|
||||
samples.push_back(std::move(sample));
|
||||
}
|
||||
return samples;
|
||||
#else
|
||||
static std::once_flag installed;
|
||||
std::call_once(installed, [] {
|
||||
struct sigaction sa{};
|
||||
@@ -181,6 +273,7 @@ namespace report
|
||||
samples.push_back(std::move(sample));
|
||||
}
|
||||
return samples;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Innermost first: where the signal found the thread, then its
|
||||
@@ -219,6 +312,19 @@ namespace report
|
||||
// read nothing for 4.6 s before the report); the boot's load read
|
||||
// 35 MB in 2 s.
|
||||
constexpr uint64_t kLoadingBytes = 4ull << 20;
|
||||
// Windows: a pipeline compile finished in the last 10 s counts as
|
||||
// loading too. Intel's driver (UHD 620, 101.2135) takes 400-800 ms a
|
||||
// pipeline, and the first race's ~120 held one frame for 19.5-21.6 s
|
||||
// on a cold cache with nothing read meanwhile: a hang report for a
|
||||
// game that was only compiling (the ThinkPad, 2026-10-06). A real
|
||||
// deadlock stops the compiles too (they come from the command
|
||||
// processor's draws), so it is reported 10 s after the last one at
|
||||
// most. Elsewhere as before for now: compiles there take 20-80 ms.
|
||||
#ifdef _WIN32
|
||||
constexpr bool kCompilesAreLoading = true;
|
||||
#else
|
||||
constexpr bool kCompilesAreLoading = false;
|
||||
#endif
|
||||
|
||||
void WriteHangReport(const std::string& reason, double sinceFrame, double sinceRead, int episode)
|
||||
{
|
||||
@@ -238,6 +344,14 @@ namespace report
|
||||
// The game's sound runs on the render callback, and some of its
|
||||
// waits on its sound commands (GitHub issue #20).
|
||||
text += "\nAudio:\n" + apu::DescribeState();
|
||||
#ifdef _WIN32
|
||||
// Where the stacks' names come from (SpeedBreaker.pdb, or why
|
||||
// not), as in crash reports.
|
||||
text += std::format("\nNames: {}", platform::stack::SymbolsState());
|
||||
if (s_unwound)
|
||||
text += std::format("; {} of the stacks below walked from the unwind tables, without DbgHelp", s_unwound);
|
||||
text += "\n";
|
||||
#endif
|
||||
text += "\nThreads, where each one is (host stacks, innermost first; recompiled guest functions are "
|
||||
"sub_<guest address>):\n";
|
||||
for (const ThreadSample& s : samples)
|
||||
@@ -245,7 +359,11 @@ namespace report
|
||||
text += " " + s.name;
|
||||
if (s.hasGuest)
|
||||
text += std::format(" (guest lr={:08X} r1={:08X})", s.lr, s.r1);
|
||||
#ifdef _WIN32
|
||||
text += s.answered ? ":\n" : ": couldn't be suspended to read its stack\n";
|
||||
#else
|
||||
text += s.answered ? ":\n" : ": didn't answer (blocked in the kernel?)\n";
|
||||
#endif
|
||||
text += FormatStack(s.pc, s.frames);
|
||||
}
|
||||
text += "\nThe log's last lines:\n" + RecentOutput(400);
|
||||
@@ -263,12 +381,23 @@ namespace report
|
||||
}
|
||||
detail::KeepNewest(dir.string(), "hang-", ".txt", 5);
|
||||
if (saved)
|
||||
fprintf(stderr, "[hang] %s. Hang report: %s\n", reason.c_str(), path.c_str());
|
||||
fprintf(stderr, "[hang] %s. Hang report: %s\n", reason.c_str(), path.string().c_str());
|
||||
else
|
||||
fprintf(stderr, "[hang] %s. Couldn't write the hang report to %s\n", reason.c_str(), path.c_str());
|
||||
fprintf(stderr, "[hang] %s. Couldn't write the hang report to %s\n", reason.c_str(), path.string().c_str());
|
||||
ui::Toast(std::format("The game seems to have stopped. A hang report was saved; to send it: {}.", detail::kSaveBugReportHint), 20.0);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The crash report's unhandled-exception filter, put back if a DLL
|
||||
// loaded since (a driver, an overlay) installed its own over it.
|
||||
void KeepCrashFilter()
|
||||
{
|
||||
static std::atomic<int> replaced{ 0 };
|
||||
if (!platform::KeepCrashFilter() && replaced.fetch_add(1) < 3)
|
||||
fprintf(stderr, "[crash] something replaced the crash report's exception filter; it is back\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
void Watchdog(Thresholds limits)
|
||||
{
|
||||
SetHostThreadName("nfsmw-watchdog");
|
||||
@@ -280,15 +409,23 @@ namespace report
|
||||
auto lastTick = now, lastFrameAt = now, lastFrameSeen = now, lastReadAt = now, resetAt = now;
|
||||
uint64_t lastFrames = gpu::FrameCount(), lastBytes = g_fileBytesRead.load(std::memory_order_relaxed);
|
||||
uint64_t gapBytes = lastBytes; // read since the last frame
|
||||
uint64_t lastPipelines = g_pipelinesBuilt.load(std::memory_order_relaxed);
|
||||
uint64_t gapPipelines = lastPipelines; // compiled since the last frame
|
||||
uint64_t lastEpoch = guesttime::Epoch(); // suspensions so far
|
||||
// Bytes read per tick over the last 10 s (20 ticks).
|
||||
uint64_t window[20] = {};
|
||||
// Bytes read (and pipelines compiled) per tick over the last 10 s
|
||||
// (20 ticks).
|
||||
uint64_t window[20] = {}, pipelineWindow[20] = {};
|
||||
size_t windowPos = 0;
|
||||
bool episode = false, pausedInGap = false;
|
||||
int episodes = 0;
|
||||
while (true)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
if (g_endingWithMessage.load(std::memory_order_relaxed))
|
||||
continue; // (the game is ending with a message: no hang to report)
|
||||
#ifdef _WIN32
|
||||
KeepCrashFilter();
|
||||
#endif
|
||||
now = clock::now();
|
||||
// Stopped (a debugger, SIGSTOP) or the machine slept: start over.
|
||||
if (seconds(now - lastTick) > 5.0)
|
||||
@@ -299,13 +436,19 @@ namespace report
|
||||
lastTick = now;
|
||||
|
||||
uint64_t bytes = g_fileBytesRead.load(std::memory_order_relaxed);
|
||||
uint64_t pipelines = g_pipelinesBuilt.load(std::memory_order_relaxed);
|
||||
pipelineWindow[windowPos % 20] = pipelines - lastPipelines;
|
||||
window[windowPos++ % 20] = bytes - lastBytes;
|
||||
if (bytes != lastBytes)
|
||||
lastReadAt = now;
|
||||
lastBytes = bytes;
|
||||
uint64_t recentBytes = 0;
|
||||
lastPipelines = pipelines;
|
||||
uint64_t recentBytes = 0, recentPipelines = 0;
|
||||
for (uint64_t b : window)
|
||||
recentBytes += b;
|
||||
for (uint64_t n : pipelineWindow)
|
||||
recentPipelines += n;
|
||||
const bool compiling = kCompilesAreLoading && recentPipelines > 0;
|
||||
|
||||
const bool visible = video::WindowVisible();
|
||||
// Suspended now, or at any time since the last tick however
|
||||
@@ -321,6 +464,9 @@ namespace report
|
||||
double gap = seconds(now - lastFrameAt);
|
||||
if (episode)
|
||||
fprintf(stderr, "[watchdog] frames again, %.1f s after the last one\n", seconds(now - lastFrameSeen));
|
||||
else if (gap >= 2.0 && !pausedInGap && kCompilesAreLoading)
|
||||
fprintf(stderr, "[watchdog] %.1f s without a new frame (%.1f MB read, %llu pipelines compiled meanwhile)\n",
|
||||
gap, double(bytes - gapBytes) / (1 << 20), (unsigned long long)(pipelines - gapPipelines));
|
||||
else if (gap >= 2.0 && !pausedInGap)
|
||||
fprintf(stderr, "[watchdog] %.1f s without a new frame (%.1f MB read meanwhile)\n", gap,
|
||||
double(bytes - gapBytes) / (1 << 20));
|
||||
@@ -329,6 +475,7 @@ namespace report
|
||||
lastFrames = frames;
|
||||
lastFrameAt = lastFrameSeen = now;
|
||||
gapBytes = bytes;
|
||||
gapPipelines = pipelines;
|
||||
}
|
||||
if (paused)
|
||||
{
|
||||
@@ -357,7 +504,7 @@ namespace report
|
||||
std::string reason;
|
||||
if (limits.cpWait > 0 && waited >= limits.cpWait)
|
||||
reason = std::format("the command processor has waited {:.0f} s in one WAIT_REG_MEM", waited);
|
||||
else if (limits.noFrame > 0 && noFrame >= limits.noFrame && recentBytes < kLoadingBytes)
|
||||
else if (limits.noFrame > 0 && noFrame >= limits.noFrame && recentBytes < kLoadingBytes && !compiling)
|
||||
reason = std::format("no new frame for {:.0f} s, and nothing is loading", noFrame);
|
||||
if (reason.empty())
|
||||
continue;
|
||||
@@ -497,7 +644,22 @@ namespace report
|
||||
{
|
||||
// A thread with a guest context but no name of its own (the vsync thread).
|
||||
char name[32] = "";
|
||||
#ifdef _WIN32
|
||||
// Its description (SetThreadDescription), if anything gave it one.
|
||||
using GetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PWSTR*);
|
||||
static const auto getDescription = reinterpret_cast<GetThreadDescriptionFn>(
|
||||
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetThreadDescription"));
|
||||
PWSTR description = nullptr;
|
||||
if (getDescription && SUCCEEDED(getDescription(GetCurrentThread(), &description)) && description)
|
||||
{
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, description, -1, name, int(sizeof(name)), nullptr, nullptr) == 0)
|
||||
name[0] = 0; // (longer than a slot's name: none)
|
||||
name[sizeof(name) - 1] = 0;
|
||||
LocalFree(description);
|
||||
}
|
||||
#else
|
||||
pthread_getname_np(pthread_self(), name, sizeof(name));
|
||||
#endif
|
||||
snprintf(t_name, sizeof(t_name), "%s", name[0] ? name : "unnamed");
|
||||
RegisterLocked(t_name);
|
||||
}
|
||||
@@ -509,6 +671,9 @@ namespace report
|
||||
{
|
||||
if (s_started.exchange(true))
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
KeepCrashFilter(); // the window and the GPU's driver are up by now
|
||||
#endif
|
||||
ArmTestSwitches();
|
||||
Thresholds limits;
|
||||
if (const char* v = std::getenv("NFSMW_HANG_SECS"))
|
||||
|
||||
@@ -528,7 +528,11 @@ namespace report
|
||||
return false;
|
||||
|
||||
struct tm local{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&local, &modified);
|
||||
#else
|
||||
localtime_r(&modified, &local);
|
||||
#endif
|
||||
int year = std::max(local.tm_year + 1900, 1980);
|
||||
Entry e;
|
||||
e.name = name;
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <platform/os.h> // the OS headers: POSIX, or <windows.h> and a few POSIX names
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h> // TARGET_OS_IOS: __APPLE__ alone means macOS or iOS
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include <future>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#ifdef _WIN32
|
||||
#include <platform/folders.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
#include <platform/ios_files.h>
|
||||
@@ -134,6 +137,14 @@ namespace ui
|
||||
}
|
||||
#else
|
||||
static const SDL_DialogFileFilter filters[] = { { "Xbox 360 disc image", "iso" }, { "All files", "*" } };
|
||||
#ifdef _WIN32
|
||||
// Downloads, where a disc image most likely is (never HOME: Git
|
||||
// Bash and MSYS2 set it to their own idea of home).
|
||||
static const std::string start = platform::GetKnownFolder(platform::KnownFolder::Downloads).string();
|
||||
const char* startAt = start.empty() ? nullptr : start.c_str();
|
||||
#else
|
||||
const char* startAt = std::getenv("HOME");
|
||||
#endif
|
||||
if (purpose == DialogFor::Destination)
|
||||
{
|
||||
// Where to install: titled as such (where the platform shows a
|
||||
@@ -146,8 +157,8 @@ namespace ui
|
||||
// Portable mode: it opens in SpeedBreaker's folder, where the game moves with it.
|
||||
if (const portable::Layout& layout = portable::Current(); layout.Active())
|
||||
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, layout.root.string().c_str());
|
||||
else if (const char* home = std::getenv("HOME"))
|
||||
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, home);
|
||||
else if (startAt)
|
||||
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, startAt);
|
||||
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Choose where to install the game");
|
||||
#ifdef __APPLE__
|
||||
SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, "Choose");
|
||||
@@ -156,9 +167,9 @@ namespace ui
|
||||
SDL_DestroyProperties(props);
|
||||
}
|
||||
else if (folder)
|
||||
SDL_ShowOpenFolderDialog(OnDialog, request, GetWindow(), std::getenv("HOME"), false);
|
||||
SDL_ShowOpenFolderDialog(OnDialog, request, GetWindow(), startAt, false);
|
||||
else
|
||||
SDL_ShowOpenFileDialog(OnDialog, request, GetWindow(), filters, 2, std::getenv("HOME"), false);
|
||||
SDL_ShowOpenFileDialog(OnDialog, request, GetWindow(), filters, 2, startAt, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -281,6 +292,11 @@ namespace ui
|
||||
#elif defined(__APPLE__)
|
||||
const char* kWhereToPut = "Put your .iso file in a Games folder in your home folder and choose Rescan, or browse for it.";
|
||||
const char* kScanning = "Looking for disc images in ~/Games...";
|
||||
#elif defined(_WIN32)
|
||||
// The folders install::DiscImageSearchFolders reads on Windows.
|
||||
const char* kWhereToPut = "Put your .iso file in your user folder, Downloads, Desktop, Documents, a Games folder in "
|
||||
"your user folder or the top folder of another drive and choose Rescan, or browse for it.";
|
||||
const char* kScanning = "Looking for disc images in your user folder, Downloads, Desktop, Documents and other drives...";
|
||||
#else
|
||||
const char* kWhereToPut = "Put your .iso file in your home folder, Downloads, a Games folder, or on an SD card or USB "
|
||||
"drive and choose Rescan, or browse for it.";
|
||||
@@ -289,6 +305,9 @@ namespace ui
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
const char* kNoDialog = "The Files picker couldn't be shown. Copy your .iso into SpeedBreaker's folder in the Files "
|
||||
"app (On My iPad or On My iPhone > SpeedBreaker) and choose Rescan.";
|
||||
#elif defined(_WIN32)
|
||||
const char* kNoDialog = "The file browser couldn't be opened. Put the .iso in Downloads, Desktop or Documents and "
|
||||
"choose Rescan, or type its path below.";
|
||||
#else
|
||||
const char* kNoDialog = "The file browser isn't available here (in Game Mode, for example). Put the .iso in Downloads, "
|
||||
"a Games folder or on an SD card and choose Rescan, or type its path below (Steam + X shows "
|
||||
@@ -406,7 +425,7 @@ namespace ui
|
||||
if (job->abandoned)
|
||||
{
|
||||
release = std::exchange(job->access, 0);
|
||||
fprintf(stderr, "[installer] the check of %s that was left behind has returned\n", path.c_str());
|
||||
fprintf(stderr, "[installer] the check of %s that was left behind has returned\n", path.string().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -733,7 +752,7 @@ namespace ui
|
||||
{
|
||||
check->abandoned = true;
|
||||
check->access = std::exchange(access, 0);
|
||||
fprintf(stderr, "[installer] left the check of %s still reading\n", sourcePath.c_str());
|
||||
fprintf(stderr, "[installer] left the check of %s still reading\n", sourcePath.string().c_str());
|
||||
}
|
||||
}
|
||||
check.reset();
|
||||
@@ -845,7 +864,7 @@ namespace ui
|
||||
installedPath = st.destination;
|
||||
// So the next launch finds it (internal storage clears the record).
|
||||
if (!install::RecordInstallPath(st.destination))
|
||||
fprintf(stderr, "[installer] couldn't record the install location %s\n", st.destination.c_str());
|
||||
fprintf(stderr, "[installer] couldn't record the install location %s\n", st.destination.string().c_str());
|
||||
st.Go(Page::Done);
|
||||
}
|
||||
else if (st.result.error == install::Error::Cancelled)
|
||||
|
||||
@@ -692,7 +692,11 @@ namespace ui
|
||||
// or to the reason it failed. The toast has the whole text.
|
||||
if (ImGui::CalcTextSize(message.c_str()).x > ImGui::GetContentRegionAvail().x)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
size_t cut = saved ? message.find_last_of("\\/") : message.rfind(": ");
|
||||
#else
|
||||
size_t cut = saved ? message.rfind('/') : message.rfind(": ");
|
||||
#endif
|
||||
if (saved && cut == std::string::npos && (cut = message.rfind(": ")) != std::string::npos)
|
||||
cut++; // iOS: "Saved to the Files app, SpeedBreaker folder: <name>"
|
||||
if (cut != std::string::npos)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <stdafx.h>
|
||||
#include "paths.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
namespace
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
@@ -35,10 +36,15 @@ namespace
|
||||
linkEc ? std::format("; no link at the old name ({})", linkEc.message()) : ", the old name now a link to it");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string MigrateUserFolders()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return {}; // no nfsmw-recomp build ever ran on Windows: nothing to rename
|
||||
#else
|
||||
if (portable::Current().Active())
|
||||
return {}; // the home folder's data isn't SpeedBreaker's in portable mode: leave it as it is
|
||||
return MigrateFolder(UserDataBase(), DataFolderName()) + MigrateFolder(CacheBase(), CacheFolderName());
|
||||
#endif
|
||||
}
|
||||
+30
-4
@@ -6,6 +6,9 @@
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include <platform/folders.h>
|
||||
#endif
|
||||
|
||||
#include "portable.h"
|
||||
|
||||
@@ -32,12 +35,14 @@ inline std::filesystem::path GetGamePath()
|
||||
return "game/files";
|
||||
}
|
||||
|
||||
// The per-user folders are named for the project: SpeedBreaker (macOS) or
|
||||
// speedbreaker (Linux), nfsmw-recomp until 2026-10. MigrateUserFolders
|
||||
// (user/paths.cpp, first thing at startup) renames an old folder; where that
|
||||
// fails, the old one is used as it is.
|
||||
// The per-user folders are named for the project: SpeedBreaker (macOS,
|
||||
// Windows) or speedbreaker (Linux), nfsmw-recomp until 2026-10.
|
||||
// MigrateUserFolders (user/paths.cpp, first thing at startup) renames an old
|
||||
// folder; where that fails, the old one is used as it is.
|
||||
#ifdef __APPLE__
|
||||
inline constexpr const char* kUserFolderName = "SpeedBreaker";
|
||||
#elif defined(_WIN32)
|
||||
inline constexpr const char* kUserFolderName = "SpeedBreaker";
|
||||
#else
|
||||
inline constexpr const char* kUserFolderName = "speedbreaker";
|
||||
#endif
|
||||
@@ -61,8 +66,18 @@ inline const char*& CacheFolderName()
|
||||
// iOS: the same in the app's container; $XDG_DATA_HOME and
|
||||
// $XDG_CACHE_HOME when set (test runs: iOS sets HOME itself)
|
||||
// Linux: $XDG_DATA_HOME or ~/.local/share, $XDG_CACHE_HOME or ~/.cache
|
||||
// Windows: %LOCALAPPDATA% (per user, never roams: a 6.6 GB install must
|
||||
// not), caches in its SpeedBreaker\Cache; $XDG_DATA_HOME and
|
||||
// $XDG_CACHE_HOME when set (test runs). Never HOME, which Git Bash
|
||||
// and MSYS2 set: where the data went would depend on the launcher.
|
||||
inline std::filesystem::path UserDataBase()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (const char* xdg = std::getenv("XDG_DATA_HOME"))
|
||||
return xdg;
|
||||
std::filesystem::path local = platform::GetKnownFolder(platform::KnownFolder::LocalAppData);
|
||||
return local.empty() ? std::filesystem::path(".") : local;
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
std::filesystem::path base = home ? home : ".";
|
||||
#ifdef __APPLE__
|
||||
@@ -76,10 +91,16 @@ inline std::filesystem::path UserDataBase()
|
||||
return xdg;
|
||||
return base / ".local/share";
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::filesystem::path CacheBase()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (const char* xdg = std::getenv("XDG_CACHE_HOME"))
|
||||
return xdg;
|
||||
return UserDataBase(); // (GetCachePath adds SpeedBreaker\Cache)
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
std::filesystem::path base = home ? home : ".";
|
||||
#ifdef __APPLE__
|
||||
@@ -93,6 +114,7 @@ inline std::filesystem::path CacheBase()
|
||||
return xdg;
|
||||
return base / ".cache";
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// This computer's own data and cache folders, portable mode or not
|
||||
@@ -104,6 +126,10 @@ inline std::filesystem::path HomeUserPath()
|
||||
|
||||
inline std::filesystem::path HomeCachePath()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!std::getenv("XDG_CACHE_HOME"))
|
||||
return CacheBase() / CacheFolderName() / "Cache";
|
||||
#endif
|
||||
return CacheBase() / CacheFolderName();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ namespace portable
|
||||
return p;
|
||||
}
|
||||
|
||||
// The record's text: UTF-8 everywhere (on Windows path.string() is
|
||||
// the code page's, UTF-8 only under the exe's manifest).
|
||||
std::string Utf8(const std::u8string& text)
|
||||
{
|
||||
return std::string(text.begin(), text.end());
|
||||
}
|
||||
|
||||
// `relative` stays inside the folder it's relative to (not "", ".",
|
||||
// or "../...").
|
||||
bool Inside(const fs::path& relative)
|
||||
@@ -57,8 +64,8 @@ namespace portable
|
||||
fs::path path = Normal(folder);
|
||||
if (layout.Active())
|
||||
if (fs::path relative = path.lexically_relative(layout.root); Inside(relative))
|
||||
return relative.generic_string();
|
||||
return path.string();
|
||||
return Utf8(relative.generic_u8string());
|
||||
return Utf8(path.u8string());
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> ParseRecord(std::string_view text, const Layout& layout)
|
||||
@@ -67,7 +74,8 @@ namespace portable
|
||||
text.remove_suffix(1);
|
||||
if (text.empty())
|
||||
return std::nullopt;
|
||||
fs::path path{ std::string(text) };
|
||||
// UTF-8 (RecordText), whatever the system's code page is on Windows.
|
||||
fs::path path{ std::u8string(text.begin(), text.end()) };
|
||||
// (Records before v0.1.1 kept `--dest dir/`'s trailing separator.)
|
||||
if (path.is_absolute())
|
||||
return Normal(path);
|
||||
|
||||
+73
-29
@@ -24,7 +24,13 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <platform/file_io.h>
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h> // TARGET_OS_IOS: __APPLE__ alone means macOS or iOS
|
||||
#endif
|
||||
@@ -483,6 +489,7 @@ namespace settings
|
||||
double s_baseline[kCount]{}; // stored values as last loaded or saved (Dirty)
|
||||
std::atomic<double> s_deviceDefault[kCount]{}; // SetDeviceDefault's, where s_hasDeviceDefault
|
||||
std::atomic<bool> s_hasDeviceDefault[kCount]{};
|
||||
bool s_inFile[kCount]{}; // settings.toml holds it (the last Load read it, or a Save wrote it)
|
||||
bool s_frozen = false; // Restart options hold their launch values (first Load)
|
||||
std::mutex s_saveMutex; // whole saves, so the last snapshot is the last file
|
||||
|
||||
@@ -944,7 +951,7 @@ namespace settings
|
||||
// of a regular file.
|
||||
ReadStatus ReadFile(const std::filesystem::path& path, std::string& out, std::string& error)
|
||||
{
|
||||
int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
|
||||
int fd = platform::OpenFile(path, O_RDONLY | O_CLOEXEC | O_NONBLOCK);
|
||||
if (fd < 0)
|
||||
{
|
||||
if (errno == ENOENT)
|
||||
@@ -952,15 +959,15 @@ namespace settings
|
||||
error = strerror(errno);
|
||||
return ReadStatus::Failed;
|
||||
}
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0)
|
||||
platform::FileInfo st;
|
||||
if (platform::InfoOf(fd, st) != 0)
|
||||
error = strerror(errno);
|
||||
else if (S_ISDIR(st.st_mode))
|
||||
else if (st.directory)
|
||||
error = "it's a folder";
|
||||
else if (!S_ISREG(st.st_mode))
|
||||
else if (!st.regular)
|
||||
error = "it isn't a regular file";
|
||||
else if (uint64_t(st.st_size) > kMaxFileSize)
|
||||
error = std::format("it's {} KB, too big for a settings file", uint64_t(st.st_size) / 1024);
|
||||
else if (st.size > kMaxFileSize)
|
||||
error = std::format("it's {} KB, too big for a settings file", st.size / 1024);
|
||||
if (!error.empty())
|
||||
{
|
||||
close(fd);
|
||||
@@ -1131,13 +1138,13 @@ namespace settings
|
||||
RemoveStaleTemps(target);
|
||||
std::filesystem::path temp = target;
|
||||
temp += std::format(".{}.tmp", long(getpid()));
|
||||
int fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
int fd = platform::OpenFile(temp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
if (fd < 0 && errno == EEXIST)
|
||||
{
|
||||
// Saves within a process are serialised, so this was left by
|
||||
// a crashed process that had the same pid.
|
||||
unlink(temp.c_str());
|
||||
fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
platform::RemoveFile(temp);
|
||||
fd = platform::OpenFile(temp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
}
|
||||
if (fd < 0)
|
||||
return std::format("can't write {}: {}", target.string(), strerror(errno));
|
||||
@@ -1165,20 +1172,25 @@ namespace settings
|
||||
error = errno;
|
||||
if (error)
|
||||
{
|
||||
unlink(temp.c_str());
|
||||
platform::RemoveFile(temp);
|
||||
return std::format("can't write {}: {}", target.string(), strerror(error));
|
||||
}
|
||||
if (rename(temp.c_str(), target.c_str()) != 0)
|
||||
// (Windows: MoveFileExW, which replaces the old file in one step
|
||||
// too; a plain _wrename would fail on every save after the first.)
|
||||
if ((error = platform::AtomicReplace(temp, target)) != 0)
|
||||
{
|
||||
error = errno;
|
||||
unlink(temp.c_str());
|
||||
platform::RemoveFile(temp);
|
||||
return std::format("can't replace {}: {}", target.string(), strerror(error));
|
||||
}
|
||||
#ifdef _WIN32
|
||||
platform::SyncDirectory(dir); // (nothing to do on NTFS)
|
||||
#else
|
||||
if (int dirFd = open(dir.empty() ? "." : dir.c_str(), O_RDONLY | O_CLOEXEC); dirFd >= 0)
|
||||
{
|
||||
fsync(dirFd); // best effort: some file systems refuse it on directories
|
||||
close(dirFd);
|
||||
}
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1260,13 +1272,41 @@ namespace settings
|
||||
std::optional<double> v = Normalize(o, value);
|
||||
if (!v)
|
||||
return;
|
||||
std::string note;
|
||||
bool changed = false;
|
||||
{
|
||||
std::lock_guard lock(s_mutex);
|
||||
double previous = Default(i);
|
||||
s_deviceDefault[i].store(*v, std::memory_order_relaxed);
|
||||
s_hasDeviceDefault[i].store(true, std::memory_order_release);
|
||||
if (detail::g_state.load(std::memory_order_relaxed) == 2)
|
||||
{
|
||||
// After Load (a GPU's defaults: its device exists only once
|
||||
// the window does): the stored value becomes what Load would
|
||||
// have given, unless the file holds the option, something
|
||||
// set it since, or it applies at a restart.
|
||||
double stored = s_stored[i].load(std::memory_order_relaxed);
|
||||
if (s_inFile[i])
|
||||
note = std::format("; settings.toml's {} stays", TomlValue(o, stored));
|
||||
else if (stored != previous || s_baseline[i] != previous)
|
||||
note = std::format("; {} (set since the start) stays", TomlValue(o, stored));
|
||||
else if (o.apply == Apply::Restart)
|
||||
note = "; too late for this run's stored value";
|
||||
else if (stored != *v)
|
||||
{
|
||||
s_stored[i].store(*v, std::memory_order_relaxed);
|
||||
s_baseline[i] = *v; // not a change to save: Save would write it, a later Load give it
|
||||
changed = RecomputeLocked(i);
|
||||
detail::g_generation.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
if (s_envSet[i].load(std::memory_order_relaxed))
|
||||
note += std::format("; {} overrides it for this run", o.env);
|
||||
}
|
||||
fprintf(stderr, "[settings] %s.%s defaults to %s on this device (%s)%s\n", o.table, o.key, TomlValue(o, *v).c_str(),
|
||||
std::string(why).c_str(), detail::g_state.load(std::memory_order_acquire) == 2 ? "; too late for this run's stored value" : "");
|
||||
std::string(why).c_str(), note.c_str());
|
||||
if (changed)
|
||||
Notify({ &id, 1 });
|
||||
}
|
||||
|
||||
double DefaultValue(Id id)
|
||||
@@ -1457,10 +1497,10 @@ namespace settings
|
||||
size_t fromFile = 0;
|
||||
ReadStatus status = ReadFile(path, text, readError);
|
||||
if (status == ReadStatus::Missing)
|
||||
fprintf(stderr, "[settings] no %s yet: defaults\n", path.c_str());
|
||||
fprintf(stderr, "[settings] no %s yet: defaults\n", path.string().c_str());
|
||||
else if (status == ReadStatus::Failed)
|
||||
{
|
||||
fprintf(stderr, "[settings] can't read %s: %s; defaults\n", path.c_str(), readError.c_str());
|
||||
fprintf(stderr, "[settings] can't read %s: %s; defaults\n", path.string().c_str(), readError.c_str());
|
||||
lastError = std::format("Couldn't read {}: {}. The settings are at their defaults.", path.string(), readError);
|
||||
}
|
||||
else
|
||||
@@ -1495,8 +1535,8 @@ namespace settings
|
||||
bad += ".bad";
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(path, bad, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
fprintf(stderr, "[settings] %s doesn't parse: %s (line %u, column %u); defaults (kept as %s)\n", path.c_str(),
|
||||
error.c_str(), unsigned(where.line), unsigned(where.column), ec ? "nothing: the copy failed" : bad.c_str());
|
||||
fprintf(stderr, "[settings] %s doesn't parse: %s (line %u, column %u); defaults (kept as %s)\n", path.string().c_str(),
|
||||
error.c_str(), unsigned(where.line), unsigned(where.column), ec ? "nothing: the copy failed" : bad.string().c_str());
|
||||
lastError = std::format("{} has a mistake at line {}, column {} ({}). The settings are at their defaults; {}.",
|
||||
path.string(), where.line, where.column, error,
|
||||
ec ? "saving them will replace the file" : std::format("the file was kept as {}", bad.string()));
|
||||
@@ -1506,7 +1546,7 @@ namespace settings
|
||||
const toml::table* table = tableNode.as_table();
|
||||
if (!table)
|
||||
{
|
||||
fprintf(stderr, "[settings] %s: %s outside a table; ignored\n", path.filename().c_str(),
|
||||
fprintf(stderr, "[settings] %s: %s outside a table; ignored\n", path.filename().string().c_str(),
|
||||
std::string(tableKey.str()).c_str());
|
||||
continue;
|
||||
}
|
||||
@@ -1518,7 +1558,7 @@ namespace settings
|
||||
if (auto v = node.as_string())
|
||||
notices = ParseRates(v->get());
|
||||
else
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't a list of rates; ignored\n", path.filename().c_str(), name.c_str(),
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't a list of rates; ignored\n", path.filename().string().c_str(), name.c_str(),
|
||||
NodeText(node).c_str());
|
||||
continue;
|
||||
}
|
||||
@@ -1527,7 +1567,7 @@ namespace settings
|
||||
if (auto v = node.as_boolean())
|
||||
rumbleNote = v->get();
|
||||
else
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't true or false; ignored\n", path.filename().c_str(), name.c_str(),
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't true or false; ignored\n", path.filename().string().c_str(), name.c_str(),
|
||||
NodeText(node).c_str());
|
||||
continue;
|
||||
}
|
||||
@@ -1536,14 +1576,14 @@ namespace settings
|
||||
if (auto v = node.as_string())
|
||||
lastVersion = v->get();
|
||||
else
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't a version; ignored\n", path.filename().c_str(), name.c_str(),
|
||||
fprintf(stderr, "[settings] %s: %s = %s isn't a version; ignored\n", path.filename().string().c_str(), name.c_str(),
|
||||
NodeText(node).c_str());
|
||||
continue;
|
||||
}
|
||||
const Option* o = Find(tableKey.str(), key.str());
|
||||
if (!o)
|
||||
{
|
||||
fprintf(stderr, "[settings] %s: unknown key %s; ignored\n", path.filename().c_str(), name.c_str());
|
||||
fprintf(stderr, "[settings] %s: unknown key %s; ignored\n", path.filename().string().c_str(), name.c_str());
|
||||
continue;
|
||||
}
|
||||
std::string source = std::format("{}: {} = {}", path.filename().string(), name, NodeText(node));
|
||||
@@ -1555,7 +1595,7 @@ namespace settings
|
||||
}
|
||||
}
|
||||
if (error.empty())
|
||||
fprintf(stderr, "[settings] %s: %zu value%s from the file\n", path.c_str(), fromFile, fromFile == 1 ? "" : "s");
|
||||
fprintf(stderr, "[settings] %s: %zu value%s from the file\n", path.string().c_str(), fromFile, fromFile == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
std::vector<Id> changed;
|
||||
@@ -1566,6 +1606,7 @@ namespace settings
|
||||
double stored = fileValues[i].value_or(Default(i));
|
||||
s_stored[i].store(stored, std::memory_order_relaxed);
|
||||
s_baseline[i] = stored;
|
||||
s_inFile[i] = fileValues[i].has_value();
|
||||
ReadEnvLocked(i, true);
|
||||
if (RecomputeLocked(i))
|
||||
changed.push_back(Id(i));
|
||||
@@ -1616,6 +1657,8 @@ namespace settings
|
||||
std::copy(values.begin(), values.end(), s_baseline);
|
||||
if (LoadedPath().empty())
|
||||
LoadedPath() = path;
|
||||
if (path == LoadedPath())
|
||||
std::fill(std::begin(s_inFile), std::end(s_inFile), true); // a save writes every option
|
||||
}
|
||||
}
|
||||
if (!error.empty())
|
||||
@@ -1623,7 +1666,7 @@ namespace settings
|
||||
fprintf(stderr, "[settings] %s\n", error.c_str());
|
||||
return false;
|
||||
}
|
||||
fprintf(stderr, "[settings] saved %s\n", path.c_str());
|
||||
fprintf(stderr, "[settings] saved %s\n", path.string().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1669,7 +1712,7 @@ namespace settings
|
||||
ReadStatus status = ReadFile(path, text, error);
|
||||
if (status == ReadStatus::Failed)
|
||||
{
|
||||
fprintf(stderr, "[settings] the version isn't recorded: can't read %s: %s\n", path.c_str(), error.c_str());
|
||||
fprintf(stderr, "[settings] the version isn't recorded: can't read %s: %s\n", path.string().c_str(), error.c_str());
|
||||
return false;
|
||||
}
|
||||
if (status == ReadStatus::Missing)
|
||||
@@ -1727,7 +1770,8 @@ namespace settings
|
||||
ReadStatus status = ReadFile(path, text, error);
|
||||
if (status == ReadStatus::Failed)
|
||||
{
|
||||
fprintf(stderr, "[settings] the refresh notice's %d Hz isn't recorded: can't read %s: %s\n", hz, path.c_str(), error.c_str());
|
||||
fprintf(stderr, "[settings] the refresh notice's %d Hz isn't recorded: can't read %s: %s\n", hz, path.string().c_str(),
|
||||
error.c_str());
|
||||
return false;
|
||||
}
|
||||
if (status == ReadStatus::Missing)
|
||||
@@ -1774,7 +1818,7 @@ namespace settings
|
||||
ReadStatus status = ReadFile(path, text, error);
|
||||
if (status == ReadStatus::Failed)
|
||||
{
|
||||
fprintf(stderr, "[settings] the rumble note isn't recorded: can't read %s: %s\n", path.c_str(), error.c_str());
|
||||
fprintf(stderr, "[settings] the rumble note isn't recorded: can't read %s: %s\n", path.string().c_str(), error.c_str());
|
||||
return false;
|
||||
}
|
||||
if (status == ReadStatus::Missing)
|
||||
|
||||
@@ -247,11 +247,14 @@ namespace settings
|
||||
double GetStoredValue(Id id);
|
||||
bool IsDefault(Id id); // stored value == default
|
||||
|
||||
// Main thread, before Load(): `value` is the option's default on this
|
||||
// device instead of the table's (Frame Rate's Auto on an iPhone): what a
|
||||
// settings.toml without the option gives, and what Reset and IsDefault
|
||||
// use. Logged, with `why`. A settings.toml that a save wrote holds every
|
||||
// option, so it keeps the value it has.
|
||||
// Main thread: `value` is the option's default on this device instead of
|
||||
// the table's (Frame Rate's Auto on an iPhone): what a settings.toml
|
||||
// without the option gives, and what Reset and IsDefault use. Logged,
|
||||
// with `why`. A settings.toml that a save wrote holds every option, so
|
||||
// it keeps the value it has. Before Load(), or after it for a GPU's
|
||||
// defaults (its device exists only once the window does): then an
|
||||
// option the file didn't hold, unchanged since and applied live, takes
|
||||
// the new default at once (listeners are told; not a change to save).
|
||||
void SetDeviceDefault(Id id, double value, std::string_view why);
|
||||
// The option's default on this device: the table's, or SetDeviceDefault's.
|
||||
double DefaultValue(Id id);
|
||||
|
||||
+257
-7
@@ -41,6 +41,10 @@
|
||||
#include <report/report.h>
|
||||
#include <platform/thermal.h>
|
||||
#include <platform/quit_signals.h>
|
||||
#ifdef _WIN32
|
||||
#include "repeats.h"
|
||||
#include <platform/timer.h>
|
||||
#endif
|
||||
#include <kernel/memory_status.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -191,6 +195,31 @@ namespace video
|
||||
float s_lastScaleX = 1.0f, s_lastScaleY = 1.0f;
|
||||
bool s_lastFreshImage = false; // the frame being drawn is a new GPU frame
|
||||
std::string s_resolutionHint; // Game Mode below the monitor's resolution
|
||||
#ifdef _WIN32
|
||||
// The display the window is on, as SDL reports it (NoteDisplay): its
|
||||
// refresh as a period, for the waiter thread's repeat count (0:
|
||||
// unknown), and what was last logged. Main thread, but for the period.
|
||||
std::atomic<double> s_panelPeriodNs{ 0.0 };
|
||||
SDL_DisplayID s_notedDisplay = 0;
|
||||
int s_notedW = 0, s_notedH = 0;
|
||||
float s_notedHz = -1.0f;
|
||||
// The system summary's "Present" (and bug reports'): the swapchain's
|
||||
// present mode and images, and present timing's extensions.
|
||||
std::string s_presentText, s_timingText;
|
||||
void NoteDisplay(const char* why);
|
||||
// NFSMW_TEST_DISPLAY_HZ=<hz>: the rate the display is taken to run at
|
||||
// instead of SDL's (0: unset), for the log, the repeat count, the
|
||||
// refresh multiple and the refresh-rate notice, tested on a 60 Hz
|
||||
// laptop.
|
||||
float TestDisplayHz()
|
||||
{
|
||||
static const float hz = [] {
|
||||
const char* v = std::getenv("NFSMW_TEST_DISPLAY_HZ");
|
||||
return v ? float(std::atof(v)) : 0.0f;
|
||||
}();
|
||||
return hz;
|
||||
}
|
||||
#endif
|
||||
std::atomic<bool> s_focused{ true };
|
||||
std::atomic<bool> s_visible{ true }; // not minimized, hidden or covered
|
||||
// The app is inactive or in the background (iOS, and other systems
|
||||
@@ -553,6 +582,19 @@ namespace video
|
||||
}
|
||||
if (lastOnScreen)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// The last frame's time on screen against the time its
|
||||
// vblanks gave it, in the panel's own refreshes (SDL's rate;
|
||||
// the fit's period without one): a 144 Hz panel shows a 60 fps
|
||||
// frame for 2 or 3 refreshes, 13.9 or 20.8 ms against 16.7,
|
||||
// and each 3 counts one (the judder: the work notes' 144HZ-NOTE.md);
|
||||
// 1/60 s rounded both to one period, so it counted none.
|
||||
// At 60 Hz this is the count it always was (video/repeats.h).
|
||||
double panelNs = s_panelPeriodNs.load(std::memory_order_relaxed);
|
||||
double refreshNs = panelNs > 0.0 ? panelNs : display.valid ? display.periodNs : 1e9 / 60.0;
|
||||
int64_t vblanks = std::max<int64_t>(1, std::llround(double(t.vblank - lastVblank) / vblank.PeriodNs()));
|
||||
s_repeated += uint64_t(ExtraRefreshes(double(onScreen - lastOnScreen), vblanks, vblank.PeriodNs(), refreshNs));
|
||||
#else
|
||||
// The last frame's time on screen, against the vblanks the
|
||||
// game gave it (a gap of more than 0.5 s is a pause, not a repeat).
|
||||
// At a multiple of 60 Hz (refresh_fit.h) a vblank is that
|
||||
@@ -563,6 +605,7 @@ namespace video
|
||||
const int64_t multiple = display.valid ? display.multiple : 1;
|
||||
if (shownFor > vblanks * multiple && shownFor <= 30 * multiple)
|
||||
s_repeated += uint64_t(shownFor - vblanks * multiple);
|
||||
#endif
|
||||
}
|
||||
lastOnScreen = onScreen;
|
||||
lastVblank = t.vblank;
|
||||
@@ -675,7 +718,11 @@ namespace video
|
||||
#else
|
||||
constexpr bool kApple = false;
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
const double hz = TestDisplayHz() > 0.0f ? double(TestDisplayHz()) : mode ? double(mode->refresh_rate) : 0.0;
|
||||
#else
|
||||
const double hz = mode ? double(mode->refresh_rate) : 0.0;
|
||||
#endif
|
||||
const int multiple = policy.For(hz, kApple);
|
||||
s_refreshMultiple.store(multiple, std::memory_order_relaxed);
|
||||
// The refresh-rate notice: the mode's rate and since when; the
|
||||
@@ -851,6 +898,37 @@ namespace video
|
||||
vkGetSwapchainImagesKHR(s_device, s_swapchain, &count, nullptr);
|
||||
s_swapImages.resize(count);
|
||||
vkGetSwapchainImagesKHR(s_device, s_swapchain, &count, s_swapImages.data());
|
||||
#ifdef _WIN32
|
||||
{
|
||||
// The present mode with what else the surface offers, and
|
||||
// the images the driver made (once per change; the system
|
||||
// summary's "Present").
|
||||
uint32_t modeCount = 0;
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(s_physical, s_surface, &modeCount, nullptr);
|
||||
std::vector<VkPresentModeKHR> modes(modeCount);
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(s_physical, s_surface, &modeCount, modes.data());
|
||||
std::string offered;
|
||||
for (VkPresentModeKHR m : modes)
|
||||
offered += std::format("{}{}", offered.empty() ? "" : ", ",
|
||||
m == VK_PRESENT_MODE_IMMEDIATE_KHR ? "IMMEDIATE" : m == VK_PRESENT_MODE_MAILBOX_KHR ? "MAILBOX"
|
||||
: m == VK_PRESENT_MODE_FIFO_KHR ? "FIFO" : m == VK_PRESENT_MODE_FIFO_RELAXED_KHR ? "FIFO_RELAXED"
|
||||
: std::to_string(int(m)));
|
||||
std::string text = std::format("{} (V-Sync {}), {} images (the surface offers {}, and {} images)",
|
||||
info.presentMode == VK_PRESENT_MODE_FIFO_KHR ? "FIFO" : info.presentMode == VK_PRESENT_MODE_MAILBOX_KHR ? "MAILBOX"
|
||||
: "IMMEDIATE", s_vsync ? "on" : "off", count, offered,
|
||||
caps.maxImageCount ? std::format("{} to {}", caps.minImageCount, caps.maxImageCount)
|
||||
: std::format("{} or more", caps.minImageCount));
|
||||
if (text != s_presentText)
|
||||
{
|
||||
s_presentText = text;
|
||||
fprintf(stderr, "[video] swapchain: %s\n", text.c_str());
|
||||
if (s_contextReady)
|
||||
report::SetSystemItem("Present", s_presentText + "; " + s_timingText);
|
||||
}
|
||||
static bool first = true;
|
||||
NoteDisplay(std::exchange(first, false) ? "seen at start" : "seen as the swapchain was rebuilt");
|
||||
}
|
||||
#endif
|
||||
s_renderDone.resize(count);
|
||||
VkSemaphoreCreateInfo si{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||||
for (auto& s : s_renderDone)
|
||||
@@ -2421,6 +2499,119 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows on an Intel integrated GPU starts with the defaults measured
|
||||
// on one: a ThinkPad T14 Gen 1's UHD 620 (Gen9.5, driver 101.2135) on
|
||||
// AC power, where after ~90 s of load the whole chip is held at 11.9 W
|
||||
// (the GPU gets ~6 W). A race at 1x in its 1280x720 window ran 20.4
|
||||
// fps, GPU-bound at 49 ms a frame (P4 tuning, 2026-10-07: 35
|
||||
// interleaved runs, base runs within 0.19 fps of each other). Race and
|
||||
// intro cinematic, frames a second:
|
||||
// - fewer and lighter render passes (the renderer, as on Apple but
|
||||
// for the pass barrier, which stays full: renderer.cpp,
|
||||
// s_passBarrierLevel): with occlusion counting +1.8 / +1.5
|
||||
// (alone: lighter +1.15 / +0.85, fewer +0.55 / +0.87; the
|
||||
// barrier's share of lighter ~0.1 in the race). No artifact seen
|
||||
// in the parked race's captures, by eye on zoomed crops: they
|
||||
// fire at wall-clock times, so they aren't the same game frame and
|
||||
// no pixel comparison was possible; the cinematic's weren't even
|
||||
// the same shot.
|
||||
// - Anti-Aliasing Off: +0.9 / +0.9 (its pass alone is 1.8 ms);
|
||||
// edges stair-step.
|
||||
// - Anisotropic Filtering 2x, not Auto's 4x: +0.3 / +0.4; the far
|
||||
// road a little softer (Off: +0.55, plainly blurred).
|
||||
// - Internal Resolution Auto at 1x (the renderer's budget, as on
|
||||
// iOS): Auto would take 2x2, four times the scene's pixels, for
|
||||
// a maximized window or a 1080p screen.
|
||||
// - Frame Rate stays 60: no setting there holds 30, so a 30 cap
|
||||
// gave the same frame rate.
|
||||
// All of it with the barrier at level 2 then (4b9b0c3) against
|
||||
// 7098f3f, each with the game's own defaults (3 runs against 4,
|
||||
// interleaved, drift-corrected): race 20.2 -> 22.8 fps (+2.6, +2.3
|
||||
// to +2.9; GPU -5.6 ms), cinematic 17.1 -> 19.4 (+2.3, +2.0 to +2.7;
|
||||
// the one run without other processes' CPU +2.5); maximized at
|
||||
// 1920x991 the race ran 8.7 fps (Auto's 2x2), then 22.2. No black
|
||||
// frame but the game's own cuts; sync validation through the
|
||||
// cinematic and the race found no hazard. Windows' Best performance
|
||||
// power mode (the player's setting) lets the same chip hold ~25 W
|
||||
// (the GPU 9-12 W, the thermal zone at 93-97 C): with these defaults
|
||||
// the race ran 25-29 fps there (1280x720 or maximized, 7 runs) and
|
||||
// the cinematic 23-24, and a 30 cap still gave the same rate
|
||||
// (maximized, 25.4 against 25.6 at 60).
|
||||
// Only Intel's: AMD's APUs (another driver, and several times this
|
||||
// GPU in a Radeon 680M-890M) and discrete GPUs are unmeasured on
|
||||
// Windows and keep the table's defaults and today's passes. Intel's
|
||||
// Xe and Arc integrated GPUs, faster than the UHD 620 but sharing
|
||||
// its driver's lineage, take them unmeasured: 1x Auto still renders
|
||||
// a quarter of 2x2's pixels. The settings are device defaults: a
|
||||
// value in settings.toml wins, as do the NFSMW_* variables (each pass
|
||||
// toggle has one). Occlusion counting is on for every GPU (see
|
||||
// Initialize).
|
||||
bool IntegratedGpuDefaults(const VkPhysicalDeviceProperties& props)
|
||||
{
|
||||
constexpr uint32_t kIntel = 0x8086;
|
||||
if (props.deviceType != VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU || props.vendorID != kIntel)
|
||||
{
|
||||
const char* kind = props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? "a discrete GPU"
|
||||
: props.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? "an integrated GPU, not Intel's"
|
||||
: "not an integrated GPU";
|
||||
fprintf(stderr, "[video] %s is %s: the full-quality defaults (the integrated-GPU ones were measured on Intel's)\n",
|
||||
props.deviceName, kind);
|
||||
return false;
|
||||
}
|
||||
fprintf(stderr, "[video] %s is an Intel integrated GPU: fewer and lighter render passes, Internal Resolution Auto at 1x, "
|
||||
"Anti-Aliasing Off and Anisotropic Filtering 2x by default (measured on a UHD 620, GPU-bound at ~20 fps in a race; "
|
||||
"settings.toml and NFSMW_* variables win)\n", props.deviceName);
|
||||
std::string why = std::format("an Intel integrated GPU on Windows, {}", props.deviceName);
|
||||
settings::SetDeviceDefault(settings::Id::AntiAliasing, 0, why);
|
||||
settings::SetDeviceDefault(settings::Id::Anisotropy, 2, why);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Windows: the display the window is on, logged when it changes (at
|
||||
// start, a swapchain rebuilt, another display, another mode). Its
|
||||
// refresh rate decides how the game's 60 fps shows: at 120 or 240 Hz
|
||||
// the guest vblank locks to every 2nd or 4th refresh (with present
|
||||
// timing: video/refresh_multiple.h), at 144 Hz each frame stays for 2
|
||||
// or 3 refreshes (the work notes' 144HZ-NOTE.md), and the repeat
|
||||
// count measures in its refreshes (OnScreen). It passes the rate on
|
||||
// for the refresh multiple (UpdateRefreshMultiple), so a rate changed
|
||||
// in Windows' settings while the game runs counts too. The one notice
|
||||
// a player sees about the rate is the refresh-rate notice
|
||||
// (MaybeRefreshNotice, video/refresh_notice.h: 90, 144 and 165 Hz,
|
||||
// not with variable refresh); the port's own refresh-rate toast
|
||||
// (MaybeHintRefresh, held in labelled builds) is gone with it.
|
||||
// NFSMW_TEST_DISPLAY_HZ=<hz> reports that rate instead (TestDisplayHz).
|
||||
void NoteDisplay(const char* why)
|
||||
{
|
||||
if (!s_window)
|
||||
return;
|
||||
SDL_DisplayID id = SDL_GetDisplayForWindow(s_window);
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(id);
|
||||
UpdateRefreshMultiple(mode);
|
||||
const float testHz = TestDisplayHz();
|
||||
float hz = testHz > 0.0f ? testHz : mode ? mode->refresh_rate : 0.0f;
|
||||
s_panelPeriodNs.store(hz > 0.0f ? 1e9 / hz : 0.0, std::memory_order_relaxed);
|
||||
int w = mode ? mode->w : 0, h = mode ? mode->h : 0;
|
||||
if (id == s_notedDisplay && w == s_notedW && h == s_notedH && hz == s_notedHz)
|
||||
return;
|
||||
s_notedDisplay = id;
|
||||
s_notedW = w;
|
||||
s_notedH = h;
|
||||
s_notedHz = hz;
|
||||
const char* name = SDL_GetDisplayName(id);
|
||||
const int multiple = RefreshFit::MultipleOf(hz);
|
||||
std::string cadence = hz <= 0.0f ? std::string("; its refresh rate is unknown")
|
||||
: multiple == 1 ? std::string()
|
||||
: multiple >= 2 ? std::format("; the game runs at 60 fps, {} of these refreshes a frame: even once the guest vblank "
|
||||
"locks to them (the [vblank] lines; it needs present timing)", multiple)
|
||||
: std::format("; the game runs at 60 fps, {:.2f} of these refreshes a frame: whole refreshes each, so they can "
|
||||
"come unevenly unless G-SYNC/FreeSync is on ([perf] \"repeated\" counts the long ones)", hz / 60.0f);
|
||||
fprintf(stderr, "[video] display %s: %dx%d at %.2f Hz%s (%s)%s\n", name ? name : "?", w, h, hz,
|
||||
testHz > 0.0f ? " (NFSMW_TEST_DISPLAY_HZ)" : "", why, cadence.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
// The device extensions the game can't start without: Initialize
|
||||
// enables them, and ChooseDevice puts a device with all of them first.
|
||||
constexpr const char* kNeededDeviceExtensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME,
|
||||
@@ -2436,7 +2627,9 @@ void main()
|
||||
// that could present, which on a laptop with two GPUs was usually the
|
||||
// integrated one; where only one device can present it still is that
|
||||
// one. A device's queue family is its first with graphics that can
|
||||
// present to the window, as before.
|
||||
// present to the window, as before. On Windows the exe's
|
||||
// NvOptimusEnablement and AmdPowerXpressRequestHighPerformance ask the
|
||||
// same of the drivers (platform/startup.h).
|
||||
void ChooseDevice(const std::vector<VkPhysicalDevice>& devices)
|
||||
{
|
||||
std::vector<GpuCandidate> candidates;
|
||||
@@ -2622,6 +2815,9 @@ void main()
|
||||
}
|
||||
VkPhysicalDeviceProperties props;
|
||||
vkGetPhysicalDeviceProperties(s_physical, &props);
|
||||
#ifdef _WIN32
|
||||
const bool integratedGpuDefaults = IntegratedGpuDefaults(props); // before anything reads those settings
|
||||
#endif
|
||||
|
||||
// The renderer needs dynamic rendering, and imports guest memory as
|
||||
// a buffer where the device can (host-memory import; Turnip, the
|
||||
@@ -2732,12 +2928,17 @@ void main()
|
||||
dynamicRendering.pNext = &presentIdFeatures; // and presentWaitFeatures
|
||||
// NFSMW_OCCLUSION (=1 on, anything else off): count the game's
|
||||
// occlusion queries on the GPU (presenter.h), which the sun's glare
|
||||
// fades by. On by default where checked (MoltenVK on an M4 Max,
|
||||
// RADV on the Steam Machine, Turnip on the Steam Frame: cost within
|
||||
// noise); off elsewhere. Needs precise occlusion queries (counting,
|
||||
// not boolean); host query reset (core 1.2) is optional. Off, the
|
||||
// device is created exactly as without it.
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
// fades by. On by default on Apple, Linux and Windows, for every GPU
|
||||
// there. Checked on MoltenVK (an M4 Max), RADV (the Steam Machine),
|
||||
// Turnip (the Steam Frame) and Intel's Windows driver (a UHD 620: the
|
||||
// sun's counts as the Mac's in the same view, the glare drawn; +0.2
|
||||
// fps in 3 of 3 runs, GPU-bound), the cost within noise everywhere;
|
||||
// NVIDIA's drivers and AMD's on Windows are on unchecked: precise
|
||||
// occlusion queries are core Vulkan, and without them the glare never
|
||||
// shows. Needs precise occlusion queries (counting, not boolean);
|
||||
// host query reset (core 1.2) is optional. Off, the device is created
|
||||
// exactly as without it.
|
||||
#if defined(__APPLE__) || defined(__linux__) || defined(_WIN32)
|
||||
constexpr bool kOcclusionByDefault = true;
|
||||
#else
|
||||
constexpr bool kOcclusionByDefault = false;
|
||||
@@ -2820,6 +3021,14 @@ void main()
|
||||
fprintf(stderr, "[vblank] %s\n", s_presentTiming ? "presents are timed (VK_KHR_present_wait)"
|
||||
: timingOff ? "presents aren't timed (NFSMW_PRESENT_TIMING=0): the guest vblank can't lock to the display"
|
||||
: "no present timing (VK_KHR_present_wait) on this device: the guest vblank can't lock to the display");
|
||||
#ifdef _WIN32
|
||||
// Which of the two the device has (NVIDIA's Windows driver is
|
||||
// expected to have both, Intel's 101.2135 has neither).
|
||||
s_timingText = std::format("VK_KHR_present_id {}, VK_KHR_present_wait {}{}", presentId ? "yes" : "no",
|
||||
presentWait ? "yes" : "no", s_presentTiming ? ": presents timed" : timingOff ? ": not used (NFSMW_PRESENT_TIMING=0)"
|
||||
: presentId && presentWait ? ": listed, but not usable" : ": presents not timed");
|
||||
fprintf(stderr, "[vblank] %s on %s\n", s_timingText.c_str(), props.deviceName);
|
||||
#endif
|
||||
|
||||
VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
|
||||
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
@@ -2858,6 +3067,9 @@ void main()
|
||||
(PFN_vkCmdBeginRenderingKHR)vkGetDeviceProcAddr(s_device, "vkCmdBeginRenderingKHR"),
|
||||
(PFN_vkCmdEndRenderingKHR)vkGetDeviceProcAddr(s_device, "vkCmdEndRenderingKHR"), rateW, rateH, occlusionCounting,
|
||||
hostQueryReset.hostQueryReset ? (PFN_vkResetQueryPool)vkGetDeviceProcAddr(s_device, "vkResetQueryPool") : nullptr };
|
||||
#ifdef _WIN32
|
||||
s_context.integratedGpuDefaults = integratedGpuDefaults;
|
||||
#endif
|
||||
s_contextReady = true;
|
||||
s_settingsGeneration = settings::Generation();
|
||||
fprintf(stderr, "[video] %s, swapchain %ux%u%s\n", props.deviceName, s_swapExtent.width, s_swapExtent.height, GeometryNote().c_str());
|
||||
@@ -2916,6 +3128,9 @@ void main()
|
||||
VK_API_VERSION_MINOR(props.apiVersion), VK_API_VERSION_PATCH(props.apiVersion), VK_API_VERSION_MAJOR(loader),
|
||||
VK_API_VERSION_MINOR(loader), VK_API_VERSION_PATCH(loader), link.empty() ? std::string() : ", " + link));
|
||||
UpdateDisplayItem();
|
||||
#ifdef _WIN32
|
||||
report::SetSystemItem("Present", s_presentText + "; " + s_timingText);
|
||||
#endif
|
||||
}
|
||||
if (s_gameMode && mode && nativeW > 0 && int64_t(mode->w) * mode->h < int64_t(nativeW) * nativeH)
|
||||
s_resolutionHint = std::format("Steam gives this game {}x{} of your {}x{} display. For full sharpness, set this game's "
|
||||
@@ -3483,6 +3698,28 @@ void main()
|
||||
const int64_t gridSince = s_gridFixedSince.load(std::memory_order_relaxed);
|
||||
in.gridFixed = gridSince != 0;
|
||||
in.gridStableSec = gridSince ? double(NowNs() - gridSince) / 1e9 : 0.0;
|
||||
#ifdef _WIN32
|
||||
// Without present timing (Intel's 101.2135 driver has none) there
|
||||
// is no grid check, so a fixed refresh can't be told from
|
||||
// variable refresh: no notice. With NFSMW_TEST_DISPLAY_HZ the grid
|
||||
// is taken as fixed at that rate (the panel tested on a laptop
|
||||
// whose driver has none).
|
||||
if (!s_presentTiming)
|
||||
{
|
||||
if (TestDisplayHz() > 0.0f)
|
||||
{
|
||||
in.gridFixed = true;
|
||||
in.gridStableSec = in.modeStableSec;
|
||||
}
|
||||
else
|
||||
{
|
||||
decided = key;
|
||||
fprintf(stderr, "[video] refresh notice for %d Hz: not shown: no present timing on this device (VK_KHR_present_wait), "
|
||||
"so a fixed refresh rate can't be told from variable refresh\n", key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
in.shownBefore = settings::RefreshNoticeShown(key);
|
||||
{
|
||||
double period = 0;
|
||||
@@ -3587,6 +3824,15 @@ void main()
|
||||
else if (e.type == SDL_EVENT_WINDOW_RESTORED || e.type == SDL_EVENT_WINDOW_SHOWN || e.type == SDL_EVENT_WINDOW_EXPOSED ||
|
||||
e.type == SDL_EVENT_WINDOW_MAXIMIZED)
|
||||
s_visible.store(true, std::memory_order_relaxed);
|
||||
#ifdef _WIN32
|
||||
else if (e.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED || e.type == SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED ||
|
||||
e.type == SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED)
|
||||
{
|
||||
// Another display, or its refresh rate changed in Windows'
|
||||
// settings: logged, and the refresh multiple (NoteDisplay).
|
||||
NoteDisplay(e.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED ? "the window moved to another display" : "a display's mode changed");
|
||||
}
|
||||
#endif
|
||||
else if (e.type == SDL_EVENT_WINDOW_ENTER_FULLSCREEN || e.type == SDL_EVENT_WINDOW_LEAVE_FULLSCREEN)
|
||||
{
|
||||
// The window's state only. A change the game didn't ask for
|
||||
@@ -4281,7 +4527,11 @@ void main()
|
||||
if (at > ready && at - ready < 100'000'000)
|
||||
{
|
||||
queueLock.unlock();
|
||||
#ifdef _WIN32
|
||||
platform::SleepUntil(at); // a high-resolution timer: a 1 ms sleep_until wakes ~1.3 ms late here
|
||||
#else
|
||||
std::this_thread::sleep_until(std::chrono::steady_clock::time_point(std::chrono::nanoseconds(at)));
|
||||
#endif
|
||||
queueLock.lock();
|
||||
presented = NowNs();
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ namespace video
|
||||
// renderer resets them in its command buffers).
|
||||
bool occlusionCounting = false;
|
||||
PFN_vkResetQueryPool resetQueryPool = nullptr;
|
||||
#ifdef _WIN32
|
||||
// Windows on an Intel integrated GPU: the defaults measured on one
|
||||
// (presenter.cpp, IntegratedGpuDefaults). The renderer's part:
|
||||
// fewer and lighter passes, and Auto internal resolution at 1x.
|
||||
bool integratedGpuDefaults = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
// nullptr when running headless.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The [perf] line's "repeated" (video::PresentStats), as the Windows
|
||||
// presenter counts it: refreshes a game frame stayed on screen past the time
|
||||
// its guest vblanks gave it, in the display's own refreshes. The game runs
|
||||
// at 60 fps whatever the panel does, so on a panel that isn't 60 Hz frames
|
||||
// show for whole refreshes of another length:
|
||||
//
|
||||
// panel a frame shows for counted (one each)
|
||||
// 60 Hz 1 refresh (16.7 ms) a frame kept 2 refreshes (a repeat)
|
||||
// 120 Hz 2 (16.7), or 1 and 3 in pairs the 3s (25.0 ms)
|
||||
// 144 Hz 2 or 3 (13.9 / 20.8 ms) the 3s, ~40% of frames: the judder
|
||||
// 165 Hz 3 or 2 (18.2 / 12.1 ms) only a 4: a 3 is a quarter refresh
|
||||
// long (the short 2s are the uneven ones)
|
||||
// 240 Hz 4 (16.7), 3 or 5 with jitter the 5s (20.8 ms)
|
||||
// G-SYNC ~16.7 ms, as delivered frames half a refresh late (3.5 ms at 144)
|
||||
//
|
||||
// The Windows port's work notes (144HZ-NOTE.md) have the model behind it.
|
||||
// Header only: tests/repeats_test.cpp checks it.
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace video
|
||||
{
|
||||
// A game frame given `vblanks` guest vblanks of `vblankNs` each was on
|
||||
// screen for `shownNs` (its present's completion to the next one's), on
|
||||
// a display refreshing every `refreshNs`: the refreshes it stayed past
|
||||
// its vblanks' time, to the nearest. A gap of more than half a second is
|
||||
// a pause, not a repeat; an unknown refresh (0) counts nothing.
|
||||
inline int64_t ExtraRefreshes(double shownNs, int64_t vblanks, double vblankNs, double refreshNs)
|
||||
{
|
||||
if (refreshNs <= 0.0 || shownNs > 500e6)
|
||||
return 0;
|
||||
int64_t extra = std::llround((shownNs - double(vblanks) * vblankNs) / refreshNs);
|
||||
return extra > 0 ? extra : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# make_ico.py: SpeedBreaker's Windows icon (runtime/platform/win32/
|
||||
# speedbreaker.ico, kept in the repo so the build needs no Pillow) from the
|
||||
# macOS one, scripts/macos/AppIcon.png. macOS draws its icons inside a
|
||||
# margin (an 824-pixel rounded square in 1024); Windows icons fill their
|
||||
# square, so the margin is cropped first. Needs Pillow.
|
||||
# python3 scripts/windows/make_ico.py
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
src = Image.open(os.path.join(root, 'scripts/macos/AppIcon.png')).convert('RGBA')
|
||||
art = src.crop((96, 96, 928, 928)).resize((256, 256), Image.LANCZOS)
|
||||
out = os.path.join(root, 'runtime/platform/win32/speedbreaker.ico')
|
||||
art.save(out, format='ICO', sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)])
|
||||
print(out, os.path.getsize(out), 'bytes')
|
||||
@@ -0,0 +1,549 @@
|
||||
#!/usr/bin/env python3
|
||||
# make_zip.py: the Windows tester kit, one zip to hand to testers who play
|
||||
# their own copy of the game. GPL-3.0-or-later (see COPYING).
|
||||
#
|
||||
# python scripts/windows/make_zip.py --build <build dir> --docs <dir> --deps-src <dir> --out <dir>
|
||||
# [--forbid <word>]... [--allow-dirty] [--checklist-name CHECKLIST.md] [--public]
|
||||
#
|
||||
# --build a labelled build's folder (CMake preset windows-clang-tester:
|
||||
# SB_WINDOWS_GUI=ON, SB_BUILD_LABEL="Windows beta"), with
|
||||
# runtime/SpeedBreaker.exe, its .pdb and generated/build_info.inc
|
||||
# --docs README-TESTER.txt, CHECKLIST.md and REPORT-TEMPLATE.txt, and
|
||||
# TESTER-NOTES.txt (a short note for this round) if it is there
|
||||
# --deps-src the dependencies' source trees the build used: SDL3-*,
|
||||
# glslang-* and ffmpeg-* (their license texts)
|
||||
# --out where the zip goes, with <zip name>.txt beside it (hashes and
|
||||
# the checks' findings, for us: not in the zip)
|
||||
# --public the download for everyone (a release asset) instead of a tester
|
||||
# kit: SpeedBreaker-<version>-windows-x64-<label>.zip with a
|
||||
# .sha256 beside it; README.txt (--docs) is the only document;
|
||||
# SpeedBreaker.pdb is checked against the exe but NOT put in the
|
||||
# zip (the symbols stay with the project, for crash reports); and
|
||||
# licenses\README.txt points to the public repository's commit and
|
||||
# the release's third-party source archives for the source code.
|
||||
# --forbid a word that must appear nowhere: the zip's text files, the
|
||||
# exe's strings, the PDB's (a name, a handle, an email address);
|
||||
# as a whole word (not inside a longer one: "alex" doesn't match
|
||||
# "scaleX"), in any case, any number of times
|
||||
#
|
||||
# The zip, SpeedBreaker-<version>-windows-x64-<label>-<date>-<commit>.zip
|
||||
# (the version from project(), "beta" for the label "Windows beta": the
|
||||
# label's words but "windows" and "build"; the build's own date and commit),
|
||||
# holds at its root (Explorer's Extract All makes the folder):
|
||||
# SpeedBreaker.exe, SpeedBreaker.pdb (the same build: names in crash and
|
||||
# hang reports), the three tester documents, and licenses\ with every
|
||||
# third-party notice the exe needs and a README.txt listing them, which
|
||||
# says the source code is in <zip name>-source.zip beside it (made
|
||||
# separately, from the commit).
|
||||
#
|
||||
# It refuses (exit 1), naming why, when:
|
||||
# - the build isn't a labelled one, is dirty (--allow-dirty), or the exe
|
||||
# doesn't carry the version, label and commit build_info.inc names;
|
||||
# - the exe isn't x64, isn't linked for the Windows subsystem, or doesn't
|
||||
# export NvOptimusEnablement and AmdPowerXpressRequestHighPerformance;
|
||||
# - the PDB isn't the exe's (the GUID in the exe's debug directory);
|
||||
# - a file in the zip isn't one of the expected kinds, has a game-data
|
||||
# extension (.xex .iso .bin .wmv ...), or has the SHA-256 of a game file
|
||||
# (runtime/install/manifest.inc);
|
||||
# - a --forbid word, or a user-profile path (C:\Users\<name>, /Users/<name>,
|
||||
# /home/<name>), is in any text file, the exe or the PDB;
|
||||
# - an email address is in the tester documents, or one at a personal mail
|
||||
# service (gmail.com, outlook.com, ...) is in the exe (FFmpeg's
|
||||
# ffmpeg-devel@ffmpeg.org is listed only; third-party license texts name
|
||||
# their authors' addresses: listed only).
|
||||
# Build paths (C:\sb\...: assert messages, the PDB's sources) are allowed and
|
||||
# counted in the .txt.
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import textwrap
|
||||
import zipfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
DOCS = ("README-TESTER.txt", "CHECKLIST.md", "REPORT-TEMPLATE.txt")
|
||||
PUBLIC_DOCS = ("README.txt",)
|
||||
REPOSITORY = "https://github.com/SpeedBreakerProject/speedbreaker"
|
||||
OPTIONAL_DOCS = ("TESTER-NOTES.txt",)
|
||||
GAME_EXTENSIONS = {".xex", ".iso", ".xiso", ".god", ".bin", ".wmv", ".xmv", ".big", ".lzc", ".xma", ".xpr", ".xtl", ".stfs"}
|
||||
|
||||
problems = []
|
||||
notes = []
|
||||
|
||||
|
||||
def fail(text):
|
||||
problems.append(text)
|
||||
print("PROBLEM: " + text, file=sys.stderr)
|
||||
|
||||
|
||||
def read(path, mode="rb"):
|
||||
with open(path, mode, **({} if "b" in mode else {"encoding": "utf-8"})) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def sha256(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def crlf(text):
|
||||
return text.replace("\r\n", "\n").replace("\n", "\r\n")
|
||||
|
||||
|
||||
# --- The build -------------------------------------------------------------
|
||||
|
||||
def build_info(build):
|
||||
path = os.path.join(build, "runtime", "generated", "build_info.inc")
|
||||
text = read(path, "r")
|
||||
info = dict(re.findall(r'#define NFSMW_BUILD_(\w+) "?([^"\n]*)"?', text))
|
||||
return info
|
||||
|
||||
|
||||
def pe_info(data):
|
||||
"""Machine, subsystem, exported names and the CodeView record (GUID, age, PDB name) of a PE file."""
|
||||
pe = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
if data[pe:pe + 4] != b"PE\0\0":
|
||||
raise ValueError("not a PE file")
|
||||
machine, sections = struct.unpack_from("<HH", data, pe + 4)
|
||||
optional_size = struct.unpack_from("<H", data, pe + 20)[0]
|
||||
opt = pe + 24
|
||||
magic = struct.unpack_from("<H", data, opt)[0]
|
||||
subsystem = struct.unpack_from("<H", data, opt + 68)[0]
|
||||
dirs = opt + (112 if magic == 0x20B else 96)
|
||||
table = []
|
||||
for i in range(sections):
|
||||
s = opt + optional_size + 40 * i
|
||||
vsize, va, rawsize, raw = struct.unpack_from("<IIII", data, s + 8)
|
||||
table.append((va, max(vsize, rawsize), raw))
|
||||
|
||||
def offset(rva):
|
||||
for va, size, raw in table:
|
||||
if va <= rva < va + size:
|
||||
return raw + rva - va
|
||||
raise ValueError("RVA %x outside the sections" % rva)
|
||||
|
||||
def cstring(off):
|
||||
return data[off:data.index(b"\0", off)].decode("ascii", "replace")
|
||||
|
||||
exports = []
|
||||
export_rva, export_size = struct.unpack_from("<II", data, dirs)
|
||||
if export_rva:
|
||||
e = offset(export_rva)
|
||||
count, names = struct.unpack_from("<I", data, e + 24)[0], struct.unpack_from("<I", data, e + 32)[0]
|
||||
for i in range(count):
|
||||
exports.append(cstring(offset(struct.unpack_from("<I", data, offset(names) + 4 * i)[0])))
|
||||
codeview = None
|
||||
debug_rva, debug_size = struct.unpack_from("<II", data, dirs + 6 * 8)
|
||||
if debug_rva:
|
||||
d = offset(debug_rva)
|
||||
for i in range(debug_size // 28):
|
||||
kind, size, rva, raw = struct.unpack_from("<IIII", data, d + 28 * i + 12)
|
||||
if kind == 2 and data[raw:raw + 4] == b"RSDS":
|
||||
codeview = (data[raw + 4:raw + 20], struct.unpack_from("<I", data, raw + 20)[0], cstring(raw + 24))
|
||||
return machine, subsystem, exports, codeview
|
||||
|
||||
|
||||
def pdb_guid(path):
|
||||
"""The GUID and age in a PDB's info stream (MSF 7.00, stream 1)."""
|
||||
data = read(path)
|
||||
if not data.startswith(b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0"):
|
||||
raise ValueError("not an MSF 7.00 PDB")
|
||||
block, _, _, directory_size, _, map_block = struct.unpack_from("<6I", data, 32)
|
||||
blocks = -(-directory_size // block)
|
||||
directory_blocks = struct.unpack_from("<%dI" % blocks, data, map_block * block)
|
||||
directory = b"".join(data[b * block:(b + 1) * block] for b in directory_blocks)[:directory_size]
|
||||
streams = struct.unpack_from("<I", directory, 0)[0]
|
||||
sizes = struct.unpack_from("<%dI" % streams, directory, 4)
|
||||
at = 4 + 4 * streams
|
||||
stream_blocks = []
|
||||
for size in sizes:
|
||||
n = 0 if size == 0xFFFFFFFF else -(-size // block)
|
||||
stream_blocks.append(struct.unpack_from("<%dI" % n, directory, at))
|
||||
at += 4 * n
|
||||
info = b"".join(data[b * block:(b + 1) * block] for b in stream_blocks[1])[:sizes[1]]
|
||||
_, _, age = struct.unpack_from("<III", info, 0)
|
||||
return info[12:28], age
|
||||
|
||||
|
||||
def guid_text(raw):
|
||||
a, b, c = struct.unpack_from("<IHH", raw, 0)
|
||||
return "%08X-%04X-%04X-%s-%s" % (a, b, c, raw[8:10].hex().upper(), raw[10:16].hex().upper())
|
||||
|
||||
|
||||
# --- Licenses ---------------------------------------------------------------
|
||||
|
||||
def find_dir(parent, prefix):
|
||||
for name in sorted(os.listdir(parent)):
|
||||
if name.lower().startswith(prefix.lower()) and os.path.isdir(os.path.join(parent, name)):
|
||||
return os.path.join(parent, name)
|
||||
raise FileNotFoundError("no %s* folder in %s" % (prefix, parent))
|
||||
|
||||
|
||||
def between(text, start, end, include_end=True):
|
||||
i = text.index(start)
|
||||
j = text.index(end, i + len(start))
|
||||
return text[i:j + (len(end) if include_end else 0)]
|
||||
|
||||
|
||||
def comment_text(block):
|
||||
"""A C comment block's text without its ' * ' margins."""
|
||||
lines = []
|
||||
for line in block.splitlines():
|
||||
line = line.strip()
|
||||
if line in ("/*", "*/", "/**"):
|
||||
continue
|
||||
line = re.sub(r"^\*+ ?", "", line)
|
||||
lines.append(line.rstrip(" *"))
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def licenses(deps):
|
||||
"""[(file name in licenses\\, text)] and the README's lines, from the trees on disk."""
|
||||
sdl = find_dir(deps, "SDL3-")
|
||||
glslang = find_dir(deps, "glslang-")
|
||||
ffmpeg = find_dir(deps, "ffmpeg-")
|
||||
xr = os.path.join(ROOT, "tools", "XenonRecomp")
|
||||
imgui = os.path.join(ROOT, "runtime", "thirdparty", "imgui")
|
||||
t = lambda *p: read(os.path.join(*p), "r")
|
||||
files = []
|
||||
|
||||
def add(name, text):
|
||||
files.append((name, text))
|
||||
return name
|
||||
|
||||
readme = []
|
||||
|
||||
def entry(what, license, names):
|
||||
wrap = lambda text, first, rest: "\n".join(textwrap.wrap(text, 78, initial_indent=first, subsequent_indent=rest,
|
||||
break_on_hyphens=False, break_long_words=False))
|
||||
readme.append("%s\n%s\n%s" % (wrap(what, "- ", " "), wrap(license, " License: ", " "),
|
||||
wrap(", ".join(names), " Text: ", " ")))
|
||||
|
||||
entry("SpeedBreaker, this build: an unofficial native port of Need for Speed: Most Wanted (2005, Xbox 360)",
|
||||
"GPL-3.0-or-later", [add("SpeedBreaker-COPYING.txt", t(ROOT, "COPYING")),
|
||||
add("SpeedBreaker-NOTICE.txt", t(ROOT, "NOTICE"))])
|
||||
entry("Unleashed Recompiled (hedge-dev and contributors): parts of the kernel, XAM, memory and threading code "
|
||||
"are adapted from it", "GPL-3.0-or-later, the same license", ["SpeedBreaker-COPYING.txt"])
|
||||
entry("XenonRecomp and its XenonUtils (hedge-dev and contributors): the recompiler, and the XEX loader in the exe",
|
||||
"MIT", [add("XenonRecomp-LICENSE.txt", t(xr, "LICENSE.md"))])
|
||||
# Xenia's own LICENSE isn't in these trees: its files' headers name the
|
||||
# holder and "the BSD license", the 3-clause one (SPDX's text from
|
||||
# glslang's LICENSES), with that holder in place of the template's line.
|
||||
holders = set()
|
||||
for folder in (os.path.join(ROOT, "runtime"), os.path.join(xr, "XenonUtils")):
|
||||
for parent, _, names in os.walk(folder):
|
||||
for name in names:
|
||||
if name.endswith((".h", ".cpp", ".inc")):
|
||||
holders.update(re.findall(rb"Copyright (\d{4}) Ben Vanik", read(os.path.join(parent, name))))
|
||||
years = sorted(y.decode() for y in holders)
|
||||
bsd = t(glslang, "LICENSES", "BSD-3-Clause.txt")
|
||||
bsd = bsd[bsd.index("\n") + 1:].lstrip("\n")
|
||||
entry("Xenia (Ben Vanik and the Xenia contributors): GPU register, microcode and Xenos headers, and XEX patching",
|
||||
"BSD 3-Clause", [add("Xenia-LICENSE.txt",
|
||||
"Xenia - Xbox 360 Emulator Research Project, https://github.com/xenia-project/xenia\n"
|
||||
"Copyright %s Ben Vanik. All rights reserved.\n"
|
||||
"Released under the BSD license (3-clause):\n\n" %
|
||||
("-".join([years[0], years[-1]]) if len(years) > 1 else years[0]) + bsd)])
|
||||
ucode = t(ROOT, "runtime", "gpu", "xenos", "ucode.h")
|
||||
entry("freedreno (Rob Clark): the Xenos shader instruction formats, through Xenia", "MIT",
|
||||
[add("freedreno-LICENSE.txt", comment_text(between(ucode, "/*\n * Copyright (c) 2012 Rob Clark", "*/")))])
|
||||
patch = t(ROOT, "patches", "ffmpeg", "0001-xma-single-stream-no-holdback.patch")
|
||||
entry("FFmpeg %s: its XMA and WMA Pro audio decoders, linked in statically, with one change of "
|
||||
"SpeedBreaker's (FFmpeg-patch.txt): single-stream XMA (one stream per decoder, as on the Xbox 360) gives "
|
||||
"out every decoded sample at once instead of holding the last 4096 back" % os.path.basename(ffmpeg).split("-", 1)[1],
|
||||
"LGPL-2.1-or-later", [add("FFmpeg-COPYING.LGPLv2.1.txt", t(ffmpeg, "COPYING.LGPLv2.1")),
|
||||
add("FFmpeg-patch.txt", patch)])
|
||||
entry("libmspack (Stuart Caie): LZX decompression of the XEX", "LGPL-2.1",
|
||||
[add("libmspack-COPYING.LIB.txt", t(xr, "thirdparty", "libmspack", "libmspack", "COPYING.LIB"))])
|
||||
entry("SDL %s: the window, input and audio, with HIDAPI (controllers) and yuv2rgb inside it" %
|
||||
os.path.basename(sdl).split("-", 1)[1], "zlib; HIDAPI used under its BSD-style license; yuv2rgb BSD 3-Clause",
|
||||
[add("SDL3-LICENSE.txt", t(sdl, "LICENSE.txt")),
|
||||
add("SDL3-hidapi-LICENSE.txt", t(sdl, "src", "hidapi", "LICENSE.txt")),
|
||||
add("SDL3-hidapi-LICENSE-bsd.txt", t(sdl, "src", "hidapi", "LICENSE-bsd.txt")),
|
||||
add("SDL3-yuv2rgb-LICENSE.txt", t(sdl, "src", "video", "yuv2rgb", "LICENSE"))])
|
||||
entry("glslang %s (Khronos Group): compiles the game's shaders at run time" % os.path.basename(glslang).split("-", 1)[1],
|
||||
"BSD 3-Clause and others, as listed in its LICENSE", [add("glslang-LICENSE.txt", t(glslang, "LICENSE.txt"))])
|
||||
entry("Dear ImGui (Omar Cornut): the installer and settings screens", "MIT",
|
||||
[add("DearImGui-LICENSE.txt", t(imgui, "LICENSE.txt"))])
|
||||
# Dear ImGui's two built-in fonts stay in the exe (IMGUI_DISABLE_DEFAULT_FONT
|
||||
# isn't defined) though only Roboto is used. Their holders, from the
|
||||
# notices in imgui_draw.cpp, over the MIT permission text of Dear
|
||||
# ImGui's own LICENSE.txt.
|
||||
draw = t(imgui, "imgui_draw.cpp")
|
||||
clean = re.search(r"// MIT License / (Copyright \(c\) [^\n]*Tristan Grimmer)\n", draw).group(1)
|
||||
forever = re.search(r"// MIT license / (Copyright \(c\) [^\n]*Tristan Grimmer)\n", draw).group(1)
|
||||
mit = t(imgui, "LICENSE.txt")
|
||||
mit = mit[mit.index("Permission is hereby granted"):]
|
||||
entry("ProggyClean and ProggyForever (Tristan Grimmer; ProggyForever also Disco Hello): Dear ImGui's built-in "
|
||||
"fonts, inside it", "MIT",
|
||||
[add("Proggy-fonts-LICENSE.txt",
|
||||
"ProggyClean.ttf, https://github.com/bluescan/proggyfonts\n%s\n\n"
|
||||
"ProggyForever (its minimal Regular), https://github.com/ocornut/proggyforever\n%s\n\n"
|
||||
"Both under the MIT License:\n\n%s" % (clean, forever.replace(", Copyright", "\nCopyright"), mit))])
|
||||
entry("stb_truetype, stb_rect_pack and stb_textedit (Sean Barrett), inside Dear ImGui",
|
||||
"MIT, or public domain", [add("stb-LICENSE.txt", between(t(imgui, "imstb_truetype.h"),
|
||||
"This software is available under 2 licenses", "\n*/",
|
||||
include_end=False) + "\n")])
|
||||
entry("Roboto (Google): the user interface's font", "Apache-2.0",
|
||||
[add("Roboto-LICENSE-Apache-2.0.txt", t(glslang, "LICENSES", "Apache-2.0.txt"))])
|
||||
entry("o1heap (Pavel Kirienko): the game's memory allocator", "MIT",
|
||||
[add("o1heap-LICENSE.txt", t(ROOT, "runtime", "thirdparty", "o1heap", "LICENSE"))])
|
||||
entry("QR Code generator library (Project Nayuki): the download page's QR code in Settings > Advanced > "
|
||||
"Check for Updates", "MIT",
|
||||
[add("qrcodegen-LICENSE.txt", t(ROOT, "runtime", "thirdparty", "qrcodegen", "LICENSE"))])
|
||||
entry("AMD FidelityFX Super Resolution 1 (Advanced Micro Devices): Settings > Graphics > Upscaling's FSR 1 "
|
||||
"(its shader code and constant setup, unmodified)", "MIT",
|
||||
[add("FidelityFX-FSR-LICENSE.txt", t(ROOT, "runtime", "thirdparty", "fsr1", "LICENSE.txt"))])
|
||||
entry("Vulkan-Headers and SPIRV-Headers (The Khronos Group): headers the game and glslang are built with",
|
||||
"Apache-2.0 or MIT (Vulkan-Headers); MIT (SPIRV-Headers)",
|
||||
[add("Vulkan-Headers-LICENSE.txt", t(ROOT, "LICENSES", "Vulkan-Headers-MIT.txt")),
|
||||
add("SPIRV-Headers-LICENSE.txt", t(ROOT, "LICENSES", "SPIRV-Headers.txt"))])
|
||||
entry("tiny-AES-c: XEX decryption", "The Unlicense (public domain)",
|
||||
[add("tiny-AES-c-unlicense.txt", t(xr, "thirdparty", "tiny-AES-c", "unlicense.txt"))])
|
||||
entry("toml++ (Mark Gillard): settings.toml", "MIT",
|
||||
[add("tomlplusplus-LICENSE.txt", t(xr, "thirdparty", "tomlplusplus", "LICENSE"))])
|
||||
entry("SIMDe (Evan Nemerson and contributors): SIMD in the recompiled code", "MIT",
|
||||
[add("SIMDe-COPYING.txt", t(xr, "thirdparty", "simde", "COPYING"))])
|
||||
sha1 = t(xr, "thirdparty", "TinySHA1", "TinySHA1.hpp")
|
||||
entry("TinySHA1 (Saurav Mohapatra): SHA-1 in the XEX loader", "ISC-style",
|
||||
[add("TinySHA1-LICENSE.txt", comment_text(between(sha1, "/*", "*/")))])
|
||||
return files, readme
|
||||
|
||||
|
||||
def licenses_readme(build_line, readme, source_zip, public=None):
|
||||
# The source goes next to the build (GPL-3.0 section 6), as <zip name>-
|
||||
# source.zip: kit 1 was repacked by hand to say so; this says it itself.
|
||||
# public: (commit, version) for the release download, whose source is the
|
||||
# public repository at that commit plus the release's third-party source
|
||||
# archives (as NOTICE says for every download).
|
||||
if public:
|
||||
commit, version = public
|
||||
head = ("LICENSES\n"
|
||||
"========\n\n"
|
||||
"%s\n\n"
|
||||
"SpeedBreaker is free software under the GNU General Public License, version 3\n"
|
||||
"or (at your option) any later version (SpeedBreaker-COPYING.txt and\n"
|
||||
"SpeedBreaker-NOTICE.txt), provided \"as is\", without warranty of any kind.\n"
|
||||
"Its complete source code is at %s, commit\n"
|
||||
"%s. The complete source code of the LGPL code by others in this\n"
|
||||
"build (FFmpeg, with SpeedBreaker's change to it and the way this build\n"
|
||||
"configures it, and libmspack) is published next to this download on the\n"
|
||||
"project's Releases page, as SpeedBreaker-%s-windows-third-party-source.tar.xz.\n"
|
||||
"If it is ever missing, ask through the project's issue tracker.\n\n"
|
||||
"This build contains code from the projects below, under the licenses named,\n"
|
||||
"whose texts are in this folder.\n\n" % (build_line, REPOSITORY, commit, version))
|
||||
return head + "\n\n".join(readme) + licenses_tail()
|
||||
head = ("LICENSES\n"
|
||||
"========\n\n"
|
||||
"%s\n\n"
|
||||
"SpeedBreaker is free software under the GNU General Public License, version 3\n"
|
||||
"or (at your option) any later version (SpeedBreaker-COPYING.txt and\n"
|
||||
"SpeedBreaker-NOTICE.txt), provided \"as is\", without warranty of any kind.\n"
|
||||
"The complete source code of this build, with the parts of other projects it\n"
|
||||
"contains and the change it makes to FFmpeg, is in\n"
|
||||
"%s, shared in the\n"
|
||||
"same place as this build. Ask the person who gave you this build if it's missing.\n\n"
|
||||
"This build contains code from the projects below, under the licenses named,\n"
|
||||
"whose texts are in this folder.\n\n" % (build_line, source_zip))
|
||||
return head + "\n\n".join(readme) + licenses_tail()
|
||||
|
||||
|
||||
def licenses_tail():
|
||||
return ("\n\nAlso linked in: Microsoft's C and C++ runtime libraries (statically, from the\n"
|
||||
"Visual Studio Build Tools). The Vulkan driver (vulkan-1.dll) is your graphics\n"
|
||||
"driver's and isn't part of this build. No game files are included.\n")
|
||||
|
||||
|
||||
# --- Checks -----------------------------------------------------------------
|
||||
|
||||
USER_PATH = re.compile(rb"(?i)(?:[a-z]:[\\/]+users[\\/]+|/users/|/home/)([a-z0-9._ -]{1,64})")
|
||||
EMAIL = re.compile(rb"[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}")
|
||||
BUILD_PATH = re.compile(rb"(?i)c:[\\/]+sb[\\/]+[a-z0-9._-]+(?:[\\/]+[a-z0-9._-]+)?")
|
||||
GENERIC_PROFILES = {b"public", b"default", b"all users", b"default user", b"shared"}
|
||||
PERSONAL_MAIL = re.compile(rb"(?i)@(gmail|googlemail|outlook|hotmail|live|msn|icloud|me|mac|yahoo|proton|protonmail|aol|gmx)\.")
|
||||
|
||||
|
||||
def strings(data):
|
||||
"""Printable runs of 6 or more: ASCII, and UTF-16LE turned into ASCII."""
|
||||
out = [m.group() for m in re.finditer(rb"[\x20-\x7e]{6,}", data)]
|
||||
out += [m.group().replace(b"\0", b"") for m in re.finditer(rb"(?:[\x20-\x7e]\x00){6,}", data)]
|
||||
return out
|
||||
|
||||
|
||||
def scan(label, data, forbid, *, binary, emails_fail, report):
|
||||
"""Forbidden words, user-profile paths and emails in `data` (bytes); build paths counted.
|
||||
emails_fail: True, any email fails; "personal", one at a personal mail service does."""
|
||||
runs = strings(data) if binary else [data]
|
||||
found_paths, found_emails, builds = set(), set(), {}
|
||||
words = [(w, re.compile(rb"(?i)(?<![a-z0-9])" + re.escape(w.encode()) + rb"(?![a-z0-9])")) for w in forbid]
|
||||
for run in runs:
|
||||
for word, pattern in words:
|
||||
m = pattern.search(run)
|
||||
if m:
|
||||
i = m.start()
|
||||
fail("%s: contains the forbidden word %r: ...%s..." % (label, word, run[max(0, i - 40):i + 40].decode("ascii", "replace")))
|
||||
for m in USER_PATH.finditer(run):
|
||||
name = m.group(1).strip().lower()
|
||||
if name.split(b"\\")[0] not in GENERIC_PROFILES:
|
||||
found_paths.add(m.group().decode("ascii", "replace"))
|
||||
for m in EMAIL.finditer(run):
|
||||
found_emails.add(m.group().decode("ascii", "replace"))
|
||||
for m in BUILD_PATH.finditer(run):
|
||||
key = re.sub(r"[\\/]+", r"\\", m.group().decode("ascii", "replace"))
|
||||
builds[key] = builds.get(key, 0) + 1
|
||||
for p in sorted(found_paths):
|
||||
fail("%s: a user-profile path: %s" % (label, p))
|
||||
if found_emails:
|
||||
personal = sorted(e for e in found_emails if PERSONAL_MAIL.search(e.encode()))
|
||||
if emails_fail is True or (emails_fail == "personal" and personal):
|
||||
fail("%s: email addresses: %s" % (label, ", ".join(sorted(found_emails) if emails_fail is True else personal)))
|
||||
report("%s: email addresses (listed): %s" % (label, ", ".join(sorted(found_emails))))
|
||||
if builds:
|
||||
report("%s: build paths (allowed): %s" % (label, ", ".join("%s (%d)" % kv for kv in sorted(builds.items()))))
|
||||
|
||||
|
||||
def game_hashes():
|
||||
text = read(os.path.join(ROOT, "runtime", "install", "manifest.inc"), "r")
|
||||
return set(re.findall(r'MANIFEST_FILE\("[^"]+", \d+ull, "([0-9a-f]{64})"\)', text))
|
||||
|
||||
|
||||
# --- Main -------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="The Windows tester kit (see the header).")
|
||||
ap.add_argument("--build", required=True)
|
||||
ap.add_argument("--docs", required=True)
|
||||
ap.add_argument("--deps-src", required=True)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--forbid", action="append", default=[])
|
||||
ap.add_argument("--allow-dirty", action="store_true")
|
||||
ap.add_argument("--checklist-name", default="CHECKLIST.md")
|
||||
ap.add_argument("--public", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
info = build_info(args.build)
|
||||
commit, release, version = info.get("COMMIT", "unknown"), info.get("RELEASE", ""), info.get("VERSION", "")
|
||||
dirty = info.get("DIRTY", "0") == "1"
|
||||
m = re.fullmatch(r"([A-Za-z0-9 ]+) (\d{4}-\d{2}-\d{2})", release)
|
||||
if not m:
|
||||
sys.exit("make_zip.py: %s isn't a labelled build (SB_BUILD_LABEL; preset windows-clang-tester)" % args.build)
|
||||
label, day = m.group(1), m.group(2)
|
||||
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
|
||||
sys.exit("make_zip.py: %s's build_info.inc has no version (%r): a build from before v0.1.0's project() version" %
|
||||
(args.build, version))
|
||||
build_id = commit + ("-dirty" if dirty else "")
|
||||
if dirty and not args.allow_dirty:
|
||||
sys.exit("make_zip.py: the build is dirty (%s): commit first, or --allow-dirty" % build_id)
|
||||
name_line = "SpeedBreaker v%s %s (%s)" % (version, release, build_id)
|
||||
kind = "-".join(w for w in label.lower().split() if w not in ("build", "windows")) or "build"
|
||||
stem = "SpeedBreaker-%s-windows-x64-%s-%s-%s" % (version, kind, day, build_id)
|
||||
if args.public:
|
||||
if dirty:
|
||||
sys.exit("make_zip.py: --public needs a clean build (%s)" % build_id)
|
||||
stem = "SpeedBreaker-%s-windows-x64-%s" % (version, kind)
|
||||
print("build: " + name_line)
|
||||
|
||||
exe_path = os.path.join(args.build, "runtime", "SpeedBreaker.exe")
|
||||
pdb_path = os.path.join(args.build, "runtime", "SpeedBreaker.pdb")
|
||||
exe, pdb = read(exe_path), read(pdb_path)
|
||||
|
||||
# The exe is that build, for testers (the line is put together at run
|
||||
# time from the version, the label and the commit, each a string in the
|
||||
# exe).
|
||||
for part in (version, release, commit):
|
||||
if part.encode() not in exe:
|
||||
fail("the exe doesn't carry %r of its build line %r (another build than build_info.inc's?)" % (part, name_line))
|
||||
machine, subsystem, exports, codeview = pe_info(exe)
|
||||
if machine != 0x8664:
|
||||
fail("the exe isn't x64 (machine 0x%04X)" % machine)
|
||||
if subsystem != 2:
|
||||
fail("the exe isn't linked for the Windows subsystem (subsystem %d; SB_WINDOWS_GUI=ON)" % subsystem)
|
||||
for wanted in ("NvOptimusEnablement", "AmdPowerXpressRequestHighPerformance"):
|
||||
if wanted not in exports:
|
||||
fail("the exe doesn't export %s (exports: %s)" % (wanted, ", ".join(exports) or "none"))
|
||||
pdb_id, pdb_age = pdb_guid(pdb_path)
|
||||
if not codeview or codeview[0] != pdb_id:
|
||||
fail("SpeedBreaker.pdb isn't the exe's (exe %s, pdb %s)" % (guid_text(codeview[0]) if codeview else "no debug record",
|
||||
guid_text(pdb_id)))
|
||||
notes.append("exe: x64, subsystem %d (Windows), exports %s; debug record %s age %d, %s" %
|
||||
(subsystem, ", ".join(exports), guid_text(codeview[0]) if codeview else "-", codeview[1] if codeview else 0,
|
||||
codeview[2] if codeview else "-"))
|
||||
notes.append("pdb: GUID %s, age %d (matches the exe)" % (guid_text(pdb_id), pdb_age))
|
||||
|
||||
# The zip's members.
|
||||
members = [("SpeedBreaker.exe", exe)] + ([] if args.public else [("SpeedBreaker.pdb", pdb)])
|
||||
docs = PUBLIC_DOCS if args.public else DOCS + tuple(d for d in OPTIONAL_DOCS if os.path.exists(os.path.join(args.docs, d)))
|
||||
for doc in docs:
|
||||
text = read(os.path.join(args.docs, doc))
|
||||
members.append((args.checklist_name if doc == "CHECKLIST.md" else doc, text))
|
||||
license_files, readme = licenses(args.deps_src)
|
||||
members.append(("licenses/README.txt", crlf(licenses_readme(name_line, readme, stem + "-source.zip",
|
||||
(commit, version) if args.public else None)).encode("utf-8")))
|
||||
for name, text in license_files:
|
||||
members.append(("licenses/" + name, crlf(text).encode("utf-8")))
|
||||
|
||||
# Checks.
|
||||
hashes = game_hashes()
|
||||
allowed = re.compile(r"(SpeedBreaker\.(exe|pdb)|[A-Z-]+\.(txt|md)|licenses/[A-Za-z0-9.+_-]+\.txt)")
|
||||
for name, data in members:
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
if not allowed.fullmatch(name):
|
||||
fail("%s: not one of the kit's kinds of file" % name)
|
||||
if ext in GAME_EXTENSIONS:
|
||||
fail("%s: a game-data extension" % name)
|
||||
if sha256(data) in hashes:
|
||||
fail("%s: has the SHA-256 of a game file" % name)
|
||||
for name, data in members:
|
||||
if name.endswith((".txt", ".md")):
|
||||
third_party = name.startswith("licenses/") and name != "licenses/README.txt"
|
||||
scan(name, data, args.forbid, binary=False, emails_fail=not third_party, report=notes.append)
|
||||
if not third_party:
|
||||
try:
|
||||
data.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
notes.append("%s: not plain ASCII" % name)
|
||||
scan("SpeedBreaker.exe", exe, args.forbid, binary=True, emails_fail="personal", report=notes.append)
|
||||
scan("SpeedBreaker.pdb", pdb, args.forbid, binary=True, emails_fail=False, report=notes.append)
|
||||
|
||||
if problems:
|
||||
print("make_zip.py: %d problem(s); no zip made" % len(problems), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
zip_path = os.path.join(args.out, stem + ".zip")
|
||||
stamp = datetime.datetime.now().timetuple()[:6]
|
||||
with zipfile.ZipFile(zip_path + ".partial", "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
|
||||
for name, data in members:
|
||||
entry = zipfile.ZipInfo(name, stamp)
|
||||
entry.compress_type = zipfile.ZIP_DEFLATED
|
||||
entry.external_attr = 0o644 << 16
|
||||
z.writestr(entry, data)
|
||||
os.replace(zip_path + ".partial", zip_path)
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
bad = z.testzip()
|
||||
if bad:
|
||||
sys.exit("make_zip.py: %s fails its own CRC check at %s" % (zip_path, bad))
|
||||
listing = [(i.filename, i.file_size, i.compress_size) for i in z.infolist()]
|
||||
|
||||
zip_sha = sha256(read(zip_path))
|
||||
if args.public:
|
||||
# As the other downloads' .sha256: "<hash> <name>" and a newline.
|
||||
with open(zip_path + ".sha256", "w", encoding="ascii", newline="\n") as f:
|
||||
f.write("%s %s\n" % (zip_sha, os.path.basename(zip_path)))
|
||||
lines = [name_line, "zip: %s (%d bytes), SHA-256 %s" % (os.path.basename(zip_path), os.path.getsize(zip_path), zip_sha),
|
||||
"exe: SHA-256 %s (%d bytes)" % (sha256(exe), len(exe)),
|
||||
"pdb: SHA-256 %s (%d bytes)%s" % (sha256(pdb), len(pdb), " (not in the zip: --public)" if args.public else ""), ""]
|
||||
lines += ["%-46s %10d -> %10d" % entry for entry in listing]
|
||||
lines += ["", "checks: no game-data extension or game file's SHA-256; no forbidden word (%d given) or user-profile path"
|
||||
% len(args.forbid)] + notes
|
||||
with open(os.path.join(args.out, stem + ".txt"), "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
print("\n".join(lines))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+79
-10
@@ -11,10 +11,25 @@ function(sb_test name)
|
||||
add_executable(${name} ${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp ${T_SOURCES})
|
||||
target_include_directories(${name} PRIVATE ${RT})
|
||||
target_link_libraries(${name} PRIVATE Threads::Threads)
|
||||
target_compile_options(${name} PRIVATE -O2 -UNDEBUG)
|
||||
if(SB_MSVC_FRONTEND)
|
||||
target_compile_options(${name} PRIVATE /O2 -UNDEBUG)
|
||||
else()
|
||||
target_compile_options(${name} PRIVATE -O2 -UNDEBUG)
|
||||
endif()
|
||||
add_test(NAME ${name} COMMAND ${name} ${T_ARGS})
|
||||
endfunction()
|
||||
|
||||
# user/settings.cpp makes its file calls through the platform layer
|
||||
# (platform/file_io.h, the Windows port's): each test that links it links
|
||||
# that layer as well (Windows: its folders too, with their system libraries).
|
||||
if(WIN32)
|
||||
set(SB_SETTINGS_SOURCES ${RT}/user/settings.cpp ${RT}/platform/win32/file_io.cpp ${RT}/platform/win32/folders.cpp)
|
||||
set(SB_SETTINGS_LIBS shell32 ole32)
|
||||
else()
|
||||
set(SB_SETTINGS_SOURCES ${RT}/user/settings.cpp ${RT}/platform/posix/file_io.cpp)
|
||||
set(SB_SETTINGS_LIBS)
|
||||
endif()
|
||||
|
||||
sb_test(block_ranges_test)
|
||||
# Where guest memory and the upload ring go (gpu/memory_plan.h, header only).
|
||||
sb_test(memory_plan_test)
|
||||
@@ -99,7 +114,8 @@ endif()
|
||||
# Upscaling's values in settings.toml (user/settings.cpp as the game links
|
||||
# it): "fsr1" read, saved and read back, the environment's override, an
|
||||
# unknown value; in a scratch folder.
|
||||
sb_test(settings_scaling_test SOURCES ${RT}/user/settings.cpp ARGS ${CMAKE_CURRENT_BINARY_DIR}/settings_scaling_scratch)
|
||||
sb_test(settings_scaling_test SOURCES ${SB_SETTINGS_SOURCES} ARGS ${CMAKE_CURRENT_BINARY_DIR}/settings_scaling_scratch)
|
||||
target_link_libraries(settings_scaling_test PRIVATE ${SB_SETTINGS_LIBS})
|
||||
# Internal Resolution Auto's budget on iPhones and iPads, chip by chip
|
||||
# (video/scale_budget.cpp), and the scale it makes on the phones' screens.
|
||||
sb_test(scale_budget_test SOURCES ${RT}/video/scale_budget.cpp ${RT}/video/picture_fit.cpp)
|
||||
@@ -112,19 +128,24 @@ sb_test(display_tone_test ARGS ${RT}/video/presenter.cpp ${RT}/user/settings.cpp
|
||||
# The display's refresh measured from present completions, at 60 Hz and at
|
||||
# whole multiples of it (video/refresh_fit.cpp, video/refresh_multiple.h).
|
||||
sb_test(refresh_fit_test SOURCES ${RT}/video/refresh_fit.cpp)
|
||||
# The [perf] "repeated" count in the panel's own refreshes (video/repeats.h,
|
||||
# header only; the Windows presenter's).
|
||||
sb_test(repeats_test)
|
||||
# The one-time notice on a screen whose rate 60 fps can't divide evenly (90,
|
||||
# 144, 165 Hz): when it shows (variable refresh, Game Mode, a Mac's ProMotion,
|
||||
# dismissed, the moment), its words, the grid check under it, and the
|
||||
# dismissal in settings.toml, in a scratch folder (video/refresh_notice.h,
|
||||
# user/settings.cpp).
|
||||
sb_test(refresh_notice_test SOURCES ${RT}/video/refresh_fit.cpp ${RT}/user/settings.cpp
|
||||
sb_test(refresh_notice_test SOURCES ${RT}/video/refresh_fit.cpp ${SB_SETTINGS_SOURCES}
|
||||
ARGS ${CMAKE_CURRENT_BINARY_DIR}/refresh_notice_scratch)
|
||||
target_link_libraries(refresh_notice_test PRIVATE ${SB_SETTINGS_LIBS})
|
||||
# The one-time note that an Xbox controller by USB on a Mac takes no rumble
|
||||
# (hid/rumble_note.h): a wired Xbox pad on a Mac gets it once, a DualSense
|
||||
# wired, an Xbox pad over Bluetooth and other platforms never; the dismissal
|
||||
# in settings.toml, in a scratch folder (user/settings.cpp).
|
||||
sb_test(rumble_note_test SOURCES ${RT}/user/settings.cpp
|
||||
sb_test(rumble_note_test SOURCES ${SB_SETTINGS_SOURCES}
|
||||
ARGS ${CMAKE_CURRENT_BINARY_DIR}/rumble_note_scratch)
|
||||
target_link_libraries(rumble_note_test PRIVATE ${SB_SETTINGS_LIBS})
|
||||
# How late the guest vblank's thread wakes on this machine, alone and with
|
||||
# every core busy (video/vblank_timer.cpp; checked on macOS, where its
|
||||
# scheduling changed in v0.1.4).
|
||||
@@ -171,6 +192,9 @@ sb_test(hfr_blend_test)
|
||||
sb_test(hfr_schedule_test)
|
||||
sb_test(hfr_grid_test)
|
||||
sb_test(guest_time_test SOURCES ${RT}/cpu/guest_time.cpp)
|
||||
if(WIN32)
|
||||
target_link_libraries(guest_time_test PRIVATE winmm) # timeBeginPeriod
|
||||
endif()
|
||||
# When the mouse cursor shows over the game (ui/cursor_idle.h, header only),
|
||||
# and how ui.cpp drives it.
|
||||
sb_test(cursor_idle_test ARGS ${RT}/ui/ui.cpp)
|
||||
@@ -182,15 +206,50 @@ if(Python3_FOUND)
|
||||
${CMAKE_CURRENT_BINARY_DIR}/report_zip_out)
|
||||
set_tests_properties(report_zip_check PROPERTIES DEPENDS report_zip_test)
|
||||
endif()
|
||||
# Crash and hang reports: which symbols are recompiled functions, and the
|
||||
# home folder scrubbed from a bug report (both header only).
|
||||
sb_test(symbol_names_test)
|
||||
sb_test(scrub_test)
|
||||
if(WIN32)
|
||||
# Windows' stacks and names (platform/stack.h: DbgHelp, StackWalk64,
|
||||
# another thread's stack), against this test's own PDB: its main file
|
||||
# with debug information, and a file of functions shaped as the
|
||||
# recompiled ones without any, as ppc/ is built.
|
||||
sb_test(win_stack_test SOURCES ${RT}/platform/win32/stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/win_stack_test_guest.cpp)
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/win_stack_test.cpp ${RT}/platform/win32/stack.cpp PROPERTIES
|
||||
COMPILE_OPTIONS /Z7)
|
||||
target_link_options(win_stack_test PRIVATE /DEBUG /OPT:REF /OPT:NOICF)
|
||||
target_link_libraries(win_stack_test PRIVATE dbghelp)
|
||||
endif()
|
||||
# Where the per-user folders go, and the platform layer's file calls.
|
||||
sb_test(paths_test)
|
||||
if(WIN32)
|
||||
sb_test(file_io_test SOURCES ${RT}/platform/win32/file_io.cpp ARGS ${CMAKE_CURRENT_BINARY_DIR}/file_io_out)
|
||||
target_sources(paths_test PRIVATE ${RT}/platform/win32/folders.cpp)
|
||||
target_link_libraries(paths_test PRIVATE shell32 ole32)
|
||||
else()
|
||||
sb_test(file_io_test SOURCES ${RT}/platform/posix/file_io.cpp ARGS ${CMAKE_CURRENT_BINARY_DIR}/file_io_out)
|
||||
endif()
|
||||
# Settings: a device's own defaults, before and after Load.
|
||||
if(WIN32)
|
||||
sb_test(settings_test SOURCES ${RT}/user/settings.cpp ${RT}/platform/win32/file_io.cpp ${RT}/platform/win32/folders.cpp
|
||||
ARGS ${CMAKE_CURRENT_BINARY_DIR}/settings_out)
|
||||
target_link_libraries(settings_test PRIVATE shell32 ole32)
|
||||
else()
|
||||
sb_test(settings_test SOURCES ${RT}/user/settings.cpp ${RT}/platform/posix/file_io.cpp
|
||||
ARGS ${CMAKE_CURRENT_BINARY_DIR}/settings_out)
|
||||
endif()
|
||||
target_include_directories(settings_test SYSTEM PRIVATE ${ROOT}/tools/XenonRecomp/thirdparty/tomlplusplus/include)
|
||||
# "Choose a folder..." in the installer (install/placement.h): where the game
|
||||
# goes in a picked folder and what is refused, on planted facts and in a
|
||||
# scratch folder with a sandbox HOME. POSIX only (mkdtemp, chmod, ACLs and
|
||||
# scratch folder with a sandbox HOME (the installer's file calls through the
|
||||
# platform layer, platform/file_io.h). POSIX only (mkdtemp, chmod, ACLs and
|
||||
# links in its disk checks); the screen hides the entry on Windows until
|
||||
# placement.cpp is ported there.
|
||||
if(NOT WIN32)
|
||||
sb_test(install_folder_test SOURCES ${RT}/install/placement.cpp ${RT}/install/installer.cpp ${RT}/install/locate.cpp
|
||||
${RT}/install/manifest.cpp ${RT}/install/result.cpp ${RT}/install/disc_source.cpp ${RT}/install/sha256.cpp
|
||||
${RT}/user/portable.cpp
|
||||
${RT}/user/portable.cpp ${RT}/platform/posix/file_io.cpp
|
||||
ARGS ${CMAKE_BINARY_DIR}/install_folder_scratch) # not beside the test: that's its "own folder", refused
|
||||
# Portable mode (user/portable.h) and --portable on|off
|
||||
# (install/portable_switch.h): the layout, the records that move with
|
||||
@@ -198,7 +257,7 @@ if(NOT WIN32)
|
||||
# the copies (a stand-in copies the game), in a scratch folder.
|
||||
sb_test(portable_test SOURCES ${RT}/user/portable.cpp ${RT}/install/portable_switch.cpp ${RT}/install/placement.cpp
|
||||
${RT}/install/installer.cpp ${RT}/install/locate.cpp ${RT}/install/manifest.cpp ${RT}/install/result.cpp
|
||||
${RT}/install/disc_source.cpp ${RT}/install/sha256.cpp
|
||||
${RT}/install/disc_source.cpp ${RT}/install/sha256.cpp ${RT}/platform/posix/file_io.cpp
|
||||
ARGS ${CMAKE_BINARY_DIR}/portable_scratch)
|
||||
if(APPLE AND NOT IOS)
|
||||
target_link_libraries(install_folder_test PRIVATE "-framework CoreFoundation") # placement.cpp's iCloud check
|
||||
@@ -240,12 +299,14 @@ set_target_properties(key_names_test PROPERTIES SKIP_BUILD_RPATH OFF)
|
||||
# The Key options (Settings > Controls): the table, the [keys] names in the
|
||||
# file, and what Set accepts (user/settings.cpp and the toml++ XenonRecomp
|
||||
# vendors; no SDL).
|
||||
sb_test(settings_keys_test SOURCES ${RT}/user/settings.cpp)
|
||||
sb_test(settings_keys_test SOURCES ${SB_SETTINGS_SOURCES})
|
||||
target_link_libraries(settings_keys_test PRIVATE ${SB_SETTINGS_LIBS})
|
||||
target_include_directories(settings_keys_test SYSTEM PRIVATE ${ROOT}/tools/XenonRecomp/thirdparty/tomlplusplus/include)
|
||||
# Settings > Game > Language (game/language.h, header only, and the option in
|
||||
# user/settings.cpp): English untouched for every caller, the game's ten
|
||||
# languages, the movie suffix by the files present, what the files give.
|
||||
sb_test(language_test SOURCES ${RT}/user/settings.cpp)
|
||||
sb_test(language_test SOURCES ${SB_SETTINGS_SOURCES})
|
||||
target_link_libraries(language_test PRIVATE ${SB_SETTINGS_LIBS})
|
||||
target_include_directories(language_test SYSTEM PRIVATE ${ROOT}/tools/XenonRecomp/thirdparty/tomlplusplus/include)
|
||||
# The keyboard's controls and how held keys become a pad (hid/key_map.h), header only.
|
||||
sb_test(key_map_test)
|
||||
@@ -277,7 +338,7 @@ endif()
|
||||
# Performance Tuning labels, GitHub issue #8) as the guest dispatches them.
|
||||
sb_test(switch_tables_test ARGS ${ROOT}/config/switch_tables.toml ${ROOT}/config/nfsmw.toml)
|
||||
# The write watch around CPU write faults, on physical memory mapped as the
|
||||
# runtime maps it.
|
||||
# runtime maps it (three windows of one shared object).
|
||||
sb_test(write_watch_test SOURCES ${RT}/kernel/write_watch.cpp ${RT}/cpu/guest_time.cpp)
|
||||
target_include_directories(write_watch_test PRIVATE ${RT}/kernel ${RT}/thirdparty/o1heap ${ROOT}/ppc
|
||||
${ROOT}/tools/XenonRecomp/XenonUtils ${ROOT}/tools/XenonRecomp/thirdparty/simde)
|
||||
@@ -285,9 +346,17 @@ target_include_directories(write_watch_test PRIVATE ${RT}/kernel ${RT}/thirdpart
|
||||
# (16 KB on Apple): only whole host pages remapped, so a 4 KB-page heap's
|
||||
# neighbouring allocation in the same host page survives (kernel/vmem.cpp; the
|
||||
# host page size injected, so 16 and 64 KB are checked on any host).
|
||||
if(NOT WIN32) # POSIX mappings; Windows' host pages are 4 KB (guest pages)
|
||||
sb_test(vmem_host_pages_test SOURCES ${RT}/kernel/vmem.cpp)
|
||||
target_include_directories(vmem_host_pages_test PRIVATE ${RT}/kernel ${RT}/thirdparty/o1heap ${ROOT}/ppc
|
||||
${ROOT}/tools/XenonRecomp/XenonUtils ${ROOT}/tools/XenonRecomp/thirdparty/simde)
|
||||
endif()
|
||||
if(WIN32)
|
||||
# The mirrors (platform/win32/memory.cpp) and the page protection.
|
||||
target_sources(write_watch_test PRIVATE ${RT}/platform/win32/memory.cpp ${RT}/platform/win32/os.cpp)
|
||||
# The process-wide settings main makes first (platform/startup.h).
|
||||
sb_test(startup_test SOURCES ${RT}/platform/win32/startup.cpp)
|
||||
endif()
|
||||
# The guest vblank lock's closed-loop simulation (a tool more than a test).
|
||||
add_executable(vblank_lock_sim ${CMAKE_CURRENT_SOURCE_DIR}/vblank_lock_sim.cpp ${RT}/video/refresh_fit.cpp ${RT}/video/vblank_lock.cpp)
|
||||
target_include_directories(vblank_lock_sim PRIVATE ${RT})
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
// The platform layer's file calls (runtime/platform/file_io.h), on the
|
||||
// host's own file system, in a folder given on the command line (made if
|
||||
// missing; the files are removed afterwards). From the repo root:
|
||||
// clang++ -std=c++20 -O2 -pthread -Iruntime tests/file_io_test.cpp runtime/platform/posix/file_io.cpp -o build/file_io_test
|
||||
// build/file_io_test build/file_io_out
|
||||
// (Windows: runtime/platform/win32/file_io.cpp; tests/CMakeLists.txt.)
|
||||
// The disc image reads past 4 GB (MSVC's off_t is 32-bit), the guest file
|
||||
// reads that must report a protected destination as EFAULT (imports_file's
|
||||
// bounce buffer), the settings save that replaces its file every time, the
|
||||
// installer's writability check and the install lock. A 5 GB sparse file
|
||||
// stands in for a disc image: it uses no disk space. On Windows the
|
||||
// permission checks also run as an ordinary user would (a restricted copy of
|
||||
// this process's token): an SSH session is elevated, and an administrator
|
||||
// passes every permission check.
|
||||
#include <platform/file_io.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/os.h>
|
||||
#include <aclapi.h>
|
||||
#include <sddl.h>
|
||||
#include <winioctl.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string ReadAll(const fs::path& path)
|
||||
{
|
||||
std::string text;
|
||||
if (FILE* f = platform::OpenStdioFile(path, "rb"))
|
||||
{
|
||||
char buffer[256];
|
||||
size_t n;
|
||||
while ((n = fread(buffer, 1, sizeof(buffer), f)) > 0)
|
||||
text.append(buffer, n);
|
||||
fclose(f);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static bool WriteText(const fs::path& path, const char* text)
|
||||
{
|
||||
FILE* f = platform::OpenStdioFile(path, "wb");
|
||||
if (!f)
|
||||
return false;
|
||||
bool ok = fwrite(text, 1, strlen(text), f) == strlen(text);
|
||||
return fclose(f) == 0 && ok;
|
||||
}
|
||||
|
||||
// Marks a new, empty file sparse, so growing it costs no disk space
|
||||
// (Windows only: NTFS files are not sparse unless asked).
|
||||
static bool MakeSparse(int fd)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
DWORD bytes = 0;
|
||||
return DeviceIoControl(reinterpret_cast<HANDLE>(_get_osfhandle(fd)), FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &bytes, nullptr) != 0;
|
||||
#else
|
||||
(void)fd;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The installer's probes leave nothing behind.
|
||||
static int Leftovers(const fs::path& dir)
|
||||
{
|
||||
int count = 0;
|
||||
std::error_code ec;
|
||||
for (const fs::directory_entry& e : fs::directory_iterator(dir, ec))
|
||||
if (e.path().filename().string().starts_with(".speedbreaker-write-test"))
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// For a thread to impersonate (SetThreadToken): this process's token as an
|
||||
// ordinary user's is, not elevated: the Administrators group deny-only,
|
||||
// every privilege but traverse checking dropped, and new objects owned by
|
||||
// the user rather than by Administrators. nullptr if it can't be made.
|
||||
static HANDLE OrdinaryUserToken()
|
||||
{
|
||||
HANDLE process = nullptr, restricted = nullptr, impersonation = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ASSIGN_PRIMARY
|
||||
| TOKEN_IMPERSONATE, &process))
|
||||
return nullptr;
|
||||
BYTE admins[SECURITY_MAX_SID_SIZE];
|
||||
DWORD size = sizeof(admins);
|
||||
SID_AND_ATTRIBUTES disable{ admins, 0 };
|
||||
if (CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &size)
|
||||
&& CreateRestrictedToken(process, DISABLE_MAX_PRIVILEGE, 1, &disable, 0, nullptr, 0, nullptr, &restricted))
|
||||
{
|
||||
alignas(TOKEN_USER) BYTE user[256];
|
||||
DWORD n = 0;
|
||||
if (GetTokenInformation(restricted, TokenUser, user, sizeof(user), &n))
|
||||
{
|
||||
TOKEN_OWNER owner{ reinterpret_cast<TOKEN_USER*>(user)->User.Sid };
|
||||
SetTokenInformation(restricted, TokenOwner, &owner, sizeof(owner));
|
||||
}
|
||||
DuplicateToken(restricted, SecurityImpersonation, &impersonation);
|
||||
CloseHandle(restricted);
|
||||
}
|
||||
CloseHandle(process);
|
||||
return impersonation;
|
||||
}
|
||||
|
||||
static bool IsAdministrator()
|
||||
{
|
||||
BYTE admins[SECURITY_MAX_SID_SIZE];
|
||||
DWORD size = sizeof(admins);
|
||||
BOOL member = FALSE;
|
||||
return CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &size)
|
||||
&& CheckTokenMembership(nullptr, admins, &member) && member;
|
||||
}
|
||||
|
||||
// Runs `f` on a thread of its own impersonating `token`.
|
||||
template <typename F>
|
||||
static bool RunAs(HANDLE token, F f)
|
||||
{
|
||||
bool ran = false;
|
||||
std::thread t([&] {
|
||||
if (!SetThreadToken(nullptr, token))
|
||||
return;
|
||||
ran = true;
|
||||
f();
|
||||
RevertToSelf();
|
||||
});
|
||||
t.join();
|
||||
return ran;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
printf("usage: %s <folder>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const fs::path dir = argv[1];
|
||||
std::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
|
||||
printf("1. create, exclusively\n");
|
||||
const fs::path file = dir / "file.bin";
|
||||
platform::RemoveFile(file);
|
||||
int fd = platform::OpenFile(file, O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
CHECK(fd >= 0, "%s", strerror(errno));
|
||||
int again = platform::OpenFile(file, O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
CHECK(again < 0 && errno == EEXIST, "a second exclusive create: %d, %s", again, strerror(errno));
|
||||
int missing = platform::OpenFile(dir / "missing.bin", O_RDONLY | O_CLOEXEC);
|
||||
CHECK(missing < 0 && errno == ENOENT, "a missing file: %d, %s", missing, strerror(errno));
|
||||
|
||||
printf("2. positional reads and writes, sizes\n");
|
||||
CHECK(platform::WriteAt(fd, "hello", 5, 0) == 5, "%s", strerror(errno));
|
||||
char buffer[16] = {};
|
||||
CHECK(platform::ReadAt(fd, buffer, sizeof(buffer), 0) == 5 && memcmp(buffer, "hello", 5) == 0, "read back '%.5s'", buffer);
|
||||
CHECK(platform::ReadAt(fd, buffer, sizeof(buffer), 5) == 0, "a read at the end isn't 0");
|
||||
platform::FileInfo info, byPath;
|
||||
CHECK(platform::InfoOf(fd, info) == 0 && info.size == 5 && info.regular && !info.directory, "size %llu", (unsigned long long)info.size);
|
||||
CHECK(platform::InfoOf(file, byPath) == 0 && byPath.device == info.device && byPath.inode == info.inode,
|
||||
"the path and the descriptor aren't the same file");
|
||||
|
||||
printf("3. past 4 GB (a 5 GB sparse file)\n");
|
||||
constexpr uint64_t kGiB = 1ull << 30;
|
||||
CHECK(platform::Truncate(fd, 0) == 0, "%s", strerror(errno));
|
||||
bool sparse = MakeSparse(fd);
|
||||
CHECK(sparse, "couldn't make the file sparse");
|
||||
if (sparse)
|
||||
{
|
||||
CHECK(platform::Truncate(fd, 5 * kGiB) == 0, "%s", strerror(errno));
|
||||
const uint64_t at = 4 * kGiB + kGiB / 2 + 3;
|
||||
CHECK(platform::WriteAt(fd, "past4GiB", 8, at) == 8, "%s", strerror(errno));
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
CHECK(platform::ReadAt(fd, buffer, 8, at) == 8 && memcmp(buffer, "past4GiB", 8) == 0, "read back '%.8s'", buffer);
|
||||
// The same bytes 4 GB lower must still be zero (a 32-bit offset would
|
||||
// have written there).
|
||||
memset(buffer, 1, sizeof(buffer));
|
||||
CHECK(platform::ReadAt(fd, buffer, 8, at - 4 * kGiB) == 8 && buffer[0] == 0 && buffer[7] == 0, "4 GB lower isn't zero");
|
||||
CHECK(platform::ReadAt(fd, buffer, 8, 5 * kGiB) == 0, "a read at 5 GB isn't the end");
|
||||
CHECK(platform::InfoOf(fd, info) == 0 && info.size == 5 * kGiB, "size %llu", (unsigned long long)info.size);
|
||||
CHECK(platform::Truncate(fd, 0) == 0, "%s", strerror(errno));
|
||||
}
|
||||
|
||||
printf("4. a read into a read-only page is EFAULT, and reads nothing\n");
|
||||
CHECK(platform::WriteAt(fd, "data", 4, 0) == 4, "%s", strerror(errno));
|
||||
#ifdef _WIN32
|
||||
void* page = VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
DWORD oldProtect;
|
||||
VirtualProtect(page, 4096, PAGE_READONLY, &oldProtect);
|
||||
#else
|
||||
void* page = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
mprotect(page, 4096, PROT_READ);
|
||||
#endif
|
||||
errno = 0;
|
||||
int64_t got = platform::ReadAt(fd, page, 4, 0);
|
||||
CHECK(got < 0 && errno == EFAULT, "%lld, %s", (long long)got, strerror(errno));
|
||||
CHECK(static_cast<const char*>(page)[0] == 0, "the page was written");
|
||||
close(fd);
|
||||
|
||||
printf("5. replace a file, twice (a settings save after the first)\n");
|
||||
const fs::path target = dir / "settings.toml", temp = dir / "settings.toml.tmp";
|
||||
CHECK(WriteText(target, "old") && WriteText(temp, "new 1"), "can't write the files");
|
||||
CHECK(platform::AtomicReplace(temp, target) == 0 && ReadAll(target) == "new 1" && !fs::exists(temp, ec), "first: '%s'", ReadAll(target).c_str());
|
||||
CHECK(WriteText(temp, "new 2"), "can't write the file");
|
||||
CHECK(platform::AtomicReplace(temp, target) == 0 && ReadAll(target) == "new 2" && !fs::exists(temp, ec), "second: '%s'", ReadAll(target).c_str());
|
||||
CHECK(platform::RemoveFile(target) == 0 && platform::RemoveFile(target) != 0 && errno == ENOENT, "removing: %s", strerror(errno));
|
||||
|
||||
printf("6. a name that isn't ASCII\n");
|
||||
const fs::path unicode = dir / fs::path(std::u8string(u8"über-日本.txt"));
|
||||
CHECK(WriteText(unicode, "unicode") && ReadAll(unicode) == "unicode", "'%s'", ReadAll(unicode).c_str());
|
||||
platform::RemoveFile(unicode);
|
||||
|
||||
printf("7. folders\n");
|
||||
CHECK(platform::InfoOf(dir, info) == 0 && info.directory && !info.regular, "the folder isn't one");
|
||||
int dirFd = platform::OpenFile(dir, O_WRONLY | O_CLOEXEC);
|
||||
CHECK(dirFd < 0 && errno == EISDIR, "writing a folder: %d, %s", dirFd, strerror(errno));
|
||||
int canFile = platform::CheckWritableFolder(dir, platform::NewEntry::File);
|
||||
int canFolder = platform::CheckWritableFolder(dir, platform::NewEntry::Folder);
|
||||
CHECK(canFile == 0 && canFolder == 0, "not writable: a file %s, a folder %s", strerror(canFile), strerror(canFolder));
|
||||
std::optional<uint64_t> space = platform::FreeSpace(dir);
|
||||
CHECK(space && *space > 0, "no free space figure");
|
||||
if (space)
|
||||
printf(" %.1f GB free\n", double(*space) / double(kGiB));
|
||||
|
||||
printf("8. writability checks at the same time (the installer screen's and an install's) don't collide\n");
|
||||
{
|
||||
std::atomic<int> refused{ 0 };
|
||||
std::vector<std::thread> checkers;
|
||||
for (int t = 0; t < 4; t++)
|
||||
checkers.emplace_back([&] {
|
||||
for (int i = 0; i < 100; i++)
|
||||
if (platform::CheckWritableFolder(dir, i % 2 ? platform::NewEntry::Folder : platform::NewEntry::File) != 0)
|
||||
refused++;
|
||||
});
|
||||
for (std::thread& t : checkers)
|
||||
t.join();
|
||||
CHECK(refused == 0, "%d of 400 checks refused", refused.load());
|
||||
CHECK(Leftovers(dir) == 0, "%d test files or folders left behind", Leftovers(dir));
|
||||
}
|
||||
|
||||
printf("9. the folder lock: a second holder is turned away until the first lets go\n");
|
||||
bool busy = true;
|
||||
intptr_t lock = platform::LockFolder(dir, busy);
|
||||
CHECK(lock >= 0 && !busy, "the first lock: %lld", (long long)lock);
|
||||
#ifdef _WIN32
|
||||
{
|
||||
// In the Global namespace: an install over SSH (session 0) and one at
|
||||
// the desktop (session 1 or more) see the same lock.
|
||||
CHECK(platform::InfoOf(dir, info) == 0, "%s", strerror(errno));
|
||||
wchar_t name[96];
|
||||
swprintf(name, std::size(name), L"Global\\SpeedBreaker-folder-%08X-%016llX", uint32_t(info.device),
|
||||
(unsigned long long)info.inode);
|
||||
HANDLE global = OpenMutexW(SYNCHRONIZE, FALSE, name);
|
||||
CHECK(global != nullptr, "no lock in the Global namespace: error %lu", GetLastError());
|
||||
if (global)
|
||||
CloseHandle(global);
|
||||
// And an ordinary user's token is told it's busy, not let in.
|
||||
if (HANDLE token = OrdinaryUserToken())
|
||||
{
|
||||
bool userBusy = false;
|
||||
intptr_t userLock = -1;
|
||||
CHECK(RunAs(token, [&] { userLock = platform::LockFolder(dir, userBusy); platform::UnlockFolder(userLock); }),
|
||||
"can't impersonate: error %lu", GetLastError());
|
||||
CHECK(userLock < 0 && userBusy, "an ordinary user's lock: %lld, busy %d", (long long)userLock, userBusy);
|
||||
CloseHandle(token);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::atomic<int> state{ 0 }; // 1: refused as busy, 2: taken after the unlock
|
||||
std::thread other([&] {
|
||||
bool b = false;
|
||||
intptr_t second = platform::LockFolder(dir, b);
|
||||
if (second < 0 && b)
|
||||
state = 1;
|
||||
platform::UnlockFolder(second);
|
||||
});
|
||||
other.join();
|
||||
CHECK(state == 1, "the second lock wasn't refused as busy");
|
||||
platform::UnlockFolder(lock);
|
||||
std::thread later([&] {
|
||||
bool b = true;
|
||||
intptr_t third = platform::LockFolder(dir, b);
|
||||
if (third >= 0 && !b)
|
||||
state = 2;
|
||||
platform::UnlockFolder(third);
|
||||
});
|
||||
later.join();
|
||||
CHECK(state == 2, "the lock wasn't free after the unlock");
|
||||
|
||||
#ifdef _WIN32
|
||||
printf("10. a drive's root, as an ordinary user: new folders yes, new files no\n");
|
||||
{
|
||||
// A folder with C:\'s own ACL, as Windows sets it on every NTFS
|
||||
// drive's root: Authenticated Users may add folders (LC,
|
||||
// FILE_ADD_SUBDIRECTORY) but not files, and may change what is in
|
||||
// the folders they add (the inherit-only Modify).
|
||||
const fs::path root = dir / "drive-root";
|
||||
if (fs::exists(root, ec)) // left by a failed run: make it deletable again
|
||||
SetNamedSecurityInfoW(const_cast<wchar_t*>(root.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr,
|
||||
nullptr, nullptr, nullptr);
|
||||
fs::remove_all(root, ec);
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:P(A;;LC;;;AU)(A;OICIIO;SDGXGWGR;;;AU)(A;OICI;FA;;;SY)"
|
||||
L"(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)", SDDL_REVISION_1, &descriptor, nullptr);
|
||||
SECURITY_ATTRIBUTES security{ sizeof(SECURITY_ATTRIBUTES), descriptor, FALSE };
|
||||
bool made = descriptor && CreateDirectoryW(root.c_str(), &security);
|
||||
CHECK(made, "can't make %s: error %lu", root.string().c_str(), GetLastError());
|
||||
LocalFree(descriptor);
|
||||
HANDLE token = OrdinaryUserToken();
|
||||
CHECK(token != nullptr, "no ordinary user's token: error %lu", GetLastError());
|
||||
if (made && token)
|
||||
{
|
||||
bool admin = true;
|
||||
int folderError = -1, fileError = -1;
|
||||
CHECK(RunAs(token, [&] {
|
||||
admin = IsAdministrator();
|
||||
folderError = platform::CheckWritableFolder(root, platform::NewEntry::Folder);
|
||||
fileError = platform::CheckWritableFolder(root, platform::NewEntry::File);
|
||||
}), "can't impersonate: error %lu", GetLastError());
|
||||
printf(" this process %s an administrator; the restricted token %s\n", IsAdministrator() ? "is" : "isn't",
|
||||
admin ? "still is (no test)" : "isn't");
|
||||
CHECK(!admin, "the restricted token is still an administrator's");
|
||||
CHECK(folderError == 0, "a new folder (what the installer makes there) was refused: %s", strerror(folderError));
|
||||
CHECK(fileError == EACCES, "a new file: %d (%s), but the ACL refuses one", fileError, strerror(fileError));
|
||||
CHECK(Leftovers(root) == 0 && fs::is_empty(root, ec), "the checks left something behind");
|
||||
}
|
||||
if (token)
|
||||
CloseHandle(token);
|
||||
// A NULL DACL (this process owns the folder, so it may set one): then
|
||||
// anyone may delete it.
|
||||
SetNamedSecurityInfoW(const_cast<wchar_t*>(root.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr,
|
||||
nullptr, nullptr, nullptr);
|
||||
fs::remove_all(root, ec);
|
||||
CHECK(!fs::exists(root, ec), "can't remove %s", root.string().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
platform::RemoveFile(file);
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -23,6 +23,16 @@
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <timeapi.h>
|
||||
#endif
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -66,10 +76,17 @@ static Reading Host()
|
||||
}
|
||||
|
||||
// Waits `us` microseconds of host time: spinning for the shortest, so a
|
||||
// window can be next to nothing, and sleeping otherwise.
|
||||
// window can be next to nothing, and sleeping otherwise. (Windows sleeps a
|
||||
// millisecond or more even after timeBeginPeriod(1), so it spins below 1 ms:
|
||||
// otherwise test 4's 20,000 windows of 0-200 us take minutes.)
|
||||
static void Pause(int64_t us)
|
||||
{
|
||||
if (us >= 20)
|
||||
#ifdef _WIN32
|
||||
constexpr int64_t kShortestSleepUs = 1000;
|
||||
#else
|
||||
constexpr int64_t kShortestSleepUs = 20;
|
||||
#endif
|
||||
if (us >= kShortestSleepUs)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(us));
|
||||
return;
|
||||
@@ -213,6 +230,11 @@ namespace hook
|
||||
|
||||
int main()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// As the runtime does first thing (platform/timer.h): 1 ms sleeps, not
|
||||
// the 15.6 ms tick.
|
||||
timeBeginPeriod(1);
|
||||
#endif
|
||||
// A thread left parked would hang the joins below: give up instead.
|
||||
std::thread([] {
|
||||
std::this_thread::sleep_for(120s);
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h> // _getpid
|
||||
#include <stdlib.h> // _putenv_s
|
||||
#else
|
||||
#include <unistd.h> // mkdtemp
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h> // TARGET_OS_IOS: the row is hidden there
|
||||
#endif
|
||||
@@ -347,6 +352,21 @@ int main()
|
||||
TestArchiveHash();
|
||||
TestFiles();
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows has no mkdtemp (settings_keys_test's way): a folder named
|
||||
// after this process, made new; and no unsetenv.
|
||||
const std::filesystem::path scratch = std::filesystem::temp_directory_path() / ("language_test-" + std::to_string(_getpid()));
|
||||
std::error_code scratchError;
|
||||
std::filesystem::remove_all(scratch, scratchError);
|
||||
const bool made = std::filesystem::create_directory(scratch, scratchError);
|
||||
CHECK(made, "a scratch folder");
|
||||
if (made)
|
||||
{
|
||||
_putenv_s("NFSMW_LANGUAGE", "");
|
||||
TestOption(scratch);
|
||||
std::filesystem::remove_all(scratch, scratchError);
|
||||
}
|
||||
#else
|
||||
char tmpl[] = "/tmp/language_test.XXXXXX";
|
||||
const char* dir = mkdtemp(tmpl);
|
||||
CHECK(dir != nullptr, "a scratch folder");
|
||||
@@ -356,6 +376,7 @@ int main()
|
||||
TestOption(dir);
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (g_failures)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// build/memory_plan_test
|
||||
// The memory types are the devices' own: the Steam Machine's from vulkaninfo
|
||||
// (2026-10-09), the others from their logs' chosen types (flags) and their
|
||||
// drivers' type lists (RADV, NVIDIA's Linux driver, ANV, Turnip, MoltenVK).
|
||||
// drivers' type lists (RADV, NVIDIA's Linux driver, ANV, Turnip, MoltenVK);
|
||||
// on Windows, Intel UHD's from vulkaninfo on the ThinkPad and NVIDIA's
|
||||
// Windows driver's type list.
|
||||
#include <gpu/memory_plan.h>
|
||||
|
||||
#include <cstdio>
|
||||
@@ -103,6 +105,24 @@ static Memory AnvArrowLake()
|
||||
return { { { 0x7, 0 }, { 0xF, 0 } }, { 15 * GB } };
|
||||
}
|
||||
|
||||
// Windows: Intel UHD (Comet Lake) on driver 101.2135, the ThinkPad that
|
||||
// tests the Windows port (vulkaninfo, 2026-10-09): one 7.88 GB heap, every
|
||||
// type device-local (the log: guest memory imported, ring in type 1, flags 7).
|
||||
static Memory IntelUhdWindows()
|
||||
{
|
||||
return { { { 0x1, 0 }, { 0x7, 0 }, { 0xF, 0 } }, { 8459300864ull } };
|
||||
}
|
||||
|
||||
// NVIDIA's Windows driver (an RTX card, the type list its Windows driver
|
||||
// reports): VRAM, system memory (plain and cached), and CPU-mapped VRAM as a
|
||||
// heap of its own, the 256 MB window (214-256 MB) unless resizable BAR is on
|
||||
// and the driver turns it on for the game; then all of VRAM (16 GB here, an
|
||||
// RTX 5070 Ti's).
|
||||
static Memory NvidiaWindows(uint64_t barHeap)
|
||||
{
|
||||
return { { { 0x0, 1 }, { 0x1, 0 }, { 0x1, 0 }, { 0x6, 1 }, { 0xE, 1 }, { 0x7, 2 } }, { 16 * GB, 16 * GB, barHeap } };
|
||||
}
|
||||
|
||||
static GuestInput Input(Gpu gpu, bool importable, uint32_t driver = 0, const char* shared = nullptr, const char* shadow = nullptr)
|
||||
{
|
||||
GuestInput in;
|
||||
@@ -240,6 +260,55 @@ static void Intel()
|
||||
"Intel's Linux driver");
|
||||
}
|
||||
|
||||
// Windows: the tester kits' machines. Intel's Windows driver (5) is not
|
||||
// ANV (6): it imports, as the ThinkPad does. NVIDIA's (4) without resizable
|
||||
// BAR: import first, then the copy in system memory (memory_plan.h,
|
||||
// "Windows"); kit 1 put the copy in the 256 MB window and quit.
|
||||
static void Windows()
|
||||
{
|
||||
printf("Windows:\n");
|
||||
{
|
||||
Memory m = IntelUhdWindows();
|
||||
Plan g = PlanGuestMemory(m, Input(Gpu::kIntegrated, true, 5)); // VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS
|
||||
Expect("Intel UHD (Windows), guest memory", g, false, "import,copy-vram:1", "an integrated GPU");
|
||||
CHECK(g.why.find("not imported") == std::string::npos, "Intel's Windows driver refused: %s", g.why.c_str());
|
||||
Expect("Intel UHD (Windows), ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:1,ring-cached:2");
|
||||
// The copy, when the import fails (NFSMW_TEST_ALLOC_FAIL=import): in
|
||||
// its one host-visible type, the ring beside it.
|
||||
Expect("Intel UHD (Windows), ring beside the copy", PlanRing(m, ~0u, 1), true, "ring-vram:1,ring-cached:2");
|
||||
}
|
||||
// What the ThinkPad runs to stand in for a discrete GPU without resizable
|
||||
// BAR: NFSMW_TEST_GPU_KIND=discrete NFSMW_TEST_BAR_HEAP_MB=256. No
|
||||
// host-visible type outside device memory there, so after the import
|
||||
// only the window, too small.
|
||||
{
|
||||
Memory m = IntelUhdWindows();
|
||||
ShrinkMappedVram(m, 256 * MB);
|
||||
Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 5));
|
||||
Expect("Intel UHD as discrete, 256 MB BAR, guest memory", g, false, "import,copy-vram:1!", "no resizable BAR");
|
||||
Expect("Intel UHD as discrete, 256 MB BAR, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-cached:2!,ring-vram:1!", "too small");
|
||||
}
|
||||
for (uint64_t bar : { 214 * MB, 256 * MB })
|
||||
{
|
||||
Memory m = NvidiaWindows(bar);
|
||||
Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 4));
|
||||
Expect("NVIDIA (Windows) without resizable BAR, guest memory", g, false, "import,copy-system:3,copy-cached:4,copy-vram:5!",
|
||||
"no resizable BAR");
|
||||
Expect("NVIDIA (Windows) without resizable BAR, ring", PlanRing(m, ~0u, CopyType(g)), true,
|
||||
"ring-cached:4,ring-system:3,ring-vram:5!");
|
||||
// The import refused (the driver won't take a file mapping's view):
|
||||
// the copy in system memory, the ring still in cached system memory.
|
||||
Expect("NVIDIA (Windows), ring beside a copy in type 3", PlanRing(m, ~0u, 3), true, "ring-cached:4,ring-system:3,ring-vram:5!");
|
||||
}
|
||||
{
|
||||
Memory m = NvidiaWindows(16 * GB);
|
||||
Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 4));
|
||||
Expect("NVIDIA (Windows) with resizable BAR, guest memory", g, false, "copy-vram:5,import,copy-system:3,copy-cached:4",
|
||||
"resizable BAR");
|
||||
Expect("NVIDIA (Windows) with resizable BAR, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:5,ring-cached:4,ring-system:3");
|
||||
}
|
||||
}
|
||||
|
||||
// The environment's overrides still lead, and the default order follows.
|
||||
static void EnvOverrides()
|
||||
{
|
||||
@@ -342,6 +411,7 @@ int main()
|
||||
NoRebar();
|
||||
OverPcie();
|
||||
Intel();
|
||||
Windows();
|
||||
EnvOverrides();
|
||||
TestHook();
|
||||
Types();
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
// The MSVC C runtime has _putenv_s, not setenv and unsetenv ("" removes).
|
||||
static int setenv(const char* name, const char* value, int) { return _putenv_s(name, value); }
|
||||
static int unsetenv(const char* name) { return _putenv_s(name, ""); }
|
||||
#endif
|
||||
|
||||
using namespace memstatus;
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Where the per-user folders go (runtime/user/paths.h), from the environment
|
||||
// alone: nothing is created or read. From the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime tests/paths_test.cpp -o build/paths_test
|
||||
// build/paths_test
|
||||
// (Windows: with runtime/platform/win32/folders.cpp, shell32 and ole32;
|
||||
// tests/CMakeLists.txt.) macOS and Linux: the HOME-based folders, and the
|
||||
// XDG_* overrides where they apply (Linux; not macOS). Windows: %LOCALAPPDATA%
|
||||
// \SpeedBreaker and its Cache folder whatever HOME says (Git Bash and MSYS2
|
||||
// set it), and the XDG_* overrides for test sandboxes.
|
||||
#include <user/paths.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <platform/folders.h>
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static void Set(const char* name, const char* value)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, value ? value : ""); // "" removes it
|
||||
#else
|
||||
if (value)
|
||||
setenv(name, value, 1);
|
||||
else
|
||||
unsetenv(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::string Show(const std::filesystem::path& p)
|
||||
{
|
||||
return p.string();
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
#ifdef _WIN32
|
||||
const fs::path home = "C:\\home-from-git-bash";
|
||||
const fs::path data = "C:\\sandbox\\data", cache = "C:\\sandbox\\cache";
|
||||
#else
|
||||
const fs::path home = "/tmp/paths-test-home";
|
||||
const fs::path data = "/tmp/paths-test-data", cache = "/tmp/paths-test-cache";
|
||||
#endif
|
||||
|
||||
printf("1. no XDG_* overrides, HOME=%s\n", home.string().c_str());
|
||||
Set("HOME", home.string().c_str());
|
||||
Set("XDG_DATA_HOME", nullptr);
|
||||
Set("XDG_CACHE_HOME", nullptr);
|
||||
printf(" data: %s\n cache: %s\n", Show(GetUserPath()).c_str(), Show(GetCachePath()).c_str());
|
||||
#ifdef _WIN32
|
||||
fs::path local = platform::GetKnownFolder(platform::KnownFolder::LocalAppData);
|
||||
CHECK(!local.empty() && local.is_absolute(), "LocalAppData: %s", local.string().c_str());
|
||||
if (const char* env = std::getenv("LOCALAPPDATA"))
|
||||
CHECK(fs::path(env) == local, "the shell's LocalAppData %s isn't %%LOCALAPPDATA%% %s", local.string().c_str(), env);
|
||||
CHECK(GetUserPath() == local / "SpeedBreaker", "data %s", Show(GetUserPath()).c_str());
|
||||
CHECK(GetCachePath() == local / "SpeedBreaker" / "Cache", "cache %s", Show(GetCachePath()).c_str());
|
||||
CHECK(GetUserPath().string().find("home-from-git-bash") == std::string::npos, "HOME was used");
|
||||
#elif defined(__APPLE__)
|
||||
CHECK(GetUserPath() == home / "Library/Application Support" / "SpeedBreaker", "data %s", Show(GetUserPath()).c_str());
|
||||
CHECK(GetCachePath() == home / "Library/Caches" / "SpeedBreaker", "cache %s", Show(GetCachePath()).c_str());
|
||||
#else
|
||||
CHECK(GetUserPath() == home / ".local/share" / "speedbreaker", "data %s", Show(GetUserPath()).c_str());
|
||||
CHECK(GetCachePath() == home / ".cache" / "speedbreaker", "cache %s", Show(GetCachePath()).c_str());
|
||||
#endif
|
||||
|
||||
printf("2. XDG_DATA_HOME=%s XDG_CACHE_HOME=%s\n", data.string().c_str(), cache.string().c_str());
|
||||
Set("XDG_DATA_HOME", data.string().c_str());
|
||||
Set("XDG_CACHE_HOME", cache.string().c_str());
|
||||
printf(" data: %s\n cache: %s\n", Show(GetUserPath()).c_str(), Show(GetCachePath()).c_str());
|
||||
#if defined(__APPLE__)
|
||||
// macOS ignores them (only iOS takes them); the HOME-based folders stay.
|
||||
CHECK(GetUserPath() == home / "Library/Application Support" / "SpeedBreaker", "data %s", Show(GetUserPath()).c_str());
|
||||
CHECK(GetCachePath() == home / "Library/Caches" / "SpeedBreaker", "cache %s", Show(GetCachePath()).c_str());
|
||||
#else
|
||||
CHECK(GetUserPath() == data / kUserFolderName, "data %s", Show(GetUserPath()).c_str());
|
||||
CHECK(GetCachePath() == cache / kUserFolderName, "cache %s", Show(GetCachePath()).c_str());
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
printf("3. XDG_DATA_HOME only: the cache goes inside it (a sandbox keeps everything)\n");
|
||||
Set("XDG_CACHE_HOME", nullptr);
|
||||
printf(" cache: %s\n", Show(GetCachePath()).c_str());
|
||||
CHECK(GetCachePath() == data / "SpeedBreaker" / "Cache", "cache %s", Show(GetCachePath()).c_str());
|
||||
#endif
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -42,7 +42,13 @@ static void Parsing()
|
||||
static void Folder(const std::filesystem::path& root)
|
||||
{
|
||||
// The slot (root port) and the GPU under it, as /sys/devices lays them out.
|
||||
// (Windows, where the test builds too: ':' can't be in a folder name, and
|
||||
// the reader doesn't look at the names.)
|
||||
#ifdef _WIN32
|
||||
std::filesystem::path port = root / "0000-00-01.1", gpu = port / "0000-01-00.0";
|
||||
#else
|
||||
std::filesystem::path port = root / "0000:00:01.1", gpu = port / "0000:01:00.0";
|
||||
#endif
|
||||
std::filesystem::create_directories(gpu);
|
||||
Put(port / "max_link_speed", "8.0 GT/s PCIe\n");
|
||||
Put(port / "max_link_width", "16\n");
|
||||
@@ -78,7 +84,7 @@ static void Folder(const std::filesystem::path& root)
|
||||
int main()
|
||||
{
|
||||
Parsing();
|
||||
std::filesystem::path root = std::filesystem::temp_directory_path() / ("pcie_link_test_" + std::to_string(std::rand()) + std::to_string(long(&g_failures) & 0xFFFF));
|
||||
std::filesystem::path root = std::filesystem::temp_directory_path() / ("pcie_link_test_" + std::to_string(std::rand()) + std::to_string(uintptr_t(&g_failures) & 0xFFFF));
|
||||
std::filesystem::remove_all(root);
|
||||
Folder(root);
|
||||
std::filesystem::remove_all(root);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// video::ExtraRefreshes (runtime/video/repeats.h): the [perf] line's
|
||||
// "repeated" as the Windows presenter counts it, in the panel's own
|
||||
// refreshes. Header only; built by tests/CMakeLists.txt (-DSB_TESTS=ON).
|
||||
//
|
||||
// 1. Single frames at 60, 120, 144, 165 and 240 Hz, under G-SYNC, at Frame
|
||||
// Rate 30, a pause, an unknown rate.
|
||||
// 2. A FIFO display model: a steady 60 fps (frames presented 3 ms after each
|
||||
// vblank, with jitter) shown at the first refresh after each present. Two
|
||||
// seconds (120 frames, one [perf] window) at 144 Hz count the ~48 frames
|
||||
// shown for 3 refreshes; at 60 Hz and under steady G-SYNC, none. The
|
||||
// count as it was before, against 1/60 s, sees nothing at 144 Hz: why it
|
||||
// changed.
|
||||
#include <video/repeats.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
using video::ExtraRefreshes;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double kVblank = 1e9 / 60.0;
|
||||
|
||||
double Period(double hz)
|
||||
{
|
||||
return 1e9 / hz;
|
||||
}
|
||||
|
||||
// The count before this change (presenter.cpp's OnScreen without a
|
||||
// valid fit): whole periods of 1/60 s past the vblanks.
|
||||
int64_t OldCount(double shownNs, int64_t vblanks)
|
||||
{
|
||||
int64_t shownFor = std::llround(shownNs / (1e9 / 60.0));
|
||||
return shownFor > vblanks && shownFor <= 30 ? shownFor - vblanks : 0;
|
||||
}
|
||||
|
||||
struct Window
|
||||
{
|
||||
int64_t extra = 0, old = 0;
|
||||
std::vector<int> refreshes; // each gap in whole refreshes
|
||||
};
|
||||
|
||||
// 120 game frames at 60 fps, each presented `delayNs` (+ jitter) after
|
||||
// its vblank and shown at the first refresh of a `hz` grid after that
|
||||
// (hz 0: variable refresh, shown as presented, 0.5 ms later).
|
||||
Window Simulate(double hz, double delayNs, double jitterNs, uint32_t seed)
|
||||
{
|
||||
std::mt19937 rng(seed);
|
||||
std::normal_distribution<double> jitter(0.0, jitterNs);
|
||||
const double period = hz > 0 ? Period(hz) : Period(144.0); // the rate SDL reports under G-SYNC
|
||||
const double phase = 0.37 * period;
|
||||
Window w;
|
||||
double last = -1;
|
||||
for (int k = 0; k < 121; k++)
|
||||
{
|
||||
double presented = k * kVblank + delayNs + (jitterNs > 0 ? std::max(-delayNs + 0.1e6, jitter(rng)) : 0.0);
|
||||
double shown = hz > 0 ? phase + std::ceil((presented - phase) / period) * period : presented + 0.5e6;
|
||||
if (last >= 0)
|
||||
{
|
||||
double gap = shown - last;
|
||||
w.extra += ExtraRefreshes(gap, 1, kVblank, period);
|
||||
w.old += OldCount(gap, 1);
|
||||
w.refreshes.push_back(int(std::lround(gap / period)));
|
||||
}
|
||||
last = shown;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("1. single frames\n");
|
||||
// 60 Hz: one refresh is the frame's own; a second one is a repeat.
|
||||
CHECK(ExtraRefreshes(Period(60), 1, kVblank, Period(60)) == 0, "60 Hz, 1 refresh");
|
||||
CHECK(ExtraRefreshes(2 * Period(60), 1, kVblank, Period(60)) == 1, "60 Hz, 2 refreshes");
|
||||
CHECK(ExtraRefreshes(3 * Period(60), 1, kVblank, Period(60)) == 2, "60 Hz, 3 refreshes");
|
||||
// A 59.94 Hz panel against the free-running 60 Hz vblank: the same.
|
||||
CHECK(ExtraRefreshes(Period(59.94), 1, kVblank, Period(59.94)) == 0, "59.94 Hz, 1 refresh");
|
||||
CHECK(ExtraRefreshes(2 * Period(59.94), 1, kVblank, Period(59.94)) == 1, "59.94 Hz, 2 refreshes");
|
||||
// 120 Hz: 2 refreshes are a frame's own; the 3 of a 1-and-3 pair counts.
|
||||
CHECK(ExtraRefreshes(2 * Period(120), 1, kVblank, Period(120)) == 0, "120 Hz, 2");
|
||||
CHECK(ExtraRefreshes(1 * Period(120), 1, kVblank, Period(120)) == 0, "120 Hz, 1");
|
||||
CHECK(ExtraRefreshes(3 * Period(120), 1, kVblank, Period(120)) == 1, "120 Hz, 3");
|
||||
// 144 Hz: 2 or 3 refreshes (13.9 / 20.8 ms); the 3s count.
|
||||
CHECK(ExtraRefreshes(2 * Period(144), 1, kVblank, Period(144)) == 0, "144 Hz, 2");
|
||||
CHECK(ExtraRefreshes(3 * Period(144), 1, kVblank, Period(144)) == 1, "144 Hz, 3");
|
||||
CHECK(ExtraRefreshes(4 * Period(144), 1, kVblank, Period(144)) == 2, "144 Hz, 4");
|
||||
CHECK(OldCount(3 * Period(144), 1) == 0, "the old count saw a 3 at 144 Hz");
|
||||
// 165 Hz: a 3 is a quarter refresh long, not counted; a 4 is.
|
||||
CHECK(ExtraRefreshes(3 * Period(165), 1, kVblank, Period(165)) == 0, "165 Hz, 3");
|
||||
CHECK(ExtraRefreshes(4 * Period(165), 1, kVblank, Period(165)) == 1, "165 Hz, 4");
|
||||
// 240 Hz: 4 is the frame's own; 5 counts.
|
||||
CHECK(ExtraRefreshes(4 * Period(240), 1, kVblank, Period(240)) == 0, "240 Hz, 4");
|
||||
CHECK(ExtraRefreshes(5 * Period(240), 1, kVblank, Period(240)) == 1, "240 Hz, 5");
|
||||
// G-SYNC: SDL reports the panel's top rate; a frame ~1 ms late is not a
|
||||
// repeat, one half a refresh late is.
|
||||
CHECK(ExtraRefreshes(kVblank + 1e6, 1, kVblank, Period(144)) == 0, "VRR, 1 ms late");
|
||||
CHECK(ExtraRefreshes(kVblank + 3.6e6, 1, kVblank, Period(144)) == 1, "VRR, 3.6 ms late");
|
||||
// Frame Rate 30: two vblanks a frame.
|
||||
CHECK(ExtraRefreshes(2 * kVblank, 2, kVblank, Period(60)) == 0, "Frame Rate 30, 2 refreshes");
|
||||
CHECK(ExtraRefreshes(3 * kVblank, 2, kVblank, Period(60)) == 1, "Frame Rate 30, 3 refreshes");
|
||||
CHECK(ExtraRefreshes(5 * Period(144), 2, kVblank, Period(144)) == 0, "Frame Rate 30 at 144 Hz, 5");
|
||||
CHECK(ExtraRefreshes(6 * Period(144), 2, kVblank, Period(144)) == 1, "Frame Rate 30 at 144 Hz, 6");
|
||||
// A pause, an unknown rate, a frame shown early.
|
||||
CHECK(ExtraRefreshes(600e6, 1, kVblank, Period(60)) == 0, "a pause");
|
||||
CHECK(ExtraRefreshes(3 * kVblank, 1, kVblank, 0.0) == 0, "unknown rate");
|
||||
CHECK(ExtraRefreshes(0.5 * kVblank, 1, kVblank, Period(60)) == 0, "a short frame");
|
||||
|
||||
printf("2. two seconds of 60 fps (one [perf] window)\n");
|
||||
{
|
||||
Window w = Simulate(144.0, 3e6, 0.0, 1);
|
||||
int threes = 0;
|
||||
for (int r : w.refreshes)
|
||||
threes += r == 3;
|
||||
printf(" 144 Hz: %lld repeated (%d of %zu frames shown for 3 refreshes); before: %lld\n", (long long)w.extra, threes,
|
||||
w.refreshes.size(), (long long)w.old);
|
||||
CHECK(w.extra == threes && threes >= 45 && threes <= 51, "144 Hz: %lld, %d threes", (long long)w.extra, threes);
|
||||
CHECK(w.old == 0, "the old count at 144 Hz: %lld", (long long)w.old);
|
||||
}
|
||||
{
|
||||
Window w = Simulate(144.0, 7e6, 1.5e6, 2);
|
||||
printf(" 144 Hz, jittery delivery: %lld repeated\n", (long long)w.extra);
|
||||
CHECK(w.extra >= 40 && w.extra <= 60, "144 Hz jittery: %lld", (long long)w.extra);
|
||||
}
|
||||
{
|
||||
Window w = Simulate(60.0, 3e6, 0.0, 3);
|
||||
printf(" 60 Hz: %lld repeated (before: %lld)\n", (long long)w.extra, (long long)w.old);
|
||||
CHECK(w.extra == 0 && w.old == 0, "60 Hz: %lld / %lld", (long long)w.extra, (long long)w.old);
|
||||
}
|
||||
{
|
||||
Window w = Simulate(120.0, 3e6, 0.0, 4);
|
||||
printf(" 120 Hz, presents clear of the refresh: %lld repeated\n", (long long)w.extra);
|
||||
CHECK(w.extra == 0, "120 Hz steady: %lld", (long long)w.extra);
|
||||
}
|
||||
{
|
||||
// Presents right at a refresh: jitter splits them over two, 1-and-3.
|
||||
Window w = Simulate(120.0, 0.37 * Period(120.0) + 2 * Period(120.0), 0.4e6, 5);
|
||||
int threes = 0;
|
||||
for (int r : w.refreshes)
|
||||
threes += r == 3;
|
||||
printf(" 120 Hz, presents at a refresh: %lld repeated (%d frames shown for 3)\n", (long long)w.extra, threes);
|
||||
CHECK(w.extra == threes && threes > 10, "120 Hz at the latch: %lld, %d", (long long)w.extra, threes);
|
||||
}
|
||||
{
|
||||
// Under G-SYNC only delivery jitter reaches the screen: a steady
|
||||
// stream counts nothing, a jittery one only its late frames (half a
|
||||
// 144 Hz refresh, 3.5 ms), far from the fixed 144 Hz's ~48.
|
||||
Window steady = Simulate(0.0, 7e6, 0.5e6, 6), jittery = Simulate(0.0, 7e6, 1.0e6, 6);
|
||||
printf(" G-SYNC (144 Hz top rate): %lld repeated with 0.5 ms of jitter, %lld with 1 ms\n", (long long)steady.extra,
|
||||
(long long)jittery.extra);
|
||||
CHECK(steady.extra == 0, "VRR, steady: %lld", (long long)steady.extra);
|
||||
CHECK(jittery.extra < 10, "VRR, jittery: %lld", (long long)jittery.extra);
|
||||
}
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// report::detail::ReplaceFolders (runtime/report/scrub.h), what ScrubHome
|
||||
// does to the text of a bug report before it leaves the machine. Header
|
||||
// only. From the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime tests/scrub_test.cpp -o build/scrub_test
|
||||
// build/scrub_test
|
||||
// - POSIX (HOME as "~"): whole folders only, as since 2026-09-28: not
|
||||
// /home/deck2, not the tail of /mnt/home/deck; the longest folder first
|
||||
// (HOME and its real path).
|
||||
// - Windows (the user's folder as %USERPROFILE%): the same rule, letters in
|
||||
// either case, \ and / alike, a backslash escaped (\\) too, the 8.3 name
|
||||
// as a folder of its own; a folder never matches inside a longer name.
|
||||
#include <report/scrub.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
using report::detail::FolderName;
|
||||
using report::detail::ReplaceFolders;
|
||||
|
||||
static void Expect(const std::string& text, const std::vector<FolderName>& folders, bool windows, const std::string& want)
|
||||
{
|
||||
std::string got = ReplaceFolders(text, folders, windows);
|
||||
CHECK(got == want, "\n in: %s\n got: %s\n want: %s", text.c_str(), got.c_str(), want.c_str());
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("1. POSIX: HOME as ~, whole folders only\n");
|
||||
const std::vector<FolderName> deck = { { "/home/deck", "~" } };
|
||||
Expect("[paths] data /home/deck/.local/share/speedbreaker", deck, false, "[paths] data ~/.local/share/speedbreaker");
|
||||
Expect("/home/deck", deck, false, "~");
|
||||
Expect("cd /home/deck; ls /home/deck2 /mnt/home/deck /home/deck-old", deck, false,
|
||||
"cd ~; ls /home/deck2 /mnt/home/deck /home/deck-old");
|
||||
Expect("/HOME/DECK/x", deck, false, "/HOME/DECK/x"); // byte for byte
|
||||
// HOME through a symlink and its real path: the longer one first.
|
||||
const std::vector<FolderName> both = { { "/home/deck", "~" }, { "/var/home/deck", "~" } };
|
||||
Expect("/var/home/deck/x and /home/deck/y", both, false, "~/x and ~/y");
|
||||
const std::vector<FolderName> mac = { { "/Users/player", "~" } };
|
||||
Expect("'/Users/player/Library/Application Support/SpeedBreaker' \"/Users/player\"", mac, false,
|
||||
"'~/Library/Application Support/SpeedBreaker' \"~\"");
|
||||
|
||||
printf("2. Windows: the user's folder as %%USERPROFILE%%\n");
|
||||
const std::vector<FolderName> user = { { "C:\\Users\\Jose", "%USERPROFILE%" } };
|
||||
Expect("[log] session 1: C:\\Users\\Jose\\AppData\\Local\\SpeedBreaker\\logs\\a.log", user, true,
|
||||
"[log] session 1: %USERPROFILE%\\AppData\\Local\\SpeedBreaker\\logs\\a.log");
|
||||
Expect("c:\\users\\jose\\OneDrive\\Desktop\\x.zip", user, true, "%USERPROFILE%\\OneDrive\\Desktop\\x.zip");
|
||||
Expect("C:/Users/Jose/AppData/Local", user, true, "%USERPROFILE%/AppData/Local");
|
||||
Expect("path = \"C:\\\\Users\\\\Jose\\\\AppData\"", user, true, "path = \"%USERPROFILE%\\\\AppData\"");
|
||||
Expect("C:\\Users\\Jose", user, true, "%USERPROFILE%");
|
||||
Expect("\\\\?\\C:\\Users\\Jose\\x", user, true, "\\\\?\\%USERPROFILE%\\x");
|
||||
Expect("C:\\Users\\Josefina\\x C:\\Users\\Jose-2 D:\\C:\\Users\\Jose", user, true,
|
||||
"C:\\Users\\Josefina\\x C:\\Users\\Jose-2 D:\\%USERPROFILE%");
|
||||
Expect("XC:\\Users\\Jose\\x", user, true, "XC:\\Users\\Jose\\x"); // the tail of a longer name
|
||||
Expect("C:\\Users\\Jos", user, true, "C:\\Users\\Jos");
|
||||
Expect("C:\\Users\\Jose\\x and C:\\Users\\Jose\\y", user, true, "%USERPROFILE%\\x and %USERPROFILE%\\y");
|
||||
// The 8.3 name next to the long one.
|
||||
const std::vector<FolderName> shortToo = { { "C:\\Users\\Jose Maria", "%USERPROFILE%" }, { "C:\\Users\\JOSEMA~1", "%USERPROFILE%" } };
|
||||
Expect("TEMP=C:\\Users\\JOSEMA~1\\AppData\\Local\\Temp; data C:\\Users\\Jose Maria\\AppData", shortToo, true,
|
||||
"TEMP=%USERPROFILE%\\AppData\\Local\\Temp; data %USERPROFILE%\\AppData");
|
||||
// UTF-8 names are bytes like any other.
|
||||
const std::vector<FolderName> utf8 = { { "C:\\Users\\Jos\xC3\xA9", "%USERPROFILE%" } };
|
||||
Expect("C:\\Users\\Jos\xC3\xA9\\Desktop", utf8, true, "%USERPROFILE%\\Desktop");
|
||||
// Nothing to replace, an empty folder: the text as it was.
|
||||
Expect("no paths here", user, true, "no paths here");
|
||||
Expect("C:\\Users\\Jose\\x", { { "", "%USERPROFILE%" } }, true, "C:\\Users\\Jose\\x");
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -18,7 +18,11 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h> // _getpid
|
||||
#else
|
||||
#include <unistd.h> // mkdtemp
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
@@ -113,6 +117,17 @@ int main()
|
||||
CHECK(EnvOverride(Id::KeyA) == nullptr, "nothing overrides a key");
|
||||
|
||||
// A scratch folder of our own.
|
||||
#ifdef _WIN32
|
||||
// Windows has no mkdtemp: a folder named after this process, made new.
|
||||
const std::filesystem::path dir = std::filesystem::temp_directory_path() / ("speedbreaker-keys-" + std::to_string(_getpid()));
|
||||
std::error_code scratchError;
|
||||
std::filesystem::remove_all(dir, scratchError);
|
||||
if (!std::filesystem::create_directory(dir, scratchError))
|
||||
{
|
||||
printf("can't make a scratch folder: %s\n", scratchError.message().c_str());
|
||||
return 2;
|
||||
}
|
||||
#else
|
||||
std::string pattern = (std::filesystem::temp_directory_path() / "speedbreaker-keys-XXXXXX").string();
|
||||
if (!mkdtemp(pattern.data()))
|
||||
{
|
||||
@@ -120,6 +135,7 @@ int main()
|
||||
return 2;
|
||||
}
|
||||
const std::filesystem::path dir = pattern;
|
||||
#endif
|
||||
const std::filesystem::path file = dir / "settings.toml";
|
||||
|
||||
// The file: names in, in any case; "none" empties a key; anything else
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
// The MSVC C runtime has _putenv_s, not setenv and unsetenv ("" removes).
|
||||
static int setenv(const char* name, const char* value, int) { return _putenv_s(name, value); }
|
||||
static int unsetenv(const char* name) { return _putenv_s(name, ""); }
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
int s_failures = 0;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Tests for settings::SetDeviceDefault (runtime/user/settings.cpp): a
|
||||
// device's own default before Load (an iPhone's Frame Rate) and after it
|
||||
// (a GPU's, known once its Vulkan device exists: Windows on an integrated
|
||||
// GPU). From the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime -isystem tools/XenonRecomp/thirdparty/tomlplusplus/include
|
||||
// tests/settings_test.cpp runtime/user/settings.cpp runtime/platform/posix/file_io.cpp -o build/settings_test
|
||||
// build/settings_test <scratch folder>
|
||||
// The scratch folder's settings.toml is written and read; nothing else.
|
||||
#include <user/settings.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator> // std::istreambuf_iterator
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using settings::Id;
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
namespace
|
||||
{
|
||||
void SetEnv(const char* name, const char* value)
|
||||
{
|
||||
if (!name)
|
||||
return; // an option without a variable (v0.1.5's Key options): the C runtime's _putenv_s fails fast on null
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, value ? value : ""); // "" removes it
|
||||
#else
|
||||
if (value)
|
||||
setenv(name, value, 1);
|
||||
else
|
||||
unsetenv(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WriteFile(const std::filesystem::path& path, const std::string& text)
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out << text;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::filesystem::path dir = argc > 1 ? argv[1] : "settings_test_out";
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(dir, ec);
|
||||
std::filesystem::create_directories(dir);
|
||||
const std::filesystem::path file = dir / "settings.toml";
|
||||
|
||||
// The options used here come from the file and these defaults only.
|
||||
for (const settings::Option& o : settings::Options())
|
||||
SetEnv(o.env, nullptr);
|
||||
SetEnv("NFSMW_TEXTURE_MIPS", "1"); // Mipmaps: an environment override
|
||||
|
||||
// Before Load, as on an iPhone: what a file without the option gives.
|
||||
settings::SetDeviceDefault(Id::FrameRate, 30, "test: before Load");
|
||||
WriteFile(file,
|
||||
"[video]\n"
|
||||
"anti_aliasing = true\n"
|
||||
"[audio]\n"
|
||||
"master_volume = 50\n");
|
||||
CHECK(settings::Load(file), "the file loads");
|
||||
CHECK(settings::GetInt(Id::FrameRate) == 30, "frame rate %d", settings::GetInt(Id::FrameRate));
|
||||
CHECK(settings::IsDefault(Id::FrameRate), "30 is the default");
|
||||
CHECK(settings::GetInt(Id::MasterVolume) == 50, "the file's volume");
|
||||
CHECK(!settings::Dirty(), "nothing to save after a load");
|
||||
|
||||
std::vector<Id> told;
|
||||
uint32_t listener = settings::Subscribe([&told](Id id) { told.push_back(id); });
|
||||
uint64_t generation = settings::Generation();
|
||||
|
||||
// After Load, an option the file holds: its value stays.
|
||||
settings::SetDeviceDefault(Id::AntiAliasing, 0, "test: in the file");
|
||||
CHECK(settings::GetBool(Id::AntiAliasing), "the file's anti-aliasing stays on");
|
||||
CHECK(settings::GetStoredValue(Id::AntiAliasing) == 1, "stored on");
|
||||
CHECK(settings::DefaultValue(Id::AntiAliasing) == 0, "but the default is off");
|
||||
CHECK(!settings::IsDefault(Id::AntiAliasing), "on isn't the default now");
|
||||
|
||||
// After Load, a live option the file doesn't hold: the new default now.
|
||||
settings::SetDeviceDefault(Id::Anisotropy, 2, "test: not in the file");
|
||||
CHECK(settings::GetInt(Id::Anisotropy) == 2, "anisotropy %d", settings::GetInt(Id::Anisotropy));
|
||||
CHECK(settings::GetStoredValue(Id::Anisotropy) == 2, "stored 2x");
|
||||
CHECK(settings::IsDefault(Id::Anisotropy), "2x is the default");
|
||||
CHECK(settings::Reset(Id::Anisotropy) == settings::SetResult::Unchanged, "a reset keeps 2x");
|
||||
CHECK(settings::Generation() > generation, "the generation moved");
|
||||
CHECK(told.size() == 1 && told[0] == Id::Anisotropy, "listeners told once, of anisotropy (%zu calls)", told.size());
|
||||
CHECK(!settings::Dirty(), "a device default isn't a change to save");
|
||||
|
||||
// A Restart option keeps this run's value.
|
||||
settings::SetDeviceDefault(Id::WindowWidth, 1600, "test: restart option");
|
||||
CHECK(settings::GetInt(Id::WindowWidth) == 1280, "window width %d", settings::GetInt(Id::WindowWidth));
|
||||
CHECK(settings::GetStoredValue(Id::WindowWidth) == 1280, "stored 1280");
|
||||
CHECK(settings::DefaultValue(Id::WindowWidth) == 1600, "the default is 1600");
|
||||
|
||||
// Changed since the start (the menu): that value stays.
|
||||
settings::Set(Id::Sharpening, 0.5);
|
||||
settings::SetDeviceDefault(Id::Sharpening, 0.1, "test: set since");
|
||||
CHECK(settings::GetFloat(Id::Sharpening) == 0.5f, "sharpening %.2f", settings::GetFloat(Id::Sharpening));
|
||||
|
||||
// An environment override still wins for the run; the stored value moves.
|
||||
settings::SetDeviceDefault(Id::Mipmaps, 0, "test: environment");
|
||||
CHECK(settings::GetBool(Id::Mipmaps), "NFSMW_TEXTURE_MIPS=1 wins");
|
||||
CHECK(settings::GetStoredValue(Id::Mipmaps) == 0, "stored off");
|
||||
CHECK(settings::EnvOverride(Id::Mipmaps) != nullptr, "the override is reported");
|
||||
|
||||
// After a save the file holds every option: a later default changes nothing.
|
||||
told.clear();
|
||||
CHECK(settings::Save(), "saved");
|
||||
settings::SetDeviceDefault(Id::Vibration, 0, "test: after a save");
|
||||
CHECK(settings::GetBool(Id::Vibration), "the saved vibration stays on");
|
||||
CHECK(told.empty(), "no listener calls");
|
||||
|
||||
// The saved file gives the values back, the device's 2x among them.
|
||||
CHECK(settings::Load(file), "the saved file loads");
|
||||
CHECK(settings::GetInt(Id::Anisotropy) == 2, "anisotropy %d after a reload", settings::GetInt(Id::Anisotropy));
|
||||
CHECK(settings::GetBool(Id::AntiAliasing), "anti-aliasing on after a reload");
|
||||
CHECK(settings::GetFloat(Id::Sharpening) == 0.5f, "sharpening %.2f after a reload", settings::GetFloat(Id::Sharpening));
|
||||
{
|
||||
std::ifstream in(file);
|
||||
std::string text((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
CHECK(text.find("anisotropic_filtering = \"2x\"") != std::string::npos, "the file holds 2x");
|
||||
CHECK(text.find("mipmaps = false") != std::string::npos, "the file holds the stored mipmaps, not the override");
|
||||
}
|
||||
|
||||
settings::Unsubscribe(listener);
|
||||
std::filesystem::remove_all(dir, ec);
|
||||
printf("%s (%d failure%s)\n", g_failures ? "FAILED" : "all passed", g_failures, g_failures == 1 ? "" : "s");
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// platform::PrepareProcess (runtime/platform/startup.h), Windows only: the
|
||||
// settings it makes for the whole process. Built and run by
|
||||
// tests/CMakeLists.txt on Windows.
|
||||
// - Drive errors come back to the caller as errors, with no system dialog
|
||||
// (SEM_FAILCRITICALERRORS).
|
||||
// - A C runtime call with an invalid argument fails with errno set, as on
|
||||
// POSIX, and the process goes on. Without the handler the C runtime ends
|
||||
// the process on the spot (__fastfail, exit code 0xC0000409), and this
|
||||
// test never prints its result.
|
||||
// - Standard streams without handles (a Windows-subsystem build started
|
||||
// from Explorer: stdin, stdout and stderr are placeholders, fileno -2)
|
||||
// come back on their own numbers, 0, 1 and 2, so fprintf(stderr) reaches
|
||||
// whatever fd 2 becomes, as the session log's tee makes it its pipe. A
|
||||
// child of this test started with no standard handles and no console
|
||||
// checks it, twice: started from here (it may attach this test's console,
|
||||
// when it has one), and through a relay without a console (NUL). Without
|
||||
// the reopen the child's line never reaches the pipe.
|
||||
// (The console's code page needs a real console: the work notes' script
|
||||
// p2_console.py runs SpeedBreaker in one and reads what it shows.)
|
||||
#include <platform/startup.h>
|
||||
#include <platform/os.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <string>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Starts this test again with `args`, no standard handles (none, not the
|
||||
// parent's) and no console of its own; inheritable handles (the pipe)
|
||||
// go along. Waits for it: its exit code, or -1.
|
||||
int StartBare(const std::string& args)
|
||||
{
|
||||
wchar_t exe[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring command = L"\"" + std::wstring(exe) + L"\" " + std::wstring(args.begin(), args.end());
|
||||
STARTUPINFOW si{ sizeof(si) };
|
||||
si.dwFlags = STARTF_USESTDHANDLES; // all three null
|
||||
PROCESS_INFORMATION pi{};
|
||||
if (!CreateProcessW(exe, command.data(), nullptr, nullptr, TRUE, DETACHED_PROCESS, nullptr, nullptr, &si, &pi))
|
||||
return -1;
|
||||
CloseHandle(pi.hThread);
|
||||
DWORD code = DWORD(-1);
|
||||
WaitForSingleObject(pi.hProcess, 60000);
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hProcess);
|
||||
return int(code);
|
||||
}
|
||||
|
||||
void Send(HANDLE pipe, const std::string& text)
|
||||
{
|
||||
DWORD written = 0;
|
||||
WriteFile(pipe, text.data(), DWORD(text.size()), &written, nullptr);
|
||||
}
|
||||
|
||||
// The bare child: what it was given, PrepareProcess, then the log's tee
|
||||
// in miniature (fd 2 becomes the pipe) and a line through stderr.
|
||||
int StreamsChild(HANDLE pipe)
|
||||
{
|
||||
int before[3] = { _fileno(stdin), _fileno(stdout), _fileno(stderr) };
|
||||
platform::PrepareProcess();
|
||||
int after[3] = { _fileno(stdin), _fileno(stdout), _fileno(stderr) };
|
||||
char line[512];
|
||||
snprintf(line, sizeof(line), "filenos before %d %d %d, after %d %d %d; note: %s\n", before[0], before[1], before[2], after[0],
|
||||
after[1], after[2], platform::StandardStreamsNote());
|
||||
Send(pipe, line);
|
||||
int fd = _open_osfhandle(reinterpret_cast<intptr_t>(pipe), _O_WRONLY | _O_BINARY);
|
||||
if (fd < 0 || _dup2(fd, 2) != 0)
|
||||
{
|
||||
Send(pipe, "the pipe couldn't become fd 2\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "stderr reached the pipe\n");
|
||||
fflush(stderr);
|
||||
return before[2] == -2 && after[0] == 0 && after[1] == 1 && after[2] == 2 ? 0 : 1;
|
||||
}
|
||||
|
||||
// Starts the bare child, through a relay without a console when asked;
|
||||
// what it wrote, and its exit code.
|
||||
std::string RunChild(bool relay, int& code)
|
||||
{
|
||||
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE };
|
||||
HANDLE read = nullptr, write = nullptr;
|
||||
if (!CreatePipe(&read, &write, &sa, 0))
|
||||
return "CreatePipe failed";
|
||||
SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0);
|
||||
char args[64];
|
||||
snprintf(args, sizeof(args), "%s %llu", relay ? "--relay" : "--streams-child",
|
||||
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(write)));
|
||||
code = StartBare(args);
|
||||
CloseHandle(write);
|
||||
std::string text;
|
||||
char buffer[512];
|
||||
DWORD got = 0;
|
||||
while (ReadFile(read, buffer, sizeof(buffer), &got, nullptr) && got > 0)
|
||||
text.append(buffer, got);
|
||||
CloseHandle(read);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc == 3 && (strcmp(argv[1], "--streams-child") == 0 || strcmp(argv[1], "--relay") == 0))
|
||||
{
|
||||
HANDLE pipe = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(std::strtoull(argv[2], nullptr, 10)));
|
||||
if (strcmp(argv[1], "--streams-child") == 0)
|
||||
return StreamsChild(pipe);
|
||||
// The relay has no console, so the child can't attach one.
|
||||
return StartBare(std::string("--streams-child ") + argv[2]);
|
||||
}
|
||||
|
||||
platform::PrepareProcess();
|
||||
|
||||
printf("1. drive errors: no dialog\n");
|
||||
CHECK(GetErrorMode() & SEM_FAILCRITICALERRORS, "error mode 0x%X", GetErrorMode());
|
||||
|
||||
printf("2. C runtime calls with an invalid argument fail, as on POSIX\n");
|
||||
fflush(stdout);
|
||||
errno = 0;
|
||||
int closed = _close(-1);
|
||||
CHECK(closed == -1 && errno == EBADF, "close(-1): %d, errno %d", closed, errno);
|
||||
char byte = 0;
|
||||
errno = 0;
|
||||
int got = _read(4000, &byte, 1);
|
||||
CHECK(got == -1 && errno == EBADF, "read(4000): %d, errno %d", got, errno);
|
||||
|
||||
printf("3. standard streams without handles come back on 0, 1 and 2\n");
|
||||
for (bool relay : { false, true })
|
||||
{
|
||||
int code = -1;
|
||||
std::string text = RunChild(relay, code);
|
||||
printf(" %s: %s", relay ? "through a relay without a console" : "started from here", text.c_str());
|
||||
CHECK(code == 0, "the child's exit code %d", code);
|
||||
CHECK(text.find("stderr reached the pipe") != std::string::npos, "no line through stderr");
|
||||
if (relay)
|
||||
CHECK(text.find("note: no console (") != std::string::npos, "the relay's child found a console");
|
||||
}
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// report::detail::GuestName (runtime/report/symbol_names.h): which symbol
|
||||
// names are recompiled functions, and the sub_<address> each is shown as in
|
||||
// crash and hang reports. Header only. From the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime tests/symbol_names_test.cpp -o build/symbol_names_test
|
||||
// build/symbol_names_test
|
||||
// - The ELF and Mach-O names (Linux's .symtab, macOS's dladdr) give what
|
||||
// they gave before the Windows forms were added, byte for byte (the body's
|
||||
// name past "__imp__" whatever follows it, as a compiler's .cold split).
|
||||
// - The PDB's names as DbgHelp gives them on Windows (the ThinkPad, 92306bf:
|
||||
// "_imp__sub_825987E0", "weak.?sub_82441190YAXAEIAUPPCContextPEAEZ.default
|
||||
// .__xmm", a hook's "sub_82441190") are recompiled functions too.
|
||||
// - Nothing else is: runtime functions, near misses (7 or 9 digits,
|
||||
// lowercase hex), names that merely contain "sub_".
|
||||
#include <report/symbol_names.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static void Expect(const char* name, const char* want)
|
||||
{
|
||||
char buffer[13];
|
||||
memset(buffer, 'x', sizeof(buffer));
|
||||
const char* got = report::detail::GuestName(name, buffer);
|
||||
if (!want)
|
||||
CHECK(got == nullptr, "%s -> %s, expected not a recompiled function", name, got ? got : "(null)");
|
||||
else
|
||||
CHECK(got && strcmp(got, want) == 0, "%s -> %s, expected %s", name, got ? got : "(null)", want);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("1. ELF and Mach-O names, as before\n");
|
||||
Expect("__imp__sub_825987E0", "sub_825987E0");
|
||||
Expect("__imp__sub_825987E0.cold", "sub_825987E0.cold"); // as before: everything past __imp__
|
||||
Expect("_Z12sub_82441190R10PPCContextPh", "sub_82441190");
|
||||
Expect("_Z12sub_82441190R10PPCContextS_", nullptr); // another signature: not one
|
||||
Expect("_ZN6report9TestCrashEv", nullptr);
|
||||
Expect("main", nullptr);
|
||||
|
||||
printf("2. A PDB's names through DbgHelp (Windows)\n");
|
||||
Expect("_imp__sub_825987E0", "sub_825987E0"); // a body's public symbol
|
||||
Expect("sub_82441190", "sub_82441190"); // a hook, or a wrapper's public symbol undecorated
|
||||
Expect("?sub_82441190@@YAXAEIAUPPCContext@@PEAE@Z", "sub_82441190"); // decorated
|
||||
Expect("weak.?sub_82441190YAXAEIAUPPCContextPEAEZ.default.__xmm", "sub_82441190"); // lld-link's weak alias
|
||||
Expect(".weak.?sub_82441190@@YAXAEIAUPPCContext@@PEAE@Z.default.__xmm@00000000000000500000000000000090", "sub_82441190");
|
||||
|
||||
printf("3. Not recompiled functions\n");
|
||||
Expect("report::TestCrash", nullptr);
|
||||
Expect("report::`anonymous namespace'::Watchdog", nullptr);
|
||||
Expect("sub_8244119", nullptr); // 7 digits
|
||||
Expect("sub_824411900", nullptr); // 9
|
||||
Expect("sub_82441l90", nullptr); // not hex
|
||||
Expect("sub_825987e0", nullptr); // XenonRecomp's are uppercase
|
||||
Expect("_imp__sub_8259", nullptr);
|
||||
Expect("?sub_8244@@YAXXZ", nullptr);
|
||||
Expect("RtlUserThreadStart", nullptr);
|
||||
Expect("my_sub_82441190", nullptr);
|
||||
Expect("", nullptr);
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
// platform::stack (runtime/platform/stack.h), Windows only: the stacks and
|
||||
// names crash and hang reports print. Built and run by tests/CMakeLists.txt
|
||||
// on Windows, with this file's debug information in win_stack_test.pdb and
|
||||
// tests/win_stack_test_guest.cpp built without any, as the recompiled code is.
|
||||
// 1. Before LoadSymbols nothing is named or walked (a crash that early gets
|
||||
// the generated table's names and RtlVirtualUnwind's frames instead).
|
||||
// 2. LoadSymbols finds win_stack_test.pdb next to the .exe.
|
||||
// 3. A stack from this thread's registers, through the "recompiled"
|
||||
// functions: each frame named, the recompiled ones (public symbols only)
|
||||
// as sub_<address> through report::detail::GuestName, a body with a
|
||||
// name of its own (__imp___xstart) exactly as it is.
|
||||
// 4. A stack from an exception's registers (what the unhandled-exception
|
||||
// filter gets): the faulting function first, then its callers.
|
||||
// 5. Another thread, blocked in a wait: Capture suspends it, copies its
|
||||
// registers and stack, and resumes it; the walk names where it waits.
|
||||
// The thread then runs on and overwrites that part of its stack, and the
|
||||
// old copy still walks to the old place (the walk reads the copy, not the
|
||||
// live stack); a new Capture finds the new place; the thread ends
|
||||
// normally (it was resumed every time).
|
||||
// 6. A DLL DbgHelp hadn't seen is loaded for its name (its exports).
|
||||
// Unwind (no DbgHelp: RtlVirtualUnwind over the copy) finds the same
|
||||
// frames as Walk, from the old copy too.
|
||||
// 7. EnterCrash: DbgHelp kept by this thread; names and walks go on, but no
|
||||
// DLL is loaded any more (nothing new is read in a crash).
|
||||
// 8. A crash on another thread meanwhile can't have DbgHelp: the state says
|
||||
// so, Walk finds nothing, and Unwind still walks a blocked thread's copy
|
||||
// (what a hang report then falls back to).
|
||||
#include <platform/stack.h>
|
||||
#include <report/symbol_names.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
extern "C" void StackTestGuestRun(void (*callback)(void*), void* arg); // win_stack_test_guest.cpp
|
||||
volatile int g_sink = 0;
|
||||
|
||||
struct Walked
|
||||
{
|
||||
void* frames[64];
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
// A frame's name as a report shows it: a return address looked up at the
|
||||
// byte before it, a recompiled function as sub_<address>.
|
||||
static std::string NameOf(void* pc, bool returnAddress)
|
||||
{
|
||||
char name[256];
|
||||
uintptr_t start = 0;
|
||||
uint32_t length = 0;
|
||||
const void* at = static_cast<const uint8_t*>(pc) - (returnAddress ? 1 : 0);
|
||||
if (!platform::stack::Name(at, name, sizeof(name), start, length))
|
||||
return "?";
|
||||
char guest[13];
|
||||
if (const char* recompiled = report::detail::GuestName(name, guest))
|
||||
return recompiled;
|
||||
return name;
|
||||
}
|
||||
|
||||
static std::vector<std::string> Names(const Walked& w)
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < w.count; i++)
|
||||
names.push_back(NameOf(w.frames[i], i > 0));
|
||||
return names;
|
||||
}
|
||||
|
||||
static std::string Join(const std::vector<std::string>& names)
|
||||
{
|
||||
std::string text;
|
||||
for (const std::string& n : names)
|
||||
text += (text.empty() ? "" : " < ") + n;
|
||||
return text;
|
||||
}
|
||||
|
||||
// `wanted` in this order (others may come between), each a name that
|
||||
// contains it.
|
||||
static bool InOrder(const std::vector<std::string>& names, std::initializer_list<const char*> wanted)
|
||||
{
|
||||
size_t at = 0;
|
||||
for (const char* w : wanted)
|
||||
{
|
||||
while (at < names.size() && names[at].find(w) == std::string::npos)
|
||||
at++;
|
||||
if (at == names.size())
|
||||
return false;
|
||||
at++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. ---------------------------------------------------------------------
|
||||
__declspec(noinline) void StackTestCapture(void* out)
|
||||
{
|
||||
CONTEXT context;
|
||||
RtlCaptureContext(&context);
|
||||
auto* w = static_cast<Walked*>(out);
|
||||
w->count = platform::stack::Walk(context, w->frames, 64);
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
__declspec(noinline) void StackTestLevel2(Walked* w)
|
||||
{
|
||||
StackTestGuestRun(StackTestCapture, w);
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
__declspec(noinline) void StackTestLevel1(Walked* w)
|
||||
{
|
||||
StackTestLevel2(w);
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
// 4. ---------------------------------------------------------------------
|
||||
static Walked g_fault;
|
||||
|
||||
static int OnFault(EXCEPTION_POINTERS* info)
|
||||
{
|
||||
g_fault.count = platform::stack::Walk(*info->ContextRecord, g_fault.frames, 64);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
__declspec(noinline) void StackTestFault()
|
||||
{
|
||||
static volatile uintptr_t address = 0x10;
|
||||
*reinterpret_cast<volatile int*>(address) = 1;
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
__declspec(noinline) void StackTestFaultCaller()
|
||||
{
|
||||
__try
|
||||
{
|
||||
StackTestFault();
|
||||
}
|
||||
__except (OnFault(GetExceptionInformation()))
|
||||
{
|
||||
}
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
// 5. ---------------------------------------------------------------------
|
||||
struct Blocked
|
||||
{
|
||||
HANDLE first = nullptr, second = nullptr;
|
||||
uintptr_t stackHigh = 0;
|
||||
std::atomic<int> phase{ 0 };
|
||||
};
|
||||
|
||||
__declspec(noinline) void StackTestBlockedFirst(Blocked* b)
|
||||
{
|
||||
b->phase = 1;
|
||||
WaitForSingleObject(b->first, INFINITE);
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
// Writes over the stack below the thread's entry, where the first wait's
|
||||
// frames were.
|
||||
__declspec(noinline) void StackTestOverwrite()
|
||||
{
|
||||
volatile char page[64 * 1024];
|
||||
for (size_t i = 0; i < sizeof(page); i++)
|
||||
page[i] = char(0xCC);
|
||||
g_sink = g_sink + page[100];
|
||||
}
|
||||
|
||||
__declspec(noinline) void StackTestBlockedSecond(Blocked* b)
|
||||
{
|
||||
b->phase = 2;
|
||||
WaitForSingleObject(b->second, INFINITE);
|
||||
g_sink = g_sink + 1;
|
||||
}
|
||||
|
||||
DWORD WINAPI StackTestThread(void* arg)
|
||||
{
|
||||
auto* b = static_cast<Blocked*>(arg);
|
||||
ULONG_PTR low = 0, high = 0;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
b->stackHigh = high;
|
||||
StackTestBlockedFirst(b);
|
||||
StackTestOverwrite();
|
||||
StackTestBlockedSecond(b);
|
||||
g_sink = g_sink + 1;
|
||||
return 7;
|
||||
}
|
||||
|
||||
static Walked g_firstWait; // 5's first wait, unwound without DbgHelp
|
||||
|
||||
static DWORD WINAPI StackTestOtherCrash(void*)
|
||||
{
|
||||
return platform::stack::EnterCrash(100) ? 1 : 0;
|
||||
}
|
||||
|
||||
static void WaitForPhase(Blocked& b, int phase)
|
||||
{
|
||||
for (int i = 0; i < 5000 && b.phase.load() != phase; i++)
|
||||
Sleep(1);
|
||||
Sleep(50); // into the wait itself
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("1. before LoadSymbols: nothing named or walked\n");
|
||||
{
|
||||
Walked w;
|
||||
CONTEXT context;
|
||||
RtlCaptureContext(&context);
|
||||
w.count = platform::stack::Walk(context, w.frames, 64);
|
||||
CHECK(w.count == 0, "%d frames", w.count);
|
||||
CHECK(NameOf(reinterpret_cast<void*>(&StackTestLevel1), false) == "?", "a name already");
|
||||
CHECK(!platform::stack::SymbolsReady(), "ready already");
|
||||
}
|
||||
|
||||
printf("2. LoadSymbols\n");
|
||||
const void* warm[] = { reinterpret_cast<const void*>(&StackTestCapture) };
|
||||
platform::stack::LoadSymbols(warm, 1);
|
||||
CHECK(platform::stack::SymbolsReady(), "not ready");
|
||||
printf(" %s\n", platform::stack::SymbolsState());
|
||||
CHECK(strcmp(platform::stack::SymbolsState(), "win_stack_test.pdb") == 0, "state: %s", platform::stack::SymbolsState());
|
||||
|
||||
printf("3. this thread, through the recompiled functions\n");
|
||||
{
|
||||
Walked w;
|
||||
StackTestLevel1(&w);
|
||||
std::vector<std::string> names = Names(w);
|
||||
printf(" %s\n", Join(names).c_str());
|
||||
CHECK(w.count >= 7, "%d frames", w.count);
|
||||
CHECK(!names.empty() && names[0].find("StackTestCapture") != std::string::npos, "first: %s", names.empty() ? "" : names[0].c_str());
|
||||
CHECK(InOrder(names, { "StackTestCapture", "sub_82000010", "sub_82000020", "__imp___xstart", "StackTestGuestRun", "StackTestLevel2",
|
||||
"StackTestLevel1", "main" }),
|
||||
"not in order");
|
||||
// Exactly the names: no underscore lost (DbgHelp's SYMOPT_UNDNAME
|
||||
// drops one from a public C name), C++ undecorated.
|
||||
bool xstart = false, guest = false;
|
||||
for (const std::string& n : names)
|
||||
{
|
||||
xstart |= n == "__imp___xstart";
|
||||
guest |= n == "sub_82000020";
|
||||
}
|
||||
CHECK(xstart && guest, "__imp___xstart %d, sub_82000020 %d", xstart, guest);
|
||||
}
|
||||
|
||||
printf("4. an exception's registers\n");
|
||||
StackTestFaultCaller();
|
||||
{
|
||||
std::vector<std::string> names = Names(g_fault);
|
||||
printf(" %s\n", Join(names).c_str());
|
||||
CHECK(!names.empty() && names[0].find("StackTestFault") != std::string::npos && names[0].find("Caller") == std::string::npos,
|
||||
"first: %s", names.empty() ? "" : names[0].c_str());
|
||||
CHECK(InOrder(names, { "StackTestFault", "StackTestFaultCaller", "main" }), "not in order");
|
||||
}
|
||||
|
||||
printf("5. another thread, blocked\n");
|
||||
{
|
||||
Blocked b;
|
||||
b.first = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
b.second = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
HANDLE thread = CreateThread(nullptr, 0, StackTestThread, &b, 0, nullptr);
|
||||
WaitForPhase(b, 1);
|
||||
std::vector<uint8_t> copy(1 << 20);
|
||||
platform::stack::Snapshot first;
|
||||
first.buffer = copy.data();
|
||||
first.capacity = copy.size();
|
||||
CHECK(platform::stack::Capture(thread, b.stackHigh, first), "Capture failed");
|
||||
CHECK(first.size > 0 && first.low == first.context.Rsp, "copied %zu bytes from %llx (rsp %llx)", first.size,
|
||||
(unsigned long long)first.low, (unsigned long long)first.context.Rsp);
|
||||
Walked w;
|
||||
w.count = platform::stack::Walk(first.context, w.frames, 64, &first);
|
||||
std::vector<std::string> names = Names(w);
|
||||
printf(" first wait: %s\n", Join(names).c_str());
|
||||
CHECK(InOrder(names, { "StackTestBlockedFirst", "StackTestThread" }), "not in order");
|
||||
Walked unwound;
|
||||
unwound.count = platform::stack::Unwind(first, unwound.frames, 64);
|
||||
std::vector<std::string> unwoundNames = Names(unwound);
|
||||
printf(" without DbgHelp: %s\n", Join(unwoundNames).c_str());
|
||||
CHECK(unwoundNames == names, "Unwind found other frames than Walk");
|
||||
g_firstWait = unwound;
|
||||
|
||||
// On to the second wait, over the first one's frames.
|
||||
SetEvent(b.first);
|
||||
WaitForPhase(b, 2);
|
||||
Walked again;
|
||||
again.count = platform::stack::Walk(first.context, again.frames, 64, &first);
|
||||
std::vector<std::string> fromCopy = Names(again);
|
||||
printf(" the first copy again: %s\n", Join(fromCopy).c_str());
|
||||
CHECK(fromCopy == names, "the old copy walks elsewhere now");
|
||||
Walked unwoundAgain;
|
||||
unwoundAgain.count = platform::stack::Unwind(first, unwoundAgain.frames, 64);
|
||||
CHECK(Names(unwoundAgain) == names, "the old copy unwinds elsewhere now: %s", Join(Names(unwoundAgain)).c_str());
|
||||
|
||||
std::vector<uint8_t> copy2(1 << 20);
|
||||
platform::stack::Snapshot second;
|
||||
second.buffer = copy2.data();
|
||||
second.capacity = copy2.size();
|
||||
CHECK(platform::stack::Capture(thread, b.stackHigh, second), "Capture failed");
|
||||
Walked now;
|
||||
now.count = platform::stack::Walk(second.context, now.frames, 64, &second);
|
||||
std::vector<std::string> nowNames = Names(now);
|
||||
printf(" second wait: %s\n", Join(nowNames).c_str());
|
||||
CHECK(InOrder(nowNames, { "StackTestBlockedSecond", "StackTestThread" }), "not in order");
|
||||
CHECK(Join(nowNames).find("StackTestBlockedFirst") == std::string::npos, "the first wait still there");
|
||||
|
||||
SetEvent(b.second);
|
||||
DWORD waited = WaitForSingleObject(thread, 5000);
|
||||
DWORD code = 0;
|
||||
GetExitCodeThread(thread, &code);
|
||||
CHECK(waited == WAIT_OBJECT_0 && code == 7, "thread: wait %lu, exit code %lu", waited, code);
|
||||
CloseHandle(thread);
|
||||
CloseHandle(b.first);
|
||||
CloseHandle(b.second);
|
||||
}
|
||||
|
||||
printf("6. a DLL DbgHelp hadn't seen: dbghelp.dll itself\n");
|
||||
{
|
||||
void* function = reinterpret_cast<void*>(GetProcAddress(GetModuleHandleW(L"dbghelp.dll"), "SymFromAddr"));
|
||||
std::string name = function ? NameOf(function, false) : "(no SymFromAddr)";
|
||||
printf(" %s\n", name.c_str());
|
||||
CHECK(name.find("SymFromAddr") != std::string::npos, "%s", name.c_str());
|
||||
}
|
||||
|
||||
printf("7. EnterCrash\n");
|
||||
{
|
||||
HMODULE version = LoadLibraryExW(L"version.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
CHECK(platform::stack::EnterCrash(1000), "EnterCrash failed");
|
||||
Walked w;
|
||||
StackTestLevel1(&w);
|
||||
std::vector<std::string> names = Names(w);
|
||||
printf(" %s\n", Join(names).c_str());
|
||||
CHECK(InOrder(names, { "StackTestCapture", "sub_82000010", "StackTestLevel1", "main" }), "not in order");
|
||||
// Code in a DLL DbgHelp hasn't seen (version.dll's entry point):
|
||||
// no name now, since nothing new is loaded in a crash.
|
||||
std::string unseen = "(no version.dll)";
|
||||
if (version)
|
||||
{
|
||||
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(version);
|
||||
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(reinterpret_cast<const uint8_t*>(version) + dos->e_lfanew);
|
||||
unseen = NameOf(reinterpret_cast<uint8_t*>(version) + nt->OptionalHeader.AddressOfEntryPoint, false);
|
||||
}
|
||||
printf(" version.dll's entry point: %s\n", unseen.c_str());
|
||||
CHECK(unseen == "?", "%s", unseen.c_str());
|
||||
}
|
||||
|
||||
printf("8. a crash on another thread meanwhile\n");
|
||||
{
|
||||
HANDLE other = CreateThread(nullptr, 0, StackTestOtherCrash, nullptr, 0, nullptr);
|
||||
WaitForSingleObject(other, 5000);
|
||||
DWORD had = 2;
|
||||
GetExitCodeThread(other, &had);
|
||||
CloseHandle(other);
|
||||
CHECK(had == 0, "the other thread had DbgHelp (%lu)", had);
|
||||
printf(" %s\n", platform::stack::SymbolsState());
|
||||
CHECK(strstr(platform::stack::SymbolsState(), "unavailable in this crash") != nullptr, "state: %s",
|
||||
platform::stack::SymbolsState());
|
||||
Blocked b;
|
||||
b.first = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
b.second = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
HANDLE thread = CreateThread(nullptr, 0, StackTestThread, &b, 0, nullptr);
|
||||
WaitForPhase(b, 1);
|
||||
std::vector<uint8_t> copy(1 << 20);
|
||||
platform::stack::Snapshot snapshot;
|
||||
snapshot.buffer = copy.data();
|
||||
snapshot.capacity = copy.size();
|
||||
CHECK(platform::stack::Capture(thread, b.stackHigh, snapshot), "Capture failed");
|
||||
Walked w;
|
||||
w.count = platform::stack::Walk(snapshot.context, w.frames, 64, &snapshot);
|
||||
CHECK(w.count == 0, "Walk found %d frames", w.count);
|
||||
Walked u;
|
||||
u.count = platform::stack::Unwind(snapshot, u.frames, 64);
|
||||
// The same code path as 5's first wait: the same return addresses.
|
||||
bool same = u.count == g_firstWait.count && u.count >= 3;
|
||||
for (int i = 1; same && i < u.count; i++)
|
||||
same = u.frames[i] == g_firstWait.frames[i];
|
||||
printf(" %d frames without DbgHelp, %s 5's\n", u.count, same ? "as" : "NOT as");
|
||||
CHECK(same, "%d frames against %d", u.count, g_firstWait.count);
|
||||
SetEvent(b.first);
|
||||
SetEvent(b.second);
|
||||
CHECK(WaitForSingleObject(thread, 5000) == WAIT_OBJECT_0, "the thread didn't end");
|
||||
CloseHandle(thread);
|
||||
CloseHandle(b.first);
|
||||
CloseHandle(b.second);
|
||||
}
|
||||
|
||||
printf(g_failures ? "%d failure(s)\n" : "all passed\n", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Half of tests/win_stack_test.cpp: functions shaped and named as the
|
||||
// recompiled code is (ppc/ppc_context.h): an extern "C" body
|
||||
// __imp__sub_<address> and a C++ function sub_<address>, built, as ppc/ is,
|
||||
// without debug information, so the PDB knows them by their public symbols
|
||||
// alone.
|
||||
extern "C" volatile int g_guestSink = 0;
|
||||
|
||||
extern "C" __declspec(noinline) void __imp__sub_82000010(void (*callback)(void*), void* arg)
|
||||
{
|
||||
callback(arg);
|
||||
g_guestSink = g_guestSink + 1; // (not a tail call: this frame stays)
|
||||
}
|
||||
|
||||
__declspec(noinline) void sub_82000020(void (*callback)(void*), void* arg)
|
||||
{
|
||||
__imp__sub_82000010(callback, arg);
|
||||
g_guestSink = g_guestSink + 1;
|
||||
}
|
||||
|
||||
// A body named after the guest's own symbol, as XenonRecomp names _xstart's
|
||||
// (its first underscore is part of the name: no 32-bit decoration here).
|
||||
extern "C" __declspec(noinline) void __imp___xstart(void (*callback)(void*), void* arg)
|
||||
{
|
||||
sub_82000020(callback, arg);
|
||||
g_guestSink = g_guestSink + 1;
|
||||
}
|
||||
|
||||
// What the test calls (it never names __imp__ itself: an undefined
|
||||
// __imp_ symbol means a DLL import to the linker).
|
||||
extern "C" __declspec(noinline) void StackTestGuestRun(void (*callback)(void*), void* arg)
|
||||
{
|
||||
__imp___xstart(callback, arg);
|
||||
g_guestSink = g_guestSink + 1;
|
||||
}
|
||||
@@ -13,7 +13,9 @@
|
||||
// page is a guest page, checks the old rule both times); with it set, it runs
|
||||
// once. Physical memory is mapped as the runtime maps it (three windows of one
|
||||
// shared object), and
|
||||
// SIGSEGV and SIGBUS go to HandleFault as main.cpp sends them. Under a second.
|
||||
// SIGSEGV and SIGBUS go to HandleFault as main.cpp sends them (Windows: a
|
||||
// vectored exception handler, as platform/win32/fault.cpp does, and the
|
||||
// runtime's own mapping, platform/win32/memory.cpp). Under a second.
|
||||
#include <stdafx.h>
|
||||
#include <kernel/write_watch.h>
|
||||
|
||||
@@ -21,15 +23,19 @@
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
#include <platform/memory.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/wait.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
// What write_watch.cpp needs of the runtime's memory: the base and the 0xE
|
||||
// window's shift (memory.cpp is not linked).
|
||||
@@ -51,6 +57,19 @@ static uint8_t* Guest(uint32_t physical, uint32_t window = 0xA0000000)
|
||||
return g_memory.base + window + physical;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static LONG CALLBACK FaultHandler(EXCEPTION_POINTERS* info)
|
||||
{
|
||||
const EXCEPTION_RECORD* r = info->ExceptionRecord;
|
||||
if (r->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || r->NumberParameters < 2)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
void* address = reinterpret_cast<void*>(r->ExceptionInformation[1]);
|
||||
if (writewatch::HandleFault(address, r->ExceptionInformation[0] == 1))
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
fprintf(stderr, "unexpected access violation at %p\n", address);
|
||||
_exit(2);
|
||||
}
|
||||
#else
|
||||
static void FaultHandler(int sig, siginfo_t* info, void* context)
|
||||
{
|
||||
bool isWrite = true;
|
||||
@@ -66,11 +85,18 @@ static void FaultHandler(int sig, siginfo_t* info, void* context)
|
||||
fprintf(stderr, "unexpected signal %d at %p\n", sig, info->si_addr);
|
||||
_exit(2);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Physical memory, mapped three times as memory.cpp maps it (the 0xE window
|
||||
// shifted 4 KB where the host page size allows).
|
||||
static bool MapMemory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// The runtime's own: a view must fill its placeholder, so 0xE is unshifted.
|
||||
g_memory.base = platform::MapGuestSpace();
|
||||
g_memory.eWindowShift = 0;
|
||||
return g_memory.base != nullptr;
|
||||
#else
|
||||
void* reserved = mmap(nullptr, size_t(1) << 32, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (reserved == MAP_FAILED)
|
||||
return false;
|
||||
@@ -92,6 +118,7 @@ static bool MapMemory()
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
static writewatch::GuardStats Stats()
|
||||
@@ -710,11 +737,15 @@ int main(int, char** argv)
|
||||
perror("map physical memory");
|
||||
return 1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
AddVectoredExceptionHandler(1, FaultHandler);
|
||||
#else
|
||||
struct sigaction sa{};
|
||||
sa.sa_sigaction = FaultHandler;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigaction(SIGSEGV, &sa, nullptr);
|
||||
sigaction(SIGBUS, &sa, nullptr);
|
||||
#endif
|
||||
writewatch::Initialize();
|
||||
if (std::getenv("NFSMW_WATCH_SUBPAGES"))
|
||||
return Run();
|
||||
@@ -736,6 +767,27 @@ int main(int, char** argv)
|
||||
for (const char* value : { "0", "1" })
|
||||
{
|
||||
setenv("NFSMW_WATCH_SUBPAGES", value, 1);
|
||||
#ifdef _WIN32
|
||||
// This executable again, with the variable in its environment.
|
||||
wchar_t self[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, self, MAX_PATH);
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
fflush(stdout);
|
||||
if (!CreateProcessW(self, nullptr, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi))
|
||||
{
|
||||
fprintf(stderr, "CreateProcess: error %lu\n", GetLastError());
|
||||
return 1;
|
||||
}
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
if (code != 0)
|
||||
result = 1;
|
||||
#else
|
||||
pid_t child;
|
||||
if (posix_spawn(&child, argv[0], nullptr, nullptr, argv, environ) != 0)
|
||||
{
|
||||
@@ -746,6 +798,7 @@ int main(int, char** argv)
|
||||
waitpid(child, &status, 0);
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
||||
result = 1;
|
||||
#endif
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user