mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-18 01:01:53 +02:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b35243244f | ||
|
|
55d921a118 | ||
|
|
9be271d49a | ||
|
|
6a3441b616 | ||
|
|
58b6d81ed1 | ||
|
|
f7ca5ecbd6 | ||
|
|
c5f9ca3e2d | ||
|
|
8e9e905b85 | ||
|
|
03db602a05 | ||
|
|
77333709ae | ||
|
|
cfd434fac8 | ||
|
|
93df677470 | ||
|
|
8f725001de | ||
|
|
e49ec771c3 | ||
|
|
db8afbb3a9 | ||
|
|
755fb0e332 | ||
|
|
f2f4f081c8 | ||
|
|
897899a5f9 | ||
|
|
c8220c0c11 | ||
|
|
c91c1b289c | ||
|
|
a23fd087ee | ||
|
|
4407013d61 | ||
|
|
20ef08b3b8 | ||
|
|
b361ecd54c | ||
|
|
c12b6b7d99 | ||
|
|
5bf1904ca8 | ||
|
|
a0f97d307d | ||
|
|
491d39503e | ||
|
|
a132a82952 | ||
|
|
cfb9623e83 | ||
|
|
35e045f9f7 | ||
|
|
504ea792da | ||
|
|
f87732d0ad | ||
|
|
152c4f5f38 | ||
|
|
794b715d31 | ||
|
|
a532ffa6f3 |
No files matched your search
+11
-1
@@ -39,9 +39,19 @@ That's ok, but rules apply:
|
||||
game (hardware comparison, logs, whatever fits).
|
||||
- Review feedback. It's about the code, not about you ;).
|
||||
|
||||
## Porting from other projects
|
||||
|
||||
This fork takes fixes from upstream WiiCompiled and other forks. When you port one:
|
||||
|
||||
- name the source repository and commit in the commit message ("Ported from owner/repo abc1234");
|
||||
- keep the original author on the commit when the change is taken as is, and say what you changed
|
||||
when it is adapted;
|
||||
- add a line to [`CREDITS.md`](CREDITS.md), and check the source's license is compatible with
|
||||
GPL v3.0.
|
||||
|
||||
## Bug reports
|
||||
|
||||
See the FAQ in the [README](README.md)
|
||||
See [Reporting problems](README.md#reporting-problems) in the README.
|
||||
|
||||
## A note on related projects
|
||||
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Credits
|
||||
|
||||
WiiCompiled VR for the Steam Frame is the work of many people. This fork adds the SteamOS build,
|
||||
the install and diagnostics scripts and the Frame-specific rendering, but most of what you play
|
||||
was written elsewhere, and a good part of this fork's own fixes were ported from other people's
|
||||
projects. This page lists where everything came from.
|
||||
|
||||
Every project below is GPL-3.0 licensed like this one unless it says otherwise. Ported commits name
|
||||
their source repository and commit in their messages, and keep the original author where the
|
||||
change was taken as is, so `git log` is the full record. Bundled libraries and their licenses are
|
||||
in [`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md).
|
||||
|
||||
## The projects this fork is built on
|
||||
|
||||
- **[WiiCompiled](https://github.com/patchzyy/Wiicompiled)** by patchzyy and its contributors: the
|
||||
static recompilation of Mario Kart Wii itself, the translator, the runtime and the Aurora-based
|
||||
renderer.
|
||||
- **[WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR)** by iChris4: the OpenXR VR
|
||||
renderer, the Quest app, the headset settings panel, foveation and the first-person cockpit. This
|
||||
fork contains all of its `openxr-work` branch up to 2026-10-01.
|
||||
- **[mario-kart-wii-VR-port](https://github.com/heurazy/mario-kart-wii-VR-port)** by heurazy: the
|
||||
cockpit's turning steering wheel, hand steering and USB wheel support, which came in through
|
||||
WiiCompiled OpenXR VR.
|
||||
- **[DolphinXR](https://github.com/iChris4/dolphinXR)**: the OpenXR Wii Remote input design and its
|
||||
default button profile, which the controller code (`runtime/src/vr/openxr_input.cpp`) and the
|
||||
Steam Frame controller profile's input paths are adapted from.
|
||||
|
||||
## Ported into this fork
|
||||
|
||||
### From WiiCompiled (upstream)
|
||||
|
||||
Cherry-picked or adapted from [patchzyy/Wiicompiled](https://github.com/patchzyy/Wiicompiled) after
|
||||
this fork's line split from it:
|
||||
|
||||
| Change | Author | Upstream |
|
||||
| --- | --- | --- |
|
||||
| TLS through mbed TLS on Linux and Android, which online play (Retro Rewind WFC) needs there | dorPXP, with patchzyy | #144 |
|
||||
| NAND moves across mount points on Linux (saves on a different drive or the SD card) | Daan Vervacke | #212 |
|
||||
| Controller mapping platform, player index sync, and socket send errors | Michael G ([DarthMDev](https://github.com/DarthMDev)) | #251 |
|
||||
| `bltl` translation | patchzyy | #254 |
|
||||
| The KartPad batch: KD and NCD kept while offline, RFL alarm interrupt context, shared LR continuation dispatch, pipeline cache size checks, pipeline waiter wake-ups, packed three-byte vertex reads, draw merging bounds, GEN_MODE decode, EFB copy and readback fixes, graphics startup errors, and the staging buffer wait | patchzyy, adapting Chris Sotraidis's [KartPad](https://github.com/chrissotraidis/wiicompiled) fixes | #244 |
|
||||
|
||||
### From other WiiCompiled forks
|
||||
|
||||
| Change | Author | Source |
|
||||
| --- | --- | --- |
|
||||
| Scene-load crash fix (`GXClearVtxDesc` keeps the position descriptor) | heurazy | [Wiicompiled_VR-PLUS](https://github.com/heurazy/Wiicompiled_VR-PLUS) c2b2500 |
|
||||
| `[vr] adaptive_resolution` | heurazy | Wiicompiled_VR-PLUS, adapted |
|
||||
| Building and installing Retro Rewind from its own update server (`steam-frame-install.sh --retro-rewind`) | heurazy | Wiicompiled_VR-PLUS, adapted |
|
||||
| Faster disc reads: bulk DVD DMA copies, Yaz0 (SZS) decoding through host buffers, per-thread disc file handles | Chris Sotraidis | [KartPad](https://github.com/chrissotraidis/wiicompiled) ec14a0d, 97cdc9a, affabbd |
|
||||
| No debug labels in the ImGui passes in normal builds (an Adreno driver crash) | Chris Sotraidis | KartPad 420b828, extended to this fork's extra passes |
|
||||
| A TLS session that failed a write is kept instead of its socket being deleted | Michael G ([DarthMDev](https://github.com/DarthMDev)) | [DarthMDev/Wiicompiled](https://github.com/DarthMDev/Wiicompiled) a2ecc0f, fcf8646 (upstream PR #258) |
|
||||
| `Config.toml` saved through a temporary file and a rename | BlackAndBlue95 | [Strikers-WiiCompiled](https://github.com/BlackAndBlue95/Strikers-WiiCompiled) 435daf3 |
|
||||
| No depth writes while the Z compare is off (the Stormship lightning) | BlackAndBlue95 | Strikers-WiiCompiled 369ec99 |
|
||||
| NEON forms of the AX audio mix kernels | rooklz | [rooklz/Wiicompiled](https://github.com/rooklz/Wiicompiled) 4346362 |
|
||||
| Emptied WC24 bootstrap files are refilled | nx-mod | [wiicompiled-nx](https://github.com/nx-mod/wiicompiled-nx) d0149e6, adapted |
|
||||
| Guest memory access checks folded away on 4 KiB-page builds | nx-mod's finding | wiicompiled-nx 18ef16f, done here as a compile-time constant |
|
||||
|
||||
## Tools and references
|
||||
|
||||
- **[aurora](https://github.com/encounter/aurora)** by Luke Street: the GX rendering and windowing
|
||||
backend the whole graphics layer sits on (MIT).
|
||||
- **[Dawn](https://dawn.googlesource.com/dawn)**: Google's WebGPU implementation, under Aurora.
|
||||
- **[OpenXR](https://www.khronos.org/openxr/)** by Khronos: the VR API.
|
||||
- **[Mbed TLS](https://github.com/Mbed-TLS/mbedtls)**: TLS for online play on Linux and Android.
|
||||
- **[Dolphin Emulator](https://github.com/dolphin-emu/dolphin)**: a reference for Wii hardware
|
||||
behaviour, and the source of the DSP coefficient ROM, the WiiConnect24 bootstrap tree and the
|
||||
Riivolution parser bundled with the runtime.
|
||||
- **[nod](https://github.com/encounter/nod)** by Luke Street: `nodtool`, the disc image extractor.
|
||||
- **[Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind)** by ZPL and team: the mod
|
||||
distribution this project supports.
|
||||
- **[Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard)**: the mod manager the PC build
|
||||
integrates with, and the model for how the install script fetches Retro Rewind.
|
||||
- **[Frame Control](https://github.com/saphid/frame-control)** by saphid: installing the game into
|
||||
the Frame's Steam library, and its notes on how the Frame's software fits together.
|
||||
- **Inkwreck**: the logo.
|
||||
- Everyone in the static recompilation community.
|
||||
|
||||
If something of yours is in this fork and missing here, or credited wrongly, please open an issue.
|
||||
@@ -1,5 +1,9 @@
|
||||
# Windows VR distribution
|
||||
|
||||
This page covers upstream's Windows releases, kept for the PC build. The Steam Frame releases
|
||||
(`frame-*` tags) are source only and are published by `.github/workflows/frame-release.yml` with
|
||||
the notes in `docs/releases/<tag>.md`; see the [README](README.md#quick-start).
|
||||
|
||||
Distribute only `WiiCompiled-Setup.exe` and its checksum from
|
||||
[iChris4/Wiicompiled_VR](https://github.com/iChris4/Wiicompiled_VR/releases), and
|
||||
`WheelWizardVRWindows.exe` from [iChris4/WheelWizard_VR](https://github.com/iChris4/WheelWizard_VR/releases).
|
||||
|
||||
@@ -503,7 +503,9 @@ publish_built_product() {
|
||||
local exe=$build/$target
|
||||
assert_file "$exe" "Locally compiled game executable"
|
||||
cp -f "$exe" "$destination/$target"
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db; do
|
||||
# cacert.pem is the TLS root bundle the mbed TLS path looks up beside the executable
|
||||
# (runtime/src/hle/net/network_ssl.cpp); without it HTTPS fails at runtime.
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
|
||||
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
|
||||
done
|
||||
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
|
||||
|
||||
@@ -27,7 +27,7 @@ done
|
||||
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
|
||||
app="$output_dir/$product.app"
|
||||
macos="$app/Contents/MacOS"
|
||||
@@ -52,7 +52,7 @@ cat > "$app/Contents/Info.plist" <<EOF
|
||||
</dict></plist>
|
||||
EOF
|
||||
ditto "$build_dir/$product" "$macos/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
|
||||
ditto "$build_dir/$asset" "$resources/$asset"
|
||||
ln -s "../Resources/$asset" "$macos/$asset"
|
||||
done
|
||||
|
||||
+271
-25
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds WiiCompiled VR for the Steam Frame from a release and your own disc, and installs it on the
|
||||
# Frame. Run it again to update: it fetches the newest release and rebuilds only what changed.
|
||||
# Frame. Run it again with --update to update: it fetches the newest release and rebuilds only what
|
||||
# changed, with the options it was installed with.
|
||||
#
|
||||
# Launcher/steam-frame-install.sh --disc PATH [--frame HOST] [options]
|
||||
# Launcher/steam-frame-install.sh --update
|
||||
#
|
||||
# Or without downloading anything first:
|
||||
#
|
||||
@@ -36,32 +38,84 @@
|
||||
# --retro-rewind-pack DIR
|
||||
# use this RetroRewind6 folder instead of downloading one (implies
|
||||
# --retro-rewind); it is copied into the work dir and never changed
|
||||
# --update update an earlier install: its options are reused (any given here win), and
|
||||
# nothing is built when the Frame already has the newest release and Retro
|
||||
# Rewind pack. Follows releases unless it was installed with --source.
|
||||
# --check only say whether an update is available; nothing is changed
|
||||
# -h, --help
|
||||
#
|
||||
# Installed with --frame local, the game can update itself: Settings > Updates in the game checks
|
||||
# for a new release, and its Update button runs this script with --update in the background,
|
||||
# through a systemd user service this script sets up. The tab shows the update's progress while the
|
||||
# game stays open, and a game closed meanwhile is opened again when the update is done.
|
||||
set -euo pipefail
|
||||
|
||||
# Everything is in main, so bash has read the whole script before running any of it: when it comes
|
||||
# through curl | bash, a step that reads stdin cannot eat the rest. (Not indented, for the heredocs.)
|
||||
main() {
|
||||
trap 'printf "\nsteam-frame-install.sh: stopped by a failed step (line %s); the output above says why.\n" "$LINENO" >&2' ERR
|
||||
# What the error path below reads, set before anything can fail into it.
|
||||
mode=install
|
||||
steam_game_id=""
|
||||
trap 'on_error "$LINENO"' ERR
|
||||
|
||||
repo=mitch030504/Wiicompiled_VR_Frame
|
||||
game_id=WiiCompiled
|
||||
image=docker.io/library/debian:trixie
|
||||
container=wiicompiled-frame-build
|
||||
nodtool_version=v2.0.0-alpha.10
|
||||
# Where the game keeps its settings, and where an update it starts reports back to it. Resolved as
|
||||
# the game resolves it (RuntimeConfigFile::ApplicationDataDirectory).
|
||||
game_data="${XDG_DATA_HOME:-$HOME/.local/share}/$game_id"
|
||||
|
||||
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
say() {
|
||||
printf '\n\033[1m==> %s\033[0m\n' "$*"
|
||||
status running "$*"
|
||||
}
|
||||
note() { printf ' %s\n' "$*"; }
|
||||
fail() {
|
||||
printf '\nsteam-frame-install.sh: error: %s\n' "$*" >&2
|
||||
finish_failed "${1%%$'\n'*}"
|
||||
}
|
||||
on_error() {
|
||||
printf '\nsteam-frame-install.sh: stopped by a failed step (line %s); the output above says why.\n' "$1" >&2
|
||||
finish_failed "a step failed (line $1)"
|
||||
}
|
||||
finish_failed() {
|
||||
trap - ERR
|
||||
status failed "$1"
|
||||
relaunch
|
||||
exit 1
|
||||
}
|
||||
usage() { awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$script_path"; }
|
||||
|
||||
# The game's Updates tab reads this: "<state>\t<unix time>\t<text>", state running, available,
|
||||
# uptodate, done or failed. Written only when the game asked for this run
|
||||
# (WIICOMPILED_UPDATE_STATUS names the file).
|
||||
status() {
|
||||
local file=${WIICOMPILED_UPDATE_STATUS:-}
|
||||
[[ -n "$file" ]] || return 0
|
||||
{ printf '%s\t%s\t%s\n' "$1" "$(date +%s)" "$2" > "$file.new" && mv -f "$file.new" "$file"; } 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Progress goes to that file rather than to Steam notifications: on the Frame, SteamOS's
|
||||
# steam_notif_daemon accepts a notify-send and Steam then shows nothing, even when given the
|
||||
# notification directly with steam -ifrunning (tried on a Frame, 2026-10).
|
||||
# An update the game started ends by opening the game again if it was closed meanwhile, which is
|
||||
# how its result gets seen; a game still open shows the result in its Updates tab.
|
||||
game_running() { pgrep -f "devkit-game/($game_id|RetroRewind)/" >/dev/null 2>&1; }
|
||||
relaunch() {
|
||||
[[ -n "$steam_game_id" ]] || return 0
|
||||
command -v steam >/dev/null 2>&1 || return 0
|
||||
if game_running; then return 0; fi
|
||||
note "opening $game_id again"
|
||||
timeout 60 steam "steam://rungameid/$steam_game_id" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
script_path=${BASH_SOURCE[0]:-}
|
||||
script_dir=""
|
||||
if [[ -n "$script_path" && -f "$script_path" ]]; then
|
||||
script_dir=$(cd "$(dirname "$script_path")" && pwd)
|
||||
script_path="$script_dir/$(basename "$script_path")"
|
||||
fi
|
||||
|
||||
disc=""
|
||||
@@ -85,15 +139,151 @@ while [[ $# -gt 0 ]]; do
|
||||
--frame-disc) frame_disc=$2; shift 2 ;;
|
||||
--retro-rewind) retro_rewind=1; shift ;;
|
||||
--retro-rewind-pack) retro_rewind=1; retro_pack=$2; shift 2 ;;
|
||||
--update) mode=update; shift ;;
|
||||
--check) mode=check; shift ;;
|
||||
-h|--help)
|
||||
if [[ -n "$script_dir" ]]; then usage; else echo "See the comment at the top of the script."; fi
|
||||
exit 0 ;;
|
||||
*) fail "unknown argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
# What --update reuses: the options given, before defaults fill the rest in.
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
|
||||
mkdir -p "$work_dir"
|
||||
work_dir=$(cd "$work_dir" && pwd)
|
||||
|
||||
# The game's Update button leaves a request file; the service that runs this script moves it here.
|
||||
if [[ -n "${WIICOMPILED_UPDATE_REQUEST:-}" && -f "$WIICOMPILED_UPDATE_REQUEST" ]]; then
|
||||
steam_game_id=$(sed -n 's/^steam_game_id=\([0-9]*\)$/\1/p' "$WIICOMPILED_UPDATE_REQUEST" | head -n 1)
|
||||
rm -f "$WIICOMPILED_UPDATE_REQUEST"
|
||||
fi
|
||||
|
||||
if [[ "$mode" != install && -f "$work_dir/install.conf" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
frame) [[ -n "$frame" ]] || frame=$value ;;
|
||||
frame_disc) [[ -n "$frame_disc" ]] || frame_disc=$value ;;
|
||||
jobs) [[ -n "$jobs" ]] || jobs=$value ;;
|
||||
source) [[ -n "$source_dir" ]] || source_dir=$value ;;
|
||||
retro_rewind) if [[ "$value" == 1 ]]; then retro_rewind=1; fi ;;
|
||||
retro_rewind_pack) [[ -n "$retro_pack" ]] || retro_pack=$value ;;
|
||||
esac
|
||||
done < "$work_dir/install.conf"
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
fi
|
||||
save_settings() {
|
||||
cat > "$work_dir/install.conf" <<EOF
|
||||
# Written by steam-frame-install.sh after each install: the options --update reuses.
|
||||
frame=$frame
|
||||
frame_disc=$frame_disc
|
||||
jobs=$jobs_given
|
||||
source=$source_given
|
||||
retro_rewind=$retro_rewind
|
||||
retro_rewind_pack=$retro_pack
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Talking to the Frame, and the versions involved. Up here because --update and --check read them
|
||||
# before anything is built.
|
||||
# One SSH connection for every step, so a password is asked for once.
|
||||
ssh_opts=(-o ControlMaster=auto -o "ControlPath=${XDG_RUNTIME_DIR:-/tmp}/wiicompiled-ssh-%C" -o ControlPersist=600)
|
||||
on_frame() {
|
||||
# Runs a bash script, given on stdin, on the Frame; its arguments follow.
|
||||
if [[ "$frame" == local ]]; then bash -s -- "$@"; else ssh "${ssh_opts[@]}" "$frame" bash -s -- "$@"; fi
|
||||
}
|
||||
read_on_frame() {
|
||||
# Prints the contents of $1 on the Frame, with its whitespace removed, or nothing.
|
||||
on_frame "$1" <<'EOF'
|
||||
[[ "$1" = /* ]] && f=$1 || f="$HOME/$1"
|
||||
tr -d '[:space:]' 2>/dev/null < "$f" || true
|
||||
EOF
|
||||
}
|
||||
frame_game_dir="devkit-game/$game_id"
|
||||
|
||||
newest_release() {
|
||||
curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" |
|
||||
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1
|
||||
}
|
||||
|
||||
# Retro Rewind's own versioning, as its update server publishes it.
|
||||
rr_server=https://update.rwfc.net/RetroRewind/
|
||||
rr_version_ok() { [[ "$1" =~ ^[0-9]+(\.[0-9]+)+$ ]]; }
|
||||
rr_newer() {
|
||||
# True when version $1 is newer than $2 (dotted numbers, as 6.12.7).
|
||||
[[ "$1" != "$2" && "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
|
||||
}
|
||||
rr_fetch() { curl -fsSL --retry 2 "$1"; }
|
||||
rr_published_version() {
|
||||
rr_fetch "${rr_server}RetroRewindVersion.txt" |
|
||||
awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1 }' | sort -V | tail -n 1
|
||||
}
|
||||
# Where the pack sits on the Frame: beside the disc, as the install below puts it.
|
||||
frame_pack_path_for() {
|
||||
local disc=${1#\~/}
|
||||
[[ -n "$disc" ]] || disc=wiicompiled/disc
|
||||
if [[ "$disc" == */* ]]; then printf '%s/RetroRewind6\n' "$(dirname "$disc")"; else printf 'RetroRewind6\n'; fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# --update and --check: what the Frame has against what is published. Nothing is built when the
|
||||
# Frame is already current, so an update that has nothing to do costs one web request.
|
||||
if [[ "$mode" != install ]]; then
|
||||
[[ -n "$frame" ]] || fail "--$mode updates an install made by this script, and there is none in
|
||||
$work_dir. Install first (see --help), or pass --work-dir and --frame."
|
||||
say "Checking for an update"
|
||||
reasons=()
|
||||
if [[ -n "$source_dir" ]]; then
|
||||
# A source install has no published version to compare against; it rebuilds what changed.
|
||||
reasons+=("it was installed from the source tree in $source_dir, which is rebuilt as it is")
|
||||
else
|
||||
if [[ -z "$release" ]]; then
|
||||
release=$(newest_release) ||
|
||||
fail "could not reach github.com to ask for the newest release. Check this machine's
|
||||
internet connection, then try again."
|
||||
[[ -n "$release" ]] || fail "could not find the newest release on github.com/$repo"
|
||||
fi
|
||||
frame_installed_release=$(read_on_frame "$frame_game_dir/.release-tag") ||
|
||||
fail "cannot reach $frame over SSH to ask which release it has."
|
||||
if [[ "$frame_installed_release" == "$release" ]]; then
|
||||
note "the Frame has release $release"
|
||||
elif [[ -z "$frame_installed_release" ]]; then
|
||||
reasons+=("the Frame does not say which release it has; release $release would be installed")
|
||||
else
|
||||
reasons+=("release $release (the Frame has $frame_installed_release)")
|
||||
fi
|
||||
fi
|
||||
if (( retro_rewind )) && [[ -z "$retro_pack" ]]; then
|
||||
rr_published=$(rr_published_version) ||
|
||||
fail "could not reach Retro Rewind's update server (${rr_server})"
|
||||
rr_installed=$(read_on_frame "$(frame_pack_path_for "$frame_disc")/version.txt")
|
||||
if ! rr_version_ok "$rr_installed"; then
|
||||
reasons+=("the Frame does not say which Retro Rewind pack it has")
|
||||
elif rr_newer "$rr_published" "$rr_installed"; then
|
||||
reasons+=("Retro Rewind $rr_published (the Frame has $rr_installed)")
|
||||
else
|
||||
note "the Frame has Retro Rewind $rr_installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ${#reasons[@]} == 0 )); then
|
||||
say "Up to date"
|
||||
status uptodate "Up to date${release:+ (}${release}${release:+)}"
|
||||
relaunch
|
||||
exit 0
|
||||
fi
|
||||
note "an update is available:"
|
||||
for line in "${reasons[@]}"; do note " $line"; done
|
||||
if [[ "$mode" == check ]]; then
|
||||
status available "${reasons[0]}"
|
||||
exit 0
|
||||
fi
|
||||
status running "Starting the update${release:+ to }$release"
|
||||
fi
|
||||
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in
|
||||
x86_64|aarch64) ;;
|
||||
@@ -344,16 +534,10 @@ fi
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Retro Rewind: its pack, as its update server publishes it, and the Retro-WFC payload its online
|
||||
# play runs. Same steps as the Quest app (android/.../RetroRewindPack.kt and GameBuild.kt).
|
||||
rr_server=https://update.rwfc.net/RetroRewind/
|
||||
# Its server, versions and comparisons are up with the other version helpers.
|
||||
rr_pack="$work_dir/RetroRewind6"
|
||||
rr_payload="$work_dir/retro-wfc/binary/payload.RMCPD00.bin"
|
||||
|
||||
rr_version_ok() { [[ "$1" =~ ^[0-9]+(\.[0-9]+)+$ ]]; }
|
||||
rr_newer() {
|
||||
# True when version $1 is newer than $2 (dotted numbers, as 6.12.7).
|
||||
[[ "$1" != "$2" && "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
|
||||
}
|
||||
rr_fetch() { curl -fsSL --retry 2 "$1"; }
|
||||
rr_unpack_update() {
|
||||
# rr_unpack_update URL DEST: downloads a published zip and lays its RetroRewind6/ tree over DEST.
|
||||
# Only that tree is kept; the Riivolution XML beside it belongs to a Wii setup.
|
||||
@@ -499,8 +683,31 @@ fi
|
||||
"$runtime" start "$container" >/dev/null
|
||||
note "container $container (mounts ${want_mounts//;/ })"
|
||||
|
||||
build_progress() {
|
||||
# Passes the build's output on, and keeps the game's status file at the stage it is on and how
|
||||
# far through it ninja is, from the "[done/total]" lines it prints.
|
||||
local line stage=Building percent last=-1
|
||||
while IFS= read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
[[ -n "${WIICOMPILED_UPDATE_STATUS:-}" ]] || continue
|
||||
if [[ "$line" == "== "* ]]; then
|
||||
stage=${line#== }
|
||||
stage=${stage%% (*}
|
||||
last=-1
|
||||
status running "$stage"
|
||||
elif [[ "$line" =~ ^\[([0-9]+)/([0-9]+)\] ]] && (( BASH_REMATCH[2] > 0 )); then
|
||||
percent=$(( BASH_REMATCH[1] * 100 / BASH_REMATCH[2] ))
|
||||
if (( percent != last )); then
|
||||
last=$percent
|
||||
status running "$stage, $percent% built"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
say "Building (the first time takes hours under emulation; a log is in $work_dir/build.log)"
|
||||
if ! "$runtime" exec -e JOBS="$jobs" -e RETRO_REWIND="$retro_rewind" "$container" bash /work/container-build.sh 2>&1 | tee "$work_dir/build.log"; then
|
||||
if ! "$runtime" exec -e JOBS="$jobs" -e RETRO_REWIND="$retro_rewind" "$container" bash /work/container-build.sh 2>&1 |
|
||||
tee "$work_dir/build.log" | build_progress; then
|
||||
fail "the build stopped; the end of $work_dir/build.log says why. Run the script again to resume.
|
||||
A machine that froze ran out of memory: pass a lower --jobs."
|
||||
fi
|
||||
@@ -520,12 +727,7 @@ if [[ -z "$frame" ]]; then
|
||||
fi
|
||||
|
||||
say "Installing on the Frame"
|
||||
# One SSH connection for every step, so a password is asked for once.
|
||||
ssh_opts=(-o ControlMaster=auto -o "ControlPath=${XDG_RUNTIME_DIR:-/tmp}/wiicompiled-ssh-%C" -o ControlPersist=600)
|
||||
on_frame() {
|
||||
# Runs a bash script, given on stdin, on the Frame; its arguments follow.
|
||||
if [[ "$frame" == local ]]; then bash -s -- "$@"; else ssh "${ssh_opts[@]}" "$frame" bash -s -- "$@"; fi
|
||||
}
|
||||
# on_frame and the SSH options it uses are up with the version helpers.
|
||||
copy_to_frame() {
|
||||
# copy_to_frame SOURCE DEST: DEST is absolute or relative to the Frame's home. A DEST ending in
|
||||
# / is an existing folder SOURCE goes into; otherwise SOURCE is copied to that new name.
|
||||
@@ -563,6 +765,11 @@ EOF
|
||||
copy_to_frame "$out/$id" "$dir/$id.new"
|
||||
on_frame "$dir" "$id" <<'EOF'
|
||||
cd "$HOME/$1" && mv -f "$2.new" "$2" && chmod -R u=rwX,go=rX . && chmod 755 "$2"
|
||||
EOF
|
||||
# Which release is on the Frame, for --check and the game's Updates tab. A source build has no
|
||||
# release to name, and the stamp is removed so neither claims a release this is not.
|
||||
on_frame "$dir" "$release" <<'EOF'
|
||||
cd "$HOME/$1" && if [[ -n "$2" ]]; then printf '%s\n' "$2" > .release-tag; else rm -f .release-tag; fi
|
||||
EOF
|
||||
}
|
||||
install_game "$game_id" "$work_dir/out"
|
||||
@@ -594,13 +801,8 @@ fi
|
||||
|
||||
if (( retro_rewind )); then
|
||||
# The pack goes beside the disc; copied again only when its version changed.
|
||||
frame_pack_path="$(dirname "$frame_disc_path")/RetroRewind6"
|
||||
[[ "$frame_disc_path" == */* ]] || frame_pack_path=RetroRewind6
|
||||
frame_pack_version=$(on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
tr -d '[:space:]' 2>/dev/null < "$d/version.txt" || true
|
||||
EOF
|
||||
)
|
||||
frame_pack_path=$(frame_pack_path_for "$frame_disc_path")
|
||||
frame_pack_version=$(read_on_frame "$frame_pack_path/version.txt")
|
||||
local_pack_version=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
||||
if [[ -z "$frame_pack_version" || "$frame_pack_version" != "$local_pack_version" ]]; then
|
||||
note "copying the Retro Rewind pack to $frame_pack_path (about 4 GB)"
|
||||
@@ -672,9 +874,53 @@ esac
|
||||
register_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then register_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Updating from inside the game. Only an install on the Frame itself can do it, since that is the
|
||||
# only one with the build here; installed from a PC, the game's Updates tab says to update there.
|
||||
if [[ "$frame" == local ]]; then
|
||||
note "setting up the game's Update button"
|
||||
update_script="$work_dir/steam-frame-install.sh"
|
||||
# A copy, so an update never runs from the source tree it is about to overwrite.
|
||||
cp -f "$source_dir/Launcher/steam-frame-install.sh" "$update_script"
|
||||
chmod +x "$update_script"
|
||||
mkdir -p "$game_data" "$HOME/.config/systemd/user"
|
||||
cat > "$game_data/update.conf" <<EOF
|
||||
# Written by steam-frame-install.sh: what the game's Updates tab runs. Remove this file to take the
|
||||
# Update button away, and the tab goes back to saying how to update from a PC.
|
||||
script=$update_script
|
||||
work_dir=$work_dir
|
||||
service=wiicompiled-update.service
|
||||
EOF
|
||||
# Its own service, so the update outlives the game that started it: Steam stops the game's own
|
||||
# process group when the game closes.
|
||||
cat > "$HOME/.config/systemd/user/wiicompiled-update.service" <<EOF
|
||||
[Unit]
|
||||
Description=WiiCompiled VR update
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=WIICOMPILED_UPDATE_STATUS=$game_data/update-status
|
||||
Environment=WIICOMPILED_UPDATE_REQUEST=$game_data/update-request
|
||||
# Below the game in line for the processor and the storage, so it can keep running meanwhile.
|
||||
Nice=19
|
||||
IOSchedulingClass=idle
|
||||
ExecStart=$update_script --update --work-dir "$work_dir"
|
||||
EOF
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 ||
|
||||
note "(systemd did not reload, so the Update button will not work until the Frame restarts)"
|
||||
fi
|
||||
|
||||
save_settings
|
||||
|
||||
say "Done"
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
status done "Updated${release:+ to }${release}"
|
||||
if [[ "$mode" == update ]]; then
|
||||
note "Updated${release:+ to }$release."
|
||||
else
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
fi
|
||||
if (( retro_rewind )); then note "RetroRewind is beside it, and shares its settings."; fi
|
||||
relaunch
|
||||
}
|
||||
|
||||
main "$@" </dev/null
|
||||
@@ -147,6 +147,15 @@ headroom. It is live: **F10 → VR → Render resolution** (also on the headset
|
||||
in percent, applies it when the slider is let go, and saves it. Below the slider, *Each eye* gives
|
||||
the left eye's size now and, while they differ, the size the slider's value gives.
|
||||
|
||||
`adaptive_resolution` (default off; *Adaptive resolution (experimental)* on the VR tab) lowers the
|
||||
race's eye resolution in 10% steps, down to 70% of `render_scale`, while new eye frames fall below
|
||||
85% of the game's 60 FPS, and raises it again after three seconds on time. The eyes render into the
|
||||
top-left corner of the full swapchain image and the projection layer shows only that corner, so the
|
||||
compositor scales them up and no swapchain is rebuilt; each step does rebuild the foveation maps.
|
||||
`console.log` records every change as `OpenXR: adaptive resolution`. It is offered on the Quest and
|
||||
the Linux (Steam Frame) build, not on Windows, whose bridges copy whole eyes. Adapted from
|
||||
heurazy's [Wiicompiled_VR-PLUS](https://github.com/heurazy/Wiicompiled_VR-PLUS).
|
||||
|
||||
A new scale never interrupts the picture. Each backend keeps two swapchain pairs, one on display and
|
||||
one Aurora writes next, and rebuilds only the second, at the start of the frame that writes it; the
|
||||
other follows a frame later, once it is the one written. The new swapchains are created before the
|
||||
|
||||
@@ -106,10 +106,33 @@ shoulder button, go to the **VR** tab and set the [recommended settings](#recomm
|
||||
|
||||
### Updating
|
||||
|
||||
Run the same command again; you can leave out `--disc`. The script downloads the newest release,
|
||||
puts only the files that changed over the old source, rebuilds what they touch, and replaces the
|
||||
game on the Frame. Dawn is only rebuilt when a release changes its patches (the release notes say
|
||||
so), so an update usually takes minutes. `--release <tag>` builds a given release instead.
|
||||
```bash
|
||||
Launcher/steam-frame-install.sh --update
|
||||
```
|
||||
|
||||
`--update` reuses the options the install was made with, so there is nothing to repeat: it reads
|
||||
them from `install.conf` in the work dir, and any option you do pass wins over the saved one. It
|
||||
downloads the newest release, puts only the files that changed over the old source, rebuilds what
|
||||
they touch, and replaces the game on the Frame. Dawn is only rebuilt when a release changes its
|
||||
patches (the release notes say so), so an update usually takes minutes. When the Frame already has
|
||||
the newest release, and the newest Retro Rewind pack if you use one, it says so and builds nothing.
|
||||
|
||||
`--check` only reports whether an update is available. `--release <tag>` builds a given release
|
||||
instead. Running the whole install command again works as it always did.
|
||||
|
||||
### Updating from inside the game
|
||||
|
||||
An install built on the Frame itself (`--frame local`, [below](#other-ways-to-build)) can update
|
||||
itself: the settings panel grows an **Updates** tab with the release you are running and a **Check
|
||||
for updates** button, and when there is one, **Update now** builds it in the background. The tab
|
||||
shows each step and how far the build is while you keep playing (the game may stutter while it
|
||||
compiles), and says when to restart the game to use the update.
|
||||
|
||||
The update runs as a systemd user service (`wiicompiled-update.service`), behind the game in line
|
||||
for the processor, so it carries on if you close the game, and then opens the game again when it is
|
||||
done. Keep the Frame on its charger for it. Installed from a PC, the tab says so and the update is
|
||||
run there. Progress is not shown as Steam notifications: on the Frame, Steam receives them from
|
||||
SteamOS's notification service but does not display them.
|
||||
|
||||
## Recommended settings
|
||||
|
||||
@@ -314,7 +337,8 @@ curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/op
|
||||
```
|
||||
|
||||
It is native ARM64, so no emulation, but the Frame has less memory and cooling than a PC; keep it
|
||||
on its charger. The game then reads the disc straight from `~/wiicompiled-frame/disc`. By hand, the
|
||||
on its charger. The game then reads the disc straight from `~/wiicompiled-frame/disc`, and this is
|
||||
the install that can [update itself from inside the game](#updating-from-inside-the-game). By hand, the
|
||||
steps under [Building by hand](#building-by-hand) work the same in a container started without
|
||||
`--platform linux/arm64`, with `nodtool-linux-aarch64` in place of `nodtool-linux-x86_64`.
|
||||
|
||||
@@ -357,6 +381,9 @@ Unraid keeps `/usr/local/bin` in memory, so repeat this after a reboot.
|
||||
Vulkan hook and density map ABIs that Aurora compiles against.
|
||||
- **Headset only.** The game neither shows nor finishes rendering a desktop window, and is compiled
|
||||
for the Frame's Cortex-X4 cores (`-mcpu=cortex-x4`, overridable with `--cpu`).
|
||||
- **Tuned for ARM64.** The Frame's kernel uses 4 KiB memory pages, so the `steam_frame` build drops
|
||||
the page-size check from every translated memory access; it checks the page size once at startup
|
||||
and refuses to start if it is ever different. Audio mixing uses NEON on ARM64.
|
||||
- **Every refresh from the game.** The game draws 60 frames a second. When it handed SteamVR only
|
||||
those, SteamVR ran it at half rate and made up every other refresh itself, even with Motion
|
||||
Smoothing off. With `[vr] repeat_frames`, the pacing thread waits for the next frame until 1.5 ms
|
||||
@@ -384,8 +411,8 @@ play. [`docs/quest-port.md`](docs/quest-port.md) covers the Android build.
|
||||
|
||||
Open an issue on this repository with:
|
||||
- what you did and what you saw (which eye, where in the picture, racing or menus);
|
||||
- the run's `console.log` (and `crash_sigsegv.txt` after a crash) from
|
||||
`~/.local/share/WiiCompiled/Logs/` on the Frame;
|
||||
- the archive [`frame-diagnostics.sh`](#troubleshooting) makes, or at least the run's `console.log`
|
||||
(and `crash_sigsegv.txt` after a crash) from `~/.local/share/WiiCompiled/Logs/` on the Frame;
|
||||
- or, for a build problem, the last lines of the failing step.
|
||||
|
||||
Problems that also happen on a PC or a Quest belong upstream, in
|
||||
@@ -394,7 +421,10 @@ Problems that also happen on a PC or a Quest belong upstream, in
|
||||
## From upstream
|
||||
|
||||
The fork keeps everything [WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR) does;
|
||||
its README covers it in full. In the headset that means:
|
||||
its README covers it in full. On top of that it carries fixes taken from
|
||||
[WiiCompiled](https://github.com/patchzyy/Wiicompiled) itself and from other WiiCompiled forks, such
|
||||
as Linux TLS for online play, faster disc reads, safer settings saves and NEON audio mixing; each is
|
||||
listed with its author in [`CREDITS.md`](CREDITS.md). In the headset that means:
|
||||
|
||||
- Menus and unsupported scenes on a head-locked virtual screen, and races in immersive stereo.
|
||||
- A first-person cockpit whose steering wheel or handlebar turns with your steering, and hand
|
||||
@@ -436,13 +466,20 @@ sharing.
|
||||
|
||||
**Can it run on other SteamOS or Linux ARM64 devices?**
|
||||
Leave out `--headset steam_frame` and pass `--cpu` for your CPU to get a generic Linux VR build;
|
||||
it needs an OpenXR runtime with `XR_KHR_vulkan_enable2`. Untested.
|
||||
it needs an OpenXR runtime with `XR_KHR_vulkan_enable2`. Untested. The generic build also works on
|
||||
kernels with memory pages larger than 4 KiB, which the `steam_frame` build refuses.
|
||||
|
||||
## AI usage
|
||||
AI coding tools were used during development of this project.
|
||||
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
|
||||
|
||||
## Credits
|
||||
|
||||
This fork stands on other people's work: WiiCompiled by patchzyy, WiiCompiled OpenXR VR by iChris4,
|
||||
and fixes ported from heurazy's Wiicompiled_VR-PLUS, Chris Sotraidis's KartPad, DarthMDev,
|
||||
BlackAndBlue95's Strikers-WiiCompiled, rooklz and nx-mod's wiicompiled-nx. [`CREDITS.md`](CREDITS.md)
|
||||
says what came from whom. In short:
|
||||
|
||||
- **inkwreck** - making the logo
|
||||
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
|
||||
project's whole graphics layer sits on. MIT licensed.
|
||||
@@ -463,8 +500,8 @@ All translated output is verified against real hardware behavior and most import
|
||||
- **[Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard)** - the mod manager this
|
||||
project integrates with as a launch backend.
|
||||
- **[nod](https://github.com/encounter/nod)** - nodtool, the disc image extractor.
|
||||
- **[DolphinXR](https://github.com/iChris4/dolphinXR)** - the Steam Frame controller profile's
|
||||
input paths.
|
||||
- **[DolphinXR](https://github.com/iChris4/dolphinXR)** - the OpenXR Wii Remote input design, and
|
||||
the Steam Frame controller profile's input paths.
|
||||
- **[Frame Control](https://github.com/saphid/frame-control)** by saphid - installing the game into
|
||||
the Frame's Steam library, and its notes on how the Frame's software fits together.
|
||||
- Everyone in the static recompilation community.
|
||||
|
||||
+18
-3
@@ -88,6 +88,19 @@ wheel geometry and guest reads in `runtime/src/vr/mkw_vr_first_person.cpp` and
|
||||
(`runtime/include/physical_wheel.h`, `runtime/src/physical_wheel.cpp` and their test). The files
|
||||
carry that attribution in their headers.
|
||||
|
||||
### Mozilla CA certificate bundle - MPL-2.0
|
||||
|
||||
The root certificates Mbed TLS verifies servers against on Linux, macOS and Android
|
||||
(`runtime/assets/certs/cacert.pem`), copied next to the built game. Extracted from Mozilla's
|
||||
`certdata.txt` and redistributed unmodified as curl's CA bundle.
|
||||
Source: <https://curl.se/docs/caextract.html>
|
||||
|
||||
### Code ported from other WiiCompiled forks - GPL-3.0-or-later
|
||||
|
||||
Fixes taken from other GPL-3.0 forks of WiiCompiled (heurazy's Wiicompiled_VR-PLUS, KartPad,
|
||||
DarthMDev, Strikers-WiiCompiled, rooklz and wiicompiled-nx) are listed with their authors and
|
||||
source commits in [`CREDITS.md`](CREDITS.md).
|
||||
|
||||
### pugixml - MIT
|
||||
|
||||
Copyright (c) 2006-2025 Arseny Kapoulkine.
|
||||
@@ -131,14 +144,16 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt`,
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`, and (for Mbed TLS) `runtime/CMakeLists.txt`. They are
|
||||
not stored in this repository; the build downloads them - each fetch is pinned to an exact version
|
||||
with a checked SHA-256 - and links or redistributes the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
|
||||
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
|
||||
|
||||
| Component | Version | License | Upstream |
|
||||
| --- | --- | --- | --- |
|
||||
| Mbed TLS | 3.6.7 | Apache-2.0 / GPL-2.0-or-later | <https://github.com/Mbed-TLS/mbedtls> |
|
||||
| Dawn (WebGPU) | `v20260603.191052` prebuilt | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
|
||||
|
||||
@@ -28,6 +28,7 @@ val prepareRuntimeResources by tasks.registering(Copy::class) {
|
||||
from(File(assets, "wii")) { into("runtime_resources/wii_bootstrap") }
|
||||
from(File(assets, "dsp/dsp_coef.bin")) { into("runtime_resources") }
|
||||
from(File(assets, "pipeline/initial_pipeline_cache.db")) { into("runtime_resources") }
|
||||
from(File(assets, "certs/cacert.pem")) { into("runtime_resources") }
|
||||
into(runtimeResources)
|
||||
}
|
||||
|
||||
|
||||
@@ -295,12 +295,20 @@ typedef struct {
|
||||
bool xrHeadsetOnly;
|
||||
} AuroraConfig;
|
||||
|
||||
typedef enum {
|
||||
AURORA_INITIALIZATION_SUCCESS = 0,
|
||||
AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE = 1,
|
||||
} AuroraInitializationStatus;
|
||||
|
||||
typedef struct {
|
||||
AuroraBackend backend;
|
||||
const char* userPath;
|
||||
const char* cachePath;
|
||||
SDL_Window* window;
|
||||
AuroraWindowSize windowSize;
|
||||
AuroraInitializationStatus initializationStatus;
|
||||
// On failure, owned by SDL on the calling thread. Copy before another SDL call.
|
||||
const char* initializationError;
|
||||
} AuroraInfo;
|
||||
|
||||
AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config);
|
||||
|
||||
@@ -1229,15 +1229,23 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
const AuroraBackend requestedBackend = config.desiredBackend;
|
||||
AuroraBackend selectedBackend = requestedBackend;
|
||||
bool windowCreated = false;
|
||||
std::string firstGraphicsError;
|
||||
const auto rememberGraphicsError = [&] {
|
||||
if (firstGraphicsError.empty() && SDL_GetError()[0] != '\0') {
|
||||
firstGraphicsError = SDL_GetError();
|
||||
}
|
||||
};
|
||||
if (selectedBackend != BACKEND_AUTO) {
|
||||
Log.info("Requested graphics backend: {}", backend_name(selectedBackend));
|
||||
if (window::create_window(selectedBackend)) {
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
Log.error("Failed to create a window for backend {}: {}", backend_name(selectedBackend), SDL_GetError());
|
||||
}
|
||||
if (!windowCreated) {
|
||||
@@ -1254,18 +1262,28 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
for (const auto backendType : PreferredBackendOrder) {
|
||||
selectedBackend = backendType;
|
||||
if (!window::create_window(selectedBackend)) {
|
||||
rememberGraphicsError();
|
||||
continue;
|
||||
}
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
break;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT(windowCreated, "Error creating window: {}", SDL_GetError());
|
||||
if (!windowCreated) {
|
||||
if (firstGraphicsError.empty()) firstGraphicsError = "No supported graphics backend is available";
|
||||
SDL_SetError("%s", firstGraphicsError.c_str());
|
||||
Log.error("Graphics initialization failed: {}", firstGraphicsError);
|
||||
return {
|
||||
.initializationStatus = AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE,
|
||||
.initializationError = SDL_GetError(),
|
||||
};
|
||||
}
|
||||
if (requestedBackend != BACKEND_AUTO && selectedBackend != requestedBackend) {
|
||||
Log.error(
|
||||
"Graphics backend fallback in effect: video.graphics_api requested {}, "
|
||||
|
||||
@@ -399,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
|
||||
}
|
||||
|
||||
void PADSetPortForIndex(const u32 idx, const u32 port) {
|
||||
if (port >= PAD_MAX_CONTROLLERS) return;
|
||||
const auto* ctrl = __PADGetControllerForIndex(idx);
|
||||
if (ctrl == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
|
||||
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
|
||||
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
|
||||
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
|
||||
aurora::input::set_player_index(dest->m_index, -1);
|
||||
}
|
||||
if (oldPort >= 0 && oldPort != port) {
|
||||
aurora::input::persist_controller_for_player(oldPort, nullptr);
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
|
||||
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
|
||||
aurora::input::persist_controller_for_player(port, ctrl);
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ void PADClearPort(const u32 port) {
|
||||
if (ctrl == nullptr) {
|
||||
return;
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
|
||||
aurora::input::set_player_index(ctrl->m_index, -1);
|
||||
}
|
||||
|
||||
// Secondary bindings live only in memory; the runtime re-applies them from its
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::vi {
|
||||
std::optional<GXRenderModeObj> g_renderMode;
|
||||
namespace {
|
||||
std::atomic<float> g_presentAspectCorrection{1.f};
|
||||
std::mutex g_renderModeMutex;
|
||||
|
||||
float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
if (rm.viWidth == 0 || rm.viHeight == 0) {
|
||||
@@ -29,9 +31,8 @@ float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
const float verticalFill = static_cast<float>(rm.viHeight) / nominalActiveHeight;
|
||||
return horizontalFill / verticalFill;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
Vec2<uint32_t> render_mode_size_locked() noexcept {
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
@@ -40,18 +41,31 @@ Vec2<uint32_t> render_mode_size() noexcept {
|
||||
return {std::max<uint32_t>(g_renderMode->fbWidth, 640), std::max<uint32_t>(g_renderMode->efbHeight, 528)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
return render_mode_size_locked();
|
||||
}
|
||||
|
||||
void configure(const GXRenderModeObj* rm) noexcept {
|
||||
const auto oldSize = render_mode_size();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
bool sizeChanged = false;
|
||||
{
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
const auto oldSize = render_mode_size_locked();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
sizeChanged = render_mode_size_locked() != oldSize;
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
if (render_mode_size() != oldSize) {
|
||||
// Never hold the mode lock across a resize request or a renderer callback.
|
||||
if (sizeChanged) {
|
||||
window::request_frame_buffer_resize();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,7 @@ Vec2<uint32_t> configured_fb_size() noexcept {
|
||||
}
|
||||
|
||||
Vec2<uint32_t> visible_fb_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common.hpp"
|
||||
#include "staging_map.hpp"
|
||||
#include "../gx/shader_info.hpp"
|
||||
|
||||
#include "clear.hpp"
|
||||
@@ -139,12 +140,7 @@ wgpu::Buffer g_storageBuffer;
|
||||
constexpr size_t FrameSlotCount = 3;
|
||||
static std::array<wgpu::Buffer, FrameSlotCount> g_stagingBuffers;
|
||||
static size_t currentStagingBuffer = 0;
|
||||
enum class BufferMapState {
|
||||
Unmapped,
|
||||
Mapping,
|
||||
Mapped,
|
||||
};
|
||||
static std::atomic s_mappingState{BufferMapState::Unmapped};
|
||||
static StagingMapState s_mappingState;
|
||||
static wgpu::Limits g_cachedLimits;
|
||||
// Advanced once per logical frame in the seal prologue, under the renderer GPU mutex and with the
|
||||
// producer blocked, so every later reader sees a value that no longer moves.
|
||||
@@ -405,7 +401,7 @@ static size_t g_recordingSnapshotSlot = 0;
|
||||
static TextureHandle new_resolve_source_snapshot(wgpu::Extent3D size, wgpu::TextureFormat format) noexcept {
|
||||
const wgpu::TextureDescriptor textureDescriptor{
|
||||
.label = "GX Copy Source Snapshot",
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst,
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst,
|
||||
.dimension = wgpu::TextureDimension::e2D,
|
||||
.size = size,
|
||||
.format = format,
|
||||
@@ -1048,7 +1044,7 @@ void initialize() {
|
||||
label.c_str());
|
||||
}
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
map_staging_buffer();
|
||||
|
||||
{
|
||||
@@ -1160,6 +1156,8 @@ void shutdown() {
|
||||
g_uniformBuffer = {};
|
||||
g_indexBuffer = {};
|
||||
g_storageBuffer = {};
|
||||
// Invalidate outstanding callbacks before releasing their buffers.
|
||||
s_mappingState.reset();
|
||||
g_stagingBuffers.fill({});
|
||||
for (auto& pool : g_resolveSourceSnapshotPools) {
|
||||
pool.entry.reset();
|
||||
@@ -1178,27 +1176,25 @@ void shutdown() {
|
||||
g_inOffscreen = false;
|
||||
g_frameIndex = UINT32_MAX;
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
}
|
||||
|
||||
void map_staging_buffer() {
|
||||
auto expected = BufferMapState::Unmapped;
|
||||
if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
const auto generation = s_mappingState.request();
|
||||
if (generation == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_stagingBuffers[currentStagingBuffer].MapAsync(
|
||||
wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous,
|
||||
[](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
[generation](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
const auto result = status == wgpu::MapAsyncStatus::Success ? BufferMapState::Mapped : BufferMapState::Unmapped;
|
||||
if (!s_mappingState.complete(generation, result)) return;
|
||||
if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) {
|
||||
Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message);
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status),
|
||||
message);
|
||||
s_mappingState.store(BufferMapState::Mapped, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1208,7 +1204,7 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
ZoneScopedN("Wait for buffer map");
|
||||
map_staging_buffer();
|
||||
while (true) {
|
||||
const auto mappingState = s_mappingState.load(std::memory_order_acquire);
|
||||
const auto mappingState = s_mappingState.state();
|
||||
if (mappingState == BufferMapState::Mapped) {
|
||||
break;
|
||||
}
|
||||
@@ -1224,6 +1220,9 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
return false;
|
||||
}
|
||||
g_instance.ProcessEvents();
|
||||
webgpu::fail_if_device_lost();
|
||||
// Sleep until the map callback lands (or 1 ms passes) instead of spinning a core on ProcessEvents.
|
||||
s_mappingState.wait_for_progress();
|
||||
}
|
||||
}
|
||||
g_recordingSnapshotSlot = currentStagingBuffer;
|
||||
@@ -1296,12 +1295,12 @@ void abort_frame() noexcept {
|
||||
g_textureUploads.clear();
|
||||
g_textureUpload.release();
|
||||
}
|
||||
if (s_mappingState.load(std::memory_order_acquire) == BufferMapState::Mapped) {
|
||||
if (s_mappingState.state() == BufferMapState::Mapped) {
|
||||
// Pending interpolation tasks hold raw pointers into the mapped staging
|
||||
// range; they must be dropped before the buffer is unmapped and rotated.
|
||||
gx::drop_pending_frame_interpolation_uniforms();
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size();
|
||||
map_staging_buffer();
|
||||
}
|
||||
@@ -1741,7 +1740,7 @@ static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame,
|
||||
g_uniformUploadDestination = nullptr;
|
||||
}
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
g_stats.drawCallCount = g_drawCallCount;
|
||||
g_stats.mergedDrawCallCount = g_mergedDrawCallCount;
|
||||
g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex");
|
||||
|
||||
@@ -109,8 +109,9 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
|
||||
*cache = pending.nativeTexture;
|
||||
}
|
||||
}
|
||||
// The shared blit shader clamps Y to flags.z/w; preserve the full source.
|
||||
const std::array nativeBlitUniform{
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
|
||||
};
|
||||
pending.nativeBlitUniform = push_uniform(nativeBlitUniform);
|
||||
}
|
||||
|
||||
@@ -426,6 +426,7 @@ static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize
|
||||
|
||||
g_priorityPipelines.emplace_back(std::move(*backgroundIt));
|
||||
g_backgroundPipelines.erase(backgroundIt);
|
||||
g_pipelineCv.notify_all();
|
||||
return &g_priorityPipelines.back();
|
||||
}
|
||||
|
||||
@@ -566,7 +567,8 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
|
||||
}
|
||||
|
||||
if (notifyWorker) {
|
||||
g_pipelineCv.notify_one();
|
||||
// Compiler workers and renderer waiters share this condition variable.
|
||||
g_pipelineCv.notify_all();
|
||||
}
|
||||
if (notifyWaiters) {
|
||||
g_pipelineCv.notify_all();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::gfx {
|
||||
|
||||
enum class BufferMapState { Unmapped, Mapping, Mapped };
|
||||
|
||||
// The renderer owns request/reset; Dawn may complete a request on another thread.
|
||||
// An old callback must never publish readiness for a different staging slot.
|
||||
class StagingMapState {
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable changed_;
|
||||
uint64_t generation_ = 0;
|
||||
BufferMapState state_ = BufferMapState::Unmapped;
|
||||
|
||||
public:
|
||||
uint64_t request() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (state_ != BufferMapState::Unmapped) return 0;
|
||||
state_ = BufferMapState::Mapping;
|
||||
return ++generation_;
|
||||
}
|
||||
|
||||
bool complete(uint64_t generation, BufferMapState state) {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (generation != generation_ || state_ != BufferMapState::Mapping) return false;
|
||||
state_ = state;
|
||||
}
|
||||
changed_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
++generation_;
|
||||
state_ = BufferMapState::Unmapped;
|
||||
}
|
||||
changed_.notify_all();
|
||||
}
|
||||
|
||||
BufferMapState state() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return state_;
|
||||
}
|
||||
|
||||
void wait_for_progress() {
|
||||
std::unique_lock lock(mutex_);
|
||||
// ProcessEvents is still serviced between waits for implementations that
|
||||
// need it. A spontaneous completion wakes immediately, without polling.
|
||||
changed_.wait_for(lock, std::chrono::milliseconds(1), [&] { return state_ != BufferMapState::Mapping; });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace aurora::gfx
|
||||
@@ -422,7 +422,7 @@ static wgpu::BindGroupLayout g_depthBindGroupLayout;
|
||||
static wgpu::Sampler g_nearestSampler;
|
||||
static wgpu::Sampler g_linearSampler;
|
||||
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
|
||||
static wgpu::RenderPipeline g_blitPipeline;
|
||||
static absl::flat_hash_map<wgpu::TextureFormat, wgpu::RenderPipeline> g_blitPipelines;
|
||||
static wgpu::RenderPipeline g_quest1BlitPipeline;
|
||||
|
||||
static bool quest1_simple_blit() noexcept {
|
||||
@@ -549,9 +549,15 @@ void initialize() {
|
||||
};
|
||||
g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor);
|
||||
|
||||
g_blitPipeline = create_pipeline(
|
||||
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
|
||||
ShaderPreamble, g_bindGroupLayout);
|
||||
// Native RAM readback uses RGBA even when the EFB/surface uses BGRA.
|
||||
// Build both variants here; frame workers only read the completed map.
|
||||
for (const auto format : {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm,
|
||||
webgpu::g_graphicsConfig.surfaceConfiguration.format}) {
|
||||
if (format != wgpu::TextureFormat::Undefined && !g_blitPipelines.contains(format)) {
|
||||
g_blitPipelines[format] = create_pipeline({GX_TF_RGBA8, FragPassthrough, format, "TexCopyConv Blit"},
|
||||
ShaderPreamble, g_bindGroupLayout);
|
||||
}
|
||||
}
|
||||
g_quest1BlitPipeline = create_pipeline(
|
||||
{GX_TF_RGBA8, {}, webgpu::g_graphicsConfig.surfaceConfiguration.format, "Quest 1 Simple TexCopy Blit"},
|
||||
SimpleBlitShader, g_bindGroupLayout);
|
||||
@@ -585,7 +591,7 @@ void initialize() {
|
||||
|
||||
void shutdown() {
|
||||
g_pipelines.clear();
|
||||
g_blitPipeline = {};
|
||||
g_blitPipelines.clear();
|
||||
g_quest1BlitPipeline = {};
|
||||
g_bindGroupLayout = {};
|
||||
g_depthBindGroupLayout = {};
|
||||
@@ -678,7 +684,15 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
}
|
||||
|
||||
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
execute(cmd, req, quest1_simple_blit() ? g_quest1BlitPipeline : g_blitPipeline);
|
||||
if (quest1_simple_blit()) {
|
||||
execute(cmd, req, g_quest1BlitPipeline);
|
||||
return;
|
||||
}
|
||||
const auto it = g_blitPipelines.find(req.dst->format);
|
||||
if (it == g_blitPipelines.end()) {
|
||||
Log.fatal("Unsupported blit destination format {}", static_cast<int>(req.dst->format));
|
||||
}
|
||||
execute(cmd, req, it->second);
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::tex_copy_conv
|
||||
@@ -33,10 +33,11 @@ using IndexBuffer = std::vector<u16>;
|
||||
static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) {
|
||||
size_t writePos = 0;
|
||||
if (prim == GX_QUADS) {
|
||||
// Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad.
|
||||
buf.resize(((static_cast<u32>(vtxCount) + 3u) / 4u) * 6u);
|
||||
// GX renders a three-vertex remainder as a triangle. One/two are ignored.
|
||||
const u32 completeVertices = static_cast<u32>(vtxCount) & ~3u;
|
||||
buf.resize((completeVertices / 4u) * 6u + (vtxCount % 4u == 3u ? 3u : 0u));
|
||||
|
||||
for (u16 v = 0; v < vtxCount; v += 4) {
|
||||
for (u32 v = 0; v < completeVertices; v += 4) {
|
||||
const u16 idx0 = v;
|
||||
const u16 idx1 = static_cast<u16>(v + 1);
|
||||
const u16 idx2 = static_cast<u16>(v + 2);
|
||||
@@ -48,15 +49,21 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = idx3;
|
||||
buf[writePos++] = idx0;
|
||||
}
|
||||
if (vtxCount % 4u == 3u) {
|
||||
buf[writePos++] = static_cast<u16>(completeVertices);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 1u);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 2u);
|
||||
}
|
||||
} else if (prim == GX_TRIANGLES) {
|
||||
buf.resize(vtxCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
const u32 completeVertices = (static_cast<u32>(vtxCount) / 3u) * 3u;
|
||||
buf.resize(completeVertices);
|
||||
for (u32 v = 0; v < completeVertices; ++v) {
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLEFAN) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -66,9 +73,9 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLESTRIP) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -91,6 +98,13 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
return static_cast<u32>(writePos);
|
||||
}
|
||||
|
||||
// Empty/incomplete draws consume FIFO bytes but cannot produce a primitive.
|
||||
static bool has_complete_primitive(GXPrimitive prim, u16 count) {
|
||||
if (prim == GX_POINTS) return count >= 1;
|
||||
if (prim == GX_LINES || prim == GX_LINESTRIP) return count >= 2;
|
||||
return count >= 3;
|
||||
}
|
||||
|
||||
// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros
|
||||
static constexpr u8 CP_CMD_NOP = GX_NOP;
|
||||
static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG;
|
||||
@@ -552,6 +566,10 @@ void process(const u8* data, u32 size, bool bigEndian) {
|
||||
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
|
||||
g_gxState.arrays[i].cachedRange = {};
|
||||
}
|
||||
// A merged draw retains its previous array uploads. Force a new draw so
|
||||
// handle_draw_unmerged observes the invalidation and uploads fresh data.
|
||||
// Pipeline configuration itself did not change.
|
||||
g_gxState.stateDirty = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1926,6 +1944,10 @@ static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
|
||||
|
||||
g_gxState.lastVtxFmt = fmt;
|
||||
g_gxState.lastVtxSize = vtxSize;
|
||||
// The format is selected by the draw opcode, without a register write.
|
||||
// Even equal-stride formats may decode bytes differently, so do not merge
|
||||
// into a draw using the previous format's shader and uniform layout.
|
||||
g_gxState.stateDirty = true;
|
||||
|
||||
return vtxSize;
|
||||
}
|
||||
@@ -2366,6 +2388,8 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!has_complete_primitive(prim, vtxCount)) return true;
|
||||
|
||||
// This entry point bypasses process(), so it owns the renderer lock itself.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
if (model_array_hidden()) return true;
|
||||
@@ -2414,7 +2438,7 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
return false;
|
||||
}
|
||||
|
||||
if (model_array_hidden()) {
|
||||
if (!has_complete_primitive(prim, vtxCount) || model_array_hidden()) {
|
||||
pos += totalVtxBytes;
|
||||
return true;
|
||||
}
|
||||
@@ -2436,9 +2460,12 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
// Only if the previous draw call was a single instance draw (no lines/points handling), and only into a draw
|
||||
// that resolved the same animated array: the merged whole renders through that draw's binding. Anything the
|
||||
// decision cache cannot vouch for (a command it was not recorded against) stays unmerged.
|
||||
// Expanded lines/points have different vertex interpretation even with one instance.
|
||||
// Triangle-list output has no restart index; index 65535 is usable.
|
||||
// Overflow would address earlier vertices instead of the appended geometry.
|
||||
if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
|
||||
!lastDraw->uniformReplayLayout.vertexMotion.enabled &&
|
||||
lastDraw->instanceCount == 1 &&
|
||||
!lastDraw->uniformReplayLayout.vertexMotion.enabled && !lastDraw->expandedPrimitive &&
|
||||
lastDraw->instanceCount == 1 && uint64_t(lastDraw->vtxCount) + vtxCount <= 65536u &&
|
||||
(nativeWheelArrays.empty() ||
|
||||
(nativeWheelLastDrawCommand == lastDraw && nativeWheelLastDecision == nativeWheel)))
|
||||
LIKELY {
|
||||
@@ -2615,6 +2642,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g
|
||||
.vtxCount = vtxCount,
|
||||
.indexCount = numIndices,
|
||||
.instanceCount = instanceCount,
|
||||
.expandedPrimitive = prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS,
|
||||
.bindGroups = bindGroups,
|
||||
.dstAlpha = pipelineState.dstAlpha,
|
||||
.screenRect = screen_rect(prim, fmt, vertices, vtxCount, vtxStride),
|
||||
|
||||
@@ -1910,7 +1910,8 @@ void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXV
|
||||
config.pixelFmt = g_gxState.pixelFmt;
|
||||
config.dstAlpha = effective_dst_alpha(g_gxState.pixelFmt, g_gxState.alphaUpdate, g_gxState.dstAlpha);
|
||||
config.depthCompare = g_gxState.depthCompare;
|
||||
config.depthUpdate = g_gxState.depthUpdate;
|
||||
// As on the hardware (and in Dolphin): with the Z compare off, the Z buffer isn't updated either.
|
||||
config.depthUpdate = g_gxState.depthCompare && g_gxState.depthUpdate;
|
||||
config.alphaUpdate = effective_alpha_update(g_gxState.pixelFmt, g_gxState.alphaUpdate);
|
||||
config.colorUpdate = g_gxState.colorUpdate;
|
||||
}
|
||||
|
||||
@@ -439,6 +439,8 @@ struct GXState {
|
||||
u32 pipelineStateGeneration = next_gx_state_epoch();
|
||||
std::array<u32, 0x100> bpRegCache = [] {
|
||||
std::array<u32, 0x100> regs{};
|
||||
// Force the first GEN_MODE decode without changing its masked reset value.
|
||||
regs[0x00] = 0xFF000000;
|
||||
regs[0xFE] = 0x00FFFFFF;
|
||||
return regs;
|
||||
}();
|
||||
|
||||
@@ -29,6 +29,7 @@ struct DrawData {
|
||||
uint32_t vtxCount;
|
||||
uint32_t indexCount;
|
||||
uint32_t instanceCount;
|
||||
bool expandedPrimitive;
|
||||
GXBindGroups bindGroups;
|
||||
uint32_t dstAlpha;
|
||||
// Valid only for simple orthographic rectangles/lines (textured or not).
|
||||
|
||||
@@ -1739,8 +1739,22 @@ fn load_u16(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
return bswap16(raw, le);
|
||||
}}
|
||||
|
||||
fn load_u24_raw(p: ptr<storage, array<u32>>, byte_off: u32) -> u32 {{
|
||||
let word_idx = byte_off >> 2u;
|
||||
let sub = byte_off & 3u;
|
||||
let word = p[word_idx];
|
||||
// Three bytes at offsets zero or one fit entirely in this word. Do not
|
||||
// access the next word: this attribute may end at the binding boundary.
|
||||
if (sub <= 1u) {{
|
||||
return (word >> (sub * 8u)) & 0x00FFFFFFu;
|
||||
}}
|
||||
let next = p[word_idx + 1u];
|
||||
let shift = sub * 8u;
|
||||
return ((word >> shift) | (next << (32u - shift))) & 0x00FFFFFFu;
|
||||
}}
|
||||
|
||||
fn load_u24(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu;
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
if (le) {{
|
||||
return raw;
|
||||
}}
|
||||
@@ -1780,7 +1794,7 @@ fn raw_fetch_u8_2(p: ptr<storage, array<u32>>, byte_off: u32) -> vec2u {{
|
||||
}}
|
||||
|
||||
fn raw_fetch_u8_3(p: ptr<storage, array<u32>>, byte_off: u32) -> vec3u {{
|
||||
let raw = load_u32_raw(p, byte_off);
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
return vec3u(
|
||||
extractBits(raw, 0u, 8u),
|
||||
extractBits(raw, 8u, 8u),
|
||||
|
||||
@@ -85,18 +85,30 @@ void initialize() noexcept {
|
||||
|
||||
void shutdown() noexcept {
|
||||
ZoneScoped;
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
// Startup can fail before either backend initializes. A context alone does
|
||||
// not mean its renderer/platform backend owns resources to release.
|
||||
if (ImGui::GetCurrentContext() != nullptr) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.BackendRendererUserData != nullptr) {
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
}
|
||||
}
|
||||
if (io.BackendPlatformUserData != nullptr) {
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
}
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
for (const auto& texture : g_sdlTextures) {
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
g_sdlTextures.clear();
|
||||
g_wgpuTextures.clear();
|
||||
g_useSdlRenderer = false;
|
||||
g_scale = 0.f;
|
||||
g_frameDataBuilt = true;
|
||||
}
|
||||
|
||||
void process_event(const SDL_Event& event) noexcept {
|
||||
@@ -209,9 +221,13 @@ void render(const wgpu::RenderPassEncoder& pass) noexcept {
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(data, renderer);
|
||||
SDL_RenderPresent(renderer);
|
||||
} else {
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(data, pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +291,13 @@ void render(const wgpu::RenderPassEncoder& pass, const ImDrawData* data) noexcep
|
||||
if (g_useSdlRenderer || data == nullptr) {
|
||||
return;
|
||||
}
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(const_cast<ImDrawData*>(data), pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
}
|
||||
|
||||
StereoOverlay latch_stereo_overlay() noexcept {
|
||||
@@ -291,9 +311,13 @@ bool render_draw_data(const wgpu::RenderPassEncoder& pass, ImDrawData* data) noe
|
||||
if (g_useSdlRenderer || data == nullptr || ImGui::GetCurrentContext() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui headset panel");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(data, pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
g_GameControllers[instance] = controller;
|
||||
ensure_player_index(g_GameControllers[instance]);
|
||||
apply_port_preferences();
|
||||
#if defined(SDL_PLATFORM_MACOS)
|
||||
// First-use convenience only: never override a saved assignment or None.
|
||||
if (g_portPreferences[0].state == PortPreferenceState::Unset) {
|
||||
bool hasOtherPortPreference = false;
|
||||
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
|
||||
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
|
||||
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
|
||||
IdentityMatch::None) {
|
||||
hasOtherPortPreference = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasOtherPortPreference) {
|
||||
const auto* p0 = get_controller_for_player(0);
|
||||
if (p0 == nullptr) {
|
||||
assign_player_index(g_GameControllers[instance], 0);
|
||||
persist_controller_for_player(0, &g_GameControllers[instance]);
|
||||
} else if (p0 == &g_GameControllers[instance]) {
|
||||
persist_controller_for_player(0, &g_GameControllers[instance]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -627,12 +627,16 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_adapter = std::move(adapter);
|
||||
} else {
|
||||
Log.warn("Adapter request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics adapter unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create {} adapter: {}", magic_enum::enum_name(backend),
|
||||
magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics adapter request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_adapter) {
|
||||
@@ -845,11 +849,16 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_device = std::move(device);
|
||||
} else {
|
||||
Log.warn("Device request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics device unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)),
|
||||
reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create device: {}", magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics device request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_device) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
@@ -313,8 +314,33 @@ static size_t load_from_database(const XXH128_hash_t& keyHash, void* value, size
|
||||
if (ret == SQLITE_ROW) {
|
||||
// Hit
|
||||
const auto foundPtr = sqlite3_column_blob(load_stmt, 0);
|
||||
foundSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const bool compressed = sqlite3_column_int(load_stmt, 2) != 0;
|
||||
const auto declaredSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const auto storedSize = sqlite3_column_bytes(load_stmt, 0);
|
||||
const auto compression = sqlite3_column_int(load_stmt, 2);
|
||||
const bool compressed = compression == 1;
|
||||
// Dawn asks for the size before allocating its destination. Validate here,
|
||||
// not only during the copy: corrupt metadata must become a cache miss.
|
||||
bool valid = declaredSize > 0 &&
|
||||
static_cast<uint64_t>(declaredSize) <= std::numeric_limits<size_t>::max() &&
|
||||
foundPtr != nullptr && storedSize > 0 && (compression == 0 || compression == 1);
|
||||
if (valid && compressed) {
|
||||
#if defined(AURORA_CACHE_USE_ZSTD)
|
||||
// Our writer uses ZSTD_compress, which records the original content size.
|
||||
const auto frameSize = ZSTD_getFrameContentSize(foundPtr, static_cast<size_t>(storedSize));
|
||||
valid = frameSize != ZSTD_CONTENTSIZE_ERROR && frameSize != ZSTD_CONTENTSIZE_UNKNOWN &&
|
||||
frameSize == static_cast<uint64_t>(declaredSize);
|
||||
#else
|
||||
valid = false;
|
||||
#endif
|
||||
} else if (valid) {
|
||||
valid = declaredSize == storedSize;
|
||||
}
|
||||
if (!valid) {
|
||||
Log.error("Ignoring cache entry with inconsistent size or compression metadata");
|
||||
check(sqlite3_reset(load_stmt));
|
||||
return 0;
|
||||
}
|
||||
foundSize = static_cast<size_t>(declaredSize);
|
||||
if (value == nullptr) {
|
||||
g_hits.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
|
||||
@@ -64,6 +64,7 @@ if (AURORA_ENABLE_GX)
|
||||
add_executable(gx_fifo_tests
|
||||
gx_fifo_test.cpp
|
||||
gx_test_stubs.cpp
|
||||
renderer_regression_test.cpp
|
||||
stereo_replay_test.cpp
|
||||
stereo_interpolation_test.cpp
|
||||
stereo_mirror_test.cpp
|
||||
|
||||
@@ -1328,12 +1328,35 @@ TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue)
|
||||
auto config = baseline;
|
||||
config.tevStages[0].colorPass.a = GX_CC_C0;
|
||||
config.tevStages[0].alphaPass.a = GX_CA_A0;
|
||||
config.tevStages[0].colorPass.b = GX_CC_KONST;
|
||||
config.tevStages[0].kcSel = GX_TEV_KCSEL_K0;
|
||||
|
||||
const auto baselineInfo = aurora::gx::build_shader_info(baseline);
|
||||
const auto info = aurora::gx::build_shader_info(config);
|
||||
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0));
|
||||
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
|
||||
EXPECT_EQ(info.uniformSize, baselineInfo.uniformSize + sizeof(aurora::Vec4<float>));
|
||||
// The final allocation is alignment-rounded, so adding one register need
|
||||
// not increase it. Verify actual packing with a distinct following K color.
|
||||
const auto savedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto savedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = {11.f, 22.f, 33.f, 44.f};
|
||||
g_gxState.kcolors[GX_KCOLOR0] = {55.f, 66.f, 77.f, 88.f};
|
||||
EXPECT_TRUE(info.sampledKColors.test(GX_KCOLOR0));
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
aurora::gx::build_uniform(info, 0, {}, {}, false);
|
||||
const auto expectedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto expectedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = savedReg;
|
||||
g_gxState.kcolors[GX_KCOLOR0] = savedKColor;
|
||||
const auto& bytes = aurora::gfx::testing::uniform_allocation(0);
|
||||
const auto* reg = reinterpret_cast<const uint8_t*>(&expectedReg);
|
||||
const auto found = std::search(bytes.begin(), bytes.end(), reg, reg + sizeof(aurora::Vec4<float>));
|
||||
ASSERT_NE(found, bytes.end());
|
||||
const size_t offset = static_cast<size_t>(found - bytes.begin());
|
||||
ASSERT_LE(offset + 2 * sizeof(aurora::Vec4<float>), bytes.size());
|
||||
EXPECT_EQ(std::memcmp(bytes.data() + offset + sizeof(aurora::Vec4<float>), &expectedKColor,
|
||||
sizeof(aurora::Vec4<float>)),
|
||||
0);
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
}
|
||||
|
||||
// BP registers (direct FIFO writes, no dirty state flush needed)
|
||||
@@ -1390,6 +1413,51 @@ TEST_F(GXFifoTest, BlendMode_Logic) {
|
||||
EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstZeroWriteDecodesAndRepeatDeduplicates) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_EQ(g_gxState.numChans, 0u);
|
||||
EXPECT_EQ(g_gxState.numTexGens, 0u);
|
||||
EXPECT_EQ(g_gxState.numIndStages, 0u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
const auto decoded = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.pipelineStateGeneration, decoded);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstMaskedWritePreservesZeroResetBits) {
|
||||
for (const u32 mask : {0u, 1u << 10}) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0xFE, mask));
|
||||
decode_fifo(bp_cmd(0, 0xFFFFFF));
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], mask);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0xFFFFFFu);
|
||||
EXPECT_EQ(g_gxState.numTevStages, mask ? 2u : 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_ColdSingleStageApiSetupDecodes) {
|
||||
reset_gx_state();
|
||||
GXSetNumTevStages(1);
|
||||
GXSetNumTexGens(0);
|
||||
GXSetNumChans(0);
|
||||
GXSetCullMode(GX_CULL_NONE);
|
||||
const auto bytes = flush_and_capture();
|
||||
decode_fifo(bytes);
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) {
|
||||
std::vector<u8> bytes;
|
||||
auto mask = bp_cmd(0xFE, 1u << 19);
|
||||
@@ -2861,6 +2929,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) {
|
||||
std::vector<u8> fifo;
|
||||
append_test_draw(fifo, primitive, count);
|
||||
aurora::gfx::testing::reset_vertex_push_record();
|
||||
decode_fifo(fifo);
|
||||
return aurora::gfx::testing::last_pushed_indices();
|
||||
};
|
||||
@@ -2871,7 +2940,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 5), (std::vector<u16>{0, 1, 2, 0, 2, 3, 0, 3, 4}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), (std::vector<u16>{0, 1}));
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), (std::vector<u16>{}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6), (std::vector<u16>{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5}));
|
||||
g_gxState.stateDirty = true;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "gx_test_common.hpp"
|
||||
#include "gfx/staging_map.hpp"
|
||||
#include "gx/pipeline.hpp"
|
||||
|
||||
#include <thread>
|
||||
|
||||
using aurora::gx::g_gxState;
|
||||
|
||||
namespace {
|
||||
std::vector<u8> draw(GXPrimitive primitive, u16 count, GXVtxFmt format = GX_VTXFMT0) {
|
||||
std::vector<u8> bytes{static_cast<u8>(primitive | format), static_cast<u8>(count >> 8), static_cast<u8>(count)};
|
||||
bytes.resize(3 + count);
|
||||
return bytes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(GXFifoTest, MaximumQuadCountTerminatesWithoutOutOfRangeIndices) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
for (const u16 count : {65532, 65533, 65534, 65535}) {
|
||||
g_gxState.stateDirty = true;
|
||||
decode_fifo(draw(GX_QUADS, count));
|
||||
const auto& indices = aurora::gfx::testing::last_pushed_indices();
|
||||
ASSERT_EQ(indices.size(), (count / 4) * 6 + (count % 4 == 3 ? 3 : 0));
|
||||
for (const auto index : indices)
|
||||
ASSERT_LT(index, count);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, IncompletePrimitivesNeverJoinAcrossDraws) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 4));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
decode_fifo(draw(GX_TRIANGLES, 5));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{4, 5, 6}));
|
||||
const auto before = aurora::gfx::testing::last_pushed_indices();
|
||||
decode_fifo(draw(GX_TRIANGLEFAN, 2));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), before);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, MergeStopsBeforeSixteenBitIndexOverflow) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 65535));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, VertexCacheInvalidationBreaksDrawMerging) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
decode_fifo({GX_CMD_INVL_VC});
|
||||
EXPECT_TRUE(g_gxState.stateDirty);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, EqualStrideVertexFormatChangeBreaksDrawMerging) {
|
||||
aurora::gfx::testing::use_real_vertex_format_helpers(true);
|
||||
g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].cnt = GX_POS_XY;
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].type = GX_U8;
|
||||
}
|
||||
g_gxState.vtxFmts[GX_VTXFMT1].attrs[GX_VA_POS].frac = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
auto bytes = draw(GX_TRIANGLES, 3, format);
|
||||
bytes.resize(9);
|
||||
decode_fifo(bytes);
|
||||
}
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_POINTS, 1));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto old = state.request();
|
||||
EXPECT_EQ(state.request(), 0u);
|
||||
state.reset();
|
||||
const auto current = state.request();
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapping);
|
||||
EXPECT_TRUE(state.complete(current, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(current, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
|
||||
TEST(StagingMapping, AsyncCompletionWakesWaiters) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto generation = state.request();
|
||||
std::thread callback([&] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
state.complete(generation, BufferMapState::Mapped);
|
||||
});
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (state.state() == BufferMapState::Mapping && std::chrono::steady_clock::now() < deadline)
|
||||
state.wait_for_progress();
|
||||
callback.join();
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
The third beta of WiiCompiled VR for the Steam Frame: Mario Kart Wii, statically recompiled to
|
||||
native ARM64 code, running in VR on SteamOS through SteamVR. This one brings in fixes from upstream
|
||||
WiiCompiled and from other WiiCompiled forks, Retro Rewind in the installer, and a diagnostics
|
||||
script. Thanks to everyone whose work is in it; [`CREDITS.md`](https://github.com/mitch030504/Wiicompiled_VR_Frame/blob/openxr-work/CREDITS.md) lists who wrote
|
||||
what.
|
||||
|
||||
**This release is source only.** The game is always built from your own clean PAL `RMCP01` disc,
|
||||
and nothing built from it may be distributed, so there is no ready-built game here.
|
||||
|
||||
## New
|
||||
|
||||
- **Retro Rewind in the installer.** `steam-frame-install.sh --retro-rewind` downloads the Retro
|
||||
Rewind pack from its own update server, builds Retro Rewind beside the base game and adds it to
|
||||
your Steam library. `--retro-rewind-pack DIR` uses a pack you already have. Online play works on
|
||||
Linux now that TLS does (below). From heurazy's Wiicompiled_VR-PLUS.
|
||||
- **Adaptive resolution** (`[vr] adaptive_resolution`, off by default): lowers the race's eye
|
||||
resolution down to 70% while frames fall behind, and raises it again once they keep up.
|
||||
Experimental and not yet tried in a race on the Frame. From Wiicompiled_VR-PLUS.
|
||||
- **Updating, from the PC or from inside the game.** `steam-frame-install.sh --update` reuses the
|
||||
options the install was made with and builds nothing when the Frame already has the newest
|
||||
release (and Retro Rewind pack); `--check` only reports. An install built on the Frame itself
|
||||
(`--frame local`) gets an **Updates** tab in the settings panel: it checks for a new release and
|
||||
builds it in the background while the game stays open, showing each step and how far the build
|
||||
is. Close the game meanwhile and the update reopens it when done. Steam notifications were tried
|
||||
for this and do not show on the Frame.
|
||||
- **`Launcher/frame-diagnostics.sh`** gathers the logs, crash files, settings, SteamVR logs and
|
||||
system state from the Frame into one archive, with a summary of how far startup got. Attach it
|
||||
to bug reports.
|
||||
|
||||
## Fixed
|
||||
|
||||
- A crash while a scene loads (`GXClearVtxDesc`). From Wiicompiled_VR-PLUS.
|
||||
- TLS on Linux, through Mbed TLS, so online services can connect at all (upstream #144), and a
|
||||
timed-out TLS write no longer frees a session the game still holds (DarthMDev).
|
||||
- Saves on a different drive or the SD card can be moved (upstream #212); the game keeps its local
|
||||
Wii identity services while offline, and emptied WiiConnect24 files are refilled, so it no longer
|
||||
reports a save error for them (upstream #244, nx-mod).
|
||||
- `Config.toml` is saved through a temporary file, so a crash mid-save can't empty it
|
||||
(BlackAndBlue95).
|
||||
- Renderer fixes from upstream's KartPad batch (#244): draws no longer read past their own vertices,
|
||||
three-byte vertex attributes, GEN_MODE, EFB copies and readback, pipeline cache rows and pipeline
|
||||
compile wake-ups. Waiting on the GPU now sleeps instead of holding a CPU core busy.
|
||||
- Effects no longer vanish while the world is darkened, such as Stormship's lightning
|
||||
(BlackAndBlue95).
|
||||
- Controller player indices and socket errors (upstream #251), and a `bltl` translation (#254).
|
||||
|
||||
## Faster
|
||||
|
||||
- Disc reads and SZS decompression take bulk copies and keep file handles open per thread
|
||||
(KartPad).
|
||||
- Audio mixing uses NEON (rooklz).
|
||||
- Translated memory accesses no longer check the page size each time, since the Frame uses 4 KiB
|
||||
pages; the game checks once at startup (from nx-mod's finding).
|
||||
|
||||
## Updating from frame-beta-2
|
||||
|
||||
Run the install command again; you can leave out `--disc`. Dawn's patches did not change, so it is
|
||||
not rebuilt. Most of the game is, since the translator changed as well as the runtime and renderer,
|
||||
so expect a longer update than usual. The build now also downloads Mbed TLS. Add `--retro-rewind` to get Retro Rewind too.
|
||||
|
||||
frame-beta-2's script has no `--update`, so this one update is the full install command; it saves
|
||||
its options, and from then on `steam-frame-install.sh --update` is enough. To update from inside
|
||||
the game later, make this install on the Frame itself with `--frame local`.
|
||||
|
||||
## Known issues
|
||||
|
||||
Unchanged from frame-beta-2:
|
||||
|
||||
- Images double in races and on the HUD, sometimes in the right eye only. Foveation is the main
|
||||
suspect; try foveation Off if it bothers you, and report the result.
|
||||
- Foveation follows the right eye less well than the left.
|
||||
- VR frame interpolation is not recommended on the Frame.
|
||||
- The Android (`steamFrame`) flavour cannot show a picture in Lepton; use the native build.
|
||||
|
||||
## Recommended settings
|
||||
|
||||
`render_scale = 1.25`, `foveation = "medium"`, `repeat_frames = true`, `frame_interpolation_fps = 0`
|
||||
under `[vr]`, and `resolution_multiplier = 2` under `[video]`. SteamVR at 120 Hz.
|
||||
|
||||
Reports with the run log from `~/.local/share/WiiCompiled/Logs/`, or the archive
|
||||
`frame-diagnostics.sh` makes, are welcome as issues.
|
||||
@@ -144,6 +144,43 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# TLS for non-Windows guest network HLE (runtime/src/hle/net/network_ssl.cpp) - the Windows path
|
||||
# uses Schannel (a Windows-only OS API), which has no equivalent on Linux/Android, so this project
|
||||
# needs its own TLS library there. mbed TLS was chosen over OpenSSL specifically because it cross-
|
||||
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
|
||||
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
|
||||
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
|
||||
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
|
||||
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
|
||||
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
|
||||
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
|
||||
# upstream's own signed `mbedtls-<version>-sha256sum.txt` release asset.
|
||||
#
|
||||
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
|
||||
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
|
||||
# TLS path (`#ifndef _WIN32`). Windows keeps Schannel and must not fetch anything: its builds run
|
||||
# with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline dependency set prepared by
|
||||
# Launcher/Prepare-Dependencies.ps1, so an unconditional fetch would fail a clean configure there
|
||||
# and would also add a dependency Windows never links.
|
||||
add_library(mkw_mbedtls INTERFACE)
|
||||
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
include(FetchContent)
|
||||
set(MKW_MBEDTLS_VERSION "3.6.7")
|
||||
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
|
||||
FetchContent_Declare(mkw_mbedtls_upstream
|
||||
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
|
||||
URL_HASH SHA256=${MKW_MBEDTLS_SHA256})
|
||||
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
|
||||
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
|
||||
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
|
||||
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
|
||||
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
|
||||
endif()
|
||||
|
||||
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
|
||||
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
|
||||
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
|
||||
@@ -469,6 +506,12 @@ target_include_directories(mkw_vr_eye_gaze_tests PRIVATE "${CMAKE_CURRENT_LIST_D
|
||||
target_compile_features(mkw_vr_eye_gaze_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_eye_gaze_tests COMMAND mkw_vr_eye_gaze_tests)
|
||||
|
||||
# The AX mix kernels' AVX2 and NEON forms against their scalar reference loops.
|
||||
add_executable(mkw_ax_mix_kernels_tests "${CMAKE_CURRENT_LIST_DIR}/tests/ax_mix_kernels_tests.cpp")
|
||||
target_include_directories(mkw_ax_mix_kernels_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_ax_mix_kernels_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_ax_mix_kernels_tests COMMAND mkw_ax_mix_kernels_tests)
|
||||
|
||||
# The in-headset settings panel's controller chord, release latch, selection,
|
||||
# scrolling and canvas mapping, plus the thread bridge they publish through.
|
||||
add_executable(mkw_vr_settings_panel_tests tests/vr_settings_panel_tests.cpp src/vr/openxr_settings_panel.cpp)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -110,7 +110,7 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_ENABLE_OPENXR)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
|
||||
endif()
|
||||
@@ -276,10 +276,10 @@ function(mkw_configure_product target)
|
||||
mkw_bound_translated_compiles(${target})
|
||||
if(MKW_PRODUCT_WITHOUT_GAME)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
else()
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
@@ -379,6 +379,21 @@ function(mkw_configure_product target)
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
|
||||
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
|
||||
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
|
||||
# Schannel, mbed TLS does not ship one itself. Not SHA256-pinned like the DSP ROM above: unlike
|
||||
# a fixed hardware ROM, this bundle is expected to be refreshed periodically as CAs rotate.
|
||||
# Windows gets its trust store from Schannel, so only the platforms that actually build the
|
||||
# mbed TLS path need the bundle beside the executable.
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_CA_CERTIFICATE_BUNDLE "${MKW_RUNTIME_SOURCE_DIR}/assets/certs/cacert.pem")
|
||||
if(NOT EXISTS "${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Android ships each product as the shared library SDLActivity loads; the base
|
||||
|
||||
@@ -71,10 +71,15 @@ bool IsActive();
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
// not become a hot-path load. On AArch64 the page size is a kernel
|
||||
// configuration (4/16/64 KiB), so it is probed at runtime, except in the native
|
||||
// Steam Frame build: that build only runs on the headset, whose SteamOS kernel
|
||||
// uses 4 KiB pages, and Initialize() refuses to start on any other page size.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#elif defined(MKW_HEADSET_STEAM_FRAME) && !defined(__ANDROID__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#define MKW_GUEST_FLAT_VERIFY_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -294,14 +295,57 @@ inline bool ReadExact(const std::filesystem::path& hostPath,
|
||||
HostReadFailure& failure) {
|
||||
failure = HostReadFailure::None;
|
||||
|
||||
std::ifstream file(hostPath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
failure = HostReadFailure::MissingFile;
|
||||
return false;
|
||||
// DVD callbacks run on the guest thread, often reading the same stream
|
||||
// file many times per second. Reuse a bounded set of handles without
|
||||
// sharing seek positions with another host thread.
|
||||
struct OpenFile {
|
||||
std::filesystem::path path;
|
||||
std::ifstream stream;
|
||||
std::streamoff size = 0;
|
||||
std::filesystem::file_time_type modified{};
|
||||
uint64_t lastUse = 0;
|
||||
};
|
||||
static thread_local std::vector<OpenFile> openFiles;
|
||||
static thread_local uint64_t useCount = 0;
|
||||
constexpr size_t kMaxOpenFiles = 8;
|
||||
|
||||
auto found = std::find_if(openFiles.begin(), openFiles.end(),
|
||||
[&](const OpenFile& entry) { return entry.path == hostPath; });
|
||||
if (found != openFiles.end()) {
|
||||
std::error_code ec;
|
||||
const auto modified = std::filesystem::last_write_time(hostPath, ec);
|
||||
if (ec || modified != found->modified) {
|
||||
openFiles.erase(found);
|
||||
found = openFiles.end();
|
||||
}
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff fileSize = file.tellg();
|
||||
if (found == openFiles.end()) {
|
||||
std::ifstream file(hostPath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
failure = HostReadFailure::MissingFile;
|
||||
return false;
|
||||
}
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff size = file.tellg();
|
||||
if (size < 0) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
const auto modified = std::filesystem::last_write_time(hostPath, ec);
|
||||
if (openFiles.size() == kMaxOpenFiles) {
|
||||
const auto oldest = std::min_element(openFiles.begin(), openFiles.end(),
|
||||
[](const OpenFile& a, const OpenFile& b) { return a.lastUse < b.lastUse; });
|
||||
openFiles.erase(oldest);
|
||||
}
|
||||
openFiles.push_back({hostPath, std::move(file), size,
|
||||
ec ? std::filesystem::file_time_type{} : modified, 0});
|
||||
found = std::prev(openFiles.end());
|
||||
}
|
||||
found->lastUse = ++useCount;
|
||||
|
||||
const std::streamoff fileSize = found->size;
|
||||
if (fileSize < 0 ||
|
||||
offset > static_cast<uint64_t>(std::numeric_limits<std::streamoff>::max()) ||
|
||||
offset >= static_cast<uint64_t>(fileSize)) {
|
||||
@@ -316,17 +360,20 @@ inline bool ReadExact(const std::filesystem::path& hostPath,
|
||||
}
|
||||
|
||||
std::vector<uint8_t> staged(length);
|
||||
file.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
|
||||
if (!file) {
|
||||
found->stream.clear();
|
||||
found->stream.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
|
||||
if (!found->stream) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
openFiles.erase(found);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length != 0) {
|
||||
file.read(reinterpret_cast<char*>(staged.data()),
|
||||
static_cast<std::streamsize>(length));
|
||||
if (file.gcount() != static_cast<std::streamsize>(length)) {
|
||||
found->stream.read(reinterpret_cast<char*>(staged.data()),
|
||||
static_cast<std::streamsize>(length));
|
||||
if (found->stream.gcount() != static_cast<std::streamsize>(length)) {
|
||||
failure = HostReadFailure::ShortRead;
|
||||
openFiles.erase(found);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,19 +86,37 @@ inline bool CopyBootstrapFile(const std::filesystem::path& sourceRoot,
|
||||
std::error_code& ec) {
|
||||
const auto source = sourceRoot / relativePath;
|
||||
const auto destination = destinationRoot / relativePath;
|
||||
auto copyOptions = std::filesystem::copy_options::none;
|
||||
if (std::filesystem::exists(destination, ec)) {
|
||||
return !ec;
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
// Keep whatever the player has, except an empty file where the payload has content. WC24
|
||||
// rejects a zero-length download or friend list outright and MKW reports that as a save
|
||||
// error; since seeding only ran for missing files, such a file stayed broken for good.
|
||||
const auto existingSize = std::filesystem::file_size(destination, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
std::error_code sourceError;
|
||||
const auto sourceSize = std::filesystem::file_size(source, sourceError);
|
||||
if (existingSize != 0 || sourceError || sourceSize == 0) {
|
||||
return true;
|
||||
}
|
||||
copyOptions = std::filesystem::copy_options::overwrite_existing;
|
||||
} else if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(destination.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
std::filesystem::copy_file(source, destination, std::filesystem::copy_options::none, ec);
|
||||
std::filesystem::copy_file(source, destination, copyOptions, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
// Create these WC24 files only for a new profile; never overwrite user data.
|
||||
// Create these WC24 files for a new profile, or refill one left empty; never overwrite user data.
|
||||
constexpr std::string_view kBootstrapFiles[] = {
|
||||
"shared2/wc24/misc.bin",
|
||||
"shared2/wc24/nwc24dl.bin",
|
||||
|
||||
@@ -1064,14 +1064,24 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
|
||||
// user-specific paths. This is used by the in-game F10 settings bar.
|
||||
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
|
||||
const auto path = ResolveConfigPath();
|
||||
std::ifstream input(path);
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
std::error_code existsError;
|
||||
if (std::filesystem::exists(path, existsError)) {
|
||||
// A file that's there but can't be read is not an empty one: rewriting it from nothing
|
||||
// would throw away every other setting.
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
while (input && std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
if (!input.eof()) {
|
||||
std::cerr << "[runtime-config] Unable to read " << PathToUtf8(path) << "; not saving "
|
||||
<< key << std::endl;
|
||||
return false;
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
|
||||
const std::string normalizedSection = Trim(section);
|
||||
@@ -1124,19 +1134,34 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
}
|
||||
}
|
||||
|
||||
// Written beside the file and renamed over it, so a crash or a full disk mid-write leaves the
|
||||
// old file whole rather than a truncated one.
|
||||
std::error_code ec;
|
||||
if (path.has_parent_path()) {
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
}
|
||||
std::ofstream output(path, std::ios::trunc);
|
||||
if (!output) {
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
output.close();
|
||||
if (!output) {
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(temporary) << std::endl;
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (ec) {
|
||||
std::cerr << "[runtime-config] Unable to replace " << PathToUtf8(path) << ": " << ec.message()
|
||||
<< std::endl;
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
return static_cast<bool>(output);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string FormatString(std::string_view value) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Settings > Updates: whether a newer release of the game exists, and, on a Frame that holds its own
|
||||
// build, updating to it without leaving the headset.
|
||||
//
|
||||
// Both answers come from Launcher/steam-frame-install.sh, which already knows how to compare
|
||||
// versions and how to build: `--check` reports, `--update` installs. The update reports its progress
|
||||
// through a status file rather than Steam notifications, which the Frame's Steam does not show.
|
||||
// The install writes update.conf beside the player's configuration when it put a build on this
|
||||
// machine, so the Update button appears only where it can actually work; installed from a PC, the
|
||||
// tab says to update there.
|
||||
namespace update_check {
|
||||
|
||||
enum class State {
|
||||
// This install cannot update itself: it was built on a PC, which is where updates are run.
|
||||
PcInstall,
|
||||
// It can, and nothing has been asked of it yet this session.
|
||||
Idle,
|
||||
Checking,
|
||||
UpToDate,
|
||||
Available,
|
||||
// An update is running beside the game, as a service of its own: it carries on if the game is
|
||||
// closed, and then opens it again when it is done.
|
||||
Updating,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
State state = State::PcInstall;
|
||||
// The release this build came from, or empty when it was built from a source tree.
|
||||
std::string installedRelease;
|
||||
// One line under the state: which release is available, what failed, or what step is running.
|
||||
std::string detail;
|
||||
// An update has installed a different build since this one started: it takes effect when the
|
||||
// game is started again.
|
||||
bool restartNeeded = false;
|
||||
};
|
||||
|
||||
// What the Updates tab draws. Cheap enough to call every frame: a check or update in flight is
|
||||
// answered from memory, and otherwise the files behind it are re-read at most once a second.
|
||||
Status Current();
|
||||
|
||||
// Whether there is an Updates tab to show at all: this install can update itself, or it at least
|
||||
// knows which release it is running. Neither is true of a build that came from somewhere else.
|
||||
inline bool Relevant(const Status& status) {
|
||||
return status.state != State::PcInstall || !status.installedRelease.empty();
|
||||
}
|
||||
|
||||
// Asks the install script whether a newer release exists, in the background. Returns at once.
|
||||
void StartCheck();
|
||||
|
||||
// Starts the update in the background: its own systemd user service runs the install script, which
|
||||
// reports each step to the file Current() reads, so the game can stay open and show the progress.
|
||||
// On false, `error` says why, for display.
|
||||
bool StartUpdate(std::string& error);
|
||||
|
||||
} // namespace update_check
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
#include <SDL3/SDL_platform.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -146,7 +147,7 @@ std::string BuildMappingString() {
|
||||
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
|
||||
}
|
||||
}
|
||||
mapping += "platform:Windows,";
|
||||
mapping += std::string("platform:") + SDL_GetPlatform() + ",";
|
||||
return mapping;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "memory.h"
|
||||
@@ -61,8 +62,9 @@ constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
constexpr size_t kHostPageSize = 0x1000;
|
||||
|
||||
// Only hosts that can expose a page larger than 4 KiB need to discover their
|
||||
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
// size at runtime, or check it when the build assumes 4 KiB; see
|
||||
// RequiresCheckedAccess() in guest_flat_memory.h.
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) || defined(MKW_GUEST_FLAT_VERIFY_PAGE_SIZE)
|
||||
size_t HostPageSize()
|
||||
{
|
||||
const long size = sysconf(_SC_PAGESIZE);
|
||||
@@ -521,6 +523,13 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
|
||||
#elif defined(MKW_GUEST_FLAT_VERIFY_PAGE_SIZE)
|
||||
if (HostPageSize() != kGuestPageSize) {
|
||||
throw std::runtime_error(
|
||||
"This is a Steam Frame build, which assumes the headset's 4 KiB memory pages, but this "
|
||||
"kernel uses " + std::to_string(HostPageSize()) + "-byte pages. Build without "
|
||||
"--headset steam_frame for this device.");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (g_initialized) {
|
||||
|
||||
@@ -12,8 +12,16 @@
|
||||
#if defined(__AVX2__)
|
||||
#include <immintrin.h>
|
||||
#define MKW_AX_MIX_AVX2 1
|
||||
#define MKW_AX_MIX_NEON 0
|
||||
#elif defined(__aarch64__)
|
||||
// Advanced SIMD is architectural on AArch64: the NEON forms below are the arm64 counterparts of
|
||||
// the AVX2 kernels, processing eight 16-bit samples per step as two int32x4 halves.
|
||||
#include <arm_neon.h>
|
||||
#define MKW_AX_MIX_AVX2 0
|
||||
#define MKW_AX_MIX_NEON 1
|
||||
#else
|
||||
#define MKW_AX_MIX_AVX2 0
|
||||
#define MKW_AX_MIX_NEON 0
|
||||
#endif
|
||||
|
||||
namespace AxMixKernels {
|
||||
@@ -99,10 +107,63 @@ inline uint16_t MixAddRampAvx2(int32_t* out, const int16_t* input, uint32_t coun
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline uint16_t MixAddRampNeon(int32_t* out, const int16_t* input, uint32_t count,
|
||||
uint16_t volume, uint16_t delta, int16_t& dpop) {
|
||||
uint32_t i = 0;
|
||||
int16_t last = dpop;
|
||||
if (count >= 8) {
|
||||
// Same ramp model as the AVX2 form: volume + k*delta stays inside int32 for every count
|
||||
// the AX mix uses, and the 16-bit wrap is applied only where the scalar loop applies it.
|
||||
const int32x4_t lanesLo = {0, 1, 2, 3};
|
||||
const int32x4_t lanesHi = {4, 5, 6, 7};
|
||||
const int32x4_t deltaVec = vdupq_n_s32(static_cast<int32_t>(delta));
|
||||
const int32x4_t wrapMask = vdupq_n_s32(0xFFFF);
|
||||
const int32x4_t blockStep = vdupq_n_s32(static_cast<int32_t>(delta) * 8);
|
||||
const int32x4_t base = vdupq_n_s32(static_cast<int32_t>(volume));
|
||||
int32x4_t rampLo = vmlaq_s32(base, lanesLo, deltaVec);
|
||||
int32x4_t rampHi = vmlaq_s32(base, lanesHi, deltaVec);
|
||||
int32x4_t lastBlock = vdupq_n_s32(0);
|
||||
for (; i + 8 <= count; i += 8) {
|
||||
const int16x8_t samples = vld1q_s16(input + i);
|
||||
const int32x4_t sLo = vmovl_s16(vget_low_s16(samples));
|
||||
const int32x4_t sHi = vmovl_s16(vget_high_s16(samples));
|
||||
// int16 x uint16 fits in int32: the low 32-bit product is exact. Signed 32 -> 16
|
||||
// saturation after the >> 15 is exactly clamp(-0x8000, 0x7fff).
|
||||
int32x4_t scaledLo = vshrq_n_s32(vmulq_s32(sLo, vandq_s32(rampLo, wrapMask)), 15);
|
||||
int32x4_t scaledHi = vshrq_n_s32(vmulq_s32(sHi, vandq_s32(rampHi, wrapMask)), 15);
|
||||
scaledLo = vmovl_s16(vqmovn_s32(scaledLo));
|
||||
scaledHi = vmovl_s16(vqmovn_s32(scaledHi));
|
||||
vst1q_s32(out + i, vaddq_s32(vld1q_s32(out + i), scaledLo));
|
||||
vst1q_s32(out + i + 4, vaddq_s32(vld1q_s32(out + i + 4), scaledHi));
|
||||
lastBlock = scaledHi;
|
||||
rampLo = vaddq_s32(rampLo, blockStep);
|
||||
rampHi = vaddq_s32(rampHi, blockStep);
|
||||
}
|
||||
if (i != 0) {
|
||||
last = static_cast<int16_t>(vgetq_lane_s32(lastBlock, 3));
|
||||
volume = static_cast<uint16_t>(static_cast<uint32_t>(vgetq_lane_s32(rampLo, 0)));
|
||||
}
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
const int32_t scaled =
|
||||
(static_cast<int32_t>(input[i]) * static_cast<int32_t>(volume)) >> 15;
|
||||
const int16_t sample = ClampToS16(scaled);
|
||||
out[i] += sample;
|
||||
volume = static_cast<uint16_t>(volume + delta);
|
||||
last = sample;
|
||||
}
|
||||
dpop = last;
|
||||
return volume;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline uint16_t MixAddRamp(int32_t* out, const int16_t* input, uint32_t count,
|
||||
uint16_t volume, uint16_t delta, int16_t& dpop) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
return MixAddRampAvx2(out, input, count, volume, delta, dpop);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
return MixAddRampNeon(out, input, count, volume, delta, dpop);
|
||||
#else
|
||||
return MixAddRampScalar(out, input, count, volume, delta, dpop);
|
||||
#endif
|
||||
@@ -162,10 +223,50 @@ inline uint16_t ScaleRampAvx2(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline uint16_t ScaleRampNeon(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
uint16_t delta) {
|
||||
uint32_t i = 0;
|
||||
if (count >= 8) {
|
||||
const int32x4_t lanesLo = {0, 1, 2, 3};
|
||||
const int32x4_t lanesHi = {4, 5, 6, 7};
|
||||
const int32x4_t deltaVec = vdupq_n_s32(static_cast<int32_t>(delta));
|
||||
const int32x4_t wrapMask = vdupq_n_s32(0xFFFF);
|
||||
const int32x4_t blockStep = vdupq_n_s32(static_cast<int32_t>(delta) * 8);
|
||||
const int32x4_t base = vdupq_n_s32(static_cast<int32_t>(volume));
|
||||
int32x4_t rampLo = vmlaq_s32(base, lanesLo, deltaVec);
|
||||
int32x4_t rampHi = vmlaq_s32(base, lanesHi, deltaVec);
|
||||
for (; i + 8 <= count; i += 8) {
|
||||
const int16x8_t block = vld1q_s16(samples + i);
|
||||
const int32x4_t scaledLo = vshrq_n_s32(
|
||||
vmulq_s32(vmovl_s16(vget_low_s16(block)), vandq_s32(rampLo, wrapMask)), 15);
|
||||
const int32x4_t scaledHi = vshrq_n_s32(
|
||||
vmulq_s32(vmovl_s16(vget_high_s16(block)), vandq_s32(rampHi, wrapMask)), 15);
|
||||
// Signed 32 -> 16 saturation is exactly clamp(-0x8000, 0x7fff).
|
||||
vst1q_s16(samples + i, vcombine_s16(vqmovn_s32(scaledLo), vqmovn_s32(scaledHi)));
|
||||
rampLo = vaddq_s32(rampLo, blockStep);
|
||||
rampHi = vaddq_s32(rampHi, blockStep);
|
||||
}
|
||||
if (i != 0) {
|
||||
volume = static_cast<uint16_t>(static_cast<uint32_t>(vgetq_lane_s32(rampLo, 0)));
|
||||
}
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
const int32_t scaled =
|
||||
(static_cast<int32_t>(samples[i]) * static_cast<int32_t>(volume)) >> 15;
|
||||
samples[i] = ClampToS16(scaled);
|
||||
volume = static_cast<uint16_t>(volume + delta);
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline uint16_t ScaleRamp(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
uint16_t delta) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
return ScaleRampAvx2(samples, count, volume, delta);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
return ScaleRampNeon(samples, count, volume, delta);
|
||||
#else
|
||||
return ScaleRampScalar(samples, count, volume, delta);
|
||||
#endif
|
||||
@@ -214,10 +315,34 @@ inline void MixAccumRamp32Avx2(int32_t* dst, const int32_t* src, const uint16_t*
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline void MixAccumRamp32Neon(int32_t* dst, const int32_t* src, const uint16_t* ramp,
|
||||
uint32_t count) {
|
||||
uint32_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const int32x4_t source = vld1q_s32(src + i);
|
||||
// The ramp is unsigned 16-bit, so it is a non-negative int32 and the widening signed
|
||||
// multiply produces the exact 64-bit product (|product| < 2^47); the arithmetic >> 15
|
||||
// then narrows to the same bits the scalar (int32)(p >> 15) keeps.
|
||||
const int32x4_t gain = vreinterpretq_s32_u32(vmovl_u16(vld1_u16(ramp + i)));
|
||||
const int64x2_t lo = vshrq_n_s64(vmull_s32(vget_low_s32(source), vget_low_s32(gain)), 15);
|
||||
const int64x2_t hi = vshrq_n_s64(vmull_high_s32(source, gain), 15);
|
||||
const int32x4_t result = vcombine_s32(vmovn_s64(lo), vmovn_s64(hi));
|
||||
vst1q_s32(dst + i, vaddq_s32(vld1q_s32(dst + i), result));
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
dst[i] += static_cast<int32_t>(
|
||||
(static_cast<int64_t>(src[i]) * static_cast<int64_t>(ramp[i])) >> 15);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void MixAccumRamp32(int32_t* dst, const int32_t* src, const uint16_t* ramp,
|
||||
uint32_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
MixAccumRamp32Avx2(dst, src, ramp, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
MixAccumRamp32Neon(dst, src, ramp, count);
|
||||
#else
|
||||
MixAccumRamp32Scalar(dst, src, ramp, count);
|
||||
#endif
|
||||
@@ -270,9 +395,32 @@ inline void LoadBigEndian32Avx2(int32_t* dst, const uint8_t* src, size_t count)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
// rev32 on byte lanes is the whole byte swap; four words per step.
|
||||
inline void StoreBigEndian32Neon(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const uint8x16_t value = vreinterpretq_u8_s32(vld1q_s32(src + i));
|
||||
vst1q_u8(dst + i * sizeof(uint32_t), vrev32q_u8(value));
|
||||
}
|
||||
StoreBigEndian32Scalar(dst + i * sizeof(uint32_t), src + i, count - i);
|
||||
}
|
||||
|
||||
inline void LoadBigEndian32Neon(int32_t* dst, const uint8_t* src, size_t count) {
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const uint8x16_t value = vld1q_u8(src + i * sizeof(uint32_t));
|
||||
vst1q_s32(dst + i, vreinterpretq_s32_u8(vrev32q_u8(value)));
|
||||
}
|
||||
LoadBigEndian32Scalar(dst + i, src + i * sizeof(uint32_t), count - i);
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void StoreBigEndian32(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
StoreBigEndian32Avx2(dst, src, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
StoreBigEndian32Neon(dst, src, count);
|
||||
#else
|
||||
StoreBigEndian32Scalar(dst, src, count);
|
||||
#endif
|
||||
@@ -281,6 +429,8 @@ inline void StoreBigEndian32(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
inline void LoadBigEndian32(int32_t* dst, const uint8_t* src, size_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
LoadBigEndian32Avx2(dst, src, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
LoadBigEndian32Neon(dst, src, count);
|
||||
#else
|
||||
LoadBigEndian32Scalar(dst, src, count);
|
||||
#endif
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
#include "hle_stubs.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include "memory.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
extern "C" void GxNotifyGuestRamDmaWrite(uint32_t addr, uint32_t size);
|
||||
|
||||
// Native because a crafted Yaz0 run writes past the caller's buffer
|
||||
// (github.com/vabold/szsHaxx)
|
||||
// https://github.com/vabold/Kinoko/blob/main/source/egg/core/Decomp.cc
|
||||
|
||||
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
{
|
||||
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
|
||||
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
|
||||
|
||||
template <typename Access>
|
||||
static uint32_t DecodeSZS(uint32_t src, uint32_t expandSize, Access& access) {
|
||||
uint32_t srcIdx = 16;
|
||||
uint32_t dstIdx = 0;
|
||||
uint32_t mask = 0;
|
||||
@@ -22,15 +21,15 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
|
||||
while (static_cast<int32_t>(dstIdx) < static_cast<int32_t>(expandSize)) {
|
||||
if (mask == 0) {
|
||||
flags = MemoryInline::FlatRead8(src + srcIdx++);
|
||||
flags = access.ReadSource(srcIdx++);
|
||||
mask = 0x80;
|
||||
}
|
||||
|
||||
if ((flags & mask) != 0) {
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(src + srcIdx++));
|
||||
access.WriteOutput(dstIdx++, access.ReadSource(srcIdx++));
|
||||
} else {
|
||||
const uint32_t high = MemoryInline::FlatRead8(src + srcIdx);
|
||||
const uint32_t low = MemoryInline::FlatRead8(src + srcIdx + 1);
|
||||
const uint32_t high = access.ReadSource(srcIdx);
|
||||
const uint32_t low = access.ReadSource(srcIdx + 1);
|
||||
srcIdx += 2;
|
||||
|
||||
const uint32_t rep = (high << 8) | low;
|
||||
@@ -48,7 +47,7 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
uint32_t count = rep >> 12;
|
||||
count = count != 0
|
||||
? count + 2
|
||||
: static_cast<uint32_t>(MemoryInline::FlatRead8(src + srcIdx++)) + 18;
|
||||
: static_cast<uint32_t>(access.ReadSource(srcIdx++)) + 18;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (dstIdx >= expandSize) {
|
||||
@@ -59,7 +58,7 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
"The game stopped decoding a malformed Yaz0 file.");
|
||||
std::abort();
|
||||
}
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(dst + copyIdx++));
|
||||
access.WriteOutput(dstIdx++, access.ReadOutput(copyIdx++));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +68,61 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
return expandSize;
|
||||
}
|
||||
|
||||
struct GuestSZSAccess {
|
||||
uint32_t src;
|
||||
uint32_t dst;
|
||||
|
||||
uint8_t ReadSource(uint32_t offset) const { return MemoryInline::FlatRead8(src + offset); }
|
||||
uint8_t ReadOutput(uint32_t offset) const { return MemoryInline::FlatRead8(dst + offset); }
|
||||
void WriteOutput(uint32_t offset, uint8_t value) const {
|
||||
MemoryInline::FlatWrite8(dst + offset, value);
|
||||
}
|
||||
};
|
||||
|
||||
struct HostSZSAccess {
|
||||
const uint8_t* src;
|
||||
uint8_t* dst;
|
||||
|
||||
uint8_t ReadSource(uint32_t offset) const { return src[offset]; }
|
||||
uint8_t ReadOutput(uint32_t offset) const { return dst[offset]; }
|
||||
void WriteOutput(uint32_t offset, uint8_t value) const { dst[offset] = value; }
|
||||
};
|
||||
|
||||
static bool HasDeferredReadPages(uint32_t address, size_t length) {
|
||||
const uint32_t first = address >> MemoryInline::kPageShift;
|
||||
const uint32_t last = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(address) + length - 1) >> MemoryInline::kPageShift);
|
||||
for (uint32_t page = first; page <= last; ++page) {
|
||||
if (MemoryInline::g_deferredReadCoveredPages[page] != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
{
|
||||
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
|
||||
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
|
||||
|
||||
// A token produces at least one byte; all-literal data is the largest
|
||||
// valid input (one flag per eight output bytes). Three extra bytes cover
|
||||
// the final malformed run before its output-overrun check aborts.
|
||||
const size_t maxSourceBytes = 16ull + expandSize + (static_cast<uint64_t>(expandSize) + 7) / 8 + 3;
|
||||
if (expandSize != 0 && expandSize <= static_cast<uint32_t>(std::numeric_limits<int32_t>::max()) &&
|
||||
!GuestFlat::RequiresCheckedAccess() &&
|
||||
Memory::Contains(src, maxSourceBytes) && Memory::Contains(dst, expandSize) &&
|
||||
!HasDeferredReadPages(src, maxSourceBytes) && !HasDeferredReadPages(dst, expandSize) &&
|
||||
!RecompMod::ExecutableWriteGuardMayHit(dst, expandSize)) {
|
||||
HostSZSAccess access{Memory::GetPointer(src, maxSourceBytes), Memory::GetPointer(dst, expandSize)};
|
||||
const uint32_t decoded = DecodeSZS(src, expandSize, access);
|
||||
GxNotifyGuestRamDmaWrite(dst, decoded);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
GuestSZSAccess access{src, dst};
|
||||
return DecodeSZS(src, expandSize, access);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(80218C2C, EGG_Decomp_decodeSZS_80218c2c, uint32_t,
|
||||
(uint32_t src, uint32_t dst), (src, dst));
|
||||
@@ -648,9 +648,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
|
||||
if (!path) {
|
||||
return -101;
|
||||
}
|
||||
if (!RuntimeConfigFile::NetworkEnabled(true)) {
|
||||
// The guest opens several /dev/net nodes at boot and retries; report the
|
||||
// reason online will not work exactly once.
|
||||
const bool isIpTop = std::strcmp(path, "/dev/net/ip/top") == 0;
|
||||
const bool isSsl = std::strcmp(path, "/dev/net/ssl") == 0;
|
||||
// KD and NCD provide local identity/configuration services even offline.
|
||||
if ((isIpTop || isSsl) && !RuntimeConfigFile::NetworkEnabled(true)) {
|
||||
static bool reported = false;
|
||||
if (!reported) {
|
||||
reported = true;
|
||||
@@ -665,10 +666,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
|
||||
kind = DeviceKind::KdTime;
|
||||
} else if (std::strcmp(path, "/dev/net/ncd/manage") == 0) {
|
||||
kind = DeviceKind::NcdManage;
|
||||
} else if (std::strcmp(path, "/dev/net/ip/top") == 0) {
|
||||
} else if (isIpTop) {
|
||||
kind = DeviceKind::IpTop;
|
||||
EnsureSocketRuntime();
|
||||
} else if (std::strcmp(path, "/dev/net/ssl") == 0) {
|
||||
} else if (isSsl) {
|
||||
kind = DeviceKind::Ssl;
|
||||
EnsureSocketRuntime();
|
||||
} else {
|
||||
|
||||
@@ -242,6 +242,7 @@ void WritePollResults(uint32_t outAddress,
|
||||
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
|
||||
|
||||
// network_socket.cpp
|
||||
int32_t DeleteWiiSocket(uint32_t fd);
|
||||
void CleanupAllWiiSockets();
|
||||
sockaddr_in ReadWiiSockAddr(uint32_t addr);
|
||||
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
|
||||
|
||||
@@ -21,7 +21,7 @@ static int32_t NewWiiSocket(uint32_t af, uint32_t type, uint32_t protocol) {
|
||||
return wiiFd;
|
||||
}
|
||||
|
||||
static int32_t DeleteWiiSocket(uint32_t fd) {
|
||||
int32_t DeleteWiiSocket(uint32_t fd) {
|
||||
WiiSocket* s = GetWiiSocket(fd);
|
||||
if (!s) {
|
||||
return -SO_EBADF;
|
||||
@@ -467,7 +467,8 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
|
||||
const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize),
|
||||
static_cast<int>(flags), destPtr, destLen);
|
||||
const int hostError = ret < 0 ? NativeLastError() : 0;
|
||||
int32_t result = SocketResult(ret);
|
||||
// Diagnostics may change the native error; use the send result captured above.
|
||||
int32_t result = ret >= 0 ? SocketResult(ret) : SocketErrorResult(hostError);
|
||||
if (patchedWrite && ret == static_cast<int>(sendSize)) {
|
||||
result = static_cast<int32_t>(in[0].size);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
#include "network_internal.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/error.h>
|
||||
#include <mbedtls/net_sockets.h>
|
||||
#include <mbedtls/ssl.h>
|
||||
#include <mbedtls/x509_crt.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#endif
|
||||
|
||||
namespace NetworkHle {
|
||||
|
||||
@@ -38,6 +54,9 @@ constexpr int kMaxSslSessions = 4;
|
||||
struct SslSession {
|
||||
bool active = false;
|
||||
bool handshaked = false;
|
||||
// A failed POSIX TLS write cannot be resumed with a new guest buffer.
|
||||
// Keep the slot and socket ownership intact until explicit teardown.
|
||||
bool failed = false;
|
||||
bool plaintextWfc = false;
|
||||
uint32_t socketFd = UINT32_MAX;
|
||||
NativeSocket native = kInvalidSocket;
|
||||
@@ -56,6 +75,11 @@ struct SslSession {
|
||||
CredHandle cred{};
|
||||
CtxtHandle context{};
|
||||
SecPkgContext_StreamSizes sizes{};
|
||||
#else
|
||||
bool haveSsl = false;
|
||||
mbedtls_ssl_context sslContext{};
|
||||
mbedtls_ssl_config sslConfig{};
|
||||
mbedtls_net_context netContext{};
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -539,26 +563,298 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
||||
return copied == 0 ? SSL_ERR_ZERO : static_cast<int32_t>(copied);
|
||||
}
|
||||
#else
|
||||
// Windows gets TLS for free from the OS (Schannel, above) - mbed TLS is this project's own
|
||||
// vendored equivalent for everywhere else (runtime/third_party/mbedtls, see runtime/CMakeLists.txt
|
||||
// for why mbed TLS specifically). The CA chain and RNG are expensive to set up (parsing ~150 root
|
||||
// certificates, seeding entropy) and read-only once built, so they're shared process-wide instead
|
||||
// of being redone per SSL session.
|
||||
static bool g_mbedtlsCaLoaded = false;
|
||||
static mbedtls_x509_crt g_mbedtlsCaChain;
|
||||
static mbedtls_entropy_context g_mbedtlsEntropy;
|
||||
static mbedtls_ctr_drbg_context g_mbedtlsCtrDrbg;
|
||||
|
||||
static ssize_t SendSslSocket(NativeSocket socket, const uint8_t* data, size_t size) {
|
||||
#ifdef __APPLE__
|
||||
const int noSigPipe = 1;
|
||||
if (setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return send(socket, data, size, 0);
|
||||
#else
|
||||
return send(socket, data, size, MSG_NOSIGNAL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int MbedtlsSend(void* context, const unsigned char* data, size_t size) {
|
||||
const auto* net = static_cast<mbedtls_net_context*>(context);
|
||||
const ssize_t result = SendSslSocket(net->fd, data, size);
|
||||
if (result >= 0) {
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
return MBEDTLS_ERR_SSL_WANT_WRITE;
|
||||
}
|
||||
if (errno == EPIPE || errno == ECONNRESET) {
|
||||
return MBEDTLS_ERR_NET_CONN_RESET;
|
||||
}
|
||||
return MBEDTLS_ERR_NET_SEND_FAILED;
|
||||
}
|
||||
|
||||
static int MbedtlsRecv(void* context, unsigned char* data, size_t size) {
|
||||
const int result = mbedtls_net_recv(context, data, size);
|
||||
// Blocking socket timeouts must leave the TLS session retryable.
|
||||
if (result == MBEDTLS_ERR_NET_RECV_FAILED && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
return MBEDTLS_ERR_SSL_WANT_READ;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mirrors ax_mix.cpp's FindDspCoefficientRom exactly - same three places a bundled asset can live
|
||||
// depending on platform and how the binary was launched (next to the desktop executable, the
|
||||
// Android app's own data directory, or a source-tree checkout during development).
|
||||
static std::optional<std::filesystem::path> FindCaCertificateBundle() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
const auto adjacent = *executableDirectory / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(adjacent)) {
|
||||
return adjacent;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
const auto androidAsset = RuntimeConfigFile::ApplicationDataDirectory() / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(androidAsset)) {
|
||||
return androidAsset;
|
||||
}
|
||||
#endif
|
||||
|
||||
for (auto base = std::filesystem::current_path(); !base.empty();) {
|
||||
const auto sourceTreeAsset = base / "runtime" / "assets" / "certs" / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(sourceTreeAsset)) {
|
||||
return sourceTreeAsset;
|
||||
}
|
||||
const auto parent = base.parent_path();
|
||||
if (parent == base) {
|
||||
break;
|
||||
}
|
||||
base = parent;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Lazy, once-per-process: the first real SSL use pays for parsing the CA bundle and seeding the
|
||||
// RNG, every session after that reuses the result. Returns false (logging once) if the bundle is
|
||||
// missing or unparseable - callers treat that as a normal handshake failure, not a crash, since a
|
||||
// missing TLS root store shouldn't take down gameplay that never touches the network.
|
||||
static bool EnsureMbedtlsGlobalsInitialized() {
|
||||
static const bool initialized = [] {
|
||||
mbedtls_x509_crt_init(&g_mbedtlsCaChain);
|
||||
mbedtls_entropy_init(&g_mbedtlsEntropy);
|
||||
mbedtls_ctr_drbg_init(&g_mbedtlsCtrDrbg);
|
||||
|
||||
const char* personalization = "wiicompiled_ssl";
|
||||
if (mbedtls_ctr_drbg_seed(&g_mbedtlsCtrDrbg, mbedtls_entropy_func, &g_mbedtlsEntropy,
|
||||
reinterpret_cast<const unsigned char*>(personalization),
|
||||
std::strlen(personalization)) != 0) {
|
||||
NetFail("ssl: failed to seed TLS random number generator");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bundle = FindCaCertificateBundle();
|
||||
if (!bundle) {
|
||||
NetFail("ssl: missing TLS root CA bundle (cacert.pem) - HTTPS connections will fail");
|
||||
return false;
|
||||
}
|
||||
const int parseRet = mbedtls_x509_crt_parse_file(&g_mbedtlsCaChain, bundle->string().c_str());
|
||||
if (parseRet < 0) {
|
||||
char errorBuffer[256];
|
||||
mbedtls_strerror(parseRet, errorBuffer, sizeof(errorBuffer));
|
||||
NetFail("ssl: failed to parse CA bundle %s: %s", bundle->string().c_str(), errorBuffer);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
g_mbedtlsCaLoaded = initialized;
|
||||
return initialized;
|
||||
}
|
||||
|
||||
// Builds this session's mbed TLS handshake state exactly once - a second call (e.g. the handshake
|
||||
// re-running after DOHANDSHAKE was already satisfied) is a no-op via ssl.haveSsl.
|
||||
static int32_t EnsureMbedtlsSession(SslSession& ssl) {
|
||||
if (ssl.haveSsl) {
|
||||
return SSL_OK;
|
||||
}
|
||||
if (!EnsureMbedtlsGlobalsInitialized()) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
|
||||
mbedtls_ssl_init(&ssl.sslContext);
|
||||
mbedtls_ssl_config_init(&ssl.sslConfig);
|
||||
if (mbedtls_ssl_config_defaults(&ssl.sslConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
// Real certificate validation, matching Schannel's SCH_CRED_AUTO_CRED_VALIDATION on the
|
||||
// Windows side above - a self-signed or wrong-hostname certificate must fail the handshake,
|
||||
// not just get logged.
|
||||
mbedtls_ssl_conf_authmode(&ssl.sslConfig, MBEDTLS_SSL_VERIFY_REQUIRED);
|
||||
mbedtls_ssl_conf_ca_chain(&ssl.sslConfig, &g_mbedtlsCaChain, nullptr);
|
||||
mbedtls_ssl_conf_rng(&ssl.sslConfig, mbedtls_ctr_drbg_random, &g_mbedtlsCtrDrbg);
|
||||
if (mbedtls_ssl_setup(&ssl.sslContext, &ssl.sslConfig) != 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
// The hostname drives both SNI (which certificate the server presents) and the CN/SAN check
|
||||
// mbedtls_ssl_conf_authmode enforces above - required, not optional, same reasoning as the
|
||||
// Windows path's own "refuse an empty hostname" check just above SslHandshakeImpl.
|
||||
mbedtls_ssl_set_hostname(&ssl.sslContext, ssl.hostname.c_str());
|
||||
|
||||
ssl.netContext.fd = static_cast<int>(ssl.native);
|
||||
mbedtls_ssl_set_bio(&ssl.sslContext, &ssl.netContext, MbedtlsSend, MbedtlsRecv, nullptr);
|
||||
|
||||
ssl.haveSsl = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
|
||||
static void ClearSslSession(SslSession& ssl) {
|
||||
if (ssl.haveSsl) {
|
||||
mbedtls_ssl_free(&ssl.sslContext);
|
||||
mbedtls_ssl_config_free(&ssl.sslConfig);
|
||||
}
|
||||
ssl = {};
|
||||
}
|
||||
|
||||
static int32_t SslHandshakeImpl(SslSession&) {
|
||||
static int32_t SslHandshakeImpl(SslSession& ssl) {
|
||||
if (ssl.plaintextWfc) {
|
||||
ssl.handshaked = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
if (ssl.handshaked) {
|
||||
return SSL_OK;
|
||||
}
|
||||
if (ssl.native == kInvalidSocket) {
|
||||
return SSL_ERR_SYSCALL;
|
||||
}
|
||||
// mbed TLS can authenticate a certificate chain without authenticating a server identity when
|
||||
// no hostname is set - refuse that ambiguous mode, matching the Windows path's own check.
|
||||
if (ssl.hostname.empty()) {
|
||||
return SSL_ERR_VCOMMONNAME;
|
||||
}
|
||||
|
||||
const int32_t setupRet = EnsureMbedtlsSession(ssl);
|
||||
if (setupRet != SSL_OK) {
|
||||
return setupRet;
|
||||
}
|
||||
|
||||
// Receive timeouts are retryable, but the handshake must still terminate.
|
||||
const auto handshakeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
|
||||
int handshakeRet;
|
||||
while ((handshakeRet = mbedtls_ssl_handshake(&ssl.sslContext)) != 0) {
|
||||
if (handshakeRet == MBEDTLS_ERR_SSL_WANT_READ || handshakeRet == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
if (std::chrono::steady_clock::now() >= handshakeDeadline) {
|
||||
NetFail("ssl handshake TIMED OUT host=%s", ssl.hostname.c_str());
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
char errorBuffer[256];
|
||||
mbedtls_strerror(handshakeRet, errorBuffer, sizeof(errorBuffer));
|
||||
NetFail("ssl handshake FAILED host=%s mbedtls_err=%s", ssl.hostname.c_str(), errorBuffer);
|
||||
return handshakeRet == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED ? SSL_ERR_VCOMMONNAME : SSL_ERR_FAILED;
|
||||
}
|
||||
ssl.handshaked = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
|
||||
static int32_t FailSslWrite(SslSession& ssl) {
|
||||
ssl.failed = true;
|
||||
// Stop transport I/O without deleting the guest descriptor or freeing a
|
||||
// session still referenced by the IOCTLV_NET_SSL_WRITE caller.
|
||||
::shutdown(ssl.native, SHUT_RDWR);
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
|
||||
static int32_t SslWrite(SslSession&, const uint8_t*, uint32_t) {
|
||||
return SSL_ERR_FAILED;
|
||||
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
|
||||
if (!data || size == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
const int32_t handshakeRet = SslHandshake(ssl);
|
||||
if (handshakeRet != SSL_OK) {
|
||||
return handshakeRet;
|
||||
}
|
||||
|
||||
if (ssl.plaintextWfc) {
|
||||
uint32_t total = 0;
|
||||
while (total < size) {
|
||||
const ssize_t sent = SendSslSocket(ssl.native, data + total, size - total);
|
||||
if (sent <= 0) {
|
||||
return SSL_ERR_SYSCALL;
|
||||
}
|
||||
total += static_cast<uint32_t>(sent);
|
||||
}
|
||||
return static_cast<int32_t>(total);
|
||||
}
|
||||
|
||||
// mbed TLS is allowed to write fewer bytes than requested in one call (e.g. when size exceeds
|
||||
// one TLS record) - the caller must resend the remainder starting from where it left off, so
|
||||
// loop here until every byte is actually written rather than returning the first partial count.
|
||||
uint32_t totalWritten = 0;
|
||||
const auto writeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
|
||||
while (totalWritten < size) {
|
||||
const int ret = mbedtls_ssl_write(&ssl.sslContext, data + totalWritten, size - totalWritten);
|
||||
if (ret > 0) {
|
||||
totalWritten += static_cast<uint32_t>(ret);
|
||||
continue;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
if (std::chrono::steady_clock::now() >= writeDeadline) {
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
return static_cast<int32_t>(totalWritten);
|
||||
}
|
||||
|
||||
static int32_t SslRead(SslSession&, uint8_t*, uint32_t) {
|
||||
return SSL_ERR_FAILED;
|
||||
static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
||||
if (!out || size == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
const int32_t handshakeRet = SslHandshake(ssl);
|
||||
if (handshakeRet != SSL_OK) {
|
||||
return handshakeRet;
|
||||
}
|
||||
|
||||
if (ssl.plaintextWfc) {
|
||||
const ssize_t ret = recv(ssl.native, out, size, 0);
|
||||
if (ret == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
if (ret < 0) {
|
||||
return SSL_ERR_RAGAIN;
|
||||
}
|
||||
return static_cast<int32_t>(ret);
|
||||
}
|
||||
|
||||
const int ret = mbedtls_ssl_read(&ssl.sslContext, out, size);
|
||||
if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
return SSL_ERR_RAGAIN;
|
||||
}
|
||||
if (ret < 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The handshake runs on every SSL read/write, so a failure repeats for as long
|
||||
// as the session lives; report only the first one.
|
||||
static int32_t SslHandshake(SslSession& ssl) {
|
||||
if (ssl.failed) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
const int32_t result = SslHandshakeImpl(ssl);
|
||||
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
|
||||
ssl.loggedHandshakeFail = true;
|
||||
@@ -640,6 +936,14 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
const int timeoutMs = 15000;
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
||||
#else
|
||||
// Match the Windows 15s timeout so a peer that accepts the TCP connection but stalls
|
||||
// during the TLS handshake or a later read/write can't hang this thread forever. POSIX
|
||||
// takes a struct timeval here, not a plain millisecond count like Windows does.
|
||||
struct timeval timeout {};
|
||||
timeout.tv_sec = 15;
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
#endif
|
||||
WriteSslReturn(in, SSL_OK);
|
||||
return 0;
|
||||
@@ -656,6 +960,11 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
WriteSslReturn(in, SSL_ERR_ID);
|
||||
return 0;
|
||||
}
|
||||
// Reject before NAS buffering can acknowledge data on a failed session.
|
||||
if (g_sslSessions[sslId].failed) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
}
|
||||
if (out.size() < 2 || !out[1].address) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
|
||||
@@ -222,7 +222,6 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
throw;
|
||||
}
|
||||
DecrementSchedulerDisableCount();
|
||||
RunDeferredReschedule(cpu);
|
||||
}
|
||||
}
|
||||
} catch (const ::Memory::AccessViolation& e) {
|
||||
@@ -233,7 +232,7 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
// Host DNS workers never touch guest memory. Commit their output here on
|
||||
// the scheduler thread, waking synchronous IOS waiters or queuing async IOS
|
||||
// callbacks before the callback drain below.
|
||||
bool completionNeedsReschedule = false;
|
||||
bool completionNeedsReschedule = handledAny;
|
||||
if (Network_HLE_ProcessCompletions(cpu)) {
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
@@ -446,15 +445,18 @@ PPC_NATIVE_OVERRIDE_VOID(801A08E0, OS__SetPeriodicAlarm_801a08e0, (CpuContext* c
|
||||
// returning 0 when the manager pointer (0x80386298) is null.
|
||||
extern "C" uint32_t RFLiIsWorking_HLE_800bd860()
|
||||
{
|
||||
// Pump alarms/callbacks on the current guest thread when available. Using a
|
||||
// detached persistent context here can leave the busy loop waiting on work
|
||||
// that completed on the wrong scheduling context.
|
||||
CpuContext* cpu = TryGetCpuContext();
|
||||
if (!cpu) {
|
||||
cpu = &GetPersistentCpuContext();
|
||||
}
|
||||
// Alarm callbacks interrupt the caller; keep their register writes private.
|
||||
GuestInterruptCallbackContext interrupt;
|
||||
CpuContext* cpu = interrupt.get();
|
||||
EnsureSda1Base(cpu);
|
||||
ProcessAlarmQueue(cpu, 32);
|
||||
IncrementSchedulerDisableCount();
|
||||
try {
|
||||
ProcessAlarmQueue(cpu, 32);
|
||||
} catch (...) {
|
||||
DecrementSchedulerDisableCount();
|
||||
throw;
|
||||
}
|
||||
DecrementSchedulerDisableCount();
|
||||
|
||||
// Now return the actual "working" status
|
||||
constexpr uint32_t kRflManagerPtrAddr = 0x80386298u;
|
||||
|
||||
@@ -104,10 +104,15 @@ extern "C" void DVDInit_8015EA1C();
|
||||
extern "C" uint32_t g_dvdFstReservedBase;
|
||||
extern "C" uint32_t g_dvdFstReservedSize;
|
||||
|
||||
// Byte-wise copy into guest RAM plus the DMA notification the GX caches need.
|
||||
// DVD DMA normally targets ordinary RAM. Keep scalar writes for executable
|
||||
// ranges so the translated-code write guard still sees every modified byte.
|
||||
static void CopyToGuestAsDma(uint32_t dest, const uint8_t* data, size_t size) {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
Memory::Write8(dest + static_cast<uint32_t>(i), data[i]);
|
||||
if (size != 0 && !RecompMod::ExecutableWriteGuardMayHit(dest, size)) {
|
||||
std::memcpy(Memory::GetPointer(dest, size), data, size);
|
||||
} else {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
Memory::Write8(dest + static_cast<uint32_t>(i), data[i]);
|
||||
}
|
||||
}
|
||||
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
#include "nand_internal.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <linux/fs.h>
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Local helpers
|
||||
// ============================================================================
|
||||
@@ -44,6 +52,32 @@ static FileHandle* ResolveNandFileHandle(const char* who, uint32_t fileInfoPtr)
|
||||
// The synchronous RVL NAND* library
|
||||
// ============================================================================
|
||||
|
||||
static bool RenameNoReplace(const std::filesystem::path& from,
|
||||
const std::filesystem::path& to,
|
||||
std::error_code& error) {
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
error = std::error_code(static_cast<int>(GetLastError()), std::system_category());
|
||||
return false;
|
||||
#elif defined(__linux__)
|
||||
const int result = syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE);
|
||||
if (result == 0) {
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
error = std::error_code(errno, std::generic_category());
|
||||
return false;
|
||||
#else
|
||||
(void)from;
|
||||
(void)to;
|
||||
error = std::make_error_code(std::errc::operation_not_supported);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" int32_t NANDInit_HLE(void) {
|
||||
// Initialize ISFS
|
||||
ISFS_OpenLib_Initialize(&GetPersistentCpuContext());
|
||||
@@ -348,6 +382,11 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
||||
PPC_NATIVE_OVERRIDE(8019BBE0, NANDCreateDir_HLE, int32_t, (uint32_t pathPtr, uint32_t perm, uint32_t attr), (pathPtr, perm, attr));
|
||||
|
||||
extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
||||
// A cross-mount move is implemented as several host operations. Keep two
|
||||
// guest moves from interleaving those operations and corrupting recovery.
|
||||
static std::mutex moveMutex;
|
||||
std::lock_guard<std::mutex> lock(moveMutex);
|
||||
|
||||
const char* srcPath = srcPathPtr ? (const char*)Memory::GetPointer(srcPathPtr) : nullptr;
|
||||
const char* dstPath = dstPathPtr ? (const char*)Memory::GetPointer(dstPathPtr) : nullptr;
|
||||
|
||||
@@ -383,6 +422,112 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
// Flatpak can expose the managed NAND and an external Riivolution save
|
||||
// directory as separate mounts. Linux cannot rename across mounts, but
|
||||
// nandMove must still work for files such as banner.bin. Preserve the
|
||||
// operation's semantics with a copy followed by source removal.
|
||||
if (ec == std::errc::cross_device_link) {
|
||||
static std::atomic<uint64_t> moveSequence{0};
|
||||
#ifdef _WIN32
|
||||
const auto processId = GetCurrentProcessId();
|
||||
#else
|
||||
const auto processId = getpid();
|
||||
#endif
|
||||
std::filesystem::path scratchHost;
|
||||
std::error_code scratchEc;
|
||||
for (unsigned attempt = 0; attempt < 128; ++attempt) {
|
||||
const auto name = ".nandmove-" + std::to_string(processId) + "-" +
|
||||
std::to_string(moveSequence.fetch_add(1)) + "-" +
|
||||
std::to_string(attempt);
|
||||
const auto candidate = dstDirectoryHost / name;
|
||||
scratchEc.clear();
|
||||
if (std::filesystem::create_directory(candidate, scratchEc)) {
|
||||
scratchHost = candidate;
|
||||
break;
|
||||
}
|
||||
if (scratchEc && scratchEc != std::errc::file_exists) {
|
||||
LogNandError("NANDMove", "failed to claim temporary directory '%s': %s",
|
||||
HostPathText(candidate).c_str(), scratchEc.message().c_str());
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
if (scratchHost.empty()) {
|
||||
LogNandError("NANDMove", "could not claim a unique temporary directory");
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
const bool sourceIsDirectory = IsDirectory(srcHost);
|
||||
const std::filesystem::path tempHost = scratchHost / srcName;
|
||||
const auto cleanupScratch = [&]() {
|
||||
std::error_code cleanupEc;
|
||||
std::filesystem::remove_all(scratchHost, cleanupEc);
|
||||
if (cleanupEc) {
|
||||
LogNandError("NANDMove", "failed to clean up temporary directory '%s': %s",
|
||||
HostPathText(scratchHost).c_str(), cleanupEc.message().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
std::error_code copyEc;
|
||||
if (sourceIsDirectory) {
|
||||
std::filesystem::copy(srcHost, tempHost,
|
||||
std::filesystem::copy_options::recursive, copyEc);
|
||||
} else {
|
||||
std::filesystem::copy_file(srcHost, tempHost, copyEc);
|
||||
}
|
||||
|
||||
if (copyEc) {
|
||||
LogNandError("NANDMove", "cross-mount copy failed: %s", copyEc.message().c_str());
|
||||
cleanupScratch();
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
std::error_code publishEc;
|
||||
if (sourceIsDirectory) {
|
||||
RenameNoReplace(tempHost, dstHost, publishEc);
|
||||
} else {
|
||||
// link(2) and CreateHardLink do not replace an existing destination,
|
||||
// unlike rename(2) on POSIX. Both paths are already on the target
|
||||
// filesystem, so the link is a no-replace publication operation.
|
||||
std::filesystem::create_hard_link(tempHost, dstHost, publishEc);
|
||||
}
|
||||
if (publishEc) {
|
||||
LogNandError("NANDMove", "failed to publish cross-mount copy: %s",
|
||||
publishEc.message().c_str());
|
||||
cleanupScratch();
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
cleanupScratch();
|
||||
|
||||
std::error_code removeEc;
|
||||
std::filesystem::remove_all(srcHost, removeEc);
|
||||
if (!removeEc) {
|
||||
LogNandWarning("NANDMove", "used copy/remove fallback across mounts");
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
// Keep the source as the authoritative copy when cleanup fails. The
|
||||
// destination was published atomically on its own mount; regular files
|
||||
// are rolled back below, while directories keep the complete copy when
|
||||
// their source removal was only partial. Cross-mount moves cannot
|
||||
// provide crash-atomicity, so this is best effort.
|
||||
LogNandError("NANDMove", "copy succeeded but source removal failed: %s",
|
||||
removeEc.message().c_str());
|
||||
if (sourceIsDirectory) {
|
||||
// remove_all may have removed only part of a directory tree. Keep
|
||||
// the complete published copy rather than rolling it back to a
|
||||
// partially deleted source.
|
||||
LogNandWarning("NANDMove", "preserving published directory copy after partial source removal");
|
||||
} else {
|
||||
std::error_code rollbackEc;
|
||||
std::filesystem::remove_all(dstHost, rollbackEc);
|
||||
if (rollbackEc) {
|
||||
LogNandError("NANDMove", "failed to roll back destination '%s': %s",
|
||||
HostPathText(dstHost).c_str(), rollbackEc.message().c_str());
|
||||
}
|
||||
}
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
LogNandError("NANDMove", "FAILED error=%d message='%s'", ec.value(), ec.message().c_str());
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -1551,6 +1551,10 @@ int RuntimeMain(int argc, char** argv) {
|
||||
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
||||
|
||||
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
||||
if (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
|
||||
throw std::runtime_error(auroraInfo.initializationError != nullptr
|
||||
? auroraInfo.initializationError : "No supported graphics backend is available");
|
||||
}
|
||||
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
|
||||
<< "\" is not available on this system; aurora fell back to \""
|
||||
|
||||
@@ -48,7 +48,7 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
|
||||
return (ec ? std::filesystem::path(path) : resolved).parent_path();
|
||||
#elif defined(__ANDROID__)
|
||||
// Bundled read-only runtime resources (wii_bootstrap/, dsp_coef.bin,
|
||||
// initial_pipeline_cache.db) unpacked from the APK by the activity. Stands
|
||||
// initial_pipeline_cache.db, cacert.pem) unpacked from the APK by the activity. Stands
|
||||
// in for the executable directory the desktop builds look next to.
|
||||
if (const char* resources = std::getenv("MKW_ANDROID_RESOURCES_DIR"); resources && *resources) {
|
||||
return std::filesystem::path(resources);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "update_check.h"
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -2094,6 +2095,107 @@ void DrawDiagnosticsSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_updatePromptOpen = false;
|
||||
std::string g_updateError;
|
||||
|
||||
void DrawUpdatePrompt() {
|
||||
constexpr const char* kTitle = "Update";
|
||||
if (g_updatePromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
|
||||
if (!ImGui::BeginPopupModal(kTitle, &g_updatePromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
ImGui::TextUnformatted("The update is built here on the Frame, in the background, which takes a "
|
||||
"while: keep the Frame on its charger. You can keep playing meanwhile, "
|
||||
"though the game may stutter while it compiles. Close the game whenever "
|
||||
"you like: the update carries on and opens it again when it is done.");
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::Button("Start the update", ImVec2(Scaled(180.0f), 0.0f))) {
|
||||
update_check::StartUpdate(g_updateError);
|
||||
g_updatePromptOpen = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(Scaled(120.0f), 0.0f))) g_updatePromptOpen = false;
|
||||
if (!g_updatePromptOpen) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void DrawUpdateSettings() {
|
||||
const update_check::Status status = update_check::Current();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
if (status.installedRelease.empty()) {
|
||||
ImGui::TextUnformatted("This build came from a source tree rather than a release.");
|
||||
} else {
|
||||
ImGui::Text("Release %s", status.installedRelease.c_str());
|
||||
}
|
||||
|
||||
const bool busy = status.state == update_check::State::Checking ||
|
||||
status.state == update_check::State::Updating;
|
||||
switch (status.state) {
|
||||
case update_check::State::PcInstall:
|
||||
ImGui::TextDisabled("The PC that built this installs its updates. Run the installer there "
|
||||
"again with --update.");
|
||||
break;
|
||||
case update_check::State::Idle:
|
||||
break;
|
||||
case update_check::State::Checking:
|
||||
ImGui::TextDisabled("Looking for a newer release...");
|
||||
break;
|
||||
case update_check::State::UpToDate:
|
||||
ImGui::TextDisabled("%s", status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Available:
|
||||
ImGui::TextUnformatted(status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Updating:
|
||||
ImGui::Text("Updating: %s", status.detail.c_str());
|
||||
ImGui::TextDisabled("Keep playing, or close the game: the update carries on and opens it "
|
||||
"again when it is done.");
|
||||
break;
|
||||
case update_check::State::Failed:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", status.detail.c_str());
|
||||
break;
|
||||
}
|
||||
if (!g_updateError.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", g_updateError.c_str());
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
// The update replaced the build on disk; this process is still the old one.
|
||||
ImGui::TextUnformatted("The update is installed. Quit and start the game again to use it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (status.state == update_check::State::PcInstall) {
|
||||
return;
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
if (ImGui::Button("Quit")) g_exitPromptOpen = true;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(busy);
|
||||
if (ImGui::Button(status.state == update_check::State::Idle ? "Check for updates" : "Check again")) {
|
||||
g_updateError.clear();
|
||||
update_check::StartCheck();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Asks github.com for the newest release, and Retro Rewind's server for\n"
|
||||
"its newest pack when this install has one. Nothing is downloaded\n"
|
||||
"until you choose to update.");
|
||||
}
|
||||
if (status.state == update_check::State::Available) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update now")) {
|
||||
g_updateError.clear();
|
||||
g_updatePromptOpen = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Builds the update here on the Frame, in the background. The game\n"
|
||||
"can stay open and shows the progress here.");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
// The prompt itself is drawn at the top level, as the exit prompt is: the tab lives both in the
|
||||
// headset's panel and in a desktop menu, and a modal must outlive whichever opened it.
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
AuroraPresentTiming presentTiming{};
|
||||
aurora_get_present_timing(&presentTiming);
|
||||
@@ -2291,6 +2393,11 @@ void DrawTopBar() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (update_check::Relevant(update_check::Current()) && ImGui::BeginMenu("Updates")) {
|
||||
DrawUpdateSettings();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f;
|
||||
const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f;
|
||||
@@ -2479,6 +2586,9 @@ void DrawVrSettingsPanelWindow() {
|
||||
tab("Controllers", DrawControllerSettings);
|
||||
tab("Audio", DrawAudioSettings);
|
||||
tab("Diagnostics", DrawDiagnosticsSettings);
|
||||
if (update_check::Relevant(update_check::Current())) {
|
||||
tab("Updates", DrawUpdateSettings);
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
@@ -2687,6 +2797,7 @@ void Draw() noexcept {
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
DrawExitPrompt();
|
||||
DrawUpdatePrompt();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "update_check.h"
|
||||
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
// Updating itself is the Frame's build, installed by Launcher/steam-frame-install.sh: a desktop
|
||||
// Linux game that holds its own build directory. Everywhere else the tab only reports the release
|
||||
// it is running, which needs none of the machinery below.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 1
|
||||
#include <sys/wait.h>
|
||||
#else
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 0
|
||||
#endif
|
||||
|
||||
namespace update_check {
|
||||
namespace {
|
||||
|
||||
// Written by Launcher/steam-frame-install.sh when it built on this machine, naming itself and the
|
||||
// service that runs it. Its absence is what tells the tab this install is updated from a PC.
|
||||
constexpr std::string_view kConfigFileName = "update.conf";
|
||||
// An update's progress, and a check's answer. Separate files, so a check never overwrites the
|
||||
// result of the update that opened the game.
|
||||
constexpr std::string_view kStatusFileName = "update-status";
|
||||
constexpr std::string_view kCheckStatusFileName = "update-status-check";
|
||||
// What the Update button leaves for the update to pick up.
|
||||
constexpr std::string_view kRequestFileName = "update-request";
|
||||
// Stamped beside the executable by the install; absent for a build from a source tree.
|
||||
constexpr std::string_view kReleaseTagFileName = ".release-tag";
|
||||
|
||||
// A finished update's status belongs on screen when the game opens right afterwards, which is what
|
||||
// the update does. Older than this it is history, and the tab starts clean.
|
||||
constexpr auto kStatusLifetime = std::chrono::hours(1);
|
||||
// A running update rewrites its status at every step and every percent of its build, so one that
|
||||
// has said nothing for this long was stopped without a chance to say so (the Frame lost power).
|
||||
constexpr auto kRunningStatusLifetime = std::chrono::hours(2);
|
||||
// The tab is drawn every frame; the files behind it are not read anywhere near that often.
|
||||
constexpr auto kReadInterval = std::chrono::seconds(1);
|
||||
|
||||
std::string Trimmed(std::string_view text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return std::string(text.substr(first, text.find_last_not_of(" \t\r\n") - first + 1));
|
||||
}
|
||||
|
||||
std::string ReadFirstLine(const std::filesystem::path& file) {
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
if (!stream || !std::getline(stream, line)) {
|
||||
return {};
|
||||
}
|
||||
return Trimmed(line);
|
||||
}
|
||||
|
||||
struct Config {
|
||||
std::filesystem::path script;
|
||||
// The install's work directory, which holds the build the update reuses. The script defaults to
|
||||
// the same place, but an install given --work-dir does not live there.
|
||||
std::filesystem::path workDir;
|
||||
std::string service;
|
||||
|
||||
bool Usable() const { return !script.empty() && !service.empty(); }
|
||||
};
|
||||
|
||||
Config ReadConfig() {
|
||||
Config config;
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
return config;
|
||||
#else
|
||||
std::ifstream stream(RuntimeConfigFile::ApplicationDataDirectory() / kConfigFileName);
|
||||
if (!stream) {
|
||||
return config;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
const auto separator = line.find('=');
|
||||
if (line.empty() || line.front() == '#' || separator == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string key = Trimmed(std::string_view(line).substr(0, separator));
|
||||
const std::string value = Trimmed(std::string_view(line).substr(separator + 1));
|
||||
if (key == "script") {
|
||||
config.script = value;
|
||||
} else if (key == "work_dir") {
|
||||
config.workDir = value;
|
||||
} else if (key == "service") {
|
||||
config.service = value;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
if (!config.script.empty() && !std::filesystem::is_regular_file(config.script, ec)) {
|
||||
// The work directory was removed, so there is nothing here to update from any more.
|
||||
config.script.clear();
|
||||
}
|
||||
return config;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InstalledRelease() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
return ReadFirstLine(*executableDirectory / kReleaseTagFileName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// What this session has been told, which outranks the files until the game is next started: a check
|
||||
// or update it asked for, and what came back.
|
||||
std::mutex g_mutex;
|
||||
bool g_sessionStateSet = false;
|
||||
State g_sessionState = State::Idle;
|
||||
std::string g_sessionDetail;
|
||||
// What was last read from disk, and when, so drawing the tab does not re-read it every frame.
|
||||
std::chrono::steady_clock::time_point g_lastRead;
|
||||
bool g_haveRead = false;
|
||||
bool g_haveFileState = false;
|
||||
State g_fileState = State::Idle;
|
||||
std::string g_fileDetail;
|
||||
bool g_canUpdate = false;
|
||||
// The release this process was started from, read once: an update replaces the stamp on disk while
|
||||
// the old build is still the one running. The stamp as it is now tells whether that has happened.
|
||||
std::string g_runningRelease;
|
||||
bool g_haveRunningRelease = false;
|
||||
std::string g_releaseOnDisk;
|
||||
|
||||
void SetSessionState(State state, std::string detail) {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_sessionStateSet = true;
|
||||
g_sessionState = state;
|
||||
g_sessionDetail = std::move(detail);
|
||||
}
|
||||
|
||||
std::string ShellQuoted(const std::string& text) {
|
||||
std::string quoted = "'";
|
||||
for (const char character : text) {
|
||||
if (character == '\'') {
|
||||
quoted += "'\\''";
|
||||
} else {
|
||||
quoted += character;
|
||||
}
|
||||
}
|
||||
quoted += '\'';
|
||||
return quoted;
|
||||
}
|
||||
|
||||
// Runs `command`, returning its exit status and keeping the last line of its output for a message.
|
||||
int Run(const std::string& command, std::string& lastLine) {
|
||||
lastLine.clear();
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
(void)command;
|
||||
return -1;
|
||||
#else
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
std::array<char, 512> buffer{};
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
if (const std::string line = Trimmed(buffer.data()); !line.empty()) {
|
||||
lastLine = line;
|
||||
}
|
||||
}
|
||||
const int status = pclose(pipe);
|
||||
if (status == -1 || !WIFEXITED(status)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reads a "<state>\t<unix time>\t<text>" line the script wrote. False when there is none, it is too
|
||||
// old to be about now, or it cannot be understood.
|
||||
bool ReadStatusFile(const std::filesystem::path& file, State& state, std::string& detail) {
|
||||
const std::string line = ReadFirstLine(file);
|
||||
if (line.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto firstTab = line.find('\t');
|
||||
const auto secondTab = line.find('\t', firstTab + 1);
|
||||
if (firstTab == std::string::npos || secondTab == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::string written = line.substr(0, firstTab);
|
||||
long long seconds = 0;
|
||||
const char* const secondsBegin = line.data() + firstTab + 1;
|
||||
if (std::from_chars(secondsBegin, line.data() + secondTab, seconds).ec != std::errc{}) {
|
||||
return false;
|
||||
}
|
||||
const auto age = std::chrono::system_clock::now() -
|
||||
std::chrono::system_clock::time_point(std::chrono::seconds(seconds));
|
||||
const auto lifetime = written == "running" ? kRunningStatusLifetime : kStatusLifetime;
|
||||
if (age > lifetime || age < -lifetime) {
|
||||
return false;
|
||||
}
|
||||
if (written == "running") {
|
||||
state = State::Updating;
|
||||
} else if (written == "available") {
|
||||
state = State::Available;
|
||||
} else if (written == "uptodate" || written == "done") {
|
||||
state = State::UpToDate;
|
||||
} else if (written == "failed") {
|
||||
state = State::Failed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
detail = line.substr(secondTab + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Current() {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
// Drawn every frame, so the files behind it are read on a timer instead.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!g_haveRead || now - g_lastRead > kReadInterval) {
|
||||
g_releaseOnDisk = InstalledRelease();
|
||||
if (!g_haveRunningRelease) {
|
||||
g_runningRelease = g_releaseOnDisk;
|
||||
g_haveRunningRelease = true;
|
||||
}
|
||||
g_lastRead = now;
|
||||
g_haveRead = true;
|
||||
g_canUpdate = ReadConfig().Usable();
|
||||
g_fileState = State::Idle;
|
||||
g_fileDetail.clear();
|
||||
g_haveFileState = ReadStatusFile(RuntimeConfigFile::ApplicationDataDirectory() / kStatusFileName,
|
||||
g_fileState, g_fileDetail);
|
||||
}
|
||||
|
||||
Status status;
|
||||
status.installedRelease = g_runningRelease;
|
||||
status.restartNeeded = g_releaseOnDisk != g_runningRelease;
|
||||
if (!g_canUpdate) {
|
||||
status.state = State::PcInstall;
|
||||
return status;
|
||||
}
|
||||
// An update's own reports outrank what this session last said: one started here (the session
|
||||
// says Updating until the update writes its first step), or one this game was reopened after.
|
||||
const bool followUpdate = !g_sessionStateSet || g_sessionState == State::Updating;
|
||||
if (followUpdate && g_haveFileState) {
|
||||
status.state = g_fileState;
|
||||
status.detail = g_fileDetail;
|
||||
} else if (g_sessionStateSet) {
|
||||
status.state = g_sessionState;
|
||||
status.detail = g_sessionDetail;
|
||||
} else {
|
||||
status.state = State::Idle;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void StartCheck() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (g_sessionStateSet && (g_sessionState == State::Checking || g_sessionState == State::Updating)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
return;
|
||||
}
|
||||
SetSessionState(State::Checking, "Asking github.com for the newest release");
|
||||
|
||||
// Detached: it talks to the network and must not hold up a frame, and there is no shutdown
|
||||
// ordering to manage because it touches nothing the game owns.
|
||||
std::thread([config] {
|
||||
const auto statusFile = RuntimeConfigFile::ApplicationDataDirectory() / kCheckStatusFileName;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
std::string command = "WIICOMPILED_UPDATE_STATUS=" + ShellQuoted(statusFile.string()) + ' ' +
|
||||
ShellQuoted(config.script.string()) + " --check";
|
||||
if (!config.workDir.empty()) {
|
||||
command += " --work-dir " + ShellQuoted(config.workDir.string());
|
||||
}
|
||||
command += " 2>&1";
|
||||
std::string lastLine;
|
||||
const int exitStatus = Run(command, lastLine);
|
||||
|
||||
State state = State::Failed;
|
||||
std::string detail;
|
||||
if (ReadStatusFile(statusFile, state, detail)) {
|
||||
SetSessionState(state, detail);
|
||||
} else if (exitStatus == 0) {
|
||||
// It ran and said nothing this can read, which is still an answer: ask again later.
|
||||
SetSessionState(State::Idle, "The check said nothing this version understands");
|
||||
} else {
|
||||
std::cerr << "[update] the check failed (exit " << exitStatus << "): " << lastLine << std::endl;
|
||||
SetSessionState(State::Failed,
|
||||
lastLine.empty() ? "The check could not be run" : lastLine);
|
||||
}
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
bool StartUpdate(std::string& error) {
|
||||
error.clear();
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
error = "This build was installed from a PC, so the update is run there.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto dataDirectory = RuntimeConfigFile::ApplicationDataDirectory();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dataDirectory, ec);
|
||||
// How the update opens the game again when it is done. Steam names the running title; without
|
||||
// one (a build started from a terminal) the update still runs and the game is started by hand.
|
||||
std::string steamGameId;
|
||||
for (const char* variable : {"SteamGameId", "SteamAppId"}) {
|
||||
if (const char* value = std::getenv(variable); value != nullptr && *value != '\0') {
|
||||
steamGameId = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::ofstream request(dataDirectory / kRequestFileName, std::ios::trunc);
|
||||
if (!request) {
|
||||
error = "Could not write to " + dataDirectory.string();
|
||||
return false;
|
||||
}
|
||||
request << "# Written by the game's Update button; the update removes it.\n";
|
||||
if (!steamGameId.empty()) {
|
||||
request << "steam_game_id=" << steamGameId << '\n';
|
||||
}
|
||||
}
|
||||
// The previous update's result must not be read as this one's before it has written anything.
|
||||
std::filesystem::remove(dataDirectory / kStatusFileName, ec);
|
||||
// The update must outlive the game if the player closes it meanwhile, and Steam ends the game's
|
||||
// own processes when it closes, so it runs as a service of its own. --no-block: started, not
|
||||
// waited for.
|
||||
std::string lastLine;
|
||||
const std::string command =
|
||||
"systemctl --user start --no-block " + ShellQuoted(config.service) + " 2>&1";
|
||||
if (const int exitStatus = Run(command, lastLine); exitStatus != 0) {
|
||||
std::filesystem::remove(dataDirectory / kRequestFileName, ec);
|
||||
std::cerr << "[update] could not start " << config.service << " (exit " << exitStatus
|
||||
<< "): " << lastLine << std::endl;
|
||||
error = lastLine.empty() ? "Could not start the update service" : lastLine;
|
||||
return false;
|
||||
}
|
||||
SetSessionState(State::Updating, "Starting the update");
|
||||
// Read the files again at once, rather than show the removed status for up to a second.
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_haveRead = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace update_check
|
||||
|
||||
#undef MKW_UPDATE_CHECK_SUPPORTED
|
||||
@@ -0,0 +1,123 @@
|
||||
// The AX mix kernels' vector forms (AVX2 on x86-64, NEON on arm64) must be bit-exact with the
|
||||
// scalar reference loops they replace. Random blocks at every tail length, plus the ramp and
|
||||
// clamp extremes; on a build with neither vector form this compares the scalar loops with
|
||||
// themselves.
|
||||
|
||||
#include "../src/hle/audio/ax_mix_kernels.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Fail(const char* kernel, uint32_t count, uint32_t volume, uint32_t delta) {
|
||||
if (++g_failures <= 10) {
|
||||
std::cerr << kernel << " differs from the scalar loop: count " << count << ", volume "
|
||||
<< volume << ", delta " << delta << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<T> RandomBlock(std::mt19937& rng, uint32_t count, int64_t low, int64_t high) {
|
||||
std::uniform_int_distribution<int64_t> dist(low, high);
|
||||
std::vector<T> block(count);
|
||||
for (auto& value : block) {
|
||||
value = static_cast<T>(dist(rng));
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
void CheckRamps(std::mt19937& rng, uint32_t count, uint16_t volume, uint16_t delta) {
|
||||
const auto input = RandomBlock<int16_t>(rng, count, INT16_MIN, INT16_MAX);
|
||||
const auto bus = RandomBlock<int32_t>(rng, count, -(1 << 24), 1 << 24);
|
||||
|
||||
auto expectedOut = bus;
|
||||
auto actualOut = bus;
|
||||
int16_t expectedDpop = 1234;
|
||||
int16_t actualDpop = 1234;
|
||||
const uint16_t expectedVolume =
|
||||
AxMixKernels::MixAddRampScalar(expectedOut.data(), input.data(), count, volume, delta, expectedDpop);
|
||||
const uint16_t actualVolume =
|
||||
AxMixKernels::MixAddRamp(actualOut.data(), input.data(), count, volume, delta, actualDpop);
|
||||
if (expectedOut != actualOut || expectedDpop != actualDpop || expectedVolume != actualVolume) {
|
||||
Fail("MixAddRamp", count, volume, delta);
|
||||
}
|
||||
|
||||
auto expectedSamples = input;
|
||||
auto actualSamples = input;
|
||||
const uint16_t expectedScaled = AxMixKernels::ScaleRampScalar(expectedSamples.data(), count, volume, delta);
|
||||
const uint16_t actualScaled = AxMixKernels::ScaleRamp(actualSamples.data(), count, volume, delta);
|
||||
if (expectedSamples != actualSamples || expectedScaled != actualScaled) {
|
||||
Fail("ScaleRamp", count, volume, delta);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckAccumAndMarshal(std::mt19937& rng, uint32_t count) {
|
||||
const auto src = RandomBlock<int32_t>(rng, count, INT32_MIN, INT32_MAX);
|
||||
const auto ramp = RandomBlock<uint16_t>(rng, count, 0, UINT16_MAX);
|
||||
const auto bus = RandomBlock<int32_t>(rng, count, -(1 << 24), 1 << 24);
|
||||
auto expected = bus;
|
||||
auto actual = bus;
|
||||
AxMixKernels::MixAccumRamp32Scalar(expected.data(), src.data(), ramp.data(), count);
|
||||
AxMixKernels::MixAccumRamp32(actual.data(), src.data(), ramp.data(), count);
|
||||
if (expected != actual) {
|
||||
Fail("MixAccumRamp32", count, 0, 0);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> expectedBytes(count * 4);
|
||||
std::vector<uint8_t> actualBytes(count * 4);
|
||||
AxMixKernels::StoreBigEndian32Scalar(expectedBytes.data(), src.data(), count);
|
||||
AxMixKernels::StoreBigEndian32(actualBytes.data(), src.data(), count);
|
||||
if (expectedBytes != actualBytes) {
|
||||
Fail("StoreBigEndian32", count, 0, 0);
|
||||
}
|
||||
|
||||
std::vector<int32_t> expectedWords(count);
|
||||
std::vector<int32_t> actualWords(count);
|
||||
AxMixKernels::LoadBigEndian32Scalar(expectedWords.data(), expectedBytes.data(), count);
|
||||
AxMixKernels::LoadBigEndian32(actualWords.data(), expectedBytes.data(), count);
|
||||
if (expectedWords != actualWords || expectedWords != src) {
|
||||
Fail("LoadBigEndian32", count, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
std::mt19937 rng(0x41584D58u);
|
||||
std::uniform_int_distribution<uint32_t> any16(0, UINT16_MAX);
|
||||
const uint16_t edges[] = {0, 1, 0x7FFF, 0x8000, 0x8001, 0xFFFE, 0xFFFF};
|
||||
|
||||
// Every tail length around the vector widths, and the AX frame sizes (96 per 3 ms frame,
|
||||
// 160 at the 5 ms subframe the AXWii list can use).
|
||||
for (uint32_t count = 0; count <= 40; ++count) {
|
||||
for (uint16_t volume : edges) {
|
||||
for (uint16_t delta : edges) {
|
||||
CheckRamps(rng, count, volume, delta);
|
||||
}
|
||||
}
|
||||
for (int trial = 0; trial < 64; ++trial) {
|
||||
CheckRamps(rng, count, static_cast<uint16_t>(any16(rng)), static_cast<uint16_t>(any16(rng)));
|
||||
}
|
||||
CheckAccumAndMarshal(rng, count);
|
||||
}
|
||||
for (uint32_t count : {96u, 160u, 255u}) {
|
||||
for (int trial = 0; trial < 256; ++trial) {
|
||||
CheckRamps(rng, count, static_cast<uint16_t>(any16(rng)), static_cast<uint16_t>(any16(rng)));
|
||||
CheckAccumAndMarshal(rng, count);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " mismatches\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "ax mix kernels match the scalar loops (avx2 " << MKW_AX_MIX_AVX2 << ", neon "
|
||||
<< MKW_AX_MIX_NEON << ")\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -27,7 +27,8 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> stateFreeAbiContracts,
|
||||
IReadOnlyDictionary<uint, string> stateFreeCallSymbols,
|
||||
IReadOnlyDictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant> stateFreeCallSiteVariants,
|
||||
IReadOnlySet<uint> modOverridableCallTargets)
|
||||
IReadOnlySet<uint> modOverridableCallTargets,
|
||||
bool shareLrContinuationDispatch)
|
||||
{
|
||||
if (ins is IrPhi)
|
||||
{
|
||||
@@ -410,11 +411,18 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
fallbackPad = IndentPad(indent + 1);
|
||||
}
|
||||
|
||||
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
|
||||
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
|
||||
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
sb.AppendLine($"{fallbackPad}}}");
|
||||
sb.AppendLine($"{fallbackPad}return;");
|
||||
if (shareLrContinuationDispatch)
|
||||
{
|
||||
sb.AppendLine($"{fallbackPad}goto lr_continuation_dispatch;");
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
|
||||
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
|
||||
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
sb.AppendLine($"{fallbackPad}}}");
|
||||
sb.AppendLine($"{fallbackPad}return;");
|
||||
}
|
||||
if (localFallthroughLr.HasValue)
|
||||
{
|
||||
sb.AppendLine($"{pad}}}");
|
||||
|
||||
@@ -241,12 +241,14 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
|
||||
var labelNames = func.Blocks.ToDictionary(b => b.Label, b => SanitizeLabel(b.Label), StringComparer.OrdinalIgnoreCase);
|
||||
var instructionContinuationLabels = new Dictionary<uint, string>();
|
||||
var needsInstructionContinuationLabels = func.Blocks
|
||||
var continuationCallCount = func.Blocks
|
||||
.SelectMany(static block => block.Instructions)
|
||||
.OfType<IrCall>()
|
||||
.Any(call =>
|
||||
.Count(call =>
|
||||
TryParseAddress(call.Target, out var target) &&
|
||||
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)));
|
||||
var needsInstructionContinuationLabels = continuationCallCount > 0;
|
||||
var shareLrContinuationDispatch = continuationCallCount > 1;
|
||||
if (needsInstructionContinuationLabels)
|
||||
{
|
||||
foreach (var trace in func.Blocks.SelectMany(static block => block.Instructions).OfType<IrTracePpc>())
|
||||
@@ -405,7 +407,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
_activeGpuFifoBurstSlot = gpuFifoBurstPlan.Slot(block.Label, i);
|
||||
try
|
||||
{
|
||||
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
|
||||
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -423,7 +425,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
switch (term)
|
||||
{
|
||||
case IrUndefined undef:
|
||||
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
|
||||
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
|
||||
AppendFlush(body, " ");
|
||||
body.AppendLine(" return;");
|
||||
break;
|
||||
@@ -509,6 +511,18 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
body.AppendLine();
|
||||
}
|
||||
|
||||
if (shareLrContinuationDispatch)
|
||||
{
|
||||
// Every call site has already reloaded the callee's state.
|
||||
// Keep the complete local target set, but emit it only once.
|
||||
body.AppendLine(" return;");
|
||||
body.AppendLine("[[maybe_unused]] lr_continuation_dispatch:");
|
||||
EmitLocalLrContinuationDispatch(body, " ", labelNames);
|
||||
body.AppendLine(" if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {");
|
||||
body.AppendLine(" InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
body.AppendLine(" }");
|
||||
body.AppendLine(" return;");
|
||||
}
|
||||
body.Append('}');
|
||||
// The residency discovery pass only exists for its side effects
|
||||
// on the recorder; materializing its text costs a full copy of
|
||||
|
||||
@@ -1942,6 +1942,22 @@ public sealed partial class PpcLifter
|
||||
new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs)
|
||||
};
|
||||
|
||||
case "bltl":
|
||||
{
|
||||
var crField = "cr0";
|
||||
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
|
||||
{
|
||||
crField = NormalizeRegister(crOp.Name);
|
||||
}
|
||||
|
||||
return new IrInstruction[]
|
||||
{
|
||||
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
|
||||
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
|
||||
$"0x{ins.EndAddress:X8}", crField)
|
||||
};
|
||||
}
|
||||
|
||||
case "bcl":
|
||||
{
|
||||
var rawInstr = ReadRawInstruction(ins);
|
||||
|
||||
@@ -14,6 +14,23 @@ public class PpcLifterAdditionalCoverageTests
|
||||
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
|
||||
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
|
||||
|
||||
[Fact]
|
||||
public void LinkedConditionalBranchSetsLrBeforeEitherPath()
|
||||
{
|
||||
var branch = PpcDecoder.Decode(0x80004394, 0x41800029);
|
||||
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
|
||||
|
||||
Assert.Equal("bltl", branch.Mnemonic);
|
||||
var lr = Assert.IsType<IrAssign>(ir[0]);
|
||||
Assert.Equal("lr", lr.Destination);
|
||||
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
|
||||
|
||||
var decision = Assert.IsType<IrBranch>(ir[1]);
|
||||
Assert.Equal("blt", decision.Condition);
|
||||
Assert.Equal("0x800043BC", decision.TrueLabel);
|
||||
Assert.Equal("0x80004398", decision.FalseLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.CodeGen;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
using Xunit;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public class SharedLrContinuationCodeGenTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(2)]
|
||||
[InlineData(20)]
|
||||
public void MultipleHooksShareOneCompleteDispatchAndKeepNormalFallthrough(int callCount)
|
||||
{
|
||||
var instructions = new List<IrInstruction>();
|
||||
for (var i = 0; i < callCount; i++)
|
||||
{
|
||||
instructions.Add(new IrAssign("lr", IrValue.Imm(unchecked((int)(0x80001004u + (uint)i * 4u)))));
|
||||
instructions.Add(new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()));
|
||||
}
|
||||
instructions.Add(new IrAssign("r3", IrValue.Imm(8)));
|
||||
instructions.Add(new IrReturn(null));
|
||||
var function = new IrFunction("shared_lr_continuation", "0x80001000", new[]
|
||||
{
|
||||
new IrBasicBlock("0x80001000", instructions),
|
||||
new IrBasicBlock("0x80001100", new IrInstruction[]
|
||||
{
|
||||
new IrAssign("r3", IrValue.Imm(1)), new IrReturn(null)
|
||||
})
|
||||
});
|
||||
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
||||
{
|
||||
["lr"] = ValueRepresentation.UInt32, ["r3"] = ValueRepresentation.UInt32
|
||||
});
|
||||
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
|
||||
new SsaTransformer().Convert(function),
|
||||
new FunctionAbiClassification("shared_lr_continuation", ValueRepresentation.Void), types,
|
||||
lrContinuationCallTargets: new HashSet<uint> { 0x81800000u });
|
||||
|
||||
Assert.Equal(1, code.Split("switch (ctx->lr)").Length - 1);
|
||||
Assert.Equal(callCount, code.Split("goto lr_continuation_dispatch;").Length - 1);
|
||||
Assert.Equal(callCount, code.Split("if (ctx->lr != ").Length - 1);
|
||||
Assert.Contains("case 0x80001100u:", code);
|
||||
Assert.Contains("goto loc_80001100;", code);
|
||||
Assert.Contains("InvokeIndirectCpu(ctx->lr, ctx);", code);
|
||||
var sharedLabel = code.IndexOf("lr_continuation_dispatch:", StringComparison.Ordinal);
|
||||
Assert.True(code.IndexOf("r3 = 8;", StringComparison.Ordinal) < sharedLabel);
|
||||
Assert.True(code.IndexOf("case 0x80001100u:", StringComparison.Ordinal) > sharedLabel);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user