578 lines
22 KiB
C#
578 lines
22 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Kernel;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Kernel;
|
|
|
|
// sceKernelGetTscFrequency must describe the same clock that sceKernelReadTsc returns. ReadTsc
|
|
// only returns the CPU's RDTSC when the host RDTSC reader is available (64-bit Windows) and
|
|
// otherwise falls back to the QPC-based Stopwatch, so the frequency selection has to follow suit.
|
|
public sealed class KernelRuntimeCompatExportsTests
|
|
{
|
|
private const ulong MemoryBase = 0x1_0000_0000;
|
|
private const ulong UnwindInfoAddress = MemoryBase + 0x400;
|
|
private const int UnwindInfoSize = 0x130;
|
|
|
|
private static KernelRuntimeCompatExports.TryGetFrequency Yields(ulong hz) =>
|
|
(out ulong frequencyHz) =>
|
|
{
|
|
frequencyHz = hz;
|
|
return true;
|
|
};
|
|
|
|
private static readonly KernelRuntimeCompatExports.TryGetFrequency Fails =
|
|
(out ulong frequencyHz) =>
|
|
{
|
|
frequencyHz = 0;
|
|
return false;
|
|
};
|
|
|
|
[Fact]
|
|
public void WithoutHostRdtsc_ReportsStopwatchFrequency_NotHardwareTsc()
|
|
{
|
|
// Regression: on Linux/macOS ReadTsc returns the Stopwatch counter, so the reported
|
|
// frequency must be the Stopwatch's, never the CPU's much larger hardware TSC frequency.
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: false,
|
|
overrideHzText: null,
|
|
tryCalibrate: Yields(2_400_000_000UL),
|
|
tryResolveCpuid: Yields(3_000_000_000UL),
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(10_000_000UL, frequencyHz);
|
|
Assert.Equal("qpc", source);
|
|
}
|
|
|
|
[Fact]
|
|
public void WithHostRdtsc_PrefersCalibratedFrequency()
|
|
{
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: true,
|
|
overrideHzText: null,
|
|
tryCalibrate: Yields(2_400_000_000UL),
|
|
tryResolveCpuid: Yields(3_000_000_000UL),
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(2_400_000_000UL, frequencyHz);
|
|
Assert.Equal("calibrated-rdtsc", source);
|
|
}
|
|
|
|
[Fact]
|
|
public void WithHostRdtsc_FallsBackToCpuid_WhenCalibrationFails()
|
|
{
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: true,
|
|
overrideHzText: null,
|
|
tryCalibrate: Fails,
|
|
tryResolveCpuid: Yields(3_000_000_000UL),
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(3_000_000_000UL, frequencyHz);
|
|
Assert.Equal("cpuid", source);
|
|
}
|
|
|
|
[Fact]
|
|
public void WithHostRdtsc_UsesStopwatch_WhenRdtscFrequencyUnknown()
|
|
{
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: true,
|
|
overrideHzText: null,
|
|
tryCalibrate: Fails,
|
|
tryResolveCpuid: Fails,
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(10_000_000UL, frequencyHz);
|
|
Assert.Equal("qpc", source);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void EnvOverride_Wins_WhenSane(bool rdtscAvailable)
|
|
{
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable,
|
|
overrideHzText: "1500000000",
|
|
tryCalibrate: Yields(2_400_000_000UL),
|
|
tryResolveCpuid: Yields(3_000_000_000UL),
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(1_500_000_000UL, frequencyHz);
|
|
Assert.Equal("env", source);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvOverride_BelowMinimum_IsIgnored()
|
|
{
|
|
// 500 kHz is below the sanity floor, so it is dropped; with rdtsc unavailable the
|
|
// hardware-TSC path is gated off and the Stopwatch frequency is used.
|
|
var (frequencyHz, _) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: false,
|
|
overrideHzText: "500000",
|
|
tryCalibrate: Fails,
|
|
tryResolveCpuid: Yields(3_000_000_000UL),
|
|
stopwatchFrequency: 10_000_000);
|
|
|
|
Assert.Equal(10_000_000UL, frequencyHz);
|
|
}
|
|
|
|
[Fact]
|
|
public void NonPositiveStopwatchFrequency_FallsBackToDefault()
|
|
{
|
|
var (frequencyHz, source) = KernelRuntimeCompatExports.SelectKernelTscFrequency(
|
|
rdtscAvailable: false,
|
|
overrideHzText: null,
|
|
tryCalibrate: Fails,
|
|
tryResolveCpuid: Fails,
|
|
stopwatchFrequency: 0);
|
|
|
|
Assert.Equal(10_000_000UL, frequencyHz); // DefaultKernelTscFrequency
|
|
Assert.Equal("qpc", source);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertUtcToLocaltime_AllowsNullLocalOutputAndWritesTimesec()
|
|
{
|
|
const long utcSeconds = 1_786_262_400;
|
|
const ulong timesecAddress = MemoryBase + 0x100;
|
|
const ulong dstSecondsAddress = MemoryBase + 0x200;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
var timesec = new byte[17];
|
|
Array.Fill(timesec, (byte)0xCC);
|
|
Assert.True(memory.TryWrite(timesecAddress, timesec));
|
|
|
|
context[CpuRegister.Rdi] = unchecked((ulong)utcSeconds);
|
|
context[CpuRegister.Rsi] = 0;
|
|
context[CpuRegister.Rdx] = timesecAddress;
|
|
context[CpuRegister.Rcx] = dstSecondsAddress;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelConvertUtcToLocaltime(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
Assert.True(memory.TryRead(timesecAddress, timesec));
|
|
Assert.Equal(utcSeconds, BinaryPrimitives.ReadInt64LittleEndian(timesec));
|
|
Assert.Equal(
|
|
unchecked((uint)GetStandardOffsetSeconds()),
|
|
BinaryPrimitives.ReadUInt32LittleEndian(timesec.AsSpan(8)));
|
|
Assert.Equal(0U, BinaryPrimitives.ReadUInt32LittleEndian(timesec.AsSpan(12)));
|
|
Assert.Equal(0xCC, timesec[16]);
|
|
|
|
Span<byte> dstSeconds = stackalloc byte[sizeof(ulong)];
|
|
Assert.True(memory.TryRead(dstSecondsAddress, dstSeconds));
|
|
Assert.Equal(0UL, BinaryPrimitives.ReadUInt64LittleEndian(dstSeconds));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Generation.Gen4)]
|
|
[InlineData(Generation.Gen5)]
|
|
public void ConvertLocaltimeToUtc_WritesCompleteTimeResult(Generation generation)
|
|
{
|
|
const long localSeconds = 1_786_240_800;
|
|
const ulong utcAddress = MemoryBase + 0x100;
|
|
const ulong timeResultAddress = MemoryBase + 0x200;
|
|
const ulong dstSecondsAddress = MemoryBase + 0x300;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, generation);
|
|
var timeResult = new byte[17];
|
|
var utc = new byte[9];
|
|
var daylightSavingSeconds = new byte[5];
|
|
Array.Fill(timeResult, (byte)0xCC);
|
|
Array.Fill(utc, (byte)0xCC);
|
|
Array.Fill(daylightSavingSeconds, (byte)0xCC);
|
|
Assert.True(memory.TryWrite(timeResultAddress, timeResult));
|
|
Assert.True(memory.TryWrite(utcAddress, utc));
|
|
Assert.True(memory.TryWrite(dstSecondsAddress, daylightSavingSeconds));
|
|
|
|
context[CpuRegister.Rdi] = unchecked((ulong)localSeconds);
|
|
context[CpuRegister.Rsi] = 0;
|
|
context[CpuRegister.Rdx] = utcAddress;
|
|
context[CpuRegister.Rcx] = timeResultAddress;
|
|
context[CpuRegister.R8] = dstSecondsAddress;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
|
|
Assert.True(memory.TryRead(utcAddress, utc));
|
|
var expectedUtcSeconds = localSeconds - GetStandardOffsetSeconds();
|
|
Assert.Equal(expectedUtcSeconds, BinaryPrimitives.ReadInt64LittleEndian(utc));
|
|
Assert.Equal(0xCC, utc[8]);
|
|
|
|
Assert.True(memory.TryRead(timeResultAddress, timeResult));
|
|
Assert.Equal(expectedUtcSeconds, BinaryPrimitives.ReadInt64LittleEndian(timeResult));
|
|
Assert.Equal(GetStandardOffsetSeconds(), BinaryPrimitives.ReadInt32LittleEndian(timeResult.AsSpan(8)));
|
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(timeResult.AsSpan(12)));
|
|
Assert.Equal(0xCC, timeResult[16]);
|
|
|
|
Assert.True(memory.TryRead(dstSecondsAddress, daylightSavingSeconds));
|
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(daylightSavingSeconds));
|
|
Assert.Equal(0xCC, daylightSavingSeconds[4]);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x00, -1)]
|
|
[InlineData(0x01, 0)]
|
|
[InlineData(0xCC, 1)]
|
|
[InlineData(0xFF, -1)]
|
|
public void TimeConversionRoundTrip_OverwritesStaleOffsetAndDaylightSavingFields(
|
|
byte initialValue,
|
|
int daylightSavingHint)
|
|
{
|
|
const ulong utcAddress = MemoryBase + 0x100;
|
|
const ulong localAddress = MemoryBase + 0x200;
|
|
const ulong forwardResultAddress = MemoryBase + 0x300;
|
|
const ulong reverseResultAddress = MemoryBase + 0x400;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
var forwardResult = new byte[16];
|
|
var reverseResult = new byte[16];
|
|
|
|
foreach (var localSeconds in new[] { 1_767_225_600L, 1_767_312_000L })
|
|
{
|
|
Array.Fill(forwardResult, initialValue);
|
|
Array.Fill(reverseResult, (byte)~initialValue);
|
|
Assert.True(memory.TryWrite(forwardResultAddress, forwardResult));
|
|
Assert.True(memory.TryWrite(reverseResultAddress, reverseResult));
|
|
context[CpuRegister.Rdi] = unchecked((ulong)localSeconds);
|
|
context[CpuRegister.Rsi] = unchecked((ulong)daylightSavingHint);
|
|
context[CpuRegister.Rdx] = utcAddress;
|
|
context[CpuRegister.Rcx] = forwardResultAddress;
|
|
context[CpuRegister.R8] = 0;
|
|
|
|
Assert.Equal(0, KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context));
|
|
Assert.True(context.TryReadUInt64(utcAddress, out var utcSeconds));
|
|
context[CpuRegister.Rdi] = utcSeconds;
|
|
context[CpuRegister.Rsi] = localAddress;
|
|
context[CpuRegister.Rdx] = reverseResultAddress;
|
|
context[CpuRegister.Rcx] = 0;
|
|
|
|
Assert.Equal(0, KernelRuntimeCompatExports.KernelConvertUtcToLocaltime(context));
|
|
Assert.True(context.TryReadUInt64(localAddress, out var returnedLocalSeconds));
|
|
Assert.Equal(unchecked((ulong)localSeconds), returnedLocalSeconds);
|
|
Assert.True(memory.TryRead(forwardResultAddress, forwardResult));
|
|
Assert.True(memory.TryRead(reverseResultAddress, reverseResult));
|
|
// Calendar conversion compares both offsets before it selects the daylight-saving state.
|
|
Assert.Equal(reverseResult.AsSpan(8, 8).ToArray(), forwardResult.AsSpan(8, 8).ToArray());
|
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(forwardResult.AsSpan(12)));
|
|
Assert.Equal(reverseResult, forwardResult);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(8)]
|
|
[InlineData(15)]
|
|
public void ConvertLocaltimeToUtc_RejectsTruncatedTimeResult(int availableBytes)
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, availableBytes);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
var initialBytes = new byte[availableBytes];
|
|
Array.Fill(initialBytes, (byte)0xCC);
|
|
Assert.True(memory.TryWrite(MemoryBase, initialBytes));
|
|
context[CpuRegister.Rdi] = 1_767_225_600;
|
|
context[CpuRegister.Rcx] = MemoryBase;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result);
|
|
var remainingBytes = new byte[availableBytes];
|
|
Assert.True(memory.TryRead(MemoryBase, remainingBytes));
|
|
Assert.Equal(initialBytes, remainingBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertLocaltimeToUtc_AllowsNullOptionalOutputs()
|
|
{
|
|
const long localSeconds = 1_767_225_600;
|
|
var memory = new FakeCpuMemory(MemoryBase, 16);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
context[CpuRegister.Rdi] = unchecked((ulong)localSeconds);
|
|
context[CpuRegister.Rcx] = MemoryBase;
|
|
|
|
Assert.Equal(0, KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context));
|
|
var timeResult = new byte[16];
|
|
Assert.True(memory.TryRead(MemoryBase, timeResult));
|
|
Assert.Equal(localSeconds - GetStandardOffsetSeconds(), BinaryPrimitives.ReadInt64LittleEndian(timeResult));
|
|
Assert.Equal(GetStandardOffsetSeconds(), BinaryPrimitives.ReadInt32LittleEndian(timeResult.AsSpan(8)));
|
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(timeResult.AsSpan(12)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertLocaltimeToUtc_RequiresTimeResult()
|
|
{
|
|
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 1_767_225_600;
|
|
|
|
Assert.Equal(
|
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
|
KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(CpuRegister.Rdx)]
|
|
[InlineData(CpuRegister.R8)]
|
|
public void ConvertLocaltimeToUtc_ReportsInvalidOptionalOutput(CpuRegister outputRegister)
|
|
{
|
|
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 1_767_225_600;
|
|
context[CpuRegister.Rcx] = MemoryBase;
|
|
context[outputRegister] = MemoryBase + 0x1000;
|
|
|
|
Assert.Equal(
|
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
|
KernelRuntimeCompatExports.KernelConvertLocaltimeToUtc(context));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(360, 0, -21_600)]
|
|
[InlineData(360, 3_600, -18_000)]
|
|
[InlineData(-345, 0, 20_700)]
|
|
public void TimeConversionArithmetic_HandlesOffsetsAndDst(
|
|
int minutesWest,
|
|
int dstSeconds,
|
|
long expectedLocalDelta)
|
|
{
|
|
const long utcSeconds = 1_786_262_400;
|
|
|
|
var localSeconds = KernelRuntimeCompatExports.ConvertUtcToLocaltimeSeconds(
|
|
utcSeconds,
|
|
minutesWest,
|
|
dstSeconds);
|
|
var roundTrip = KernelRuntimeCompatExports.ConvertLocaltimeToUtcSeconds(
|
|
localSeconds,
|
|
minutesWest,
|
|
dstSeconds);
|
|
|
|
Assert.Equal(utcSeconds + expectedLocalDelta, localSeconds);
|
|
Assert.Equal(utcSeconds, roundTrip);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void GetModuleInfoForUnwind_HostAddressWritesSyntheticBoundary(bool useSysmoduleAlias)
|
|
{
|
|
const ulong queriedAddress = 0x00000243_A84C_FFFF;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
var payload = new byte[UnwindInfoSize + 1];
|
|
Array.Fill(payload, (byte)0xCC);
|
|
Assert.True(memory.TryWrite(UnwindInfoAddress, payload));
|
|
Assert.True(context.TryWriteUInt64(UnwindInfoAddress, UnwindInfoSize));
|
|
context[CpuRegister.Rdi] = queriedAddress;
|
|
context[CpuRegister.Rsi] = 1;
|
|
context[CpuRegister.Rdx] = UnwindInfoAddress;
|
|
|
|
var result = useSysmoduleAlias
|
|
? KernelRuntimeCompatExports.SysmoduleGetModuleInfoForUnwind(context)
|
|
: KernelRuntimeCompatExports.KernelGetModuleInfoForUnwind(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
Assert.True(memory.TryRead(UnwindInfoAddress, payload));
|
|
Assert.Equal((ulong)UnwindInfoSize, BinaryPrimitives.ReadUInt64LittleEndian(payload));
|
|
Assert.Equal("SharpEmuHostBoundary", ReadUtf8Z(payload.AsSpan(0x08, 0x100)));
|
|
Assert.All(payload.AsSpan(0x108, 0x18).ToArray(), value => Assert.Equal(0, value));
|
|
Assert.Equal(
|
|
queriedAddress & ~0xF_FFFFUL,
|
|
BinaryPrimitives.ReadUInt64LittleEndian(payload.AsSpan(0x120)));
|
|
Assert.Equal(0x10_0000UL, BinaryPrimitives.ReadUInt64LittleEndian(payload.AsSpan(0x128)));
|
|
Assert.Equal(0xCC, payload[UnwindInfoSize]);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetModuleInfoForUnwind_MissingGuestAddressReturnsNotFound()
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
Assert.True(context.TryWriteUInt64(UnwindInfoAddress, UnwindInfoSize));
|
|
context[CpuRegister.Rdi] = 0x00000008_7FFF_1234;
|
|
context[CpuRegister.Rsi] = 1;
|
|
context[CpuRegister.Rdx] = UnwindInfoAddress;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelGetModuleInfoForUnwind(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetModuleInfoForUnwind_InvalidFlagsReturnsInvalidArgument()
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
Assert.True(context.TryWriteUInt64(UnwindInfoAddress, UnwindInfoSize));
|
|
context[CpuRegister.Rdi] = 0x00000243_A84C_FFFF;
|
|
context[CpuRegister.Rsi] = 3;
|
|
context[CpuRegister.Rdx] = UnwindInfoAddress;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelGetModuleInfoForUnwind(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsSignalReturn_ReportsNoSyntheticSignalFrame()
|
|
{
|
|
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 0x00000243_A84C_FFFF;
|
|
|
|
var result = KernelRuntimeCompatExports.KernelIsSignalReturn(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadStartModule_UsesConfiguredLoaderForExactGuestPath()
|
|
{
|
|
const ulong pathAddress = MemoryBase + 0x500;
|
|
const ulong resultAddress = MemoryBase + 0x600;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
Assert.True(memory.TryWrite(pathAddress, Encoding.UTF8.GetBytes("/app0/5/tomb5.prx\0")));
|
|
context[CpuRegister.Rdi] = pathAddress;
|
|
context[CpuRegister.R9] = resultAddress;
|
|
string? requestedPath = null;
|
|
|
|
KernelModuleRegistry.Reset();
|
|
try
|
|
{
|
|
KernelModuleRegistry.ConfigureModuleLoader(path =>
|
|
{
|
|
requestedPath = path;
|
|
var handle = KernelModuleRegistry.RegisterModule(
|
|
"C:/games/app0/5/tomb5.prx",
|
|
0x8100_0000,
|
|
0x10_0000,
|
|
0x8100_0000,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
isMain: false);
|
|
return KernelModuleRegistry.ModuleLoadResult.Success(handle);
|
|
});
|
|
|
|
var result = KernelRuntimeCompatExports.KernelLoadStartModule(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal("/app0/5/tomb5.prx", requestedPath);
|
|
Assert.Equal(1UL, context[CpuRegister.Rax]);
|
|
Span<byte> startResult = stackalloc byte[sizeof(int)];
|
|
Assert.True(memory.TryRead(resultAddress, startResult));
|
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(startResult));
|
|
}
|
|
finally
|
|
{
|
|
KernelModuleRegistry.Reset();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadStartModule_DoesNotCreateSyntheticModuleWhenExactLoadFails()
|
|
{
|
|
const ulong pathAddress = MemoryBase + 0x500;
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
Assert.True(memory.TryWrite(pathAddress, Encoding.UTF8.GetBytes("/app0/missing.prx\0")));
|
|
context[CpuRegister.Rdi] = pathAddress;
|
|
|
|
KernelModuleRegistry.Reset();
|
|
try
|
|
{
|
|
KernelModuleRegistry.ConfigureModuleLoader(_ =>
|
|
KernelModuleRegistry.ModuleLoadResult.Failure(
|
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND));
|
|
|
|
var result = KernelRuntimeCompatExports.KernelLoadStartModule(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
|
|
Assert.Empty(KernelModuleRegistry.GetModuleHandles(includeSystemModules: true));
|
|
}
|
|
finally
|
|
{
|
|
KernelModuleRegistry.Reset();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ModuleRegistry_ExactPathNormalizesEquivalentPaths()
|
|
{
|
|
var modulePath = Path.Combine(Path.GetTempPath(), "sharpemu", "app0", "PSNCommon.prx");
|
|
var equivalentPath = Path.Combine(
|
|
Path.GetDirectoryName(modulePath)!,
|
|
".",
|
|
Path.GetFileName(modulePath));
|
|
|
|
KernelModuleRegistry.Reset();
|
|
try
|
|
{
|
|
var handle = KernelModuleRegistry.RegisterModule(
|
|
modulePath,
|
|
0x8100_0000,
|
|
0x10_0000,
|
|
0x8100_0000,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
isMain: false);
|
|
|
|
Assert.True(KernelModuleRegistry.TryFindByExactPath(equivalentPath, out var module));
|
|
Assert.Equal(handle, module.Handle);
|
|
}
|
|
finally
|
|
{
|
|
KernelModuleRegistry.Reset();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ModuleRegistry_ExactPathDoesNotFallBackToMatchingFileName()
|
|
{
|
|
var registeredPath = Path.Combine(Path.GetTempPath(), "sharpemu", "first", "PSNCommon.prx");
|
|
var differentPath = Path.Combine(Path.GetTempPath(), "sharpemu", "second", "PSNCommon.prx");
|
|
|
|
KernelModuleRegistry.Reset();
|
|
try
|
|
{
|
|
_ = KernelModuleRegistry.RegisterModule(
|
|
registeredPath,
|
|
0x8100_0000,
|
|
0x10_0000,
|
|
0x8100_0000,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
isMain: false);
|
|
|
|
Assert.False(KernelModuleRegistry.TryFindByExactPath(differentPath, out _));
|
|
}
|
|
finally
|
|
{
|
|
KernelModuleRegistry.Reset();
|
|
}
|
|
}
|
|
|
|
private static int GetMinutesWest() =>
|
|
unchecked((int)-TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes);
|
|
|
|
private static int GetStandardOffsetSeconds() =>
|
|
unchecked(-GetMinutesWest() * 60);
|
|
|
|
private static string ReadUtf8Z(ReadOnlySpan<byte> value)
|
|
{
|
|
var length = value.IndexOf((byte)0);
|
|
Assert.True(length >= 0);
|
|
return Encoding.UTF8.GetString(value[..length]);
|
|
}
|
|
}
|