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:
committed 2026-10-24 19:31:48 +00:00
1 parent 57b0c69334
commit 61365bd067
99 files changed
+7457 -431

No files matched your search

+43 -3
View File
@@ -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
+38
View File
@@ -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"
}
}
]
}
+41 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+14 -43
View File
@@ -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 -8
View File
@@ -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
+1 -1
View File
@@ -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];
+26
View File
@@ -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
View File
@@ -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
+46 -14
View File
@@ -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"(
}
)";
}
+4
View File
@@ -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)
{
+23 -8
View File
@@ -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);
+29 -7
View File
@@ -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);
+34 -36
View File
@@ -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 {};
}
+47 -6
View File
@@ -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)
+17 -15
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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;
+7 -13
View File
@@ -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)
+8 -8
View File
@@ -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
+11
View File
@@ -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)
+1 -1
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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, &params);
platform::Thread thread{};
platform::StartThread(&thread, HOST_STACK, GuestMain, &params);
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;
}
+21
View File
@@ -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
}
+94
View File
@@ -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);
}
+41
View File
@@ -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
+31
View File
@@ -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
}
+16
View File
@@ -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
+129
View File
@@ -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));
}
}
+34 -1
View File
@@ -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";
}
}
+87
View File
@@ -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
+70
View File
@@ -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
}
+72
View File
@@ -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
}
+53
View File
@@ -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
}
+75
View File
@@ -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);
}
+60
View File
@@ -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;
}
}
}
+79
View File
@@ -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);
}
}
+280
View File
@@ -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));
}
}
+79
View File
@@ -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;
}
}
+37
View File
@@ -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))
+125
View File
@@ -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;
}
}
+106
View File
@@ -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;
}
+117
View File
@@ -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>
+38
View File
@@ -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
+472
View File
@@ -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();
}
}
+136
View File
@@ -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;
}
}
+68
View File
@@ -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);
}
}
+81
View File
@@ -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");
}
}
+75 -33
View File
@@ -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
}
}
+15 -4
View File
@@ -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}")
+48 -1
View File
@@ -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
View File
@@ -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;
+25 -1
View File
@@ -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
+6 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+107
View File
@@ -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;
}
}
+76
View File
@@ -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;
}
}
+159
View File
@@ -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
View File
@@ -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"))
+4
View File
@@ -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
View File
@@ -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
+26 -7
View File
@@ -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)
+4
View File
@@ -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)
+6
View File
@@ -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
View File
@@ -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();
}
+11 -3
View File
@@ -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
View File
@@ -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)
+8 -5
View File
@@ -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
View File
@@ -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();
}
+6
View File
@@ -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.
+38
View File
@@ -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;
}
}
+16
View File
@@ -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')
+549
View File
@@ -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
View File
@@ -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})
+355
View File
@@ -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;
}
+24 -2
View File
@@ -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);
+21
View File
@@ -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)
{
+71 -1
View File
@@ -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();
+6
View File
@@ -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;
+94
View File
@@ -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;
}
+7 -1
View File
@@ -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);
+164
View File
@@ -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;
}
+72
View File
@@ -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;
}
+16
View File
@@ -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
+6
View File
@@ -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;
+134
View File
@@ -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;
}
+149
View File
@@ -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;
}
+65
View File
@@ -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;
}
+390
View File
@@ -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;
}
+34
View File
@@ -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;
}
+56 -3
View File
@@ -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);
}