diff --git a/SharpEmu.slnx b/SharpEmu.slnx index bfdfc119..aa185930 100644 --- a/SharpEmu.slnx +++ b/SharpEmu.slnx @@ -19,7 +19,6 @@ SPDX-License-Identifier: GPL-2.0-or-later - diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs index 6b223033..c741472d 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Diagnostics.cs @@ -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 = ""; - 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() { var trace = _recentImportTrace; @@ -333,15 +268,14 @@ public sealed partial class DirectExecutionBackend ulong callRip = returnRip + (ulong)i; ulong target = unchecked((ulong)((long)(callRip + 5) + rel32)); 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; } - string nid = importEntries[importIndex].Nid; + string nid = _importEntries[importIndex].Nid; if (_moduleManager.TryGetExport(nid, out var export)) { Log.Debug( @@ -368,14 +302,14 @@ public sealed partial class DirectExecutionBackend { Log.Debug( $"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; } - string nid = importEntries[importIndex].Nid; + string nid = _importEntries[importIndex].Nid; if (_moduleManager.TryGetExport(nid, out var export)) { Log.Debug( diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index b76069ee..ca25bb1c 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -55,7 +55,6 @@ public sealed partial class DirectExecutionBackend } _exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub); Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}"); - SharpEmu.HLE.GuestImageWriteTracker.WarmUp(); _unhandledFilterDelegate = UnhandledExceptionFilter; _unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate); @@ -118,13 +117,6 @@ public sealed partial class DirectExecutionBackend { return -1; } - if (exceptionCode == 3221225477u && - exceptionRecord->NumberParameters >= 2 && - SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault( - exceptionRecord->ExceptionInformation[1])) - { - return -1; - } if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip)) { return -1; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index c6516079..345a23f3 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -69,15 +69,6 @@ public sealed partial class DirectExecutionBackend 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); } @@ -2033,96 +2024,38 @@ public sealed partial class DirectExecutionBackend { return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - - NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress); - try + ulong symbolNameAddress = cpuContext[CpuRegister.Rsi]; + ulong outputAddress = cpuContext[CpuRegister.Rdx]; + if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName)) { - if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName)) - { - return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); - } - - var moduleHandle = unchecked((int)cpuContext[CpuRegister.Rdi]); - if (!TryResolveModuleSymbolAddress(moduleHandle, symbolName, out var resolvedAddress) && - !TryResolveRuntimeSymbolAddress(symbolName, out resolvedAddress) && - !TryResolveRuntimeSymbolAddress(ComputePsNid(symbolName), out resolvedAddress) && - !TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress)) - { - Console.Error.WriteLine( - $"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'"); - return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); - } - - if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DLSYM"), "1", StringComparison.Ordinal)) - { - Console.Error.WriteLine( - $"[LOADER][TRACE] sceKernelDlsym: handle=0x{moduleHandle:X} symbol='{symbolName}' -> 0x{resolvedAddress:X16}"); - } - - if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress)) - { - return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); - } + cpuContext[CpuRegister.Rax] = 18446744073709551615uL; + return OrbisGen2Result.ORBIS_GEN2_OK; } - catch + var moduleHandle = unchecked((int)cpuContext[CpuRegister.Rdi]); + if (!TryResolveModuleSymbolAddress(moduleHandle, symbolName, out var resolvedAddress) && + !TryResolveRuntimeSymbolAddress(symbolName, out resolvedAddress) && + !TryResolveRuntimeSymbolAddress(ComputePsNid(symbolName), out resolvedAddress) && + !TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress)) { - return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress); + Console.Error.WriteLine( + $"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'"); + cpuContext[CpuRegister.Rax] = 18446744073709551615uL; + return OrbisGen2Result.ORBIS_GEN2_OK; + } + if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DLSYM"), "1", StringComparison.Ordinal)) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] sceKernelDlsym: handle=0x{moduleHandle:X} symbol='{symbolName}' -> 0x{resolvedAddress:X16}"); + } + if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress)) + { + cpuContext[CpuRegister.Rax] = 18446744073709551615uL; + return OrbisGen2Result.ORBIS_GEN2_OK; } - cpuContext[CpuRegister.Rax] = 0uL; 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) { if (KernelModuleRegistry.TryResolveModuleSymbol(moduleHandle, symbolName, out address)) @@ -2177,18 +2110,25 @@ public sealed partial class DirectExecutionBackend } var symbolNameAddress = cpuContext[CpuRegister.Rdi]; + var outputAddress = cpuContext[CpuRegister.Rsi]; 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( - $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}'"); - // il2cpp_api_lookup_symbol is a normal one-argument pointer-returning - // 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. - return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: false, address: 0); + $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}"); + if (outputAddress != 0) + { + _ = TryWriteUInt64Compat(outputAddress, 0); + } + + cpuContext[CpuRegister.Rax] = ulong.MaxValue; + return OrbisGen2Result.ORBIS_GEN2_OK; } - return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: true, resolvedAddress); + cpuContext[CpuRegister.Rax] = 0; + return OrbisGen2Result.ORBIS_GEN2_OK; } private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address) @@ -2198,23 +2138,8 @@ public sealed partial class DirectExecutionBackend return true; } - if (Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && - 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; + return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && + TryResolveRuntimeSymbolAddress(symbol.Nid, out address); } private OrbisGen2Result DispatchBootstrapBridge() diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index a8b1f65d..4fb10e6f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -3,7 +3,6 @@ using System; using System.Buffers.Binary; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -89,13 +88,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ulong GuestThreadHandle, int ManagedThreadId); - private readonly record struct DeferredBootstrapTraceEntry( - long DispatchIndex, - ulong Op, - ulong SymbolPointer, - ulong OutputPointer, - ulong ReturnRip); - #pragma warning disable CS0649 private struct EXCEPTION_POINTERS { @@ -311,31 +303,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private KeyValuePair[] _runtimeSymbolsByAddress = Array.Empty>(); - private readonly ConcurrentDictionary _runtimeSymbolsByName = - 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 _lazyDlsymStubCache = new(StringComparer.Ordinal); - - private ulong _lazyImportStubPoolBase; - - private ulong _lazyImportStubNextSlot; - - private ulong _lazyImportStubPoolLimit; - - private bool _lazyImportStubPoolMapped; + private readonly Dictionary _runtimeSymbolsByName = new Dictionary(StringComparer.Ordinal); private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64]; @@ -343,14 +311,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I 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 int _distinctImportNidHistoryCount; @@ -1180,14 +1140,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I result = OrbisGen2Result.ORBIS_GEN2_OK; LastError = null; InitializeRuntimeSymbolIndex(runtimeSymbols); - ResetLazyDlsymStubState(); _recentImportTraceCount = 0; _recentImportTraceWriteIndex = 0; - lock (_deferredBootstrapTraceGate) - { - _deferredBootstrapTraceCount = 0; - _deferredBootstrapTraceWriteIndex = 0; - } _distinctImportNidHistoryCount = 0; _distinctImportNidHistoryWriteIndex = 0; _lastDistinctImportNid = string.Empty; @@ -1270,7 +1224,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I finally { HostSessionControl.SetShutdownHandler(null); - DrainDeferredBootstrapTraces(); GuestThreadExecution.Scheduler = previousGuestThreadScheduler; 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]; 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; - 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; } - string text = importEntries[i].Nid; + string text = _importEntries[i].Nid; 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}"); @@ -7027,7 +6979,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _importEntries = Array.Empty(); _runtimeSymbolsByName.Clear(); StopReadyThreadDispatcher(); - ResetLazyDlsymStubState(); StopStallWatchdog(); if (_exceptionHandler != 0) { diff --git a/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs b/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs deleted file mode 100644 index 683946e3..00000000 --- a/src/SharpEmu.Core/Cpu/Native/Il2CppApiLookupAbi.cs +++ /dev/null @@ -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; - } -} diff --git a/src/SharpEmu.GUI/EmulatorProcess.cs b/src/SharpEmu.GUI/EmulatorProcess.cs index 822b181c..290d3f9e 100644 --- a/src/SharpEmu.GUI/EmulatorProcess.cs +++ b/src/SharpEmu.GUI/EmulatorProcess.cs @@ -387,7 +387,7 @@ internal sealed class EmulatorProcess : IDisposable private void ForwardOutput(string? line, bool isError) { - if (line is not null) + if (!string.IsNullOrEmpty(line)) { OutputReceived?.Invoke(line, isError); } diff --git a/src/SharpEmu.HLE/GuestImageWriteTracker.cs b/src/SharpEmu.HLE/GuestImageWriteTracker.cs index c72b3263..c749f7a3 100644 --- a/src/SharpEmu.HLE/GuestImageWriteTracker.cs +++ b/src/SharpEmu.HLE/GuestImageWriteTracker.cs @@ -1,7 +1,6 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; @@ -81,7 +80,7 @@ public static unsafe class GuestImageWriteTracker 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"; private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter = ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS")); @@ -96,36 +95,12 @@ public static unsafe class GuestImageWriteTracker _enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0; private static long _lifetimeTraceSequence; - private const uint PageReadonly = 0x02; - private const uint PageReadWrite = 0x04; - [DllImport("libc", EntryPoint = "mprotect", SetLastError = true)] private static extern int Mprotect(nint address, nuint length, int protection); [DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)] 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; /// @@ -140,17 +115,7 @@ public static unsafe class GuestImageWriteTracker return; } - // VirtualProtect only belongs on VirtualAlloc/mmap pages. Warming on - // 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; - } - + var scratch = NativeMemory.AllocZeroed(4096); try { // Warm the timestamp P/Invoke used by the signal-safe scalar @@ -164,14 +129,7 @@ public static unsafe class GuestImageWriteTracker } finally { - if (OperatingSystem.IsWindows()) - { - _ = VirtualFree(scratch, 0, MemRelease); - } - else - { - NativeMemory.Free((void*)scratch); - } + NativeMemory.Free(scratch); } } @@ -487,7 +445,10 @@ public static unsafe class GuestImageWriteTracker } if (needsUnprotect && - !TrySetProtection(writableStart, writableEnd - writableStart, writable: true)) + Mprotect( + (nint)writableStart, + (nuint)(writableEnd - writableStart), + ProtRead | ProtWrite) != 0) { return false; } @@ -536,7 +497,10 @@ public static unsafe class GuestImageWriteTracker // A new publication/rearm starts a new first-write lifetime. 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) { Volatile.Write(ref range.Armed, 0); @@ -556,7 +520,10 @@ public static unsafe class GuestImageWriteTracker var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1; 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) @@ -712,35 +679,8 @@ public static unsafe class GuestImageWriteTracker $"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() { - if (OperatingSystem.IsWindows()) - { - return Stopwatch.GetTimestamp() * 1_000_000_000L / Stopwatch.Frequency; - } - Timespec time; return ClockGetTime(ClockMonotonicRaw, &time) == 0 ? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds) diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index a18aaa7e..62257b6c 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -80,13 +80,8 @@ public static partial class AgcExports private const uint RIndexCount = 0x1C; private const uint SpiShaderPgmLoPs = 0x8; private const uint SpiShaderPgmHiPs = 0x9; - private const uint SpiShaderPgmLoVs = 0x48; - private const uint SpiShaderPgmHiVs = 0x49; private const uint SpiShaderPgmLoEs = 0xC8; 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 SpiShaderPgmHiLs = 0x149; private const uint SpiShaderPgmLoGs = 0x8A; @@ -280,7 +275,6 @@ public static partial class AgcExports private static int _tracedVertexRangeCount; private static long _dcbWaitRegMemTraceCount; private static long _createShaderTraceCount; - private static long _cbMetadataSkipTraceCount; private static long _packetPayloadTraceCount; private static bool _tracedMissingPixelShaderBindings; 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}"); 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 private static void EnqueueSubmittedDcb( @@ -5684,25 +5646,6 @@ public static partial class AgcExports } state.TranslatedDraw = null; 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) { state.KnownRenderTargets[target.Address] = target; @@ -6488,32 +6431,6 @@ public static partial class AgcExports 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 // 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. @@ -7295,7 +7212,6 @@ public static partial class AgcExports Mix(input.NumberFormat); Mix(input.Stride); Mix(input.OffsetBytes); - Mix(input.PerInstance ? 1u : 0u); } } @@ -7341,35 +7257,6 @@ public static partial class AgcExports return hash; } - private enum CbColorMode : byte - { - Disable = 0, - Normal = 1, - EliminateFastClear = 2, - Resolve = 3, - FmaskDecompress = 5, - DccDecompress = 6, - } - - private static bool TryGetCbColorControlMode( - IReadOnlyDictionary 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( IReadOnlyDictionary registers, out RenderTargetDescriptor source, @@ -7377,8 +7264,8 @@ public static partial class AgcExports { source = default; destination = default; - if (!TryGetCbColorControlMode(registers, out var mode) || - mode != (uint)CbColorMode.Resolve) + if (!registers.TryGetValue(CbColorControl, out var colorControl) || + ((colorControl >> 4) & 0x7u) != 3u) { return false; } @@ -8304,8 +8191,7 @@ public static partial class AgcExports binding.OffsetBytes, binding.Data, binding.DataLength, - binding.DataPooled, - binding.PerInstance); + binding.DataPooled); } return buffers; @@ -11102,14 +10988,18 @@ public static partial class AgcExports return false; } + if (!TryReadUInt32(ctx, shRegistersAddress, out var loRegister) || + !TryReadUInt32(ctx, shRegistersAddress + 8, out var hiRegister)) + { + return false; + } + var expectedLo = shaderType switch { 0 => ComputePgmLo, 1 => SpiShaderPgmLoPs, 2 or 6 => SpiShaderPgmLoEs, - 3 => SpiShaderPgmLoVs, 4 => SpiShaderPgmLoGs, - 5 => SpiShaderPgmLoHs, 7 => SpiShaderPgmLoLs, _ => 0u, }; @@ -11118,187 +11008,20 @@ public static partial class AgcExports 0 => ComputePgmHi, 1 => SpiShaderPgmHiPs, 2 or 6 => SpiShaderPgmHiEs, - 3 => SpiShaderPgmHiVs, 4 => SpiShaderPgmHiGs, - 5 => SpiShaderPgmHiHs, 7 => SpiShaderPgmHiLs, _ => 0u, }; - - // 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)) + if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi) { - TryReadUInt32(ctx, shRegistersAddress, out var firstLo); - // 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}"); + TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}"); return false; } var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); var hiValue = (uint)((codeAddress >> 40) & 0xFFUL); - if (!TryWriteUInt32(ctx, loEntryAddress + sizeof(uint), loValue) || - !TryWriteUInt32(ctx, hiEntryAddress + 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; + return TryWriteUInt32(ctx, shRegistersAddress + sizeof(uint), loValue) && + TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue); } private static bool IsEsGeometryShaderType(byte shaderType) => diff --git a/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs b/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs deleted file mode 100644 index 8537ef27..00000000 --- a/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs +++ /dev/null @@ -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; - -/// -/// AGC embedded vertex metadata. Locates -/// PtrVertexBufferTable / PtrVertexAttribDescTable and builds authoritative -/// attribute layouts that draw translation merges onto IR-discovered fetches. -/// -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); - - /// - /// One AGC attrib-table resource. - /// Representation: is the V# base; attribute byte - /// offset is applied as (Vulkan bind offset), - /// not folded into the base — avoids double-counting when the IR prolog - /// already bumped the sharp address. - /// - 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); - - /// - /// Reads AGC user-data direct-resource offsets for the ES header mapped to - /// . Returns false when the header is - /// unknown or the tables are absent (attribute-less clears). - /// - 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; - } - - /// - /// 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 - /// - internal static bool TryBuildVertexResourcesFromMetadata( - CpuContext ctx, - IReadOnlyList scalarRegisters, - VertexTableRegisters tables, - out IReadOnlyList resources) - { - resources = Array.Empty(); - 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((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; - } - - /// - /// 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. - /// - internal static IReadOnlyList MergeVertexInputsFromMetadata( - CpuContext ctx, - IReadOnlyList scalarRegisters, - VertexTableRegisters tables, - IReadOnlyList 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(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; - } - - /// - /// 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. - /// - private static bool TryMergeByLocationPairing( - IReadOnlyList discovered, - IReadOnlyList resources, - out IReadOnlyList 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(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, - }; - } - - /// - /// Legacy entry point — forwards to . - /// - internal static IReadOnlyList RefineVertexInputs( - CpuContext ctx, - IReadOnlyList scalarRegisters, - VertexTableRegisters tables, - IReadOnlyList discovered) => - MergeVertexInputsFromMetadata(ctx, scalarRegisters, tables, discovered); - - /// - /// 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. - /// - internal static HashSet CollectFetchPrologPcs( - Gen5ShaderProgram program, - VertexTableRegisters tables) - { - var pcs = new HashSet(); - if (tables.VertexAttribReg < 0 || tables.VertexBufferReg < 0) - { - return pcs; - } - - var tableRegs = new HashSet - { - (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 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 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; - } - - /// - /// Attrib-table format - /// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat. - /// Unknown values pass through (already BufferFormat). - /// - 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, - }; - - /// - /// Maps Prospero attrib-table formats onto GNM (DataFormat, NumberFormat, - /// Components) for ToVkVertexFormat. Accepts VertexAttribFormat - /// or BufferFormat (pass-through). NumberFormat: 0 Unorm, 1 SNorm, - /// 2 UScaled, 3 SScaled, 4 UInt, 5 SInt, 7 Float. - /// - 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 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 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 buffer = stackalloc byte[8]; - if (!ctx.Memory.TryRead(address, buffer)) - { - value = 0; - return false; - } - - value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buffer); - return true; - } -} diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index 517d1ab0..18b37515 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -15,40 +15,16 @@ public static class AudioOut2Exports // Clearing 0x80 bytes here overwrote the caller's stack canary immediately // following the 0x40-byte parameter block. private const int AudioOut2ContextParamSize = 0x40; - // Keep these modest. Some Prospero titles stack-allocate QueryMemory results - // next to the frame canary: a 16-byte {size,align} write to [rbp-0x38] plants - // 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 const int AudioOut2ContextMemorySize = 0x10000; + private const int AudioOut2ContextMemoryAlignment = 0x10000; private static long _nextContextHandle = 1; private static long _nextUserHandle = 1; private static int _nextPortId; private static long _pushTraceCount; - private static readonly ConcurrentDictionary 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 Contexts = new(); - private static readonly ConcurrentDictionary Ports = new(); private sealed class ContextState { @@ -66,6 +42,9 @@ public static class AudioOut2Exports public uint Channels { 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() { long delay; @@ -133,36 +112,19 @@ public static class AudioOut2Exports public static int AudioOut2ContextQueryMemory(CpuContext ctx) { var paramAddress = ctx[CpuRegister.Rdi]; - var memoryInfoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); + var memoryInfoAddress = ctx[CpuRegister.Rsi]; if (paramAddress == 0 || memoryInfoAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - // Heap: {size, alignment} (16 bytes), matching sceAudioPropagationSystemQueryMemory. - // 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 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 memoryInfo = stackalloc byte[0x10]; + Span memoryInfo = stackalloc byte[0x20]; memoryInfo.Clear(); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment); - TraceAudioOut2( - $"context-query-memory out=0x{memoryInfoAddress:X} " + - $"size=0x{AudioOut2ContextMemorySize:X} align=0x{AudioOut2ContextMemoryAlignment:X}"); + BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize); + BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment); + return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo) ? SetReturn(ctx, 0) : 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); } + // Read channels/frequency/grain from the reset-param blob so the + // context can pace advances to the real audio cadence. uint channels = 2; uint frequency = 48000; uint grain = 256; @@ -195,6 +159,8 @@ public static class AudioOut2Exports var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]); if (pc is > 0 and <= 8) channels = pc; 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; 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) { 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)) { + // 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(); } @@ -256,9 +223,11 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] 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); @@ -271,35 +240,38 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2ContextGetQueueLevel(CpuContext ctx) { - // ABI out is a 32-bit queue depth (callers compare dword [out]). A - // uint64 write into a stack slot at [rbp-0x14] next to the canary at - // [rbp-0x10] zeroed the canary low half and aborted the audio thread. - var outLevelAddress = ctx[CpuRegister.Rsi]; - if (outLevelAddress == 0) + // The advance path paces synchronously, so the queue is always drained. + var levelAddress = ctx[CpuRegister.Rsi]; + if (levelAddress != 0) { - outLevelAddress = ctx[CpuRegister.Rdx]; - } - - if (outLevelAddress != 0) - { - Span 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); - } + _ = TryWriteUInt64(ctx, levelAddress, 0); } return SetReturn(ctx, 0); } [SysAbiExport( - Nid = "Q8DZkKQ-SYc", - ExportName = "sceAudioOut2LoContextGetQueueLevel", + Nid = "JK2wamZPzwM", + ExportName = "sceAudioOut2PortCreate", Target = Generation.Gen5, LibraryName = "libSceAudioOut2")] - public static int AudioOut2LoContextGetQueueLevel(CpuContext ctx) => - AudioOut2ContextGetQueueLevel(ctx); + public static int AudioOut2PortCreate(CpuContext 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( Nid = "8XTArSPyWHk", @@ -308,39 +280,6 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] 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( Nid = "gatEUKG+Ea4", ExportName = "sceAudioOut2PortGetState", @@ -348,44 +287,27 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2PortGetState(CpuContext ctx) { - var portHandle = ctx[CpuRegister.Rdi]; - var stateAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); - if (stateAddress == 0) + var handle = ctx[CpuRegister.Rdi]; + var stateAddress = ctx[CpuRegister.Rsi]; + if (handle == 0 || stateAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - // Stack out-buffers with garbage handles were writing 0x20 bytes over - // caller frames / canaries (state=0x7FFFDE1FF688 right before fail). - // 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 state = stackalloc byte[PortStateSize]; + var type = (int)((handle >> 16) & 0xFF); + Span state = stackalloc byte[0x20]; state.Clear(); - // +0x00 u16 output = CONNECTED_PRIMARY (1) - // +0x02 u8 channels = 2 - // +0x04 s16 volume = -1 (N/A for main) - BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary); - state[0x02] = 2; + var output = type == 2 ? 0x40 : 0x01; + var channels = type == 2 ? 1 : 2; + BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output)); + state[0x02] = unchecked((byte)channels); BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1); - if (!ctx.Memory.TryWrite(stateAddress, state)) - { - return 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); + return ctx.Memory.TryWrite(stateAddress, state) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - // rdi=out buffer, rsi=type/flag (not a pointer). Fixed-size write only. [SysAbiExport( Nid = "DImz2Ft9E2g", ExportName = "sceAudioOut2GetSpeakerInfo", @@ -393,134 +315,21 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2GetSpeakerInfo(CpuContext ctx) { - var infoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdi], ctx[CpuRegister.Rdx]); + var infoAddress = ctx[CpuRegister.Rdi]; if (infoAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - // Same rule as PortGetState — never bulk-write speaker info onto the stack. - if (IsGuestStackAddress(infoAddress)) - { - TraceAudioOut2($"get-speaker-info skip-stack out=0x{infoAddress:X}"); - return SetReturn(ctx, 0); - } - - Span info = stackalloc byte[SpeakerInfoSize]; + Span info = stackalloc byte[0x40]; info.Clear(); - BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 2); - BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 48000); - BinaryPrimitives.WriteUInt16LittleEndian(info[0x08..], PortStateOutputConnectedPrimary); + BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1); + BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2); + BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000); - if (!ctx.Memory.TryWrite(infoAddress, info)) - { - return 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); + return ctx.Memory.TryWrite(infoAddress, info) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } [SysAbiExport( @@ -528,11 +337,7 @@ public static class AudioOut2Exports ExportName = "sceAudioOut2PortDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioOut2")] - public static int AudioOut2PortDestroy(CpuContext ctx) - { - Ports.TryRemove(ctx[CpuRegister.Rdi], out _); - return SetReturn(ctx, 0); - } + public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0); [SysAbiExport( Nid = "IaZXJ9M79uo", @@ -562,129 +367,6 @@ public static class AudioOut2Exports : 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 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 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) { Span buffer = stackalloc byte[sizeof(ulong)]; diff --git a/src/SharpEmu.Libs/Audio/AudioOutExports.cs b/src/SharpEmu.Libs/Audio/AudioOutExports.cs index db9fc70a..ceee3fb0 100644 --- a/src/SharpEmu.Libs/Audio/AudioOutExports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOutExports.cs @@ -175,14 +175,6 @@ public static class AudioOutExports 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 // so pacing/mixing code sees a live port. We do no host rerouting, so // rerouteCounter and flag stay zero. @@ -200,9 +192,6 @@ public static class AudioOutExports return ctx.SetReturn(0); } - private static bool IsGuestStackAddress(ulong value) => - value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL; - [SysAbiExport( Nid = "QOQtbeDqsT4", ExportName = "sceAudioOutOutput", diff --git a/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs index 0cc2a697..6294402f 100644 --- a/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs +++ b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs @@ -68,7 +68,7 @@ internal static class AudioPcmConversion value = Math.Clamp(value, -1.0f, 1.0f); var scale = value < 0.0f ? 32768.0f : short.MaxValue; - return unchecked((short)MathF.Round(value * scale)); + return checked((short)MathF.Round(value * scale)); } // is expected pre-clamped to [0, 1] by the caller. diff --git a/src/SharpEmu.Libs/CxxAbiExports.cs b/src/SharpEmu.Libs/CxxAbiExports.cs index 805c8c96..1d174dc0 100644 --- a/src/SharpEmu.Libs/CxxAbiExports.cs +++ b/src/SharpEmu.Libs/CxxAbiExports.cs @@ -21,11 +21,6 @@ public static class CxaGuardExports } private static readonly ConcurrentDictionary _inProgress = new(); - private static readonly ConcurrentDictionary _onceGates = new(); - - private const int OnceUninitialized = 0; - private const int OnceInProgress = 1; - private const int OnceComplete = 2; [SysAbiExport( Nid = "3GPpjQdAMTw", @@ -176,167 +171,6 @@ public static class CxaGuardExports 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) { word = 0; diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs index ab18d9a0..91a2ea3f 100644 --- a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs +++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs @@ -76,8 +76,7 @@ internal sealed record GuestVertexBuffer( uint OffsetBytes, byte[] Data, int Length, - bool Pooled, - bool PerInstance = false); + bool Pooled); internal sealed record GuestIndexBuffer( byte[] Data, diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs index ffcef9d8..86740ef0 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs @@ -1225,11 +1225,8 @@ internal static partial class MetalVideoPresenter ? vertexBuffer.Stride : Math.Max(vertexBuffer.ComponentCount, 1) * 4; MetalNative.Send(layout, MetalNative.Selector("setStride:"), (nint)stride); - // MTLVertexStepFunction: PerVertex = 1, PerInstance = 2. - MetalNative.Send( - layout, - MetalNative.Selector("setStepFunction:"), - vertexBuffer.PerInstance ? 2 : 1); + // MTLVertexStepFunction.PerVertex = 1. + MetalNative.Send(layout, MetalNative.Selector("setStepFunction:"), 1); } return descriptor; diff --git a/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs b/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs index 2aeb7f51..bbd44c30 100644 --- a/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelFileExtendedExports.cs @@ -46,13 +46,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixPread(CpuContext ctx) - { - var result = KernelPreadCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx); [SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -103,13 +97,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixPwrite(CpuContext ctx) - { - var result = KernelPwriteCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx); [SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -161,13 +149,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFsync(CpuContext ctx) - { - var result = KernelFsyncCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx); [SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -175,13 +157,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFdatasync(CpuContext ctx) - { - var result = KernelFsyncCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx); [SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -234,13 +210,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFtruncate(CpuContext ctx) - { - var result = KernelFtruncateCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx); [SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -276,13 +246,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixTruncate(CpuContext ctx) - { - var result = KernelTruncateCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result); - } + public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx); [SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -330,13 +294,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixRename(CpuContext ctx) - { - var result = KernelRenameCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result); - } + public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx); [SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] @@ -405,10 +363,7 @@ public static partial class KernelMemoryCompatExports { if (!_openFiles.TryGetValue(fd, out var stream)) { - return PosixFailure( - ctx, - (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, - notFoundErrno: Ebadf); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } // 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)) { - return PosixFailure( - ctx, - (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, - notFoundErrno: Ebadf); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } if (oldFd == newFd) @@ -460,13 +412,7 @@ public static partial class KernelMemoryCompatExports [SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] - public static int PosixFcntl(CpuContext ctx) - { - var result = KernelFcntlCore(ctx); - return result == (int)OrbisGen2Result.ORBIS_GEN2_OK - ? 0 - : PosixFailure(ctx, result, notFoundErrno: Ebadf); - } + public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx); [SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")] diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 983ec441..2b1461d6 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -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}"); 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; } } @@ -1773,113 +1773,6 @@ public static partial class KernelMemoryCompatExports 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. // Games that stream via AMPR APR call this to turn asset paths into file // IDs, then hand those IDs to sceAmprAprCommandBufferReadFile. Without it @@ -2335,7 +2228,8 @@ public static partial class KernelMemoryCompatExports 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); @@ -5855,7 +5749,7 @@ public static partial class KernelMemoryCompatExports return true; } - if (!TryReadTrackedLibcHeap(address, destination) && !TryReadHostMemory(address, destination)) + if (!TryReadHostMemory(address, destination)) { return false; } @@ -5921,7 +5815,7 @@ public static partial class KernelMemoryCompatExports return true; } - if (!TryWriteTrackedLibcHeap(address, source) && !TryWriteHostMemory(address, source)) + if (!TryWriteHostMemory(address, source)) { return false; } @@ -6663,7 +6557,7 @@ public static partial class KernelMemoryCompatExports } } - internal static unsafe bool TryReadTrackedLibcHeap( + internal static bool TryReadTrackedLibcHeap( ulong address, Span destination) { @@ -6687,54 +6581,7 @@ public static partial class KernelMemoryCompatExports continue; } - try - { - // Marshal.AllocHGlobal allocations are not present in the - // emulator's POSIX HostMemory map, so TryReadHostMemory - // would reject this already-bounds-checked range. - new ReadOnlySpan((void*)address, destination.Length).CopyTo(destination); - return true; - } - catch - { - return false; - } - } - } - - return false; - } - - private static unsafe bool TryWriteTrackedLibcHeap(ulong address, ReadOnlySpan 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((void*)address, source.Length)); - return true; - } - catch - { - return false; - } + return TryReadHostMemory(address, destination); } } @@ -7335,41 +7182,28 @@ public static partial class KernelMemoryCompatExports { 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; } OpenDirectory? directory; - bool isOpenFile; lock (_fdGate) { _openDirectories.TryGetValue(fd, out directory); - isOpenFile = directory is null && _openFiles.ContainsKey(fd); } if (directory is null) { - // A regular file fd used with getdents must not look like EOF (rax=0); - // 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; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } var currentIndex = directory.NextIndex; 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; } if (currentIndex >= directory.Entries.Length) { - LogIoTrace("getdents", directory.Path, $"fd={fd} result=eof entries={directory.Entries.Length}"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -7400,16 +7234,11 @@ public static partial class KernelMemoryCompatExports private static string[] EnumerateDirectoryEntries(string hostPath) { - // Real getdents always yields "." / ".." before other names. An empty - // 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) + return Directory.EnumerateFileSystemEntries(hostPath) .Select(Path.GetFileName) .Where(static name => !string.IsNullOrEmpty(name)) - .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase); - - return new[] { ".", ".." }.Concat(children).ToArray()!; + .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase) + .ToArray()!; } private static uint ComputeDirectoryEntryHash(ReadOnlySpan utf8Name) diff --git a/src/SharpEmu.Libs/LibcStdioExports.cs b/src/SharpEmu.Libs/LibcStdioExports.cs index 15d4421f..b9a17af4 100644 --- a/src/SharpEmu.Libs/LibcStdioExports.cs +++ b/src/SharpEmu.Libs/LibcStdioExports.cs @@ -45,11 +45,7 @@ public static class LibcStdioExports private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t' 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 _ctypeLowerTableBase; - private static nint _ctypeUpperTableBase; [SysAbiExport( Nid = "xeYO4u7uyJ0", @@ -720,28 +716,6 @@ public static class LibcStdioExports 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() { 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((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((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) { var isUpper = c is >= 'A' and <= 'Z'; diff --git a/src/SharpEmu.Libs/MessengerCompatExports.cs b/src/SharpEmu.Libs/MessengerCompatExports.cs deleted file mode 100644 index cbdeaa04..00000000 --- a/src/SharpEmu.Libs/MessengerCompatExports.cs +++ /dev/null @@ -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; - -/// Small Unity/libc compatibility shims exercised by The Messenger. -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 diff --git a/src/SharpEmu.Libs/Remoteplay/RemoteplayExports.cs b/src/SharpEmu.Libs/Remoteplay/RemoteplayExports.cs deleted file mode 100644 index e6b9aecf..00000000 --- a/src/SharpEmu.Libs/Remoteplay/RemoteplayExports.cs +++ /dev/null @@ -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 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; - } -} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs b/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs index d71d10c3..b6d2404f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanHostBufferPool.cs @@ -18,7 +18,6 @@ internal readonly record struct VulkanHostBufferAllocation( internal sealed class VulkanHostBufferPool : IDisposable { - private readonly object _gate = new(); private readonly Dictionary> _available = []; private readonly Dictionary _allocations = []; @@ -41,19 +40,16 @@ internal sealed class VulkanHostBufferPool : IDisposable VulkanHostBufferPoolKey key, out VulkanHostBufferAllocation allocation) { - lock (_gate) + if (!_available.TryGetValue(key, out var available) || + !available.TryPop(out allocation)) { - if (!_available.TryGetValue(key, out var available) || - !available.TryPop(out allocation)) - { - allocation = default; - return false; - } - - _cachedHandles.Remove(allocation.Buffer.Handle); - CachedBytes -= allocation.Key.Capacity; - return true; + allocation = default; + return false; } + + _cachedHandles.Remove(allocation.Buffer.Handle); + CachedBytes -= allocation.Key.Capacity; + return true; } public void Register(VulkanHostBufferAllocation allocation) @@ -63,79 +59,51 @@ internal sealed class VulkanHostBufferPool : IDisposable 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) { - VulkanHostBufferAllocation? toDestroy = null; - lock (_gate) + if (!_allocations.TryGetValue(buffer.Handle, out var allocation) || + allocation.Memory.Handle != memory.Handle) { - if (!_allocations.TryGetValue(buffer.Handle, out var allocation) || - allocation.Memory.Handle != memory.Handle) - { - return false; - } - - if (!_cachedHandles.Add(buffer.Handle)) - { - return true; - } - - if (allocation.Key.Capacity > MaximumCachedBytes - CachedBytes) - { - _cachedHandles.Remove(buffer.Handle); - _allocations.Remove(buffer.Handle); - toDestroy = allocation; - } - else - { - if (!_available.TryGetValue(allocation.Key, out var available)) - { - available = []; - _available.Add(allocation.Key, available); - } - - available.Push(allocation); - CachedBytes += allocation.Key.Capacity; - } + return false; } - // 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); - + if (!_cachedHandles.Add(buffer.Handle)) + { + return true; } + if (allocation.Key.Capacity > MaximumCachedBytes - CachedBytes) + { + _cachedHandles.Remove(buffer.Handle); + _allocations.Remove(buffer.Handle); + _destroy(allocation); + return true; + } + + if (!_available.TryGetValue(allocation.Key, out var available)) + { + available = []; + _available.Add(allocation.Key, available); + } + + available.Push(allocation); + CachedBytes += allocation.Key.Capacity; return true; } public void Dispose() { - // Snapshot under the lock, destroy outside — _destroy calls into - // 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 toDestroy; - lock (_gate) - { - toDestroy = new List(_allocations.Values); - _allocations.Clear(); - _available.Clear(); - _cachedHandles.Clear(); - CachedBytes = 0; - } - - foreach (var allocation in toDestroy) + foreach (var allocation in _allocations.Values) { _destroy(allocation); } + + _allocations.Clear(); + _available.Clear(); + _cachedHandles.Clear(); + CachedBytes = 0; } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index f1f50bc4..e2ecd73e 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -3170,7 +3170,6 @@ internal static unsafe class VulkanVideoPresenter public uint NumberFormat; public uint Stride; public uint OffsetBytes; - public bool PerInstance; } private const Format DepthFormat = Format.D32Sfloat; @@ -6659,47 +6658,33 @@ internal static unsafe class VulkanVideoPresenter PName = entryPoint, }; - // One Vulkan binding per unique host buffer and input rate - // (fetch_index). Attributes share that binding with - // Offset = OffsetBytes. - var bindingByBuffer = new Dictionary<(ulong Handle, bool PerInstance), uint>(); - var vertexBindingList = new List(); + var vertexBindingDescriptions = + new VertexInputBindingDescription[resources.VertexBuffers.Length]; var vertexAttributeDescriptions = new VertexInputAttributeDescription[resources.VertexBuffers.Length]; for (var index = 0; index < resources.VertexBuffers.Length; index++) { var vertexBuffer = resources.VertexBuffers[index]; - var bufferKey = (vertexBuffer.Buffer.Handle, vertexBuffer.PerInstance); - if (!bindingByBuffer.TryGetValue(bufferKey, out var bindingIndex)) + vertexBindingDescriptions[index] = new VertexInputBindingDescription { - bindingIndex = (uint)vertexBindingList.Count; - bindingByBuffer[bufferKey] = bindingIndex; - vertexBindingList.Add(new VertexInputBindingDescription - { - Binding = bindingIndex, - Stride = vertexBuffer.Stride == 0 - ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) - : vertexBuffer.Stride, - InputRate = vertexBuffer.PerInstance - ? VertexInputRate.Instance - : VertexInputRate.Vertex, - }); - } - + Binding = (uint)index, + Stride = vertexBuffer.Stride == 0 + ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) + : vertexBuffer.Stride, + InputRate = VertexInputRate.Vertex, + }; vertexAttributeDescriptions[index] = new VertexInputAttributeDescription { Location = vertexBuffer.Location, - Binding = bindingIndex, + Binding = (uint)index, Format = ToVkVertexFormat( vertexBuffer.DataFormat, vertexBuffer.NumberFormat, vertexBuffer.ComponentCount), - Offset = vertexBuffer.OffsetBytes, + Offset = 0, }; } - var vertexBindingDescriptions = vertexBindingList.ToArray(); - fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions) fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions) { @@ -9295,7 +9280,6 @@ internal static unsafe class VulkanVideoPresenter NumberFormat = guestBuffer.NumberFormat, Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, - PerInstance = guestBuffer.PerInstance, }; } @@ -9313,7 +9297,6 @@ internal static unsafe class VulkanVideoPresenter NumberFormat = guestBuffer.NumberFormat, Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, - PerInstance = guestBuffer.PerInstance, }; private VkBuffer CreateHostBuffer( @@ -9415,28 +9398,21 @@ internal static unsafe class VulkanVideoPresenter private static Format ToVkVertexFormat( uint dataFormat, uint numberFormat, - uint componentCount) - { - var format = (dataFormat, numberFormat) switch + uint componentCount) => + (dataFormat, numberFormat) switch { (1, 0) => Format.R8Unorm, (1, 1) => Format.R8SNorm, - (1, 2) => Format.R8Uscaled, - (1, 3) => Format.R8Sscaled, (1, 4) => Format.R8Uint, (1, 5) => Format.R8Sint, (1, 9) => Format.R8Srgb, (2, 0) => Format.R16Unorm, (2, 1) => Format.R16SNorm, - (2, 2) => Format.R16Uscaled, - (2, 3) => Format.R16Sscaled, (2, 4) => Format.R16Uint, (2, 5) => Format.R16Sint, (2, 7) => Format.R16Sfloat, (3, 0) => Format.R8G8Unorm, (3, 1) => Format.R8G8SNorm, - (3, 2) => Format.R8G8Uscaled, - (3, 3) => Format.R8G8Sscaled, (3, 4) => Format.R8G8Uint, (3, 5) => Format.R8G8Sint, (3, 9) => Format.R8G8Srgb, @@ -9491,9 +9467,6 @@ internal static unsafe class VulkanVideoPresenter (14, 4) => Format.R32G32B32A32Uint, (14, 5) => Format.R32G32B32A32Sint, (14, 7) => Format.R32G32B32A32Sfloat, - // Prospero VertexAttribFormat quirks also seen as buffer formats. - (113, _) => Format.R32G32B32A32Sfloat, - (121, _) => Format.R16G16Sfloat, (16, 0) => Format.B5G6R5UnormPack16, (17, 0) => Format.R5G5B5A1UnormPack16, (19, 0) => Format.R4G4B4A4UnormPack16, @@ -9501,38 +9474,6 @@ internal static unsafe class VulkanVideoPresenter _ => ToVkFloatVertexFormat(componentCount), }; - return NarrowVkVertexFormat(format, componentCount); - } - - /// - /// Narrow a sharp's full VkFormat to the component count the VS fetch - /// actually consumes. - /// - 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) => componentCount switch { diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index 19c01505..d00a6e89 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -312,8 +312,7 @@ public sealed record Gen5VertexInputBinding( uint OffsetBytes, byte[] Data, int DataLength, - bool DataPooled, - bool PerInstance = false); + bool DataPooled); public sealed record Gen5ShaderEvaluation( IReadOnlyList InitialScalarRegisters, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 6c5e9a37..369ecf1b 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -883,11 +883,8 @@ public static class Gen5ShaderScalarEvaluator Gen5ShaderInstruction instruction, Gen5BufferMemoryControl control, 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.OffsetEnabled && control.DwordCount is >= 1 and <= 4 && descriptor.BaseAddress != 0 && descriptor.Stride != 0 && diff --git a/tests/SharpEmu.Debugger.Tests/BreakpointStoreTests.cs b/tests/SharpEmu.Debugger.Tests/BreakpointStoreTests.cs deleted file mode 100644 index ed031f88..00000000 --- a/tests/SharpEmu.Debugger.Tests/BreakpointStoreTests.cs +++ /dev/null @@ -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)); - } -} diff --git a/tests/SharpEmu.Debugger.Tests/DebugCommandDispatcherTests.cs b/tests/SharpEmu.Debugger.Tests/DebugCommandDispatcherTests.cs deleted file mode 100644 index d3fc693b..00000000 --- a/tests/SharpEmu.Debugger.Tests/DebugCommandDispatcherTests.cs +++ /dev/null @@ -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(breakpoint["kind"])); - Assert.Equal(1UL, Convert.ToUInt64(breakpoint["length"])); - Assert.True(Assert.IsType(breakpoint["enabled"])); - - var listed = dispatcher.Dispatch(Parse("""{"command":"list-breakpoints"}""")); - Assert.True(listed.Ok); - var list = Assert.IsAssignableFrom(listed.Data!["breakpoints"]); - Assert.Single(list.Cast()); - - 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 data, string key) - { - Assert.True(data.TryGetValue(key, out var value)); - Assert.NotNull(value); - return Assert.IsType(value); - } - - private static IReadOnlyDictionary GetDict( - IReadOnlyDictionary data, - string key) - { - Assert.True(data.TryGetValue(key, out var value)); - Assert.NotNull(value); - return Assert.IsAssignableFrom>(value); - } -} diff --git a/tests/SharpEmu.Debugger.Tests/DebugRequestTests.cs b/tests/SharpEmu.Debugger.Tests/DebugRequestTests.cs deleted file mode 100644 index 950fb74b..00000000 --- a/tests/SharpEmu.Debugger.Tests/DebugRequestTests.cs +++ /dev/null @@ -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 _)); - } -} diff --git a/tests/SharpEmu.Debugger.Tests/FakeDebuggerSession.cs b/tests/SharpEmu.Debugger.Tests/FakeDebuggerSession.cs deleted file mode 100644 index 8729ba2f..00000000 --- a/tests/SharpEmu.Debugger.Tests/FakeDebuggerSession.cs +++ /dev/null @@ -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; - -/// -/// In-memory session for dispatcher tests. Tracks run state and an optional -/// flat guest memory region without parking a real emulation thread. -/// -internal sealed class FakeDebuggerSession : IDebuggerSession -{ - private readonly Dictionary _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? 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 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 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 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) - { - } - } -} diff --git a/tests/SharpEmu.Debugger.Tests/SharpEmu.Debugger.Tests.csproj b/tests/SharpEmu.Debugger.Tests/SharpEmu.Debugger.Tests.csproj deleted file mode 100644 index b5a6be76..00000000 --- a/tests/SharpEmu.Debugger.Tests/SharpEmu.Debugger.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - false - false - - - - - - - - - - - - diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs deleted file mode 100644 index 32d1703e..00000000 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs +++ /dev/null @@ -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; - -/// -/// Coverage for AGC attrib-table → BufferFormat merge and semantic indexing. -/// -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 bytes = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); - Assert.True(memory.TryWrite(address, bytes)); - } -} diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs index c303c8d1..cf587f8d 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs @@ -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] public void ResolveFilepathsToIdsAndFileSizes_MissingFile_FailsFastWithErrorIndex() { diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs deleted file mode 100644 index a09b0658..00000000 --- a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs +++ /dev/null @@ -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 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 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 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 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]); - } -} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs deleted file mode 100644 index 8ada9051..00000000 --- a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs +++ /dev/null @@ -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 bytes = stackalloc byte[8]; - BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); - Assert.True(memory.TryWrite(address, bytes)); - } - - private static ulong ReadU64(FakeCpuMemory memory, ulong address) - { - Span bytes = stackalloc byte[8]; - Assert.True(memory.TryRead(address, bytes)); - return BinaryPrimitives.ReadUInt64LittleEndian(bytes); - } - - private static uint ReadU32(FakeCpuMemory memory, ulong address) - { - Span 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); - } -} diff --git a/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs index c016a46d..defe7f78 100644 --- a/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs +++ b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs @@ -7,18 +7,15 @@ namespace SharpEmu.Libs.Tests; // 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. -internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator +internal sealed class FakeCpuMemory : ICpuMemory { private readonly ulong _base; private readonly byte[] _storage; - private ulong _allocBump; public FakeCpuMemory(ulong baseAddress, int size) { _base = baseAddress; _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 destination) @@ -43,33 +40,6 @@ internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator 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) { var bytes = System.Text.Encoding.UTF8.GetBytes(text); diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs index a6ba24e5..f1662443 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs @@ -125,125 +125,6 @@ public sealed class KernelMemoryCompatExportsTests 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 bytes = stackalloc byte[sizeof(int)]; - Assert.True(memory.TryRead(fsBase + TlsErrnoOffset, bytes)); - return BitConverter.ToInt32(bytes); - } - [Fact] public void Sprintf_ReadsVariadicDoubleFromXmmRegister() { diff --git a/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs index 9a7d92be..ad564297 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs @@ -24,53 +24,13 @@ public sealed unsafe class GuestImageWriteTrackerTests // spilling onto neighbouring heap pages. private const nuint TrackedByteCount = 4096; 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) { - // 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); return (ulong)allocation; } - private static void FreeTrackedPages(void* allocation) - { - if (OperatingSystem.IsWindows()) - { - _ = VirtualFree((nint)allocation, 0, MemRelease); - return; - } - - NativeMemory.Free(allocation); - } - [Fact] public void GenerationSurvivesDirtyConsume() { @@ -98,7 +58,7 @@ public sealed unsafe class GuestImageWriteTrackerTests finally { GuestImageWriteTracker.Untrack(address); - FreeTrackedPages(allocation); + NativeMemory.Free(allocation); } } @@ -129,7 +89,7 @@ public sealed unsafe class GuestImageWriteTrackerTests finally { GuestImageWriteTracker.Untrack(address); - FreeTrackedPages(allocation); + NativeMemory.Free(allocation); } } @@ -158,7 +118,7 @@ public sealed unsafe class GuestImageWriteTrackerTests finally { GuestImageWriteTracker.Untrack(address); - FreeTrackedPages(allocation); + NativeMemory.Free(allocation); } } diff --git a/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs deleted file mode 100644 index 0ca9f498..00000000 --- a/tests/SharpEmu.Libs.Tests/MessengerCompatExportsTests.cs +++ /dev/null @@ -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 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); - } - } -} diff --git a/tests/SharpEmu.Libs.Tests/Remoteplay/RemoteplayExportsTests.cs b/tests/SharpEmu.Libs.Tests/Remoteplay/RemoteplayExportsTests.cs deleted file mode 100644 index 9c0b6dbf..00000000 --- a/tests/SharpEmu.Libs.Tests/Remoteplay/RemoteplayExportsTests.cs +++ /dev/null @@ -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); - } -}