Added Support for VR in Launcher Setup

This commit is contained in:
iChris4 committed 2026-09-09 20:40:23 +02:00
1 parent d86dcb0aeb
commit 5caec70da9
28 files changed
+397 -157

No files matched your search

+32 -127
View File
@@ -1,160 +1,65 @@
name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI.
name: Package WiiCompiled OpenXR VR
on:
push:
tags:
- '*'
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: package-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: package-vr-${{ github.ref }}
cancel-in-progress: false
jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
arch: x86_64
- runner: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
# appimagetool ships as an AppImage itself and needs to self-mount via FUSE to run directly;
# GitHub's ubuntu-latest runners don't have libfuse2 preinstalled.
- name: Install dependencies (required by appimagetool and SDL3 build)
run: |
sudo apt-get update
sudo apt-get install -y libfuse2 build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev libusb-1.0-0-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
- name: Build AppImage
run: bash Launcher/build-appimage.sh
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-linux-${{ matrix.arch }}
path: Launcher/dist/WiiCompiled-Setup-${{ matrix.arch }}.AppImage
if-no-files-found: error
archive: false
windows-installer:
name: Windows (installer exe)
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Build installer
- name: Verify release version
shell: pwsh
run: ./Launcher/Build-Installer.ps1
env:
RELEASE_TAG: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
run: ./Launcher/Verify-Release.ps1 -Tag $env:RELEASE_TAG
- name: Test translator
run: dotnet test translator/Translator.sln -c Release
- name: Build and audit setup payload
shell: pwsh
run: |
./Launcher/Build-Installer.ps1
./Launcher/Verify-Release.ps1 -SetupPath (Resolve-Path Launcher/dist/WiiCompiled-Setup.exe)
./Launcher/dist/WiiCompiled-Setup.exe --self-test
if ($LASTEXITCODE -ne 0) { throw 'Setup self-tests failed.' }
$hash = (Get-FileHash Launcher/dist/WiiCompiled-Setup.exe -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash WiiCompiled-Setup.exe" | Set-Content Launcher/dist/SHA256SUMS.txt
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-windows-x64
path: Launcher/dist/WiiCompiled-Setup.exe
name: WiiCompiled-OpenXR-VR-windows-x64
path: |
Launcher/dist/WiiCompiled-Setup.exe
Launcher/dist/SHA256SUMS.txt
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest
needs: [windows, recompilation]
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported
# version differs from the release tag, so a tag that was pushed without bumping every pinned
# version would ship an update nobody can install. Catch that before anything is published.
- name: Verify the tag matches the pinned setup version
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${TAG#v}"
status=0
check() {
if ! grep -Fq "$2" "$1"; then
echo "::error file=$1::expected '$2' for tag $TAG"
status=1
fi
}
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file
# rather than a zip (and names the artifact after the file). Only download-artifact v8 and
# later understand such direct uploads; v7 tries to unzip everything and fails on them.
- uses: actions/download-artifact@v8
with:
name: WiiCompiled-OpenXR-VR-windows-x64
path: artifacts
merge-multiple: true
- name: Publish release
- name: Publish Windows setup
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
ls -lR artifacts
assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
done
# A re-run of a tag whose release already exists only refreshes the assets.
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
else
gh release create "$TAG" "${assets[@]}" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--verify-tag \
--generate-notes
fi
gh release create $env:TAG artifacts/WiiCompiled-Setup.exe artifacts/SHA256SUMS.txt --repo $env:REPOSITORY --verify-tag --title "WiiCompiled OpenXR VR $env:TAG (beta)" --notes "Windows x64 OpenXR beta. Install through WheelWizard VR. Requires your own clean PAL RMCP01 disc image; game code and assets are compiled locally."
if ($LASTEXITCODE -ne 0) { throw 'Release publication failed.' }
+56
View File
@@ -0,0 +1,56 @@
# Windows VR distribution
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).
Installation requires the user's clean PAL RMCP01 image. Neither release includes a disc image,
extracted game assets, translated game code, compiled game executables, saves, or ghosts.
The backend version is declared once in `Launcher/Directory.Build.props`. Before choosing a new
patch version, compare source pins, published releases, and tags in the VR fork. Tags use `vX.Y.Z`.
The Windows workflow checks the tag, executable, and payload contract and runs the translator,
synthetic runtime, setup self-tests, pinned dependency preparation, payload boundary audit, and
native dependency audit. OpenXR uses the pinned SDK sources and static loader; license notices
are included in the installer.
Build from the repository root on Windows:
```powershell
./Launcher/Build-Installer.ps1 -OutputDirectory Launcher/dist-vr
./Launcher/Verify-Release.ps1 -Tag v0.2.32 -SetupPath (Resolve-Path Launcher/dist-vr/WiiCompiled-Setup.exe)
./Launcher/dist-vr/WiiCompiled-Setup.exe --self-test
./Launcher/Test-Recompilation.ps1 -StageDirectory build/vr-synthetic-validation
dotnet test translator/Translator.sln -c Release
```
`--version` still prints a plain version. `--info-json` additionally reports `productId` as
`wiicompiled-openxr-vr`, `version`, and `openxrD3D12: true`. VR refuses installation, repair,
launch, or uninstall of a normal or unidentified installation. Nonportable VR installations use
`WiiCompiledOpenXRVR` application data and a distinct uninstall registration and shortcut.
WheelWizard VR keeps its own `config-vr.json`, importing `config.json` only on first use. Its
`Recomp` and `RecompVR` roots own their setup caches, toolkits, build workspaces, extracted assets,
binaries, configuration, runtime caches, and logs. Only the normal NAND and canonical Retro Rewind
content are shared. Controller mappings are seeded from normal once when available. Uninstall
removes the selected `Install` and `Cache` directories and retains `UserData` and imported NAND.
A shared file lease covers supported launcher operations through mod preparation, reconciliation,
and game exit, including shared-data edits and uninstall. Installation locks and running processes
are also checked. Arbitrary direct launches of older game binaries do not participate in this
guarantee. Close games before using another launcher or editing shared data externally.
Retro Rewind remains supplied by WheelWizard's existing distribution service. The chosen backend
builds both products with its own toolkit. Asset-only changes do not require compilation; changes
to Code.pul, translation inputs, or Retro-WFC payloads are reconciled before Retro Rewind launches.
The inactive backend is checked on its next selection. Base launches use `--launch-base`.
WheelWizard VR prepares Retro Rewind updates and reinstalls in a sibling staging tree. Only a
completed update is published. The journal supports recovery after interrupted directory swaps;
existing ghosts, saves, XML save directories, and local patches survive package replacement.
Before publishing, validate both artifacts and install beside an existing normal installation.
Check all four backend/game combinations, saves and Miis across switches and reinstall/uninstall,
Retro Rewind ghosts and content updates, cancellation, interrupted downloads, rollback, and
overlapping processes. Headset acceptance also requires menus, races, headset absence, and runtime
loss for both VR products. Record unperformed checks explicitly. Publish the backend first; the
launcher release workflow requires a published VR backend before releasing the launcher.
+3 -1
View File
@@ -15,6 +15,7 @@ Set-StrictMode -Version 3.0
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$releaseVersion = ([xml](Get-Content -LiteralPath (Join-Path $PSScriptRoot 'Directory.Build.props') -Raw)).Project.PropertyGroup.Version
$outputRoot = [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory))
$portableTools = [IO.Path]::GetFullPath((Join-Path $repoRoot $PortableToolsDirectory))
$dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySourceDirectory))
@@ -282,7 +283,8 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.31'
ProductVersion = $releaseVersion
ProductId = 'wiicompiled-openxr-vr'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
+5
View File
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<Version>0.2.32</Version>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -390,7 +390,7 @@ try {
-SourceDirectory (Join-Path $Workspace 'runtime') -BuildDirectory $build `
-Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres `
-DependenciesDirectory $dependencies -NativePrebuiltDirectory $nativePrebuilt `
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs")
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs", '-DMKW_ENABLE_OPENXR=ON')
Invoke-Checked $cmake $configure 'Configuring the bundled native toolchain' -StepId 'configure-native'
if (-not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint)) {
[ordered]@{ SchemaVersion = 1; NativeToolchainFingerprint = $NativeToolchainFingerprint } |
+4 -2
View File
@@ -9,9 +9,11 @@ $forbiddenNames = @(
'WiiCompiled.exe', 'RetroRewind.exe', 'mkw_recompiled.exe',
'main.dol', 'StaticR.rel', 'Code.pul', 'base_translation_sources.bin',
'data_sections_init.cpp', 'data_sections_init_blobs.S', 'mkwii_base_manifest.json',
'generated_func_aliases.cpp', 'translated_function_decls.h'
'generated_func_aliases.cpp', 'translated_function_decls.h',
'rksys.dat', 'RFL_DB.dat', 'Config.toml', 'config-vr.json', 'install-state.json'
)
$forbiddenExtensions = @('.iso', '.gcm', '.gcz', '.ciso', '.wbfs', '.wia', '.rvz', '.dol', '.rel', '.pul')
$forbiddenExtensions = @('.iso', '.gcm', '.gcz', '.ciso', '.wbfs', '.wia', '.rvz', '.dol', '.rel', '.pul',
'.szs', '.brres', '.thp', '.brsar', '.rkg')
$forbiddenSegments = @(
'\BuildWorkspace\generated\', '\BuildWorkspace\runtime\build\', '\GameAssets\', '\BuildWorkspace\Assets\',
'\BuildWorkspace\PulsarPacks\', '\BuildWorkspace\build\base\', '\BuildWorkspace\build\mods\'
+4 -3
View File
@@ -45,13 +45,14 @@ foreach ($relative in $sourceFiles | Sort-Object -Unique) {
}
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
# Native HLE wrappers also call these eight guest symbols directly. Give each
# Native HLE and VR wrappers also call these guest symbols directly. Give each
# its own generated blr function so the real product can link without game code.
# Keep this list explicit: a new unresolved guest dependency must fail the test.
[uint32]$entry = 0x80001000L
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL,
0x8056A470L, 0x8056A580L, 0x805A6C58L
)
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
$dataOffset = 0x100 + $textSize
@@ -135,7 +136,7 @@ try {
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON', '-DMKW_ENABLE_OPENXR=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
+49
View File
@@ -0,0 +1,49 @@
[CmdletBinding()]
param([string]$Tag, [string]$SetupPath)
$ErrorActionPreference = 'Stop'
$version = ([xml](Get-Content -LiteralPath (Join-Path $PSScriptRoot 'Directory.Build.props') -Raw)).Project.PropertyGroup.Version
if ($Tag -and $Tag -cne "v$version") { throw "Release tag must be v$version, got $Tag" }
if ($SetupPath) {
$reported = (& $SetupPath --version | Out-String).Trim()
if ($LASTEXITCODE -ne 0 -or $reported -cne $version) { throw 'Setup executable version mismatch.' }
$info = (& $SetupPath --info-json | Out-String) | ConvertFrom-Json
if ($LASTEXITCODE -ne 0 -or $info.productId -cne 'wiicompiled-openxr-vr' -or
$info.version -cne $version -or $info.openxrD3D12 -ne $true) { throw 'Setup VR identity mismatch.' }
# Inspect the appended payload as well: a correctly versioned host can still wrap stale tools.
Add-Type -AssemblyName System.IO.Compression
$stream = [IO.File]::OpenRead((Resolve-Path -LiteralPath $SetupPath))
$payload = [IO.MemoryStream]::new()
try {
if ($stream.Length -lt 24) { throw 'Missing payload footer.' }
$stream.Position = $stream.Length - 24
$reader = [IO.BinaryReader]::new($stream, [Text.Encoding]::ASCII, $true)
try {
$magic = [Text.Encoding]::ASCII.GetString($reader.ReadBytes(8))
$offset = $reader.ReadInt64()
$length = $reader.ReadInt64()
} finally { $reader.Dispose() }
if ($magic -cne 'MKWCPAY1' -or $offset -lt 0 -or $length -le 0 -or $offset + $length -ne $stream.Length - 24) {
throw 'Invalid payload footer.'
}
$stream.Position = $offset
$buffer = [byte[]]::new(1048576)
while ($length -gt 0) {
$count = $stream.Read($buffer, 0, [int][Math]::Min($length, $buffer.Length))
if ($count -eq 0) { throw 'Truncated payload.' }
$payload.Write($buffer, 0, $count)
$length -= $count
}
$payload.Position = 0
$zip = [IO.Compression.ZipArchive]::new($payload, [IO.Compression.ZipArchiveMode]::Read, $true)
try {
$entry = $zip.GetEntry('payload-manifest.json')
if (-not $entry) { throw 'Missing payload manifest.' }
$textReader = [IO.StreamReader]::new($entry.Open())
try { $manifest = $textReader.ReadToEnd() | ConvertFrom-Json } finally { $textReader.Dispose() }
if ($manifest.ProductId -cne $info.productId -or $manifest.ProductVersion -cne $version) {
throw 'Payload identity/version does not match the executable.'
}
} finally { $zip.Dispose() }
} finally { $payload.Dispose(); $stream.Dispose() }
}
Write-Output "Verified WiiCompiled OpenXR VR $version"
@@ -26,7 +26,7 @@ public static class RuntimeConfiguration
/// <summary>The per-user configuration an ordinary (non-portable) installation shares.</summary>
public static string ApplicationDataConfigPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"WiiCompiled", ConfigFileName);
OperatingSystem.IsWindows() ? "WiiCompiledOpenXRVR" : "WiiCompiled", ConfigFileName);
/// <summary>
/// The configuration file that governs <paramref name="installDirectory"/>. This mirrors
@@ -14,6 +14,7 @@ internal enum AppMode
CheckProducts,
RepairProducts,
Version,
InfoJson,
EmitPayloadIdentities
}
@@ -58,6 +59,7 @@ internal sealed class CommandLine
switch (args[i].ToLowerInvariant())
{
case "--version": result.Mode = AppMode.Version; break;
case "--info-json": result.Mode = AppMode.InfoJson; break;
case "--silent": result.Mode = AppMode.SilentInstall; break;
case "--verify-inputs": result.Mode = AppMode.VerifyInputs; break;
case "--uninstall": result.Mode = AppMode.Uninstall; break;
@@ -19,6 +19,7 @@ internal static class ConsoleCommands
Console.Out.WriteLine(" --launch-retro | --launch-base");
Console.Out.WriteLine(" --uninstall --install-dir <dir>");
Console.Out.WriteLine(" --version");
Console.Out.WriteLine(" --info-json");
return 0;
}
@@ -28,6 +29,15 @@ internal static class ConsoleCommands
return 0;
}
public static int InfoJson()
{
Console.Out.WriteLine(JsonSerializer.Serialize(new
{
productId = ProductInfo.Id, version = ProductInfo.Version, openxrD3D12 = true
}));
return 0;
}
public static int EmitPayloadIdentities(CommandLine command)
{
@@ -7,6 +7,7 @@ internal static class GameLaunchService
public static Task<int> LaunchAsync(BuildProfile profile)
{
var installation = new Installation(AppContext.BaseDirectory);
ProductOwnership.Ensure(installation.Root);
using var operation = InstallOperationLock.Acquire(installation.Root);
// A portable installation the user moved is reconciled before anything is read from its
// recorded location. Launch never needs the discarded native build tree.
@@ -12,6 +12,7 @@ internal sealed class InstallerEngine
{
var installDirectory = Path.GetFullPath(options.InstallDirectory);
ValidateInstallDirectory(installDirectory);
ProductOwnership.Ensure(installDirectory, allowEmpty: true);
InputValidation.ValidateExtension(options.GamePath);
var parent = Directory.GetParent(installDirectory)?.FullName
@@ -37,6 +38,8 @@ internal sealed class InstallerEngine
using var payload = PayloadArchive.OpenCurrent();
var manifest = payload.ReadManifest();
if (manifest.ProductId != ProductInfo.Id || manifest.ProductVersion != ProductInfo.Version)
throw new InvalidDataException("This setup payload does not match WiiCompiled OpenXR VR and its version.");
var toolkit = InstalledLayout.Toolkit(staging);
var workspace = InstalledLayout.Workspace(staging);
@@ -366,6 +369,7 @@ internal sealed class InstallerEngine
ProductVersion = manifest.ProductVersion,
InstallDir = installDirectory,
InstalledUtc = previousState?.InstalledUtc ?? DateTime.UtcNow,
ProductId = ProductInfo.Id,
RetroRewindInstalled = previousState?.RetroRewindInstalled ?? false,
ToolkitReleaseTag = manifest.ToolkitReleaseTag,
DolSha256 = manifest.ExpectedDolSha256,
@@ -35,6 +35,7 @@ internal sealed class InstallOptions
internal sealed class PayloadManifest
{
public string ProductId { get; set; } = "";
public int SchemaVersion { get; set; }
public string ProductVersion { get; set; } = "";
public string ExpectedGameId { get; set; } = "RMCP01";
@@ -72,6 +73,7 @@ internal sealed class DiscHeader
internal sealed class InstallState
{
public string ProductId { get; set; } = "";
[JsonRequired]
public int SchemaVersion { get; set; } = 1;
@@ -0,0 +1,29 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Windows;
internal static class ProductOwnership
{
public static void Ensure(string directory, bool allowEmpty = false)
{
for (var current = new DirectoryInfo(Path.GetFullPath(directory)); current is not null; current = current.Parent)
if (current.Exists && (current.Attributes & FileAttributes.ReparsePoint) != 0)
throw new InvalidDataException("Use a VR installation directory without junctions or symbolic links.");
if (!Directory.Exists(directory))
{
if (allowEmpty) return;
throw new InvalidDataException("WiiCompiled OpenXR VR is not installed in this directory.");
}
// A junction must not let an apparently separate VR destination overwrite another install.
if (allowEmpty && !Directory.EnumerateFileSystemEntries(directory).Any()) return;
var statePath = Path.Combine(directory, InstalledLayout.InstallStateFileName);
if (File.Exists(statePath))
{
using var state = JsonDocument.Parse(File.ReadAllText(statePath));
if (state.RootElement.TryGetProperty("ProductId", out var id) && id.GetString() == ProductInfo.Id)
return;
}
throw new InvalidDataException("This folder belongs to a normal or unidentified WiiCompiled installation. " +
"Choose a separate empty folder for OpenXR VR; existing files have not been changed.");
}
}
@@ -77,6 +77,7 @@ internal sealed class ProductRepairService
RetroWfcPayloadMode requestedPayloadMode, ReconcileOptions options,
CancellationToken cancellationToken)
{
ProductOwnership.Ensure(_installation.Root);
if (!_installation.IsPresent)
throw new InvalidOperationException("WiiCompiled is not installed here.");
if (!_installation.HasToolkit)
@@ -16,7 +16,7 @@ internal static class Program
{
Console.Out.WriteLine(
"Mario Kart WiiCompiled is installed through Wheel Wizard - download it from " +
"https://github.com/TeamWheelWizard/WheelWizard");
"https://github.com/iChris4/WheelWizard_VR");
Console.ReadKey(intercept: true);
return 0;
}
@@ -37,6 +37,8 @@ internal static class Program
if (command.Mode == AppMode.Version)
return ConsoleCommands.Version();
if (command.Mode == AppMode.InfoJson)
return ConsoleCommands.InfoJson();
if (command.Mode == AppMode.Help)
return ConsoleCommands.Help();
@@ -120,8 +122,9 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.31";
public const string Name = "WiiCompiled OpenXR VR";
public const string Id = "wiicompiled-openxr-vr";
public static string Version => typeof(ProductInfo).Assembly.GetName().Version!.ToString(3);
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -129,8 +132,8 @@ internal static class ProductInfo
/// </summary>
public const string SetupCopyName = "WiiCompiled-Setup.exe";
public const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\WiiCompiled";
public const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\WiiCompiledOpenXRVR";
public static string DefaultInstallDirectory =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "WiiCompiled");
"Programs", "WiiCompiledOpenXRVR");
}
@@ -6,9 +6,36 @@ namespace WiiCompiled.Setup.Windows;
internal static class SelfTests
{
private static void TestVrOwnership()
{
var root = Path.Combine(Path.GetTempPath(), "WiiCompiled-VR-ownership-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
ProductOwnership.Ensure(root, allowEmpty: true);
var statePath = Path.Combine(root, InstalledLayout.InstallStateFileName);
File.WriteAllText(statePath, "{\"SetupVersion\":\"0.2.32\"}");
var original = File.ReadAllText(statePath);
try { ProductOwnership.Ensure(root, allowEmpty: true); throw new Exception("Accepted an unidentified install."); }
catch (InvalidDataException) { }
if (File.ReadAllText(statePath) != original) throw new Exception("Normal state was changed by rejection.");
File.WriteAllText(statePath, "{\"ProductId\":\"wiicompiled\"}");
try { ProductOwnership.Ensure(root); throw new Exception("Accepted a normal install."); }
catch (InvalidDataException) { }
JsonState.Write(statePath, new InstallState { ProductId = ProductInfo.Id, InstallDir = root });
ProductOwnership.Ensure(root);
if (RuntimeConfiguration.ApplicationDataConfigPath.Contains("WiiCompiled\\Config.toml", StringComparison.Ordinal))
throw new Exception("The VR setup still uses normal application data.");
if (!ProductInfo.UninstallKey.EndsWith("WiiCompiledOpenXRVR", StringComparison.Ordinal))
throw new Exception("The VR uninstall identity is not isolated.");
}
finally { Directory.Delete(root, recursive: true); }
}
public static int Run()
{
var failures = new List<string>();
Test("VR installation ownership", TestVrOwnership, failures);
Test("ISO extension", () => TestAcceptedExtension(".iso"), failures);
Test("GCM extension", () => TestAcceptedExtension(".gcm"), failures);
Test("GCZ extension", () => TestAcceptedExtension(".gcz"), failures);
@@ -1291,7 +1318,7 @@ internal static class SelfTests
RetroWfcPayloadMode = "skipped" });
JsonState.Write(installation.InstallStatePath, new InstallState
{
InstallDir = root, RetroRewindInstalled = true,
ProductId = ProductInfo.Id, InstallDir = root, RetroRewindInstalled = true,
DolSha256 = dolSha, RelSha256 = relSha,
RetroRewindCodePulSha256 = inputsA.CodePulSha256,
RetroRewindCompileInputsSha256 = inputsA.CompileInputsSha256,
@@ -4,7 +4,7 @@ namespace WiiCompiled.Setup.Windows;
internal static class ShellIntegration
{
private const string ShortcutFileName = "wiicompiled (base) (beta).lnk";
private const string ShortcutFileName = "WiiCompiled OpenXR VR (base) (beta).lnk";
public static void RegisterUninstaller(string installDirectory, bool retroInstalled)
{
@@ -28,6 +28,9 @@ internal static class UninstallService
public static int RunWorker(string installDirectory, bool quiet)
{
installDirectory = Path.GetFullPath(installDirectory);
ProductOwnership.Ensure(installDirectory);
using var operation = InstallOperationLock.Acquire(installDirectory);
RunningProductGuard.EnsureProductsNotRunning(installDirectory);
Thread.Sleep(750);
TryCleanup("remove shortcuts", quiet, ShellIntegration.RemoveShortcuts);
@@ -7,9 +7,8 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.31</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Product>WiiCompiled OpenXR VR</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
+2
View File
@@ -0,0 +1,2 @@
64fdd492f6ce71fd223b49f493e0809d9a7f9ed45ebaa3605f626eb77282dbe4 WiiCompiled-Setup.exe
b319b86d46bea6ebfa52965c65706976d6004abc01914ff82a2b73c71de84def WheelWizardVRWindows.exe
+10 -3
View File
@@ -15,8 +15,14 @@ This is an experimental renderer, not yet a release-ready VR mode.
- A build made with `MKW_ENABLE_OPENXR=ON`, which defaults on for Windows and off elsewhere while
the Vulkan bridge remains capability-gated.
OpenXR remains disabled until requested in `Config.toml`. The file is next to the installed game
configuration and is created with the following defaults:
For managed installation, use [WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest)
and enable **Settings → Other → WiiCompiled (beta) → Enable WiiCompiled OpenXR VR (beta)**.
The launcher sets `vr.enabled=true`, `vr.required=false`, and `video.graphics_api="d3d12"` before
each VR launch, preserving other preferences. Its portable configuration lives at
`RecompVR/UserData/Config.toml` beneath WheelWizard's data folder. Normal graphics settings remain
in `Recomp/UserData/Config.toml`. Both backends use the normal installation's effective NAND.
Standalone launches remain opt-in. `Config.toml` is created with the following defaults:
```toml
[vr]
@@ -46,7 +52,8 @@ at launch. The in-game F10 settings bar also exposes the enable switch, but a re
required.
`required = false` is the safe default: an absent runtime, disconnected headset, unsupported GPU,
or graphics-binding failure is logged and the game continues in ordinary desktop mode. Set it to
or graphics-binding failure is logged and the game continues in ordinary desktop mode. A temporary
notification explains the failure; the message remains available under **F10 → VR**. Set it to
`true` only when a failed VR startup should stop the game with an error.
`mirror_view` chooses what the desktop window shows while the headset is running: `"normal"`
+19 -9
View File
@@ -1,5 +1,5 @@
# WiiCompiled
# WiiCompiled OpenXR VR
A native PC port of Mario Kart Wii, made with static recompilation.
@@ -12,7 +12,7 @@ anywhere at runtime.
> toolchain, the translation runs on your machine against your disc image, and nothing ever gets
> uploaded.
[What is a github, I just want to play](https://github.com/TeamWheelWizard/WheelWizard/releases/latest)
[Download WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest)
---
@@ -96,8 +96,7 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- This fork's packaged release supports Windows x64. Other platforms are not release targets.
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -113,15 +112,26 @@ regions, patched executables) is rejected outright.
## Installing
For an easy experience, use [Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard). Pick your clean PAL `RMCP01`
image under Settings, turn on **WiiCompiled (beta)**, and hit install from the Home page.
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
Use [WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest). Select your clean PAL
`RMCP01` image in Settings, then open **Settings → Other → WiiCompiled (beta)** and enable
**Enable WiiCompiled OpenXR VR (beta)**. Press Install on Home. Installation builds both Base game
and Retro Rewind locally using the bundled toolchain; a developer toolchain is not required.
Home lets you choose **Base game** or **Retro Rewind**. The normal WiiCompiled switch selects the
original backend; turning both switches off selects Dolphin. Only one recompilation switch can
be enabled at a time. VR uses a separate `RecompVR` installation beside the normal `Recomp` folder.
Saves and Miis use the normal installation's effective NAND; Retro Rewind retains its separate
XML-directed saves and ghosts. Graphics, VR preferences, caches, and compiled binaries stay separate.
Uninstalling either backend in WheelWizard VR preserves configuration and shared progress.
Managed VR launches enable OpenXR with D3D12. If the runtime or headset is unavailable, the game
continues on the desktop and displays the failure briefly; **F10 → VR** retains the explanation.
See [OpenXR configuration](OPENXR.md) and [distribution and validation](DISTRIBUTION.md).
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
> [Releases](https://github.com/iChris4/Wiicompiled_VR/releases) page. If someone's sharing an
> installer through Discord or some random download site, don't touch it!!
## A note on related projects
+85
View File
@@ -0,0 +1,85 @@
# WheelWizard VR integration validation
Validated locally on Windows x64 on 2026-09-09. These are release candidates; public release
acceptance is not complete.
## Candidates
| Product | Version | Local artifact | SHA-256 |
| --- | --- | --- | --- |
| WiiCompiled OpenXR VR setup | 0.2.32 | `Launcher/dist-vr/WiiCompiled-Setup.exe` | `64fdd492f6ce71fd223b49f493e0809d9a7f9ed45ebaa3605f626eb77282dbe4` |
| WheelWizard VR | 2.5.5 | `Launcher/dist-vr/WheelWizardVRWindows.exe` | `b319b86d46bea6ebfa52965c65706976d6004abc01914ff82a2b73c71de84def` |
The launcher artifact is also in the launcher checkout at `artifacts/WheelWizardVRWindows.exe`.
`Launcher/dist-vr/SHA256SUMS.txt` contains both checksums. Only these executables and their checksums
are intended for distribution. The setup contains the toolkit and source payload, not a compiled
game. All extracted assets, normal installation copies, compiled games, and test NANDs remain in
ignored local validation directories.
The existing local setup in `Launcher/dist` is the user's 0.2.31 rebuild. Its SHA-256 matches the
setup asset published under the VR fork's `0.2` tag. The backend fork also has tag `0.1`; the
launcher fork has no published release. These were checked through the GitHub API before finalizing
the candidates. The next source patch versions are 0.2.32 and 2.5.5. New release tags use `vX.Y.Z`
and must agree with the single version source in each product's `Directory.Build.props`.
## Passed
- WheelWizard suite: **255 tests**, including backend selection/migration, configuration isolation,
all four backend/game launch command paths, same-version wrong-product rejection, separate
release routing, exact self-update asset selection, shared NAND configuration, Mii edit blocking,
and uninstall preservation of both backends' configuration, NAND, and ghosts.
- Retro Rewind transaction tests: staged replacement, cancellation, preservation of saves/ghosts
and XML save directories, current distribution ZIP layout, archive path validation, interrupted
publication recovery, and a real Windows locked-file failure after one directory was published.
- Translator suite: **576 tests**.
- Packaged setup self-tests, including product ownership, configuration, portable move healing,
install transaction rollback, interrupted-install recovery, Retro-WFC snapshot/retry behavior,
compile-input fingerprints, and product report contracts.
- Full synthetic PowerPC translation and native OpenXR runtime link. The synthetic fixture now
includes the three callbacks used by the VR camera; no game translation is used for this test.
- Pinned-fact audit and setup payload boundary audit. Native dependency audit inspected **524
binaries and 5,277 imports**. The pinned OpenXR SDK and static loader were built; licenses remain
included. A configured bundled SDK takes precedence over a developer machine's OpenXR package.
- Executable identity/version and embedded setup payload identity/version agree. Both Windows
release workflows use existing GitHub Actions versions and have no Linux/macOS release jobs.
- Final packaged setup installed both VR products from the owned clean PAL image into an isolated
portable installation. Updating that installation with the final package succeeded, and both
products reported `current` afterward.
- Real product checks after modifying isolated Retro Rewind content: an asset-only change kept
both products current; a Code.pul change reported `code-pul-changed` for Retro Rewind and left
Base current; restoring the content returned both products to current.
- Normal Base, normal Retro Rewind, VR Base, and VR Retro Rewind each stayed running during a
20-second startup observation through their installed setup host and closed with setup exit 0.
Normal tests used an isolated copy of the existing normal installation. Test configurations
pointed to the same isolated NAND and isolated Retro Rewind content.
- Both VR runs used a child-process-only missing OpenXR runtime override. Logs confirmed
`vr.enabled=true`, `vr.required=false`, D3D12, and desktop fallback. System OpenXR registration
was not changed.
- The final single-file launcher started using an isolated portable configuration, displayed
`WheelWizard VR` as its window title, and closed with exit 0. Its startup log contained no
application-start or unhandled-exception errors. Publishing succeeded with existing Windows
registry platform-analyzer warnings.
- Source synchronization checked destination hashes before replacing files and retained backups.
Both repositories pass `git diff --check`. Source changes are local and have not been committed
or pushed as part of this validation.
## Still required for release acceptance
- Interactive Home/settings verification and complete install/launch flows for all four selections.
Command-level tests and isolated startup observations do not establish menu or race correctness.
- Headset menus and races for Base and Retro Rewind, physical headset absence, runtime loss during
a session, and visual confirmation of the nonblocking fallback notification.
- Real player progress/Miis/ghosts exercised across gameplay, updates, reinstall, and uninstall.
Automated preservation tests passed; live player data was deliberately excluded from smoke tests.
- Fresh installation on a clean Windows machine without developer toolchains. The tested package
used its bundled tools on this workstation; a pristine-machine run has not been performed.
- Live network-interruption/download tests and interactive overlapping-launcher scenarios.
File leases, content cancellation, interrupted publication, and setup rollback have automated
coverage, but the complete manual matrix remains outstanding.
After acceptance, publish the VR backend first, then the launcher. The launcher release workflow
downloads the published backend and checks its VR identity, capability, and tag/version agreement
before publishing. Until that backend release exists, a fresh launcher install cannot resolve this
local 0.2.32 candidate from GitHub automatically.
Detailed local logs and isolated installations are under `.scratch/` (excluded from distribution).
+4 -1
View File
@@ -254,7 +254,10 @@ if(MKW_ENABLE_OPENXR)
message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR")
set(MKW_ENABLE_OPENXR OFF)
else()
find_package(OpenXR CONFIG QUIET)
# An installer-supplied SDK must win over any developer machine package.
if(NOT MKW_OPENXR_SDK_DIR AND NOT FETCHCONTENT_SOURCE_DIR_OPENXR)
find_package(OpenXR CONFIG QUIET)
endif()
if(NOT TARGET OpenXR::openxr_loader AND NOT TARGET openxr_loader)
include(FetchContent)
set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE)
+4
View File
@@ -130,7 +130,11 @@ inline std::filesystem::path PathFromUtf8(std::string_view text) {
}
inline constexpr const char* kConfigFileName = "Config.toml";
#ifdef _WIN32
inline constexpr const char* kApplicationDirectoryName = "WiiCompiledOpenXRVR";
#else
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
#endif
// Portable layout. A directory holding kPortableMarkerFileName is a portable root; every piece of
// runtime user state (Config.toml, NAND, Cache, Logs) lives in <root>/UserData instead of
+28
View File
@@ -907,6 +907,11 @@ void DrawGraphicsSettings() {
// group, and keeping them out of Graphics stops that menu from running off the
// bottom of the screen.
void DrawVrSettings() {
const auto xrError = mkw::vr::OpenXRLastError();
if (!xrError.empty()) {
ImGui::TextWrapped("OpenXR unavailable: %s", xrError.c_str());
ImGui::TextWrapped("Playing on the desktop. Check your headset and active OpenXR runtime, then restart.");
}
if (ImGui::Checkbox("Enable OpenXR VR", &g_vrEnabled)) {
RuntimeConfigFile::SetVrEnabled(g_vrEnabled);
}
@@ -1420,6 +1425,29 @@ void Draw() noexcept {
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
aurora_wait_for_frame_worker();
// Explain fallback without interrupting gameplay or capturing input.
static std::string shownXrError;
static double xrNoticeUntil = 0.0;
const auto xrError = mkw::vr::OpenXRLastError();
if (!xrError.empty() && xrError != shownXrError) {
shownXrError = xrError;
xrNoticeUntil = ImGui::GetTime() + 15.0;
}
if (!shownXrError.empty() && ImGui::GetTime() < xrNoticeUntil) {
ImGui::SetNextWindowPos(ImVec2(16.0f, 60.0f), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(0.9f);
if (ImGui::Begin("OpenXR desktop fallback", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::PushTextWrapPos(440.0f);
ImGui::TextUnformatted("OpenXR unavailable - playing on the desktop.");
ImGui::TextUnformatted(shownXrError.c_str());
ImGui::TextUnformatted("Check your headset and active OpenXR runtime, then restart. Details: F10 > VR.");
ImGui::PopTextWrapPos();
}
ImGui::End();
}
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
// runs once per presented frame whatever the game is doing (e.g. sitting in
// its "communications interrupted" prompt without polling pads). Same guest