mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-25 23:34:34 -04:00
let wrapper natives run mod overlays (#359)
This commit is contained in:
@@ -157,7 +157,7 @@ public:
|
||||
#define MKW_DETAIL_CAT(a, b) a##b
|
||||
#define MKW_DETAIL_MAKE_UNIQUE(a, b) MKW_DETAIL_CAT(a, b)
|
||||
|
||||
// Three hand-written registration macros, given the PAL address. Generated code never registers
|
||||
// Hand-written registration macros, given the PAL address. Generated code never registers
|
||||
// per function; every translated function reaches the registry via the bulk
|
||||
// BulkTranslatedFunctionRecord tables in the translator's *_registration TUs.
|
||||
#define REGISTER_TRANSLATED_FUNCTION(address, fn) \
|
||||
@@ -168,3 +168,9 @@ public:
|
||||
|
||||
#define REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name) \
|
||||
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(::GuestRegion::Translate(address), pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
|
||||
|
||||
// A native that calls through with MKW_GUEST_NEXT, so a mod overlay at its address still runs.
|
||||
#define REGISTER_NATIVE_WRAPPER_AS(address, fn, pretty_name) REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name)
|
||||
|
||||
// A native that deliberately takes over its address from any mod overlay there.
|
||||
#define REGISTER_NATIVE_REPLACEMENT_AS(address, fn, pretty_name) REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// Base translations the runtime calls directly, named by PAL identity and resolved through the
|
||||
// function registry on first use.
|
||||
// MKW_GUEST_FUNC(801A0620)(ctx);
|
||||
// A native registered with REGISTER_NATIVE_WRAPPER_AS calls through with MKW_GUEST_NEXT instead,
|
||||
// so a mod overlay at the same address still runs.
|
||||
|
||||
#include "guest_abi/cpu_context.h"
|
||||
#include "guest_abi/guest_region.h"
|
||||
@@ -12,6 +14,10 @@ using Entry = void (*)(CpuContext*);
|
||||
|
||||
// The base translation at a guest address, also where a native or mod overrides it.
|
||||
Entry BaseTranslation(uint32_t address);
|
||||
|
||||
// The translation a native wrapper sits on top of: the winning mod overlay, else the base
|
||||
// translation.
|
||||
Entry NextTranslation(uint32_t address);
|
||||
} // namespace GuestFunctions
|
||||
|
||||
#define MKW_GUEST_FUNC(pal) \
|
||||
@@ -19,3 +25,9 @@ Entry BaseTranslation(uint32_t address);
|
||||
static const ::GuestFunctions::Entry entry = ::GuestFunctions::BaseTranslation(MKW_GADDR(pal)); \
|
||||
return entry; \
|
||||
}())
|
||||
|
||||
#define MKW_GUEST_NEXT(pal) \
|
||||
(*[] { \
|
||||
static const ::GuestFunctions::Entry entry = ::GuestFunctions::NextTranslation(MKW_GADDR(pal)); \
|
||||
return entry; \
|
||||
}())
|
||||
|
||||
+41
-14
@@ -426,20 +426,10 @@ const TranslatedFunctionInfo* TranslatedFunctionRegistry::FindByAddressPtrSlow(u
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
|
||||
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
|
||||
if (!RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
lock.lock();
|
||||
}
|
||||
for (const auto& entry : Registry()) {
|
||||
if (entry.address == address && entry.kind == FunctionKind::BaseTranslated && entry.rawCpuInvoker) {
|
||||
return entry.rawCpuInvoker;
|
||||
}
|
||||
}
|
||||
if (lock.owns_lock()) {
|
||||
lock.unlock();
|
||||
}
|
||||
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no base translation registered at 0x" << std::hex << address << std::dec
|
||||
namespace {
|
||||
|
||||
[[noreturn]] void ReportMissingGuestFunction(uint32_t address) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "ERROR: no translation registered at 0x" << std::hex << address << std::dec
|
||||
<< std::endl;
|
||||
std::ostringstream message;
|
||||
message << "The runtime calls the translated function at guest address 0x" << std::hex << address
|
||||
@@ -451,6 +441,43 @@ GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GuestFunctions::Entry GuestFunctions::BaseTranslation(uint32_t address) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
|
||||
if (!RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
lock.lock();
|
||||
}
|
||||
for (const auto& entry : Registry()) {
|
||||
if (entry.address == address && entry.kind == FunctionKind::BaseTranslated && entry.rawCpuInvoker) {
|
||||
return entry.rawCpuInvoker;
|
||||
}
|
||||
}
|
||||
}
|
||||
ReportMissingGuestFunction(address);
|
||||
}
|
||||
|
||||
GuestFunctions::Entry GuestFunctions::NextTranslation(uint32_t address) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(RegistryMutex(), std::defer_lock);
|
||||
if (!RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
lock.lock();
|
||||
}
|
||||
const TranslatedFunctionInfo* best = nullptr;
|
||||
for (const auto& entry : Registry()) {
|
||||
if (entry.address == address && entry.kind != FunctionKind::Native && entry.rawCpuInvoker &&
|
||||
(!best || IsBetterCandidate(entry, *best))) {
|
||||
best = &entry;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
return best->rawCpuInvoker;
|
||||
}
|
||||
}
|
||||
ReportMissingGuestFunction(address);
|
||||
}
|
||||
|
||||
const RawDispatchRecord* TranslatedFunctionRegistry::FindRawByAddressPtrSlow(uint32_t address) {
|
||||
if (!RegistryFrozen().load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
|
||||
@@ -162,7 +162,7 @@ REGISTER_NATIVE_FUNCTION_AS(0x805B9344, ArgumentParser_Parse_805b9344, "Argument
|
||||
// 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);
|
||||
MKW_GUEST_NEXT(8052F4E8)(ctx);
|
||||
return;
|
||||
}
|
||||
const uint32_t players = ctx->gpr[3] + kPlayersOffset;
|
||||
@@ -170,7 +170,7 @@ extern "C" void RacedataScenario_InitScreens_8052f4e8(CpuContext* ctx) {
|
||||
for (uint32_t i = 0; i < kPlayerCount; ++i) {
|
||||
channels[i] = Memory::Read8(players + i * kPlayerSize + kPlayerControllerChannelOffset);
|
||||
}
|
||||
MKW_GUEST_FUNC(8052F4E8)(ctx);
|
||||
MKW_GUEST_NEXT(8052F4E8)(ctx);
|
||||
for (uint32_t i = 0; i < kPlayerCount; ++i) {
|
||||
const uint32_t player = players + i * kPlayerSize;
|
||||
if (Memory::Read32(player + kPlayerTypeOffset) == kPlayerRealLocal) {
|
||||
@@ -178,7 +178,7 @@ extern "C" void RacedataScenario_InitScreens_8052f4e8(CpuContext* ctx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
REGISTER_NATIVE_FUNCTION_AS(0x8052F4E8, RacedataScenario_InitScreens_8052f4e8, "RacedataScenario_InitScreens_8052f4e8");
|
||||
REGISTER_NATIVE_WRAPPER_AS(0x8052F4E8, RacedataScenario_InitScreens_8052f4e8, "RacedataScenario_InitScreens_8052f4e8");
|
||||
|
||||
// KartAIController::IsCPU
|
||||
extern "C" void KartAIController_IsCPU_8072624c(CpuContext* ctx) {
|
||||
|
||||
@@ -436,7 +436,7 @@ extern "C" void NAND_IOS_OpenBody_HLE_801938FC(CpuContext* ctx) {
|
||||
ctx->gpr[3] = static_cast<uint32_t>(result);
|
||||
ctx->gpr[1] = ctx->gpr[1] + 32u;
|
||||
}
|
||||
REGISTER_NATIVE_FUNCTION_AS(0x801938FC, NAND_IOS_OpenBody_HLE_801938FC, "NAND_IOS_OpenBody_HLE_801938FC");
|
||||
REGISTER_NATIVE_REPLACEMENT_AS(0x801938FC, NAND_IOS_OpenBody_HLE_801938FC, "NAND_IOS_OpenBody_HLE_801938FC");
|
||||
|
||||
extern "C" int32_t NAND_IOS_Close_HLE(uint32_t fd) {
|
||||
if (fd == ISFS_DEV_FD) {
|
||||
|
||||
@@ -2278,12 +2278,21 @@ void WriteResolvedDispatchProfile(
|
||||
// functions the runtime must own (hardware, the strap scene). But when the mod's whole point
|
||||
// is replacing that function, e.g. Retro Rewind's DVDOpen override at 0x8015E2BC, the overlay
|
||||
// is silently dropped. Name every such address so the tradeoff is a choice, not an accident.
|
||||
// Wrapper natives still run the overlay, and replacement natives already made that choice.
|
||||
var nativesKeepingModOverlayDecision = runtimeNativeIndex.Value.Registrations
|
||||
.Where(static registration => registration.ModOverlay != NativeModOverlay.Unspecified)
|
||||
.Select(static registration => (registration.Address, registration.Symbol))
|
||||
.ToHashSet();
|
||||
var nativeOverriddenModPatches = groups
|
||||
.Where(group =>
|
||||
group.Any(entry => entry.Kind == "rr") &&
|
||||
group.OrderByDescending(entry => entry.Priority)
|
||||
.ThenByDescending(entry => KindRank(entry.Kind))
|
||||
.First().Kind == "native")
|
||||
{
|
||||
var winner = group.OrderByDescending(entry => entry.Priority)
|
||||
.ThenByDescending(entry => KindRank(entry.Kind))
|
||||
.First();
|
||||
return group.Any(entry => entry.Kind == "rr") &&
|
||||
winner.Kind == "native" &&
|
||||
!nativesKeepingModOverlayDecision.Contains((winner.Address, winner.Symbol));
|
||||
})
|
||||
.Select(group => new
|
||||
{
|
||||
Address = group.Key,
|
||||
|
||||
@@ -83,9 +83,11 @@ public static partial class GeneratedMarkers
|
||||
|
||||
/// <summary>
|
||||
/// <c>REGISTER_NATIVE_FUNCTION(0x…, symbol)</c> and its <c>_AS</c> alias form;
|
||||
/// the <c>as</c> group tells the two apart.
|
||||
/// the <c>as</c> group tells the two apart. <c>REGISTER_NATIVE_WRAPPER_AS</c> and
|
||||
/// <c>REGISTER_NATIVE_REPLACEMENT_AS</c> are alias forms too, named by the
|
||||
/// <c>modOverlay</c> group.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"REGISTER_NATIVE_FUNCTION(?<as>_AS)?\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
[GeneratedRegex(@"REGISTER_NATIVE_(?:FUNCTION(?<as>_AS)?|(?<modOverlay>WRAPPER|REPLACEMENT)_AS)\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex NativeFunctionRegistrationPattern();
|
||||
|
||||
[GeneratedRegex(@"REGISTER_TRANSLATED_FUNCTION\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
|
||||
@@ -11,7 +11,19 @@ public sealed record RuntimeNativeRegistration(
|
||||
string Symbol,
|
||||
string SourceFile,
|
||||
bool IsTranslatedOverride,
|
||||
bool ExcludesBaseTranslation);
|
||||
bool ExcludesBaseTranslation,
|
||||
NativeModOverlay ModOverlay = NativeModOverlay.Unspecified);
|
||||
|
||||
/// <summary>What happens to a mod overlay at a native's address.</summary>
|
||||
public enum NativeModOverlay
|
||||
{
|
||||
/// <summary>Plain registration: the overlay is lost, and reported as such.</summary>
|
||||
Unspecified,
|
||||
/// <summary>The native calls through to the overlay (<c>MKW_GUEST_NEXT</c>).</summary>
|
||||
Wrapper,
|
||||
/// <summary>The native deliberately takes over the overlay's address.</summary>
|
||||
Replacement
|
||||
}
|
||||
|
||||
public sealed record RuntimeNativeAbiEntry(
|
||||
uint Address,
|
||||
@@ -119,7 +131,17 @@ public static class RuntimeNativeIndexBuilder
|
||||
{
|
||||
var source = sourceFile.Content;
|
||||
foreach (Match match in GeneratedMarkers.NativeFunctionRegistrationPattern().Matches(source))
|
||||
Add(match, match.Groups["symbol"].Value, false, !match.Groups["as"].Success);
|
||||
{
|
||||
var modOverlay = match.Groups["modOverlay"];
|
||||
Add(match, match.Groups["symbol"].Value, false,
|
||||
!match.Groups["as"].Success && !modOverlay.Success,
|
||||
modOverlay.Value switch
|
||||
{
|
||||
"WRAPPER" => NativeModOverlay.Wrapper,
|
||||
"REPLACEMENT" => NativeModOverlay.Replacement,
|
||||
_ => NativeModOverlay.Unspecified
|
||||
});
|
||||
}
|
||||
foreach (Match match in GeneratedMarkers.TranslatedFunctionRegistrationPattern().Matches(source))
|
||||
Add(match, match.Groups["symbol"].Value, true, true);
|
||||
foreach (Match match in GeneratedMarkers.NativeOverridePattern().Matches(source))
|
||||
@@ -127,10 +149,11 @@ public static class RuntimeNativeIndexBuilder
|
||||
foreach (Match match in GeneratedMarkers.FatalStubPattern().Matches(source))
|
||||
Add(match, $"GX_FATAL_STUB_{match.Groups["address"].Value}", false, true);
|
||||
|
||||
void Add(Match match, string symbol, bool translated, bool excludesBase) =>
|
||||
void Add(Match match, string symbol, bool translated, bool excludesBase,
|
||||
NativeModOverlay modOverlay = NativeModOverlay.Unspecified) =>
|
||||
registrations.Add(new RuntimeNativeRegistration(
|
||||
ParseAddress(match.Groups["address"].Value), symbol, sourceFile.RelativePath,
|
||||
translated, excludesBase));
|
||||
translated, excludesBase, modOverlay));
|
||||
}
|
||||
|
||||
return registrations
|
||||
@@ -139,7 +162,8 @@ public static class RuntimeNativeIndexBuilder
|
||||
.ThenBy(static registration => registration.Symbol, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.IsTranslatedOverride)
|
||||
.ThenBy(static registration => registration.ExcludesBaseTranslation);
|
||||
.ThenBy(static registration => registration.ExcludesBaseTranslation)
|
||||
.ThenBy(static registration => registration.ModOverlay);
|
||||
}
|
||||
|
||||
private static uint ParseAddress(string value) => GuestTargetParser.ParseHexAddress(value);
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace Translator.Core;
|
||||
public static class RuntimeNativeIndexFile
|
||||
{
|
||||
public const string Format = "wiicompiled-runtime-native-index";
|
||||
public const int FormatVersion = 1;
|
||||
public const int FormatVersion = 2;
|
||||
// Bump whenever the scanner or the ABI/effect analyzers can produce different output for the same sources.
|
||||
public const int ExtractorVersion = 1;
|
||||
public const int ExtractorVersion = 2;
|
||||
public const string DefaultFileName = "native-index.json";
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
@@ -59,13 +59,15 @@ public static class RuntimeNativeIndexFile
|
||||
.ThenBy(static registration => registration.SourceFile, StringComparer.Ordinal)
|
||||
.ThenBy(static registration => registration.IsTranslatedOverride)
|
||||
.ThenBy(static registration => registration.ExcludesBaseTranslation)
|
||||
.ThenBy(static registration => registration.ModOverlay)
|
||||
.Select(static registration => new RegistrationDto
|
||||
{
|
||||
Address = FormatAddress(registration.Address),
|
||||
Symbol = registration.Symbol,
|
||||
SourceFile = registration.SourceFile,
|
||||
IsTranslatedOverride = registration.IsTranslatedOverride,
|
||||
ExcludesBaseTranslation = registration.ExcludesBaseTranslation
|
||||
ExcludesBaseTranslation = registration.ExcludesBaseTranslation,
|
||||
ModOverlay = registration.ModOverlay.ToString()
|
||||
})
|
||||
.ToList(),
|
||||
VoidStubAbis = index.VoidStubAbis
|
||||
@@ -160,7 +162,8 @@ public static class RuntimeNativeIndexFile
|
||||
Present(registration.Symbol, "symbol"),
|
||||
Present(registration.SourceFile, "sourceFile"),
|
||||
registration.IsTranslatedOverride,
|
||||
registration.ExcludesBaseTranslation))
|
||||
registration.ExcludesBaseTranslation,
|
||||
ParseModOverlay(Present(registration.ModOverlay, "modOverlay"))))
|
||||
.ToArray(),
|
||||
Present(document.VoidStubAbis, "voidStubAbis")
|
||||
.Select(static entry => new RuntimeNativeAbiEntry(
|
||||
@@ -185,6 +188,11 @@ public static class RuntimeNativeIndexFile
|
||||
|
||||
private static string FormatAddress(uint address) => $"0x{address:X8}";
|
||||
|
||||
private static NativeModOverlay ParseModOverlay(string text) =>
|
||||
Enum.TryParse<NativeModOverlay>(text, ignoreCase: false, out var value) && Enum.IsDefined(value)
|
||||
? value
|
||||
: throw new FormatException($"Unknown modOverlay '{text}'.");
|
||||
|
||||
private static uint ParseAddress(string text) => GuestTargetParser.ParseHexAddress(text);
|
||||
|
||||
private static T Present<T>(T? value, string name) where T : class =>
|
||||
@@ -244,6 +252,7 @@ public static class RuntimeNativeIndexFile
|
||||
public required string SourceFile { get; init; }
|
||||
public required bool IsTranslatedOverride { get; init; }
|
||||
public required bool ExcludesBaseTranslation { get; init; }
|
||||
public required string ModOverlay { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AbiDto
|
||||
|
||||
@@ -129,7 +129,7 @@ public sealed class GuestAddressTableTests
|
||||
var calls = Directory.EnumerateFiles(Path.Combine(root, "runtime"), "*.*", SearchOption.AllDirectories)
|
||||
.Where(path => (path.EndsWith(".cpp", StringComparison.Ordinal) || path.EndsWith(".h", StringComparison.Ordinal)) &&
|
||||
!path.Contains(thirdParty, StringComparison.Ordinal))
|
||||
.SelectMany(static path => Regex.Matches(File.ReadAllText(path), @"MKW_GUEST_FUNC\(\s*([0-9A-Fa-f]{8})\s*\)")
|
||||
.SelectMany(static path => Regex.Matches(File.ReadAllText(path), @"MKW_GUEST_(?:FUNC|NEXT)\(\s*([0-9A-Fa-f]{8})\s*\)")
|
||||
.Select(static match => Convert.ToUInt32(match.Groups[1].Value, 16)))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
@@ -145,7 +145,7 @@ public sealed class GuestAddressTableTests
|
||||
foreach (var (region, starts) in entries)
|
||||
{
|
||||
var address = table.Map(pal, region);
|
||||
Assert.True(starts.Contains(address), $"MKW_GUEST_FUNC({pal:X8}) is not a {region} function entry (0x{address:X8})");
|
||||
Assert.True(starts.Contains(address), $"MKW_GUEST_FUNC/NEXT({pal:X8}) is not a {region} function entry (0x{address:X8})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Translator.Core;
|
||||
using Translator.Core.CodeGen;
|
||||
using Xunit;
|
||||
@@ -40,14 +41,24 @@ public sealed class RuntimeNativeIndexTests
|
||||
PPC_NATIVE_OVERRIDE_VOID(80000040, Stub, (void), ());
|
||||
GX_FATAL_STUB(80000050, "Fatal")
|
||||
// REGISTER_NATIVE_FUNCTION(0x80000060, CommentedOut);
|
||||
REGISTER_NATIVE_WRAPPER_AS(0x80000070, Wrapper, "wrapper");
|
||||
REGISTER_NATIVE_REPLACEMENT_AS(0x80000080, Replacement, "replacement");
|
||||
""");
|
||||
|
||||
var registrations = RuntimeNativeIndexBuilder.Build(directory).Registrations;
|
||||
Assert.Equal(5, registrations.Length);
|
||||
Assert.Equal(7, registrations.Length);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation);
|
||||
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation &&
|
||||
entry.ModOverlay == NativeModOverlay.Unspecified);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation);
|
||||
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation &&
|
||||
entry.ModOverlay == NativeModOverlay.Unspecified);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
entry.Address == 0x80000070u && entry.Symbol == "Wrapper" && !entry.ExcludesBaseTranslation &&
|
||||
entry.ModOverlay == NativeModOverlay.Wrapper);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
entry.Address == 0x80000080u && entry.Symbol == "Replacement" && !entry.ExcludesBaseTranslation &&
|
||||
entry.ModOverlay == NativeModOverlay.Replacement);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
entry.Address == 0x80000030u && entry.IsTranslatedOverride);
|
||||
Assert.Contains(registrations, static entry =>
|
||||
@@ -108,6 +119,8 @@ public sealed class RuntimeNativeIndexTests
|
||||
var sources = Path.Combine(ProjectPathsForTests.FindRepositoryRoot(), "runtime", "src");
|
||||
var built = RuntimeNativeIndexBuilder.Build(sources);
|
||||
Assert.NotEmpty(built.Registrations);
|
||||
Assert.Contains(built.Registrations, static entry => entry.ModOverlay == NativeModOverlay.Wrapper);
|
||||
Assert.Contains(built.Registrations, static entry => entry.ModOverlay == NativeModOverlay.Replacement);
|
||||
Assert.NotEmpty(built.VoidStubAbis);
|
||||
Assert.NotEmpty(built.Effects);
|
||||
|
||||
@@ -126,6 +139,26 @@ public sealed class RuntimeNativeIndexTests
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper that calls through with MKW_GUEST_FUNC would silently drop the mod overlay it promises to run.
|
||||
[Fact]
|
||||
public void RepositoryWrapperNativesCallThroughToTheNextTranslation()
|
||||
{
|
||||
var sources = Path.Combine(ProjectPathsForTests.FindRepositoryRoot(), "runtime", "src");
|
||||
var wrappers = RuntimeNativeIndexBuilder.Build(sources).Registrations
|
||||
.Where(static entry => entry.ModOverlay == NativeModOverlay.Wrapper)
|
||||
.ToArray();
|
||||
Assert.NotEmpty(wrappers);
|
||||
foreach (var wrapper in wrappers)
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(sources, wrapper.SourceFile));
|
||||
var pal = $"{wrapper.Address:X8}";
|
||||
Assert.True(Regex.IsMatch(source, $@"MKW_GUEST_NEXT\(\s*{pal}\s*\)", RegexOptions.IgnoreCase),
|
||||
$"{wrapper.Symbol} is a wrapper but never calls MKW_GUEST_NEXT({pal})");
|
||||
Assert.False(Regex.IsMatch(source, $@"MKW_GUEST_FUNC\(\s*{pal}\s*\)", RegexOptions.IgnoreCase),
|
||||
$"{wrapper.Symbol} is a wrapper but calls MKW_GUEST_FUNC({pal})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveUsesAMatchingIndexAndKeepsWorkingWithoutSources()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user