mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Scan GX_DEFERRED overrides, and guard the Retro-WFC payload
The native-registration scan only knew PPC_NATIVE_OVERRIDE, so every GX_DEFERRED_OVERRIDE_VOID was missing from the index; the deferred form registers `symbol` and posts the hand-written `symbol_gx`. A Retro Rewind kit built from a translation without the Retro-WFC payload now refuses to package instead of crashing on entering WFC, and on Android ImGui keeps off SDL's cursor, which ART aborts on from a guest fiber's stack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c84de48f7d
commit
1d8fcac35e
7 files changed
+175
-13
No files matched your search
@@ -497,6 +497,19 @@ function Invoke-QuestGameBuild {
|
|||||||
$sources[$slot] = @($slotSources | ForEach-Object { if ($_.EndsWith('.S')) { & $toElf $_ } else { $_ } })
|
$sources[$slot] = @($slotSources | ForEach-Object { if ($_.EndsWith('.S')) { & $toElf $_ } else { $_ } })
|
||||||
}
|
}
|
||||||
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
|
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
|
||||||
|
# Online play needs the Retro-WFC payload translated into the mod (translate-mod
|
||||||
|
# --retro-wfc-payload). Without it the mod downloads the payload at run time and jumps into
|
||||||
|
# code that was never translated: the game crashes on entering Retro Rewind WFC.
|
||||||
|
if ($Product -eq 'retro_rewind') {
|
||||||
|
$dataPatches = @($sources.Values | ForEach-Object { $_ } |
|
||||||
|
Where-Object { [IO.Path]::GetFileName($_) -eq 'mod_data_patches.cpp' })
|
||||||
|
if ($dataPatches.Count -ne 1 -or
|
||||||
|
-not (Select-String -LiteralPath $dataPatches[0] -Pattern 'kRetroWfcInitializerAddress' -SimpleMatch -Quiet)) {
|
||||||
|
throw ('This Retro Rewind translation has no Retro-WFC payload, so online play would crash. ' +
|
||||||
|
'Translate the mod again with its payload (translate-mod --retro-wfc-payload, or repair ' +
|
||||||
|
'Retro Rewind in WiiCompiled with the payload download on), then build again.')
|
||||||
|
}
|
||||||
|
}
|
||||||
foreach ($slot in $sources.Keys) {
|
foreach ($slot in $sources.Keys) {
|
||||||
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $Product game are missing from $shards" }
|
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $Product game are missing from $shards" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import org.wiicompiled.quest.GameStorage
|
|||||||
* game kit (assets/game_kit) into private storage;
|
* game kit (assets/game_kit) into private storage;
|
||||||
* 2. download the Android NDK files the build needs from Google, checked against the pins the
|
* 2. download the Android NDK files the build needs from Google, checked against the pins the
|
||||||
* toolchain carries (ndk.json);
|
* toolchain carries (ndk.json);
|
||||||
* 3. translate main.dol and StaticR.rel: translate-recursive, generate-data-init, emit-build-shards;
|
* 3. translate main.dol and StaticR.rel: translate-recursive, generate-data-init, emit-build-shards
|
||||||
|
* (Retro Rewind adds translate-mod, with the Retro-WFC payload downloaded from rwfc.net);
|
||||||
* 4. compile the generated sources with the kit's flags, several at a time;
|
* 4. compile the generated sources with the kit's flags, several at a time;
|
||||||
* 5. link them with the kit's objects and archives (kit.json link.lld);
|
* 5. link them with the kit's objects and archives (kit.json link.lld);
|
||||||
* 6. install libmain.so and its game.json like an imported game.
|
* 6. install libmain.so and its game.json like an imported game.
|
||||||
@@ -65,6 +66,12 @@ object GameBuild {
|
|||||||
private const val TRANSLATOR_THREADS = 4
|
private const val TRANSLATOR_THREADS = 4
|
||||||
private const val EXPECTED_TRANSLATION_SECONDS = 600.0
|
private const val EXPECTED_TRANSLATION_SECONDS = 600.0
|
||||||
private const val COMPILE_MEMORY_BYTES = 700L * 1024 * 1024
|
private const val COMPILE_MEMORY_BYTES = 700L * 1024 * 1024
|
||||||
|
// WiiCompiled Setup's fixed endpoint, size cap and staging layout
|
||||||
|
// (Launcher/WiiCompiled.Setup.Common/RetroWfcPayload.cs), which validate-retro-wfc-payload expects.
|
||||||
|
private const val RETRO_WFC_PAYLOAD_URL = "https://rwfc.net/api/wfc/payload?g=RMCPD00"
|
||||||
|
private const val RETRO_WFC_PAYLOAD_MAX_BYTES = 16 * 1024 * 1024
|
||||||
|
private const val RETRO_WFC_DIRECTORY = "retro-wfc"
|
||||||
|
private const val RETRO_WFC_PAYLOAD_FILE = "binary/payload.RMCPD00.bin"
|
||||||
|
|
||||||
/** Builds [profile]'s game. Null on success, otherwise the message to show. */
|
/** Builds [profile]'s game. Null on success, otherwise the message to show. */
|
||||||
fun run(context: Context, profile: GameProfile, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
|
fun run(context: Context, profile: GameProfile, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
|
||||||
@@ -276,13 +283,82 @@ object GameBuild {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Retro-WFC payload Retro Rewind's online play runs. translate-mod lowers it into the
|
||||||
|
* mod; without it the mod downloads the payload while connecting and jumps into code that
|
||||||
|
* was never translated. Retried once, like Setup's download.
|
||||||
|
*/
|
||||||
|
fun downloadRetroWfcPayload(): File {
|
||||||
|
val file = File(workspace, "$RETRO_WFC_DIRECTORY/$RETRO_WFC_PAYLOAD_FILE")
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
log.line("Downloading the Retro-WFC payload from $RETRO_WFC_PAYLOAD_URL")
|
||||||
|
var failure: IOException? = null
|
||||||
|
for (attempt in 1..2) {
|
||||||
|
if (cancelled()) throw InterruptedIOException("Build cancelled")
|
||||||
|
try {
|
||||||
|
file.writeBytes(fetchRetroWfcPayload())
|
||||||
|
log.line("Retro-WFC payload: ${file.length()} bytes, sha256 ${BuildRecipe.hex(sha256(file))}")
|
||||||
|
return file
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// A socket timeout is an InterruptedIOException too, which run() takes for a cancel.
|
||||||
|
failure = e
|
||||||
|
log.line("Retro-WFC payload download attempt $attempt failed: $e")
|
||||||
|
if (attempt == 1) Thread.sleep(1_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cancelled()) throw InterruptedIOException("Build cancelled")
|
||||||
|
throw IOException(
|
||||||
|
"Retro Rewind's online play needs the Retro-WFC payload from rwfc.net, which could not be " +
|
||||||
|
"downloaded (${failure?.message}). Check the headset's internet connection, then build again.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchRetroWfcPayload(): ByteArray {
|
||||||
|
val connection = URL(RETRO_WFC_PAYLOAD_URL).openConnection() as HttpURLConnection
|
||||||
|
try {
|
||||||
|
connection.connectTimeout = 30_000
|
||||||
|
connection.readTimeout = 30_000
|
||||||
|
// A redirect would fetch from a target other than the fixed endpoint; Setup refuses it too.
|
||||||
|
connection.instanceFollowRedirects = false
|
||||||
|
connection.setRequestProperty("Accept-Encoding", "identity")
|
||||||
|
val code = connection.responseCode
|
||||||
|
if (code != HttpURLConnection.HTTP_OK) throw IOException("rwfc.net answered $code")
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
val bytes = java.io.ByteArrayOutputStream()
|
||||||
|
val buffer = ByteArray(64 * 1024)
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0) break
|
||||||
|
bytes.write(buffer, 0, read)
|
||||||
|
if (bytes.size() > RETRO_WFC_PAYLOAD_MAX_BYTES) throw IOException("the payload is unexpectedly large")
|
||||||
|
}
|
||||||
|
return bytes.toByteArray()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Translates the disc unless this workspace already holds a translation of the same inputs. */
|
/** Translates the disc unless this workspace already holds a translation of the same inputs. */
|
||||||
fun translate(tools: ToolProcess, identity: String): String? {
|
fun translate(tools: ToolProcess, identity: String): String? {
|
||||||
val generated = File(workspace, "generated")
|
val generated = File(workspace, "generated")
|
||||||
val provenance = File(generated, "translation-provenance.txt")
|
val provenance = File(generated, "translation-provenance.txt")
|
||||||
|
// Fetched before anything else, so an unreachable server fails the build before the
|
||||||
|
// long base translation rather than after it.
|
||||||
|
val payload = if (profile.modPack) downloadRetroWfcPayload() else null
|
||||||
|
if (payload != null) {
|
||||||
|
translator(
|
||||||
|
tools, "checking the Retro-WFC payload",
|
||||||
|
"validate-retro-wfc-payload", "--directory", File(workspace, RETRO_WFC_DIRECTORY).absolutePath,
|
||||||
|
)?.let { return it }
|
||||||
|
}
|
||||||
// A modded game needs a base translation that knows this Code.pul, so the pack's own
|
// A modded game needs a base translation that knows this Code.pul, so the pack's own
|
||||||
// identity is part of what the stored translation is reused for.
|
// identity (and the payload's) is part of what the stored translation is reused for.
|
||||||
val modIdentity = if (profile.modPack) BuildRecipe.hex(sha256(GameStorage.modCodePul(context))) else ""
|
val modIdentity = if (payload != null) {
|
||||||
|
"${BuildRecipe.hex(sha256(GameStorage.modCodePul(context)))} ${BuildRecipe.hex(sha256(payload))}"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
val expected = "$identity ${profile.id} ${BuildConfig.DISC_DOL_SHA256} ${BuildConfig.DISC_REL_SHA256} $modIdentity"
|
val expected = "$identity ${profile.id} ${BuildConfig.DISC_DOL_SHA256} ${BuildConfig.DISC_REL_SHA256} $modIdentity"
|
||||||
val shards = File(generated, "build_shards/shards.cmake")
|
val shards = File(generated, "build_shards/shards.cmake")
|
||||||
if (provenance.isFile && provenance.readText() == expected && shards.isFile) {
|
if (provenance.isFile && provenance.readText() == expected && shards.isFile) {
|
||||||
@@ -324,7 +400,7 @@ object GameBuild {
|
|||||||
}?.let { return it }
|
}?.let { return it }
|
||||||
|
|
||||||
// Retro Rewind's own code: its Code.pul translated against the base translation, as
|
// Retro Rewind's own code: its Code.pul translated against the base translation, as
|
||||||
// Launcher/LocalBuild.ps1 does on a PC. Online play needs a payload this cannot fetch.
|
// Launcher/LocalBuild.ps1 does on a PC, with the Retro-WFC payload for online play.
|
||||||
val modOutput = "build/mods/retro_rewind_full_cpp"
|
val modOutput = "build/mods/retro_rewind_full_cpp"
|
||||||
if (profile.modPack) {
|
if (profile.modPack) {
|
||||||
report(350, Step.Translate, 2, steps)
|
report(350, Step.Translate, 2, steps)
|
||||||
@@ -342,7 +418,7 @@ object GameBuild {
|
|||||||
"--code-pul", GameStorage.modCodePul(context).absolutePath,
|
"--code-pul", GameStorage.modCodePul(context).absolutePath,
|
||||||
"--mod-root", GameStorage.modDirectory(context).absolutePath,
|
"--mod-root", GameStorage.modDirectory(context).absolutePath,
|
||||||
"--mod-name", "Retro Rewind", "--region", "P", "--out", modOutput,
|
"--mod-name", "Retro Rewind", "--region", "P", "--out", modOutput,
|
||||||
"--prefer-cached-inputs", "--emit-cpp", "--skip-retro-wfc",
|
"--prefer-cached-inputs", "--emit-cpp", "--retro-wfc-payload", payload!!.absolutePath,
|
||||||
"--threads", TRANSLATOR_THREADS.toString(),
|
"--threads", TRANSLATOR_THREADS.toString(),
|
||||||
)?.let { return it }
|
)?.let { return it }
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -297,6 +297,17 @@ Home's main button becomes **Download Retro Rewind** whenever that game is selec
|
|||||||
missing, and Settings → About shows the installed version with an Update button. Building the mod on
|
missing, and Settings → About shows the installed version with an Update button. Building the mod on
|
||||||
the headset needs the mod's `Code.pul`, which is part of the pack, so the same rule covers it.
|
the headset needs the mod's `Code.pul`, which is part of the pack, so the same rule covers it.
|
||||||
|
|
||||||
|
Online play (Retro Rewind WFC) needs the Retro-WFC payload translated into the mod, as on a PC:
|
||||||
|
`translate-mod --retro-wfc-payload`, with the payload Setup downloads and verifies from
|
||||||
|
`https://rwfc.net/api/wfc/payload?g=RMCPD00`. Without it the mod downloads `WWFC/Payload` while
|
||||||
|
connecting and jumps into code that was never translated, and the game stops with a missing
|
||||||
|
translated function (seen: `0x81895BF4`, called from `rr_kamek_*` on the `NHTTPi_CommThreadProc`
|
||||||
|
thread, with `r3` pointing at `"WWFC/Payload"`). So the headset
|
||||||
|
build downloads the payload before translating and checks it with `validate-retro-wfc-payload`, and
|
||||||
|
`Invoke-QuestGameBuild` refuses a Retro Rewind translation whose `mod_data_patches.cpp` has no
|
||||||
|
`kRetroWfcInitializerAddress`. The payload is fixed at build time: when rwfc.net publishes a new
|
||||||
|
one, rebuild the game.
|
||||||
|
|
||||||
### Game packages (.wcgame) and Import from computer
|
### Game packages (.wcgame) and Import from computer
|
||||||
|
|
||||||
A `.wcgame` is a zip holding `game.json`, `libmain.so` and optionally `DATA/…` (the extracted
|
A `.wcgame` is a zip holding `game.json`, `libmain.so` and optionally `DATA/…` (the extracted
|
||||||
@@ -465,7 +476,7 @@ installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline)
|
|||||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
|
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output)
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
|
||||||
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1875,6 +1875,12 @@ bool IsMouseActivity(const SDL_Event& event) {
|
|||||||
// Runs on the thread that pumps SDL events (the same one that calls Draw), so
|
// Runs on the thread that pumps SDL events (the same one that calls Draw), so
|
||||||
// the SDL cursor calls are safe here.
|
// the SDL cursor calls are safe here.
|
||||||
void UpdateCursorAutoHide() {
|
void UpdateCursorAutoHide() {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
// No pointer icon to manage on a headset, and SDL changes it through Java,
|
||||||
|
// which ART aborts on from a guest fiber's stack: Draw runs on whichever
|
||||||
|
// guest thread advances the retrace. InitializeRuntimeSettings keeps
|
||||||
|
// ImGui's SDL backend off the cursor for the same reason.
|
||||||
|
#else
|
||||||
const bool shouldHide =
|
const bool shouldHide =
|
||||||
!g_topBarVisible && Clock::now() - g_lastMouseActivity >= kCursorAutoHideDelay;
|
!g_topBarVisible && Clock::now() - g_lastMouseActivity >= kCursorAutoHideDelay;
|
||||||
if (shouldHide == g_cursorHidden) {
|
if (shouldHide == g_cursorHidden) {
|
||||||
@@ -1889,6 +1895,7 @@ void UpdateCursorAutoHide() {
|
|||||||
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange;
|
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange;
|
||||||
SDL_ShowCursor();
|
SDL_ShowCursor();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alt+Enter toggles the display mode inside aurora without going through the
|
// Alt+Enter toggles the display mode inside aurora without going through the
|
||||||
@@ -2045,6 +2052,11 @@ void DrawVrSettingsPanel() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void InitializeRuntimeSettings() noexcept {
|
void InitializeRuntimeSettings() noexcept {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
// ImGui_ImplSDL3_NewFrame would otherwise call SDL_SetCursor/SDL_HideCursor
|
||||||
|
// (Java on Android) from the guest fiber that starts the next host frame.
|
||||||
|
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
|
||||||
|
#endif
|
||||||
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||||
InputBindings::Reload();
|
InputBindings::Reload();
|
||||||
controller_mapping_wizard::LoadPersistedMappings();
|
controller_mapping_wizard::LoadPersistedMappings();
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ public static class RuntimeNativeGuestEffectAnalyzer
|
|||||||
var address = ParseAddress(match.Groups["address"].Value);
|
var address = ParseAddress(match.Groups["address"].Value);
|
||||||
stubAddresses.Add(address);
|
stubAddresses.Add(address);
|
||||||
var symbol = match.Groups["symbol"].Value;
|
var symbol = match.Groups["symbol"].Value;
|
||||||
|
// GX_DEFERRED_OVERRIDE_VOID generates `symbol` itself, which only posts the
|
||||||
|
// hand-written `symbol_gx` to the GX thread with the same arguments.
|
||||||
|
if (match.Groups["deferred"].Success)
|
||||||
|
symbol += "_gx";
|
||||||
var tail = match.Groups["tail"].Value;
|
var tail = match.Groups["tail"].Value;
|
||||||
var isVoid = match.Groups["void"].Success;
|
var isVoid = match.Groups["void"].Success;
|
||||||
if (TryParseStubTail(tail, isVoid, out var returnType, out var arguments) &&
|
if (TryParseStubTail(tail, isVoid, out var returnType, out var arguments) &&
|
||||||
|
|||||||
@@ -94,24 +94,27 @@ public static partial class GeneratedMarkers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The registration half of <c>PPC_NATIVE_OVERRIDE</c>: address and symbol
|
/// The registration half of <c>PPC_NATIVE_OVERRIDE</c>: address and symbol
|
||||||
/// only. Deliberately a prefix match, so an override whose argument list
|
/// only. Deliberately a prefix match, so an override whose argument list
|
||||||
/// contains a semicolon still registers.
|
/// contains a semicolon still registers. <c>GX_DEFERRED_OVERRIDE_VOID</c>
|
||||||
|
/// (runtime/src/hle/gx/gx_internal.h) takes the same arguments and expands
|
||||||
|
/// to <c>PPC_NATIVE_OVERRIDE_VOID</c>, which a source scan cannot see.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE(?:_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
[GeneratedRegex(@"(?:PPC_NATIVE|GX_DEFERRED)_OVERRIDE(?:_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||||
public static partial Regex NativeOverridePattern();
|
public static partial Regex NativeOverridePattern();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The same macro read for its signature rather than its registration: the
|
/// The same macros read for their signature rather than their registration:
|
||||||
/// effect analyzer needs the argument list, which only the full
|
/// the effect analyzer needs the argument list, which only the full
|
||||||
/// statement-terminated form can capture.
|
/// statement-terminated form can capture. <c>deferred</c> marks the GX form,
|
||||||
|
/// whose hand-written body is <c>symbol_gx</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE(?<void>_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*,?\s*(?<tail>[^;]*?)\);", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
[GeneratedRegex(@"(?:PPC_NATIVE|(?<deferred>GX_DEFERRED))_OVERRIDE(?<void>_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*,?\s*(?<tail>[^;]*?)\);", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
||||||
public static partial Regex NativeOverrideSignaturePattern();
|
public static partial Regex NativeOverrideSignaturePattern();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The void-stub form read for its declared host parameter list, which is
|
/// The void-stub form read for its declared host parameter list, which is
|
||||||
/// what the void-stub ABI provider infers argument registers from.
|
/// what the void-stub ABI provider infers argument registers from.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE_VOID\s*\(\s*(?<addr>[0-9A-Fa-f]+)\s*,\s*[^,]+,\s*\((?<args>[^)]*)\)\s*,", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
[GeneratedRegex(@"(?:PPC_NATIVE|GX_DEFERRED)_OVERRIDE_VOID\s*\(\s*(?<addr>[0-9A-Fa-f]+)\s*,\s*[^,]+,\s*\((?<args>[^)]*)\)\s*,", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
||||||
public static partial Regex NativeOverrideVoidArgumentsPattern();
|
public static partial Regex NativeOverrideVoidArgumentsPattern();
|
||||||
|
|
||||||
[GeneratedRegex(@"GX_FATAL_STUB\s*\(\s*(?<address>[0-9A-Fa-f]{8})", RegexOptions.CultureInvariant)]
|
[GeneratedRegex(@"GX_FATAL_STUB\s*\(\s*(?<address>[0-9A-Fa-f]{8})", RegexOptions.CultureInvariant)]
|
||||||
|
|||||||
@@ -80,4 +80,47 @@ public sealed class RuntimeNativeIndexTests
|
|||||||
Directory.Delete(directory, recursive: true);
|
Directory.Delete(directory, recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildReadsGxDeferredOverridesLikeTheStubsTheyExpandTo()
|
||||||
|
{
|
||||||
|
// runtime/src/hle/gx: the macro defines Deferred itself and registers it through
|
||||||
|
// PPC_NATIVE_OVERRIDE_VOID; missing it put a translated copy of the function beside
|
||||||
|
// the native one (duplicate symbols at link time).
|
||||||
|
var directory = Path.Combine(Path.GetTempPath(), $"mkw-native-deferred-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(directory, "gx_internal.h"), """
|
||||||
|
#define GX_DEFERRED_OVERRIDE_VOID(addr_hex, name, arg_list, call_list) \
|
||||||
|
extern "C" void name arg_list { GxThread::Post(&name##_gx GX_COMMA_ARGS call_list); } \
|
||||||
|
PPC_NATIVE_OVERRIDE_VOID(addr_hex, name, arg_list, call_list)
|
||||||
|
""");
|
||||||
|
File.WriteAllText(Path.Combine(directory, "gx_pixel.cpp"), """
|
||||||
|
static void Deferred_gx(uint32_t mode, float value) { GXSetDither(mode); (void)value; }
|
||||||
|
GX_DEFERRED_OVERRIDE_VOID(80172930, Deferred, (uint32_t mode, float value), (mode, value));
|
||||||
|
""");
|
||||||
|
|
||||||
|
var index = RuntimeNativeIndexBuilder.Build(directory);
|
||||||
|
var registration = Assert.Single(index.Registrations);
|
||||||
|
Assert.Equal(0x80172930u, registration.Address);
|
||||||
|
Assert.Equal("Deferred", registration.Symbol);
|
||||||
|
Assert.True(registration.ExcludesBaseTranslation);
|
||||||
|
|
||||||
|
var abi = Assert.Single(index.VoidStubAbis);
|
||||||
|
Assert.Equal(["f1", "r3"], abi.ArgumentRegisters);
|
||||||
|
Assert.Equal(["f1"], abi.ScalarFloatArgumentRegisters);
|
||||||
|
|
||||||
|
// Analyzed from the hand-written Deferred_gx body, as the stub was before it moved.
|
||||||
|
var effect = Assert.Single(index.Effects);
|
||||||
|
Assert.True(effect.IsPrecise);
|
||||||
|
Assert.Equal(1u << 3, effect.Contract.GprReadBeforeWriteMask);
|
||||||
|
Assert.Equal(1u << 1, effect.Contract.FprReadBeforeWriteMask);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(directory))
|
||||||
|
Directory.Delete(directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user