Merge pull request #7 from mitch030504/claude/project-thread-ix2nko

Port upstream game fixes: Linux TLS, saves across drives, controllers, alarms
This commit is contained in:
mitch030504 authored and GitHub committed 2026-10-05 15:15:42 +02:00
commit a132a82952
22 files changed
+3621 -41

No files matched your search

+3 -1
View File
@@ -503,7 +503,9 @@ publish_built_product() {
local exe=$build/$target
assert_file "$exe" "Locally compiled game executable"
cp -f "$exe" "$destination/$target"
for name in dsp_coef.bin initial_pipeline_cache.db; do
# cacert.pem is the TLS root bundle the mbed TLS path looks up beside the executable
# (runtime/src/hle/net/network_ssl.cpp); without it HTTPS fails at runtime.
for name in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
done
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
+2 -2
View File
@@ -27,7 +27,7 @@ done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
macos="$app/Contents/MacOS"
@@ -52,7 +52,7 @@ cat > "$app/Contents/Info.plist" <<EOF
</dict></plist>
EOF
ditto "$build_dir/$product" "$macos/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ditto "$build_dir/$asset" "$resources/$asset"
ln -s "../Resources/$asset" "$macos/$asset"
done
+5 -3
View File
@@ -131,14 +131,16 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
## Fetched at build time and redistributed in release builds
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
downloads them, and release installers carry the resulting binaries. Their license texts are
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt`,
`aurora-main/cmake/AuroraDawnProvider.cmake`, and (for Mbed TLS) `runtime/CMakeLists.txt`. They are
not stored in this repository; the build downloads them - each fetch is pinned to an exact version
with a checked SHA-256 - and links or redistributes the resulting binaries. Their license texts are
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
| Component | Version | License | Upstream |
| --- | --- | --- | --- |
| Mbed TLS | 3.6.7 | Apache-2.0 / GPL-2.0-or-later | <https://github.com/Mbed-TLS/mbedtls> |
| Dawn (WebGPU) | `v20260603.191052` prebuilt | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
+1
View File
@@ -28,6 +28,7 @@ val prepareRuntimeResources by tasks.registering(Copy::class) {
from(File(assets, "wii")) { into("runtime_resources/wii_bootstrap") }
from(File(assets, "dsp/dsp_coef.bin")) { into("runtime_resources") }
from(File(assets, "pipeline/initial_pipeline_cache.db")) { into("runtime_resources") }
from(File(assets, "certs/cacert.pem")) { into("runtime_resources") }
into(runtimeResources)
}
+5 -4
View File
@@ -399,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
}
void PADSetPortForIndex(const u32 idx, const u32 port) {
if (port >= PAD_MAX_CONTROLLERS) return;
const auto* ctrl = __PADGetControllerForIndex(idx);
if (ctrl == nullptr) {
return;
}
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
aurora::input::set_player_index(dest->m_index, -1);
}
if (oldPort >= 0 && oldPort != port) {
aurora::input::persist_controller_for_player(oldPort, nullptr);
}
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
aurora::input::persist_controller_for_player(port, ctrl);
}
@@ -437,7 +438,7 @@ void PADClearPort(const u32 port) {
if (ctrl == nullptr) {
return;
}
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
aurora::input::set_player_index(ctrl->m_index, -1);
}
// Secondary bindings live only in memory; the runtime re-applies them from its
+23
View File
@@ -414,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
apply_port_preferences();
#if defined(SDL_PLATFORM_MACOS)
// First-use convenience only: never override a saved assignment or None.
if (g_portPreferences[0].state == PortPreferenceState::Unset) {
bool hasOtherPortPreference = false;
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
IdentityMatch::None) {
hasOtherPortPreference = true;
break;
}
}
if (!hasOtherPortPreference) {
const auto* p0 = get_controller_for_player(0);
if (p0 == nullptr) {
assign_player_index(g_GameControllers[instance], 0);
persist_controller_for_player(0, &g_GameControllers[instance]);
} else if (p0 == &g_GameControllers[instance]) {
persist_controller_for_player(0, &g_GameControllers[instance]);
}
}
}
#endif
return instance;
}
+37
View File
@@ -144,6 +144,43 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF)
endif()
# TLS for non-Windows guest network HLE (runtime/src/hle/net/network_ssl.cpp) - the Windows path
# uses Schannel (a Windows-only OS API), which has no equivalent on Linux/Android, so this project
# needs its own TLS library there. mbed TLS was chosen over OpenSSL specifically because it cross-
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
# upstream's own signed `mbedtls-<version>-sha256sum.txt` release asset.
#
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
# TLS path (`#ifndef _WIN32`). Windows keeps Schannel and must not fetch anything: its builds run
# with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline dependency set prepared by
# Launcher/Prepare-Dependencies.ps1, so an unconditional fetch would fail a clean configure there
# and would also add a dependency Windows never links.
add_library(mkw_mbedtls INTERFACE)
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
if(NOT MKW_PLATFORM_WINDOWS)
include(FetchContent)
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
FetchContent_Declare(mkw_mbedtls_upstream
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
URL_HASH SHA256=${MKW_MBEDTLS_SHA256})
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
endif()
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
File diff suppressed because it is too large. Load diff
+18 -3
View File
@@ -110,7 +110,7 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_ENABLE_OPENXR)
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
endif()
@@ -276,10 +276,10 @@ function(mkw_configure_product target)
mkw_bound_translated_compiles(${target})
if(MKW_PRODUCT_WITHOUT_GAME)
target_link_libraries(${target} PRIVATE
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
else()
target_link_libraries(${target} PRIVATE
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
endif()
target_link_libraries(${target} PRIVATE
@@ -379,6 +379,21 @@ function(mkw_configure_product target)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_INITIAL_PIPELINE_CACHE}"
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
# Schannel, mbed TLS does not ship one itself. Not SHA256-pinned like the DSP ROM above: unlike
# a fixed hardware ROM, this bundle is expected to be refreshed periodically as CAs rotate.
# Windows gets its trust store from Schannel, so only the platforms that actually build the
# mbed TLS path need the bundle beside the executable.
if(NOT MKW_PLATFORM_WINDOWS)
set(MKW_CA_CERTIFICATE_BUNDLE "${MKW_RUNTIME_SOURCE_DIR}/assets/certs/cacert.pem")
if(NOT EXISTS "${MKW_CA_CERTIFICATE_BUNDLE}")
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
endif()
endfunction()
# Android ships each product as the shared library SDLActivity loads; the base
+2 -1
View File
@@ -5,6 +5,7 @@
#include <imgui.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_platform.h>
#include <algorithm>
#include <array>
@@ -146,7 +147,7 @@ std::string BuildMappingString() {
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
}
}
mapping += "platform:Windows,";
mapping += std::string("platform:") + SDL_GetPlatform() + ",";
return mapping;
}
+6 -5
View File
@@ -648,9 +648,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
if (!path) {
return -101;
}
if (!RuntimeConfigFile::NetworkEnabled(true)) {
// The guest opens several /dev/net nodes at boot and retries; report the
// reason online will not work exactly once.
const bool isIpTop = std::strcmp(path, "/dev/net/ip/top") == 0;
const bool isSsl = std::strcmp(path, "/dev/net/ssl") == 0;
// KD and NCD provide local identity/configuration services even offline.
if ((isIpTop || isSsl) && !RuntimeConfigFile::NetworkEnabled(true)) {
static bool reported = false;
if (!reported) {
reported = true;
@@ -665,10 +666,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
kind = DeviceKind::KdTime;
} else if (std::strcmp(path, "/dev/net/ncd/manage") == 0) {
kind = DeviceKind::NcdManage;
} else if (std::strcmp(path, "/dev/net/ip/top") == 0) {
} else if (isIpTop) {
kind = DeviceKind::IpTop;
EnsureSocketRuntime();
} else if (std::strcmp(path, "/dev/net/ssl") == 0) {
} else if (isSsl) {
kind = DeviceKind::Ssl;
EnsureSocketRuntime();
} else {
+1
View File
@@ -242,6 +242,7 @@ void WritePollResults(uint32_t outAddress,
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
// network_socket.cpp
int32_t DeleteWiiSocket(uint32_t fd);
void CleanupAllWiiSockets();
sockaddr_in ReadWiiSockAddr(uint32_t addr);
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
+3 -2
View File
@@ -21,7 +21,7 @@ static int32_t NewWiiSocket(uint32_t af, uint32_t type, uint32_t protocol) {
return wiiFd;
}
static int32_t DeleteWiiSocket(uint32_t fd) {
int32_t DeleteWiiSocket(uint32_t fd) {
WiiSocket* s = GetWiiSocket(fd);
if (!s) {
return -SO_EBADF;
@@ -467,7 +467,8 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize),
static_cast<int>(flags), destPtr, destLen);
const int hostError = ret < 0 ? NativeLastError() : 0;
int32_t result = SocketResult(ret);
// Diagnostics may change the native error; use the send result captured above.
int32_t result = ret >= 0 ? SocketResult(ret) : SocketErrorResult(hostError);
if (patchedWrite && ret == static_cast<int>(sendSize)) {
result = static_cast<int32_t>(in[0].size);
}
+296 -5
View File
@@ -1,4 +1,20 @@
#include "network_internal.h"
#include "runtime_config.h"
#include "runtime_log.h"
#ifndef _WIN32
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>
#include <mbedtls/error.h>
#include <mbedtls/net_sockets.h>
#include <mbedtls/ssl.h>
#include <mbedtls/x509_crt.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <optional>
#endif
namespace NetworkHle {
@@ -56,6 +72,11 @@ struct SslSession {
CredHandle cred{};
CtxtHandle context{};
SecPkgContext_StreamSizes sizes{};
#else
bool haveSsl = false;
mbedtls_ssl_context sslContext{};
mbedtls_ssl_config sslConfig{};
mbedtls_net_context netContext{};
#endif
};
@@ -539,21 +560,283 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
return copied == 0 ? SSL_ERR_ZERO : static_cast<int32_t>(copied);
}
#else
// Windows gets TLS for free from the OS (Schannel, above) - mbed TLS is this project's own
// vendored equivalent for everywhere else (runtime/third_party/mbedtls, see runtime/CMakeLists.txt
// for why mbed TLS specifically). The CA chain and RNG are expensive to set up (parsing ~150 root
// certificates, seeding entropy) and read-only once built, so they're shared process-wide instead
// of being redone per SSL session.
static bool g_mbedtlsCaLoaded = false;
static mbedtls_x509_crt g_mbedtlsCaChain;
static mbedtls_entropy_context g_mbedtlsEntropy;
static mbedtls_ctr_drbg_context g_mbedtlsCtrDrbg;
static ssize_t SendSslSocket(NativeSocket socket, const uint8_t* data, size_t size) {
#ifdef __APPLE__
const int noSigPipe = 1;
if (setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)) != 0) {
return -1;
}
return send(socket, data, size, 0);
#else
return send(socket, data, size, MSG_NOSIGNAL);
#endif
}
static int MbedtlsSend(void* context, const unsigned char* data, size_t size) {
const auto* net = static_cast<mbedtls_net_context*>(context);
const ssize_t result = SendSslSocket(net->fd, data, size);
if (result >= 0) {
return static_cast<int>(result);
}
if (errno == EINTR) {
return MBEDTLS_ERR_SSL_WANT_WRITE;
}
if (errno == EPIPE || errno == ECONNRESET) {
return MBEDTLS_ERR_NET_CONN_RESET;
}
return MBEDTLS_ERR_NET_SEND_FAILED;
}
static int MbedtlsRecv(void* context, unsigned char* data, size_t size) {
const int result = mbedtls_net_recv(context, data, size);
// Blocking socket timeouts must leave the TLS session retryable.
if (result == MBEDTLS_ERR_NET_RECV_FAILED && (errno == EAGAIN || errno == EWOULDBLOCK)) {
return MBEDTLS_ERR_SSL_WANT_READ;
}
return result;
}
// Mirrors ax_mix.cpp's FindDspCoefficientRom exactly - same three places a bundled asset can live
// depending on platform and how the binary was launched (next to the desktop executable, the
// Android app's own data directory, or a source-tree checkout during development).
static std::optional<std::filesystem::path> FindCaCertificateBundle() {
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
const auto adjacent = *executableDirectory / "cacert.pem";
if (std::filesystem::is_regular_file(adjacent)) {
return adjacent;
}
}
#if defined(__ANDROID__)
const auto androidAsset = RuntimeConfigFile::ApplicationDataDirectory() / "cacert.pem";
if (std::filesystem::is_regular_file(androidAsset)) {
return androidAsset;
}
#endif
for (auto base = std::filesystem::current_path(); !base.empty();) {
const auto sourceTreeAsset = base / "runtime" / "assets" / "certs" / "cacert.pem";
if (std::filesystem::is_regular_file(sourceTreeAsset)) {
return sourceTreeAsset;
}
const auto parent = base.parent_path();
if (parent == base) {
break;
}
base = parent;
}
return std::nullopt;
}
// Lazy, once-per-process: the first real SSL use pays for parsing the CA bundle and seeding the
// RNG, every session after that reuses the result. Returns false (logging once) if the bundle is
// missing or unparseable - callers treat that as a normal handshake failure, not a crash, since a
// missing TLS root store shouldn't take down gameplay that never touches the network.
static bool EnsureMbedtlsGlobalsInitialized() {
static const bool initialized = [] {
mbedtls_x509_crt_init(&g_mbedtlsCaChain);
mbedtls_entropy_init(&g_mbedtlsEntropy);
mbedtls_ctr_drbg_init(&g_mbedtlsCtrDrbg);
const char* personalization = "wiicompiled_ssl";
if (mbedtls_ctr_drbg_seed(&g_mbedtlsCtrDrbg, mbedtls_entropy_func, &g_mbedtlsEntropy,
reinterpret_cast<const unsigned char*>(personalization),
std::strlen(personalization)) != 0) {
NetFail("ssl: failed to seed TLS random number generator");
return false;
}
const auto bundle = FindCaCertificateBundle();
if (!bundle) {
NetFail("ssl: missing TLS root CA bundle (cacert.pem) - HTTPS connections will fail");
return false;
}
const int parseRet = mbedtls_x509_crt_parse_file(&g_mbedtlsCaChain, bundle->string().c_str());
if (parseRet < 0) {
char errorBuffer[256];
mbedtls_strerror(parseRet, errorBuffer, sizeof(errorBuffer));
NetFail("ssl: failed to parse CA bundle %s: %s", bundle->string().c_str(), errorBuffer);
return false;
}
return true;
}();
g_mbedtlsCaLoaded = initialized;
return initialized;
}
// Builds this session's mbed TLS handshake state exactly once - a second call (e.g. the handshake
// re-running after DOHANDSHAKE was already satisfied) is a no-op via ssl.haveSsl.
static int32_t EnsureMbedtlsSession(SslSession& ssl) {
if (ssl.haveSsl) {
return SSL_OK;
}
if (!EnsureMbedtlsGlobalsInitialized()) {
return SSL_ERR_FAILED;
}
mbedtls_ssl_init(&ssl.sslContext);
mbedtls_ssl_config_init(&ssl.sslConfig);
if (mbedtls_ssl_config_defaults(&ssl.sslConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
return SSL_ERR_FAILED;
}
// Real certificate validation, matching Schannel's SCH_CRED_AUTO_CRED_VALIDATION on the
// Windows side above - a self-signed or wrong-hostname certificate must fail the handshake,
// not just get logged.
mbedtls_ssl_conf_authmode(&ssl.sslConfig, MBEDTLS_SSL_VERIFY_REQUIRED);
mbedtls_ssl_conf_ca_chain(&ssl.sslConfig, &g_mbedtlsCaChain, nullptr);
mbedtls_ssl_conf_rng(&ssl.sslConfig, mbedtls_ctr_drbg_random, &g_mbedtlsCtrDrbg);
if (mbedtls_ssl_setup(&ssl.sslContext, &ssl.sslConfig) != 0) {
return SSL_ERR_FAILED;
}
// The hostname drives both SNI (which certificate the server presents) and the CN/SAN check
// mbedtls_ssl_conf_authmode enforces above - required, not optional, same reasoning as the
// Windows path's own "refuse an empty hostname" check just above SslHandshakeImpl.
mbedtls_ssl_set_hostname(&ssl.sslContext, ssl.hostname.c_str());
ssl.netContext.fd = static_cast<int>(ssl.native);
mbedtls_ssl_set_bio(&ssl.sslContext, &ssl.netContext, MbedtlsSend, MbedtlsRecv, nullptr);
ssl.haveSsl = true;
return SSL_OK;
}
static void ClearSslSession(SslSession& ssl) {
if (ssl.haveSsl) {
mbedtls_ssl_free(&ssl.sslContext);
mbedtls_ssl_config_free(&ssl.sslConfig);
}
ssl = {};
}
static int32_t SslHandshakeImpl(SslSession&) {
return SSL_ERR_FAILED;
static int32_t SslHandshakeImpl(SslSession& ssl) {
if (ssl.plaintextWfc) {
ssl.handshaked = true;
return SSL_OK;
}
if (ssl.handshaked) {
return SSL_OK;
}
if (ssl.native == kInvalidSocket) {
return SSL_ERR_SYSCALL;
}
// mbed TLS can authenticate a certificate chain without authenticating a server identity when
// no hostname is set - refuse that ambiguous mode, matching the Windows path's own check.
if (ssl.hostname.empty()) {
return SSL_ERR_VCOMMONNAME;
}
static int32_t SslWrite(SslSession&, const uint8_t*, uint32_t) {
return SSL_ERR_FAILED;
const int32_t setupRet = EnsureMbedtlsSession(ssl);
if (setupRet != SSL_OK) {
return setupRet;
}
static int32_t SslRead(SslSession&, uint8_t*, uint32_t) {
// Receive timeouts are retryable, but the handshake must still terminate.
const auto handshakeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
int handshakeRet;
while ((handshakeRet = mbedtls_ssl_handshake(&ssl.sslContext)) != 0) {
if (handshakeRet == MBEDTLS_ERR_SSL_WANT_READ || handshakeRet == MBEDTLS_ERR_SSL_WANT_WRITE) {
if (std::chrono::steady_clock::now() >= handshakeDeadline) {
NetFail("ssl handshake TIMED OUT host=%s", ssl.hostname.c_str());
return SSL_ERR_FAILED;
}
continue;
}
char errorBuffer[256];
mbedtls_strerror(handshakeRet, errorBuffer, sizeof(errorBuffer));
NetFail("ssl handshake FAILED host=%s mbedtls_err=%s", ssl.hostname.c_str(), errorBuffer);
return handshakeRet == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED ? SSL_ERR_VCOMMONNAME : SSL_ERR_FAILED;
}
ssl.handshaked = true;
return SSL_OK;
}
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
if (!data || size == 0) {
return SSL_ERR_ZERO;
}
const int32_t handshakeRet = SslHandshake(ssl);
if (handshakeRet != SSL_OK) {
return handshakeRet;
}
if (ssl.plaintextWfc) {
uint32_t total = 0;
while (total < size) {
const ssize_t sent = SendSslSocket(ssl.native, data + total, size - total);
if (sent <= 0) {
return SSL_ERR_SYSCALL;
}
total += static_cast<uint32_t>(sent);
}
return static_cast<int32_t>(total);
}
// mbed TLS is allowed to write fewer bytes than requested in one call (e.g. when size exceeds
// one TLS record) - the caller must resend the remainder starting from where it left off, so
// loop here until every byte is actually written rather than returning the first partial count.
uint32_t totalWritten = 0;
const auto writeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
while (totalWritten < size) {
const int ret = mbedtls_ssl_write(&ssl.sslContext, data + totalWritten, size - totalWritten);
if (ret > 0) {
totalWritten += static_cast<uint32_t>(ret);
continue;
}
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
if (std::chrono::steady_clock::now() >= writeDeadline) {
DeleteWiiSocket(ssl.socketFd);
return SSL_ERR_FAILED;
}
continue;
}
return SSL_ERR_FAILED;
}
return static_cast<int32_t>(totalWritten);
}
static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
if (!out || size == 0) {
return SSL_ERR_ZERO;
}
const int32_t handshakeRet = SslHandshake(ssl);
if (handshakeRet != SSL_OK) {
return handshakeRet;
}
if (ssl.plaintextWfc) {
const ssize_t ret = recv(ssl.native, out, size, 0);
if (ret == 0) {
return SSL_ERR_ZERO;
}
if (ret < 0) {
return SSL_ERR_RAGAIN;
}
return static_cast<int32_t>(ret);
}
const int ret = mbedtls_ssl_read(&ssl.sslContext, out, size);
if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
return SSL_ERR_ZERO;
}
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
return SSL_ERR_RAGAIN;
}
if (ret < 0) {
return SSL_ERR_FAILED;
}
return ret;
}
#endif
// The handshake runs on every SSL read/write, so a failure repeats for as long
@@ -640,6 +923,14 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
const int timeoutMs = 15000;
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
#else
// Match the Windows 15s timeout so a peer that accepts the TCP connection but stalls
// during the TLS handshake or a later read/write can't hang this thread forever. POSIX
// takes a struct timeval here, not a plain millisecond count like Windows does.
struct timeval timeout {};
timeout.tv_sec = 15;
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
#endif
WriteSslReturn(in, SSL_OK);
return 0;
+11 -9
View File
@@ -222,7 +222,6 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
throw;
}
DecrementSchedulerDisableCount();
RunDeferredReschedule(cpu);
}
}
} catch (const ::Memory::AccessViolation& e) {
@@ -233,7 +232,7 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
// Host DNS workers never touch guest memory. Commit their output here on
// the scheduler thread, waking synchronous IOS waiters or queuing async IOS
// callbacks before the callback drain below.
bool completionNeedsReschedule = false;
bool completionNeedsReschedule = handledAny;
if (Network_HLE_ProcessCompletions(cpu)) {
handledAny = true;
completionNeedsReschedule = true;
@@ -446,15 +445,18 @@ PPC_NATIVE_OVERRIDE_VOID(801A08E0, OS__SetPeriodicAlarm_801a08e0, (CpuContext* c
// returning 0 when the manager pointer (0x80386298) is null.
extern "C" uint32_t RFLiIsWorking_HLE_800bd860()
{
// Pump alarms/callbacks on the current guest thread when available. Using a
// detached persistent context here can leave the busy loop waiting on work
// that completed on the wrong scheduling context.
CpuContext* cpu = TryGetCpuContext();
if (!cpu) {
cpu = &GetPersistentCpuContext();
}
// Alarm callbacks interrupt the caller; keep their register writes private.
GuestInterruptCallbackContext interrupt;
CpuContext* cpu = interrupt.get();
EnsureSda1Base(cpu);
IncrementSchedulerDisableCount();
try {
ProcessAlarmQueue(cpu, 32);
} catch (...) {
DecrementSchedulerDisableCount();
throw;
}
DecrementSchedulerDisableCount();
// Now return the actual "working" status
constexpr uint32_t kRflManagerPtrAddr = 0x80386298u;
+145
View File
@@ -4,6 +4,14 @@
#include "nand_internal.h"
#include <atomic>
#include <cerrno>
#ifdef __linux__
#include <linux/fs.h>
#include <sys/syscall.h>
#endif
// ============================================================================
// Local helpers
// ============================================================================
@@ -44,6 +52,32 @@ static FileHandle* ResolveNandFileHandle(const char* who, uint32_t fileInfoPtr)
// The synchronous RVL NAND* library
// ============================================================================
static bool RenameNoReplace(const std::filesystem::path& from,
const std::filesystem::path& to,
std::error_code& error) {
#ifdef _WIN32
if (MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_WRITE_THROUGH)) {
error.clear();
return true;
}
error = std::error_code(static_cast<int>(GetLastError()), std::system_category());
return false;
#elif defined(__linux__)
const int result = syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE);
if (result == 0) {
error.clear();
return true;
}
error = std::error_code(errno, std::generic_category());
return false;
#else
(void)from;
(void)to;
error = std::make_error_code(std::errc::operation_not_supported);
return false;
#endif
}
extern "C" int32_t NANDInit_HLE(void) {
// Initialize ISFS
ISFS_OpenLib_Initialize(&GetPersistentCpuContext());
@@ -348,6 +382,11 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
PPC_NATIVE_OVERRIDE(8019BBE0, NANDCreateDir_HLE, int32_t, (uint32_t pathPtr, uint32_t perm, uint32_t attr), (pathPtr, perm, attr));
extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
// A cross-mount move is implemented as several host operations. Keep two
// guest moves from interleaving those operations and corrupting recovery.
static std::mutex moveMutex;
std::lock_guard<std::mutex> lock(moveMutex);
const char* srcPath = srcPathPtr ? (const char*)Memory::GetPointer(srcPathPtr) : nullptr;
const char* dstPath = dstPathPtr ? (const char*)Memory::GetPointer(dstPathPtr) : nullptr;
@@ -383,6 +422,112 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
return NAND_RESULT_OK;
}
// Flatpak can expose the managed NAND and an external Riivolution save
// directory as separate mounts. Linux cannot rename across mounts, but
// nandMove must still work for files such as banner.bin. Preserve the
// operation's semantics with a copy followed by source removal.
if (ec == std::errc::cross_device_link) {
static std::atomic<uint64_t> moveSequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
std::filesystem::path scratchHost;
std::error_code scratchEc;
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto name = ".nandmove-" + std::to_string(processId) + "-" +
std::to_string(moveSequence.fetch_add(1)) + "-" +
std::to_string(attempt);
const auto candidate = dstDirectoryHost / name;
scratchEc.clear();
if (std::filesystem::create_directory(candidate, scratchEc)) {
scratchHost = candidate;
break;
}
if (scratchEc && scratchEc != std::errc::file_exists) {
LogNandError("NANDMove", "failed to claim temporary directory '%s': %s",
HostPathText(candidate).c_str(), scratchEc.message().c_str());
return NAND_RESULT_UNKNOWN;
}
}
if (scratchHost.empty()) {
LogNandError("NANDMove", "could not claim a unique temporary directory");
return NAND_RESULT_UNKNOWN;
}
const bool sourceIsDirectory = IsDirectory(srcHost);
const std::filesystem::path tempHost = scratchHost / srcName;
const auto cleanupScratch = [&]() {
std::error_code cleanupEc;
std::filesystem::remove_all(scratchHost, cleanupEc);
if (cleanupEc) {
LogNandError("NANDMove", "failed to clean up temporary directory '%s': %s",
HostPathText(scratchHost).c_str(), cleanupEc.message().c_str());
}
};
std::error_code copyEc;
if (sourceIsDirectory) {
std::filesystem::copy(srcHost, tempHost,
std::filesystem::copy_options::recursive, copyEc);
} else {
std::filesystem::copy_file(srcHost, tempHost, copyEc);
}
if (copyEc) {
LogNandError("NANDMove", "cross-mount copy failed: %s", copyEc.message().c_str());
cleanupScratch();
return NAND_RESULT_UNKNOWN;
}
std::error_code publishEc;
if (sourceIsDirectory) {
RenameNoReplace(tempHost, dstHost, publishEc);
} else {
// link(2) and CreateHardLink do not replace an existing destination,
// unlike rename(2) on POSIX. Both paths are already on the target
// filesystem, so the link is a no-replace publication operation.
std::filesystem::create_hard_link(tempHost, dstHost, publishEc);
}
if (publishEc) {
LogNandError("NANDMove", "failed to publish cross-mount copy: %s",
publishEc.message().c_str());
cleanupScratch();
return NAND_RESULT_UNKNOWN;
}
cleanupScratch();
std::error_code removeEc;
std::filesystem::remove_all(srcHost, removeEc);
if (!removeEc) {
LogNandWarning("NANDMove", "used copy/remove fallback across mounts");
return NAND_RESULT_OK;
}
// Keep the source as the authoritative copy when cleanup fails. The
// destination was published atomically on its own mount; regular files
// are rolled back below, while directories keep the complete copy when
// their source removal was only partial. Cross-mount moves cannot
// provide crash-atomicity, so this is best effort.
LogNandError("NANDMove", "copy succeeded but source removal failed: %s",
removeEc.message().c_str());
if (sourceIsDirectory) {
// remove_all may have removed only part of a directory tree. Keep
// the complete published copy rather than rolling it back to a
// partially deleted source.
LogNandWarning("NANDMove", "preserving published directory copy after partial source removal");
} else {
std::error_code rollbackEc;
std::filesystem::remove_all(dstHost, rollbackEc);
if (rollbackEc) {
LogNandError("NANDMove", "failed to roll back destination '%s': %s",
HostPathText(dstHost).c_str(), rollbackEc.message().c_str());
}
}
return NAND_RESULT_UNKNOWN;
}
LogNandError("NANDMove", "FAILED error=%d message='%s'", ec.value(), ec.message().c_str());
return NAND_RESULT_UNKNOWN;
}
+1 -1
View File
@@ -48,7 +48,7 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#elif defined(__ANDROID__)
// Bundled read-only runtime resources (wii_bootstrap/, dsp_coef.bin,
// initial_pipeline_cache.db) unpacked from the APK by the activity. Stands
// initial_pipeline_cache.db, cacert.pem) unpacked from the APK by the activity. Stands
// in for the executable directory the desktop builds look next to.
if (const char* resources = std::getenv("MKW_ANDROID_RESOURCES_DIR"); resources && *resources) {
return std::filesystem::path(resources);
@@ -27,7 +27,8 @@ public sealed partial class CxxLinearCodeGenerator
IReadOnlyDictionary<uint, GuestAbiContract> stateFreeAbiContracts,
IReadOnlyDictionary<uint, string> stateFreeCallSymbols,
IReadOnlyDictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant> stateFreeCallSiteVariants,
IReadOnlySet<uint> modOverridableCallTargets)
IReadOnlySet<uint> modOverridableCallTargets,
bool shareLrContinuationDispatch)
{
if (ins is IrPhi)
{
@@ -410,11 +411,18 @@ public sealed partial class CxxLinearCodeGenerator
fallbackPad = IndentPad(indent + 1);
}
if (shareLrContinuationDispatch)
{
sb.AppendLine($"{fallbackPad}goto lr_continuation_dispatch;");
}
else
{
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
sb.AppendLine($"{fallbackPad}}}");
sb.AppendLine($"{fallbackPad}return;");
}
if (localFallthroughLr.HasValue)
{
sb.AppendLine($"{pad}}}");
@@ -241,12 +241,14 @@ public sealed partial class CxxLinearCodeGenerator
var labelNames = func.Blocks.ToDictionary(b => b.Label, b => SanitizeLabel(b.Label), StringComparer.OrdinalIgnoreCase);
var instructionContinuationLabels = new Dictionary<uint, string>();
var needsInstructionContinuationLabels = func.Blocks
var continuationCallCount = func.Blocks
.SelectMany(static block => block.Instructions)
.OfType<IrCall>()
.Any(call =>
.Count(call =>
TryParseAddress(call.Target, out var target) &&
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)));
var needsInstructionContinuationLabels = continuationCallCount > 0;
var shareLrContinuationDispatch = continuationCallCount > 1;
if (needsInstructionContinuationLabels)
{
foreach (var trace in func.Blocks.SelectMany(static block => block.Instructions).OfType<IrTracePpc>())
@@ -405,7 +407,7 @@ public sealed partial class CxxLinearCodeGenerator
_activeGpuFifoBurstSlot = gpuFifoBurstPlan.Slot(block.Label, i);
try
{
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
}
finally
{
@@ -423,7 +425,7 @@ public sealed partial class CxxLinearCodeGenerator
switch (term)
{
case IrUndefined undef:
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
AppendFlush(body, " ");
body.AppendLine(" return;");
break;
@@ -509,6 +511,18 @@ public sealed partial class CxxLinearCodeGenerator
body.AppendLine();
}
if (shareLrContinuationDispatch)
{
// Every call site has already reloaded the callee's state.
// Keep the complete local target set, but emit it only once.
body.AppendLine(" return;");
body.AppendLine("[[maybe_unused]] lr_continuation_dispatch:");
EmitLocalLrContinuationDispatch(body, " ", labelNames);
body.AppendLine(" if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {");
body.AppendLine(" InvokeIndirectCpu(ctx->lr, ctx);");
body.AppendLine(" }");
body.AppendLine(" return;");
}
body.Append('}');
// The residency discovery pass only exists for its side effects
// on the recorder; materializing its text costs a full copy of
@@ -1942,6 +1942,22 @@ public sealed partial class PpcLifter
new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs)
};
case "bltl":
{
var crField = "cr0";
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
{
crField = NormalizeRegister(crOp.Name);
}
return new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
$"0x{ins.EndAddress:X8}", crField)
};
}
case "bcl":
{
var rawInstr = ReadRawInstruction(ins);
@@ -14,6 +14,23 @@ public class PpcLifterAdditionalCoverageTests
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
[Fact]
public void LinkedConditionalBranchSetsLrBeforeEitherPath()
{
var branch = PpcDecoder.Decode(0x80004394, 0x41800029);
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
Assert.Equal("bltl", branch.Mnemonic);
var lr = Assert.IsType<IrAssign>(ir[0]);
Assert.Equal("lr", lr.Destination);
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
var decision = Assert.IsType<IrBranch>(ir[1]);
Assert.Equal("blt", decision.Condition);
Assert.Equal("0x800043BC", decision.TrueLabel);
Assert.Equal("0x80004398", decision.FalseLabel);
}
[Fact]
public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
{
@@ -0,0 +1,52 @@
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class SharedLrContinuationCodeGenTests
{
[Theory]
[InlineData(2)]
[InlineData(20)]
public void MultipleHooksShareOneCompleteDispatchAndKeepNormalFallthrough(int callCount)
{
var instructions = new List<IrInstruction>();
for (var i = 0; i < callCount; i++)
{
instructions.Add(new IrAssign("lr", IrValue.Imm(unchecked((int)(0x80001004u + (uint)i * 4u)))));
instructions.Add(new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()));
}
instructions.Add(new IrAssign("r3", IrValue.Imm(8)));
instructions.Add(new IrReturn(null));
var function = new IrFunction("shared_lr_continuation", "0x80001000", new[]
{
new IrBasicBlock("0x80001000", instructions),
new IrBasicBlock("0x80001100", new IrInstruction[]
{
new IrAssign("r3", IrValue.Imm(1)), new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["lr"] = ValueRepresentation.UInt32, ["r3"] = ValueRepresentation.UInt32
});
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("shared_lr_continuation", ValueRepresentation.Void), types,
lrContinuationCallTargets: new HashSet<uint> { 0x81800000u });
Assert.Equal(1, code.Split("switch (ctx->lr)").Length - 1);
Assert.Equal(callCount, code.Split("goto lr_continuation_dispatch;").Length - 1);
Assert.Equal(callCount, code.Split("if (ctx->lr != ").Length - 1);
Assert.Contains("case 0x80001100u:", code);
Assert.Contains("goto loc_80001100;", code);
Assert.Contains("InvokeIndirectCpu(ctx->lr, ctx);", code);
var sharedLabel = code.IndexOf("lr_continuation_dispatch:", StringComparison.Ordinal);
Assert.True(code.IndexOf("r3 = 8;", StringComparison.Ordinal) < sharedLabel);
Assert.True(code.IndexOf("case 0x80001100u:", StringComparison.Ordinal) > sharedLabel);
}
}