6 Commits

Author SHA1 Message Date
patchzyy 3d2c1fd8b8 extra safety 2026-09-06 10:49:01 +02:00
patchzyy 0e647b3dbb Merge commit '28cbea46ab44105fde928653507add5d9d4873f4' into fix/pr168-save-safety 2026-09-06 10:13:19 +02:00
patchzyy 28cbea46ab import setting.txt 2026-09-06 10:04:07 +02:00
JGM01 55ea5dfbda I dont really want to change this to be honest. 2026-09-05 22:55:59 -04:00
JGM01 4a7f592b8a shorten 2026-09-05 22:51:58 -04:00
JGM01 c95f1c95b7 treat empty mkw save as missing
first-run format zero-fills rksys.dat before any real save; a quit before
the first save left an all-zero file that read back as corrupt and trapped
the user in a delete/recreate loop. read opens now treat an all-zero
rksys.dat as absent (a real save always begins with the RKSD0006 header),
so the game recreates it from scratch. also ignore native build output.
2026-09-05 22:48:05 -04:00
27 changed files with 119 additions and 675 deletions
-27
View File
@@ -30,14 +30,6 @@ jobs:
with:
dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore translator/Translator.sln
@@ -46,22 +38,3 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
cpp:
name: C++ runtime tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-14]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure
run: cmake -S runtime/tests -B build/tests -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build/tests --config Release --parallel
- name: Test
run: ctest --test-dir build/tests -C Release --output-on-failure --no-tests=error
+1 -21
View File
@@ -28,19 +28,6 @@ jobs:
path: Launcher/artifacts/downloads
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
# Install sccache.
- name: Run sccache-action
uses: mozilla/sccache-action@v0.0.11
# Tell CMake to use sccache and use GitHub's API.
- name: Configure sccache environment
shell: pwsh
run: |
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
- name: Prepare the shipped Windows toolchain
shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1
@@ -49,13 +36,6 @@ jobs:
shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1
# Expose cache token context to the build script.
- name: Translate, compile the full runtime, and link
shell: pwsh
run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
# Print cache results (even if the build fails)
- name: Show sccache stats
if: always()
shell: pwsh
run: sccache --show-stats
run: ./Launcher/Test-Recompilation.ps1 -Parallel 3
-2
View File
@@ -72,5 +72,3 @@ project.lock.json
*.log
output.txt
# Operating System
.DS_Store
+1 -1
View File
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.31'
ProductVersion = '0.2.29'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
+8 -14
View File
@@ -117,13 +117,12 @@ function Get-MkwProjectPins([string]$ProjectFile) {
}
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
<#
Runs a build tool and turns a non-zero exit code into a described failure. By default,
Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Callers that
need to avoid waiting on unrelated descendants can opt into the call-operator path.
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#>
@@ -133,14 +132,9 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"'
})
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
$global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
}
+2 -4
View File
@@ -136,11 +136,9 @@ try {
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime'
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
'Compiling and linking the synthetic product with the full runtime' `
-WaitForProcessTree $false
'Compiling and linking the synthetic product with the full runtime'
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
} finally {
$env:PATH = $oldPath
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.31</Version>
<Version>0.2.29</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.31</Version>
<Version>0.2.29</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
+1 -1
View File
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.31";
public const string Version = "0.2.29";
}
/// <summary>One installed product's record inside install-state.json.</summary>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.31</Version>
<Version>0.2.29</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.31";
public const string Version = "0.2.29";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.31</Version>
<Version>0.2.29</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
+68 -1
View File
@@ -269,8 +269,75 @@ target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/includ
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
add_subdirectory(tests)
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
+5 -26
View File
@@ -129,20 +129,6 @@ inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& seri
return bytes; // The unused tail stays raw zero, as in Dolphin.
}
// Atomically claim our own scratch directory. A collision belongs to another
// launch (or a previous crashed launch); leave it untouched and try another name.
inline std::optional<std::filesystem::path> CreateScratchDirectory(
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
ec.clear();
if (std::filesystem::create_directory(candidate, ec)) return candidate;
if (ec && ec != std::errc::file_exists) return std::nullopt;
}
ec = std::make_error_code(std::errc::file_exists);
return std::nullopt;
}
// Never replace an existing file, including an unreadable or damaged one.
// Publish a complete file atomically so simultaneous launches use one identity.
inline bool Ensure(const std::filesystem::path& root, std::string& error,
@@ -175,20 +161,13 @@ inline bool Ensure(const std::filesystem::path& root, std::string& error,
return false;
}
static std::atomic<unsigned> sequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
const auto scratch = CreateScratchDirectory(path.parent_path(),
std::to_string(processId) + "-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
std::to_string(sequence++), ec);
if (!scratch) {
const auto scratch = path.parent_path() / (".setting-init-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(sequence++));
if (!std::filesystem::create_directory(scratch, ec)) {
error = "Cannot create temporary NAND settings directory: " + ec.message();
return false;
}
const auto temporary = *scratch / "setting.txt";
const auto temporary = scratch / "setting.txt";
bool written = false;
{
std::ofstream output(temporary, std::ios::binary);
@@ -205,7 +184,7 @@ inline bool Ensure(const std::filesystem::path& root, std::string& error,
#endif
}
std::filesystem::remove(temporary, ec);
std::filesystem::remove(*scratch, ec);
std::filesystem::remove(scratch, ec);
// A competing launcher may have published its settings first. Always read
// the winner from NAND rather than using our unpersisted candidate serial.
const auto persisted = Read(root);
-26
View File
@@ -1,26 +0,0 @@
#pragma once
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <system_error>
namespace RuntimeScSerial {
// SCGetProductSN's output is a u32, not a character buffer. DWC loads
// that word and formats it with the product code to construct csnum.
template <typename RangeValidator, typename WordWriter>
uint32_t Write(std::string_view serial, uint32_t address,
RangeValidator&& contains, WordWriter&& write32) {
if (serial.empty() || serial.size() > 9 ||
serial.find_first_not_of("0123456789") != std::string_view::npos) return 0;
uint32_t number = 0;
const auto parsed = std::from_chars(serial.data(), serial.data() + serial.size(), number);
if (parsed.ec != std::errc{} || parsed.ptr != serial.data() + serial.size() ||
!address || !contains(address, sizeof(uint32_t))) return 0;
write32(address, number);
return 1;
}
} // namespace RuntimeScSerial
+6 -4
View File
@@ -1,7 +1,6 @@
#include "hle_stubs.h"
#include "console_identity.h"
#include "sc_serial_contract.h"
#include <cstdlib>
#include <cstddef>
#include <cstdint>
@@ -98,9 +97,12 @@ PPC_NATIVE_OVERRIDE(801B2424, SCGetProductCode_HLE, uint32_t, (), ());
extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress)
{
const std::string& serial = RuntimeConsoleIdentity::Current().serial;
return RuntimeScSerial::Write(serial, serialAddress,
[](uint32_t address, size_t size) { return Memory::Contains(address, size); },
[](uint32_t address, uint32_t value) { Memory::Write32(address, value); });
if (!serialAddress || !Memory::Contains(serialAddress, serial.size() + 1)) {
return 0;
}
std::memcpy(Memory::GetPointer(serialAddress, serial.size() + 1),
serial.c_str(), serial.size() + 1);
return 1;
}
PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress));
+1 -6
View File
@@ -3,7 +3,6 @@
// Shared state and helpers live in nand_internal.h.
#include "nand_internal.h"
#include "nand_file_ops.h"
// ============================================================================
// Local helpers
@@ -379,15 +378,11 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
}
std::error_code ec;
NandMove(srcHost, dstHost, ec);
std::filesystem::rename(srcHost, dstHost, ec);
if (!ec) {
return NAND_RESULT_OK;
}
if (ec == std::errc::file_exists) {
return NAND_RESULT_EXISTS;
}
LogNandError("NANDMove", "FAILED error=%d message='%s'", ec.value(), ec.message().c_str());
return NAND_RESULT_UNKNOWN;
}
-115
View File
@@ -1,115 +0,0 @@
#include "nand_file_ops.h"
#include <atomic>
#include <cerrno>
#include <chrono>
#include <filesystem>
#include <string>
#include <system_error>
#ifdef _WIN32
#include <windows.h>
#elif defined(__linux__)
#include <fcntl.h>
#include <linux/fs.h>
#include <sys/syscall.h>
#include <unistd.h>
#elif defined(__APPLE__)
#include <stdio.h>
#endif
namespace {
// Unlike std::filesystem::rename on POSIX, this cannot overwrite a destination
// created by another writer between checking it and publishing the move.
void RenameNoReplace(const std::filesystem::path& source,
const std::filesystem::path& destination, std::error_code& ec) {
#ifdef _WIN32
if (MoveFileExW(source.c_str(), destination.c_str(), 0)) ec.clear();
else ec = std::error_code(GetLastError(), std::system_category());
#else
#ifdef __linux__
const auto result = syscall(SYS_renameat2, AT_FDCWD, source.c_str(),
AT_FDCWD, destination.c_str(), RENAME_NOREPLACE);
#else
const auto result = renamex_np(source.c_str(), destination.c_str(), RENAME_EXCL);
#endif
if (result == 0) {
ec.clear();
return;
}
ec = std::error_code(errno, std::generic_category());
if (ec != std::errc::function_not_supported && ec != std::errc::invalid_argument &&
ec != std::errc::operation_not_supported) return;
// Older filesystems may lack exclusive rename. Linking also publishes
// without replacement; never fall back to an overwriting rename.
std::filesystem::create_hard_link(source, destination, ec);
if (!ec) std::filesystem::remove(source, ec);
#endif
}
struct StagedMove {
std::filesystem::path directory;
std::filesystem::path file;
~StagedMove() {
std::error_code ignored;
std::filesystem::remove(file, ignored);
std::filesystem::remove(directory, ignored);
}
};
} // namespace
// NANDMove does not replace an existing entry. Riivolution save redirects can
// put the destination on a different filesystem from the guest's /tmp files.
void NandMove(const std::filesystem::path& source, const std::filesystem::path& destination,
std::error_code& ec) {
namespace fs = std::filesystem;
const auto status = fs::symlink_status(destination, ec);
if (ec && ec != std::errc::no_such_file_or_directory) return;
ec.clear();
if (fs::exists(status)) {
ec = std::make_error_code(std::errc::file_exists);
return;
}
RenameNoReplace(source, destination, ec);
if (ec != std::errc::cross_device_link) return;
// Do not turn a directory move into a partially completed recursive copy,
// or follow a symlink and delete the link after copying its target.
const auto sourceStatus = fs::symlink_status(source, ec);
if (ec) return;
if (!fs::is_regular_file(sourceStatus)) {
ec = std::make_error_code(std::errc::cross_device_link);
return;
}
static std::atomic<unsigned long long> sequence{0};
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
fs::path stagingDirectory;
bool created = false;
for (int attempt = 0; attempt < 64; ++attempt) {
stagingDirectory = destination.parent_path() /
(".nand-move-" + std::to_string(stamp) + "-" + std::to_string(sequence++));
created = fs::create_directory(stagingDirectory, ec);
if (created) break;
if (ec && ec != std::errc::file_exists) return;
}
if (!created) {
ec = std::make_error_code(std::errc::file_exists);
return;
}
const StagedMove staging{stagingDirectory, stagingDirectory / "data"};
const auto& staged = staging.file;
fs::copy_file(source, staged, fs::copy_options::none, ec);
if (!ec) {
// Publication is a same-filesystem rename: readers never see a partial
// copy. Keep the source until the complete destination is in place.
RenameNoReplace(staged, destination, ec);
if (!ec) fs::remove(source, ec);
}
// A failed source removal leaves both complete copies and reports failure.
// Staging cleanup preserves ec, including on exceptions.
}
-12
View File
@@ -1,12 +0,0 @@
#pragma once
#include <filesystem>
#include <system_error>
// Move without replacing an existing entry. Cross-filesystem regular files are
// staged at the destination before removing the source. On failure the source
// remains; a failed source removal can leave both complete copies.
// The caller must keep the source stable for the duration of the move; NAND HLE
// calls run synchronously on the cooperative guest thread without yielding.
void NandMove(const std::filesystem::path& source, const std::filesystem::path& destination,
std::error_code& ec);
+6 -19
View File
@@ -9,24 +9,11 @@
#include <unordered_map>
namespace InputExpr {
#ifdef MKW_INPUT_EXPR_TEST_CLOCK
// Only the standalone test target supplies this clock; runtime builds use the
// steady clock directly, with no mutable override or extra runtime state.
std::chrono::steady_clock::time_point TestClockNow();
#endif
namespace {
using Clock = std::chrono::steady_clock;
using FSec = std::chrono::duration<double>;
static Clock::time_point Now() {
#ifdef MKW_INPUT_EXPR_TEST_CLOCK
return TestClockNow();
#else
return Clock::now();
#endif
}
enum class Kind {
Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor,
Greater, Less, Equal,
@@ -69,7 +56,7 @@ struct Node {
mutable bool state = false;
mutable unsigned taps = 0;
mutable double value = 0.0;
mutable Clock::time_point mark = Now();
mutable Clock::time_point mark = Clock::now();
mutable bool marked = false;
};
@@ -413,7 +400,7 @@ double Eval(const Node& node, const InputSource& source) {
return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v);
}
case Kind::FnTimer: {
const auto now = Now();
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
@@ -444,7 +431,7 @@ double Eval(const Node& node, const InputSource& source) {
return node.state ? 1.0 : 0.0;
}
case Kind::FnHold: {
const auto now = Now();
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
@@ -461,7 +448,7 @@ double Eval(const Node& node, const InputSource& source) {
return node.state ? 1.0 : 0.0;
}
case Kind::FnTap: {
const auto now = Now();
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
@@ -493,7 +480,7 @@ double Eval(const Node& node, const InputSource& source) {
return desired == node.taps ? 1.0 : 0.0;
}
case Kind::FnPulse: {
const auto now = Now();
const auto now = Clock::now();
const double input = Arg(node, 0, source);
if (input < kConditionThreshold) {
node.released = true;
@@ -515,7 +502,7 @@ double Eval(const Node& node, const InputSource& source) {
return node.state ? 1.0 : 0.0;
}
case Kind::FnSmooth: {
const auto now = Now();
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
-7
View File
@@ -46,13 +46,6 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#elif defined(__linux__)
std::error_code ec;
const auto executable = std::filesystem::read_symlink("/proc/self/exe", ec);
if (ec) {
return std::nullopt;
}
return executable.parent_path();
#else
return std::nullopt;
#endif
-64
View File
@@ -1,64 +0,0 @@
cmake_minimum_required(VERSION 3.16)
# Share the same test registrations with the full runtime build, while allowing
# CI and developers to run them without translated game files or Aurora.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
project(WiiCompiledRuntimeTests LANGUAGES C CXX)
enable_testing()
endif()
get_filename_component(runtime_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
find_package(Threads REQUIRED)
function(mkw_add_test name)
add_executable(${name} ${ARGN})
target_include_directories(${name} PRIVATE "${runtime_root}/include")
target_compile_features(${name} PRIVATE cxx_std_17)
target_link_libraries(${name} PRIVATE Threads::Threads)
add_test(NAME ${name} COMMAND ${name})
endfunction()
if(NOT TARGET mkw_platform)
add_library(mkw_platform STATIC "${runtime_root}/src/platform/host_platform.cpp")
target_include_directories(mkw_platform PUBLIC "${runtime_root}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
endif()
if(WIN32)
target_link_libraries(mkw_platform PUBLIC shell32 ole32)
endif()
mkw_add_test(mkw_platform_paths_tests platform_paths_tests.cpp)
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
mkw_add_test(mkw_nand_save_tests nand_save_tests.cpp)
mkw_add_test(mkw_nand_move_tests nand_move_tests.cpp
"${runtime_root}/src/hle/storage/nand_file_ops.cpp")
target_include_directories(mkw_nand_move_tests PRIVATE "${runtime_root}/src/hle/storage")
mkw_add_test(mkw_nand_settings_tests nand_settings_tests.cpp)
mkw_add_test(mkw_sc_serial_tests sc_serial_tests.cpp)
mkw_add_test(mkw_input_expr_tests test_expr.cpp "${runtime_root}/src/input_expr.cpp")
target_compile_definitions(mkw_input_expr_tests PRIVATE MKW_INPUT_EXPR_TEST_CLOCK)
if(WIN32)
mkw_add_test(mkw_windows_host_context_tests host_context_tests.cpp
"${runtime_root}/src/host_context.cpp")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if(NOT TARGET mkw::libco)
add_library(mkw_libco STATIC "${runtime_root}/third_party/libco/libco.c")
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC "${runtime_root}/third_party/libco")
endif()
mkw_add_test(mkw_linux_host_context_tests host_context_tests.cpp
"${runtime_root}/src/host_context.cpp")
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
elseif(APPLE)
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64|aarch64)$")
message(FATAL_ERROR "The macOS runtime tests require Apple Silicon")
endif()
enable_language(ASM)
set(context_asm "${runtime_root}/src/platform/macos/co_switch.S")
mkw_add_test(mkw_macos_context_abi_tests macos_context_abi_tests.cpp "${context_asm}")
mkw_add_test(mkw_macos_host_context_tests host_context_tests.cpp
"${runtime_root}/src/host_context.cpp" "${context_asm}")
mkw_add_test(mkw_macos_guest_flat_memory_tests macos_guest_flat_memory_tests.cpp
"${runtime_root}/src/guest_flat_memory_macos.cpp")
endif()
-181
View File
@@ -1,181 +0,0 @@
#include "nand_file_ops.h"
#include <atomic>
#include <chrono>
#include <fstream>
#include <functional>
#include <iterator>
#include <string>
#include <thread>
#include <iostream>
#include <stdexcept>
#ifdef __linux__
#include <csignal>
#include <sys/resource.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static void Write(const fs::path& path, const std::string& data) {
std::ofstream output(path, std::ios::binary);
output << data;
output.close();
Require(bool(output), "Fixture write failed");
}
static std::string Read(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
Require(bool(input), "Fixture read failed");
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
// Competing moves must have exactly one winner, retain the losing source, and
// publish the winner's complete contents. Exercise both direct and staged moves.
static void CheckCompetingMoves(const fs::path& sourceRoot, const fs::path& destinationRoot) {
const auto left = sourceRoot / "race-left";
const auto right = sourceRoot / "race-right";
const auto target = destinationRoot / "race-target";
const std::string leftBytes(65536, 'L'), rightBytes(65536, 'R');
for (int attempt = 0; attempt < 128; ++attempt) {
Write(left, leftBytes);
Write(right, rightBytes);
std::atomic<int> ready{0};
std::error_code leftError, rightError;
auto move = [&](const fs::path& source, std::error_code& ec) {
++ready;
while (ready.load() != 2) std::this_thread::yield();
NandMove(source, target, ec);
};
std::thread first(move, std::cref(left), std::ref(leftError));
std::thread second(move, std::cref(right), std::ref(rightError));
first.join();
second.join();
Require(bool(leftError) != bool(rightError), "Competing moves must have exactly one winner");
const bool leftWon = !leftError;
Require((leftWon ? rightError : leftError) == std::errc::file_exists,
"Losing move must report an existing destination");
Require(Read(target) == (leftWon ? leftBytes : rightBytes), "Winner's contents were overwritten");
Require(!fs::exists(leftWon ? left : right), "Winning source was not removed");
Require(Read(leftWon ? right : left) == (leftWon ? rightBytes : leftBytes),
"Losing source must remain intact");
fs::remove(leftWon ? right : left);
fs::remove(target);
}
}
int main() {
const auto name = "wiicomp-nand-move-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const auto root = fs::temp_directory_path() / name;
fs::path other;
try {
fs::create_directory(root);
const auto source = root / "banner.bin";
const auto destination = root / "moved.bin";
std::error_code ec;
Write(source, "banner");
NandMove(source, destination, ec);
Require(!ec && !fs::exists(source) && Read(destination) == "banner", "Same-device move failed");
CheckCompetingMoves(root, root);
Write(source, "keep source");
NandMove(source, destination, ec);
Require(ec == std::errc::file_exists && Read(source) == "keep source" && Read(destination) == "banner",
"Existing destination must not be overwritten");
NandMove(root / "missing", root / "absent", ec);
Require(bool(ec) && !fs::exists(root / "absent"), "Missing source must fail");
NandMove(source, root / "missing-parent/file", ec);
Require(bool(ec) && Read(source) == "keep source", "Missing parent must preserve source");
fs::create_directory(root / "directory");
Write(root / "directory/child", "child");
NandMove(root / "directory", root / "renamed-directory", ec);
Require(!ec && Read(root / "renamed-directory/child") == "child", "Same-device directory move regressed");
#ifndef _WIN32
fs::create_symlink(root / "missing", root / "same-link");
NandMove(root / "same-link", root / "moved-link", ec);
Require(!ec && fs::is_symlink(fs::symlink_status(root / "moved-link")) &&
!fs::is_symlink(fs::symlink_status(root / "same-link")), "Same-device symlink move regressed");
#endif
#ifdef __linux__
// /dev/shm is a separate tmpfs on ordinary Linux systems, including CI
// and WSL. Fail rather than silently passing without exercising EXDEV.
other = fs::path("/dev/shm") / name;
fs::create_directory(other);
struct stat left{}, right{};
Require(::stat(root.c_str(), &left) == 0 && ::stat(other.c_str(), &right) == 0 && left.st_dev != right.st_dev,
"Cross-device test requires /tmp and /dev/shm on separate filesystems");
CheckCompetingMoves(root, other);
Require(fs::is_empty(other), "Competing moves left staging files behind");
const auto target = other / "banner.bin";
const std::string bytes = std::string(8192, '\0') + "banner payload";
Write(source, bytes);
fs::rename(source, target, ec);
Require(ec == std::errc::cross_device_link, "Fixture must reproduce the original EXDEV failure");
NandMove(source, target, ec);
Require(!ec && !fs::exists(source) && Read(target) == bytes, "Cross-device move must preserve every byte");
Write(source, "do not overwrite");
NandMove(source, target, ec);
Require(ec == std::errc::file_exists && Read(source) == "do not overwrite" && Read(target) == bytes,
"Cross-device move must preserve an existing destination");
fs::remove(target);
fs::create_symlink(other / "missing", target);
NandMove(source, target, ec);
Require(ec == std::errc::file_exists && fs::is_symlink(target) && Read(source) == "do not overwrite",
"Dangling destination symlink must not be replaced");
fs::remove(target);
NandMove(root / "renamed-directory", other / "directory", ec);
Require(ec == std::errc::cross_device_link && Read(root / "renamed-directory/child") == "child" &&
!fs::exists(other / "directory"), "Unsupported directory move must leave source intact");
fs::create_symlink(source, root / "link");
NandMove(root / "link", other / "link", ec);
Require(ec == std::errc::cross_device_link && fs::is_symlink(root / "link") && !fs::exists(other / "link"),
"Cross-device source symlinks must not be dereferenced");
// Force a real write failure after a partial copy without filling disk.
Write(source, bytes);
struct rlimit saved{}, limited{};
Require(getrlimit(RLIMIT_FSIZE, &saved) == 0, "Cannot read file-size limit");
limited = saved;
limited.rlim_cur = 1024;
const auto oldHandler = std::signal(SIGXFSZ, SIG_IGN);
Require(setrlimit(RLIMIT_FSIZE, &limited) == 0, "Cannot set file-size limit");
NandMove(source, target, ec);
const auto copyError = ec;
const auto restored = setrlimit(RLIMIT_FSIZE, &saved);
std::signal(SIGXFSZ, oldHandler);
Require(restored == 0, "Cannot restore file-size limit");
Require(bool(copyError) && Read(source) == bytes && !fs::exists(target) && fs::is_empty(other),
"Failed copy must retain source and remove partial staging files");
Require(geteuid() != 0, "Run permission tests as an unprivileged user");
fs::permissions(root, fs::perms::owner_read | fs::perms::owner_exec);
NandMove(source, target, ec);
fs::permissions(root, fs::perms::owner_all);
Require(bool(ec) && Read(source) == bytes && Read(target) == bytes,
"Failed source deletion must leave both complete copies");
Require(std::distance(fs::directory_iterator(other), fs::directory_iterator{}) == 1,
"Move must clean up staging directory");
fs::remove_all(other);
#endif
fs::remove_all(root);
std::cout << "NAND move tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
std::error_code ignored;
fs::permissions(root, fs::perms::owner_all, ignored);
fs::remove_all(root, ignored);
if (!other.empty()) fs::remove_all(other, ignored);
return 1;
}
}
+6 -10
View File
@@ -1,4 +1,5 @@
#include "nand_save_probe.h"
#include "nand_settings.h"
#include <algorithm>
#include <chrono>
@@ -6,10 +7,6 @@
#include <sstream>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#endif
namespace fs = std::filesystem;
using RuntimeNandSave::ReadAction;
using RuntimeNandSave::Contents;
@@ -50,11 +47,9 @@ int main() {
try {
const auto save = root / "title/00010004/524d4350/data/rksys.dat";
const auto shadow = fs::path(save.native() + fs::path(".nandsafe.tmp").native());
// Save inspection must leave unrelated NAND data alone. Settings
// initialization is covered separately by nand_settings_tests.
const auto settingsPath = root / "title/00000001/00000002/data/setting.txt";
const std::string identity(256, '\x5a');
Write(settingsPath, identity);
std::string error;
Require(RuntimeNandSettings::Ensure(root, error, 1800000123), "New profile settings bootstrap");
const auto identity = Read(RuntimeNandSettings::FilePath(root));
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Fresh profile follows normal missing-file handling");
Require(!fs::exists(save), "Probing fresh profile must not create a save");
@@ -131,7 +126,8 @@ int main() {
SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_NORMAL);
Require(readOnlyResult == ReadAction::Proceed && Read(save) == existing, "Readable read-only save remains available");
#endif
Require(Read(settingsPath) == identity, "Save inspection must not change NAND settings");
Require(RuntimeNandSettings::Ensure(root, error, 1900000123), "Existing profile settings bootstrap");
Require(Read(RuntimeNandSettings::FilePath(root)) == identity, "Save recovery must not change console identity");
fs::remove_all(root);
std::cout << "NAND save startup, preservation, interrupted-write and I/O failure scenarios passed\n";
return 0;
-32
View File
@@ -43,38 +43,6 @@ int main() {
Require(!RuntimeNandSettings::Read(root));
Require(!std::filesystem::exists(root));
std::filesystem::create_directories(path.parent_path());
const auto scratchParent = root / "scratch-collisions";
std::filesystem::create_directories(scratchParent / ".setting-init-fixed-0");
const auto sentinel = scratchParent / ".setting-init-fixed-0" / "setting.txt";
{ std::ofstream output(sentinel); output << "another launch owns this"; }
const auto occupiedFile = scratchParent / ".setting-init-fixed-1";
{ std::ofstream output(occupiedFile); output << "leave this file alone"; }
std::error_code scratchError;
const auto claimed = CreateScratchDirectory(scratchParent, "fixed", scratchError);
Require(claimed && *claimed == scratchParent / ".setting-init-fixed-2" && !scratchError,
"Retry collisions with both existing directories and files");
Require(ReadBytes(sentinel) == "another launch owns this" &&
ReadBytes(occupiedFile) == "leave this file alone", "Never modify another launch's scratch data");
Require(!CreateScratchDirectory(occupiedFile / "not-a-directory", "fixed", scratchError) && scratchError,
"Real filesystem errors must fail rather than retry indefinitely");
// Force all claimants to use the same token; this deterministically
// exercises the collision path even when host clock precision is high.
std::array<std::optional<std::filesystem::path>, 16> claims;
std::vector<std::thread> claimants;
for (size_t i = 0; i < claims.size(); ++i) {
claimants.emplace_back([&, i] {
std::error_code ec;
claims[i] = CreateScratchDirectory(scratchParent, "shared", ec);
});
}
for (auto& claimant : claimants) claimant.join();
for (size_t i = 0; i < claims.size(); ++i) {
Require(claims[i].has_value(), "Every concurrent claimant must acquire a scratch directory");
for (size_t j = 0; j < i; ++j) {
Require(claims[i] != claims[j], "Concurrent claimants must own different scratch directories");
}
}
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
std::array<uint8_t, 256> fixture{};
for (size_t i = 0; i < fixture.size(); ++i) {
-81
View File
@@ -1,81 +0,0 @@
#include "sc_serial_contract.h"
#include "nand_settings.h"
#include <algorithm>
#include <array>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <string>
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static uint32_t ReadWord(const unsigned char* bytes) {
return (uint32_t(bytes[0]) << 24) | (uint32_t(bytes[1]) << 16) |
(uint32_t(bytes[2]) << 8) | uint32_t(bytes[3]);
}
int main(int argc, char** argv) {
try {
// Feed real generator + SC ABI outputs to the upstream bot decoder.
// Usage: mkw_sc_serial_tests --timestamp-vectors <unix-seconds> ...
if (argc > 1 && std::string(argv[1]) == "--timestamp-vectors") {
for (int i = 2; i < argc; ++i) {
const auto timestamp = std::stoll(argv[i]);
const auto serial = RuntimeNandSettings::GenerateSerial(static_cast<std::time_t>(timestamp));
std::array<unsigned char, 4> output{};
Require(RuntimeScSerial::Write(serial, 4,
[](uint32_t address, size_t size) { return address == 4 && size == 4; },
[&](uint32_t, uint32_t value) {
for (unsigned j = 0; j < 4; ++j)
output[j] = static_cast<unsigned char>(value >> (24 - 8 * j));
}) == 1, "Generated serial must pass SC ABI");
std::cout << timestamp << '\t' << serial << "\tLEH"
<< std::setfill('0') << std::setw(9) << ReadWord(output.data()) << '\n';
}
return 0;
}
// Reproduce the reported csnums from the old string-writing override.
const unsigned char old7886[] = {'7', '8', '8', '6'};
const unsigned char old7618[] = {'7', '6', '1', '8'};
Require(ReadWord(old7886) == 926431286, "Reproduce shared LEH926431286");
Require(ReadWord(old7618) == 926298424, "Reproduce shared LEH926298424");
std::array<unsigned char, 16> memory;
size_t available = 4;
unsigned writes = 0;
const auto contains = [&](uint32_t address, size_t size) {
return address == 4 && size <= available;
};
const auto write32 = [&](uint32_t address, uint32_t value) {
++writes;
for (unsigned i = 0; i < 4; ++i)
memory[address + i] = static_cast<unsigned char>(value >> (24 - 8 * i));
};
for (const auto& pair : {std::pair{"788600001", 788600001u}, {"788699999", 788699999u},
{"761800001", 761800001u}, {"761899999", 761899999u},
{"012345678", 12345678u}, {"000000001", 1u}, {"999999999", 999999999u}}) {
memory.fill(0xa5);
writes = 0;
Require(RuntimeScSerial::Write(pair.first, 4, contains, write32) == 1, "Accept an exactly four-byte output buffer");
Require(writes == 1 && ReadWord(memory.data() + 4) == pair.second, "Return full numeric serial, including digits after common prefix");
for (size_t i = 0; i < memory.size(); ++i)
if (i < 4 || i >= 8) Require(memory[i] == 0xa5, "Do not overwrite adjacent guest stack data");
}
for (const char* serial : {"", "1234567890", "7886x1234", "-12345678", "+12345678"}) {
writes = 0;
Require(RuntimeScSerial::Write(serial, 4, contains, write32) == 0 && writes == 0, "Reject malformed serial without a write");
}
writes = 0;
Require(RuntimeScSerial::Write("788600001", 0, contains, write32) == 0 && writes == 0, "Reject null output");
available = 3;
Require(RuntimeScSerial::Write("788600001", 4, contains, write32) == 0 && writes == 0, "Reject undersized output");
std::cout << "SC serial collision reproduction, numeric output and memory-boundary tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+9 -16
View File
@@ -8,14 +8,7 @@
#include <cstdio>
#include <map>
#include <string>
#ifndef MKW_INPUT_EXPR_TEST_CLOCK
#error "Build this test through CMake so the deterministic clock is enabled"
#endif
static std::chrono::steady_clock::time_point g_now{};
namespace InputExpr {
std::chrono::steady_clock::time_point TestClockNow() { return g_now; }
}
#include <thread>
static int g_failures = 0;
static std::map<std::string, double> g_inputs;
@@ -48,7 +41,7 @@ static bool Pressed(const InputExpr::Expression& e) {
return e.Evaluate(Source()) > InputExpr::kConditionThreshold;
}
static void AdvanceTime(int ms) { g_now += std::chrono::milliseconds(ms); }
static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
int main() {
std::printf("Dolphin expression engine\n");
@@ -89,7 +82,7 @@ int main() {
auto hold = Compile("hold(`H`, 0.05)");
g_inputs["H"] = 1.0;
Check(!Pressed(hold), "hold not satisfied immediately");
AdvanceTime(70);
Sleep(70);
Check(Pressed(hold), "hold satisfied after the interval");
g_inputs["H"] = 0.0;
Check(!Pressed(hold), "hold clears on release");
@@ -100,7 +93,7 @@ int main() {
pulse.Evaluate(Source());
g_inputs["P"] = 1.0;
Check(Pressed(pulse), "pulse fires on rising edge");
AdvanceTime(80);
Sleep(80);
Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs.
@@ -109,9 +102,9 @@ int main() {
window.Evaluate(Source());
g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start");
AdvanceTime(90);
Sleep(90);
Check(Pressed(window), "window open between the two pulses");
AdvanceTime(90);
Sleep(90);
Check(!Pressed(window), "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
@@ -121,7 +114,7 @@ int main() {
int low = 0;
for (int i = 0; i < 40; ++i) {
(Pressed(timer) ? high : low)++;
AdvanceTime(5);
Sleep(5);
}
Check(high > 5 && low > 5, "timer alternates high and low");
@@ -137,7 +130,7 @@ int main() {
high = low = 0;
for (int i = 0; i < 60; ++i) {
(Pressed(dolphinLine) ? high : low)++;
AdvanceTime(2);
Sleep(2);
}
Check(high > 5 && low > 5, "LB alternates via timer(0.01)");
@@ -199,7 +192,7 @@ int main() {
auto sm = Compile("smooth(`A`, 0)");
g_inputs["A"] = 1.0;
sm.Evaluate(Source());
AdvanceTime(5);
Sleep(5);
Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite");
}