mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Merge pull request #6 from mitch030504/claude/project-thread-asqg38
Port Retro Rewind install, adaptive resolution and a scene-load crash fix from VR-PLUS
This commit is contained in:
13 files changed
+414
-21
No files matched your search
+189
-15
@@ -30,6 +30,12 @@
|
||||
# --frame-disc DIR where the extracted disc goes on the Frame: absolute, or relative to the
|
||||
# Frame's home (default wiicompiled/disc; with --frame local, the disc folder
|
||||
# the build used)
|
||||
# --retro-rewind also build Retro Rewind and install it beside the base game. Its pack (about
|
||||
# 4 GB) comes from Retro Rewind's own update server, as Wheel Wizard and the
|
||||
# Quest app fetch it, and later runs apply only the updates published since.
|
||||
# --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
|
||||
# -h, --help
|
||||
set -euo pipefail
|
||||
|
||||
@@ -65,6 +71,8 @@ release=""
|
||||
source_dir=""
|
||||
jobs=""
|
||||
frame_disc=""
|
||||
retro_rewind=0
|
||||
retro_pack=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -75,6 +83,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--source) source_dir=$2; shift 2 ;;
|
||||
--jobs) jobs=$2; shift 2 ;;
|
||||
--frame-disc) frame_disc=$2; shift 2 ;;
|
||||
--retro-rewind) retro_rewind=1; shift ;;
|
||||
--retro-rewind-pack) retro_rewind=1; retro_pack=$2; shift 2 ;;
|
||||
-h|--help)
|
||||
if [[ -n "$script_dir" ]]; then usage; else echo "See the comment at the top of the script."; fi
|
||||
exit 0 ;;
|
||||
@@ -331,6 +341,108 @@ if [[ -n "$disc_dir" ]]; then
|
||||
cp "$disc_dir/sys/main.dol" "$disc_dir/files/rel/StaticR.rel" "$source_dir/Assets/"
|
||||
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/
|
||||
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.
|
||||
local url=${1/http:\/\/update.rwfc.net:8000\//https://update.rwfc.net/} dest=$2
|
||||
[[ "$url" == https://* ]] || fail "Retro Rewind's server gave a download that is not https: $url"
|
||||
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
||||
mkdir -p "$work_dir/rr-download"
|
||||
curl -fL --retry 2 --progress-bar -o "$work_dir/rr-download.zip" "$url" ||
|
||||
fail "could not download $url"
|
||||
unpack_archive "$work_dir/rr-download.zip" "$work_dir/rr-download" zip ||
|
||||
fail "could not unpack $url"
|
||||
[[ -d "$work_dir/rr-download/RetroRewind6" ]] || fail "$url holds no RetroRewind6 folder"
|
||||
mkdir -p "$dest"
|
||||
cp -a "$work_dir/rr-download/RetroRewind6/." "$dest/"
|
||||
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
||||
}
|
||||
|
||||
if (( retro_rewind )); then
|
||||
if [[ -n "$retro_pack" ]]; then
|
||||
[[ -f "$retro_pack/Binaries/Code.pul" ]] ||
|
||||
fail "$retro_pack is not a RetroRewind6 folder: it needs Binaries/Code.pul"
|
||||
say "Copying your Retro Rewind pack"
|
||||
rm -rf "$rr_pack.partial"
|
||||
cp -a "$retro_pack" "$rr_pack.partial"
|
||||
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
||||
else
|
||||
say "Checking Retro Rewind's published version"
|
||||
feed=$(rr_fetch "${rr_server}RetroRewindVersion.txt") ||
|
||||
fail "could not reach Retro Rewind's update server (${rr_server})"
|
||||
# One update per line: <version> <url> <path> <description>, oldest first.
|
||||
updates=$(awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1, $2 }' <<<"$feed" | sort -V -k1,1)
|
||||
[[ -n "$updates" ]] || fail "Retro Rewind's version list is empty"
|
||||
latest=$(tail -n 1 <<<"$updates" | cut -d' ' -f1)
|
||||
installed=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
||||
rr_version_ok "$installed" && [[ -f "$rr_pack/Binaries/Code.pul" ]] || installed=""
|
||||
if [[ -z "$installed" ]]; then
|
||||
say "Downloading Retro Rewind (about 4 GB)"
|
||||
base=$(rr_fetch "${rr_server}RetroRewindInstall.txt" | tr -d '[:space:]') ||
|
||||
fail "Retro Rewind's server did not say where its download is"
|
||||
rm -rf "$rr_pack.partial"
|
||||
rr_unpack_update "$base" "$rr_pack.partial" </dev/null
|
||||
[[ -f "$rr_pack.partial/Binaries/Code.pul" && -f "$rr_pack.partial/version.txt" ]] ||
|
||||
fail "Retro Rewind's download did not contain the pack"
|
||||
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
||||
installed=$(tr -d '[:space:]' < "$rr_pack/version.txt")
|
||||
fi
|
||||
if rr_newer "$latest" "$installed"; then
|
||||
deletions=$(rr_fetch "${rr_server}RetroRewindDelete.txt") ||
|
||||
fail "could not read Retro Rewind's deletion list"
|
||||
previous=$installed
|
||||
while read -r version url; do
|
||||
rr_newer "$version" "$previous" || continue
|
||||
say "Applying Retro Rewind $version"
|
||||
rr_unpack_update "$url" "$rr_pack" </dev/null
|
||||
# Each update's deletions follow it, before the next update can put a file back.
|
||||
# Only paths inside the pack are touched, and none that climb out of it.
|
||||
while read -r dversion dpath; do
|
||||
rr_version_ok "$dversion" || continue
|
||||
rr_newer "$dversion" "$previous" || continue
|
||||
rr_newer "$dversion" "$version" && continue
|
||||
dpath=${dpath//\\//}
|
||||
dpath=${dpath#/}
|
||||
[[ "$dpath" == RetroRewind6/?* && "/$dpath/" != */../* ]] || continue
|
||||
rm -rf "${rr_pack:?}/${dpath#RetroRewind6/}"
|
||||
done <<<"$deletions"
|
||||
previous=$version
|
||||
done <<<"$updates"
|
||||
# Written last, so an interrupted update runs again next time.
|
||||
printf '%s\n' "$latest" > "$rr_pack/version.txt"
|
||||
fi
|
||||
note "Retro Rewind $(tr -d '[:space:]' < "$rr_pack/version.txt")"
|
||||
fi
|
||||
|
||||
# translate-mod builds the payload into the game; without it, going online jumps into code that
|
||||
# was never translated. Fetched on every build, as the Quest app does.
|
||||
say "Downloading the Retro-WFC payload for online play"
|
||||
mkdir -p "$(dirname "$rr_payload")"
|
||||
if curl -fsS --retry 2 --max-filesize 16777216 -H 'Accept-Encoding: identity' \
|
||||
-o "$rr_payload.partial" 'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
|
||||
mv "$rr_payload.partial" "$rr_payload"
|
||||
elif [[ -f "$rr_payload" ]]; then
|
||||
rm -f "$rr_payload.partial"
|
||||
note "rwfc.net did not answer; using the payload from the last build"
|
||||
else
|
||||
fail "Retro Rewind's online play needs the Retro-WFC payload from rwfc.net, which could not be
|
||||
downloaded. Check this machine's internet connection, then run the script again."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
say "Preparing the ARM64 build container"
|
||||
cat > "$work_dir/container-build.sh" <<'EOF'
|
||||
@@ -359,7 +471,13 @@ echo "== Dawn (built once; reused until a release changes its patches)"
|
||||
Launcher/build-dawn-linux.sh --work-dir /work/dawn --cc "$T/clang" --cxx "$T/clang++" \
|
||||
--cmake "$T/cmake" --ninja "$T/ninja" --jobs "$JOBS"
|
||||
echo "== The game"
|
||||
Launcher/local-build.sh --output-dir /work/out --cc "$T/clang" --cxx "$T/clang++" --fuse-ld lld \
|
||||
products=(--output-dir /work/out)
|
||||
if [[ "$RETRO_REWIND" == 1 ]]; then
|
||||
# Both products from one translation: the base game in out/, Retro Rewind in out-retro-rewind/.
|
||||
products=(--profile both --output-dir /work/out-retro-rewind --base-output-dir /work/out
|
||||
--retro-rewind-package-dir /work/RetroRewind6 --retro-wfc-offline-dir /work/retro-wfc)
|
||||
fi
|
||||
Launcher/local-build.sh "${products[@]}" --cc "$T/clang" --cxx "$T/clang++" --fuse-ld lld \
|
||||
--cmake "$T/cmake" --ninja "$T/ninja" --dotnet /work/dotnet/dotnet --parallel "$JOBS" \
|
||||
--openxr --dawn-package /work/dawn/package --headset steam_frame
|
||||
EOF
|
||||
@@ -382,12 +500,17 @@ fi
|
||||
note "container $container (mounts ${want_mounts//;/ })"
|
||||
|
||||
say "Building (the first time takes hours under emulation; a log is in $work_dir/build.log)"
|
||||
if ! "$runtime" exec -e JOBS="$jobs" "$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"; 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
|
||||
[[ -x "$work_dir/out/$game_id" ]] || fail "the build finished without $work_dir/out/$game_id"
|
||||
note "built $work_dir/out/$game_id"
|
||||
if (( retro_rewind )); then
|
||||
[[ -x "$work_dir/out-retro-rewind/RetroRewind" ]] ||
|
||||
fail "the build finished without $work_dir/out-retro-rewind/RetroRewind"
|
||||
note "built $work_dir/out-retro-rewind/RetroRewind"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
if [[ -z "$frame" ]]; then
|
||||
@@ -427,18 +550,23 @@ if [[ "$frame" != local ]] && command -v rsync >/dev/null 2>&1 &&
|
||||
frame_rsync=yes
|
||||
fi
|
||||
|
||||
game_dir="devkit-game/$game_id"
|
||||
on_frame "$game_dir" <<'EOF'
|
||||
install_game() {
|
||||
# install_game ID OUT: the built game in OUT goes to ~/devkit-game/ID on the Frame.
|
||||
local id=$1 out=$2 dir="devkit-game/$1"
|
||||
on_frame "$dir" <<'EOF'
|
||||
mkdir -p "$HOME/$1"
|
||||
EOF
|
||||
note "copying the game to ~/$game_dir"
|
||||
(cd "$work_dir/out" && find . -mindepth 1 -maxdepth 1 ! -name "$game_id" -print0) |
|
||||
while IFS= read -r -d '' item; do copy_to_frame "$work_dir/out/${item#./}" "$game_dir/"; done
|
||||
# The executable goes in under a new name and is moved into place: that works while it runs.
|
||||
copy_to_frame "$work_dir/out/$game_id" "$game_dir/$game_id.new"
|
||||
on_frame "$game_dir" "$game_id" <<'EOF'
|
||||
note "copying $id to ~/$dir"
|
||||
(cd "$out" && find . -mindepth 1 -maxdepth 1 ! -name "$id" -print0) |
|
||||
while IFS= read -r -d '' item; do copy_to_frame "$out/${item#./}" "$dir/"; done
|
||||
# The executable goes in under a new name and is moved into place: that works while it runs.
|
||||
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
|
||||
}
|
||||
install_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then install_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
# The Frame's disc folder, absolute or relative to its home.
|
||||
if [[ -z "$frame_disc" ]]; then
|
||||
@@ -464,6 +592,45 @@ rm -rf "$d" && mv "$d.partial" "$d"
|
||||
EOF
|
||||
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
|
||||
)
|
||||
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)"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
mkdir -p "$(dirname "$d")" && rm -rf "$d.partial"
|
||||
EOF
|
||||
copy_to_frame "$rr_pack" "$frame_pack_path.partial"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
rm -rf "$d" && mv "$d.partial" "$d"
|
||||
EOF
|
||||
fi
|
||||
note "pointing Retro Rewind at its pack"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
config="$HOME/.local/share/WiiCompiled/Config.toml"
|
||||
mkdir -p "$(dirname "$config")"
|
||||
if [[ ! -f "$config" ]]; then
|
||||
printf '[paths]\nretro_rewind_root = "%s"\n' "$d" > "$config"
|
||||
elif ! grep -q '^retro_rewind_root *=' "$config"; then
|
||||
if grep -q '^\[paths\]' "$config"; then
|
||||
sed -i "/^\[paths\]/a retro_rewind_root = \"$d\"" "$config"
|
||||
else
|
||||
printf '\n[paths]\nretro_rewind_root = "%s"\n' "$d" >> "$config"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
note "pointing the game at the disc"
|
||||
on_frame "$frame_disc_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
@@ -483,7 +650,10 @@ EOF
|
||||
# Steam's library: through Valve's devkit tools when Frame Control or the Devkit Client put them on
|
||||
# the Frame, as a Steam Linux Runtime ARM64 title (Steam starts an ARM64 program natively).
|
||||
# Registering again is harmless and refreshes the entry, as Valve's Devkit Client does each upload.
|
||||
registered=$(on_frame "$game_dir" "$game_id" <<'EOF'
|
||||
register_game() {
|
||||
# register_game ID OUT: adds ~/devkit-game/ID to Steam's library, or says how to.
|
||||
local id=$1 out=$2 dir="devkit-game/$1" registered
|
||||
registered=$(on_frame "$dir" "$id" <<'EOF'
|
||||
if [[ ! -f "$HOME/devkit-utils/steam-client-create-shortcut" ]]; then echo no-tools; exit 0; fi
|
||||
parms=$(printf '{"gameid": "%s", "directory": "%s", "argv": ["%s"], "env": {}, "settings": {"steam_play": "0", "compat_tool": "SteamLinuxRuntime_4-arm64"}, "clear_settings": true, "force_appid": "", "lepton_args": ""}' "$2" "$HOME/$1" "$2")
|
||||
reply=$(python3 "$HOME/devkit-utils/steam-client-create-shortcut" --parms "$parms" 2>/dev/null | tail -n 1)
|
||||
@@ -491,16 +661,20 @@ case "$reply" in *'"success"'*) echo added ;; *) echo "failed $reply" ;; esac
|
||||
EOF
|
||||
)
|
||||
case "$registered" in
|
||||
added) note "it is in your Steam library" ;;
|
||||
added) note "$id is in your Steam library" ;;
|
||||
no-tools|failed*)
|
||||
[[ "$registered" == failed* ]] && note "Steam did not take the shortcut: ${registered#failed }"
|
||||
note "Add it to your Steam library once: in Frame Control, Send to Frame the folder"
|
||||
note " $work_dir/out (name it $game_id), or on the Frame in Desktop Mode, Steam >"
|
||||
note " Add a Non-Steam Game > ~/$game_dir/$game_id. Later runs of this script keep it updated." ;;
|
||||
note "Add $id to your Steam library once: in Frame Control, Send to Frame the folder"
|
||||
note " $out (name it $id), or on the Frame in Desktop Mode, Steam >"
|
||||
note " Add a Non-Steam Game > ~/$dir/$id. Later runs of this script keep it updated." ;;
|
||||
esac
|
||||
}
|
||||
register_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then register_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
say "Done"
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
if (( retro_rewind )); then note "RetroRewind is beside it, and shares its settings."; fi
|
||||
}
|
||||
|
||||
main "$@" </dev/null
|
||||
@@ -90,6 +90,12 @@ curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/op
|
||||
It is added to your Steam library through Valve's devkit tools in `~/devkit-utils`, which Frame
|
||||
Control and Valve's Devkit Client put there. Without them, the script says how to add it once
|
||||
yourself. Steam must be running on the Frame for this step.
|
||||
- Add `--retro-rewind` to also build [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind).
|
||||
The script downloads its pack (about 4 GB) from Retro Rewind's own update server, as Wheel Wizard
|
||||
does, and later runs apply only the updates published since. It also fetches the Retro-WFC
|
||||
payload online play needs. On the Frame, Retro Rewind goes to `~/devkit-game/RetroRewind/` and
|
||||
its pack to `~/wiicompiled/RetroRewind6`, and it shares WiiCompiled's settings.
|
||||
`--retro-rewind-pack DIR` uses a RetroRewind6 folder you already have instead.
|
||||
- `--help` lists every option. From a downloaded release, run `Launcher/steam-frame-install.sh`
|
||||
with the same options: it then builds that release's source.
|
||||
|
||||
@@ -117,6 +123,7 @@ settings back when it closes.
|
||||
| `[vr] foveation` | `medium` | `off` shades every pixel and costs the most. See [Known issues](#known-issues) if images double. |
|
||||
| `[vr] repeat_frames` | `true` (default) | Without it SteamVR halves the game's rate and fills refreshes itself. |
|
||||
| `[vr] frame_interpolation_fps` | `0` | Rendering in-between frames needs 120 eye pairs a second, which made things worse on the Frame. |
|
||||
| `[vr] adaptive_resolution` | `false` (default) | Experimental and untested on the Frame. When on, races drop to as little as 70% of `render_scale` while new frames fall behind 60 FPS, and climb back once they keep up. Each step rebuilds the foveation maps; `console.log` records every change as `OpenXR: adaptive resolution`. |
|
||||
| `[video] resolution_multiplier` | `2` | The game's own frame, which the eyes are made from. 4x is far too heavy for the Frame's GPU. |
|
||||
|
||||
Keep SteamVR's refresh rate at 120 Hz. Motion Smoothing makes no difference to this game.
|
||||
|
||||
@@ -559,6 +559,12 @@ static AlphaCompareExpr alpha_compare(GXCompare comp, u8 ref) {
|
||||
static inline std::string vtx_attr(const ShaderConfig& config, GXAttr attr) {
|
||||
const auto type = config.attrs[attr].attrType;
|
||||
if (type == GX_NONE) {
|
||||
if (attr == GX_VA_POS) {
|
||||
// Some scene transitions submit a vertex stream with no position attribute.
|
||||
// The resulting primitive has no useful geometry, but shader generation
|
||||
// must not abort the entire game while the next scene is loading.
|
||||
return "vec3f(0.0)"s;
|
||||
}
|
||||
if (attr == GX_VA_PNMTXIDX) {
|
||||
return "ubuf.current_pnmtx";
|
||||
}
|
||||
|
||||
@@ -150,8 +150,11 @@ public:
|
||||
for (uint32_t i = 0; i < images; ++i) {
|
||||
const auto& eye = sources[i];
|
||||
const auto& target = targets[i];
|
||||
if (!eye.texture || eye.size.width != target.width || eye.size.height != target.height ||
|
||||
!CopyCompatible(VkFormat(eye.format), target.dxgiFormat)) return false;
|
||||
// An eye smaller than its target goes in the target's top-left corner, which is all the
|
||||
// projection layer shows of it; the panel and the virtual screen are always full size.
|
||||
const bool fits = i < count ? eye.size.width <= target.width && eye.size.height <= target.height
|
||||
: eye.size.width == target.width && eye.size.height == target.height;
|
||||
if (!eye.texture || !fits || !CopyCompatible(VkFormat(eye.format), target.dxgiFormat)) return false;
|
||||
const uint64_t image = reinterpret_cast<uintptr_t>(target.resource);
|
||||
auto it = std::find_if(imports.begin(), imports.end(), [&](const Import& entry) { return entry.image == image; });
|
||||
if (it == imports.end()) {
|
||||
@@ -179,7 +182,7 @@ public:
|
||||
wgpu::TexelCopyTextureInfo source, destination;
|
||||
source.texture = *sources[i].texture;
|
||||
destination.texture = active[i];
|
||||
wgpu::Extent3D size{targets[i].width, targets[i].height, 1};
|
||||
wgpu::Extent3D size{sources[i].size.width, sources[i].size.height, 1};
|
||||
encoder.CopyTextureToTexture(&source, &destination, &size);
|
||||
}
|
||||
encoded = true;
|
||||
|
||||
@@ -460,6 +460,10 @@ target_include_directories(mkw_vr_wii_remote_tests PRIVATE "${CMAKE_CURRENT_LIST
|
||||
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
||||
# Eye-tracked foveation: the gaze in each eye's view (vr/eye_gaze.h).
|
||||
add_executable(mkw_vr_adaptive_resolution_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_adaptive_resolution_tests.cpp")
|
||||
target_include_directories(mkw_vr_adaptive_resolution_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_adaptive_resolution_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_adaptive_resolution_tests COMMAND mkw_vr_adaptive_resolution_tests)
|
||||
add_executable(mkw_vr_eye_gaze_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_eye_gaze_tests.cpp")
|
||||
target_include_directories(mkw_vr_eye_gaze_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_eye_gaze_tests PRIVATE cxx_std_17)
|
||||
|
||||
@@ -101,6 +101,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<std::string> vrFoveation;
|
||||
std::optional<bool> vrEyeTrackedFoveation;
|
||||
std::optional<bool> vrRepeatFrames;
|
||||
std::optional<bool> vrAdaptiveResolution;
|
||||
std::optional<std::string> vrRecenterKey;
|
||||
std::optional<float> vrLeanBackDegrees;
|
||||
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
|
||||
@@ -607,6 +608,10 @@ inline void EnsureConfigFile() {
|
||||
"# Submit the last frame again for each refresh the game has no new\n"
|
||||
"# frame for, so the runtime does not fill those refreshes itself. Live.\n"
|
||||
"repeat_frames = " MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "\n"
|
||||
"# Lower the race's eye resolution, down to 70% of render_scale, while\n"
|
||||
"# new frames fall behind the game's 60 FPS, and raise it again once\n"
|
||||
"# they keep up. Experimental; the Steam Frame and the Quest. Live.\n"
|
||||
"adaptive_resolution = false\n"
|
||||
"render_scale = " MKW_VR_RENDER_SCALE_DEFAULT_TEXT "\n"
|
||||
"world_units_per_meter = 500.0\n"
|
||||
"hud_distance_meters = 2.0\n"
|
||||
@@ -928,6 +933,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
}
|
||||
config.vrEyeTrackedFoveation = FindConfigValue<bool>(document, "vr", "eye_tracked_foveation");
|
||||
config.vrRepeatFrames = FindConfigValue<bool>(document, "vr", "repeat_frames");
|
||||
config.vrAdaptiveResolution = FindConfigValue<bool>(document, "vr", "adaptive_resolution");
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
@@ -1345,6 +1351,11 @@ inline bool SetVrRepeatFrames(bool value) {
|
||||
return WriteSetting("vr", "repeat_frames", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrAdaptiveResolution(bool value) {
|
||||
Mutable().vrAdaptiveResolution = value;
|
||||
return WriteSetting("vr", "adaptive_resolution", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrEyeTrackedFoveation(bool value) {
|
||||
Mutable().vrEyeTrackedFoveation = value;
|
||||
return WriteSetting("vr", "eye_tracked_foveation", value ? "true" : "false");
|
||||
@@ -1880,6 +1891,10 @@ inline bool VrRepeatFrames(bool fallback = kVrRepeatFramesDefault) {
|
||||
return Get().vrRepeatFrames.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrAdaptiveResolution(bool fallback = false) {
|
||||
return Get().vrAdaptiveResolution.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrEyeTrackedFoveation(bool fallback = kVrEyeTrackedFoveationDefault) {
|
||||
return Get().vrEyeTrackedFoveation.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// Adaptive resolution for the immersive eyes ([vr] adaptive_resolution), adapted from
|
||||
// heurazy/Wiicompiled_VR-PLUS. Once a second the pacing thread reports how many new eye frames
|
||||
// reached the headset and how many it should have had. A second well short of the target lowers
|
||||
// the eyes' scale by a step; three seconds in a row at the target raise it again. The steps are
|
||||
// slow so a single spike never resizes the eyes, which rebuilds their foveation maps.
|
||||
//
|
||||
// The eyes are rendered at the scaled size into a corner of the full swapchain image, and the
|
||||
// projection layer shows that corner, so the compositor does the upscaling and the swapchains are
|
||||
// never recreated. Nothing here depends on OpenXR (tests/vr_adaptive_resolution_tests.cpp).
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
class AdaptiveResolution {
|
||||
public:
|
||||
static constexpr float kMinimumScale = 0.7f;
|
||||
static constexpr float kStep = 0.1f;
|
||||
// Below this share of the target a second counts as short, at or above the other as on time.
|
||||
static constexpr float kShortShare = 0.85f;
|
||||
static constexpr float kOnTimeShare = 0.97f;
|
||||
static constexpr unsigned kOnTimeSecondsToRaise = 3;
|
||||
|
||||
float Scale() const noexcept { return scale_; }
|
||||
|
||||
// One second's measurement: `fps` new eye frames against `target_fps`. Off resets to full size.
|
||||
float Observe(float fps, float target_fps, bool enabled) noexcept {
|
||||
if (!enabled) {
|
||||
scale_ = 1.0f;
|
||||
on_time_seconds_ = 0;
|
||||
return scale_;
|
||||
}
|
||||
if (!(target_fps > 0.0f) || !(fps > 0.0f)) {
|
||||
return scale_;
|
||||
}
|
||||
if (fps < target_fps * kShortShare) {
|
||||
scale_ = std::max(kMinimumScale, scale_ - kStep);
|
||||
on_time_seconds_ = 0;
|
||||
} else if (fps >= target_fps * kOnTimeShare) {
|
||||
if (++on_time_seconds_ >= kOnTimeSecondsToRaise) {
|
||||
scale_ = std::min(1.0f, scale_ + kStep);
|
||||
on_time_seconds_ = 0;
|
||||
}
|
||||
} else {
|
||||
on_time_seconds_ = 0;
|
||||
}
|
||||
// Whole steps only, so float drift never leaves the eyes a pixel off full size.
|
||||
scale_ = static_cast<float>(static_cast<int>(scale_ / kStep + 0.5f)) * kStep;
|
||||
return scale_;
|
||||
}
|
||||
|
||||
// The scaled size of an eye `full` pixels across, never below 2.
|
||||
static unsigned Scaled(unsigned full, float scale) noexcept {
|
||||
if (scale >= 1.0f) {
|
||||
return full;
|
||||
}
|
||||
return std::max(2u, static_cast<unsigned>(static_cast<float>(full) * scale));
|
||||
}
|
||||
|
||||
private:
|
||||
float scale_ = 1.0f;
|
||||
unsigned on_time_seconds_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -41,6 +41,11 @@ static void GX__ClearVtxDesc_8016dc34_gx() {
|
||||
changed |= g_hleGxState.vtxDesc[i] != GX_NONE;
|
||||
g_hleGxState.vtxDesc[i]=GX_NONE;
|
||||
}
|
||||
// Nintendo's GXClearVtxDesc retains a direct position descriptor. Aurora's
|
||||
// GXClearVtxDesc does the same; keep the HLE mirror in sync so the next
|
||||
// draw does not publish GX_VA_POS=GX_NONE and abort shader generation.
|
||||
changed |= g_hleGxState.vtxDesc[GX_VA_POS] != GX_DIRECT;
|
||||
g_hleGxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
if (changed) g_hleGxState.InvalidateVtxLayoutHash();
|
||||
// GXClearVtxDesc resets descriptors only; array base/stride state persists.
|
||||
GXClearVtxDesc();
|
||||
@@ -49,6 +54,7 @@ extern "C" void GX__ClearVtxDesc_8016dc34() {
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
g_gxGameVertexState.vtxDesc[i] = GX_NONE;
|
||||
}
|
||||
g_gxGameVertexState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
GxThread::Post(&GX__ClearVtxDesc_8016dc34_gx);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016dc34, GX__ClearVtxDesc_8016dc34, (), ());
|
||||
|
||||
@@ -159,6 +159,7 @@ int g_vrFoveation = static_cast<int>(RuntimeConfigFile::VrFoveationLevelIndex(Ru
|
||||
bool g_vrEyeTrackedFoveation = RuntimeConfigFile::VrEyeTrackedFoveation();
|
||||
#endif
|
||||
bool g_vrRepeatFrames = RuntimeConfigFile::VrRepeatFrames();
|
||||
bool g_vrAdaptiveResolution = RuntimeConfigFile::VrAdaptiveResolution();
|
||||
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
||||
bool g_vrFirstPersonToggleClick = RuntimeConfigFile::VrFirstPersonToggleClick();
|
||||
// Set from any thread by the right-thumbstick click, applied on the game thread.
|
||||
@@ -1568,6 +1569,18 @@ void DrawVrSettings() {
|
||||
"fill the gaps itself, which doubles the HUD and the menu screen as you turn your head "
|
||||
"(SteamVR on the Steam Frame). Applies immediately.");
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
// Only the Vulkan backends show part of an eye image; the PC's copy whole eyes.
|
||||
if (ImGui::Checkbox("Adaptive resolution (experimental)", &g_vrAdaptiveResolution)) {
|
||||
RuntimeConfigFile::SetVrAdaptiveResolution(g_vrAdaptiveResolution);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"During races, lowers the eyes' resolution a step at a time, down to 70%%, while new "
|
||||
"frames fall behind the game's 60 FPS, and raises it again once they keep up. Each change "
|
||||
"rebuilds the foveation maps. Applies immediately.");
|
||||
}
|
||||
#endif
|
||||
if (ImGui::Combo("VR frame interpolation (experimental)", &g_vrFrameInterpolationMode,
|
||||
kVrInterpolationLabels.data(), static_cast<int>(kVrInterpolationLabels.size()))) {
|
||||
const auto target = kVrInterpolationFps[static_cast<size_t>(g_vrFrameInterpolationMode)];
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "gx_thread.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/adaptive_resolution.h"
|
||||
#include "vr/eye_gaze.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -111,6 +112,15 @@ inline constexpr bool kWindowShapedEyesSupported = true;
|
||||
inline constexpr bool kWindowShapedEyesSupported = false;
|
||||
#endif
|
||||
|
||||
// Whether the immersive eyes can be rendered smaller than their swapchain images for
|
||||
// [vr] adaptive_resolution: the backend copies them into a corner of the image and its projection
|
||||
// layer shows that corner. The Vulkan backends do; the PC's D3D12 one copies whole eyes.
|
||||
#if defined(_WIN32)
|
||||
inline constexpr bool kScaledEyesSupported = false;
|
||||
#else
|
||||
inline constexpr bool kScaledEyesSupported = true;
|
||||
#endif
|
||||
|
||||
struct Quaternion {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
@@ -1506,6 +1516,15 @@ private:
|
||||
destination.eyes[eye].height = source.render_height[eye];
|
||||
}
|
||||
}
|
||||
// The backend shows the part of each image this size fills, so the field of view is kept.
|
||||
if (const float scale = adaptive_scale_.load(std::memory_order_relaxed); kScaledEyesSupported && scale < 1.0f) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
source.render_width[eye] = AdaptiveResolution::Scaled(source.render_width[eye], scale);
|
||||
source.render_height[eye] = AdaptiveResolution::Scaled(source.render_height[eye], scale);
|
||||
destination.eyes[eye].width = source.render_width[eye];
|
||||
destination.eyes[eye].height = source.render_height[eye];
|
||||
}
|
||||
}
|
||||
// Read once so both eyes are built from the same angle even if the
|
||||
// settings slider moves between them.
|
||||
const float lean_back_radians =
|
||||
@@ -1879,7 +1898,21 @@ private:
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float elapsed = std::chrono::duration<float>(now - timing_start_).count();
|
||||
if (elapsed >= 1.0f) {
|
||||
rendered_fps_.store(static_cast<float>(timing_submissions_) / elapsed, std::memory_order_relaxed);
|
||||
const float fps = static_cast<float>(timing_submissions_) / elapsed;
|
||||
rendered_fps_.store(fps, std::memory_order_relaxed);
|
||||
// Measured during races only: menus and loading screens run below 60 on their own.
|
||||
// The target is the game's 60 FPS, or the headset's rate when frames are interpolated.
|
||||
const bool adaptive = kScaledEyesSupported && RuntimeConfigFile::VrAdaptiveResolution();
|
||||
if (!adaptive || last_immersive_) {
|
||||
const float target = aurora_get_stereo_frame_interpolation() ? hz : std::min(hz, 60.0f);
|
||||
const float previous = adaptive_resolution_.Scale();
|
||||
const float scale = adaptive_resolution_.Observe(fps, target, adaptive);
|
||||
adaptive_scale_.store(scale, std::memory_order_relaxed);
|
||||
if (scale != previous) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: adaptive resolution " << static_cast<int>(scale * 100.0f + 0.5f)
|
||||
<< "% (" << fps << " of " << target << " FPS)" << std::endl;
|
||||
}
|
||||
}
|
||||
timing_start_ = now;
|
||||
timing_submissions_ = 0;
|
||||
}
|
||||
@@ -2031,6 +2064,9 @@ private:
|
||||
std::atomic<float> rendered_fps_{0};
|
||||
std::chrono::steady_clock::time_point timing_start_ = std::chrono::steady_clock::now();
|
||||
uint32_t timing_submissions_ = 0;
|
||||
// [vr] adaptive_resolution: measured on the pacing thread, read where eyes are published.
|
||||
AdaptiveResolution adaptive_resolution_;
|
||||
std::atomic<float> adaptive_scale_{1.0f};
|
||||
PFN_xrGetDisplayRefreshRateFB get_display_refresh_rate_ = nullptr;
|
||||
PFN_xrEnumerateDisplayRefreshRatesFB enumerate_refresh_rates_ = nullptr;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate_ = nullptr;
|
||||
|
||||
@@ -744,10 +744,11 @@ public:
|
||||
views[eye].pose.position = frame.xr_frame.views[eye].pose.position;
|
||||
views[eye].fov = frame.xr_frame.views[eye].fov;
|
||||
views[eye].subImage.swapchain = retained_swapchains_[eye].handle;
|
||||
// The eyes may fill only a corner of the image ([vr] adaptive_resolution).
|
||||
views[eye].subImage.imageRect = {
|
||||
{0, 0},
|
||||
{static_cast<int32_t>(retained_swapchains_[eye].width),
|
||||
static_cast<int32_t>(retained_swapchains_[eye].height)}};
|
||||
{static_cast<int32_t>(std::min(frame.render_width[eye], retained_swapchains_[eye].width)),
|
||||
static_cast<int32_t>(std::min(frame.render_height[eye], retained_swapchains_[eye].height))}};
|
||||
views[eye].subImage.imageArrayIndex = 0;
|
||||
}
|
||||
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "vr/adaptive_resolution.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
using mkw::vr::AdaptiveResolution;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "vr_adaptive_resolution_tests: %s\n", what);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNear(float value, float expected, const char* what) {
|
||||
Check(std::fabs(value - expected) <= 1.0e-5f, what);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
{
|
||||
AdaptiveResolution adaptive;
|
||||
CheckNear(adaptive.Observe(40.0f, 60.0f, false), 1.0f, "off stays at full size");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 1.0f, "on time at full size stays there");
|
||||
}
|
||||
{
|
||||
AdaptiveResolution adaptive;
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.9f, "a short second lowers a step");
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.8f, "another lowers another");
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.7f, "and another");
|
||||
CheckNear(adaptive.Observe(10.0f, 60.0f, true), 0.7f, "never below the minimum");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.7f, "one second on time is not enough");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.7f, "two are not enough");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "three raise a step");
|
||||
CheckNear(adaptive.Observe(55.0f, 60.0f, true), 0.8f, "between the thresholds holds");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "and restarts the on-time count");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "still counting");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.9f, "three more raise again");
|
||||
for (int second = 0; second < 30; ++second) adaptive.Observe(60.0f, 60.0f, true);
|
||||
Check(adaptive.Scale() == 1.0f, "back at exactly full size");
|
||||
CheckNear(adaptive.Observe(0.0f, 60.0f, true), 1.0f, "a second with no measurement changes nothing");
|
||||
CheckNear(adaptive.Observe(30.0f, 0.0f, true), 1.0f, "nor one with no target");
|
||||
adaptive.Observe(30.0f, 60.0f, true);
|
||||
CheckNear(adaptive.Observe(30.0f, 60.0f, false), 1.0f, "turning it off restores full size");
|
||||
}
|
||||
Check(AdaptiveResolution::Scaled(2064, 1.0f) == 2064, "full size is unchanged");
|
||||
Check(AdaptiveResolution::Scaled(2000, 0.7f) == 1400, "scaled size");
|
||||
Check(AdaptiveResolution::Scaled(2, 0.7f) == 2, "never below two pixels");
|
||||
std::puts("vr_adaptive_resolution_tests: ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -84,6 +84,9 @@ int main() {
|
||||
Require(Parse("[vr]\nrepeat_frames = false\n").vrRepeatFrames == false);
|
||||
Require(!Parse("[vr]\nrepeat_frames = 1\n").vrRepeatFrames.has_value());
|
||||
Require(!Parse("[vr]\n").vrRepeatFrames.has_value());
|
||||
// [vr] adaptive_resolution: off unless set.
|
||||
Require(Parse("[vr]\nadaptive_resolution = true\n").vrAdaptiveResolution == true);
|
||||
Require(!Parse("[vr]\n").vrAdaptiveResolution.has_value());
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
Require(RuntimeConfigFile::kVrRepeatFramesDefault);
|
||||
Require(std::string_view(MKW_VR_REPEAT_FRAMES_DEFAULT_TOML) == "true");
|
||||
|
||||
Reference in new issue
Block a user