diff --git a/projects/mkwii-ntsc-u/MAP.txt b/projects/mkwii-ntsc-u/MAP.txt index 27bebf6f4..f8953aa64 100644 --- a/projects/mkwii-ntsc-u/MAP.txt +++ b/projects/mkwii-ntsc-u/MAP.txt @@ -1,6 +1,6 @@ # NTSC-U (RMCE01) function map, ported from ../mkwii/MAP.txt (PAL RMCP01). # Every entry was validated against the RMCE01 main.dol/StaticR.rel. -# Verdicts: PROVEN=12178, REFERENCED=12573, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29409 +# Verdicts: PROVEN=12178, REFERENCED=12583, PLAUSIBLE=4500, IDENTITY=15, INTERPOLATED=0, ADDED=143 (binary-proven entries no PAL entry covers); entries=29419 800018a8 0x800018a8 80001d50 0x80001d50 80001d6c 0x80001d6c @@ -10702,6 +10702,14 @@ 80243e80 nw4r::g3d::ScnRfl::GetTypeName 80243eb0 nw4r::g3d::ScnRfl::IsDerivedFrom 80243f18 0x80243f18 +80243f68 0x80243f68 +80243fa8 0x80243fa8 +80244010 0x80244010 +80244064 0x80244064 +802440a0 0x802440a0 +802440f8 0x802440f8 +802441ac 0x802441ac +802441f0 0x802441f0 802441fc shadTex_802402E0 80244274 EGG::EGG__GlobalScreenCapture_Take 80244760 EGG::EGG__GlobalScreenCapture_Release @@ -20501,6 +20509,7 @@ 806c3c40 Objects::Heyho::vf_0xc 806c3c48 Objects::Heyho::UpdateCollision 806c3d28 Objects::Heyho::GetPropertiesBitfield +806c3d30 0x806c3d30 806c3d38 Objects::HighwayManager::__ct 806c4418 Objects::HighwayManager::__dt 806c44ec 0x806c44ec @@ -21390,6 +21399,7 @@ 806f1e54 Objects::HwanwanBall::LoadRoute 806f1e58 Objects::HwanwanBall::GetSubFileName 806f1e64 Objects::HwanwanBall::GetPropertiesBitfield +806f1e6c Objects::HwanwanBall::__dt(thunk) 806f1e74 0x806f1e74 806f2024 0x806f2024 806f2194 0x806f2194 diff --git a/projects/mkwii-ntsc-u/region_port.json b/projects/mkwii-ntsc-u/region_port.json index caf284525..aac5f3e00 100644 --- a/projects/mkwii-ntsc-u/region_port.json +++ b/projects/mkwii-ntsc-u/region_port.json @@ -33,9 +33,14 @@ ], [ "0x80021BA8", - "0x80225F1C", + "0x80225F14", "0x80021B08" ], + [ + "0x80225F14", + "0x802261F8", + "0x80243F18" + ], [ "0x802261F8", "0x802402E0", @@ -608,13 +613,13 @@ ], [ "0x806CE828", - "0x806D02BC", + "0x806D02C4", "0x806C229C" ], [ - "0x806D02BC", + "0x806D02C4", "0x806D28F4", - "0x806EA19C" + "0x806EA1A4" ], [ "0x806D2908", @@ -673,13 +678,13 @@ ], [ "0x806E95B0", - "0x806EC7C0", + "0x806EC7C8", "0x806EEC5C" ], [ - "0x806EC7C0", + "0x806EC7C8", "0x806ED53C", - "0x806D8564" + "0x806D856C" ], [ "0x806ED53C", diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index 9a277f08f..809f22e76 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -37,7 +37,7 @@ endif() function(mkw_apply_translated_compile_options target) target_compile_options(${target} PRIVATE - -O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe) + -O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -mllvm -hot-cold-split=true -w -pipe) endfunction() # Translated shard TUs are the memory-hungry compiles; everything else in the build is diff --git a/runtime/include/aurora_events.h b/runtime/include/aurora_events.h index 211ebf603..27e84eaeb 100644 --- a/runtime/include/aurora_events.h +++ b/runtime/include/aurora_events.h @@ -4,6 +4,7 @@ #include "settings_overlay.h" #include "runtime_config.h" #include "gx_worker.h" +#include "perf_stats.h" #include #include @@ -142,7 +143,12 @@ inline bool BeginAuroraFrame() { if (GxWorker::ShouldDefer()) { // The window half can touch the surface and render targets the worker draws into. GxWorker::Sync(); - if (!aurora_begin_frame_window()) { + bool begun = false; + { + PerfStats::WaitScope wait; + begun = aurora_begin_frame_window(); + } + if (!begun) { return false; } GxWorker::Submit([] { GxWorkerBeginFrame(); }); diff --git a/runtime/include/boot_race.h b/runtime/include/boot_race.h new file mode 100644 index 000000000..18623e68c --- /dev/null +++ b/runtime/include/boot_race.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// `--race[=]`: boot straight into a 1P VS race instead of the title screen. +// `--race-cpu`: the same, with the local racer driven by the CPU. +namespace BootRace { +// False when `arg` is not a boot-race option; throws on a malformed one. +bool ParseArgument(std::string_view arg); +} // namespace BootRace diff --git a/runtime/include/guest_abi/dispatch.h b/runtime/include/guest_abi/dispatch.h index 841ae697e..dba9ef15e 100644 --- a/runtime/include/guest_abi/dispatch.h +++ b/runtime/include/guest_abi/dispatch.h @@ -291,6 +291,11 @@ struct KnownTypedNativeCpuCall { static constexpr bool kAvailable = false; }; +template +struct IsPureTypedNativeCpuCall : std::false_type {}; +template +struct IsPureTypedNativeCpuCall> : std::bool_constant {}; + #include "guest_abi/native_cpu_calls.inc" // Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs; @@ -554,7 +559,10 @@ inline void InvokeDirectCpu(CpuContext* ctx) { return; } - if constexpr (KnownTypedNativeCpuCall::kAvailable) { + if constexpr (IsPureTypedNativeCpuCall>::value) { + KnownTypedNativeCpuCall::Invoke(cpu); + return; + } else if constexpr (KnownTypedNativeCpuCall::kAvailable) { PpcNonvolatileGprGuard gprGuard(cpu); if (TryGetCpuContext() != cpu) { CpuContextScope scope(cpu); @@ -581,12 +589,20 @@ inline void InvokeDirectCpu(CpuContext* ctx) { // Cold tail of InvokeIndirectJump: a bctr target that is not a registered function. [[noreturn]] MKW_PPC_NO_INLINE MKW_PPC_COLD void ReportMissingIndirectJumpTarget(uint32_t target, CpuContext* cpu); -// Indirect jump (bctr without link) - used for switch tables. -inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) { - // Use the caller's context, not the persistent global context! - // This is essential for tail calls via bctr where the 'this' pointer (r3) - // must be passed correctly to the target function. - CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); +// The common indirect case: an unguarded record called from its own CpuContext, so the full +// TryDispatchRawCpuTarget would only set the execution address and call. +MKW_PPC_FORCE_INLINE bool TryDispatchUnguardedRawCpuTarget(uint32_t target, CpuContext* cpu) { + const RawDispatchRecord* record = TranslatedFunctionRegistry::FindRawByAddressPtr(target); + if (record == nullptr || record->entry == nullptr || record->preserveNonvolatileGprs || + record->nonvolatileFprWriteMask != 0 || TryGetCpuContext() != cpu) { + return false; + } + RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address); + record->entry(cpu); + return true; +} + +MKW_PPC_NO_INLINE inline void InvokeIndirectJumpSlow(uint32_t target, CpuContext* cpu) { if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) { return; } @@ -601,12 +617,22 @@ inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) { ReportMissingIndirectJumpTarget(target, cpu); } -inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) { +// Indirect jump (bctr without link) - used for switch tables. +inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) { + // Use the caller's context, not the persistent global context! + // This is essential for tail calls via bctr where the 'this' pointer (r3) + // must be passed correctly to the target function. CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); + if (TryDispatchUnguardedRawCpuTarget(target, cpu)) { + return; + } + InvokeIndirectJumpSlow(target, cpu); +} + +MKW_PPC_NO_INLINE inline void InvokeIndirectCpuSlow(uint32_t target, CpuContext* cpu) { if (target == 0) { ReportMissingCpuTarget(target, cpu); } - ApplyRuntimeCallOptions(target, cpu); if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) { return; } @@ -615,3 +641,12 @@ inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) { } ReportMissingCpuTarget(target, cpu); } + +inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) { + CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext(); + ApplyRuntimeCallOptions(target, cpu); + if (TryDispatchUnguardedRawCpuTarget(target, cpu)) { + return; + } + InvokeIndirectCpuSlow(target, cpu); +} diff --git a/runtime/include/guest_abi/guest_versions.inc b/runtime/include/guest_abi/guest_versions.inc index 8ea986015..71aaafb1d 100644 --- a/runtime/include/guest_abi/guest_versions.inc +++ b/runtime/include/guest_abi/guest_versions.inc @@ -9,7 +9,8 @@ MKW_REGION_RANGE(NtscU, 8000808C, 8000ADBB, -0x40) MKW_REGION_RANGE(NtscU, 8000AF24, 8000B6B3, -0xA0) MKW_REGION_RANGE(NtscU, 8000B6B4, 8000C173, +0x15994) MKW_REGION_RANGE(NtscU, 8000C174, 80021BA7, -0xB60) -MKW_REGION_RANGE(NtscU, 80021BA8, 80225F1B, -0xA0) +MKW_REGION_RANGE(NtscU, 80021BA8, 80225F13, -0xA0) +MKW_REGION_RANGE(NtscU, 80225F14, 802261F7, +0x1E004) MKW_REGION_RANGE(NtscU, 802261F8, 802402DF, -0x384) MKW_REGION_RANGE(NtscU, 802402E0, 80240E17, +0x3F1C) MKW_REGION_RANGE(NtscU, 80240E18, 80244DD3, -0xEBC) @@ -132,8 +133,8 @@ MKW_REGION_RANGE(NtscU, 806C4EF0, 806C63A7, +0x21CD0) MKW_REGION_RANGE(NtscU, 806C63B0, 806C6B43, +0x21CC8) MKW_REGION_RANGE(NtscU, 806C6B44, 806CCE8F, -0xABF4) MKW_REGION_RANGE(NtscU, 806CCE90, 806CE81F, +0x1B97C) -MKW_REGION_RANGE(NtscU, 806CE828, 806D02BB, -0xC58C) -MKW_REGION_RANGE(NtscU, 806D02BC, 806D28F3, +0x19EE0) +MKW_REGION_RANGE(NtscU, 806CE828, 806D02C3, -0xC58C) +MKW_REGION_RANGE(NtscU, 806D02C4, 806D28F3, +0x19EE0) MKW_REGION_RANGE(NtscU, 806D2908, 806D5C5B, -0xEBD0) MKW_REGION_RANGE(NtscU, 806D5C60, 806D5ED7, +0x16B74) MKW_REGION_RANGE(NtscU, 806D5ED8, 806DA913, -0xEE40) @@ -145,8 +146,8 @@ MKW_REGION_RANGE(NtscU, 806DF7D0, 806DFD13, +0xEBB4) MKW_REGION_RANGE(NtscU, 806DFD14, 806E3A8B, -0x10CB0) MKW_REGION_RANGE(NtscU, 806E3A8C, 806E3E1F, +0xAE3C) MKW_REGION_RANGE(NtscU, 806E3E20, 806E95AF, -0x11044) -MKW_REGION_RANGE(NtscU, 806E95B0, 806EC7BF, +0x56AC) -MKW_REGION_RANGE(NtscU, 806EC7C0, 806ED53B, -0x1425C) +MKW_REGION_RANGE(NtscU, 806E95B0, 806EC7C7, +0x56AC) +MKW_REGION_RANGE(NtscU, 806EC7C8, 806ED53B, -0x1425C) MKW_REGION_RANGE(NtscU, 806ED53C, 806F62FB, -0x14254) MKW_REGION_RANGE(NtscU, 806F62FC, 806F7697, -0x4488) MKW_REGION_RANGE(NtscU, 806F77C4, 806F7A53, +0x1C2B4) diff --git a/runtime/include/guest_abi/native_cpu_calls.inc b/runtime/include/guest_abi/native_cpu_calls.inc index 035c60842..6190bed8d 100644 --- a/runtime/include/guest_abi/native_cpu_calls.inc +++ b/runtime/include/guest_abi/native_cpu_calls.inc @@ -43,6 +43,9 @@ extern "C" void GX__SetNumTevStages_801722a8(uint32_t n); extern "C" void GX__SetBlendMode_8017277c(uint32_t t, uint32_t s, uint32_t d, uint32_t op); extern "C" void GX__LoadPosMtxImm_8017310c(uint32_t ma, uint32_t id); extern "C" void GX__SetCurrentMtx_80173214(uint32_t id); +extern "C" int32_t OS__DisableInterrupts_801a65ac(); +extern "C" int32_t OS__EnableInterrupts_801a65c0(); +extern "C" int32_t OS__RestoreInterrupts_801a65d4(int32_t level); // Keyed by the translated region's addresses, so only translated code sees them; // runtime calls resolve through the registry. @@ -90,6 +93,20 @@ MKW_KNOWN_TYPED_NATIVE(80173214, GX__SetCurrentMtx_80173214, cpu->gpr[3]); #undef MKW_KNOWN_TYPED_NATIVE +// Natives that neither touch CpuContext nor re-enter guest code: no register guard or context scope. +#define MKW_KNOWN_PURE_TYPED_NATIVE_R3(addr, fn, ...) \ +template <> struct KnownTypedNativeCpuCall { \ + static constexpr bool kAvailable = true; \ + static constexpr bool kPure = true; \ + static void Invoke(CpuContext* cpu) { cpu->gpr[3] = static_cast(fn(__VA_ARGS__)); } \ +} + +MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65AC, OS__DisableInterrupts_801a65ac); +MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65C0, OS__EnableInterrupts_801a65c0); +MKW_KNOWN_PURE_TYPED_NATIVE_R3(801A65D4, OS__RestoreInterrupts_801a65d4, static_cast(cpu->gpr[3])); + +#undef MKW_KNOWN_PURE_TYPED_NATIVE_R3 + #define MKW_KNOWN_NATIVE_CPU_CALL(addr, fn) \ template <> struct KnownNativeCpuCall { \ static constexpr bool kAvailable = true; \ diff --git a/runtime/include/perf_stats.h b/runtime/include/perf_stats.h new file mode 100644 index 000000000..4152ec0b9 --- /dev/null +++ b/runtime/include/perf_stats.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +// MKW_PERF_STATS=1: logs the game thread's busy time per presented frame, i.e. wall time minus the +// time it spends blocked on VI pacing, the GX worker or the audio mixer. +namespace PerfStats { + +bool Enabled() noexcept; +void AddWait(std::chrono::steady_clock::duration wait) noexcept; +void OnFramePresented() noexcept; + +class WaitScope { +public: + WaitScope() noexcept : m_start(Enabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}) {} + ~WaitScope() { + if (m_start != std::chrono::steady_clock::time_point{}) { + AddWait(std::chrono::steady_clock::now() - m_start); + } + } + WaitScope(const WaitScope&) = delete; + WaitScope& operator=(const WaitScope&) = delete; + +private: + std::chrono::steady_clock::time_point m_start; +}; + +} // namespace PerfStats diff --git a/runtime/src/hle/audio/ax_mix.cpp b/runtime/src/hle/audio/ax_mix.cpp index c110c6c4a..f95550c2c 100644 --- a/runtime/src/hle/audio/ax_mix.cpp +++ b/runtime/src/hle/audio/ax_mix.cpp @@ -3,6 +3,7 @@ #include "ax_internal.h" #include "abi_bridge.h" +#include "perf_stats.h" #include "audio_backend.h" #include "ax_mix_kernels.h" #include "isa/big_endian.h" @@ -122,6 +123,7 @@ class AXWii { void JoinMix() { if (m_mixThread.joinable()) { + PerfStats::WaitScope wait; std::unique_lock lock(m_mixMutex); m_mixIdle.wait(lock, [this] { return !m_mixPending && !m_mixBusy; }); } diff --git a/runtime/src/hle/boot_race.cpp b/runtime/src/hle/boot_race.cpp new file mode 100644 index 000000000..dd5228e35 --- /dev/null +++ b/runtime/src/hle/boot_race.cpp @@ -0,0 +1,216 @@ +#include "boot_race.h" + +#include "abi_bridge.h" +#include "guest_abi/guest_region.h" +#include "guest_function_imports.h" +#include "memory.h" +#include "runtime_log.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::string_view kOption = "--race"; +constexpr uint32_t kLuigiCircuit = 0x08; +constexpr uint32_t kLastRaceCourse = 0x1F; + +// Racedata (menu scenario, copied into the race scenario by Racedata::InitRace) +constexpr uint32_t kMenuScenarioOffset = 0xC10; +constexpr uint32_t kPlayersOffset = 0x8; +constexpr uint32_t kPlayerSize = 0xF0; +constexpr uint32_t kPlayerKartOffset = 0x8; +constexpr uint32_t kPlayerCharacterOffset = 0xC; +constexpr uint32_t kPlayerTypeOffset = 0x10; +constexpr uint32_t kPlayerControllerChannelOffset = 0x6; +constexpr uint32_t kPlayerCount = 12; +constexpr uint32_t kSettingsOffset = 0xB48; + +constexpr uint32_t kSectionPadOffset = 0x34; +constexpr uint32_t kFirstGcnControllerId = 0x124; +constexpr uint32_t kSectionP1Vs = 0x20; + +enum : uint32_t { kPlayerRealLocal = 0, kPlayerCpu = 1 }; +enum : uint32_t { kGameModeVs = 1, kCc150 = 2, kCpuHard = 2, kItemsBalanced = 0 }; + +struct Racer { + uint32_t character; + uint32_t kart; +}; + +// Standard kart matching each character's weight class. +constexpr std::array kRacers{{ + {0x00, 0x01}, // Mario + {0x07, 0x01}, // Luigi + {0x10, 0x01}, // Peach + {0x0A, 0x01}, // Yoshi + {0x0F, 0x01}, // Daisy + {0x08, 0x00}, // Toad + {0x06, 0x00}, // Baby Mario + {0x0E, 0x00}, // Koopa Troopa + {0x03, 0x02}, // Bowser + {0x09, 0x02}, // Donkey Kong + {0x0B, 0x02}, // Wario + {0x16, 0x02}, // Funky Kong +}}; + +std::optional g_course; +bool g_cpuDriven = false; + +uint32_t ParseCourse(std::string_view text) { + int base = 10; + if (text.starts_with("0x") || text.starts_with("0X")) { + text.remove_prefix(2); + base = 16; + } + uint32_t course = 0; + const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), course, base); + if (text.empty() || ec != std::errc{} || end != text.data() + text.size() || course > kLastRaceCourse) { + throw std::invalid_argument("--race expects a race course id from 0 to 0x1F"); + } + return course; +} + +void SetUpMenuScenario(uint32_t course) { + const uint32_t racedata = Memory::Read32(MKW_GADDR(809BD728)); + if (racedata == 0) { + throw std::runtime_error("--race: Racedata does not exist yet at SectionMgr::Init"); + } + const uint32_t scenario = racedata + kMenuScenarioOffset; + for (uint32_t i = 0; i < kRacers.size(); ++i) { + const uint32_t player = scenario + kPlayersOffset + i * kPlayerSize; + Memory::Write32(player + kPlayerKartOffset, kRacers[i].kart); + Memory::Write32(player + kPlayerCharacterOffset, kRacers[i].character); + Memory::Write32(player + kPlayerTypeOffset, i == 0 ? kPlayerRealLocal : kPlayerCpu); + } + const uint32_t settings = scenario + kSettingsOffset; + Memory::Write32(settings + 0x00, course); + Memory::Write32(settings + 0x04, kCc150); + Memory::Write32(settings + 0x08, kGameModeVs); + Memory::Write32(settings + 0x0C, 0); + Memory::Write32(settings + 0x14, kCpuHard); + Memory::Write32(settings + 0x18, kItemsBalanced); + Memory::Write8(settings + 0x24, 0); + Memory::Write8(settings + 0x25, 3); + Memory::Write32(settings + 0x28, 0); +} + +} // namespace + +namespace BootRace { + +bool ParseArgument(std::string_view arg) { + if (arg == "--race-cpu") { + g_cpuDriven = true; + if (!g_course) { + g_course = kLuigiCircuit; + } + return true; + } + if (!arg.starts_with(kOption)) { + return false; + } + const std::string_view rest = arg.substr(kOption.size()); + if (rest.empty()) { + g_course = kLuigiCircuit; + } else if (rest.front() == '=') { + g_course = ParseCourse(rest.substr(1)); + } else { + return false; + } + return true; +} + +} // namespace BootRace + +// ArgumentParser::parse, picks the boot section in SectionMgr::Init. +extern "C" void ArgumentParser_Parse_805b9344(CpuContext* ctx) { + static bool consumed = false; + if (!g_course || consumed) { + MKW_GUEST_FUNC(805B9344)(ctx); + return; + } + consumed = true; + SetUpMenuScenario(*g_course); + + if (!g_cpuDriven) { + const uint32_t sectionMgr = Memory::Read32(MKW_GADDR(809C1E38)); + if (sectionMgr == 0) { + throw std::runtime_error("--race: SectionMgr instance is not set at SectionMgr::Init"); + } + ctx->gpr[3] = sectionMgr + kSectionPadOffset; + ctx->gpr[4] = 0; + ctx->gpr[5] = kFirstGcnControllerId; + MKW_GUEST_FUNC(8061B490)(ctx); + } + + RT_LOGF("boot-race", "booting into 1P VS on course 0x%02X%s\n", *g_course, g_cpuDriven ? " (CPU driven)" : ""); + ctx->gpr[3] = kSectionP1Vs; +} +REGISTER_NATIVE_FUNCTION_AS(0x805B9344, ArgumentParser_Parse_805b9344, "ArgumentParser_Parse_805b9344"); + +// --race-cpu hands the local racer to the CPU driver while it keeps its camera and HUD. + +// RacedataScenario::InitScreens; local racers keep no real controller channel. +extern "C" void RacedataScenario_InitScreens_8052f4e8(CpuContext* ctx) { + if (!g_cpuDriven) { + MKW_GUEST_FUNC(8052F4E8)(ctx); + return; + } + const uint32_t players = ctx->gpr[3] + kPlayersOffset; + std::array channels{}; + for (uint32_t i = 0; i < kPlayerCount; ++i) { + channels[i] = Memory::Read8(players + i * kPlayerSize + kPlayerControllerChannelOffset); + } + MKW_GUEST_FUNC(8052F4E8)(ctx); + for (uint32_t i = 0; i < kPlayerCount; ++i) { + const uint32_t player = players + i * kPlayerSize; + if (Memory::Read32(player + kPlayerTypeOffset) == kPlayerRealLocal) { + Memory::Write8(player + kPlayerControllerChannelOffset, channels[i]); + } + } +} +REGISTER_NATIVE_FUNCTION_AS(0x8052F4E8, RacedataScenario_InitScreens_8052f4e8, "RacedataScenario_InitScreens_8052f4e8"); + +// KartAIController::IsCPU +extern "C" void KartAIController_IsCPU_8072624c(CpuContext* ctx) { + MKW_GUEST_FUNC(8072624C)(ctx); + if (g_cpuDriven) { + ctx->gpr[3] = 1; + } +} +REGISTER_NATIVE_FUNCTION_AS(0x8072624C, KartAIController_IsCPU_8072624c, "KartAIController_IsCPU_8072624c"); + +// Kart::Link::IsCPU; bit 0 marks a CPU racer, bit 1 the local one. +extern "C" void KartLink_IsCPU_80590664(CpuContext* ctx) { + MKW_GUEST_FUNC(80590664)(ctx); + if (g_cpuDriven) { + ctx->gpr[3] = ctx->gpr[0] & 3; + } +} +REGISTER_NATIVE_FUNCTION_AS(0x80590664, KartLink_IsCPU_80590664, "KartLink_IsCPU_80590664"); + +// RacedataScenario::InitRNG; fixed seeds keep --race-cpu runs repeatable for benchmarking. +extern "C" void RacedataScenario_InitRng_8052f924(CpuContext* ctx) { + const uint32_t scenario = ctx->gpr[3]; + MKW_GUEST_FUNC(8052F924)(ctx); + if (g_cpuDriven) { + Memory::Write32(scenario + kSettingsOffset + 0x2C, 0x74A1B095u); + Memory::Write32(scenario + kSettingsOffset + 0x30, 0x1234567u); + } +} +REGISTER_NATIVE_FUNCTION_AS(0x8052F924, RacedataScenario_InitRng_8052f924, "RacedataScenario_InitRng_8052f924"); + +// SectionPad::GetFirstDeactivatedPad; a CPU driven racer needs no controller. +extern "C" void SectionPad_GetFirstDeactivatedPad_8061b9bc(CpuContext* ctx) { + MKW_GUEST_FUNC(8061B9BC)(ctx); + if (g_cpuDriven) { + ctx->gpr[3] = static_cast(-1); + } +} +REGISTER_NATIVE_FUNCTION_AS(0x8061B9BC, SectionPad_GetFirstDeactivatedPad_8061b9bc, + "SectionPad_GetFirstDeactivatedPad_8061b9bc"); diff --git a/runtime/src/hle/gx/gx_worker.cpp b/runtime/src/hle/gx/gx_worker.cpp index 717ebc716..ac8983e05 100644 --- a/runtime/src/hle/gx/gx_worker.cpp +++ b/runtime/src/hle/gx/gx_worker.cpp @@ -1,5 +1,6 @@ #include "gx_worker.h" +#include "perf_stats.h" #include "runtime_config.h" #include "runtime_log.h" @@ -84,12 +85,37 @@ void WaitTailBriefly(std::unique_lock& lock) { #endif } +// Producer-only: the last write position handed to the worker. Publishing every record kept the +// head cache line bouncing between the game thread and the polling worker. +uint64_t g_published = 0; +constexpr uint64_t kPublishBytes = 2048; + +void Publish() { + if (g_published == g_write) { + return; + } + g_published = g_write; + g_head.store(g_write, std::memory_order_seq_cst); + if (g_consumerWaiting.load(std::memory_order_seq_cst)) { + std::lock_guard lock{g_headMutex}; + g_headCv.notify_one(); + } +} + void WaitForTail(uint64_t minTail, bool service) { - for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) { - CpuRelax(); + if (g_tail.load(std::memory_order_acquire) >= minTail) { + return; + } + Publish(); + { + PerfStats::WaitScope wait; + for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) { + CpuRelax(); + } } while (g_tail.load(std::memory_order_acquire) < minTail) { { + PerfStats::WaitScope wait; std::unique_lock lock{g_tailMutex}; g_producerWaiting.store(true, std::memory_order_seq_cst); if (g_tail.load(std::memory_order_seq_cst) < minTail) { @@ -103,9 +129,19 @@ void WaitForTail(uint64_t minTail, bool service) { } } +// Producer-only copy of a tail already observed; avoids reading the worker-written line per record. +uint64_t g_tailSeen = 0; + void WaitForSpace(size_t bytes) { if (g_write + bytes > kCapacity) { - WaitForTail(g_write + bytes - kCapacity, false); + const uint64_t minTail = g_write + bytes - kCapacity; + if (g_tailSeen >= minTail) { + return; + } + g_tailSeen = g_tail.load(std::memory_order_acquire); + if (g_tailSeen < minTail) { + WaitForTail(minTail, false); + } } } @@ -234,11 +270,10 @@ void* Reserve(size_t payloadSize, void (*run)(void*)) { return g_ring + offset + kHeaderSize; } +// Anything that waits on the worker publishes first (WaitForTail), so batching here only delays it. void Commit() { - g_head.store(g_write, std::memory_order_seq_cst); - if (g_consumerWaiting.load(std::memory_order_seq_cst)) { - std::lock_guard lock{g_headMutex}; - g_headCv.notify_one(); + if (g_write - g_published >= kPublishBytes) { + Publish(); } } @@ -333,6 +368,7 @@ void Stop() { detail::g_active.store(false, std::memory_order_release); g_stop.store(true, std::memory_order_release); Submit([] {}); + Publish(); if (g_thread != nullptr) { g_thread->join(); delete g_thread; diff --git a/runtime/src/hle/vi.cpp b/runtime/src/hle/vi.cpp index e0a160d03..cd236a977 100644 --- a/runtime/src/hle/vi.cpp +++ b/runtime/src/hle/vi.cpp @@ -8,6 +8,7 @@ #include "fiber_manager.h" #include "platform/host_platform.h" #include "runtime_log.h" +#include "perf_stats.h" #include @@ -161,6 +162,7 @@ constexpr std::chrono::microseconds kFinalSpinWindow{500}; void SleepPreciselyUntil(Clock::time_point deadline, bool finishWithSpin = false, std::chrono::microseconds spinWindow = 750us) { + PerfStats::WaitScope wait; const auto now = Clock::now(); if (now >= deadline) { return; @@ -584,6 +586,7 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) { s_lastPacedRetraceCount = g_vi.retraceCount; } settings_overlay::AdvancePresentedFrame(); + PerfStats::OnFramePresented(); g_auroraFrameActive.store(false, std::memory_order_release); g_auroraFrameHadWork.store(false, std::memory_order_release); if (presentedXfb) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index ae85098fd..929723fd3 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -57,6 +57,8 @@ #endif #include "abi_bridge.h" +#include "boot_race.h" +#include "guest_abi/guest_region.h" #include "guest_flat_memory.h" #include "gx_guest_write.h" #include "memory.h" @@ -696,6 +698,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails = out << "[runtime] fatal log" << std::endl; out << "[runtime] reason: " << reason << std::endl; out << "[runtime] timestamp(seconds): " << nowSecs << std::endl; + out << "[runtime] game: " << GuestRegion::CurrentFacts().gameId << std::endl; out << "[runtime] entry: " << (g_lastEntryLabel.empty() ? "" : g_lastEntryLabel) << std::endl; if (!extraDetails.empty()) { out << "[runtime] details: " << extraDetails << std::endl; @@ -1343,8 +1346,11 @@ int RuntimeMain(int argc, char** argv) { std::string currentEntryLabel; try { - if (argc != 1) { - throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host."); + for (int i = 1; i < argc; ++i) { + if (!BootRace::ParseArgument(argv[i])) { + throw std::invalid_argument(std::string("Unknown command-line option '") + argv[i] + + "'; the game runtime only accepts --race[=] and --race-cpu, use Config.toml through the installed host."); + } } RuntimeConfigFile::LogLoadedConfig(); if (RuntimeConfigFile::DiscordPresenceEnabled()) { diff --git a/runtime/src/perf_stats.cpp b/runtime/src/perf_stats.cpp new file mode 100644 index 000000000..ee09b0c89 --- /dev/null +++ b/runtime/src/perf_stats.cpp @@ -0,0 +1,67 @@ +#include "perf_stats.h" + +#include "runtime_log.h" + +#include +#include +#include +#include + +namespace PerfStats { +namespace { + +using Clock = std::chrono::steady_clock; + +constexpr size_t kWindow = 240; + +thread_local Clock::duration t_wait{}; + +struct FrameWindow { + Clock::time_point last{}; + std::array busyMs{}; + size_t count = 0; +}; + +FrameWindow g_window; + +} // namespace + +bool Enabled() noexcept { + static const bool enabled = [] { + const char* value = std::getenv("MKW_PERF_STATS"); + return value != nullptr && std::strcmp(value, "0") != 0; + }(); + return enabled; +} + +void AddWait(Clock::duration wait) noexcept { t_wait += wait; } + +void OnFramePresented() noexcept { + if (!Enabled()) { + return; + } + const auto now = Clock::now(); + const Clock::duration wait = t_wait; + t_wait = {}; + if (g_window.last == Clock::time_point{}) { + g_window.last = now; + return; + } + const auto busy = (now - g_window.last) - wait; + g_window.last = now; + g_window.busyMs[g_window.count++] = std::chrono::duration(busy).count(); + if (g_window.count < kWindow) { + return; + } + std::array sorted = g_window.busyMs; + std::sort(sorted.begin(), sorted.end()); + double sum = 0; + for (const double ms : sorted) { + sum += ms; + } + RT_LOGF("perf", "busy ms/frame over %zu frames: avg %.3f median %.3f p95 %.3f max %.3f\n", kWindow, + sum / kWindow, sorted[kWindow / 2], sorted[kWindow * 95 / 100], sorted[kWindow - 1]); + g_window.count = 0; +} + +} // namespace PerfStats