revert: restore state before huge regression

This commit is contained in:
ParantezTech
2026-07-23 16:03:45 +03:00
parent 5a08a9bb43
commit 6db095ec82
39 changed files with 230 additions and 4186 deletions
-1
View File
@@ -19,7 +19,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" /> <Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="tests/SharpEmu.Debugger.Tests/SharpEmu.Debugger.Tests.csproj" />
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" /> <Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
<Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" /> <Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" /> <Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
@@ -91,71 +91,6 @@ public sealed partial class DirectExecutionBackend
} }
} }
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace() private void DumpRecentImportTrace()
{ {
var trace = _recentImportTrace; var trace = _recentImportTrace;
@@ -333,15 +268,14 @@ public sealed partial class DirectExecutionBackend
ulong callRip = returnRip + (ulong)i; ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32)); ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}"); Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
var importEntries = _importEntries; for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{ {
if (importEntries[importIndex].Address != target) if (_importEntries[importIndex].Address != target)
{ {
continue; continue;
} }
string nid = importEntries[importIndex].Nid; string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export)) if (_moduleManager.TryGetExport(nid, out var export))
{ {
Log.Debug( Log.Debug(
@@ -368,14 +302,14 @@ public sealed partial class DirectExecutionBackend
{ {
Log.Debug( Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}"); $"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++) for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
{ {
if (importEntries[importIndex].Address != slotTarget) if (_importEntries[importIndex].Address != slotTarget)
{ {
continue; continue;
} }
string nid = importEntries[importIndex].Nid; string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export)) if (_moduleManager.TryGetExport(nid, out var export))
{ {
Log.Debug( Log.Debug(
@@ -55,7 +55,6 @@ public sealed partial class DirectExecutionBackend
} }
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub); _exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}"); Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_unhandledFilterDelegate = UnhandledExceptionFilter; _unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate); _unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
@@ -118,13 +117,6 @@ public sealed partial class DirectExecutionBackend
{ {
return -1; return -1;
} }
if (exceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{
return -1;
}
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip)) if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
{ {
return -1; return -1;
@@ -69,15 +69,6 @@ public sealed partial class DirectExecutionBackend
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo) private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
{ {
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{
return -1;
}
return TryRecoverUnresolvedSentinel(exceptionInfo); return TryRecoverUnresolvedSentinel(exceptionInfo);
} }
@@ -2033,15 +2024,13 @@ public sealed partial class DirectExecutionBackend
{ {
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress); ulong outputAddress = cpuContext[CpuRegister.Rdx];
try
{
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName)) if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
{ {
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
} }
var moduleHandle = unchecked((int)cpuContext[CpuRegister.Rdi]); var moduleHandle = unchecked((int)cpuContext[CpuRegister.Rdi]);
if (!TryResolveModuleSymbolAddress(moduleHandle, symbolName, out var resolvedAddress) && if (!TryResolveModuleSymbolAddress(moduleHandle, symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAddress(symbolName, out resolvedAddress) && !TryResolveRuntimeSymbolAddress(symbolName, out resolvedAddress) &&
@@ -2050,79 +2039,23 @@ public sealed partial class DirectExecutionBackend
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'"); $"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
} }
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DLSYM"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DLSYM"), "1", StringComparison.Ordinal))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] sceKernelDlsym: handle=0x{moduleHandle:X} symbol='{symbolName}' -> 0x{resolvedAddress:X16}"); $"[LOADER][TRACE] sceKernelDlsym: handle=0x{moduleHandle:X} symbol='{symbolName}' -> 0x{resolvedAddress:X16}");
} }
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress)) if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
{ {
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
} }
}
catch
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
cpuContext[CpuRegister.Rax] = 0uL; cpuContext[CpuRegister.Rax] = 0uL;
return OrbisGen2Result.ORBIS_GEN2_OK; return OrbisGen2Result.ORBIS_GEN2_OK;
} }
private static void NormalizeKernelDynlibDlsymArguments(
CpuContext cpuContext,
out ulong symbolNameAddress,
out ulong outputAddress)
{
var handle = cpuContext[CpuRegister.Rdi];
symbolNameAddress = cpuContext[CpuRegister.Rsi];
outputAddress = cpuContext[CpuRegister.Rdx];
// Standalone bootstrap loaders sometimes call through the bridge with
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
if (symbolNameAddress < 0x10000 &&
IsPlausibleDynlibSymbolPointer(handle))
{
symbolNameAddress = handle;
handle = cpuContext[CpuRegister.Rsi];
cpuContext[CpuRegister.Rdi] = handle;
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
}
}
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
{
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
}
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
{
if (outputAddress != 0)
{
_ = TryWriteUInt64Compat(outputAddress, 0);
}
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private void ResetLazyDlsymStubState()
{
lock (_lazyDlsymStubGate)
{
_lazyDlsymStubCache.Clear();
_lazyImportStubPoolMapped = false;
_lazyImportStubPoolBase = 0;
_lazyImportStubNextSlot = 0;
_lazyImportStubPoolLimit = 0;
}
}
private static bool TryResolveModuleSymbolAddress(int moduleHandle, string symbolName, out ulong address) private static bool TryResolveModuleSymbolAddress(int moduleHandle, string symbolName, out ulong address)
{ {
if (KernelModuleRegistry.TryResolveModuleSymbol(moduleHandle, symbolName, out address)) if (KernelModuleRegistry.TryResolveModuleSymbol(moduleHandle, symbolName, out address))
@@ -2177,18 +2110,25 @@ public sealed partial class DirectExecutionBackend
} }
var symbolNameAddress = cpuContext[CpuRegister.Rdi]; var symbolNameAddress = cpuContext[CpuRegister.Rdi];
var outputAddress = cpuContext[CpuRegister.Rsi];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) || if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress)) outputAddress == 0 ||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}'"); $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
// il2cpp_api_lookup_symbol is a normal one-argument pointer-returning if (outputAddress != 0)
// function. In particular, RSI is not an output pointer; the title's {
// caller leaves it live from an earlier call. Do not write through it. _ = TryWriteUInt64Compat(outputAddress, 0);
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: false, address: 0);
} }
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: true, resolvedAddress); cpuContext[CpuRegister.Rax] = ulong.MaxValue;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
cpuContext[CpuRegister.Rax] = 0;
return OrbisGen2Result.ORBIS_GEN2_OK;
} }
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address) private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
@@ -2198,23 +2138,8 @@ public sealed partial class DirectExecutionBackend
return true; return true;
} }
if (Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
TryResolveRuntimeSymbolAddress(symbol.Nid, out address)) TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
{
return true;
}
// Unity's IL2CPP API table is populated through this resolver, then the
// returned pointers are called directly by the title. Use the callable
// zero-return stub for missing APIs rather than a non-callable sentinel.
if (symbolName.StartsWith("il2cpp_", StringComparison.Ordinal) &&
_unresolvedReturnStub != 0)
{
address = (ulong)_unresolvedReturnStub;
return true;
}
return false;
} }
private OrbisGen2Result DispatchBootstrapBridge() private OrbisGen2Result DispatchBootstrapBridge()
@@ -3,7 +3,6 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -89,13 +88,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong GuestThreadHandle, ulong GuestThreadHandle,
int ManagedThreadId); int ManagedThreadId);
private readonly record struct DeferredBootstrapTraceEntry(
long DispatchIndex,
ulong Op,
ulong SymbolPointer,
ulong OutputPointer,
ulong ReturnRip);
#pragma warning disable CS0649 #pragma warning disable CS0649
private struct EXCEPTION_POINTERS private struct EXCEPTION_POINTERS
{ {
@@ -311,31 +303,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private KeyValuePair<string, ulong>[] _runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>(); private KeyValuePair<string, ulong>[] _runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
private readonly ConcurrentDictionary<string, ulong> _runtimeSymbolsByName = private readonly Dictionary<string, ulong> _runtimeSymbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
new(StringComparer.Ordinal);
// Keep in sync with SelfLoader import-stub mapping constants.
private const ulong ImportStubRegionCanonicalBase = 0x0000_7000_0000_0000UL;
private const ulong ImportStubRegionAddressStride = 0x0000_0000_0100_0000UL;
private const ulong LazyImportStubSlotSize = 0x10;
private const ulong ImportStubRegionPageSize = 0x1000UL;
private const string KernelDynlibDlsymAerolibNid = "LwG8g3niqwA";
private readonly object _lazyDlsymStubGate = new();
private readonly Dictionary<string, ulong> _lazyDlsymStubCache = new(StringComparer.Ordinal);
private ulong _lazyImportStubPoolBase;
private ulong _lazyImportStubNextSlot;
private ulong _lazyImportStubPoolLimit;
private bool _lazyImportStubPoolMapped;
private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64]; private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64];
@@ -343,14 +311,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private int _recentImportTraceWriteIndex; private int _recentImportTraceWriteIndex;
private readonly DeferredBootstrapTraceEntry[] _deferredBootstrapTrace = new DeferredBootstrapTraceEntry[32];
private int _deferredBootstrapTraceCount;
private int _deferredBootstrapTraceWriteIndex;
private readonly object _deferredBootstrapTraceGate = new();
private readonly string[] _distinctImportNidHistory = new string[128]; private readonly string[] _distinctImportNidHistory = new string[128];
private int _distinctImportNidHistoryCount; private int _distinctImportNidHistoryCount;
@@ -1180,14 +1140,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
result = OrbisGen2Result.ORBIS_GEN2_OK; result = OrbisGen2Result.ORBIS_GEN2_OK;
LastError = null; LastError = null;
InitializeRuntimeSymbolIndex(runtimeSymbols); InitializeRuntimeSymbolIndex(runtimeSymbols);
ResetLazyDlsymStubState();
_recentImportTraceCount = 0; _recentImportTraceCount = 0;
_recentImportTraceWriteIndex = 0; _recentImportTraceWriteIndex = 0;
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTraceCount = 0;
_deferredBootstrapTraceWriteIndex = 0;
}
_distinctImportNidHistoryCount = 0; _distinctImportNidHistoryCount = 0;
_distinctImportNidHistoryWriteIndex = 0; _distinctImportNidHistoryWriteIndex = 0;
_lastDistinctImportNid = string.Empty; _lastDistinctImportNid = string.Empty;
@@ -1270,7 +1224,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
finally finally
{ {
HostSessionControl.SetShutdownHandler(null); HostSessionControl.SetShutdownHandler(null);
DrainDeferredBootstrapTraces();
GuestThreadExecution.Scheduler = previousGuestThreadScheduler; GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ==="); Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
} }
@@ -6727,14 +6680,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong rsp = cpuContext[CpuRegister.Rsp]; ulong rsp = cpuContext[CpuRegister.Rsp];
Console.Error.WriteLine($"[LOADER][ERROR] Stall snapshot: rip=0x{cpuContext.Rip:X16} rsp=0x{rsp:X16} rbp=0x{cpuContext[CpuRegister.Rbp]:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16} rbx=0x{cpuContext[CpuRegister.Rbx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16}"); Console.Error.WriteLine($"[LOADER][ERROR] Stall snapshot: rip=0x{cpuContext.Rip:X16} rsp=0x{rsp:X16} rbp=0x{cpuContext[CpuRegister.Rbp]:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16} rbx=0x{cpuContext[CpuRegister.Rbx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16}");
ulong num = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL; ulong num = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
var importEntries = _importEntries; for (int i = 0; i < _importEntries.Length; i++)
for (int i = 0; i < importEntries.Length; i++)
{ {
if (importEntries[i].Address != num) if (_importEntries[i].Address != num)
{ {
continue; continue;
} }
string text = importEntries[i].Nid; string text = _importEntries[i].Nid;
if (_moduleManager.TryGetExport(text, out ExportedFunction export)) if (_moduleManager.TryGetExport(text, out ExportedFunction export))
{ {
Console.Error.WriteLine($"[LOADER][ERROR] Stall import-stub: rip=0x{num:X16} nid={text} -> {export.LibraryName}:{export.Name}"); Console.Error.WriteLine($"[LOADER][ERROR] Stall import-stub: rip=0x{num:X16} nid={text} -> {export.LibraryName}:{export.Name}");
@@ -7027,7 +6979,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_importEntries = Array.Empty<ImportStubEntry>(); _importEntries = Array.Empty<ImportStubEntry>();
_runtimeSymbolsByName.Clear(); _runtimeSymbolsByName.Clear();
StopReadyThreadDispatcher(); StopReadyThreadDispatcher();
ResetLazyDlsymStubState();
StopStallWatchdog(); StopStallWatchdog();
if (_exceptionHandler != 0) if (_exceptionHandler != 0)
{ {
@@ -1,18 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
internal static class Il2CppApiLookupAbi
{
internal static OrbisGen2Result SetResult(CpuContext context, bool resolved, ulong address)
{
// il2cpp_api_lookup_symbol is a normal pointer-returning function:
// the result is in RAX and RSI remains caller-owned state.
context[CpuRegister.Rax] = resolved ? address : 0;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+1 -1
View File
@@ -387,7 +387,7 @@ internal sealed class EmulatorProcess : IDisposable
private void ForwardOutput(string? line, bool isError) private void ForwardOutput(string? line, bool isError)
{ {
if (line is not null) if (!string.IsNullOrEmpty(line))
{ {
OutputReceived?.Invoke(line, isError); OutputReceived?.Invoke(line, isError);
} }
+15 -75
View File
@@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -81,7 +80,7 @@ public static unsafe class GuestImageWriteTracker
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty; private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
private static readonly bool _enabled = private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0"; Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter = private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS")); ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
@@ -96,36 +95,12 @@ public static unsafe class GuestImageWriteTracker
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0; _enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
private static long _lifetimeTraceSequence; private static long _lifetimeTraceSequence;
private const uint PageReadonly = 0x02;
private const uint PageReadWrite = 0x04;
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)] [DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
private static extern int Mprotect(nint address, nuint length, int protection); private static extern int Mprotect(nint address, nuint length, int protection);
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)] [DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
private static extern int ClockGetTime(int clockId, Timespec* time); private static extern int ClockGetTime(int clockId, Timespec* time);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualProtect(
nint lpAddress,
nuint dwSize,
uint flNewProtect,
out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(
nint lpAddress,
nuint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
public static bool Enabled => _enabled; public static bool Enabled => _enabled;
/// <summary> /// <summary>
@@ -140,17 +115,7 @@ public static unsafe class GuestImageWriteTracker
return; return;
} }
// VirtualProtect only belongs on VirtualAlloc/mmap pages. Warming on var scratch = NativeMemory.AllocZeroed(4096);
// CRT heap memory makes neighbouring heap metadata read-only and
// crashes the process on Windows.
var scratch = OperatingSystem.IsWindows()
? VirtualAlloc(0, 4096, MemCommit | MemReserve, PageReadWrite)
: (nint)NativeMemory.AllocZeroed(4096);
if (scratch == 0)
{
return;
}
try try
{ {
// Warm the timestamp P/Invoke used by the signal-safe scalar // Warm the timestamp P/Invoke used by the signal-safe scalar
@@ -164,14 +129,7 @@ public static unsafe class GuestImageWriteTracker
} }
finally finally
{ {
if (OperatingSystem.IsWindows()) NativeMemory.Free(scratch);
{
_ = VirtualFree(scratch, 0, MemRelease);
}
else
{
NativeMemory.Free((void*)scratch);
}
} }
} }
@@ -487,7 +445,10 @@ public static unsafe class GuestImageWriteTracker
} }
if (needsUnprotect && if (needsUnprotect &&
!TrySetProtection(writableStart, writableEnd - writableStart, writable: true)) Mprotect(
(nint)writableStart,
(nuint)(writableEnd - writableStart),
ProtRead | ProtWrite) != 0)
{ {
return false; return false;
} }
@@ -536,7 +497,10 @@ public static unsafe class GuestImageWriteTracker
// A new publication/rearm starts a new first-write lifetime. // A new publication/rearm starts a new first-write lifetime.
Volatile.Write(ref range.FirstCpuWriteSeen, 0); Volatile.Write(ref range.FirstCpuWriteSeen, 0);
var failed = !TrySetProtection(range.Start, range.End - range.Start, writable: false); var failed = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead) != 0;
if (failed) if (failed)
{ {
Volatile.Write(ref range.Armed, 0); Volatile.Write(ref range.Armed, 0);
@@ -556,7 +520,10 @@ public static unsafe class GuestImageWriteTracker
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1; var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
if (wasArmed) if (wasArmed)
{ {
_ = TrySetProtection(range.Start, range.End - range.Start, writable: true); _ = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead | ProtWrite);
} }
if (range.TraceLifetime) if (range.TraceLifetime)
@@ -712,35 +679,8 @@ public static unsafe class GuestImageWriteTracker
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}"); $"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
} }
private static bool TrySetProtection(ulong start, ulong length, bool writable)
{
if (length == 0)
{
return true;
}
if (OperatingSystem.IsWindows())
{
return VirtualProtect(
(nint)start,
(nuint)length,
writable ? PageReadWrite : PageReadonly,
out _) != 0;
}
return Mprotect(
(nint)start,
(nuint)length,
writable ? ProtRead | ProtWrite : ProtRead) == 0;
}
private static long GetMonotonicNanoseconds() private static long GetMonotonicNanoseconds()
{ {
if (OperatingSystem.IsWindows())
{
return Stopwatch.GetTimestamp() * 1_000_000_000L / Stopwatch.Frequency;
}
Timespec time; Timespec time;
return ClockGetTime(ClockMonotonicRaw, &time) == 0 return ClockGetTime(ClockMonotonicRaw, &time) == 0
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds) ? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
+13 -290
View File
@@ -80,13 +80,8 @@ public static partial class AgcExports
private const uint RIndexCount = 0x1C; private const uint RIndexCount = 0x1C;
private const uint SpiShaderPgmLoPs = 0x8; private const uint SpiShaderPgmLoPs = 0x8;
private const uint SpiShaderPgmHiPs = 0x9; private const uint SpiShaderPgmHiPs = 0x9;
private const uint SpiShaderPgmLoVs = 0x48;
private const uint SpiShaderPgmHiVs = 0x49;
private const uint SpiShaderPgmLoEs = 0xC8; private const uint SpiShaderPgmLoEs = 0xC8;
private const uint SpiShaderPgmHiEs = 0xC9; private const uint SpiShaderPgmHiEs = 0xC9;
private const uint SpiShaderPgmLoHs = 0x108;
private const uint SpiShaderPgmHiHs = 0x109;
private const uint SpiShaderPgmRsrc1Hs = 0x10A;
private const uint SpiShaderPgmLoLs = 0x148; private const uint SpiShaderPgmLoLs = 0x148;
private const uint SpiShaderPgmHiLs = 0x149; private const uint SpiShaderPgmHiLs = 0x149;
private const uint SpiShaderPgmLoGs = 0x8A; private const uint SpiShaderPgmLoGs = 0x8A;
@@ -280,7 +275,6 @@ public static partial class AgcExports
private static int _tracedVertexRangeCount; private static int _tracedVertexRangeCount;
private static long _dcbWaitRegMemTraceCount; private static long _dcbWaitRegMemTraceCount;
private static long _createShaderTraceCount; private static long _createShaderTraceCount;
private static long _cbMetadataSkipTraceCount;
private static long _packetPayloadTraceCount; private static long _packetPayloadTraceCount;
private static bool _tracedMissingPixelShaderBindings; private static bool _tracedMissingPixelShaderBindings;
private static long _unsatisfiedWaitTraceCount; private static long _unsatisfiedWaitTraceCount;
@@ -2971,38 +2965,6 @@ public static partial class AgcExports
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}"); $"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
return ReturnPointer(ctx, commandAddress); return ReturnPointer(ctx, commandAddress);
} }
// Synthetic labels for uncatalogued AGC helper NIDs; the NID is authoritative.
#pragma warning disable SHEM006
[SysAbiExport(
Nid = "zlqfTyrQSPk",
ExportName = "sceAgcUnknownZlqfTyrQSPk",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int UnknownZlqfTyrQSPk(CpuContext ctx)
{
TraceAgc(
$"agc.unknown_zlqf rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dZGYu5wObJs",
ExportName = "sceAgcUnknownDZGYu5wObJs",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int UnknownDZGYu5wObJs(CpuContext ctx)
{
TraceAgc(
$"agc.unknown_dzgy rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
#pragma warning restore SHEM006 #pragma warning restore SHEM006
private static void EnqueueSubmittedDcb( private static void EnqueueSubmittedDcb(
@@ -5684,25 +5646,6 @@ public static partial class AgcExports
} }
state.TranslatedDraw = null; state.TranslatedDraw = null;
state.GuestDrawKind = GuestDrawKind.None; state.GuestDrawKind = GuestDrawKind.None;
// CB modes EliminateFastClear / FmaskDecompress / DccDecompress run
// colour-buffer metadata ops. The bound shader is only a vehicle and
// must not be applied as a normal colour draw.
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
IsCbMetadataColorMode(cbMode))
{
if (_traceAgcShader || ShouldTraceHotPath(ref _cbMetadataSkipTraceCount))
{
TraceAgcShader(
$"agc.cb_metadata_skip seq={drawSequence} mode={cbMode} " +
$"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " +
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16} " +
$"vertices={vertexCount}");
}
return;
}
foreach (var target in renderTargets) foreach (var target in renderTargets)
{ {
state.KnownRenderTargets[target.Address] = target; state.KnownRenderTargets[target.Address] = target;
@@ -6488,32 +6431,6 @@ public static partial class AgcExports
TraceAstroTitlePixelGlobalProbe(pixelEvaluation); TraceAstroTitlePixelGlobalProbe(pixelEvaluation);
} }
// Patch BufferFormat from the attrib table onto the V# before host
// vertex input. IR discovery often keeps a stale float format from the
// unpatched sharp — that turns UI glyphs into gradient triangles.
// Match by stride+offset (not bare base address) so interleaved streams
// keep loading-video bindings intact.
if (exportEvaluation.VertexInputs is { Count: > 0 } discoveredInputs &&
AgcVertexMetadata.TryGetVertexTableRegisters(
ctx,
exportShaderAddress,
exportShaderHeader,
out var vertexTables))
{
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
exportEvaluation.ScalarRegisters,
vertexTables,
discoveredInputs);
if (!ReferenceEquals(merged, discoveredInputs))
{
TraceAgcShader(
$"agc.vertex_metadata_format es=0x{exportShaderAddress:X16} " +
$"count={merged.Count}");
exportEvaluation = exportEvaluation with { VertexInputs = merged };
}
}
// Every bound color target the shader exports to. Deferred renderers // Every bound color target the shader exports to. Deferred renderers
// draw a multi-render-target G-buffer (up to eight slots) in one pass. // draw a multi-render-target G-buffer (up to eight slots) in one pass.
// Fall back to slot 0 if we cannot match any export to a bound target. // Fall back to slot 0 if we cannot match any export to a bound target.
@@ -7295,7 +7212,6 @@ public static partial class AgcExports
Mix(input.NumberFormat); Mix(input.NumberFormat);
Mix(input.Stride); Mix(input.Stride);
Mix(input.OffsetBytes); Mix(input.OffsetBytes);
Mix(input.PerInstance ? 1u : 0u);
} }
} }
@@ -7341,35 +7257,6 @@ public static partial class AgcExports
return hash; return hash;
} }
private enum CbColorMode : byte
{
Disable = 0,
Normal = 1,
EliminateFastClear = 2,
Resolve = 3,
FmaskDecompress = 5,
DccDecompress = 6,
}
private static bool TryGetCbColorControlMode(
IReadOnlyDictionary<uint, uint> registers,
out uint mode)
{
mode = 0;
if (!registers.TryGetValue(CbColorControl, out var colorControl))
{
return false;
}
mode = (colorControl >> 4) & 0x7u;
return true;
}
private static bool IsCbMetadataColorMode(uint mode) =>
mode is (uint)CbColorMode.EliminateFastClear or
(uint)CbColorMode.FmaskDecompress or
(uint)CbColorMode.DccDecompress;
private static bool TryGetHardwareColorResolveTargets( private static bool TryGetHardwareColorResolveTargets(
IReadOnlyDictionary<uint, uint> registers, IReadOnlyDictionary<uint, uint> registers,
out RenderTargetDescriptor source, out RenderTargetDescriptor source,
@@ -7377,8 +7264,8 @@ public static partial class AgcExports
{ {
source = default; source = default;
destination = default; destination = default;
if (!TryGetCbColorControlMode(registers, out var mode) || if (!registers.TryGetValue(CbColorControl, out var colorControl) ||
mode != (uint)CbColorMode.Resolve) ((colorControl >> 4) & 0x7u) != 3u)
{ {
return false; return false;
} }
@@ -8304,8 +8191,7 @@ public static partial class AgcExports
binding.OffsetBytes, binding.OffsetBytes,
binding.Data, binding.Data,
binding.DataLength, binding.DataLength,
binding.DataPooled, binding.DataPooled);
binding.PerInstance);
} }
return buffers; return buffers;
@@ -11102,14 +10988,18 @@ public static partial class AgcExports
return false; return false;
} }
if (!TryReadUInt32(ctx, shRegistersAddress, out var loRegister) ||
!TryReadUInt32(ctx, shRegistersAddress + 8, out var hiRegister))
{
return false;
}
var expectedLo = shaderType switch var expectedLo = shaderType switch
{ {
0 => ComputePgmLo, 0 => ComputePgmLo,
1 => SpiShaderPgmLoPs, 1 => SpiShaderPgmLoPs,
2 or 6 => SpiShaderPgmLoEs, 2 or 6 => SpiShaderPgmLoEs,
3 => SpiShaderPgmLoVs,
4 => SpiShaderPgmLoGs, 4 => SpiShaderPgmLoGs,
5 => SpiShaderPgmLoHs,
7 => SpiShaderPgmLoLs, 7 => SpiShaderPgmLoLs,
_ => 0u, _ => 0u,
}; };
@@ -11118,187 +11008,20 @@ public static partial class AgcExports
0 => ComputePgmHi, 0 => ComputePgmHi,
1 => SpiShaderPgmHiPs, 1 => SpiShaderPgmHiPs,
2 or 6 => SpiShaderPgmHiEs, 2 or 6 => SpiShaderPgmHiEs,
3 => SpiShaderPgmHiVs,
4 => SpiShaderPgmHiGs, 4 => SpiShaderPgmHiGs,
5 => SpiShaderPgmHiHs,
7 => SpiShaderPgmHiLs, 7 => SpiShaderPgmHiLs,
_ => 0u, _ => 0u,
}; };
if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi)
// GTA V Enhanced hull shaders (type 5) put RSRC1/RSRC2 (0x10A/0x10B) at
// the front of the SH default table; PGM_LO/HI sit elsewhere (or are
// filled later via SetShRegisterDirect).
if (!TryFindShaderProgramRegisterPair(
ctx,
shRegistersAddress,
registerCount,
expectedLo,
expectedHi,
out var loEntryAddress,
out var hiEntryAddress,
out var foundLo,
out var foundHi))
{ {
TryReadUInt32(ctx, shRegistersAddress, out var firstLo); TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}");
// GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and
// omit PGM_LO/HI from the default table. Still succeed: the code VA
// lives at ShaderCodeOffset and later binder paths republish it.
if (shaderType == 5 && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs)
{
TraceCreateShader(
0,
headerAddress,
codeAddress,
$"skip-pgm-patch type=5 first_lo=0x{firstLo:X8}");
return true;
}
TraceCreateShader(
0,
headerAddress,
codeAddress,
$"unexpected-registers type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}");
return false; return false;
} }
var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL);
var hiValue = (uint)((codeAddress >> 40) & 0xFFUL); var hiValue = (uint)((codeAddress >> 40) & 0xFFUL);
if (!TryWriteUInt32(ctx, loEntryAddress + sizeof(uint), loValue) || return TryWriteUInt32(ctx, shRegistersAddress + sizeof(uint), loValue) &&
!TryWriteUInt32(ctx, hiEntryAddress + sizeof(uint), hiValue)) TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue);
{
return false;
}
if (foundLo != expectedLo || foundHi != expectedHi)
{
TraceCreateShader(
0,
headerAddress,
codeAddress,
$"patched-alt-registers type={shaderType} lo=0x{foundLo:X8} hi=0x{foundHi:X8}");
}
return true;
}
private static readonly (uint Lo, uint Hi)[] ShaderProgramRegisterPairs =
[
(ComputePgmLo, ComputePgmHi),
(SpiShaderPgmLoPs, SpiShaderPgmHiPs),
(SpiShaderPgmLoVs, SpiShaderPgmHiVs),
(SpiShaderPgmLoEs, SpiShaderPgmHiEs),
(SpiShaderPgmLoGs, SpiShaderPgmHiGs),
(SpiShaderPgmLoHs, SpiShaderPgmHiHs),
(SpiShaderPgmLoLs, SpiShaderPgmHiLs),
];
private static bool TryFindShaderProgramRegisterPair(
CpuContext ctx,
ulong shRegistersAddress,
byte registerCount,
uint preferredLo,
uint preferredHi,
out ulong loEntryAddress,
out ulong hiEntryAddress,
out uint foundLo,
out uint foundHi)
{
loEntryAddress = 0;
hiEntryAddress = 0;
foundLo = 0;
foundHi = 0;
ulong preferredLoAddress = 0;
ulong preferredHiAddress = 0;
ulong fallbackLoAddress = 0;
ulong fallbackHiAddress = 0;
uint fallbackLo = 0;
uint fallbackHi = 0;
for (uint index = 0; index < registerCount; index++)
{
var entryAddress = shRegistersAddress + ((ulong)index * 8);
if (!TryReadUInt32(ctx, entryAddress, out var offset))
{
return false;
}
if (preferredLo != 0 && offset == preferredLo)
{
preferredLoAddress = entryAddress;
}
else if (preferredHi != 0 && offset == preferredHi)
{
preferredHiAddress = entryAddress;
}
if (fallbackLoAddress != 0)
{
continue;
}
foreach (var pair in ShaderProgramRegisterPairs)
{
if (offset != pair.Lo)
{
continue;
}
// Prefer a contiguous LO/HI pair when present.
if (index + 1 < registerCount &&
TryReadUInt32(ctx, entryAddress + 8, out var nextOffset) &&
nextOffset == pair.Hi)
{
fallbackLoAddress = entryAddress;
fallbackHiAddress = entryAddress + 8;
fallbackLo = pair.Lo;
fallbackHi = pair.Hi;
break;
}
for (uint hiIndex = 0; hiIndex < registerCount; hiIndex++)
{
if (hiIndex == index)
{
continue;
}
var hiAddress = shRegistersAddress + ((ulong)hiIndex * 8);
if (!TryReadUInt32(ctx, hiAddress, out var hiOffset) || hiOffset != pair.Hi)
{
continue;
}
fallbackLoAddress = entryAddress;
fallbackHiAddress = hiAddress;
fallbackLo = pair.Lo;
fallbackHi = pair.Hi;
break;
}
break;
}
}
if (preferredLoAddress != 0 && preferredHiAddress != 0)
{
loEntryAddress = preferredLoAddress;
hiEntryAddress = preferredHiAddress;
foundLo = preferredLo;
foundHi = preferredHi;
return true;
}
if (fallbackLoAddress != 0 && fallbackHiAddress != 0)
{
loEntryAddress = fallbackLoAddress;
hiEntryAddress = fallbackHiAddress;
foundLo = fallbackLo;
foundHi = fallbackHi;
return true;
}
return false;
} }
private static bool IsEsGeometryShaderType(byte shaderType) => private static bool IsEsGeometryShaderType(byte shaderType) =>
-788
View File
@@ -1,788 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
namespace SharpEmu.Libs.Agc;
/// <summary>
/// AGC embedded vertex metadata. Locates
/// PtrVertexBufferTable / PtrVertexAttribDescTable and builds authoritative
/// attribute layouts that draw translation merges onto IR-discovered fetches.
/// </summary>
internal static class AgcVertexMetadata
{
private const ushort IllegalDirectOffset = 0xFFFF;
private const ulong ShaderUserDataOffset = 0x08;
private const ulong ShaderInputSemanticsOffset = 0x30;
private const ulong ShaderNumInputSemanticsOffset = 0x50;
internal enum AgcDirectResourceType : uint
{
PtrVertexBufferTable = 8,
PtrVertexAttribDescTable = 10,
Last = PtrVertexAttribDescTable,
}
internal readonly record struct VertexTableRegisters(
int VertexBufferReg,
int VertexAttribReg,
uint InputSemanticsCount,
ulong InputSemanticsAddress);
/// <summary>
/// One AGC attrib-table resource.
/// Representation: <see cref="SharpBase"/> is the V# base; attribute byte
/// offset is applied as <see cref="OffsetBytes"/> (Vulkan bind offset),
/// not folded into the base — avoids double-counting when the IR prolog
/// already bumped the sharp address.
/// </summary>
internal readonly record struct MetadataVertexResource(
uint Location,
uint Semantic,
uint HardwareMapping,
uint SizeInElements,
ulong SharpBase,
uint Stride,
uint OffsetBytes,
uint DataFormat,
uint NumberFormat,
uint ComponentCount,
bool PerInstance);
/// <summary>
/// Reads AGC user-data direct-resource offsets for the ES header mapped to
/// <paramref name="shaderCodeAddress"/>. Returns false when the header is
/// unknown or the tables are absent (attribute-less clears).
/// </summary>
internal static bool TryGetVertexTableRegisters(
CpuContext ctx,
ulong shaderCodeAddress,
ulong shaderHeaderAddress,
out VertexTableRegisters registers)
{
registers = new VertexTableRegisters(-1, -1, 0, 0);
if (shaderHeaderAddress == 0 ||
!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0)
{
return false;
}
// ShaderUserData layout:
// 0x00: uint16_t* direct_resource_offset
// 0x08: sharp_resource_offset[4]
// 0x28: eud_size_dw, srt_size_dw
// 0x2C: direct_resource_count
if (!TryReadUInt64(ctx, userDataAddress, out var directResourceOffset) ||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount))
{
return false;
}
var maxTypes = (uint)AgcDirectResourceType.Last + 1u;
if (directResourceCount > maxTypes || directResourceOffset == 0)
{
return false;
}
var vertexBufferReg = -1;
var vertexAttribReg = -1;
for (uint type = 0; type < directResourceCount; type++)
{
if (!TryReadUInt16(
ctx,
directResourceOffset + (type * sizeof(ushort)),
out var reg) ||
reg == IllegalDirectOffset)
{
continue;
}
switch ((AgcDirectResourceType)type)
{
case AgcDirectResourceType.PtrVertexBufferTable:
vertexBufferReg = reg;
break;
case AgcDirectResourceType.PtrVertexAttribDescTable:
vertexAttribReg = reg;
break;
}
}
if (vertexBufferReg < 0 || vertexAttribReg < 0)
{
return false;
}
if (!TryReadUInt64(
ctx,
shaderHeaderAddress + ShaderInputSemanticsOffset,
out var inputSemanticsAddress) ||
!TryReadUInt32(
ctx,
shaderHeaderAddress + ShaderNumInputSemanticsOffset,
out var inputSemanticsCount) ||
inputSemanticsCount == 0 ||
inputSemanticsAddress == 0)
{
return false;
}
registers = new VertexTableRegisters(
vertexBufferReg,
vertexAttribReg,
inputSemanticsCount,
inputSemanticsAddress);
return true;
}
/// <summary>
/// Builds attrib resources from AGC input_semantics + tables.
/// ShaderSemantic packing:
/// bits [7:0] semantic → attrib table index
/// bits [15:8] hardware_mapping → VGPR destination
/// bits [19:16] size_in_elements
/// </summary>
internal static bool TryBuildVertexResourcesFromMetadata(
CpuContext ctx,
IReadOnlyList<uint> scalarRegisters,
VertexTableRegisters tables,
out IReadOnlyList<MetadataVertexResource> resources)
{
resources = Array.Empty<MetadataVertexResource>();
if (tables.VertexAttribReg < 0 ||
tables.VertexBufferReg < 0 ||
tables.VertexAttribReg + 1 >= scalarRegisters.Count ||
tables.VertexBufferReg + 1 >= scalarRegisters.Count ||
tables.InputSemanticsCount == 0)
{
return false;
}
var attribTable =
((ulong)scalarRegisters[tables.VertexAttribReg + 1] << 32) |
scalarRegisters[tables.VertexAttribReg];
var bufferTable =
((ulong)scalarRegisters[tables.VertexBufferReg + 1] << 32) |
scalarRegisters[tables.VertexBufferReg];
if (attribTable == 0 || bufferTable == 0)
{
return false;
}
var built = new List<MetadataVertexResource>((int)tables.InputSemanticsCount);
for (uint i = 0; i < tables.InputSemanticsCount; i++)
{
if (!TryReadUInt32(
ctx,
tables.InputSemanticsAddress + (i * sizeof(uint)),
out var semanticWord))
{
return false;
}
// Attrib index is semantic bits [7:0], not hardware_mapping.
var semantic = semanticWord & 0xFFu;
var hardwareMapping = (semanticWord >> 8) & 0xFFu;
var sizeInElements = (semanticWord >> 16) & 0xFu;
if (!TryReadUInt32(ctx, attribTable + (semantic * sizeof(uint)), out var attribWord))
{
return false;
}
// Attrib dword: buffer index [4:0], format [13:5], offset [25:14], fetch [26].
var bufferIndex = attribWord & 0x1Fu;
var format = (attribWord >> 5) & 0x1FFu;
var offset = (attribWord >> 14) & 0xFFFu;
var fetchIndex = (attribWord >> 26) & 0x1u;
var sharpAddress = bufferTable + (bufferIndex * 16u);
if (!TryReadUInt32(ctx, sharpAddress, out var sharp0) ||
!TryReadUInt32(ctx, sharpAddress + 4, out var sharp1))
{
return false;
}
var sharpBase = sharp0 | ((ulong)(sharp1 & 0xFFFFu) << 32);
var stride = (sharp1 >> 16) & 0x3FFFu;
if (sharpBase == 0 || stride == 0)
{
continue;
}
var fallbackComponents = sizeInElements != 0 ? sizeInElements : 4u;
var (dataFormat, numberFormat, components) =
MapAttribFormat(format, fallbackComponents);
built.Add(new MetadataVertexResource(
Location: i,
Semantic: semantic,
HardwareMapping: hardwareMapping,
SizeInElements: sizeInElements,
SharpBase: sharpBase,
Stride: stride,
OffsetBytes: offset,
DataFormat: dataFormat,
NumberFormat: numberFormat,
ComponentCount: components,
PerInstance: fetchIndex != 0));
}
if (built.Count == 0)
{
return false;
}
resources = built;
return true;
}
/// <summary>
/// Patch IR-discovered fetches from the attrib table onto the V# format/offset.
/// Prefer 1:1 Location pairing when counts match on one interleaved stream
/// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases
/// BaseAddress/Data/Location/Pc/PerInstance.
/// </summary>
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
CpuContext ctx,
IReadOnlyList<uint> scalarRegisters,
VertexTableRegisters tables,
IReadOnlyList<Gen5VertexInputBinding> discovered)
{
if (discovered.Count == 0 ||
!TryBuildVertexResourcesFromMetadata(
ctx,
scalarRegisters,
tables,
out var resources))
{
return discovered;
}
if (TryMergeByLocationPairing(discovered, resources, out var paired))
{
return paired;
}
var merged = new List<Gen5VertexInputBinding>(discovered.Count);
var usedResources = new bool[resources.Count];
var changed = false;
foreach (var input in discovered)
{
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset))
{
merged.Add(input);
continue;
}
var refined = ApplyMetadataFormat(input, resource, fillOffset);
changed |= refined != input;
merged.Add(refined);
}
return changed ? merged : discovered;
}
/// <summary>
/// When discovery and metadata describe the same interleaved stream with
/// equal attribute counts, pair by sorted Location (semantic order).
/// Keeps each binding's Pc/Location for SPIR-V; overlays format + offset.
/// </summary>
private static bool TryMergeByLocationPairing(
IReadOnlyList<Gen5VertexInputBinding> discovered,
IReadOnlyList<MetadataVertexResource> resources,
out IReadOnlyList<Gen5VertexInputBinding> merged)
{
merged = discovered;
if (discovered.Count != resources.Count || discovered.Count == 0)
{
return false;
}
var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray();
var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
var streamBase = orderedResources[0].SharpBase;
var streamStride = orderedResources[0].Stride;
for (var index = 0; index < orderedResources.Length; index++)
{
var resource = orderedResources[index];
var input = orderedInputs[index];
if (resource.SharpBase != streamBase ||
resource.Stride != streamStride ||
(input.Stride != 0 && input.Stride != streamStride) ||
!IsSameVertexStream(input, resource))
{
return false;
}
}
var byPc = new Dictionary<uint, Gen5VertexInputBinding>(discovered.Count);
var changed = false;
for (var index = 0; index < orderedInputs.Length; index++)
{
var input = orderedInputs[index];
var resource = orderedResources[index];
var fillOffset = input.BaseAddress == resource.SharpBase ||
IsAddressInsideCapturedSpan(input, resource.SharpBase);
var refined = ApplyMetadataFormat(input, resource, fillOffset);
changed |= refined != input;
byPc[input.Pc] = refined;
}
if (!changed)
{
return false;
}
var result = new Gen5VertexInputBinding[discovered.Count];
for (var index = 0; index < discovered.Count; index++)
{
result[index] = byPc[discovered[index].Pc];
}
merged = result;
return true;
}
private static Gen5VertexInputBinding ApplyMetadataFormat(
Gen5VertexInputBinding input,
MetadataVertexResource resource,
bool fillOffsetBytes)
{
var components = input.ComponentCount != 0 &&
input.ComponentCount < resource.ComponentCount
? input.ComponentCount
: resource.ComponentCount;
return input with
{
DataFormat = resource.DataFormat,
NumberFormat = resource.NumberFormat,
ComponentCount = components,
OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes,
};
}
/// <summary>
/// Legacy entry point — forwards to <see cref="MergeVertexInputsFromMetadata"/>.
/// </summary>
internal static IReadOnlyList<Gen5VertexInputBinding> RefineVertexInputs(
CpuContext ctx,
IReadOnlyList<uint> scalarRegisters,
VertexTableRegisters tables,
IReadOnlyList<Gen5VertexInputBinding> discovered) =>
MergeVertexInputsFromMetadata(ctx, scalarRegisters, tables, discovered);
/// <summary>
/// Collects SBufferLoad / SLoad PCs that read the AGC attrib or buffer
/// tables (embedded-fetch prolog). Those loads are executed on the
/// CPU during scalar evaluation; once vertex inputs are bound they must
/// not run again as live SSBOs on the GPU.
/// </summary>
internal static HashSet<uint> CollectFetchPrologPcs(
Gen5ShaderProgram program,
VertexTableRegisters tables)
{
var pcs = new HashSet<uint>();
if (tables.VertexAttribReg < 0 || tables.VertexBufferReg < 0)
{
return pcs;
}
var tableRegs = new HashSet<uint>
{
(uint)tables.VertexAttribReg,
(uint)tables.VertexAttribReg + 1u,
(uint)tables.VertexBufferReg,
(uint)tables.VertexBufferReg + 1u,
};
foreach (var instruction in program.Instructions)
{
var isScalarLoad =
instruction.Opcode.StartsWith("SBufferLoad", StringComparison.Ordinal) ||
instruction.Opcode.StartsWith("SLoad", StringComparison.Ordinal);
if (!isScalarLoad)
{
continue;
}
// SMEM loads encode the scalar base pointer in Sources[0].
if (instruction.Sources.Count > 0 &&
instruction.Sources[0] is
{
Kind: Gen5OperandKind.ScalarRegister,
Value: var scalarBase,
} &&
tableRegs.Contains(scalarBase))
{
pcs.Add(instruction.Pc);
continue;
}
if (instruction.Control is Gen5BufferMemoryControl buffer &&
tableRegs.Contains(buffer.ScalarResource))
{
pcs.Add(instruction.Pc);
}
}
return pcs;
}
private static bool TryMatchMetadataResource(
Gen5VertexInputBinding input,
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
out MetadataVertexResource resource,
out bool fillOffsetBytes)
{
resource = default;
fillOffsetBytes = false;
var bestScore = int.MinValue;
var bestIndex = -1;
var bestFillOffset = false;
for (var index = 0; index < resources.Count; index++)
{
if (usedResources[index])
{
continue;
}
var candidate = resources[index];
if (candidate.Stride != 0 &&
input.Stride != 0 &&
candidate.Stride != input.Stride)
{
continue;
}
if (!IsSameVertexStream(input, candidate))
{
continue;
}
var attrAddress = candidate.SharpBase + candidate.OffsetBytes;
var score = int.MinValue;
var fillOffset = false;
// Post-capture interleaved: shared BaseAddress, distinct OffsetBytes.
if (input.OffsetBytes == candidate.OffsetBytes &&
(input.BaseAddress == candidate.SharpBase ||
IsAddressInsideCapturedSpan(input, candidate.SharpBase)))
{
score = 400;
}
// IR prolog baked attrib offset into the V# base.
else if (input.BaseAddress == attrAddress)
{
score = 350;
}
// Discovery never saw the attrib offset — only safe when this
// resource's offset uniquely identifies it among unused entries.
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes != 0 &&
IsUniqueUnusedOffset(resources, usedResources, candidate.OffsetBytes, index))
{
score = 300;
fillOffset = true;
}
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes == 0)
{
score = 250;
}
if (score > bestScore)
{
bestScore = score;
bestIndex = index;
bestFillOffset = fillOffset;
}
}
// Require an offset-aware match. Bare SharpBase ties (score 250) are
// only accepted when a single unused resource remains for that stream.
if (bestIndex < 0 || bestScore < 300)
{
if (bestIndex < 0 || bestScore < 250)
{
return false;
}
var unusedSameStream = 0;
for (var index = 0; index < resources.Count; index++)
{
if (!usedResources[index] && IsSameVertexStream(input, resources[index]))
{
unusedSameStream++;
}
}
if (unusedSameStream != 1)
{
return false;
}
}
usedResources[bestIndex] = true;
resource = resources[bestIndex];
fillOffsetBytes = bestFillOffset;
return true;
}
private static bool IsSameVertexStream(
Gen5VertexInputBinding input,
MetadataVertexResource resource)
{
if (input.BaseAddress == resource.SharpBase ||
input.BaseAddress == resource.SharpBase + resource.OffsetBytes)
{
return true;
}
return IsAddressInsideCapturedSpan(input, resource.SharpBase);
}
private static bool IsAddressInsideCapturedSpan(
Gen5VertexInputBinding input,
ulong address) =>
input.DataLength > 0 &&
address >= input.BaseAddress &&
address < input.BaseAddress + (ulong)input.DataLength;
private static bool IsUniqueUnusedOffset(
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
uint offsetBytes,
int candidateIndex)
{
for (var index = 0; index < resources.Count; index++)
{
if (index == candidateIndex || usedResources[index])
{
continue;
}
if (resources[index].OffsetBytes == offsetBytes)
{
return false;
}
}
return true;
}
/// <summary>
/// Attrib-table format
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
/// Unknown values pass through (already BufferFormat).
/// </summary>
private static uint VertexAttribFormatToBufferFormat(uint format) =>
format switch
{
0 => 0, // Invalid
4 => 1, // k8UNorm
8 => 2, // k8SNorm
12 => 3, // k8UScaled
16 => 4, // k8SScaled
20 => 5, // k8UInt
24 => 6, // k8SInt
28 => 7, // k16UNorm
32 => 8, // k16SNorm
36 => 9, // k16UScaled
40 => 10, // k16SScaled
44 => 11, // k16UInt
48 => 12, // k16SInt
52 => 13, // k16Float
57 => 14, // k8_8UNorm
61 => 15, // k8_8SNorm
65 => 16, // k8_8UScaled
69 => 17, // k8_8SScaled
73 => 18, // k8_8UInt
77 => 19, // k8_8SInt
80 => 20, // k32UInt
84 => 21, // k32SInt
88 => 22, // k32Float
93 => 23, // k16_16UNorm
97 => 24, // k16_16SNorm
101 => 25, // k16_16UScaled
105 => 26, // k16_16SScaled
109 => 27, // k16_16UInt
113 => 28, // k16_16SInt
117 => 29, // k16_16Float
122 => 30, // k11_11_10UNorm
126 => 31,
130 => 32,
134 => 33,
138 => 34,
142 => 35,
146 => 36,
150 => 37, // k10_11_11UNorm
154 => 38,
158 => 39,
162 => 40,
166 => 41,
170 => 42,
174 => 43,
179 => 44, // k2_10_10_10UNorm
183 => 45,
187 => 46,
191 => 47,
195 => 48,
199 => 49,
203 => 50, // k10_10_10_2UNorm
207 => 51,
211 => 52,
215 => 53,
219 => 54,
223 => 55,
227 => 56, // k8_8_8_8UNorm
231 => 57,
235 => 58,
239 => 59,
243 => 60,
247 => 61,
249 => 62, // k32_32UInt
253 => 63,
257 => 64, // k32_32Float
263 => 65, // k16_16_16_16UNorm
267 => 66,
271 => 67,
275 => 68,
279 => 69,
283 => 70,
287 => 71, // k16_16_16_16Float
290 => 72, // k32_32_32UInt
294 => 73,
298 => 74,
303 => 75, // k32_32_32_32UInt
307 => 76,
311 => 77, // k32_32_32_32Float
_ => format,
};
/// <summary>
/// Maps Prospero attrib-table formats onto GNM (DataFormat, NumberFormat,
/// Components) for <c>ToVkVertexFormat</c>. Accepts VertexAttribFormat
/// or BufferFormat (pass-through). NumberFormat: 0 Unorm, 1 SNorm,
/// 2 UScaled, 3 SScaled, 4 UInt, 5 SInt, 7 Float.
/// </summary>
private static (uint DataFormat, uint NumberFormat, uint Components) MapAttribFormat(
uint attribFormat,
uint fallbackComponents)
{
// Prospero VertexAttribFormat quirks before BufferFormat conversion.
if (attribFormat == 113)
{
return (14, 7, 4); // R32G32B32A32_SFLOAT
}
if (attribFormat == 121)
{
return (5, 7, 2); // R16G16_SFLOAT
}
var bufferFormat = VertexAttribFormatToBufferFormat(attribFormat);
// Prospero::BufferFormat numeric values (gpu_defs.h).
return bufferFormat switch
{
1 => (1, 0, 1), // k8UNorm
2 => (1, 1, 1), // k8SNorm
3 => (1, 2, 1), // k8UScaled
4 => (1, 3, 1), // k8SScaled
5 => (1, 4, 1), // k8UInt
6 => (1, 5, 1), // k8SInt
7 => (2, 0, 1), // k16UNorm
8 => (2, 1, 1), // k16SNorm
9 => (2, 2, 1), // k16UScaled
10 => (2, 3, 1), // k16SScaled
11 => (2, 4, 1), // k16UInt
12 => (2, 5, 1), // k16SInt
13 => (2, 7, 1), // k16Float
14 => (3, 0, 2), // k8_8UNorm
15 => (3, 1, 2), // k8_8SNorm
16 => (3, 2, 2), // k8_8UScaled
17 => (3, 3, 2), // k8_8SScaled
18 => (3, 4, 2), // k8_8UInt
19 => (3, 5, 2), // k8_8SInt
20 => (4, 4, 1), // k32UInt
21 => (4, 5, 1), // k32SInt
22 => (4, 7, 1), // k32Float
23 => (5, 0, 2), // k16_16UNorm
24 => (5, 1, 2), // k16_16SNorm
25 => (5, 2, 2), // k16_16UScaled
26 => (5, 3, 2), // k16_16SScaled
27 => (5, 4, 2), // k16_16UInt
28 => (5, 5, 2), // k16_16SInt
29 => (5, 7, 2), // k16_16Float
50 => (9, 0, 4), // k10_10_10_2UNorm
51 => (9, 1, 4), // k10_10_10_2SNorm
56 => (10, 0, 4), // k8_8_8_8UNorm
57 => (10, 1, 4), // k8_8_8_8SNorm
58 => (10, 2, 4), // k8_8_8_8UScaled
59 => (10, 3, 4), // k8_8_8_8SScaled
60 => (10, 4, 4), // k8_8_8_8UInt
61 => (10, 5, 4), // k8_8_8_8SInt
62 => (11, 4, 2), // k32_32UInt
63 => (11, 5, 2), // k32_32SInt
64 => (11, 7, 2), // k32_32Float
65 => (12, 0, 4), // k16_16_16_16UNorm
66 => (12, 1, 4), // k16_16_16_16SNorm
67 => (12, 2, 4), // k16_16_16_16UScaled
68 => (12, 3, 4), // k16_16_16_16SScaled
69 => (12, 4, 4), // k16_16_16_16UInt
70 => (12, 5, 4), // k16_16_16_16SInt
71 => (12, 7, 4), // k16_16_16_16Float
72 => (13, 4, 3), // k32_32_32UInt
73 => (13, 5, 3), // k32_32_32SInt
74 => (13, 7, 3), // k32_32_32Float
75 => (14, 4, 4), // k32_32_32_32UInt
76 => (14, 5, 4), // k32_32_32_32SInt
77 => (14, 7, 4), // k32_32_32_32Float
_ => (14, 7, Math.Clamp(fallbackComponents, 1u, 4u)),
};
}
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> buffer = stackalloc byte[2];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(buffer);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[4];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[8];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
}
+70 -388
View File
@@ -15,40 +15,16 @@ public static class AudioOut2Exports
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately // Clearing 0x80 bytes here overwrote the caller's stack canary immediately
// following the 0x40-byte parameter block. // following the 0x40-byte parameter block.
private const int AudioOut2ContextParamSize = 0x40; private const int AudioOut2ContextParamSize = 0x40;
// Keep these modest. Some Prospero titles stack-allocate QueryMemory results private const int AudioOut2ContextMemorySize = 0x10000;
// next to the frame canary: a 16-byte {size,align} write to [rbp-0x38] plants private const int AudioOut2ContextMemoryAlignment = 0x10000;
// align at [rbp-0x30] (observed canary=0x100). Size-only (8 bytes) on stack.
private const int AudioOut2ContextMemorySize = 0x4000;
private const int AudioOut2ContextMemoryAlignment = 0x100;
// Exact object body size. Do not page-align to 64K — callers that
// stack-allocate from this size planted 0x10000 on the canary with a 64K VLA.
private const int SpeakerArrayHeaderSize = 0x40;
private const int SpeakerArrayEntrySize = 0x100;
// Extra scratch the title writes after the per-channel entries (coefficients).
private const int SpeakerArrayScratchBytes = 0x400;
private const uint SpeakerArrayDefaultChannels = 8;
private const uint SpeakerArrayMaxChannels = 32;
// Field read by titles at object+0x34 (mov eax,[rbx+0x34]).
private const int SpeakerArrayDivisorFieldOffset = 0x34;
private const int SpeakerArrayResultFieldOffset = 0x3C;
private const uint SpeakerArrayDefaultDivisor = 1;
private const int SpeakerArrayCoefficientBytes = 0x400;
// OrbisAudioOutPortState is 0x20 bytes. Never grow this from r8/r9 — those
// regs arrive polluted with GetSize leftovers (0x840/0x10C/0x180) and caused
// PortGetState/GetSpeakerInfo to overwrite the speaker-array param block
// (param+0x18 == first PortGetState out) and smash the Main Thread canary
// with ContextMemoryAlignment (0x100).
private const int PortStateSize = 0x20;
private const int SpeakerInfoSize = 0x20;
private const ushort PortStateOutputConnectedPrimary = 0x01;
private static long _nextContextHandle = 1; private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1; private static long _nextUserHandle = 1;
private static int _nextPortId; private static int _nextPortId;
private static long _pushTraceCount; private static long _pushTraceCount;
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new(); // Per-context audio parameters captured at ContextCreate so ContextAdvance
// can pace to the real playback cadence (grain samples at the sample rate).
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new(); private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
private static readonly ConcurrentDictionary<ulong, int> Ports = new();
private sealed class ContextState private sealed class ContextState
{ {
@@ -66,6 +42,9 @@ public static class AudioOut2Exports
public uint Channels { get; } public uint Channels { get; }
public uint GrainSamples { get; } public uint GrainSamples { get; }
// Blocks the advancing thread until one grain worth of wall-clock time
// has elapsed since the previous advance, matching hardware timing so
// audio-gated titles neither spin nor drift ahead.
public void PaceAdvance() public void PaceAdvance()
{ {
long delay; long delay;
@@ -133,36 +112,19 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx) public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{ {
var paramAddress = ctx[CpuRegister.Rdi]; var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0) if (paramAddress == 0 || memoryInfoAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Heap: {size, alignment} (16 bytes), matching sceAudioPropagationSystemQueryMemory. Span<byte> memoryInfo = stackalloc byte[0x20];
// Stack: SIZE ONLY as a full ulong (8 bytes). Writing alignment at +8 is how
// [rbp-0x30] became 0x100 on GTA V Enhanced. Do NOT shrink this to uint32 —
// Main reads the out as a 64-bit size; a 4-byte write leaves a garbage high
// dword (observed 0x7<<32|0x4000) and the allocator aborts with int 0x41.
if (IsGuestStackAddress(memoryInfoAddress))
{
Span<byte> sizeOnly = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(sizeOnly, AudioOut2ContextMemorySize);
TraceAudioOut2(
$"context-query-memory stack-size-only out=0x{memoryInfoAddress:X} " +
$"size=0x{AudioOut2ContextMemorySize:X}");
return ctx.Memory.TryWrite(memoryInfoAddress, sizeOnly)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Span<byte> memoryInfo = stackalloc byte[0x10];
memoryInfo.Clear(); memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
TraceAudioOut2( BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
$"context-query-memory out=0x{memoryInfoAddress:X} " + BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
$"size=0x{AudioOut2ContextMemorySize:X} align=0x{AudioOut2ContextMemoryAlignment:X}");
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo) return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0) ? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -184,6 +146,8 @@ public static class AudioOut2Exports
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Read channels/frequency/grain from the reset-param blob so the
// context can pace advances to the real audio cadence.
uint channels = 2; uint channels = 2;
uint frequency = 48000; uint frequency = 48000;
uint grain = 256; uint grain = 256;
@@ -195,6 +159,8 @@ public static class AudioOut2Exports
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]); var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
if (pc is > 0 and <= 8) channels = pc; if (pc is > 0 and <= 8) channels = pc;
if (pf is >= 8000 and <= 192000) frequency = pf; if (pf is >= 8000 and <= 192000) frequency = pf;
// Values below one cache line are flags/counts in observed PS5
// callers, not audio grains. Keep the hardware-sized default.
if (pg is >= 64 and <= 0x4000) grain = pg; if (pg is >= 64 and <= 0x4000) grain = pg;
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}"); TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
} }
@@ -233,16 +199,17 @@ public static class AudioOut2Exports
public static int AudioOut2ContextPush(CpuContext ctx) public static int AudioOut2ContextPush(CpuContext ctx)
{ {
var handle = ctx[CpuRegister.Rdi]; var handle = ctx[CpuRegister.Rdi];
if (Interlocked.Increment(ref _pushTraceCount) <= 8) var traceCount = Interlocked.Increment(ref _pushTraceCount);
if (traceCount <= 16)
{ {
TraceAudioOut2($"context-push handle=0x{handle:X} data=0x{ctx[CpuRegister.Rsi]:X}"); TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
} }
// FMOD's PS5 output path uses ContextPush as the submission clock and
// does not call ContextAdvance. Pace pushes to one hardware grain so
// the feeder cannot outrun playback and starve the title.
if (Contexts.TryGetValue(handle, out var context)) if (Contexts.TryGetValue(handle, out var context))
{ {
// FMOD's PS5 output path uses ContextPush as the submission clock
// and does not call ContextAdvance. Pace pushes to one hardware
// grain so the feeder cannot outrun playback and starve the game.
context.PaceAdvance(); context.PaceAdvance();
} }
@@ -256,9 +223,11 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextAdvance(CpuContext ctx) public static int AudioOut2ContextAdvance(CpuContext ctx)
{ {
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var state)) // Advancing renders one grain of audio on hardware; pace it to the same
// wall-clock cadence so the guest audio thread runs at the right speed.
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
{ {
state.PaceAdvance(); context.PaceAdvance();
} }
return SetReturn(ctx, 0); return SetReturn(ctx, 0);
@@ -271,35 +240,38 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx) public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
{ {
// ABI out is a 32-bit queue depth (callers compare dword [out]). A // The advance path paces synchronously, so the queue is always drained.
// uint64 write into a stack slot at [rbp-0x14] next to the canary at var levelAddress = ctx[CpuRegister.Rsi];
// [rbp-0x10] zeroed the canary low half and aborted the audio thread. if (levelAddress != 0)
var outLevelAddress = ctx[CpuRegister.Rsi];
if (outLevelAddress == 0)
{ {
outLevelAddress = ctx[CpuRegister.Rdx]; _ = TryWriteUInt64(ctx, levelAddress, 0);
}
if (outLevelAddress != 0)
{
Span<byte> level = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(level, 0);
if (!ctx.Memory.TryWrite(outLevelAddress, level))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
} }
return SetReturn(ctx, 0); return SetReturn(ctx, 0);
} }
[SysAbiExport( [SysAbiExport(
Nid = "Q8DZkKQ-SYc", Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2LoContextGetQueueLevel", ExportName = "sceAudioOut2PortCreate",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2LoContextGetQueueLevel(CpuContext ctx) => public static int AudioOut2PortCreate(CpuContext ctx)
AudioOut2ContextGetQueueLevel(ctx); {
var type = unchecked((int)ctx[CpuRegister.Rdi]);
var paramAddress = ctx[CpuRegister.Rsi];
var outPortAddress = ctx[CpuRegister.Rdx];
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return TryWriteUInt64(ctx, outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport( [SysAbiExport(
Nid = "8XTArSPyWHk", Nid = "8XTArSPyWHk",
@@ -308,39 +280,6 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0); public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2PortCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortCreate(CpuContext ctx)
{
// rdi=user/context, rsi=type, rdx=outPort* (fallback rcx if rdx unusable).
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
if (outPortAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = unchecked((int)ctx[CpuRegister.Rsi]);
if (type is < 0 or > 0x100)
{
type = 0;
}
var portId = (uint)Interlocked.Increment(ref _nextPortId);
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
Ports[handle] = type;
if (!TryWriteUInt64(ctx, outPortAddress, handle))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAudioOut2($"port-create handle=0x{handle:X} type={type} out=0x{outPortAddress:X}");
return SetReturn(ctx, 0);
}
// Fixed-size connected stereo state. Do not trust r8/r9 for byte counts.
[SysAbiExport( [SysAbiExport(
Nid = "gatEUKG+Ea4", Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState", ExportName = "sceAudioOut2PortGetState",
@@ -348,44 +287,27 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortGetState(CpuContext ctx) public static int AudioOut2PortGetState(CpuContext ctx)
{ {
var portHandle = ctx[CpuRegister.Rdi]; var handle = ctx[CpuRegister.Rdi];
var stateAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0) if (handle == 0 || stateAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Stack out-buffers with garbage handles were writing 0x20 bytes over var type = (int)((handle >> 16) & 0xFF);
// caller frames / canaries (state=0x7FFFDE1FF688 right before fail). Span<byte> state = stackalloc byte[0x20];
// Heap outs still get a real state blob even when the handle wasn't
// minted by PortCreate — some titles synthesize port ids themselves.
if (IsGuestStackAddress(stateAddress))
{
TraceAudioOut2(
$"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}");
return SetReturn(ctx, 0);
}
Span<byte> state = stackalloc byte[PortStateSize];
state.Clear(); state.Clear();
// +0x00 u16 output = CONNECTED_PRIMARY (1) var output = type == 2 ? 0x40 : 0x01;
// +0x02 u8 channels = 2 var channels = type == 2 ? 1 : 2;
// +0x04 s16 volume = -1 (N/A for main) BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary); state[0x02] = unchecked((byte)channels);
state[0x02] = 2;
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1); BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
if (!ctx.Memory.TryWrite(stateAddress, state)) return ctx.Memory.TryWrite(stateAddress, state)
{ ? SetReturn(ctx, 0)
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAudioOut2(
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
return SetReturn(ctx, 0);
}
// rdi=out buffer, rsi=type/flag (not a pointer). Fixed-size write only.
[SysAbiExport( [SysAbiExport(
Nid = "DImz2Ft9E2g", Nid = "DImz2Ft9E2g",
ExportName = "sceAudioOut2GetSpeakerInfo", ExportName = "sceAudioOut2GetSpeakerInfo",
@@ -393,134 +315,21 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerInfo(CpuContext ctx) public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
{ {
var infoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdi], ctx[CpuRegister.Rdx]); var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0) if (infoAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Same rule as PortGetState — never bulk-write speaker info onto the stack. Span<byte> info = stackalloc byte[0x40];
if (IsGuestStackAddress(infoAddress))
{
TraceAudioOut2($"get-speaker-info skip-stack out=0x{infoAddress:X}");
return SetReturn(ctx, 0);
}
Span<byte> info = stackalloc byte[SpeakerInfoSize];
info.Clear(); info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 2); BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 48000); BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
BinaryPrimitives.WriteUInt16LittleEndian(info[0x08..], PortStateOutputConnectedPrimary); BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
if (!ctx.Memory.TryWrite(infoAddress, info)) return ctx.Memory.TryWrite(infoAddress, info)
{ ? SetReturn(ctx, 0)
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAudioOut2(
$"get-speaker-info out=0x{infoAddress:X} type=0x{ctx[CpuRegister.Rsi]:X} bytes=0x{SpeakerInfoSize:X}");
return SetReturn(ctx, 0);
}
// Matches sceAudio3dGetSpeakerArrayMemorySize(uiNumSpeakers, bIs3d): size is
// returned directly in rax. Exact channel-scaled body — never a 64K slab.
[SysAbiExport(
Nid = "G1YOKDJYX2Y",
ExportName = "sceAudioOut2GetSpeakerArrayMemorySize",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerArrayMemorySize(CpuContext ctx)
{
var numChannels = (uint)ctx[CpuRegister.Rdi];
if (numChannels == 0 || numChannels > SpeakerArrayMaxChannels)
{
numChannels = SpeakerArrayDefaultChannels;
}
var size = ComputeSpeakerArrayBytes(numChannels);
TraceAudioOut2(
$"speaker-array-get-size rdi=0x{ctx[CpuRegister.Rdi]:X} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} -> 0x{size:X}");
ctx[CpuRegister.Rax] = unchecked((ulong)size);
return size;
}
[SysAbiExport(
Nid = "4BlZurolOAo",
ExportName = "sceAudioOut2GetSpeakerArrayCoefficients",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerArrayCoefficients(CpuContext ctx) =>
WriteZeroSpeakerArrayCoefficients(ctx, "coefficients");
[SysAbiExport(
Nid = "28QqMnuuJ9Y",
ExportName = "sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerArrayAmbisonicsCoefficients(CpuContext ctx) =>
WriteZeroSpeakerArrayCoefficients(ctx, "ambisonics-coefficients");
// rdi = param (may share a heap slab with PortGetState/GetSpeakerInfo outs —
// do NOT read buffer*/size* from it). rsi = &outHandle, rdx = reserved/size
// slot (leave alone), rcx = channels. Always heap-allocate a fresh object.
[SysAbiExport(
Nid = "+k91hoTuoA8",
ExportName = "sceAudioOut2SpeakerArrayCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2SpeakerArrayCreate(CpuContext ctx)
{
var param = ctx[CpuRegister.Rdi];
var outHandleAddress = ctx[CpuRegister.Rsi];
var outReservedAddress = ctx[CpuRegister.Rdx];
var channels = (uint)ctx[CpuRegister.Rcx];
if (channels == 0 || channels > SpeakerArrayMaxChannels)
{
channels = SpeakerArrayDefaultChannels;
}
if (outHandleAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var bytes = ComputeSpeakerArrayBytes(channels);
if (!TryAllocateSpeakerArrayMemory(ctx, (ulong)bytes, out var memory) ||
!InitializeSpeakerArrayObject(ctx, memory, channels))
{
TraceAudioOut2(
$"speaker-array-create alloc-failed bytes=0x{bytes:X} channels={channels}");
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
SpeakerArrays[memory] = 0;
// Publish ONLY the out-handle slot. rdx is often an adjacent size /
// reserved local on the caller stack — writing it previously fed canary
// corruption.
if (!TryWriteUInt64(ctx, outHandleAddress, memory))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAudioOut2(
$"speaker-array-create object=0x{memory:X} bytes=0x{bytes:X} " +
$"channels={channels} param=0x{param:X} out=0x{outHandleAddress:X} " +
$"reserved=0x{outReservedAddress:X} (untouched)");
ctx[CpuRegister.Rax] = memory;
return 0;
}
[SysAbiExport(
Nid = "erCWQR5eKiQ",
ExportName = "sceAudioOut2SpeakerArrayDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2SpeakerArrayDestroy(CpuContext ctx)
{
SpeakerArrays.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, 0);
} }
[SysAbiExport( [SysAbiExport(
@@ -528,11 +337,7 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2PortDestroy", ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
{
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, 0);
}
[SysAbiExport( [SysAbiExport(
Nid = "IaZXJ9M79uo", Nid = "IaZXJ9M79uo",
@@ -562,129 +367,6 @@ public static class AudioOut2Exports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
private static int ComputeSpeakerArrayBytes(uint channels) =>
SpeakerArrayHeaderSize + (int)(channels * SpeakerArrayEntrySize) + SpeakerArrayScratchBytes;
private static bool InitializeSpeakerArrayObject(CpuContext ctx, ulong memory, uint channels)
{
// Header only — never wipe the full GetSize slab (and never touch stack).
Span<byte> body = stackalloc byte[SpeakerArrayHeaderSize];
body.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(body[0x00..], (uint)SpeakerArrayHeaderSize);
BinaryPrimitives.WriteUInt32LittleEndian(body[0x04..], channels);
BinaryPrimitives.WriteUInt32LittleEndian(body[SpeakerArrayDivisorFieldOffset..], SpeakerArrayDefaultDivisor);
BinaryPrimitives.WriteUInt32LittleEndian(body[SpeakerArrayResultFieldOffset..], 0);
return ctx.Memory.TryWrite(memory, body);
}
// Prefer the high guest arena (0x6000_xxxx). TryAllocateHleData advances
// _nextVirtualAddress into the title's direct-memory window (~0x1559_xxxx);
// publishing an object there made sceKernelBatchMap(fixed, 0x1559C80000,
// 0x20000) return NOT_FOUND and abort RenderThread with int 0x41.
// Never mint the old 0x1559C0xxxx "cookie" pointers — they are unmapped and
// collide with dmem VAs.
private static bool TryAllocateSpeakerArrayMemory(CpuContext ctx, ulong bytes, out ulong memory)
{
memory = 0;
var length = Math.Max(bytes, 0x1000UL);
if (TryAllocateViaGuestAllocator(ctx, length, 0x1000, out memory) &&
IsSafeSpeakerArrayAddress(memory))
{
return true;
}
if (Kernel.KernelMemoryCompatExports.TryAllocateHleData(ctx, length, 0x1000, out memory) &&
IsSafeSpeakerArrayAddress(memory))
{
return true;
}
memory = 0;
return false;
}
private static bool TryAllocateViaGuestAllocator(CpuContext ctx, ulong length, ulong alignment, out ulong memory)
{
memory = 0;
var allocator = ctx.Memory as IGuestMemoryAllocator;
if (allocator is null && ctx.Memory is ICpuMemoryWrapper { Inner: IGuestMemoryAllocator inner })
{
allocator = inner;
}
return allocator is not null && allocator.TryAllocateGuestMemory(length, alignment, out memory);
}
private static bool IsSafeSpeakerArrayAddress(ulong value) =>
IsPlausibleGuestObjectPointer(value) &&
!IsGuestStackAddress(value) &&
!IsDirectMemoryWindowAddress(value);
// BatchMap fixed dmem VAs have been observed around 0x1559_xxxx_xxxx.
// Keep HLE speaker-array objects out of that window.
private static bool IsDirectMemoryWindowAddress(ulong value) =>
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
private static bool IsPlausibleGuestObjectPointer(ulong value) =>
value >= 0x1000_0000UL &&
value != 0x10000UL &&
value < 0x0000_8000_0000_0000UL;
// Windows user stacks sit in 0x00007FFFxxxxxxxx. Never treat those as
// heap objects we can bulk-initialize.
private static bool IsGuestStackAddress(ulong value) =>
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
{
// Accept heap or stack out-buffers (PortGetState legitimately uses both),
// but never small integers / size constants.
if (IsWritableOutBuffer(primary))
{
return primary;
}
if (IsWritableOutBuffer(secondary))
{
return secondary;
}
return 0;
}
private static bool IsWritableOutBuffer(ulong value) =>
value != 0 &&
value != 0x10000UL &&
value >= 0x1000UL &&
(IsPlausibleGuestObjectPointer(value) || IsGuestStackAddress(value));
private static int WriteZeroSpeakerArrayCoefficients(CpuContext ctx, string label)
{
var destination = ctx[CpuRegister.Rsi];
if (destination == 0)
{
destination = ctx[CpuRegister.Rdx];
}
// Coefficients are large — only wipe real heap objects, never stack.
if (destination != 0 &&
IsPlausibleGuestObjectPointer(destination) &&
!IsGuestStackAddress(destination))
{
Span<byte> zeros = stackalloc byte[SpeakerArrayCoefficientBytes];
zeros.Clear();
if (!ctx.Memory.TryWrite(destination, zeros))
{
TraceAudioOut2($"{label} write-failed dest=0x{destination:X}");
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceAudioOut2($"{label} ok dest=0x{destination:X}");
return SetReturn(ctx, 0);
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value) private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{ {
Span<byte> buffer = stackalloc byte[sizeof(ulong)]; Span<byte> buffer = stackalloc byte[sizeof(ulong)];
@@ -175,14 +175,6 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
// Same rule as AudioOut2 PortGetState: never bulk-write onto the caller
// stack. Some titles place small locals next to the canary; a full
// SceAudioOutPortState write smashes it.
if (IsGuestStackAddress(stateAddress))
{
return ctx.SetReturn(0);
}
// SceAudioOutPortState: report a connected primary output at full volume // SceAudioOutPortState: report a connected primary output at full volume
// so pacing/mixing code sees a live port. We do no host rerouting, so // so pacing/mixing code sees a live port. We do no host rerouting, so
// rerouteCounter and flag stay zero. // rerouteCounter and flag stay zero.
@@ -200,9 +192,6 @@ public static class AudioOutExports
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
private static bool IsGuestStackAddress(ulong value) =>
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
[SysAbiExport( [SysAbiExport(
Nid = "QOQtbeDqsT4", Nid = "QOQtbeDqsT4",
ExportName = "sceAudioOutOutput", ExportName = "sceAudioOutOutput",
@@ -68,7 +68,7 @@ internal static class AudioPcmConversion
value = Math.Clamp(value, -1.0f, 1.0f); value = Math.Clamp(value, -1.0f, 1.0f);
var scale = value < 0.0f ? 32768.0f : short.MaxValue; var scale = value < 0.0f ? 32768.0f : short.MaxValue;
return unchecked((short)MathF.Round(value * scale)); return checked((short)MathF.Round(value * scale));
} }
// <paramref name="volume"/> is expected pre-clamped to [0, 1] by the caller. // <paramref name="volume"/> is expected pre-clamped to [0, 1] by the caller.
-166
View File
@@ -21,11 +21,6 @@ public static class CxaGuardExports
} }
private static readonly ConcurrentDictionary<ulong, GuardState> _inProgress = new(); private static readonly ConcurrentDictionary<ulong, GuardState> _inProgress = new();
private static readonly ConcurrentDictionary<ulong, object> _onceGates = new();
private const int OnceUninitialized = 0;
private const int OnceInProgress = 1;
private const int OnceComplete = 2;
[SysAbiExport( [SysAbiExport(
Nid = "3GPpjQdAMTw", Nid = "3GPpjQdAMTw",
@@ -176,167 +171,6 @@ public static class CxaGuardExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "DiGVep5yB5w",
ExportName = "_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int ExecuteOnce(CpuContext ctx)
{
var onceAddress = ctx[CpuRegister.Rdi];
var callbackAddress = ctx[CpuRegister.Rsi];
var parameter = ctx[CpuRegister.Rdx];
if (onceAddress == 0 || callbackAddress == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (onceValue == OnceComplete)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var gate = _onceGates.GetOrAdd(onceAddress, static _ => new object());
lock (gate)
{
if (!ctx.TryReadInt32(onceAddress, out onceValue))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
while (onceValue == OnceInProgress)
{
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
if (!ctx.TryReadInt32(onceAddress, out onceValue))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
if (onceValue == OnceComplete)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!ctx.TryWriteInt32(onceAddress, OnceInProgress))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
var scheduler = GuestThreadExecution.Scheduler;
var callbackSucceeded = false;
string? callbackError = null;
var allocator = ctx.Memory as IGuestMemoryAllocator;
var hasScratchContext = false;
ulong scratchContextAddress = 0;
try
{
if (allocator is not null && allocator.TryAllocateGuestMemory(0x10, 0x10, out scratchContextAddress))
{
hasScratchContext = true;
_ = ctx.TryWriteUInt64(scratchContextAddress, 0);
_ = ctx.TryWriteUInt64(scratchContextAddress + 8, 0);
}
if (scheduler is null)
{
callbackError = "guest scheduler unavailable";
}
else if (scheduler.TryCallGuestFunction(
ctx,
callbackAddress,
onceAddress,
parameter,
scratchContextAddress,
0,
0,
"std::_Execute_once",
out var returnValue,
out callbackError))
{
callbackSucceeded = returnValue != 0;
}
}
finally
{
if (hasScratchContext && allocator is not null)
{
_ = allocator.TryFreeGuestMemory(scratchContextAddress);
}
}
lock (gate)
{
if (!callbackSucceeded)
{
_ = ctx.TryWriteInt32(onceAddress, OnceUninitialized);
Monitor.PulseAll(gate);
if (!string.IsNullOrWhiteSpace(callbackError))
{
Console.Error.WriteLine(
$"[LOADER][WARN] std::_Execute_once callback failed: {callbackError}");
}
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
if (!ctx.TryWriteInt32(onceAddress, OnceComplete))
{
_ = ctx.TryWriteInt32(onceAddress, OnceUninitialized);
Monitor.PulseAll(gate);
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Monitor.PulseAll(gate);
// A completed once flag no longer needs a host-side gate. Waiters
// already holding this gate will observe OnceComplete after the
// pulse; future callers take the fast path before looking it up.
_ = _onceGates.TryRemove(onceAddress, out _);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "PsrRUg671K0",
ExportName = "__cxa_increment_exception_refcount",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaIncrementExceptionRefcount(CpuContext ctx)
{
_ = ctx[CpuRegister.Rdi];
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MQFPAqQPt1s",
ExportName = "__cxa_decrement_exception_refcount",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int CxaDecrementExceptionRefcount(CpuContext ctx)
{
_ = ctx[CpuRegister.Rdi];
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadGuardState(CpuContext ctx, ulong guardPtr, out ulong word, out bool initialized, out bool inProgress) private static bool TryReadGuardState(CpuContext ctx, ulong guardPtr, out ulong word, out bool initialized, out bool inProgress)
{ {
word = 0; word = 0;
+1 -2
View File
@@ -76,8 +76,7 @@ internal sealed record GuestVertexBuffer(
uint OffsetBytes, uint OffsetBytes,
byte[] Data, byte[] Data,
int Length, int Length,
bool Pooled, bool Pooled);
bool PerInstance = false);
internal sealed record GuestIndexBuffer( internal sealed record GuestIndexBuffer(
byte[] Data, byte[] Data,
@@ -1225,11 +1225,8 @@ internal static partial class MetalVideoPresenter
? vertexBuffer.Stride ? vertexBuffer.Stride
: Math.Max(vertexBuffer.ComponentCount, 1) * 4; : Math.Max(vertexBuffer.ComponentCount, 1) * 4;
MetalNative.Send(layout, MetalNative.Selector("setStride:"), (nint)stride); MetalNative.Send(layout, MetalNative.Selector("setStride:"), (nint)stride);
// MTLVertexStepFunction: PerVertex = 1, PerInstance = 2. // MTLVertexStepFunction.PerVertex = 1.
MetalNative.Send( MetalNative.Send(layout, MetalNative.Selector("setStepFunction:"), 1);
layout,
MetalNative.Selector("setStepFunction:"),
vertexBuffer.PerInstance ? 2 : 1);
} }
return descriptor; return descriptor;
@@ -46,13 +46,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread", [SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPread(CpuContext ctx) public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx);
{
var result = KernelPreadCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread", [SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -103,13 +97,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite", [SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPwrite(CpuContext ctx) public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
{
var result = KernelPwriteCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite", [SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -161,13 +149,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync", [SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFsync(CpuContext ctx) public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx);
{
var result = KernelFsyncCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync", [SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -175,13 +157,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync", [SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFdatasync(CpuContext ctx) public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
{
var result = KernelFsyncCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync", [SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -234,13 +210,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate", [SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFtruncate(CpuContext ctx) public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
{
var result = KernelFtruncateCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate", [SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -276,13 +246,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate", [SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixTruncate(CpuContext ctx) public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
{
var result = KernelTruncateCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
}
[SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate", [SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -330,13 +294,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename", [SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixRename(CpuContext ctx) public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx);
{
var result = KernelRenameCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
}
[SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename", [SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -405,10 +363,7 @@ public static partial class KernelMemoryCompatExports
{ {
if (!_openFiles.TryGetValue(fd, out var stream)) if (!_openFiles.TryGetValue(fd, out var stream))
{ {
return PosixFailure( return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND,
notFoundErrno: Ebadf);
} }
// POSIX dup shares the open file description (and offset), which is // POSIX dup shares the open file description (and offset), which is
@@ -431,10 +386,7 @@ public static partial class KernelMemoryCompatExports
{ {
if (!_openFiles.TryGetValue(oldFd, out var stream)) if (!_openFiles.TryGetValue(oldFd, out var stream))
{ {
return PosixFailure( return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND,
notFoundErrno: Ebadf);
} }
if (oldFd == newFd) if (oldFd == newFd)
@@ -460,13 +412,7 @@ public static partial class KernelMemoryCompatExports
[SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl", [SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFcntl(CpuContext ctx) public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
{
var result = KernelFcntlCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result, notFoundErrno: Ebadf);
}
[SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl", [SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
@@ -1558,7 +1558,7 @@ public static partial class KernelMemoryCompatExports
{ {
LogOpenTrace($"_open fail path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} ex={ex.GetType().Name}: {ex.Message}"); LogOpenTrace($"_open fail path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} ex={ex.GetType().Name}: {ex.Message}");
return ex is UnauthorizedAccessException return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
} }
@@ -1773,113 +1773,6 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
// WithPrefix sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes.
// Resource streamers resolve relative asset paths against a shared directory
// prefix. Without HLE, every call returned the generic NOT_FOUND sentinel
// and no asset received a real file id/size. Signature inferred from
// observed guest registers (rdi=prefix, rsi=path list, rdx=count, rcx=ids,
// r8=sizes, r9=error index): the no-prefix sibling's args shifted right by
// one with a leading `const char* prefix`.
[SysAbiExport(
Nid = "w5fcCG+t31g",
ExportName = "sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(CpuContext ctx)
{
var prefixAddress = ctx[CpuRegister.Rdi];
var pathListAddress = ctx[CpuRegister.Rsi];
var count = ctx[CpuRegister.Rdx];
var idsAddress = ctx[CpuRegister.Rcx];
var sizesAddress = ctx[CpuRegister.R8];
var errorIndexAddress = ctx[CpuRegister.R9];
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var prefix = string.Empty;
if (prefixAddress != 0)
{
_ = TryReadNullTerminatedUtf8(ctx, prefixAddress, MaxGuestStringLength, out prefix);
}
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var relativePath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var guestPath = CombineAprPrefixedPath(prefix, relativePath);
var hostPath = ResolveGuestPath(guestPath);
if (!TryGetAprFileSize(hostPath, out var fileSize))
{
LogIoTrace("apr_resolve_with_prefix", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (errorIndexAddress != 0 &&
!TryWriteUInt32Compat(ctx, errorIndexAddress, (uint)i))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelRuntimeCompatExports.TrySetErrno(ctx, 2); // ENOENT
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
LogIoTrace("apr_resolve_with_prefix", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static string CombineAprPrefixedPath(string prefix, string relative)
{
if (string.IsNullOrEmpty(prefix))
{
return relative;
}
if (string.IsNullOrEmpty(relative))
{
return prefix;
}
return $"{prefix.TrimEnd('/')}/{relative.TrimStart('/')}";
}
// The IDs-only sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes. // The IDs-only sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes.
// Games that stream via AMPR APR call this to turn asset paths into file // Games that stream via AMPR APR call this to turn asset paths into file
// IDs, then hand those IDs to sceAmprAprCommandBufferReadFile. Without it // IDs, then hand those IDs to sceAmprAprCommandBufferReadFile. Without it
@@ -2335,7 +2228,8 @@ public static partial class KernelMemoryCompatExports
if (result != OrbisGen2Result.ORBIS_GEN2_OK) if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{ {
return PosixFailure(ctx, (int)result, notFoundErrno: Ebadf); ctx[CpuRegister.Rax] = ulong.MaxValue;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
ctx[CpuRegister.Rax] = unchecked((ulong)position); ctx[CpuRegister.Rax] = unchecked((ulong)position);
@@ -5855,7 +5749,7 @@ public static partial class KernelMemoryCompatExports
return true; return true;
} }
if (!TryReadTrackedLibcHeap(address, destination) && !TryReadHostMemory(address, destination)) if (!TryReadHostMemory(address, destination))
{ {
return false; return false;
} }
@@ -5921,7 +5815,7 @@ public static partial class KernelMemoryCompatExports
return true; return true;
} }
if (!TryWriteTrackedLibcHeap(address, source) && !TryWriteHostMemory(address, source)) if (!TryWriteHostMemory(address, source))
{ {
return false; return false;
} }
@@ -6663,7 +6557,7 @@ public static partial class KernelMemoryCompatExports
} }
} }
internal static unsafe bool TryReadTrackedLibcHeap( internal static bool TryReadTrackedLibcHeap(
ulong address, ulong address,
Span<byte> destination) Span<byte> destination)
{ {
@@ -6687,54 +6581,7 @@ public static partial class KernelMemoryCompatExports
continue; continue;
} }
try return TryReadHostMemory(address, destination);
{
// Marshal.AllocHGlobal allocations are not present in the
// emulator's POSIX HostMemory map, so TryReadHostMemory
// would reject this already-bounds-checked range.
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
return true;
}
catch
{
return false;
}
}
}
return false;
}
private static unsafe bool TryWriteTrackedLibcHeap(ulong address, ReadOnlySpan<byte> source)
{
if (source.IsEmpty)
{
return true;
}
var length = (ulong)source.Length;
lock (_libcAllocGate)
{
foreach (var (allocationAddress, allocation) in _libcAllocations)
{
var allocationSize = (ulong)allocation.Size;
var offset = address >= allocationAddress
? address - allocationAddress
: ulong.MaxValue;
if (offset > allocationSize || length > allocationSize - offset)
{
continue;
}
try
{
source.CopyTo(new Span<byte>((void*)address, source.Length));
return true;
}
catch
{
return false;
}
} }
} }
@@ -7335,41 +7182,28 @@ public static partial class KernelMemoryCompatExports
{ {
if (fd < 0 || bufferAddress == 0 || requested < 512) if (fd < 0 || bufferAddress == 0 || requested < 512)
{ {
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
OpenDirectory? directory; OpenDirectory? directory;
bool isOpenFile;
lock (_fdGate) lock (_fdGate)
{ {
_openDirectories.TryGetValue(fd, out directory); _openDirectories.TryGetValue(fd, out directory);
isOpenFile = directory is null && _openFiles.ContainsKey(fd);
} }
if (directory is null) if (directory is null)
{ {
// A regular file fd used with getdents must not look like EOF (rax=0); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
// that path has caused GTA's fiWriteAsyncDataWorker to treat the fd
// integer as a pointer and AV at address 0xB1.
var error = isOpenFile
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
LogIoTrace("getdents", $"fd:{fd}", $"result={(isOpenFile ? "not_directory" : "badfd")}");
ctx[CpuRegister.Rax] = unchecked((ulong)(int)error);
return (int)error;
} }
var currentIndex = directory.NextIndex; var currentIndex = directory.NextIndex;
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex)) if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{ {
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
if (currentIndex >= directory.Entries.Length) if (currentIndex >= directory.Entries.Length)
{ {
LogIoTrace("getdents", directory.Path, $"fd={fd} result=eof entries={directory.Entries.Length}");
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -7400,16 +7234,11 @@ public static partial class KernelMemoryCompatExports
private static string[] EnumerateDirectoryEntries(string hostPath) private static string[] EnumerateDirectoryEntries(string hostPath)
{ {
// Real getdents always yields "." / ".." before other names. An empty return Directory.EnumerateFileSystemEntries(hostPath)
// host dir previously returned EOF on the first call (rax=0), which
// sent GTA's fiWriteAsyncDataWorker down a path that treated the fd as
// a pointer (AV at 0xB1 on /download0/cloudcache/).
var children = Directory.EnumerateFileSystemEntries(hostPath)
.Select(Path.GetFileName) .Select(Path.GetFileName)
.Where(static name => !string.IsNullOrEmpty(name)) .Where(static name => !string.IsNullOrEmpty(name))
.OrderBy(static name => name, StringComparer.OrdinalIgnoreCase); .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase)
.ToArray()!;
return new[] { ".", ".." }.Concat(children).ToArray()!;
} }
private static uint ComputeDirectoryEntryHash(ReadOnlySpan<byte> utf8Name) private static uint ComputeDirectoryEntryHash(ReadOnlySpan<byte> utf8Name)
-78
View File
@@ -45,11 +45,7 @@ public static class LibcStdioExports
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t' private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
private static readonly object _ctypeTableGate = new(); private static readonly object _ctypeTableGate = new();
private static readonly object _ctypeLowerTableGate = new();
private static readonly object _ctypeUpperTableGate = new();
private static nint _ctypeTableBase; private static nint _ctypeTableBase;
private static nint _ctypeLowerTableBase;
private static nint _ctypeUpperTableBase;
[SysAbiExport( [SysAbiExport(
Nid = "xeYO4u7uyJ0", Nid = "xeYO4u7uyJ0",
@@ -720,28 +716,6 @@ public static class LibcStdioExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "1uJgoVq3bQU",
ExportName = "_Getptolower",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int GetPtolower(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeLowerTable());
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "rcQCUr0EaRU",
ExportName = "_Getptoupper",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int GetPtoupper(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeUpperTable());
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static unsafe nint EnsureCtypeTable() private static unsafe nint EnsureCtypeTable()
{ {
lock (_ctypeTableGate) lock (_ctypeTableGate)
@@ -766,58 +740,6 @@ public static class LibcStdioExports
} }
} }
private static unsafe nint EnsureCtypeLowerTable()
{
lock (_ctypeLowerTableGate)
{
if (_ctypeLowerTableBase != 0)
{
return _ctypeLowerTableBase;
}
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
for (var i = 0; i < CtypeTableEntryCount; i++)
{
var c = i + CtypeTableLowerBound;
entries[i] = c == -1
? ushort.MaxValue
: c is >= 0 and <= 0x7F
? (ushort)char.ToLowerInvariant((char)c)
: (ushort)(c & 0xFF);
}
_ctypeLowerTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
return _ctypeLowerTableBase;
}
}
private static unsafe nint EnsureCtypeUpperTable()
{
lock (_ctypeUpperTableGate)
{
if (_ctypeUpperTableBase != 0)
{
return _ctypeUpperTableBase;
}
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
for (var i = 0; i < CtypeTableEntryCount; i++)
{
var c = i + CtypeTableLowerBound;
entries[i] = c == -1
? ushort.MaxValue
: c is >= 0 and <= 0x7F
? (ushort)char.ToUpperInvariant((char)c)
: (ushort)(c & 0xFF);
}
_ctypeUpperTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
return _ctypeUpperTableBase;
}
}
private static ushort ComputeCtypeFlags(int c) private static ushort ComputeCtypeFlags(int c)
{ {
var isUpper = c is >= 'A' and <= 'Z'; var isUpper = c is >= 'A' and <= 'Z';
@@ -1,82 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Messenger;
/// <summary>Small Unity/libc compatibility shims exercised by The Messenger.</summary>
public static class MessengerCompatExports
{
[SysAbiExport(Nid = "wLlFkwG9UcQ", ExportName = "time", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
public static int Time(CpuContext ctx)
{
var seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var output = ctx[CpuRegister.Rdi];
if (output != 0 && !ctx.TryWriteUInt64(output, unchecked((ulong)seconds)))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)seconds);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "M4YYbSFfJ8g", ExportName = "setenv", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
public static int Setenv(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "-P6FNMzk2Kc", ExportName = "cosf", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
public static int Cosf(CpuContext ctx)
{
// AMD64 passes a scalar float in XMM0 and returns it in XMM0. RDI is
// unrelated caller state and must not be interpreted as the argument.
ctx.GetXmmRegister(0, out var low, out var high);
var value = BitConverter.Int32BitsToSingle(unchecked((int)low));
var resultBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(value)));
ctx.SetXmmRegister(0, (low & 0xFFFFFFFF00000000UL) | resultBits, high);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "YQ0navp+YIc", ExportName = "puts", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
public static int Puts(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "KuOuD58hqn4", ExportName = "malloc_stats_fast", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
public static int MallocStatsFast(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "-pnj3-7a6QA", ExportName = "unity_mono_set_user_malloc_mutex", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")]
public static int UnityMonoSetMallocMutex(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "35NoyMOtYpE", ExportName = "SetDataFolder", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")]
public static int SetDataFolder(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
#pragma warning disable SHEM006
[SysAbiExport(Nid = "cJ2Y4E-t258", ExportName = "il2cpp_api_register_symbol", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libil2cpp")]
public static int Il2CppRegisterSymbol(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
#pragma warning restore SHEM006
@@ -1,51 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Remoteplay;
// SharpEmu does not implement PS5 Remote Play. Titles still probe this API
// during startup (initialize + connection-status checks while bringing up
// pad/network subsystems). Without a handler they get ORBIS_GEN2_ERROR_NOT_FOUND
// instead of a real status code. Reporting a clean "initialized, not connected"
// state lets callers take their normal no-remote-play path.
public static class RemoteplayExports
{
private const int StatusDisconnected = 0;
[SysAbiExport(
Nid = "k1SwgkMSOM8",
ExportName = "sceRemoteplayInitialize",
Target = Generation.Gen5,
LibraryName = "libSceRemoteplay")]
public static int RemoteplayInitialize(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "g3PNjYKWqnQ",
ExportName = "sceRemoteplayGetConnectionStatus",
Target = Generation.Gen5,
LibraryName = "libSceRemoteplay")]
public static int RemoteplayGetConnectionStatus(CpuContext ctx)
{
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress != 0)
{
Span<byte> status = stackalloc byte[0x10];
status.Clear();
status[0] = StatusDisconnected;
if (!ctx.Memory.TryWrite(statusAddress, status))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -18,7 +18,6 @@ internal readonly record struct VulkanHostBufferAllocation(
internal sealed class VulkanHostBufferPool : IDisposable internal sealed class VulkanHostBufferPool : IDisposable
{ {
private readonly object _gate = new();
private readonly Dictionary<VulkanHostBufferPoolKey, Stack<VulkanHostBufferAllocation>> private readonly Dictionary<VulkanHostBufferPoolKey, Stack<VulkanHostBufferAllocation>>
_available = []; _available = [];
private readonly Dictionary<ulong, VulkanHostBufferAllocation> _allocations = []; private readonly Dictionary<ulong, VulkanHostBufferAllocation> _allocations = [];
@@ -40,8 +39,6 @@ internal sealed class VulkanHostBufferPool : IDisposable
public bool TryRent( public bool TryRent(
VulkanHostBufferPoolKey key, VulkanHostBufferPoolKey key,
out VulkanHostBufferAllocation allocation) out VulkanHostBufferAllocation allocation)
{
lock (_gate)
{ {
if (!_available.TryGetValue(key, out var available) || if (!_available.TryGetValue(key, out var available) ||
!available.TryPop(out allocation)) !available.TryPop(out allocation))
@@ -54,7 +51,6 @@ internal sealed class VulkanHostBufferPool : IDisposable
CachedBytes -= allocation.Key.Capacity; CachedBytes -= allocation.Key.Capacity;
return true; return true;
} }
}
public void Register(VulkanHostBufferAllocation allocation) public void Register(VulkanHostBufferAllocation allocation)
{ {
@@ -63,16 +59,10 @@ internal sealed class VulkanHostBufferPool : IDisposable
throw new ArgumentException("A pooled buffer must have a valid handle.", nameof(allocation)); throw new ArgumentException("A pooled buffer must have a valid handle.", nameof(allocation));
} }
lock (_gate)
{
_allocations.Add(allocation.Buffer.Handle, allocation); _allocations.Add(allocation.Buffer.Handle, allocation);
} }
}
public bool Return(VkBuffer buffer, DeviceMemory memory) public bool Return(VkBuffer buffer, DeviceMemory memory)
{
VulkanHostBufferAllocation? toDestroy = null;
lock (_gate)
{ {
if (!_allocations.TryGetValue(buffer.Handle, out var allocation) || if (!_allocations.TryGetValue(buffer.Handle, out var allocation) ||
allocation.Memory.Handle != memory.Handle) allocation.Memory.Handle != memory.Handle)
@@ -89,10 +79,10 @@ internal sealed class VulkanHostBufferPool : IDisposable
{ {
_cachedHandles.Remove(buffer.Handle); _cachedHandles.Remove(buffer.Handle);
_allocations.Remove(buffer.Handle); _allocations.Remove(buffer.Handle);
toDestroy = allocation; _destroy(allocation);
return true;
} }
else
{
if (!_available.TryGetValue(allocation.Key, out var available)) if (!_available.TryGetValue(allocation.Key, out var available))
{ {
available = []; available = [];
@@ -101,41 +91,19 @@ internal sealed class VulkanHostBufferPool : IDisposable
available.Push(allocation); available.Push(allocation);
CachedBytes += allocation.Key.Capacity; CachedBytes += allocation.Key.Capacity;
}
}
// Destroy outside the lock — _destroy calls into Vulkan which may
// grab device-level locks, and holding _gate while doing so risks
// a lock-ordering deadlock with a thread that holds the device lock
// and is waiting on _gate.
if (toDestroy is { } td)
{
_destroy(td);
}
return true; return true;
} }
public void Dispose() public void Dispose()
{ {
// Snapshot under the lock, destroy outside — _destroy calls into foreach (var allocation in _allocations.Values)
// Vulkan which may grab device-level locks; holding _gate while
// doing so risks a lock-ordering deadlock with any thread that
// acquires the device lock first and then waits on _gate.
List<VulkanHostBufferAllocation> toDestroy;
lock (_gate)
{ {
toDestroy = new List<VulkanHostBufferAllocation>(_allocations.Values); _destroy(allocation);
}
_allocations.Clear(); _allocations.Clear();
_available.Clear(); _available.Clear();
_cachedHandles.Clear(); _cachedHandles.Clear();
CachedBytes = 0; CachedBytes = 0;
} }
foreach (var allocation in toDestroy)
{
_destroy(allocation);
}
}
} }
@@ -3170,7 +3170,6 @@ internal static unsafe class VulkanVideoPresenter
public uint NumberFormat; public uint NumberFormat;
public uint Stride; public uint Stride;
public uint OffsetBytes; public uint OffsetBytes;
public bool PerInstance;
} }
private const Format DepthFormat = Format.D32Sfloat; private const Format DepthFormat = Format.D32Sfloat;
@@ -6659,47 +6658,33 @@ internal static unsafe class VulkanVideoPresenter
PName = entryPoint, PName = entryPoint,
}; };
// One Vulkan binding per unique host buffer and input rate var vertexBindingDescriptions =
// (fetch_index). Attributes share that binding with new VertexInputBindingDescription[resources.VertexBuffers.Length];
// Offset = OffsetBytes.
var bindingByBuffer = new Dictionary<(ulong Handle, bool PerInstance), uint>();
var vertexBindingList = new List<VertexInputBindingDescription>();
var vertexAttributeDescriptions = var vertexAttributeDescriptions =
new VertexInputAttributeDescription[resources.VertexBuffers.Length]; new VertexInputAttributeDescription[resources.VertexBuffers.Length];
for (var index = 0; index < resources.VertexBuffers.Length; index++) for (var index = 0; index < resources.VertexBuffers.Length; index++)
{ {
var vertexBuffer = resources.VertexBuffers[index]; var vertexBuffer = resources.VertexBuffers[index];
var bufferKey = (vertexBuffer.Buffer.Handle, vertexBuffer.PerInstance); vertexBindingDescriptions[index] = new VertexInputBindingDescription
if (!bindingByBuffer.TryGetValue(bufferKey, out var bindingIndex))
{ {
bindingIndex = (uint)vertexBindingList.Count; Binding = (uint)index,
bindingByBuffer[bufferKey] = bindingIndex;
vertexBindingList.Add(new VertexInputBindingDescription
{
Binding = bindingIndex,
Stride = vertexBuffer.Stride == 0 Stride = vertexBuffer.Stride == 0
? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float)
: vertexBuffer.Stride, : vertexBuffer.Stride,
InputRate = vertexBuffer.PerInstance InputRate = VertexInputRate.Vertex,
? VertexInputRate.Instance };
: VertexInputRate.Vertex,
});
}
vertexAttributeDescriptions[index] = new VertexInputAttributeDescription vertexAttributeDescriptions[index] = new VertexInputAttributeDescription
{ {
Location = vertexBuffer.Location, Location = vertexBuffer.Location,
Binding = bindingIndex, Binding = (uint)index,
Format = ToVkVertexFormat( Format = ToVkVertexFormat(
vertexBuffer.DataFormat, vertexBuffer.DataFormat,
vertexBuffer.NumberFormat, vertexBuffer.NumberFormat,
vertexBuffer.ComponentCount), vertexBuffer.ComponentCount),
Offset = vertexBuffer.OffsetBytes, Offset = 0,
}; };
} }
var vertexBindingDescriptions = vertexBindingList.ToArray();
fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions) fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions)
fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions) fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions)
{ {
@@ -9295,7 +9280,6 @@ internal static unsafe class VulkanVideoPresenter
NumberFormat = guestBuffer.NumberFormat, NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride, Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes, OffsetBytes = guestBuffer.OffsetBytes,
PerInstance = guestBuffer.PerInstance,
}; };
} }
@@ -9313,7 +9297,6 @@ internal static unsafe class VulkanVideoPresenter
NumberFormat = guestBuffer.NumberFormat, NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride, Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes, OffsetBytes = guestBuffer.OffsetBytes,
PerInstance = guestBuffer.PerInstance,
}; };
private VkBuffer CreateHostBuffer( private VkBuffer CreateHostBuffer(
@@ -9415,28 +9398,21 @@ internal static unsafe class VulkanVideoPresenter
private static Format ToVkVertexFormat( private static Format ToVkVertexFormat(
uint dataFormat, uint dataFormat,
uint numberFormat, uint numberFormat,
uint componentCount) uint componentCount) =>
{ (dataFormat, numberFormat) switch
var format = (dataFormat, numberFormat) switch
{ {
(1, 0) => Format.R8Unorm, (1, 0) => Format.R8Unorm,
(1, 1) => Format.R8SNorm, (1, 1) => Format.R8SNorm,
(1, 2) => Format.R8Uscaled,
(1, 3) => Format.R8Sscaled,
(1, 4) => Format.R8Uint, (1, 4) => Format.R8Uint,
(1, 5) => Format.R8Sint, (1, 5) => Format.R8Sint,
(1, 9) => Format.R8Srgb, (1, 9) => Format.R8Srgb,
(2, 0) => Format.R16Unorm, (2, 0) => Format.R16Unorm,
(2, 1) => Format.R16SNorm, (2, 1) => Format.R16SNorm,
(2, 2) => Format.R16Uscaled,
(2, 3) => Format.R16Sscaled,
(2, 4) => Format.R16Uint, (2, 4) => Format.R16Uint,
(2, 5) => Format.R16Sint, (2, 5) => Format.R16Sint,
(2, 7) => Format.R16Sfloat, (2, 7) => Format.R16Sfloat,
(3, 0) => Format.R8G8Unorm, (3, 0) => Format.R8G8Unorm,
(3, 1) => Format.R8G8SNorm, (3, 1) => Format.R8G8SNorm,
(3, 2) => Format.R8G8Uscaled,
(3, 3) => Format.R8G8Sscaled,
(3, 4) => Format.R8G8Uint, (3, 4) => Format.R8G8Uint,
(3, 5) => Format.R8G8Sint, (3, 5) => Format.R8G8Sint,
(3, 9) => Format.R8G8Srgb, (3, 9) => Format.R8G8Srgb,
@@ -9491,9 +9467,6 @@ internal static unsafe class VulkanVideoPresenter
(14, 4) => Format.R32G32B32A32Uint, (14, 4) => Format.R32G32B32A32Uint,
(14, 5) => Format.R32G32B32A32Sint, (14, 5) => Format.R32G32B32A32Sint,
(14, 7) => Format.R32G32B32A32Sfloat, (14, 7) => Format.R32G32B32A32Sfloat,
// Prospero VertexAttribFormat quirks also seen as buffer formats.
(113, _) => Format.R32G32B32A32Sfloat,
(121, _) => Format.R16G16Sfloat,
(16, 0) => Format.B5G6R5UnormPack16, (16, 0) => Format.B5G6R5UnormPack16,
(17, 0) => Format.R5G5B5A1UnormPack16, (17, 0) => Format.R5G5B5A1UnormPack16,
(19, 0) => Format.R4G4B4A4UnormPack16, (19, 0) => Format.R4G4B4A4UnormPack16,
@@ -9501,38 +9474,6 @@ internal static unsafe class VulkanVideoPresenter
_ => ToVkFloatVertexFormat(componentCount), _ => ToVkFloatVertexFormat(componentCount),
}; };
return NarrowVkVertexFormat(format, componentCount);
}
/// <summary>
/// Narrow a sharp's full VkFormat to the component count the VS fetch
/// actually consumes.
/// </summary>
private static Format NarrowVkVertexFormat(Format format, uint usedComponents)
{
if (usedComponents == 0)
{
return format;
}
return (format, usedComponents) switch
{
(Format.R32G32B32A32Sfloat, 1) => Format.R32Sfloat,
(Format.R32G32B32A32Sfloat, 2) => Format.R32G32Sfloat,
(Format.R32G32B32A32Sfloat, 3) => Format.R32G32B32Sfloat,
(Format.R32G32B32Sfloat, 1) => Format.R32Sfloat,
(Format.R32G32B32Sfloat, 2) => Format.R32G32Sfloat,
(Format.R16G16B16A16Sfloat, 1) => Format.R16Sfloat,
(Format.R16G16B16A16Sfloat, 2) => Format.R16G16Sfloat,
(Format.R8G8B8A8Unorm, 1) => Format.R8Unorm,
(Format.R8G8B8A8Unorm, 2) => Format.R8G8Unorm,
(Format.R8G8B8A8SNorm, 2) => Format.R8G8SNorm,
(Format.R8G8B8A8Uint, 1) => Format.R8Uint,
(Format.R8G8B8A8Uint, 2) => Format.R8G8Uint,
_ => format,
};
}
private static Format ToVkFloatVertexFormat(uint componentCount) => private static Format ToVkFloatVertexFormat(uint componentCount) =>
componentCount switch componentCount switch
{ {
+1 -2
View File
@@ -312,8 +312,7 @@ public sealed record Gen5VertexInputBinding(
uint OffsetBytes, uint OffsetBytes,
byte[] Data, byte[] Data,
int DataLength, int DataLength,
bool DataPooled, bool DataPooled);
bool PerInstance = false);
public sealed record Gen5ShaderEvaluation( public sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters, IReadOnlyList<uint> InitialScalarRegisters,
@@ -883,11 +883,8 @@ public static class Gen5ShaderScalarEvaluator
Gen5ShaderInstruction instruction, Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control, Gen5BufferMemoryControl control,
BufferDescriptor descriptor) => BufferDescriptor descriptor) =>
// AGC embedded fetch is BufferLoadFormat/TBufferLoadFormat with idxen.
// offen is allowed: the constant/scalar offset folds into OffsetBytes
// (UI glyph shaders use this shape). Rejecting offen left those loads
// as live SSBOs and dropped vertex attributes.
control.IndexEnabled && control.IndexEnabled &&
!control.OffsetEnabled &&
control.DwordCount is >= 1 and <= 4 && control.DwordCount is >= 1 and <= 4 &&
descriptor.BaseAddress != 0 && descriptor.BaseAddress != 0 &&
descriptor.Stride != 0 && descriptor.Stride != 0 &&
@@ -1,94 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Debugger.Breakpoints;
using Xunit;
namespace SharpEmu.Debugger.Tests;
public sealed class BreakpointStoreTests
{
[Fact]
public void Add_AssignsIncrementalIds()
{
var store = new BreakpointStore();
var first = store.Add(BreakpointKind.Execute, 0x1000);
var second = store.Add(BreakpointKind.WriteWatch, 0x2000, length: 8);
Assert.Equal(1, first.Id);
Assert.Equal(2, second.Id);
Assert.Equal(2, store.Snapshot().Count);
}
[Fact]
public void Add_ForcesExecuteLengthToOne()
{
var store = new BreakpointStore();
var breakpoint = store.Add(BreakpointKind.Execute, 0x4000, length: 64);
Assert.Equal(BreakpointKind.Execute, breakpoint.Kind);
Assert.Equal(1UL, breakpoint.Length);
Assert.True(breakpoint.Covers(0x4000));
Assert.False(breakpoint.Covers(0x4001));
}
[Fact]
public void Add_PreservesWatchLengthAtLeastOne()
{
var store = new BreakpointStore();
var withLength = store.Add(BreakpointKind.ReadWatch, 0x5000, length: 16);
var zeroClamped = store.Add(BreakpointKind.AccessWatch, 0x6000, length: 0);
Assert.Equal(16UL, withLength.Length);
Assert.Equal(1UL, zeroClamped.Length);
}
[Fact]
public void Remove_And_SetEnabled_MutateStore()
{
var store = new BreakpointStore();
var breakpoint = store.Add(BreakpointKind.Execute, 0x1000);
Assert.True(store.SetEnabled(breakpoint.Id, enabled: false));
Assert.False(store.Snapshot().Single().Enabled);
Assert.True(store.Remove(breakpoint.Id));
Assert.Empty(store.Snapshot());
Assert.False(store.Remove(breakpoint.Id));
Assert.False(store.SetEnabled(breakpoint.Id, enabled: true));
}
[Fact]
public void FindExecuteHit_ReturnsFirstEnabledExecuteMatch()
{
var store = new BreakpointStore();
var disabled = store.Add(BreakpointKind.Execute, 0x1000);
var watch = store.Add(BreakpointKind.WriteWatch, 0x1000, length: 4);
var hit = store.Add(BreakpointKind.Execute, 0x1000);
store.SetEnabled(disabled.Id, enabled: false);
var found = store.FindExecuteHit(0x1000);
Assert.NotNull(found);
Assert.Equal(hit.Id, found!.Id);
Assert.Equal(BreakpointKind.Execute, found.Kind);
Assert.NotEqual(watch.Id, found.Id);
Assert.Null(store.FindExecuteHit(0x1001));
}
[Fact]
public void Clear_RemovesAllBreakpoints()
{
var store = new BreakpointStore();
store.Add(BreakpointKind.Execute, 0x1);
store.Add(BreakpointKind.Execute, 0x2);
store.Clear();
Assert.Empty(store.Snapshot());
Assert.Null(store.FindExecuteHit(0x1));
}
}
@@ -1,203 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Debugger.Protocol;
using SharpEmu.Debugger.Session;
using Xunit;
namespace SharpEmu.Debugger.Tests;
public sealed class DebugCommandDispatcherTests
{
private const int MaxMemoryChunk = 64 * 1024;
[Fact]
public void Dispatch_UnknownCommand_Fails()
{
var dispatcher = CreateDispatcher();
var response = dispatcher.Dispatch(Parse("""{"command":"nope"}"""));
Assert.False(response.Ok);
Assert.Equal("nope", response.Command);
Assert.Contains("Unknown command", response.Error, StringComparison.Ordinal);
}
[Fact]
public void Dispatch_Registers_WhenNotPaused_Fails()
{
var session = new FakeDebuggerSession(DebuggerRunState.Running);
var dispatcher = new DebugCommandDispatcher(session);
var response = dispatcher.Dispatch(Parse("""{"command":"registers"}"""));
Assert.False(response.Ok);
Assert.Equal("Target is not paused.", response.Error);
}
[Fact]
public void Dispatch_ContinueAndStep_WhenNotPaused_Fail()
{
var session = new FakeDebuggerSession(DebuggerRunState.Running);
var dispatcher = new DebugCommandDispatcher(session);
var cont = dispatcher.Dispatch(Parse("""{"command":"continue"}"""));
var step = dispatcher.Dispatch(Parse("""{"command":"step"}"""));
Assert.False(cont.Ok);
Assert.Equal("Target is not paused.", cont.Error);
Assert.False(step.Ok);
Assert.Equal("Target is not paused.", step.Error);
Assert.Equal(1, session.ContinueCallCount);
Assert.Equal(1, session.StepFrameCallCount);
}
[Fact]
public void Dispatch_ReadMemory_RejectsLengthAboveCap()
{
var session = new FakeDebuggerSession();
session.SeedMemory(0x1000, new byte[16]);
var dispatcher = new DebugCommandDispatcher(session);
var oversize = dispatcher.Dispatch(Parse(
$$"""{"command":"read-memory","address":"0x1000","length":{{MaxMemoryChunk + 1}}}"""));
var zero = dispatcher.Dispatch(Parse(
"""{"command":"read-memory","address":"0x1000","length":0}"""));
Assert.False(oversize.Ok);
Assert.Contains(MaxMemoryChunk.ToString(), oversize.Error);
Assert.False(zero.Ok);
Assert.Contains(MaxMemoryChunk.ToString(), zero.Error);
}
[Fact]
public void Dispatch_WriteMemory_RejectsPayloadAboveCap()
{
var session = new FakeDebuggerSession();
var dispatcher = new DebugCommandDispatcher(session);
var hex = new string('A', (MaxMemoryChunk + 1) * 2);
var response = dispatcher.Dispatch(Parse(
$$"""{"command":"write-memory","address":"0x1000","bytes":"{{hex}}"}"""));
Assert.False(response.Ok);
Assert.Contains(MaxMemoryChunk.ToString(), response.Error);
}
[Fact]
public void Dispatch_ReadMemory_WhenPaused_ReturnsHexBytes()
{
var session = new FakeDebuggerSession();
session.SeedMemory(0x2000, [0xDE, 0xAD, 0xBE, 0xEF]);
var dispatcher = new DebugCommandDispatcher(session);
var response = dispatcher.Dispatch(Parse(
"""{"command":"read-memory","address":"0x2000","length":4}"""));
Assert.True(response.Ok);
Assert.NotNull(response.Data);
Assert.Equal("DEADBEEF", GetString(response.Data!, "bytes"));
Assert.Equal("0x0000000000002000", GetString(response.Data!, "address"));
}
[Fact]
public void Dispatch_BreakpointCrud_HappyPath()
{
var session = new FakeDebuggerSession();
var dispatcher = new DebugCommandDispatcher(session);
var added = dispatcher.Dispatch(Parse(
"""{"command":"add-breakpoint","address":"0x401000","kind":"Execute","length":8}"""));
Assert.True(added.Ok);
Assert.NotNull(added.Data);
var breakpoint = GetDict(added.Data!, "breakpoint");
Assert.Equal(1, Convert.ToInt32(breakpoint["id"]));
Assert.Equal("Execute", Assert.IsType<string>(breakpoint["kind"]));
Assert.Equal(1UL, Convert.ToUInt64(breakpoint["length"]));
Assert.True(Assert.IsType<bool>(breakpoint["enabled"]));
var listed = dispatcher.Dispatch(Parse("""{"command":"list-breakpoints"}"""));
Assert.True(listed.Ok);
var list = Assert.IsAssignableFrom<System.Collections.IEnumerable>(listed.Data!["breakpoints"]);
Assert.Single(list.Cast<object?>());
var disabled = dispatcher.Dispatch(Parse(
"""{"command":"enable-breakpoint","id":1,"enabled":false}"""));
Assert.True(disabled.Ok);
Assert.False(session.Breakpoints.Snapshot().Single().Enabled);
var removed = dispatcher.Dispatch(Parse(
"""{"command":"remove-breakpoint","id":1}"""));
Assert.True(removed.Ok);
Assert.Empty(session.Breakpoints.Snapshot());
var missing = dispatcher.Dispatch(Parse(
"""{"command":"remove-breakpoint","id":1}"""));
Assert.False(missing.Ok);
Assert.Contains("No breakpoint", missing.Error, StringComparison.Ordinal);
}
[Fact]
public void Dispatch_PingAndState_Succeed()
{
var session = new FakeDebuggerSession(DebuggerRunState.Running);
var dispatcher = new DebugCommandDispatcher(session);
var ping = dispatcher.Dispatch(Parse("""{"command":"ping"}"""));
var state = dispatcher.Dispatch(Parse("""{"command":"state"}"""));
Assert.True(ping.Ok);
Assert.True(state.Ok);
Assert.Equal("Running", GetString(state.Data!, "state"));
}
[Fact]
public void Dispatch_Pause_RequestsPause()
{
var session = new FakeDebuggerSession(DebuggerRunState.Running);
var dispatcher = new DebugCommandDispatcher(session);
var response = dispatcher.Dispatch(Parse("""{"command":"pause"}"""));
Assert.True(response.Ok);
Assert.True(session.PauseRequested);
}
[Fact]
public void Dispatch_ParseErrorCommand_SurfacesMessage()
{
var dispatcher = CreateDispatcher();
var response = dispatcher.Dispatch(Parse(
$$"""{"command":"{{JsonLineDebugProtocol.ParseErrorCommand}}","message":"bad line"}"""));
Assert.False(response.Ok);
Assert.Equal("bad line", response.Error);
}
private static DebugCommandDispatcher CreateDispatcher()
=> new(new FakeDebuggerSession());
private static DebugRequest Parse(string json)
{
Assert.True(DebugRequest.TryParse(json, out var request, out var error), error);
return request;
}
private static string GetString(IReadOnlyDictionary<string, object?> data, string key)
{
Assert.True(data.TryGetValue(key, out var value));
Assert.NotNull(value);
return Assert.IsType<string>(value);
}
private static IReadOnlyDictionary<string, object?> GetDict(
IReadOnlyDictionary<string, object?> data,
string key)
{
Assert.True(data.TryGetValue(key, out var value));
Assert.NotNull(value);
return Assert.IsAssignableFrom<IReadOnlyDictionary<string, object?>>(value);
}
}
@@ -1,80 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Debugger.Protocol;
using Xunit;
namespace SharpEmu.Debugger.Tests;
public sealed class DebugRequestTests
{
[Fact]
public void TryParse_AcceptsValidCommandAndNormalizesCase()
{
Assert.True(DebugRequest.TryParse("""{"command":"Ping"}""", out var request, out var error));
Assert.Equal(string.Empty, error);
Assert.Equal("ping", request.Command);
}
[Fact]
public void TryParse_ReadsNumericAndHexAddresses()
{
Assert.True(DebugRequest.TryParse(
"""{"command":"read-memory","address":"0x1000","length":16}""",
out var request,
out _));
Assert.True(request.TryGetUInt64("address", out var address));
Assert.Equal(0x1000UL, address);
Assert.True(request.TryGetInt32("length", out var length));
Assert.Equal(16, length);
}
[Fact]
public void TryParse_ReadsHexLengthStrings()
{
Assert.True(DebugRequest.TryParse(
"""{"command":"read-memory","address":4096,"length":"0x20"}""",
out var request,
out _));
Assert.True(request.TryGetUInt64("address", out var address));
Assert.Equal(4096UL, address);
Assert.True(request.TryGetInt32("length", out var length));
Assert.Equal(0x20, length);
}
[Fact]
public void TryParse_RejectsMissingCommand()
{
Assert.False(DebugRequest.TryParse("""{"address":1}""", out _, out var error));
Assert.Contains("command", error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryParse_RejectsNonObjectRoot()
{
Assert.False(DebugRequest.TryParse("""["ping"]""", out _, out var error));
Assert.Contains("object", error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryParse_RejectsMalformedJson()
{
Assert.False(DebugRequest.TryParse("""{command:""", out _, out var error));
Assert.Contains("Malformed JSON", error, StringComparison.Ordinal);
}
[Fact]
public void TryGetBool_ReadsJsonBooleans()
{
Assert.True(DebugRequest.TryParse(
"""{"command":"enable-breakpoint","id":1,"enabled":false}""",
out var request,
out _));
Assert.True(request.TryGetBool("enabled", out var enabled));
Assert.False(enabled);
Assert.False(request.TryGetBool("missing", out _));
}
}
@@ -1,215 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Debugger;
using SharpEmu.Debugger.Breakpoints;
using SharpEmu.Debugger.Session;
using SharpEmu.HLE;
namespace SharpEmu.Debugger.Tests;
/// <summary>
/// In-memory session for dispatcher tests. Tracks run state and an optional
/// flat guest memory region without parking a real emulation thread.
/// </summary>
internal sealed class FakeDebuggerSession : IDebuggerSession
{
private readonly Dictionary<ulong, byte> _memory = new();
private DebugRegisterFile _registers = new(
new ulong[16],
rip: 0,
rflags: 0,
fsBase: 0,
gsBase: 0);
public FakeDebuggerSession(DebuggerRunState state = DebuggerRunState.Paused)
{
State = state;
Breakpoints = new BreakpointStore();
Hook = NullCpuDebugHook.Instance;
}
public BreakpointStore Breakpoints { get; }
public ICpuDebugHook Hook { get; }
public DebuggerRunState State { get; set; }
public DebugStopEvent? LastStop { get; set; }
public bool PauseRequested { get; private set; }
public int ContinueCallCount { get; private set; }
public int StepFrameCallCount { get; private set; }
public event EventHandler<DebugStopEvent>? Stopped
{
add { }
remove { }
}
public event EventHandler? Resumed
{
add { }
remove { }
}
public event EventHandler? Terminated
{
add { }
remove { }
}
public void SetRegisters(DebugRegisterFile registers) => _registers = registers;
public void SeedMemory(ulong address, ReadOnlySpan<byte> bytes)
{
for (var i = 0; i < bytes.Length; i++)
{
_memory[address + (ulong)i] = bytes[i];
}
}
public bool TryGetRegisters(out DebugRegisterFile registers)
{
if (State != DebuggerRunState.Paused)
{
registers = default;
return false;
}
registers = _registers;
return true;
}
public bool TrySetRegister(DebugRegisterId id, ulong value)
{
if (State != DebuggerRunState.Paused)
{
return false;
}
var gpr = new ulong[16];
for (var i = 0; i < 16; i++)
{
gpr[i] = _registers[(CpuRegister)i];
}
var rip = _registers.Rip;
var rflags = _registers.Rflags;
var fsBase = _registers.FsBase;
var gsBase = _registers.GsBase;
switch (id)
{
case DebugRegisterId.Rip:
rip = value;
break;
case DebugRegisterId.Rflags:
rflags = value;
break;
case DebugRegisterId.FsBase:
fsBase = value;
break;
case DebugRegisterId.GsBase:
gsBase = value;
break;
default:
if (!id.IsGeneralPurpose())
{
return false;
}
gpr[(int)id] = value;
break;
}
_registers = new DebugRegisterFile(gpr, rip, rflags, fsBase, gsBase);
return true;
}
public bool TryReadMemory(ulong address, Span<byte> destination)
{
if (State != DebuggerRunState.Paused)
{
return false;
}
for (var i = 0; i < destination.Length; i++)
{
if (!_memory.TryGetValue(address + (ulong)i, out var value))
{
return false;
}
destination[i] = value;
}
return true;
}
public bool TryWriteMemory(ulong address, ReadOnlySpan<byte> source)
{
if (State != DebuggerRunState.Paused)
{
return false;
}
SeedMemory(address, source);
return true;
}
public bool TryReadXmm(int registerIndex, out ulong low, out ulong high)
{
low = 0;
high = 0;
return State == DebuggerRunState.Paused;
}
public bool Continue()
{
ContinueCallCount++;
if (State != DebuggerRunState.Paused)
{
return false;
}
State = DebuggerRunState.Running;
return true;
}
public bool StepFrame()
{
StepFrameCallCount++;
if (State != DebuggerRunState.Paused)
{
return false;
}
State = DebuggerRunState.Running;
return true;
}
public void RequestPause() => PauseRequested = true;
public void NotifyTerminated() => State = DebuggerRunState.Terminated;
private sealed class NullCpuDebugHook : ICpuDebugHook
{
public static readonly NullCpuDebugHook Instance = new();
public void OnFrameEnter(ICpuDebugFrame frame)
{
}
public void OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result)
{
}
public void OnStall(ICpuDebugFrame frame, CpuStallInfo info)
{
}
}
}
@@ -1,21 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpEmu.Debugger\SharpEmu.Debugger.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>
@@ -1,293 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using SharpEmu.ShaderCompiler;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
/// <summary>
/// Coverage for AGC attrib-table → BufferFormat merge and semantic indexing.
/// </summary>
public sealed class AgcVertexMetadataTests
{
[Fact]
public void BuildVertexResources_UsesSemanticNotHardwareMappingAsAttribIndex()
{
// input_semantics[0]: semantic=1, hardware_mapping=4, size=2
// If hardware_mapping were wrongly used as the attrib index, we'd read
// attrib[4] instead of attrib[1] and get the wrong format/offset.
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
// ShaderSemantic word: semantic=1, hw_mapping=4, size_in_elements=2
WriteUInt32(memory, semanticsAddress, 1u | (4u << 8) | (2u << 16));
// attrib[0] unused garbage
WriteUInt32(memory, attribTable, 0xDEAD_BEEFu);
// attrib[1]: buffer=0, format=k16_16Float(29), offset=8, fetch=0
WriteUInt32(memory, attribTable + 4, 0u | (29u << 5) | (8u << 14));
// V# at buffer table[0]: base=sharpBase, stride=16
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(
memory,
bufferTable + 4,
(uint)(sharpBase >> 32) | (16u << 16));
var scalars = new uint[32];
scalars[8] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[9] = (uint)(attribTable >> 32);
scalars[10] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[11] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 10,
VertexAttribReg: 8,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
Assert.True(
AgcVertexMetadata.TryBuildVertexResourcesFromMetadata(
ctx,
scalars,
tables,
out var resources));
Assert.Single(resources);
Assert.Equal(1u, resources[0].Semantic);
Assert.Equal(4u, resources[0].HardwareMapping);
Assert.Equal(8u, resources[0].OffsetBytes);
Assert.Equal(5u, resources[0].DataFormat); // R16G16
Assert.Equal(7u, resources[0].NumberFormat); // Float
Assert.Equal(2u, resources[0].ComponentCount);
Assert.Equal(sharpBase, resources[0].SharpBase);
Assert.False(resources[0].PerInstance);
}
[Fact]
public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
// format k8_8_8_8UNorm(56), offset=12
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[64];
var discovered = new[]
{
new Gen5VertexInputBinding(
Pc: 0x40,
Location: 0,
ComponentCount: 4,
DataFormat: 14, // wrong IR guess
NumberFormat: 7,
BaseAddress: sharpBase,
Stride: 16,
OffsetBytes: 0,
Data: data,
DataLength: data.Length,
DataPooled: false),
};
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
discovered);
Assert.Single(merged);
Assert.Equal(0u, merged[0].Location);
Assert.Equal(sharpBase, merged[0].BaseAddress);
Assert.Same(data, merged[0].Data);
Assert.Equal(10u, merged[0].DataFormat); // RGBA8
Assert.Equal(0u, merged[0].NumberFormat); // Unorm
Assert.Equal(12u, merged[0].OffsetBytes);
Assert.Equal(0x40u, merged[0].Pc);
}
[Fact]
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
{
// Attrib tables store VertexAttribFormat (227 = rgba8 unorm), not
// BufferFormat (56). Without conversion the format patch is a no-op.
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (227u << 5) | (12u << 14)); // VertexAttribFormat
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[64];
var discovered = new[]
{
new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 16, 12, data, data.Length, false),
};
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
discovered);
Assert.Equal(10u, merged[0].DataFormat);
Assert.Equal(0u, merged[0].NumberFormat);
Assert.Equal(12u, merged[0].OffsetBytes);
}
[Fact]
public void MergeVertexInputs_MatchesInterleavedAttrsByOffsetNotBareBase()
{
// Both attributes share SharpBase. Matching by base alone would assign
// the color format to position (video/UI regression).
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
// semantic0 → pos float4 @0; semantic1 → color rgba8 @12
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, semanticsAddress + 4, 1u | (4u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (77u << 5) | (0u << 14)); // k32_32_32_32Float
WriteUInt32(memory, attribTable + 4, 0u | (56u << 5) | (12u << 14)); // rgba8unorm @12
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 2,
InputSemanticsAddress: semanticsAddress);
var data = new byte[64];
var discovered = new[]
{
new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 16, 0, data, data.Length, false),
new Gen5VertexInputBinding(
0x80, 1, 4, 14, 7, sharpBase, 16, 12, data, data.Length, false),
};
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
discovered);
Assert.Equal(2, merged.Count);
Assert.Equal(0u, merged[0].OffsetBytes);
Assert.Equal(12u, merged[1].OffsetBytes);
Assert.Equal(0u, merged[1].NumberFormat); // Unorm color, not float
Assert.Equal(10u, merged[1].DataFormat); // RGBA8
Assert.Equal(sharpBase, merged[0].BaseAddress);
Assert.Equal(sharpBase, merged[1].BaseAddress);
Assert.Same(data, merged[0].Data);
}
[Fact]
public void CollectFetchPrologPcs_FindsSBufferLoadsFromTableRegisters()
{
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 10,
VertexAttribReg: 8,
InputSemanticsCount: 1,
InputSemanticsAddress: 1);
var program = new Gen5ShaderProgram(
0,
[
new Gen5ShaderInstruction(
0x10,
Gen5ShaderEncoding.Smem,
"SBufferLoadDword",
Words: [],
Sources: [Gen5Operand.Scalar(8)],
Destinations: [Gen5Operand.Scalar(20)],
new Gen5ScalarMemoryControl(1, 0, null)),
new Gen5ShaderInstruction(
0x20,
Gen5ShaderEncoding.Smem,
"SBufferLoadDword",
Words: [],
Sources: [Gen5Operand.Scalar(12)],
Destinations: [Gen5Operand.Scalar(24)],
new Gen5ScalarMemoryControl(1, 0, null)),
new Gen5ShaderInstruction(
0x30,
Gen5ShaderEncoding.Sopp,
"SEndpgm",
Words: [],
Sources: [],
Destinations: [],
null),
]);
var pcs = AgcVertexMetadata.CollectFetchPrologPcs(program, tables);
Assert.Contains(0x10u, pcs);
Assert.DoesNotContain(0x20u, pcs);
}
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
}
@@ -105,92 +105,6 @@ public sealed class AprStreamingContractTests
} }
} }
[Fact]
public void ResolveFilepathsWithPrefixToIdsAndFileSizes_CombinesPrefixAndResolvesRealFile()
{
// Resource streamers call WithPrefix to join a directory prefix with a
// relative asset path. Without HLE every call returned NOT_FOUND and no
// asset received a real file id/size.
const ulong memoryBase = 0x1_0000_0000;
const ulong prefixAddress = memoryBase + 0x80;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
byte[] fileContents = [1, 2, 3, 4, 5, 6];
var mountRoot = Path.Combine(
Path.GetTempPath(),
$"sharpemu-apr-prefix-{Guid.NewGuid():N}");
Directory.CreateDirectory(mountRoot);
var mountPoint = $"/sharpemu_apr_prefix_mnt_{Guid.NewGuid():N}";
const string fileName = "asset.bin";
var hostPath = Path.Combine(mountRoot, fileName);
try
{
File.WriteAllBytes(hostPath, fileContents);
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, mountRoot);
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(prefixAddress, mountPoint);
memory.WriteCString(pathAddress, fileName);
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = prefixAddress;
context[CpuRegister.Rsi] = pathListAddress;
context[CpuRegister.Rdx] = 1;
context[CpuRegister.Rcx] = idsAddress;
context[CpuRegister.R8] = sizesAddress;
context[CpuRegister.R9] = 0;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(context));
Assert.NotEqual(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal((ulong)fileContents.Length, ReadUInt64(memory, sizesAddress));
}
finally
{
KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
if (Directory.Exists(mountRoot))
{
Directory.Delete(mountRoot, recursive: true);
}
}
}
[Fact]
public void ResolveFilepathsWithPrefixToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong prefixAddress = memoryBase + 0x80;
const ulong pathListAddress = memoryBase + 0x100;
const ulong pathAddress = memoryBase + 0x200;
const ulong idsAddress = memoryBase + 0x800;
const ulong sizesAddress = memoryBase + 0x880;
const ulong errorIndexAddress = memoryBase + 0x8F0;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(prefixAddress, "/does-not-exist-prefix");
memory.WriteCString(pathAddress, $"missing-{Guid.NewGuid():N}.bin");
WriteUInt64(memory, pathListAddress, pathAddress);
context[CpuRegister.Rdi] = prefixAddress;
context[CpuRegister.Rsi] = pathListAddress;
context[CpuRegister.Rdx] = 1;
context[CpuRegister.Rcx] = idsAddress;
context[CpuRegister.R8] = sizesAddress;
context[CpuRegister.R9] = errorIndexAddress;
Assert.Equal(
-1,
KernelMemoryCompatExports.KernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(uint.MaxValue, ReadUInt32(memory, idsAddress));
Assert.Equal(0ul, ReadUInt64(memory, sizesAddress));
Assert.Equal(0u, ReadUInt32(memory, errorIndexAddress));
}
[Fact] [Fact]
public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex() public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex()
{ {
@@ -1,86 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2PortGetStateExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong StateAddress = MemoryBase + 0x100;
private static CpuContext CreateContext(out FakeCpuMemory memory)
{
memory = new FakeCpuMemory(MemoryBase, 0x1000);
return new CpuContext(memory, Generation.Gen5);
}
[Fact]
public void PortGetState_WritesFixedSizeIgnoringPollutedR9()
{
var ctx = CreateContext(out var memory);
// Paint the buffer so we can see the write footprint.
Span<byte> paint = stackalloc byte[0x100];
paint.Fill(0xAB);
Assert.True(memory.TryWrite(StateAddress, paint));
ctx[CpuRegister.Rdi] = 0xDE1FF6800001UL;
ctx[CpuRegister.Rsi] = StateAddress;
ctx[CpuRegister.Rdx] = StateAddress + 0x200;
// Polluted GetSize leftover — must NOT enlarge the write.
ctx[CpuRegister.R9] = 0x180;
var result = AudioOut2Exports.AudioOut2PortGetState(ctx);
Assert.Equal(0, result);
Span<byte> state = stackalloc byte[0x100];
Assert.True(memory.TryRead(StateAddress, state));
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state));
Assert.Equal(2, state[2]);
// Bytes past the fixed 0x20 header must remain untouched.
Assert.Equal(0xAB, state[0x20]);
Assert.Equal(0xAB, state[0x7F]);
}
[Fact]
public void PortGetState_SkipsGuestStackOutBuffer()
{
var ctx = CreateContext(out _);
const ulong stackOut = 0x00007FFFDE1FF688UL;
ctx[CpuRegister.Rdi] = 0xDE1FF688004DUL;
ctx[CpuRegister.Rsi] = stackOut;
ctx[CpuRegister.Rdx] = 0;
var result = AudioOut2Exports.AudioOut2PortGetState(ctx);
Assert.Equal(0, result);
}
[Fact]
public void GetSpeakerInfo_WritesFixedSizeToRdiNotRsiTypeFlag()
{
var ctx = CreateContext(out var memory);
Span<byte> paint = stackalloc byte[0x80];
paint.Fill(0xCD);
Assert.True(memory.TryWrite(StateAddress, paint));
ctx[CpuRegister.Rdi] = StateAddress;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = StateAddress + 0x200;
ctx[CpuRegister.R8] = 0x840;
ctx[CpuRegister.R9] = 0x10C;
var result = AudioOut2Exports.AudioOut2GetSpeakerInfo(ctx);
Assert.Equal(0, result);
Span<byte> info = stackalloc byte[0x80];
Assert.True(memory.TryRead(StateAddress, info));
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(info));
Assert.Equal(48000u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..]));
Assert.Equal(0xCD, info[0x20]);
}
}
@@ -1,140 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2SpeakerArrayExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong OutHandleAddress = MemoryBase + 0x100;
private const ulong ReservedAddress = MemoryBase + 0x120;
private const ulong ParamAddress = MemoryBase + 0x200;
private const ulong SpeakerMemoryAddress = MemoryBase + 0x400;
private static CpuContext CreateContext(out FakeCpuMemory memory)
{
memory = new FakeCpuMemory(MemoryBase, 0x2000);
return new CpuContext(memory, Generation.Gen5);
}
private static void WriteU64(FakeCpuMemory memory, ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
Assert.True(memory.TryWrite(address, bytes));
}
private static ulong ReadU64(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[8];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
private static uint ReadU32(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[4];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
[Fact]
public void GetSpeakerArrayMemorySize_NeverReturnsTheNotFoundSentinel()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 8;
var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx);
Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
Assert.Equal(0x40 + 8 * 0x100 + 0x400, result);
Assert.Equal((ulong)result, ctx[CpuRegister.Rax]);
Assert.True(result < 0x10000);
}
[Fact]
public void GetSpeakerArrayMemorySize_TwoChannelsIsExactChannelScaledSize()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 2;
var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx);
Assert.Equal(0x40 + 2 * 0x100 + 0x400, result);
Assert.Equal(0x640UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void SpeakerArrayCreate_PublishesObjectPointerAndLeavesReservedSizeAlone()
{
var ctx = CreateContext(out var memory);
// Stage a size in the reserved slot the way callers do before Create.
WriteU64(memory, ReservedAddress, 0x100);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(0UL, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]);
Assert.Equal(ctx[CpuRegister.Rax], ReadU64(memory, OutHandleAddress));
// Reserved/size slot must remain untouched — writing it corrupted canaries.
Assert.Equal(0x100UL, ReadU64(memory, ReservedAddress));
}
[Fact]
public void SpeakerArrayCreate_PublishesHandleForTypicalCallShape()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(0UL, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x10000UL, ctx[CpuRegister.Rax]);
Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result);
}
[Fact]
public void SpeakerArrayCreate_IgnoresCorruptedParamBufferFields()
{
var ctx = CreateContext(out var memory);
// Simulate PortGetState having overwritten param+0x18 (size) with a
// state blob — Create must NOT adopt that as an in-place buffer.
WriteU64(memory, ParamAddress + 0x10, SpeakerMemoryAddress);
WriteU64(memory, ParamAddress + 0x18, 0x100);
ctx[CpuRegister.Rdi] = ParamAddress;
ctx[CpuRegister.Rsi] = OutHandleAddress;
ctx[CpuRegister.Rdx] = ReservedAddress;
ctx[CpuRegister.Rcx] = 2;
var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx);
Assert.Equal(0, result);
Assert.NotEqual(SpeakerMemoryAddress, ctx[CpuRegister.Rax]);
Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]);
}
[Fact]
public void SpeakerArrayDestroy_UnknownHandleStillSucceeds()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 0xDEAD_BEEF;
var result = AudioOut2Exports.AudioOut2SpeakerArrayDestroy(ctx);
Assert.Equal(0, result);
}
}
+1 -31
View File
@@ -7,18 +7,15 @@ namespace SharpEmu.Libs.Tests;
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small // A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
// structures to HLE exports under test without a live guest. // structures to HLE exports under test without a live guest.
internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator internal sealed class FakeCpuMemory : ICpuMemory
{ {
private readonly ulong _base; private readonly ulong _base;
private readonly byte[] _storage; private readonly byte[] _storage;
private ulong _allocBump;
public FakeCpuMemory(ulong baseAddress, int size) public FakeCpuMemory(ulong baseAddress, int size)
{ {
_base = baseAddress; _base = baseAddress;
_storage = new byte[size]; _storage = new byte[size];
// Bump from the top so test fixtures at low offsets stay intact.
_allocBump = baseAddress + (ulong)size;
} }
public bool TryRead(ulong virtualAddress, Span<byte> destination) public bool TryRead(ulong virtualAddress, Span<byte> destination)
@@ -43,33 +40,6 @@ internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator
return true; return true;
} }
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
address = 0;
if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0)
{
return false;
}
var alignedSize = (size + alignment - 1) & ~(alignment - 1);
if (alignedSize > _allocBump - _base)
{
return false;
}
var next = (_allocBump - alignedSize) & ~(alignment - 1);
if (next < _base)
{
return false;
}
_allocBump = next;
address = next;
return true;
}
public bool TryFreeGuestMemory(ulong address) => false;
public ulong WriteCString(ulong virtualAddress, string text) public ulong WriteCString(ulong virtualAddress, string text)
{ {
var bytes = System.Text.Encoding.UTF8.GetBytes(text); var bytes = System.Text.Encoding.UTF8.GetBytes(text);
@@ -125,125 +125,6 @@ public sealed class KernelMemoryCompatExportsTests
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]); Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
} }
// FreeBSD/PS4 EBADF; PosixFailure maps ORBIS NOT_FOUND on fd calls to this.
private const int Ebadf = 9;
// FreeBSD/PS4 EACCES; PosixFailure maps ORBIS PERMISSION_DENIED to this.
private const int Eacces = 13;
// TLS slot used by KernelRuntimeCompatExports.TrySetErrno (FsBase + 0x40).
private const ulong TlsErrnoOffset = 0x40;
[Fact]
public void PosixLseek_BadDescriptorReturnsMinusOneWithEbadf()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong fsBase = memoryBase + 0x100;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5)
{
FsBase = fsBase,
};
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0; // SEEK_SET
var result = KernelMemoryCompatExports.PosixLseek(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(Ebadf, ReadErrno(memory, fsBase));
}
[Fact]
public void PosixPread_BadDescriptorReturnsMinusOneWithEbadf()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong bufferAddress = memoryBase + 0x200;
const ulong fsBase = memoryBase + 0x100;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5)
{
FsBase = fsBase,
};
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
context[CpuRegister.Rsi] = bufferAddress;
context[CpuRegister.Rdx] = 0x40;
context[CpuRegister.Rcx] = 0; // offset
var result = KernelMemoryCompatExports.PosixPread(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(Ebadf, ReadErrno(memory, fsBase));
}
[Fact]
public void KernelPread_BadDescriptorStillReturnsOrbisNotFound()
{
// sceKernel* entry points keep the raw Orbis ABI; only Posix* maps to -1/errno.
const ulong memoryBase = 0x1_0000_0000;
const ulong bufferAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = 0x80020002;
context[CpuRegister.Rsi] = bufferAddress;
context[CpuRegister.Rdx] = 0x40;
context[CpuRegister.Rcx] = 0;
var result = KernelMemoryCompatExports.KernelPread(context);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
}
[Fact]
public void PosixOpen_MutatingApp0ReturnsMinusOneWithEacces()
{
// /app0 is read-only for mutating opens (retail semantics). That path
// returns PERMISSION_DENIED which PosixFailure maps to EACCES - the same
// errno UnauthorizedAccessException open failures now produce.
var tempRoot = Path.Combine(
Path.GetTempPath(),
$"sharpemu-posix-open-eacces-{Guid.NewGuid():N}");
var app0Root = Path.Combine(tempRoot, "app0");
Directory.CreateDirectory(app0Root);
KernelMemoryCompatExports.RegisterGuestPathMount("/app0", app0Root);
try
{
const ulong memoryBase = 0x1_0000_0000;
const ulong pathAddress = memoryBase + 0x200;
const ulong fsBase = memoryBase + 0x100;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5)
{
FsBase = fsBase,
};
memory.WriteCString(pathAddress, "/app0/readonly-create.bin");
context[CpuRegister.Rdi] = pathAddress;
context[CpuRegister.Rsi] = 0x0201; // O_WRONLY | O_CREAT
var result = KernelMemoryCompatExports.PosixOpen(context);
Assert.Equal(-1, result);
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.Equal(Eacces, ReadErrno(memory, fsBase));
}
finally
{
KernelMemoryCompatExports.UnregisterGuestPathMount("/app0");
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
private static int ReadErrno(FakeCpuMemory memory, ulong fsBase)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
Assert.True(memory.TryRead(fsBase + TlsErrnoOffset, bytes));
return BitConverter.ToInt32(bytes);
}
[Fact] [Fact]
public void Sprintf_ReadsVariadicDoubleFromXmmRegister() public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
{ {
@@ -24,53 +24,13 @@ public sealed unsafe class GuestImageWriteTrackerTests
// spilling onto neighbouring heap pages. // spilling onto neighbouring heap pages.
private const nuint TrackedByteCount = 4096; private const nuint TrackedByteCount = 4096;
private const nuint HostPageAlignment = 16384; private const nuint HostPageAlignment = 16384;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
private const uint PageReadWrite = 0x04;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(
nint lpAddress,
nuint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private static ulong AllocateTrackedPages(out void* allocation) private static ulong AllocateTrackedPages(out void* allocation)
{ {
// VirtualProtect (Windows) / mprotect (POSIX) must target
// VirtualAlloc/mmap pages. Protecting CRT heap pages poisons
// neighbouring allocator metadata and crashes the test host.
if (OperatingSystem.IsWindows())
{
var windowsAllocation = VirtualAlloc(
0,
HostPageAlignment,
MemCommit | MemReserve,
PageReadWrite);
Assert.NotEqual(nint.Zero, windowsAllocation);
allocation = (void*)windowsAllocation;
return (ulong)windowsAllocation;
}
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment); allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
return (ulong)allocation; return (ulong)allocation;
} }
private static void FreeTrackedPages(void* allocation)
{
if (OperatingSystem.IsWindows())
{
_ = VirtualFree((nint)allocation, 0, MemRelease);
return;
}
NativeMemory.Free(allocation);
}
[Fact] [Fact]
public void GenerationSurvivesDirtyConsume() public void GenerationSurvivesDirtyConsume()
{ {
@@ -98,7 +58,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally finally
{ {
GuestImageWriteTracker.Untrack(address); GuestImageWriteTracker.Untrack(address);
FreeTrackedPages(allocation); NativeMemory.Free(allocation);
} }
} }
@@ -129,7 +89,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally finally
{ {
GuestImageWriteTracker.Untrack(address); GuestImageWriteTracker.Untrack(address);
FreeTrackedPages(allocation); NativeMemory.Free(allocation);
} }
} }
@@ -158,7 +118,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally finally
{ {
GuestImageWriteTracker.Untrack(address); GuestImageWriteTracker.Untrack(address);
FreeTrackedPages(allocation); NativeMemory.Free(allocation);
} }
} }
@@ -1,119 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.LibcStdio;
using SharpEmu.Libs.Messenger;
using Xunit;
namespace SharpEmu.Libs.Tests;
public sealed class MessengerCompatExportsTests
{
[Fact]
public void Cosf_UsesScalarXmmArgumentAndReturn()
{
const ulong memoryBase = 0x1_0000_0000;
var context = new CpuContext(new FakeCpuMemory(memoryBase, 0x1000), Generation.Gen5);
var input = 0.5f;
var inputBits = unchecked((uint)BitConverter.SingleToInt32Bits(input));
context[CpuRegister.Rdi] = 0xDEAD_BEEF; // Must not be used as the argument.
context.SetXmmRegister(0, 0xAABB_CCDD_0000_0000UL | inputBits, 0x1122_3344_5566_7788UL);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, MessengerCompatExports.Cosf(context));
context.GetXmmRegister(0, out var low, out var high);
var expectedBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(input)));
Assert.Equal(expectedBits, unchecked((uint)low));
Assert.Equal(0xAABB_CCDDUL, low >> 32);
Assert.Equal(0x1122_3344_5566_7788UL, high);
}
[Fact]
public void CtypeCaseTables_MapAsciiCharacters()
{
var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtolower(context));
var lower = unchecked((nint)(long)context[CpuRegister.Rax]);
Assert.Equal((short)'a', Marshal.ReadInt16(lower + ('A' * sizeof(short))));
Assert.Equal((short)'z', Marshal.ReadInt16(lower + ('z' * sizeof(short))));
Assert.Equal((short)-1, Marshal.ReadInt16(lower - sizeof(short)));
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtoupper(context));
var upper = unchecked((nint)(long)context[CpuRegister.Rax]);
Assert.Equal((short)'A', Marshal.ReadInt16(upper + ('a' * sizeof(short))));
Assert.Equal((short)'Z', Marshal.ReadInt16(upper + ('Z' * sizeof(short))));
Assert.Equal((short)-1, Marshal.ReadInt16(upper - sizeof(short)));
}
[Fact]
public void Il2CppLookup_ReturnsPointerInRaxWithoutWritingRsi()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong nameAddress = memoryBase + 0x100;
const ulong outputAddress = memoryBase + 0x200;
const ulong resolvedAddress = 0x2_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
memory.WriteCString(nameAddress, "il2cpp_test");
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = nameAddress;
context[CpuRegister.Rsi] = outputAddress; // Live caller state, not an output pointer.
Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE));
var result = Il2CppApiLookupAbi.SetResult(context, resolved: true, resolvedAddress);
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
Assert.Equal(resolvedAddress, context[CpuRegister.Rax]);
Assert.True(context.TryReadUInt64(outputAddress, out var output));
Assert.Equal(0xCAFE_BABEUL, output);
}
[Fact]
public void Il2CppLookup_MissingApiReturnsNullWithoutWritingRsi()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong nameAddress = memoryBase + 0x100;
const ulong outputAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
memory.WriteCString(nameAddress, "il2cpp_missing");
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = nameAddress;
context[CpuRegister.Rsi] = outputAddress;
Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE));
var result = Il2CppApiLookupAbi.SetResult(context, resolved: false, address: 0);
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
Assert.Equal(0UL, context[CpuRegister.Rax]);
Assert.True(context.TryReadUInt64(outputAddress, out var output));
Assert.Equal(0xCAFE_BABEUL, output);
}
[Fact]
public void TrackedLibcHeapFallback_ReadsAndWritesHostAllocation()
{
var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
context[CpuRegister.Rdi] = 16;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelMemoryCompatExports.Malloc(context));
var address = context[CpuRegister.Rax];
Assert.NotEqual(0UL, address);
try
{
Assert.True(KernelMemoryCompatExports.TryWriteUInt64Compat(context, address, 0x1234_5678_9ABC_DEF0UL));
Span<byte> bytes = stackalloc byte[8];
Assert.True(KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, bytes));
Assert.True(KernelMemoryCompatExports.TryReadUInt64Compat(context, address, out var value));
Assert.Equal(0x1234_5678_9ABC_DEF0UL, value);
}
finally
{
context[CpuRegister.Rdi] = address;
_ = KernelMemoryCompatExports.Free(context);
}
}
}
@@ -1,58 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Remoteplay;
using Xunit;
namespace SharpEmu.Libs.Tests.Remoteplay;
public sealed class RemoteplayExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong StatusAddress = MemoryBase + 0x100;
private static CpuContext CreateContext(out FakeCpuMemory memory)
{
memory = new FakeCpuMemory(MemoryBase, 0x1000);
return new CpuContext(memory, Generation.Gen5);
}
[Fact]
public void Initialize_Succeeds()
{
var ctx = CreateContext(out _);
var result = RemoteplayExports.RemoteplayInitialize(ctx);
Assert.Equal(0, result);
}
[Fact]
public void GetConnectionStatus_WritesDisconnectedStatus()
{
var ctx = CreateContext(out var memory);
ctx[CpuRegister.Rdi] = 0x1000_0000;
ctx[CpuRegister.Rsi] = StatusAddress;
memory.TryWrite(StatusAddress, stackalloc byte[] { 0xFF, 0xFF, 0xFF, 0xFF });
var result = RemoteplayExports.RemoteplayGetConnectionStatus(ctx);
Assert.Equal(0, result);
var status = new byte[4];
Assert.True(ctx.Memory.TryRead(StatusAddress, status));
Assert.Equal(0, status[0]);
}
[Fact]
public void GetConnectionStatus_NullOutPointerStillSucceeds()
{
var ctx = CreateContext(out _);
ctx[CpuRegister.Rdi] = 0x1000_0000;
ctx[CpuRegister.Rsi] = 0;
var result = RemoteplayExports.RemoteplayGetConnectionStatus(ctx);
Assert.Equal(0, result);
}
}