Compare commits

...

9 Commits

Author SHA1 Message Date
patchzyy f715d37678 Fix native build paths for special characters 2026-09-29 10:28:51 +02:00
patchzyy a05c89739d Always refresh Retro-WFC payload with fallback (#264) 2026-09-28 21:01:40 +02:00
patchzyy 82991ec337 Version centrilsation 2026-09-27 13:00:12 +02:00
patchzyy e164af9ff4 Shader wait screen 2026-09-27 12:39:10 +02:00
patchzyy e409d9f99b undo black kart fix
it causes stutters
2026-09-27 12:11:03 +02:00
Michael G 85f250155f Fix controller mapping platform, player index sync, and socket errors (#251)
* Fix macOS controller platform, sync player indices, and preserve socket send error

- Preserve captured socket send error across diagnostic logging in network_socket.cpp
- Use SDL_GetPlatform() instead of hardcoded Windows in controller mapping wizard
- Synchronize Aurora and SDL player indices on port assignment and clear in pad.cpp
- Auto-assign Player 1 on macOS when unconfigured in input.cpp

* fix(input): refine macOS port 0 assignment and persistence logic

- Persist port 0 preference for newly connected controller even if already assigned player index 0
- Verify controller has no preference on other ports before assigning to port 0
2026-09-24 22:01:49 +02:00
patchzyy a9c9c0de26 Add force 16:9 viewport mode (#256) 2026-09-24 22:00:57 +02:00
patchzyy c92e45c2e7 Add early Linux linker dependency probe (#255) 2026-09-24 18:53:56 +02:00
patchzyy 583e702547 add bltl (#254) 2026-09-24 18:06:32 +02:00
36 changed files with 382 additions and 92 deletions
+2 -4
View File
@@ -107,7 +107,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported # 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 differs from the release tag, so a tag that was pushed without bumping the pinned
# version would ship an update nobody can install. Catch that before anything is published. # version would ship an update nobody can install. Catch that before anything is published.
- name: Verify the tag matches the pinned setup version - name: Verify the tag matches the pinned setup version
env: env:
@@ -122,9 +122,7 @@ jobs:
status=1 status=1
fi fi
} }
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";" check Launcher/Directory.Build.props "<Version>$version</Version>"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
exit $status exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file # The build jobs upload with `archive: false`, which stores each installer as a raw file
+4 -1
View File
@@ -279,9 +279,12 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." } if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." }
} }
$productVersion = ((& $setupHost --version) -join '').Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($productVersion)) { throw 'The setup host did not report its version.' }
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.32' ProductVersion = $productVersion
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
+7
View File
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.33</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+10 -2
View File
@@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) {
function Reset-LocalDirectory([string]$Path) { function Reset-LocalDirectory([string]$Path) {
$full = [IO.Path]::GetFullPath($Path) $full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\' $root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\' $installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\'
# The caller-supplied output destinations are legitimate reset targets by # The caller-supplied output destinations are legitimate reset targets by
# definition, wherever the caller placed them: a fresh install's operation # definition, wherever the caller placed them: a fresh install's operation
# scratch lives beside the installation directory rather than inside it. # scratch lives beside the installation directory rather than inside it.
@@ -150,8 +150,16 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory))
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) { if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
throw '-BaseOutputDirectory is valid only with -Profile both.' throw '-BaseOutputDirectory is valid only with -Profile both.'
} }
$realWorkspace = $Workspace.TrimEnd('\')
$Workspace = Get-MkwBuildSafePath $realWorkspace 'workspace' 'runtime\CMakeLists.txt'
# A selected package inside the install may also name the same Code.pul through the real path.
# Give it the workspace spelling before comparing it with the staged copy.
if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and
$RetroRewindPackageDirectory.StartsWith($realWorkspace + '\', [StringComparison]::OrdinalIgnoreCase)) {
$RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length)
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe' $translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit $toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe' $cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe' $ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin' $toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
+29 -15
View File
@@ -40,41 +40,55 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';') ) -join ';')
} }
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) { function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' } <#
$full = [IO.Path]::GetFullPath($ToolchainRoot) The Windows native build passes workspace paths through CMake, Ninja response files and
clang, which do not all interpret quotes the same way. Keep those paths plain even when the
user's install directory contains an apostrophe, ampersand or other punctuation.
#>
if ([string]::IsNullOrWhiteSpace($Path)) { throw "A $Kind path is required." }
$full = [IO.Path]::GetFullPath($Path)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive. # A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') } if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -notmatch '[()&^%!]') { return $full } if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full }
Assert-File (Join-Path $full $MarkerFile) "$Kind marker"
$sha = [Security.Cryptography.SHA256]::Create() $sha = [Security.Cryptography.SHA256]::Create()
try { try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant())) $bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() } } finally { $sha.Dispose() }
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '') $linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @() $failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) { foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue } if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName $link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try { try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null [IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is $existing = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse if ($null -ne $existing) {
# point itself, where Remove-Item -Recurse would delete the toolchain it points at. # Never trust a directory just because it has the marker: it could point at a
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) { # different installation. Nor may we remove a directory we did not create.
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } if ($existing.LinkType -ne 'Junction' -or
@($existing.Target).Count -ne 1 -or
-not [string]::Equals([IO.Path]::GetFullPath(@($existing.Target)[0]),
$full, [StringComparison]::OrdinalIgnoreCase)) {
throw "An existing path is not the expected junction: $link"
}
} else {
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
} }
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse" Assert-File (Join-Path $link $MarkerFile) "$Kind junction marker"
Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably"
return $link return $link
} catch { } catch {
$failures += "$link ($($_.Exception.Message))" $failures += "$link ($($_.Exception.Message))"
} }
} }
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " + throw ("The $Kind path $full cannot be passed safely to the native build, and no junction " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') + 'to it could be created: ' + ($failures -join '; ') + '. Install to a path of plain ' +
'. Install to a path without those characters.') 'letters, digits and spaces, or make a safe junction location available.')
} }
function Get-MkwProjectPins([string]$ProjectFile) { function Get-MkwProjectPins([string]$ProjectFile) {
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description> <Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -5,7 +5,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description> <Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
+5 -1
View File
@@ -1,9 +1,13 @@
using System.Reflection;
namespace WiiCompiled.Setup.Linux; namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.32"; public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
} }
/// <summary>One installed product's record inside install-state.json.</summary> /// <summary>One installed product's record inside install-state.json.</summary>
+28 -7
View File
@@ -127,19 +127,40 @@ internal static class Program
string? retroWfcOfflineDir = null; string? retroWfcOfflineDir = null;
if (downloadPayload) if (downloadPayload)
{ {
// Reused if a previous install already downloaded and it's still valid - matches
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload"); var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1); reporter.Progress(InstallStages.Validate,
"Downloading the current Retro-WFC payload", 1);
try try
{ {
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir); // A valid signature authenticates a payload, but does not prove it is the latest
} // signed revision. Always ask the fixed endpoint for the current snapshot; the
catch (InvalidDataException) // downloader verifies it before atomically replacing the cache.
{
await RetroWfcPayload.DownloadRetroWfcPayloadAsync( await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token); RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
} }
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
downloadFailure is HttpRequestException or TimeoutException
or IOException)
{
// Offline installs may continue with a previously authenticated snapshot. Do not
// use this path for a newly downloaded payload that failed signature validation:
// that must remain a hard failure instead of hiding possible endpoint tampering.
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
{
throw new InvalidOperationException(
"The current Retro-WFC payload could not be downloaded and no valid cached " +
$"payload is available ({cacheFailure.Message.TrimEnd('.')}).", downloadFailure);
}
reporter.Diagnostic(
"The current Retro-WFC payload could not be downloaded; using the previously " +
$"verified cached payload instead ({downloadFailure.Message.TrimEnd('.')}).");
}
retroWfcOfflineDir = cacheDir; retroWfcOfflineDir = cacheDir;
} }
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName> <AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace> <RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description> <Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -110,8 +110,7 @@ internal sealed class ProductRepairService
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken); InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
} }
catch (Exception ex) when (!cancellationToken.IsCancellationRequested && catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or IOException or InvalidDataException ex is HttpRequestException or TimeoutException or IOException)
or InvalidOperationException or OperationCanceledException)
{ {
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint, payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken); Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
@@ -1,5 +1,6 @@
namespace WiiCompiled.Setup.Windows; namespace WiiCompiled.Setup.Windows;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
internal static class Program internal static class Program
@@ -121,7 +122,9 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.32"; public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,6 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+24
View File
@@ -190,6 +190,30 @@ assert_file "$project" "Translation project"
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)" assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)" assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
# The AppImage bundles Clang, but Linux startup objects and the C/C++ link runtimes
# still come from the host. Check them before the expensive translation so a missing
# development package produces a useful error instead of CMake's generic exit 1.
link_probe_dir=$(mktemp -d)
link_probe_flags=()
[[ -z "$sysroot" ]] || link_probe_flags+=(--sysroot="$sysroot")
[[ -z "$fuse_ld_override" ]] || link_probe_flags+=(-fuse-ld="$fuse_ld_override")
printf 'int main(void) { return 0; }\n' > "$link_probe_dir/probe.c"
cat > "$link_probe_dir/probe.cpp" <<'EOF'
#include <vector>
int main() { std::vector<int> values{1}; return values.front() - 1; }
EOF
if ! "$cc_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.c" -o "$link_probe_dir/probe-c" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C compiler cannot link a test program. Linux needs C development files (glibc startup objects and a compiler runtime) in addition to bundled Clang. Install your distribution's development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
if ! "$cxx_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.cpp" -o "$link_probe_dir/probe-cxx" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C++ compiler cannot link a test program. Install your distribution's C++ development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
rm -rf "$link_probe_dir"
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same # Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one # approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
# field this script actually needs from the manifest. # field this script actually needs from the manifest.
+27 -13
View File
@@ -77,26 +77,40 @@ if [[ -n "$retro_dir" ]]; then
# verify its pinned signature before publishing it into the local cache. # verify its pinned signature before publishing it into the local cache.
retro_wfc_dir="$support_root/RetroWfcPayload" retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin" retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then cached_payload_valid=0
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2 if [[ -f "$retro_wfc_payload" ]]; then
rm -f "$retro_wfc_payload" if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
cached_payload_valid=1
else
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload"
fi
fi fi
if [[ ! -f "$retro_wfc_payload" ]]; then
printf 'Downloading the Retro-WFC payload needed for online play...\n' # A signed cache may still be an older vulnerable revision, so always attempt to replace it
mkdir -p "$retro_wfc_dir" # with the current signed snapshot. A transport failure may fall back to the verified cache;
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX") # a downloaded snapshot with an invalid signature remains a hard failure.
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin" printf 'Downloading the current Retro-WFC payload needed for online play...\n'
mkdir -p "$(dirname "$temporary_payload")" mkdir -p "$retro_wfc_dir"
trap 'rm -rf "$payload_stage"' EXIT payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
--retry 1 --output "$temporary_payload" \ mkdir -p "$(dirname "$temporary_payload")"
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play' trap 'rm -rf "$payload_stage"' EXIT
if /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation' fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary" mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload" mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage" rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT trap - EXIT
elif (( cached_payload_valid )); then
printf 'Could not download the current Retro-WFC payload; using the previously verified cached payload.\n' >&2
rm -rf "$payload_stage"
trap - EXIT
else
fail 'could not download the current Retro-WFC payload and no valid cached payload is available'
fi fi
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir") build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
fi fi
@@ -88,6 +88,7 @@ typedef enum _AuroraViewportPolicy {
AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer
AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface
AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly
AURORA_VIEWPORT_16_9 = 3, // Fixed 16:9 content with bars on other surfaces
} AuroraViewportPolicy; } AuroraViewportPolicy;
/** /**
+1
View File
@@ -46,6 +46,7 @@ void AuroraSetViewportPolicy(AuroraViewportPolicy policy) {
} }
g_gxState.viewportPolicy = policy; g_gxState.viewportPolicy = policy;
aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT); aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT);
aurora::window::set_force_aspect_16_9(policy == AURORA_VIEWPORT_16_9);
aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH); aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH);
if (changed) { if (changed) {
// Reapply the guest viewport and scissor after a resize. // Reapply the guest viewport and scissor after a resize.
+3 -5
View File
@@ -520,11 +520,9 @@ void GXCopyTex(void* dest, GXBool clear) {
clearState.clearAlpha = clear && alphaUpdate; clearState.clearAlpha = clear && alphaUpdate;
} }
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter); const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
// Every GXCopyTex is observable texture data. Reusing a destination in this // Skip only recurring color copies so one-shot copies are never lost.
// or the previous frame does not guarantee another redraw: menu thumbnail const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
// scratch targets can be reused and then retained. Depth copies have the const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
// same requirement. Only display presentation may skip unfinished draws.
const bool persistentCopy = true;
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth, aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt, clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha, &sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha,
+5 -4
View File
@@ -399,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
} }
void PADSetPortForIndex(const u32 idx, const u32 port) { void PADSetPortForIndex(const u32 idx, const u32 port) {
if (port >= PAD_MAX_CONTROLLERS) return;
const auto* ctrl = __PADGetControllerForIndex(idx); const auto* ctrl = __PADGetControllerForIndex(idx);
if (ctrl == nullptr) { if (ctrl == nullptr) {
return; 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) { 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) { if (oldPort >= 0 && oldPort != port) {
aurora::input::persist_controller_for_player(oldPort, nullptr); 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); aurora::input::persist_controller_for_player(port, ctrl);
} }
@@ -437,7 +438,7 @@ void PADClearPort(const u32 port) {
if (ctrl == nullptr) { if (ctrl == nullptr) {
return; 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 // Secondary bindings live only in memory; the runtime re-applies them from its
+2 -7
View File
@@ -1259,11 +1259,6 @@ void split_staging_batch() {
retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]); retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]);
} }
} }
// GXCopyTex can capture the ordinary EFB as well as an explicit offscreen
// target. Its command may arrive in the next batch, after this prefix has
// already been submitted. Preserve every prefix; target kind cannot tell
// us whether the guest will later retain these pixels in a texture.
for (auto& pass : g_renderPasses) pass.requireReadyPipelines = true;
auto encoder = g_device.CreateCommandEncoder(); auto encoder = g_device.CreateCommandEncoder();
end_batch(encoder); end_batch(encoder);
render(encoder); render(encoder);
@@ -1614,8 +1609,8 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, Pipelin
if (!skip_unready_pipelines()) { if (!skip_unready_pipelines()) {
pipelineReady = wait_pipeline(ref, pipeline); pipelineReady = wait_pipeline(ref, pipeline);
} else if (requireReady) { } else if (requireReady) {
// Texture copies and capacity prefixes must retain complete draw results. // The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a
// A future display frame cannot repair a texture that already captured them. // skipped draw would never be re-issued. These run behind loads, not mid-race.
pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline); pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline);
} else { } else {
pipelineReady = try_pipeline(ref, pipeline); pipelineReady = try_pipeline(ref, pipeline);
+2 -1
View File
@@ -214,7 +214,8 @@ Params make_params(wgpu::Extent3D sourceSize, const FrameMapping& mapping) noexc
return params; return params;
} }
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH; const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH ||
mapping.viewportPolicy == AURORA_VIEWPORT_16_9;
const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x); const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x);
const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y); const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y);
const float scale = std::min(scaleX, scaleY); const float scale = std::min(scaleX, scaleY);
+23
View File
@@ -414,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
g_GameControllers[instance] = controller; g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]); ensure_player_index(g_GameControllers[instance]);
apply_port_preferences(); 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; return instance;
} }
+27 -1
View File
@@ -46,6 +46,7 @@ SDL_Window* g_window;
SDL_Renderer* g_renderer; SDL_Renderer* g_renderer;
float g_frameBufferScale = 0.f; float g_frameBufferScale = 0.f;
bool g_frameBufferAspectFit = true; bool g_frameBufferAspectFit = true;
std::atomic_bool g_forceAspect169{false};
bool g_presentSurfaceFill = false; bool g_presentSurfaceFill = false;
int g_presentAspectWidth = 0; int g_presentAspectWidth = 0;
int g_presentAspectHeight = 0; int g_presentAspectHeight = 0;
@@ -525,7 +526,20 @@ AuroraWindowSize get_window_size() {
int fb_w = native_fb_w; int fb_w = native_fb_w;
int fb_h = native_fb_h; int fb_h = native_fb_h;
const auto [baseW, baseH] = vi::configured_fb_size(); const auto [baseW, baseH] = vi::configured_fb_size();
if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) { if (g_forceAspect169.load(std::memory_order_acquire) && native_fb_w > 0 && native_fb_h > 0) {
if (g_frameBufferScale > 0.f && baseW > 0 && baseH > 0) {
const auto [scaledW, scaledH] =
scale_frame_buffer_to_aspect(static_cast<int>(baseW), static_cast<int>(baseH),
g_frameBufferScale, 16.f / 9.f);
fb_w = scaledW;
fb_h = scaledH;
} else {
fb_w = std::min(native_fb_w,
std::max(1, static_cast<int>(std::lround(native_fb_h * (16.f / 9.f)))));
fb_h = std::min(native_fb_h,
std::max(1, static_cast<int>(std::lround(native_fb_w * (9.f / 16.f)))));
}
} else if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f; float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f;
if (g_frameBufferScale <= 0.f) { if (g_frameBufferScale <= 0.f) {
renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW), renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW),
@@ -759,6 +773,14 @@ void set_frame_buffer_aspect_fit(bool fit) {
request_frame_buffer_resize(); request_frame_buffer_resize();
} }
void set_force_aspect_16_9(bool force) {
if (g_forceAspect169.load(std::memory_order_relaxed) == force) {
return;
}
g_forceAspect169.store(force, std::memory_order_release);
request_frame_buffer_resize();
}
void set_present_surface_fill(bool fill) { void set_present_surface_fill(bool fill) {
g_presentSurfaceFill = fill; g_presentSurfaceFill = fill;
} }
@@ -781,6 +803,10 @@ void unlock_present_aspect_ratio() {
} }
bool get_present_aspect_ratio(float& aspect) noexcept { bool get_present_aspect_ratio(float& aspect) noexcept {
if (g_forceAspect169.load(std::memory_order_acquire)) {
aspect = 16.f / 9.f;
return true;
}
if (g_presentSurfaceFill && g_window != nullptr) { if (g_presentSurfaceFill && g_window != nullptr) {
// Queried once per presentation snapshot; use the cached native client size // Queried once per presentation snapshot; use the cached native client size
// instead of re-entering SDL for a value the window procedure already knows. // instead of re-entering SDL for a value the window procedure already knows.
+1
View File
@@ -56,6 +56,7 @@ void sync_frame_buffer_size() noexcept;
void request_frame_buffer_resize(); void request_frame_buffer_resize();
void set_frame_buffer_scale(float scale); void set_frame_buffer_scale(float scale);
void set_frame_buffer_aspect_fit(bool fit); void set_frame_buffer_aspect_fit(bool fit);
void set_force_aspect_16_9(bool force);
void set_present_surface_fill(bool fill); void set_present_surface_fill(bool fill);
void lock_present_aspect_ratio(int width, int height); void lock_present_aspect_ratio(int width, int height);
void unlock_present_aspect_ratio(); void unlock_present_aspect_ratio();
+4 -4
View File
@@ -4267,7 +4267,7 @@ TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
EXPECT_TRUE(records.front().persistentCopy); EXPECT_TRUE(records.front().persistentCopy);
} }
TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) { TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
std::array<u8, 152 * 114 * 4> image{}; std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24; gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4281,7 +4281,7 @@ TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) {
const auto& records = aurora::gfx::testing::resolve_pass_records(); const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u); ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy); EXPECT_TRUE(records[0].persistentCopy);
EXPECT_TRUE(records[1].persistentCopy); EXPECT_FALSE(records[1].persistentCopy);
} }
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) { TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
@@ -4301,7 +4301,7 @@ TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
EXPECT_TRUE(records[1].persistentCopy); EXPECT_TRUE(records[1].persistentCopy);
} }
TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) { TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
std::array<u8, 4 * 4 * 4> image{}; std::array<u8, 4 * 4 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24; gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4311,7 +4311,7 @@ TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) {
const auto& records = aurora::gfx::testing::resolve_pass_records(); const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u); ASSERT_EQ(records.size(), 1u);
EXPECT_TRUE(records.front().persistentCopy); EXPECT_FALSE(records.front().persistentCopy);
} }
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) { TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
+3 -1
View File
@@ -16,7 +16,9 @@
#endif #endif
extern "C" bool g_dynamicAspectRatioEnabled; extern "C" bool g_dynamicAspectRatioEnabled;
void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight); void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight);
void SetMkwForceAspect169(bool enabled);
bool MkwForceAspect169Requested();
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight); void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight);
// Arms the "keep EGG::Frustum's projection scale" flag on every screen that // Arms the "keep EGG::Frustum's projection scale" flag on every screen that
// renders to a fixed-size offscreen target. Cheap and idempotent; called from // renders to a fixed-size offscreen target. Cheap and idempotent; called from
+12
View File
@@ -33,6 +33,7 @@
struct RuntimeUserConfig { struct RuntimeUserConfig {
std::optional<bool> widescreen; std::optional<bool> widescreen;
std::optional<bool> forceAspect169;
std::optional<int32_t> windowPosX; std::optional<int32_t> windowPosX;
std::optional<int32_t> windowPosY; std::optional<int32_t> windowPosY;
std::optional<uint32_t> windowWidth; std::optional<uint32_t> windowWidth;
@@ -298,6 +299,7 @@ inline void EnsureConfigFile() {
"# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n" "# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n"
"[video]\n" "[video]\n"
"widescreen = true\n" "widescreen = true\n"
"force_16_9 = false\n"
"resolution_multiplier = 1.0\n" "resolution_multiplier = 1.0\n"
"frame_interpolation_fps = 0\n" "frame_interpolation_fps = 0\n"
"display_mode = \"windowed\"\n" "display_mode = \"windowed\"\n"
@@ -426,6 +428,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
} }
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen"); config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.forceAspect169 = FindConfigValue<bool>(document, "video", "force_16_9");
config.windowPosX = FindConfigInt(document, "video", "window_x"); config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y"); config.windowPosY = FindConfigInt(document, "video", "window_y");
if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) { if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) {
@@ -782,6 +785,15 @@ inline bool WidescreenEnabled(bool fallback = false) {
return Get().widescreen.value_or(fallback); return Get().widescreen.value_or(fallback);
} }
inline bool ForceAspect169Enabled(bool fallback = false) {
return Get().forceAspect169.value_or(fallback);
}
inline bool SetForceAspect169(bool value) {
Mutable().forceAspect169 = value;
return WriteSetting("video", "force_16_9", value ? "true" : "false");
}
inline bool WindowPosition(int32_t& x, int32_t& y) { inline bool WindowPosition(int32_t& x, int32_t& y) {
if (!Get().windowPosX || !Get().windowPosY) { if (!Get().windowPosX || !Get().windowPosY) {
return false; return false;
+2 -1
View File
@@ -5,6 +5,7 @@
#include <imgui.h> #include <imgui.h>
#include <SDL3/SDL_gamepad.h> #include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h> #include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_platform.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -146,7 +147,7 @@ std::string BuildMappingString() {
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ","; mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
} }
} }
mapping += "platform:Windows,"; mapping += std::string("platform:") + SDL_GetPlatform() + ",";
return mapping; return mapping;
} }
+46 -8
View File
@@ -2,6 +2,7 @@
#include "memory.h" #include "memory.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include "aurora_events.h" #include "aurora_events.h"
#include "hle/gx/gx_dynamic_aspect.h" #include "hle/gx/gx_dynamic_aspect.h"
@@ -14,6 +15,10 @@ extern "C" int g_gxFrameCount;
namespace { namespace {
bool g_widescreenConfigured = false; bool g_widescreenConfigured = false;
bool g_widescreenSetting = false;
bool g_forceAspect169 = false;
std::atomic_bool g_requestedForceAspect169{false};
bool g_policyDirty = false;
uint32_t g_lastEggWidth43 = 0; uint32_t g_lastEggWidth43 = 0;
uint32_t g_lastEggWidth169 = 0; uint32_t g_lastEggWidth169 = 0;
@@ -167,21 +172,54 @@ void AssertMkwOffscreenScreenBypass() {
} }
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight) { void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight) {
if (!g_widescreenConfigured || surfaceWidth == 0 || surfaceHeight == 0) { const bool requestedForceAspect169 = g_requestedForceAspect169.load(std::memory_order_acquire);
if (g_forceAspect169 != requestedForceAspect169) {
g_forceAspect169 = requestedForceAspect169;
g_widescreenConfigured = g_widescreenSetting || g_forceAspect169;
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
g_policyDirty = true;
}
if (surfaceWidth == 0 || surfaceHeight == 0) {
return; return;
} }
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); if (!g_widescreenConfigured) {
ApplyEggScreenRecords(surfaceWidth, surfaceHeight); if (g_policyDirty) {
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
VILockAspectRatio(4, 3);
ApplyEggScreenRecords(surfaceWidth, surfaceHeight);
g_policyDirty = false;
}
return;
}
if (g_policyDirty) {
VIUnlockAspectRatio();
g_policyDirty = false;
}
AuroraSetViewportPolicy(g_forceAspect169 ? AURORA_VIEWPORT_16_9 : AURORA_VIEWPORT_STRETCH);
ApplyEggScreenRecords(g_forceAspect169 ? 16u : surfaceWidth,
g_forceAspect169 ? 9u : surfaceHeight);
} }
void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight) { void SetMkwForceAspect169(bool enabled) {
g_widescreenConfigured = widescreen; g_requestedForceAspect169.store(enabled, std::memory_order_release);
g_dynamicAspectRatioEnabled = widescreen; }
bool MkwForceAspect169Requested() {
return g_requestedForceAspect169.load(std::memory_order_acquire);
}
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight) {
g_widescreenSetting = widescreen;
g_widescreenConfigured = widescreen || forceAspect169;
g_forceAspect169 = forceAspect169;
g_requestedForceAspect169.store(forceAspect169, std::memory_order_relaxed);
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
g_lastEggWidth43 = 0; g_lastEggWidth43 = 0;
g_lastEggWidth169 = 0; g_lastEggWidth169 = 0;
if (widescreen) { if (g_widescreenConfigured) {
VIUnlockAspectRatio(); VIUnlockAspectRatio();
ApplyEggScreenRecords(surfaceWidth, surfaceHeight); ApplyEggScreenRecords(forceAspect169 ? 16u : surfaceWidth,
forceAspect169 ? 9u : surfaceHeight);
return; return;
} }
+21 -2
View File
@@ -1,4 +1,5 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "input_bindings.h"
#include "memory.h" #include "memory.h"
#include "wii_remote_input.h" #include "wii_remote_input.h"
@@ -203,6 +204,24 @@ void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample)
} }
} }
// Neutral sticks and no buttons while an overlay owns input; the remote stays connected.
bool ReadSample(uint32_t chan, WiiRemoteInput::KpadSample& sample) {
if (!WiiRemoteInput::ReadKpadSample(chan, sample)) {
return false;
}
if (InputBindings::InputBlocked()) {
sample.hold = 0;
sample.clHold = 0;
sample.stick[0] = sample.stick[1] = 0.0f;
sample.clLStick[0] = sample.clLStick[1] = 0.0f;
sample.clRStick[0] = sample.clRStick[1] = 0.0f;
sample.clLStickRaw[0] = sample.clLStickRaw[1] = 0;
sample.clRStickRaw[0] = sample.clRStickRaw[1] = 0;
sample.clTriggerL = sample.clTriggerR = 0;
}
return true;
}
} // namespace } // namespace
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count. // KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
@@ -212,7 +231,7 @@ extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t co
return 0; return 0;
} }
WiiRemoteInput::KpadSample sample; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample); const bool have = ReadSample(chan, sample);
try { try {
return WriteStatus(chan, statusPtr, have ? &sample : nullptr); return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
} catch (const Memory::AccessViolation&) { } catch (const Memory::AccessViolation&) {
@@ -234,7 +253,7 @@ extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t status
return 0; return 0;
} }
WiiRemoteInput::KpadSample sample; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample); const bool have = ReadSample(chan, sample);
try { try {
const uint32_t entries = std::min(count, kMaxEntries); const uint32_t entries = std::min(count, kMaxEntries);
for (uint32_t i = 0; i < entries; ++i) { for (uint32_t i = 0; i < entries; ++i) {
+2 -1
View File
@@ -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), const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize),
static_cast<int>(flags), destPtr, destLen); static_cast<int>(flags), destPtr, destLen);
const int hostError = ret < 0 ? NativeLastError() : 0; 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)) { if (patchedWrite && ret == static_cast<int>(sendSize)) {
result = static_cast<int32_t>(in[0].size); result = static_cast<int32_t>(in[0].size);
} }
+2 -1
View File
@@ -9,6 +9,7 @@
#include <cstring> #include <cstring>
#include "memory.h" #include "memory.h"
#include "runtime_config.h" #include "runtime_config.h"
#include "aurora_events.h"
#include "runtime_log.h" #include "runtime_log.h"
namespace { namespace {
@@ -52,7 +53,7 @@ PPC_NATIVE_OVERRIDE(801B0220, SCCheckStatus_HLE, uint32_t, (), ());
// Returns: 0 = 4:3, 1 = 16:9 // Returns: 0 = 4:3, 1 = 16:9
extern "C" uint32_t SCGetAspectRatio_HLE() extern "C" uint32_t SCGetAspectRatio_HLE()
{ {
return RuntimeConfigFile::WidescreenEnabled(true) ? 1u : 0u; return (RuntimeConfigFile::WidescreenEnabled(true) || MkwForceAspect169Requested()) ? 1u : 0u;
} }
PPC_NATIVE_OVERRIDE(801B1BE4, SCGetAspectRatio_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B1BE4, SCGetAspectRatio_HLE, uint32_t, (), ());
+8 -5
View File
@@ -1354,7 +1354,8 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.logCallback = &RuntimeAuroraLogCallback; auroraConfig.logCallback = &RuntimeAuroraLogCallback;
auroraConfig.logLevel = LOG_DEBUG; auroraConfig.logLevel = LOG_DEBUG;
const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true); const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true);
auroraConfig.windowWidth = configWidescreen ? 854 : 640; const bool forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
auroraConfig.windowWidth = (configWidescreen || forceAspect169) ? 854 : 640;
auroraConfig.windowHeight = 480; auroraConfig.windowHeight = 480;
auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth); auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth);
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight); auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
@@ -1372,7 +1373,8 @@ int RuntimeMain(int argc, char** argv) {
// No vsync knob: aurora always configures a non-blocking present mode. // No vsync knob: aurora always configures a non-blocking present mode.
auroraConfig.desiredBackend = BACKEND_AUTO; auroraConfig.desiredBackend = BACKEND_AUTO;
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f); const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
ConfigureMkwDynamicAspect(configWidescreen, auroraConfig.windowWidth, auroraConfig.windowHeight); ConfigureMkwDynamicAspect(configWidescreen, forceAspect169,
auroraConfig.windowWidth, auroraConfig.windowHeight);
VISetFrameBufferScale(resolutionMultiplier); VISetFrameBufferScale(resolutionMultiplier);
// One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi // One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi
// whitelists exactly these config names, so an unrecognised value has // whitelists exactly these config names, so an unrecognised value has
@@ -1439,14 +1441,15 @@ int RuntimeMain(int argc, char** argv) {
auroraInfo.windowSize.native_fb_height); auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings(); settings_overlay::InitializeRuntimeSettings();
RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false") RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen ? 1 : 0) << " force_16_9=" << (forceAspect169 ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen || forceAspect169 ? 1 : 0)
<< " resolutionMultiplier=" << resolutionMultiplier << " resolutionMultiplier=" << resolutionMultiplier
<< " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height << " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height
<< " native=" << auroraInfo.windowSize.native_fb_width << "x" << " native=" << auroraInfo.windowSize.native_fb_width << "x"
<< auroraInfo.windowSize.native_fb_height << auroraInfo.windowSize.native_fb_height
<< " viewportPolicy=" << (g_dynamicAspectRatioEnabled ? "stretch" : "fit") << " viewportPolicy=" << (forceAspect169 ? "16:9" : (configWidescreen ? "stretch" : "fit"))
<< " presentAspect=" << " presentAspect="
<< (g_dynamicAspectRatioEnabled ? "surface (dynamic EGG canvas)" : "4:3") << (forceAspect169 ? "16:9" : (configWidescreen ? "surface (dynamic EGG canvas)" : "4:3"))
<< std::endl; << std::endl;
g_auroraInitialized.store(true, std::memory_order_release); g_auroraInitialized.store(true, std::memory_order_release);
+43 -1
View File
@@ -26,6 +26,7 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdio>
#include <limits> #include <limits>
#include <iostream> #include <iostream>
#include <string> #include <string>
@@ -109,6 +110,7 @@ int g_displayMode = [] {
bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true); bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true); bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true); bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0); uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] { std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{}; std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -161,6 +163,11 @@ uint64_t g_presentedFrame = 0;
std::atomic_bool g_strapInputAccepted = false; std::atomic_bool g_strapInputAccepted = false;
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX; std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
constexpr uint64_t kStrapTransitionCoverFrames = 60; constexpr uint64_t kStrapTransitionCoverFrames = 60;
std::atomic_bool g_bootShadersReady = false;
bool g_bootShaderNotice = false;
Clock::time_point g_bootShaderWaitStart{};
constexpr uint32_t kBootShaderNoticeThreshold = 100;
constexpr auto kBootShaderWaitLimit = std::chrono::minutes(3);
constexpr std::array<ResolutionItem, 8> kResolutions = {{ constexpr std::array<ResolutionItem, 8> kResolutions = {{
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f}, {"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
@@ -1006,6 +1013,12 @@ void DrawAudioSettings() {
void DrawGraphicsSettings() { void DrawGraphicsSettings() {
g_displayMode = static_cast<int>(aurora_get_display_mode()); g_displayMode = static_cast<int>(aurora_get_display_mode());
if (ImGui::Checkbox("Force 16:9", &g_forceAspect169)) {
SetMkwForceAspect169(g_forceAspect169);
RuntimeConfigFile::SetForceAspect169(g_forceAspect169);
}
ImGui::TextDisabled("Keep a 16:9 image with black bars when the window has another shape.");
ImGui::Separator();
struct EffectFlag { struct EffectFlag {
const char* label; const char* label;
uint32_t flag; uint32_t flag;
@@ -1186,12 +1199,36 @@ void DrawStartupScreen() {
const float startY = std::max(0.0f, (viewport->Size.y - titleSize.y) * 0.5f); const float startY = std::max(0.0f, (viewport->Size.y - titleSize.y) * 0.5f);
ImGui::SetCursorPos(ImVec2(titleX, startY)); ImGui::SetCursorPos(ImVec2(titleX, startY));
ImGui::TextUnformatted(kTitle); ImGui::TextUnformatted(kTitle);
if (g_bootShaderNotice && !g_bootShadersReady.load(std::memory_order_relaxed)) {
ImGui::SetWindowFontScale(0.9f);
char line[96];
std::snprintf(line, sizeof(line), "Compiling shaders, please hold on: %u remaining",
aurora_get_queued_pipeline_count());
const float lineX = std::max(0.0f, (viewport->Size.x - ImGui::CalcTextSize(line).x) * 0.5f);
ImGui::SetCursorPos(ImVec2(lineX, startY + titleSize.y * 1.8f));
ImGui::TextUnformatted(line);
}
} }
ImGui::End(); ImGui::End();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
void UpdateBootShaderState() {
if (g_bootShadersReady.load(std::memory_order_relaxed)) {
return;
}
const auto now = Clock::now();
if (g_bootShaderWaitStart == Clock::time_point{}) {
g_bootShaderWaitStart = now;
}
const uint32_t queued = aurora_get_queued_pipeline_count();
g_bootShaderNotice |= queued > kBootShaderNoticeThreshold;
if (queued == 0 || now - g_bootShaderWaitStart > kBootShaderWaitLimit) {
g_bootShadersReady.store(true, std::memory_order_release);
}
}
void DrawExitPrompt() { void DrawExitPrompt() {
constexpr const char* kTitle = "Exit"; constexpr const char* kTitle = "Exit";
if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle); if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
@@ -1341,7 +1378,7 @@ void PersistDisplayModeIfChanged() {
void ApplyInputBlockState() { void ApplyInputBlockState() {
const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active || const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active ||
g_exitPromptOpen || g_topBarVisible; g_exitPromptOpen || g_topBarVisible || StartupScreenVisible();
PADBlockInput(blocked); PADBlockInput(blocked);
InputBindings::SetInputBlocked(blocked); InputBindings::SetInputBlocked(blocked);
} }
@@ -1369,6 +1406,9 @@ void InitializeRuntimeSettings() noexcept {
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines); aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
g_strapInputAccepted.store(false, std::memory_order_relaxed); g_strapInputAccepted.store(false, std::memory_order_relaxed);
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed); g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
g_bootShadersReady.store(false, std::memory_order_relaxed);
g_bootShaderNotice = false;
g_bootShaderWaitStart = {};
PADBlockInput(false); PADBlockInput(false);
InputBindings::SetInputBlocked(false); InputBindings::SetInputBlocked(false);
} }
@@ -1449,6 +1489,7 @@ void Draw() noexcept {
ApplyConfiguredMappings(); ApplyConfiguredMappings();
PersistDisplayModeIfChanged(); PersistDisplayModeIfChanged();
UpdateCursorAutoHide(); UpdateCursorAutoHide();
UpdateBootShaderState();
if (!StartupScreenVisible()) { if (!StartupScreenVisible()) {
DrawShaderCompilationStatus(); DrawShaderCompilationStatus();
} }
@@ -1462,6 +1503,7 @@ void Draw() noexcept {
bool StartupScreenVisible() noexcept { bool StartupScreenVisible() noexcept {
return !g_strapInputAccepted.load(std::memory_order_acquire) || return !g_strapInputAccepted.load(std::memory_order_acquire) ||
!g_bootShadersReady.load(std::memory_order_acquire) ||
g_presentedFrame < g_startupDismissFrame.load(std::memory_order_relaxed); g_presentedFrame < g_startupDismissFrame.load(std::memory_order_relaxed);
} }
@@ -1942,6 +1942,22 @@ public sealed partial class PpcLifter
new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs) 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": case "bcl":
{ {
var rawInstr = ReadRawInstruction(ins); var rawInstr = ReadRawInstruction(ins);
@@ -14,6 +14,23 @@ public class PpcLifterAdditionalCoverageTests
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands) private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, 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] [Fact]
public void LiftsAdditionalNonDotArithmeticAndLogicalForms() public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
{ {