From d43edc865ad1dfd75cc108de3fe1beb453f5206b Mon Sep 17 00:00:00 2001 From: Spooks <62370103+Spooks4576@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:41:42 -0600 Subject: [PATCH] Agent/fix gen5 thread agc compat (#130) * Fix Gen5 thread and AGC compatibility * Trim compatibility comments * Report selected Vulkan GPU * Clean up CPU title label * Improve emulator frame pacing and performance * Regenerate package locks with pinned SDK --------- Co-authored-by: Spooks4576 --- src/SharpEmu.CLI/SharpEmu.CLI.csproj | 3 + .../Native/DirectExecutionBackend.Imports.cs | 35 +- .../Cpu/Native/DirectExecutionBackend.cs | 66 +++- src/SharpEmu.GUI/MainWindow.axaml.cs | 4 +- src/SharpEmu.Libs/Agc/AgcExports.cs | 302 +++++++++++++++--- src/SharpEmu.Libs/Ajm/AjmExports.cs | 37 +++ src/SharpEmu.Libs/Audio/AudioOut2Exports.cs | 2 +- src/SharpEmu.Libs/HostTimerResolution.cs | 46 +++ src/SharpEmu.Libs/Json/JsonExports.cs | 52 +++ .../Kernel/KernelAprCompatExports.cs | 31 +- .../Kernel/KernelEventQueueCompatExports.cs | 89 +++++- .../Kernel/KernelPthreadCompatExports.cs | 220 +++++++++++-- .../Kernel/KernelRuntimeCompatExports.cs | 7 + src/SharpEmu.Libs/Np/NpManagerExports.cs | 30 +- src/SharpEmu.Libs/Np/NpWebApi2Exports.cs | 12 + src/SharpEmu.Libs/Pad/PadExports.cs | 77 +++-- src/SharpEmu.Libs/PlayGo/PlayGoExports.cs | 4 +- src/SharpEmu.Libs/SaveData/SaveDataExports.cs | 16 +- .../UserService/UserServiceExports.cs | 28 +- src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 155 ++++++--- .../VideoOut/VulkanVideoPresenter.cs | 255 +++++++++++++-- src/SharpEmu.Libs/Voice/VoiceQoSExports.cs | 19 ++ src/SharpEmu.Logging/FileLogSink.cs | 42 ++- src/SharpEmu.Logging/HostSystemInfo.cs | 15 +- 24 files changed, 1318 insertions(+), 229 deletions(-) create mode 100644 src/SharpEmu.Libs/Ajm/AjmExports.cs create mode 100644 src/SharpEmu.Libs/HostTimerResolution.cs create mode 100644 src/SharpEmu.Libs/Voice/VoiceQoSExports.cs diff --git a/src/SharpEmu.CLI/SharpEmu.CLI.csproj b/src/SharpEmu.CLI/SharpEmu.CLI.csproj index 8cd33a5..3621327 100644 --- a/src/SharpEmu.CLI/SharpEmu.CLI.csproj +++ b/src/SharpEmu.CLI/SharpEmu.CLI.csproj @@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later enable true 0.0.1 + true + true + true diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 0ab8d6d..a70a686 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -161,7 +161,7 @@ public sealed partial class DirectExecutionBackend *(ulong*)(xmmSlot + 8)); } cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; - if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal)) + if (importStubEntry.Kind == ImportStubKind.BootstrapBridge) { NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _); *(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi]; @@ -245,12 +245,13 @@ public sealed partial class DirectExecutionBackend bool flag4 = !string.IsNullOrWhiteSpace(_importFilter); bool flag5 = false; ExportedFunction? matchedExport = importStubEntry.Export; + var traceFlags = importStubEntry.TraceFlags; bool periodicTrace = num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || - (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || - (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || + ((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) || + ((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3; @@ -311,15 +312,15 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.Rsi], cpuContext[CpuRegister.Rdx]); } - if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256) + if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256) { Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}"); } - if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64) + if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64) { Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}"); } - if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg") + if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0) { Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}"); } @@ -349,7 +350,7 @@ public sealed partial class DirectExecutionBackend Console.Error.Flush(); } } - if (importStubEntry.Nid == "Ou3iL1abvng") + if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0) { if (_logStackCheck) { @@ -381,7 +382,7 @@ public sealed partial class DirectExecutionBackend ActiveGuestReturnSlotAddress); try { - if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal)) + if (importStubEntry.Kind == ImportStubKind.BootstrapBridge) { if (_logBootstrap) { @@ -390,12 +391,11 @@ public sealed partial class DirectExecutionBackend orbisGen2Result = DispatchBootstrapBridge(); } - else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) || - string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal)) + else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym) { orbisGen2Result = DispatchKernelDynlibDlsym(); } - else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal)) + else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol) { orbisGen2Result = DispatchIl2CppApiLookupSymbol(); } @@ -818,6 +818,7 @@ public sealed partial class DirectExecutionBackend "2Z+PpY6CaJg" or // pthread_mutex_unlock "EgmLo6EWgso" or // pthread_rwlock_unlock "+L98PIbGttk" or // scePthreadRwlockUnlock + "q1cHNfGycLI" or // scePadRead "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "zgXifHT9ErY" or // sceVideoOutIsFlipPending "V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress @@ -1076,9 +1077,15 @@ public sealed partial class DirectExecutionBackend } private static bool IsImportLoopGuardBoundary(string nid) => - string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal) || - string.Equals(nid, "Zxa0VhQVTsk", StringComparison.Ordinal) || - string.Equals(nid, "T72hz6ffq08", StringComparison.Ordinal); + nid switch + { + "1jfXLRVzisc" => true, // sceKernelUsleep + "QcteRwbsnV0" => true, // usleep + "n88vx3C5nW8" => true, // gettimeofday + "Zxa0VhQVIsk" => true, + "T72hz6ffq08" => true, // scePthreadYield + _ => false + }; private void ResetImportLoopPattern() { diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index f4ad3f6..65f997c 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -25,6 +25,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private const int DefaultImportLoopGuardSeconds = 5; + private enum ImportStubKind : byte + { + Normal = 0, + BootstrapBridge = 1, + KernelDynlibDlsym = 2, + Il2CppApiLookupSymbol = 3, + } + + [Flags] + private enum ImportStubTraceFlags : byte + { + None = 0, + Memset = 1 << 0, + CxaAtexit = 1 << 1, + RawArgs = 1 << 2, + StackChkFail = 1 << 3, + PeriodicEvery1000 = 1 << 4, + PeriodicEvery128 = 1 << 5, + } + private readonly struct ImportStubEntry { public ulong Address { get; } @@ -33,12 +53,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public ExportedFunction? Export { get; } + public ImportStubKind Kind { get; } + + public ImportStubTraceFlags TraceFlags { get; } + public ImportStubEntry(ulong address, string nid, ExportedFunction? export) { Address = address; Nid = nid; Export = export; + Kind = ClassifyKind(nid); + TraceFlags = ClassifyTraceFlags(nid); } + + private static ImportStubKind ClassifyKind(string nid) => nid switch + { + RuntimeStubNids.BootstrapBridge => ImportStubKind.BootstrapBridge, + RuntimeStubNids.KernelDynlibDlsym or "LwG8g3niqwA" => ImportStubKind.KernelDynlibDlsym, + "r8mvOaWdi28" => ImportStubKind.Il2CppApiLookupSymbol, + _ => ImportStubKind.Normal, + }; + + private static ImportStubTraceFlags ClassifyTraceFlags(string nid) => nid switch + { + "8zTFvBIAIN8" => ImportStubTraceFlags.Memset, + "tsvEmnenz48" => ImportStubTraceFlags.CxaAtexit | ImportStubTraceFlags.PeriodicEvery1000, + "bzQExy189ZI" or "8G2LB+A3rzg" => ImportStubTraceFlags.RawArgs, + "Ou3iL1abvng" => ImportStubTraceFlags.StackChkFail, + "rTXw65xmLIA" => ImportStubTraceFlags.PeriodicEvery128, + _ => ImportStubTraceFlags.None, + }; } private readonly record struct RecentImportTraceEntry( @@ -125,6 +169,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private const ulong GuestThreadRegionStride = 0x0100_0000UL; + private const int GuestThreadRegionSlotCount = 256; + private const uint PAGE_EXECUTE_READWRITE = 64u; private const uint PAGE_READWRITE = 4u; @@ -2757,7 +2803,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I if (hostThread is not null && !ReferenceEquals(hostThread, Thread.CurrentThread)) { - hostThread.Join(1); + // The handle is published before the host thread starts. + if ((hostThread.ThreadState & System.Threading.ThreadState.Unstarted) != 0) + { + Thread.Sleep(1); + continue; + } + + try + { + hostThread.Join(1); + } + catch (ThreadStateException) + { + Thread.Sleep(1); + } } else { @@ -3446,7 +3506,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I out ulong mappedBase, out string? error) { - for (int i = 0; i < 64; i++) + for (int i = 0; i < GuestThreadRegionSlotCount; i++) { var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride); if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size)) @@ -3480,7 +3540,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I out ulong tlsBase, out string? error) { - for (int i = 0; i < 64; i++) + for (int i = 0; i < GuestThreadRegionSlotCount; i++) { var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride); var mappedBase = candidateBase - GuestThreadTlsPrefixSize; diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs index 392c348..7794f87 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml.cs +++ b/src/SharpEmu.GUI/MainWindow.axaml.cs @@ -25,6 +25,7 @@ namespace SharpEmu.GUI; public partial class MainWindow : Window { private const int MaxConsoleLines = 4000; + private const int MaxConsoleLinesPerFlush = 500; private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE")); private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488")); @@ -1504,7 +1505,8 @@ public partial class MainWindow : Window } var incoming = new List(); - while (_pendingLines.TryDequeue(out var pending)) + while (incoming.Count < MaxConsoleLinesPerFlush && + _pendingLines.TryDequeue(out var pending)) { WriteFileLog(pending.Line); incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line))); diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 420a274..0715071 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -52,6 +52,8 @@ public static class AgcExports private const uint RFlip = 0x17; private const uint RReleaseMem = 0x18; private const uint RDmaData = 0x19; + private const uint RPredication = 0x1A; + private const uint RJump = 0x1B; private const uint SpiShaderPgmLoPs = 0x8; private const uint SpiShaderPgmHiPs = 0x9; private const uint SpiShaderPgmLoEs = 0xC8; @@ -389,6 +391,7 @@ public static class AgcExports public Dictionary ComputeQueues { get; } = new(); public Dictionary ComputeImageWriters { get; } = new(); public ulong WorkSequence { get; set; } + public ulong FlipSequence { get; set; } } private readonly record struct RegisterDefaultValue(uint Offset, uint Value); @@ -1923,6 +1926,90 @@ public static class AgcExports return ReturnPointer(ctx, commandAddress); } + [SysAbiExport( + Nid = "bbFueFP+J4k", + ExportName = "sceAgcDcbSetPredication", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetPredication(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var enable = (uint)ctx[CpuRegister.Rsi]; + var predicateType = (uint)ctx[CpuRegister.Rdx]; + var continuePredicate = (uint)ctx[CpuRegister.Rcx]; + var predicateAddress = ctx[CpuRegister.R8]; + if (commandBufferAddress == 0 || enable > 1 || continuePredicate > 1) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || + !ctx.TryWriteUInt32(commandAddress, Pm4(5, ItNop, RPredication)) || + !ctx.TryWriteUInt32(commandAddress + 4, enable) || + !ctx.TryWriteUInt32(commandAddress + 8, predicateType) || + !ctx.TryWriteUInt32(commandAddress + 12, continuePredicate) || + !ctx.TryWriteUInt32(commandAddress + 16, unchecked((uint)predicateAddress))) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc( + $"agc.dcb_set_predication buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + + $"enable={enable} type={predicateType} continue={continuePredicate} addr=0x{predicateAddress:X16}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "xSAR0LTcRKM", + ExportName = "sceAgcDcbJump", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbJump(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var targetAddress = ctx[CpuRegister.Rsi]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) || + !ctx.TryWriteUInt32(commandAddress, Pm4(3, ItNop, RJump)) || + !ctx.TryWriteUInt32(commandAddress + 4, unchecked((uint)targetAddress)) || + !ctx.TryWriteUInt32(commandAddress + 8, unchecked((uint)(targetAddress >> 32)))) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc( + $"agc.dcb_jump buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + + $"target=0x{targetAddress:X16}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "w6Dj1VJt5qY", + ExportName = "sceAgcSetPacketPredication", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int SetPacketPredication(CpuContext ctx) + { + var packetAddress = ctx[CpuRegister.Rdi]; + var enabled = ctx[CpuRegister.Rsi] != 0; + if (packetAddress == 0 || !ctx.TryReadUInt32(packetAddress, out var packetHeader)) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + packetHeader = enabled ? packetHeader | 1u : packetHeader & ~1u; + if (!ctx.TryWriteUInt32(packetAddress, packetHeader)) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); + } + [SysAbiExport( Nid = "w2rJhmD+dsE", ExportName = "sceAgcDriverAddEqEvent", @@ -1975,9 +2062,22 @@ public static class AgcExports public static int DriverSubmitDcb(CpuContext ctx) { var packetAddress = ctx[CpuRegister.Rdi]; - if (packetAddress == 0 || - !ctx.TryReadUInt64(packetAddress, out var commandAddress) || - !ctx.TryReadUInt32(packetAddress + 8, out var dwordCount)) + // Gen5 supports direct RSI/RDX and packed RDI submissions. + var commandAddress = ctx[CpuRegister.Rsi]; + var directDwordCount = ctx[CpuRegister.Rdx]; + uint dwordCount; + var directArguments = commandAddress != 0 && + directDwordCount is > 0 and <= 1_000_000UL; + if (directArguments) + { + dwordCount = (uint)directDwordCount; + } + else if (packetAddress == 0 || + !ctx.TryReadUInt64(packetAddress, out commandAddress) || + !ctx.TryReadUInt32(packetAddress + 8, out dwordCount) || + commandAddress == 0 || + dwordCount == 0 || + dwordCount > 1_000_000) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } @@ -1993,16 +2093,29 @@ public static class AgcExports if (tracePackets) { - TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); + TraceAgc( + $"agc.driver_submit_dcb abi={(directArguments ? "direct" : "packed")} " + + $"packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); } var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); + ulong flipSequenceBefore; lock (gpuState.Gate) { + flipSequenceBefore = gpuState.FlipSequence; ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets); DrainResumableDcbs(ctx, gpuState, tracePackets); } + // Flip submissions are completed through VideoOut pacing. + if (gpuState.FlipSequence == flipSequenceBefore) + { + KernelEventQueueCompatExports.TriggerRegisteredEvents( + 0, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + 0); + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -2028,8 +2141,10 @@ public static class AgcExports Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal); var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); + ulong flipSequenceBefore; lock (gpuState.Gate) { + flipSequenceBefore = gpuState.FlipSequence; for (uint i = 0; i < bufferCount; i++) { if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) || @@ -2053,6 +2168,14 @@ public static class AgcExports DrainResumableDcbs(ctx, gpuState, tracePackets); } + if (gpuState.FlipSequence == flipSequenceBefore) + { + KernelEventQueueCompatExports.TriggerRegisteredEvents( + 0, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + 0); + } + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -2301,6 +2424,40 @@ public static class AgcExports } var packetType = header >> 30; + if (packetType == 0) + { + // Consume alignment padding one dword at a time. + if (header == 0) + { + offset++; + continue; + } + + // Preserve stream alignment across type-0 packets. + var type0Length = ((header >> 16) & 0x3FFFu) + 2u; + if (offset + type0Length > dwordCount) + { + offset++; + continue; + } + + offset += type0Length; + continue; + } + + if (packetType == 1) + { + const uint type1Length = 3; + if (offset + type1Length > dwordCount) + { + offset++; + continue; + } + + offset += type1Length; + continue; + } + if (packetType == 2) { if (tracePackets) @@ -2315,13 +2472,15 @@ public static class AgcExports if (packetType != 3) { - return; + offset++; + continue; } var length = Pm4Length(header); if (length == 0 || offset + length > dwordCount) { - return; + offset++; + continue; } var op = (header >> 8) & 0xFFu; @@ -2559,6 +2718,7 @@ public static class AgcExports if (op == ItNop && register == RFlip && length >= 6) { + gpuState.FlipSequence++; if (!ctx.TryReadUInt32(currentAddress + 4, out var videoOutHandle) || !ctx.TryReadUInt32(currentAddress + 8, out var displayBufferIndexRaw) || !ctx.TryReadUInt32(currentAddress + 12, out var flipMode) || @@ -2571,23 +2731,8 @@ public static class AgcExports var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo)); var displayBufferIndex = unchecked((int)displayBufferIndexRaw); var handle = unchecked((int)videoOutHandle); - if (VideoOutExports.TryGetDisplayBufferInfo( - handle, - displayBufferIndex, - out var cachedDisplayBuffer) && - VulkanVideoPresenter.TrySubmitGuestImage( - cachedDisplayBuffer.Address, - cachedDisplayBuffer.Width, - cachedDisplayBuffer.Height, - cachedDisplayBuffer.PitchInPixel)) - { - TraceDisplayBuffer( - handle, - displayBufferIndex, - cachedDisplayBuffer, - "gpu-cache"); - } - else if (state.SawIndexedDraw && + // Prefer output decoded from the current DCB. + if (state.SawIndexedDraw && state.TranslatedDraw is { } translatedDraw && VideoOutExports.TryGetDisplayBufferInfo( handle, @@ -2651,6 +2796,22 @@ public static class AgcExports displayBuffer.Width, displayBuffer.Height); } + else if (VideoOutExports.TryGetDisplayBufferInfo( + handle, + displayBufferIndex, + out var cachedDisplayBuffer) && + VulkanVideoPresenter.TrySubmitGuestImage( + cachedDisplayBuffer.Address, + cachedDisplayBuffer.Width, + cachedDisplayBuffer.Height, + cachedDisplayBuffer.PitchInPixel)) + { + TraceDisplayBuffer( + handle, + displayBufferIndex, + cachedDisplayBuffer, + "gpu-cache"); + } _ = VideoOutExports.SubmitFlipFromAgc(ctx, handle, displayBufferIndex, unchecked((int)flipMode), flipArg); state.SawIndexedDraw = false; @@ -3972,25 +4133,28 @@ public static class AgcExports TraceTextureHash(descriptor, source); - var nonZero = 0; - for (var i = 0; i < source.Length; i++) + if (_traceAgcShader) { - if (source[i] != 0) + var nonZero = 0; + for (var i = 0; i < source.Length; i++) { - nonZero++; - if (nonZero >= 64) + if (source[i] != 0) { - break; + nonZero++; + if (nonZero >= 64) + { + break; + } } } - } - TraceAgcShader( - $"agc.texture_source addr=0x{descriptor.Address:X16} " + - $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + - $"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " + - $"dst=0x{descriptor.DstSelect:X3} " + - $"bytes={source.Length} nonzero64={nonZero}"); + TraceAgcShader( + $"agc.texture_source addr=0x{descriptor.Address:X16} " + + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + + $"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " + + $"dst=0x{descriptor.DstSelect:X3} " + + $"bytes={source.Length} nonzero64={nonZero}"); + } var rgba = source; texture = new VulkanGuestDrawTexture( @@ -5701,6 +5865,16 @@ public static class AgcExports Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } + private static void TraceAgc(ref AgcTraceHandler message) + { + if (!_traceAgc) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}"); + } + private static void TraceAgcShader(string message) { if (!_traceAgcShader) @@ -5711,6 +5885,62 @@ public static class AgcExports Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } + private static void TraceAgcShader(ref AgcShaderTraceHandler message) + { + if (!_traceAgcShader) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}"); + } + + [InterpolatedStringHandler] + internal ref struct AgcTraceHandler + { + private DefaultInterpolatedStringHandler _inner; + + public AgcTraceHandler(int literalLength, int formattedCount, out bool isEnabled) + { + isEnabled = _traceAgc; + _inner = isEnabled + ? new DefaultInterpolatedStringHandler(literalLength, formattedCount) + : default; + } + + public void AppendLiteral(string value) => _inner.AppendLiteral(value); + + public void AppendFormatted(T value) => _inner.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => + _inner.AppendFormatted(value, format); + + public string ToStringAndClear() => _inner.ToStringAndClear(); + } + + [InterpolatedStringHandler] + internal ref struct AgcShaderTraceHandler + { + private DefaultInterpolatedStringHandler _inner; + + public AgcShaderTraceHandler(int literalLength, int formattedCount, out bool isEnabled) + { + isEnabled = _traceAgcShader; + _inner = isEnabled + ? new DefaultInterpolatedStringHandler(literalLength, formattedCount) + : default; + } + + public void AppendLiteral(string value) => _inner.AppendLiteral(value); + + public void AppendFormatted(T value) => _inner.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => + _inner.AppendFormatted(value, format); + + public string ToStringAndClear() => _inner.ToStringAndClear(); + } + private static string FormatShaderDwords(IReadOnlyList values) => values.Count == 0 ? "none" diff --git a/src/SharpEmu.Libs/Ajm/AjmExports.cs b/src/SharpEmu.Libs/Ajm/AjmExports.cs new file mode 100644 index 0000000..c9b8741 --- /dev/null +++ b/src/SharpEmu.Libs/Ajm/AjmExports.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Ajm; + +public static class AjmExports +{ + private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005); + private static int _nextContextId; + + [SysAbiExport( + Nid = "dl+4eHSzUu4", + ExportName = "sceAjmInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAjm")] + public static int Initialize(CpuContext ctx) + { + var reserved = ctx[CpuRegister.Rdi]; + var outContextAddress = ctx[CpuRegister.Rsi]; + + // AJM requires a zero reserved argument. + if (reserved != 0 || outContextAddress == 0) + { + return ctx.SetReturn(OrbisAjmErrorInvalidParameter); + } + + var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId)); + if (!ctx.TryWriteUInt32(outContextAddress, contextId)) + { + return ctx.SetReturn(OrbisAjmErrorInvalidParameter); + } + + return ctx.SetReturn(0); + } +} diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index 6a616f4..730e3e9 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -202,7 +202,7 @@ public static class AudioOut2Exports { var userId = unchecked((int)ctx[CpuRegister.Rdi]); var outUserAddress = ctx[CpuRegister.Rsi]; - if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0) + if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0) { return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } diff --git a/src/SharpEmu.Libs/HostTimerResolution.cs b/src/SharpEmu.Libs/HostTimerResolution.cs new file mode 100644 index 0000000..cb2651a --- /dev/null +++ b/src/SharpEmu.Libs/HostTimerResolution.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace SharpEmu.Libs; + +public static class HostTimerResolution +{ + private const uint TargetPeriodMilliseconds = 1; + + private static int _requested; + + public static void Request() + { + if (Interlocked.Exchange(ref _requested, 1) != 0) + { + return; + } + + if (!OperatingSystem.IsWindows()) + { + return; + } + + try + { + if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0) + { + Console.Error.WriteLine( + "[LOADER][WARN] Host timer resolution request rejected; " + + "timed waits keep the default ~15.6 ms granularity."); + } + } + catch (DllNotFoundException exception) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}"); + } + } + + [SupportedOSPlatform("windows")] + [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)] + private static extern uint TimeBeginPeriod(uint uPeriod); +} diff --git a/src/SharpEmu.Libs/Json/JsonExports.cs b/src/SharpEmu.Libs/Json/JsonExports.cs index 4d2522d..4b25abd 100644 --- a/src/SharpEmu.Libs/Json/JsonExports.cs +++ b/src/SharpEmu.Libs/Json/JsonExports.cs @@ -78,6 +78,58 @@ public static class JsonExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "WSOuge5IsCg", + ExportName = "_ZN3sce4Json15InitParameter2C2Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson2")] + public static int InitParameter2Constructor(CpuContext ctx) + { + var thisAddress = ctx[CpuRegister.Rdi]; + TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]); + ctx[CpuRegister.Rax] = thisAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "I2QC8PYhJWY", + ExportName = "_ZN3sce4Json15InitParameter212setAllocatorERNS0_12MemAllocatorE", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson2")] + public static int InitParameter2SetAllocator(CpuContext ctx) + { + var thisAddress = ctx[CpuRegister.Rdi]; + TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]); + ctx[CpuRegister.Rax] = thisAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "Eu95jmqn5Rw", + ExportName = "_ZN3sce4Json15InitParameter217setFileBufferSizeEm", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson2")] + public static int InitParameter2SetFileBufferSize(CpuContext ctx) + { + var thisAddress = ctx[CpuRegister.Rdi]; + TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]); + ctx[CpuRegister.Rax] = thisAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "IXW-z8pggfg", + ExportName = "_ZN3sce4Json12Initializer2C1Ev", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceJson2")] + public static int Initializer2Constructor(CpuContext ctx) + { + var thisAddress = ctx[CpuRegister.Rdi]; + TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]); + ctx[CpuRegister.Rax] = thisAddress; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + private static void TraceJson(string operation, ulong thisAddress, ulong argument) { if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs index 83f0261..131bf95 100644 --- a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs @@ -80,14 +80,8 @@ public static class KernelAprCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress); - if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) - { - TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - - TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress); + // Completion output was written when the command was submitted. + TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -169,27 +163,6 @@ public static class KernelAprCompatExports return ctx.Memory.TryWrite(resultAddress, result); } - private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress) - { - if (waitArg2 == 0) - { - return submittedResultAddress; - } - - if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF) - { - return submittedResultAddress; - } - - return waitArg2; - } - - private static bool IsAmprCompletionToken(ulong value) - { - var tag = value >> 56; - return tag is 0x0C or 0x10; - } - private static void TraceApr( CpuContext ctx, string operation, diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index f2d03e9..3073ee8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports { private const int KernelEventSize = 0x20; public const short KernelEventFilterGraphics = -14; + public const short KernelEventFilterUser = -11; public const short KernelEventFilterAmpr = -16; public const short KernelEventFilterAmprSystem = -17; @@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelAddUserEventEdge(CpuContext ctx) { - TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var registered = RegisterEvent( + handle, + ctx[CpuRegister.Rsi], + KernelEventFilterUser, + 0); + TraceEventQueue(ctx, "add_user_edge", handle); + return registered + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelAddUserEvent(CpuContext ctx) { - TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var registered = RegisterEvent( + handle, + ctx[CpuRegister.Rsi], + KernelEventFilterUser, + 0); + TraceEventQueue(ctx, "add_user", handle); + return registered + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelDeleteUserEvent(CpuContext ctx) { - TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var deleted = DeleteRegisteredEvent( + handle, + ctx[CpuRegister.Rsi], + KernelEventFilterUser); + TraceEventQueue(ctx, "delete_user", handle); + return deleted + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelTriggerUserEvent(CpuContext ctx) { - TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var triggered = TriggerRegisteredEvent( + handle, + ctx[CpuRegister.Rsi], + KernelEventFilterUser, + flags: 0x21, + fflags: 0, + data: ctx[CpuRegister.Rdx]); + TraceEventQueue(ctx, "trigger_user", handle); + return triggered + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports return triggeredCount; } + private static bool TriggerRegisteredEvent( + ulong handle, + ulong ident, + short filter, + ushort flags, + uint fflags, + ulong data) + { + lock (_eventQueueGate) + { + if (!_registeredEvents.TryGetValue(handle, out var registrations) || + !registrations.TryGetValue((ident, filter), out var registration)) + { + return false; + } + + if (!_pendingEvents.TryGetValue(handle, out var queue)) + { + queue = new LinkedList(); + _pendingEvents[handle] = queue; + } + + QueueOrUpdateEvent( + queue, + new KernelQueuedEvent( + registration.Ident, + registration.Filter, + flags, + fflags, + data, + registration.UserData)); + } + + WakeEventQueue(handle); + return true; + } + public static bool TriggerDisplayEvent( ulong handle, ulong ident, @@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip); Console.Error.WriteLine( - $"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}"); + $"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}"); } } diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index fb2cfe6..6bfd038 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using SharpEmu.HLE; +using SharpEmu.Libs.Fiber; using System.Diagnostics.CodeAnalysis; namespace SharpEmu.Libs.Kernel; @@ -44,6 +45,8 @@ public static class KernelPthreadCompatExports // type initializer that first runs on a guest thread and fail-fasts the CLR. private static long _nextMutexWakeId; private static long _nextCondWakeId; + private static readonly ConcurrentDictionary _posixTimedWaitCounts = new(); + private static readonly ConcurrentDictionary _reportedContendedMutexes = new(); private sealed class PthreadMutexState { @@ -351,29 +354,95 @@ public static class KernelPthreadCompatExports LibraryName = "libKernel")] public static int PosixPthreadCondTimedwait(CpuContext ctx) { - // POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113). - var abstimeAddress = ctx[CpuRegister.Rdx]; - if (abstimeAddress == 0) + var absoluteTimeAddress = ctx[CpuRegister.Rdx]; + if (absoluteTimeAddress == 0 || + !ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) || + !ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw)) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - if (!ctx.TryReadUInt64(abstimeAddress, out var deadlineSeconds) || - !ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds)) + var deadlineSeconds = unchecked((long)secondsRaw); + var deadlineNanoseconds = unchecked((long)nanosecondsRaw); + if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - var nowMicroseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000L; - var deadlineMicroseconds = - deadlineSeconds >= long.MaxValue / 1_000_000UL - ? long.MaxValue - : (long)(deadlineSeconds * 1_000_000UL + Math.Min(deadlineNanoseconds, 999_999_999UL) / 1000UL); - var remainingMicroseconds = deadlineMicroseconds - nowMicroseconds; - var timeoutUsec = remainingMicroseconds <= 0 - ? 1u - : (uint)Math.Min(remainingMicroseconds, uint.MaxValue); - return PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: timeoutUsec); + long nowSeconds; + long nowNanoseconds; + if (deadlineSeconds >= 1_000_000_000) + { + var now = DateTimeOffset.UtcNow; + nowSeconds = now.ToUnixTimeSeconds(); + nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100; + } + else + { + // Engine condition variables use monotonic deadlines. + KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds); + } + + var remainingNanoseconds = + ((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) - + ((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds); + var timeoutUsec = remainingNanoseconds <= 0 + ? 0u + : (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000); + + TracePosixTimedWait( + ctx, + deadlineSeconds, + deadlineNanoseconds, + nowSeconds, + nowNanoseconds, + remainingNanoseconds, + timeoutUsec); + + return PthreadCondWaitCore( + ctx, + ctx[CpuRegister.Rdi], + ctx[CpuRegister.Rsi], + timed: true, + timeoutUsec, + posixResult: true); + } + + private static void TracePosixTimedWait( + CpuContext ctx, + long deadlineSeconds, + long deadlineNanoseconds, + long nowSeconds, + long nowNanoseconds, + Int128 remainingNanoseconds, + uint timeoutUsec) + { + var condAddress = ctx[CpuRegister.Rdi]; + var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1); + if (count != 1 && count % 1_000_000 != 0) + { + return; + } + + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip); + Console.Error.WriteLine( + $"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " + + $"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " + + $"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " + + $"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}"); + + if (GuestThreadExecution.Scheduler is not { } scheduler) + { + return; + } + + foreach (var snapshot in scheduler.SnapshotThreads()) + { + Console.Error.WriteLine( + $"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " + + $"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " + + $"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}"); + } } [SysAbiExport( @@ -654,11 +723,12 @@ public static class KernelPthreadCompatExports var acquired = state.Semaphore.Wait(0); if (!acquired) { + TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId); var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId }; - // Cooperative deschedule on a contended lock corrupts fiber state - // (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the - // synchronous host-thread wait below. - if (_enableMutexLockBlocking && + // Fibers retain the synchronous fallback to preserve switch state. + var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); + var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0; + if (canCooperativelyBlock && !tryOnly && GuestThreadExecution.IsGuestThread && GuestThreadExecution.TryGetCurrentImportCallFrame(out _) && @@ -697,6 +767,53 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static void TraceContendedMutex( + CpuContext ctx, + ulong mutexAddress, + ulong resolvedAddress, + PthreadMutexState state, + ulong currentThreadId) + { + if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0)) + { + return; + } + + ulong ownerThreadId; + int recursionCount; + int type; + lock (state) + { + ownerThreadId = state.OwnerThreadId; + recursionCount = state.RecursionCount; + type = state.Type; + } + + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip); + Console.Error.WriteLine( + $"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " + + $"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " + + $"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}"); + + if (GuestThreadExecution.Scheduler is not { } scheduler) + { + return; + } + + foreach (var snapshot in scheduler.SnapshotThreads()) + { + if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId) + { + continue; + } + + Console.Error.WriteLine( + $"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " + + $"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " + + $"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}"); + } + } + private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner) { if (mutexAddress == 0) @@ -1076,6 +1193,13 @@ public static class KernelPthreadCompatExports lock (_stateGate) { + if (_condStates.TryGetValue(condAddress, out var raced)) + { + resolvedAddress = condAddress; + state = raced; + return true; + } + _condStates[condAddress] = createdState; _condStates[handle] = createdState; } @@ -1172,7 +1296,13 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0) + private static int PthreadCondWaitCore( + CpuContext ctx, + ulong condAddress, + ulong mutexAddress, + bool timed, + uint timeoutUsec = 0, + bool posixResult = false) { if (condAddress == 0 || mutexAddress == 0) { @@ -1210,9 +1340,10 @@ public static class KernelPthreadCompatExports if (consumedPendingSignal) { - // Leave SyncRoot before relocking the mutex to avoid lock-order inversion with cond-signal (#113). state.Waiters = Math.Max(0, state.Waiters - 1); TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult); + + // Relock outside SyncRoot to preserve lock ordering. Monitor.Exit(state.SyncRoot); try { @@ -1221,6 +1352,7 @@ public static class KernelPthreadCompatExports } finally { + // Balance the surrounding lock statement. Monitor.Enter(state.SyncRoot); } } @@ -1231,7 +1363,7 @@ public static class KernelPthreadCompatExports ctx, timed ? "pthread_cond_timedwait" : "pthread_cond_wait", state.WakeKey, - () => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion), + () => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult), () => state.SignalEpoch != observedEpoch, timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0)) { @@ -1315,7 +1447,7 @@ public static class KernelPthreadCompatExports state, timed, waitResult); - return waitResult; + return TranslatePthreadResult(waitResult, posixResult); } private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast) @@ -1372,7 +1504,8 @@ public static class KernelPthreadCompatExports PthreadCondState state, ulong observedEpoch, bool timed, - int releasedRecursion) + int releasedRecursion, + bool posixResult) { var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK; lock (state.SyncRoot) @@ -1393,7 +1526,32 @@ public static class KernelPthreadCompatExports } var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion); - return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult; + return TranslatePthreadResult( + lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult, + posixResult); + } + + private static int TranslatePthreadResult(int result, bool posixResult) + { + if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + return result; + } + + return result switch + { + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT + _ => result + }; } private static string GetCondWakeKey(ulong resolvedCondAddress) => @@ -1559,7 +1717,10 @@ public static class KernelPthreadCompatExports return TimeSpan.Zero; } - return TimeSpan.FromTicks((long)timeoutUsec * 10L); + // Round positive sub-millisecond waits to the host sleep quantum. + return timeoutUsec < 1_000 + ? TimeSpan.FromMilliseconds(1) + : TimeSpan.FromTicks((long)timeoutUsec * 10L); } private static TimeSpan GetCondSpuriousWakeTimeout() @@ -1701,7 +1862,8 @@ public static class KernelPthreadCompatExports } Console.Error.WriteLine( - $"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " + + $"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " + + $"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " + $"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}"); } diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 178c205..086d2e4 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -213,6 +213,13 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "QcteRwbsnV0", + ExportName = "usleep", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx); + private static void TraceUsleepSpin(CpuContext ctx, ulong micros) { if (!_traceUsleep) diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index 3ecd42c..a86d7c8 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -53,8 +53,19 @@ public static class NpManagerExports LibraryName = "libSceNpManager")] public static int NpGetOnlineId(CpuContext ctx) { - ctx[CpuRegister.Rax] = 0; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + // Gen5 ABI: user ID, then output structure. + return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]); + } + + [SysAbiExport( + Nid = "rbknaUjpqWo", + ExportName = "sceNpGetOnlineIdA", + Target = Generation.Gen5, + LibraryName = "libSceNpManager")] + public static int NpGetOnlineIdA(CpuContext ctx) + { + // The A variant takes a user ID before OnlineId. + return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]); } [SysAbiExport( @@ -169,4 +180,19 @@ public static class NpManagerExports Console.Error.WriteLine($"[LOADER][TRACE] np.{message}"); } + + private static int WriteOfflineOnlineId(CpuContext ctx, ulong address) + { + if (address == 0) + { + return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + // SceNpOnlineId is a 16-byte handle plus four trailing bytes. + Span onlineId = stackalloc byte[20]; + "Player"u8.CopyTo(onlineId); + return ctx.Memory.TryWrite(address, onlineId) + ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK) + : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } } diff --git a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs index 9b7684f..dd0e869 100644 --- a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs +++ b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs @@ -31,6 +31,18 @@ public static class NpWebApi2Exports return ctx.SetReturn(0); } + [SysAbiExport( + Nid = "WV1GwM32NgY", + ExportName = "sceNpWebApi2InitializeForToolkit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpWebApi2")] + public static int NpWebApi2InitializeAlt(CpuContext ctx) + { + Interlocked.Exchange(ref _initialized, 1); + TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]); + return ctx.SetReturn(0); + } + [SysAbiExport( Nid = "bEvXpcEk200", ExportName = "sceNpWebApi2Terminate", diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index 3f01e52..88dd900 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -14,11 +14,18 @@ public static class PadExports private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005); private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007); private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008); - private const int PrimaryUserId = 1; + private const int PrimaryUserId = 1000; private const int StandardPortType = 0; private const int PrimaryPadHandle = 1; private const int ControllerInformationSize = 0x1C; private const int PadDataSize = 0x78; + private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000); + + [ThreadStatic] + private static long _lastInputSampleTicks; + + [ThreadStatic] + private static PadState _cachedInputState; private static bool _initialized; @@ -270,6 +277,44 @@ public static class PadExports { Span data = stackalloc byte[PadDataSize]; data.Clear(); + var input = ReadHostInputState(); + var buttons = input.Buttons; + var leftX = input.LeftX; + var leftY = input.LeftY; + var rightX = input.RightX; + var rightY = input.RightY; + var l2 = input.L2; + var r2 = input.R2; + + BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons); + data[0x04] = leftX; + data[0x05] = leftY; + data[0x06] = rightX; + data[0x07] = rightY; + data[0x08] = l2; + data[0x09] = r2; + BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f); + data[0x4C] = 1; + var timestampTicks = Stopwatch.GetTimestamp(); + var timestampMicroseconds = + ((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) + + ((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency); + BinaryPrimitives.WriteUInt64LittleEndian( + data[0x50..], + timestampMicroseconds); + data[0x68] = 1; + + return ctx.Memory.TryWrite(dataAddress, data); + } + + private static PadState ReadHostInputState() + { + var now = Stopwatch.GetTimestamp(); + if (_lastInputSampleTicks != 0 && now - _lastInputSampleTicks < InputSampleIntervalTicks) + { + return _cachedInputState; + } + var acceptsKeyboardInput = IsEmulatorWindowFocused(); var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0; var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128; @@ -303,25 +348,17 @@ public static class PadExports r2 = Math.Max(r2, xpad.R2); } - BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons); - data[0x04] = leftX; - data[0x05] = leftY; - data[0x06] = rightX; - data[0x07] = rightY; - data[0x08] = l2; - data[0x09] = r2; - BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f); - data[0x4C] = 1; - var timestampTicks = Stopwatch.GetTimestamp(); - var timestampMicroseconds = - ((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) + - ((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency); - BinaryPrimitives.WriteUInt64LittleEndian( - data[0x50..], - timestampMicroseconds); - data[0x68] = 1; - - return ctx.Memory.TryWrite(dataAddress, data); + _cachedInputState = new PadState( + Connected: true, + Buttons: buttons, + LeftX: leftX, + LeftY: leftY, + RightX: rightX, + RightY: rightY, + L2: l2, + R2: r2); + _lastInputSampleTicks = now; + return _cachedInputState; } [DllImport("user32.dll")] diff --git a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs index ea8f782..b4f68f8 100644 --- a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs +++ b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs @@ -682,7 +682,9 @@ public static class PlayGoExports var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml); if (!hasMetadata) { - return PlayGoMetadata.Empty; + // Full installs may omit PlayGo sidecar metadata. + TracePlayGo("metadata_missing; using fully-installed default chunk"); + return new PlayGoMetadata(true, [(ushort)0]); } var chunkIds = LoadChunkIds(chunkDefsXml); diff --git a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs index b0da09f..2d3d921 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs @@ -244,7 +244,6 @@ public static class SaveDataExports } } - private static int _nextTransactionResource; [SysAbiExport( Nid = "gjRZNnw0JPE", ExportName = "sceSaveDataCreateTransactionResource", @@ -252,24 +251,25 @@ public static class SaveDataExports LibraryName = "libSceSaveData")] public static int SaveDataCreateTransactionResource(CpuContext ctx) { - var userId = unchecked((int)ctx[CpuRegister.Rdi]); - var reserved = ctx[CpuRegister.Rsi]; - var resourceAddress = ctx[CpuRegister.Rdx]; + // Gen5 ABI: memory size, resource output, reserved. + var memorySize = ctx[CpuRegister.Rdi]; + var resourceAddress = ctx[CpuRegister.Rsi]; + var reserved = ctx[CpuRegister.Rdx]; if (resourceAddress == 0) { return ctx.SetReturn(OrbisSaveDataErrorParameter); } - var id = (uint)Interlocked.Increment(ref _nextTransactionResource); - - if (!ctx.TryWriteUInt32(resourceAddress, id)) + // Offline HLE operations carry no transaction state. + if (!ctx.TryWriteUInt64(resourceAddress, 0)) { return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceSaveData( - $"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}"); + $"create_transaction_resource memory_size=0x{memorySize:X} reserved=0x{reserved:X} " + + $"resource_addr=0x{resourceAddress:X} resource=0x0"); return ctx.SetReturn(0); } diff --git a/src/SharpEmu.Libs/UserService/UserServiceExports.cs b/src/SharpEmu.Libs/UserService/UserServiceExports.cs index 3738dc9..5190ff2 100644 --- a/src/SharpEmu.Libs/UserService/UserServiceExports.cs +++ b/src/SharpEmu.Libs/UserService/UserServiceExports.cs @@ -13,7 +13,8 @@ public static class UserServiceExports private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007); private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009); private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A); - private const int PrimaryUserId = 1; + // Retail user-service IDs begin at 0x3E8. + private const int PrimaryUserId = 1000; private const int InvalidUserId = -1; private const string PrimaryUserName = "SharpEmu"; private static int _loginEventDelivered; @@ -111,7 +112,8 @@ public static class UserServiceExports var userId = unchecked((int)ctx[CpuRegister.Rdi]); var nameAddress = ctx[CpuRegister.Rsi]; var capacity = ctx[CpuRegister.Rdx]; - if (userId != PrimaryUserId) + // Zero selects the current user. + if (userId != 0 && userId != PrimaryUserId) { return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter); } @@ -137,6 +139,28 @@ public static class UserServiceExports return ctx.SetReturn(result); } + [SysAbiExport( + Nid = "-sD02mFDBh4", + ExportName = "sceUserServiceGetGamePresets", + Target = Generation.Gen5, + LibraryName = "libSceUserService")] + public static int UserServiceGetGamePresets(CpuContext ctx) + { + // Return deterministic defaults for the offline profile. + var userId = unchecked((int)ctx[CpuRegister.Rdi]); + var resultAddress = ctx[CpuRegister.Rcx]; + Span defaults = stackalloc byte[0x18]; + if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults)) + { + return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument); + } + + TraceUserService( + $"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " + + $"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000"); + return ctx.SetReturn(0); + } + [SysAbiExport( Nid = "D-CzAxQL0XI", ExportName = "sceUserServiceGetPlatformPrivacySetting", diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index b77505a..3775dda 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -48,54 +48,106 @@ public static class VideoOutExports private static readonly Dictionary _ports = new(); // Hardware raises vblank autonomously; UE blocks its frame loop on it. - private static Timer? _vblankPumpTimer; - private static int _vblankPumpActive; - private const int VblankPumpIntervalMs = 16; + private const double VblankHz = 60.0; + private const int VblankWaitTimeoutMilliseconds = 100; + private static Thread? _vblankPumpThread; + private static int _vblankPumpStarted; + + private static readonly object _vblankEdgeGate = new(); + private static ulong _vblankEdgeSequence; + private static long _vblankMissedEdges; private static void EnsureVblankPumpStarted() { - if (_vblankPumpTimer is not null) + if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0) { return; } - _vblankPumpTimer = new Timer( - static _ => PumpVblanks(), - null, - VblankPumpIntervalMs, - VblankPumpIntervalMs); + HostTimerResolution.Request(); + + _vblankPumpThread = new Thread(VblankPumpLoop) + { + IsBackground = true, + Name = "SharpEmu VideoOut vblank", + Priority = ThreadPriority.AboveNormal, + }; + _vblankPumpThread.Start(); + } + + private static void VblankPumpLoop() + { + var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz)); + var nextEdge = Stopwatch.GetTimestamp() + intervalTicks; + + while (true) + { + WaitUntilTimestamp(nextEdge); + PumpVblanks(); + + nextEdge += intervalTicks; + + var now = Stopwatch.GetTimestamp(); + if (nextEdge < now) + { + var missed = (now - nextEdge) / intervalTicks + 1; + Interlocked.Add(ref _vblankMissedEdges, missed); + nextEdge = now + intervalTicks; + } + } + } + + private static void WaitUntilTimestamp(long deadlineTicks) + { + var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L; + + while (true) + { + var remaining = deadlineTicks - Stopwatch.GetTimestamp(); + if (remaining <= 0) + { + return; + } + + if (remaining > spinThresholdTicks) + { + var sleepMilliseconds = + (int)((remaining - spinThresholdTicks) * 1000L / Stopwatch.Frequency); + if (sleepMilliseconds > 0) + { + Thread.Sleep(sleepMilliseconds); + continue; + } + } + + Thread.SpinWait(64); + } } private static void PumpVblanks() { - if (Interlocked.Exchange(ref _vblankPumpActive, 1) != 0) + lock (_vblankEdgeGate) { - return; + _vblankEdgeSequence++; + Monitor.PulseAll(_vblankEdgeGate); } - try + VideoOutPortState[] ports; + lock (_stateGate) { - VideoOutPortState[] ports; - lock (_stateGate) + if (_ports.Count == 0) { - if (_ports.Count == 0) - { - return; - } - - // Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global - // flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads. - ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray(); + return; } - foreach (var port in ports) - { - SignalVblank(port); - } + // Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global + // flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads. + ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray(); } - finally + + foreach (var port in ports) { - Volatile.Write(ref _vblankPumpActive, 0); + SignalVblank(port); } } @@ -103,6 +155,7 @@ public static class VideoOutExports private static int _nextHandle = 1; private static int _frameDumpCount; private static long _nextFrameDumpIndex; + private static string _windowTitleBase = "SharpEmu VideoOut"; private static string _windowTitle = "SharpEmu VideoOut"; private static readonly bool _logFrameRate = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"), @@ -112,6 +165,10 @@ public static class VideoOutExports Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"), "1", StringComparison.Ordinal); + private static readonly bool _dumpVideoOut = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"), + "1", + StringComparison.Ordinal); private static long _frameRateWindowStart = Stopwatch.GetTimestamp(); private static long _submittedFrameCount; private static long _presentedFrameCount; @@ -134,10 +191,24 @@ public static class VideoOutExports var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts); var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}"; var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}"; - var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName} \u00b7 {HostSystemInfo.GpuName}"; + var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName}"; lock (_stateGate) { - _windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}"; + _windowTitleBase = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}"; + _windowTitle = $"{_windowTitleBase} \u00b7 {HostSystemInfo.GpuName}"; + } + } + + internal static void SetSelectedGpuName(string gpuName) + { + if (string.IsNullOrWhiteSpace(gpuName)) + { + return; + } + + lock (_stateGate) + { + _windowTitle = $"{_windowTitleBase} \u00b7 {gpuName.Trim()}"; } } @@ -376,7 +447,20 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidHandle; } - Thread.Sleep(1); + EnsureVblankPumpStarted(); + + lock (_vblankEdgeGate) + { + var entryEdge = _vblankEdgeSequence; + while (_vblankEdgeSequence == entryEdge) + { + if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds)) + { + break; + } + } + } + SignalVblank(port); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -987,10 +1071,7 @@ public static class VideoOutExports displayBuffer.PitchInPixel); } - if (string.Equals( - Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"), - "1", - StringComparison.Ordinal)) + if (_dumpVideoOut) { _ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg); } @@ -1050,9 +1131,11 @@ public static class VideoOutExports var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency; var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0); var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0); + var missedEdges = Interlocked.Exchange(ref _vblankMissedEdges, 0); Console.Error.WriteLine( $"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " + - $"presented_fps={presentedCount / elapsedSeconds:F1}"); + $"presented_fps={presentedCount / elapsedSeconds:F1} " + + $"vblank_missed={missedEdges}"); } private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan addresses, BufferAttribute attribute, int requestedGroupIndex = -1) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 91964aa..9994aed 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -11,6 +11,7 @@ using Silk.NET.Vulkan.Extensions.EXT; using Silk.NET.Windowing; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using VkBuffer = Silk.NET.Vulkan.Buffer; @@ -600,7 +601,8 @@ internal static unsafe class VulkanVideoPresenter var traceSubmission = false; lock (_gate) { - var known = _availableGuestImages.ContainsKey(address); + // VideoOut registration does not imply a rendered Vulkan image. + var known = _gpuGuestImages.ContainsKey(address); if (ShouldTracePresentedGuestImageContentsForDiagnostics()) { Console.Error.WriteLine( @@ -622,7 +624,7 @@ internal static unsafe class VulkanVideoPresenter { Console.Error.WriteLine( $"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " + - $"{width}x{height} - flip target was never registered as a render output"); + $"{width}x{height} - flip target has no completed GPU render output"); } return false; @@ -933,6 +935,8 @@ internal static unsafe class VulkanVideoPresenter private ExtDebugUtils? _debugUtils; private PhysicalDevice _physicalDevice; private Device _device; + private PipelineCache _pipelineCache; + private string? _pipelineCachePath; private Queue _queue; private uint _queueFamilyIndex; private SwapchainKHR _swapchain; @@ -950,6 +954,9 @@ internal static unsafe class VulkanVideoPresenter private CommandBuffer _presentationCommandBuffer; private VkSemaphore _imageAvailable; private VkSemaphore _renderFinished; + private Fence _presentationFence; + private bool _presentationInFlight; + private TranslatedDrawResources? _pendingPresentationResources; private VkBuffer _stagingBuffer; private DeviceMemory _stagingMemory; private ulong _stagingSize; @@ -966,6 +973,10 @@ internal static unsafe class VulkanVideoPresenter private int _directPresentationCount; private readonly Dictionary _guestImages = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new(); + private static readonly bool _dumpTextures = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"), + "1", + StringComparison.Ordinal); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new(); private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new(); @@ -988,7 +999,7 @@ internal static unsafe class VulkanVideoPresenter private readonly record struct GraphicsPipelineKey( string VertexShader, string FragmentShader, - ulong RenderPass, + Format RenderTargetFormat, PrimitiveTopology Topology, VulkanGuestBlendState Blend, string ResourceLayout, @@ -1150,6 +1161,7 @@ internal static unsafe class VulkanVideoPresenter CreateSurface(); SelectPhysicalDevice(); CreateDevice(); + CreatePipelineCache(); CreateSwapchain(); CreateCommandResources(); CreateGuestDrawResources(); @@ -1559,8 +1571,11 @@ internal static unsafe class VulkanVideoPresenter } _vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected); + var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown"; Console.Error.WriteLine( - $"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})"); + $"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})"); + VideoOutExports.SetSelectedGpuName(selectedName); + _window.Title = VideoOutExports.GetWindowTitle(); } private static int ScorePhysicalDevice( @@ -1743,6 +1758,110 @@ internal static unsafe class VulkanVideoPresenter } } + private void CreatePipelineCache() + { + _vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties); + var directory = Path.Combine(AppContext.BaseDirectory, "pipeline-cache"); + _pipelineCachePath = Path.Combine( + directory, + $"vulkan-{properties.VendorID:X4}-{properties.DeviceID:X4}-{properties.DriverVersion:X8}.bin"); + + byte[] initialData = []; + try + { + if (File.Exists(_pipelineCachePath)) + { + initialData = File.ReadAllBytes(_pipelineCachePath); + } + } + catch (IOException exception) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan pipeline cache could not be read: {exception.Message}"); + } + + fixed (byte* initialDataPointer = initialData) + { + var createInfo = new PipelineCacheCreateInfo + { + SType = StructureType.PipelineCacheCreateInfo, + InitialDataSize = (nuint)initialData.Length, + PInitialData = initialData.Length == 0 ? null : initialDataPointer, + }; + var result = _vk.CreatePipelineCache( + _device, + &createInfo, + null, + out _pipelineCache); + if (result != Result.Success && initialData.Length != 0) + { + createInfo.InitialDataSize = 0; + createInfo.PInitialData = null; + Check( + _vk.CreatePipelineCache( + _device, + &createInfo, + null, + out _pipelineCache), + "vkCreatePipelineCache(empty)"); + initialData = []; + } + else + { + Check(result, "vkCreatePipelineCache"); + } + } + + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan pipeline cache: " + + $"{(initialData.Length == 0 ? "new" : $"loaded {initialData.Length} bytes")}"); + } + + private void SavePipelineCache() + { + if (_pipelineCache.Handle == 0 || string.IsNullOrWhiteSpace(_pipelineCachePath)) + { + return; + } + + try + { + nuint size = 0; + Check( + _vk.GetPipelineCacheData(_device, _pipelineCache, &size, null), + "vkGetPipelineCacheData(size)"); + if (size == 0 || size > int.MaxValue) + { + return; + } + + var data = new byte[(int)size]; + fixed (byte* dataPointer = data) + { + Check( + _vk.GetPipelineCacheData( + _device, + _pipelineCache, + &size, + dataPointer), + "vkGetPipelineCacheData"); + } + + var directory = Path.GetDirectoryName(_pipelineCachePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + File.WriteAllBytes(_pipelineCachePath, data.AsSpan(0, (int)size).ToArray()); + } + catch (Exception exception) when ( + exception is IOException or UnauthorizedAccessException or InvalidOperationException) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan pipeline cache could not be saved: {exception.Message}"); + } + } + private void CreateSwapchain() { Check( @@ -1835,9 +1954,44 @@ internal static unsafe class VulkanVideoPresenter Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _imageAvailable), "vkCreateSemaphore"); Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "vkCreateSemaphore"); + var fenceInfo = new FenceCreateInfo + { + SType = StructureType.FenceCreateInfo, + }; + Check( + _vk.CreateFence(_device, &fenceInfo, null, out _presentationFence), + "vkCreateFence(presentation)"); + CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4); } + private void CompletePendingPresentation(bool wait) + { + if (!_presentationInFlight) + { + return; + } + + var fence = _presentationFence; + var result = wait + ? _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue) + : _vk.GetFenceStatus(_device, fence); + if (!wait && result == Result.NotReady) + { + return; + } + + Check(result, "vkWaitForFences(presentation)"); + _presentationInFlight = false; + if (_pendingPresentationResources is not null) + { + MarkSampledImagesInitialized(_pendingPresentationResources); + MarkStorageImagesInitialized(_pendingPresentationResources); + DestroyTranslatedDrawResources(_pendingPresentationResources); + _pendingPresentationResources = null; + } + } + private CommandBuffer AllocateGuestCommandBuffer() { var allocateInfo = new CommandBufferAllocateInfo @@ -2184,7 +2338,7 @@ internal static unsafe class VulkanVideoPresenter Check( _vk.CreateGraphicsPipelines( _device, - default, + _pipelineCache, 1, &pipelineInfo, null, @@ -2219,6 +2373,7 @@ internal static unsafe class VulkanVideoPresenter private TranslatedDrawResources CreateTranslatedDrawResources( VulkanTranslatedGuestDraw draw, RenderPass renderPass, + Format renderTargetFormat, Extent2D extent) { var vertexSpirv = draw.VertexSpirv; @@ -2285,7 +2440,13 @@ internal static unsafe class VulkanVideoPresenter CreateTranslatedDescriptorResources( resources, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit); - CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent); + CreateTranslatedPipeline( + resources, + vertexSpirv, + draw.PixelSpirv, + renderPass, + renderTargetFormat, + extent); return resources; } catch @@ -2581,12 +2742,13 @@ internal static unsafe class VulkanVideoPresenter byte[] vertexSpirv, byte[] fragmentSpirv, RenderPass renderPass, + Format renderTargetFormat, Extent2D extent) { var pipelineKey = new GraphicsPipelineKey( GetShaderDigest(vertexSpirv), GetShaderDigest(fragmentSpirv), - renderPass.Handle, + renderTargetFormat, resources.Topology, resources.Blend, GetResourceLayoutKey(resources), @@ -2742,7 +2904,7 @@ internal static unsafe class VulkanVideoPresenter Check( _vk.CreateGraphicsPipelines( _device, - default, + _pipelineCache, 1, &pipelineInfo, null, @@ -2922,7 +3084,7 @@ internal static unsafe class VulkanVideoPresenter Check( _vk.CreateComputePipelines( _device, - default, + _pipelineCache, 1, &pipelineInfo, null, @@ -3448,11 +3610,36 @@ internal static unsafe class VulkanVideoPresenter { const ulong offsetBasis = 14695981039346656037; const ulong prime = 1099511628211; - var hash = offsetBasis; - foreach (var value in pixels) + + var h0 = offsetBasis ^ (ulong)pixels.Length; + var h1 = offsetBasis; + var h2 = offsetBasis; + var h3 = offsetBasis; + + var words = MemoryMarshal.Cast(pixels); + var index = 0; + var blockEnd = words.Length - (words.Length & 3); + for (; index < blockEnd; index += 4) { - hash ^= value; - hash *= prime; + h0 = (h0 ^ words[index]) * prime; + h1 = (h1 ^ words[index + 1]) * prime; + h2 = (h2 ^ words[index + 2]) * prime; + h3 = (h3 ^ words[index + 3]) * prime; + } + + for (; index < words.Length; index++) + { + h0 = (h0 ^ words[index]) * prime; + } + + var hash = h0; + hash = (hash ^ h1) * prime; + hash = (hash ^ h2) * prime; + hash = (hash ^ h3) * prime; + + foreach (var value in pixels[(words.Length * sizeof(ulong))..]) + { + hash = (hash ^ value) * prime; } return hash; @@ -3465,10 +3652,7 @@ internal static unsafe class VulkanVideoPresenter uint width, uint height) { - if (!string.Equals( - Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"), - "1", - StringComparison.Ordinal) || + if (!_dumpTextures || texture.IsFallback || texture.IsStorage || GetTextureBytesPerPixel(texture.Format) != 4 || @@ -4489,6 +4673,7 @@ internal static unsafe class VulkanVideoPresenter resources = CreateTranslatedDrawResources( work.Draw, target.RenderPass, + target.Format, extent); resources.DebugName = $"SharpEmu offscreen rt=0x{work.Target.Address:X16} " + @@ -5125,6 +5310,8 @@ internal static unsafe class VulkanVideoPresenter return; } + CompletePendingPresentation(wait: true); + byte[]? pixels = null; if (presentation.Pixels is { } sourcePixels) { @@ -5172,6 +5359,7 @@ internal static unsafe class VulkanVideoPresenter translatedResources = CreateTranslatedDrawResources( translatedDraw, _renderPass, + _swapchainFormat, _extent); if (ShouldTracePresentedGuestImageContentsForDiagnostics() && !_firstGuestDrawPresented) @@ -5303,7 +5491,16 @@ internal static unsafe class VulkanVideoPresenter SignalSemaphoreCount = 1, PSignalSemaphores = &renderFinished, }; - Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit"); + var presentationFence = _presentationFence; + Check( + _vk.ResetFences(_device, 1, &presentationFence), + "vkResetFences(presentation)"); + Check( + _vk.QueueSubmit(_queue, 1, &submitInfo, presentationFence), + "vkQueueSubmit"); + _presentationInFlight = true; + _pendingPresentationResources = translatedResources; + translatedResources = null; var swapchain = _swapchain; var presentInfo = new PresentInfoKHR @@ -5319,6 +5516,7 @@ internal static unsafe class VulkanVideoPresenter if (presentResult == Result.ErrorOutOfDateKhr) { Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); + CompletePendingPresentation(wait: false); CollectCompletedGuestSubmissions(waitForOldest: false); if (translatedResources is not null) { @@ -5331,19 +5529,13 @@ internal static unsafe class VulkanVideoPresenter CheckSwapchainResult(presentResult, "vkQueuePresentKHR"); recreateAfterPresent |= presentResult == Result.SuboptimalKhr; - Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); VideoOutExports.ReportPresentedFrame(); if (_swapchainReadbackPending) { + CompletePendingPresentation(wait: true); TraceSwapchainReadback(); } CollectCompletedGuestSubmissions(waitForOldest: false); - if (translatedResources is not null) - { - MarkSampledImagesInitialized(translatedResources); - MarkStorageImagesInitialized(translatedResources); - DestroyTranslatedDrawResources(translatedResources); - } _imageInitialized[imageIndex] = true; _presentedSequence = presentation.Sequence; @@ -6597,7 +6789,9 @@ internal static unsafe class VulkanVideoPresenter } _vulkanReady = false; _vk.DeviceWaitIdle(_device); + CompletePendingPresentation(wait: false); CollectCompletedGuestSubmissions(waitForOldest: false); + SavePipelineCache(); foreach (var pipeline in _computePipelines.Values) { _vk.DestroyPipeline(_device, pipeline, null); @@ -6608,6 +6802,11 @@ internal static unsafe class VulkanVideoPresenter _vk.DestroyPipeline(_device, pipeline, null); } _graphicsPipelines.Clear(); + if (_pipelineCache.Handle != 0) + { + _vk.DestroyPipelineCache(_device, _pipelineCache, null); + _pipelineCache = default; + } foreach (var layout in _descriptorLayouts.Values) { _vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null); @@ -6699,6 +6898,7 @@ internal static unsafe class VulkanVideoPresenter Console.Error.WriteLine( $"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}"); _vk.DeviceWaitIdle(_device); + CompletePendingPresentation(wait: false); CollectCompletedGuestSubmissions(waitForOldest: false); DestroySwapchainResources(); CreateSwapchain(); @@ -6732,6 +6932,11 @@ internal static unsafe class VulkanVideoPresenter _vk.DestroySemaphore(_device, _renderFinished, null); _renderFinished = default; } + if (_presentationFence.Handle != 0) + { + _vk.DestroyFence(_device, _presentationFence, null); + _presentationFence = default; + } if (_barycentricPipeline.Handle != 0) { _vk.DestroyPipeline(_device, _barycentricPipeline, null); diff --git a/src/SharpEmu.Libs/Voice/VoiceQoSExports.cs b/src/SharpEmu.Libs/Voice/VoiceQoSExports.cs new file mode 100644 index 0000000..8a49560 --- /dev/null +++ b/src/SharpEmu.Libs/Voice/VoiceQoSExports.cs @@ -0,0 +1,19 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Voice; + +public static class VoiceQoSExports +{ + [SysAbiExport( + Nid = "U8IfNl6-Css", + ExportName = "sceVoiceQoSInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceVoiceQoS")] + public static int VoiceQoSInit(CpuContext ctx) + { + return ctx.SetReturn(0); + } +} diff --git a/src/SharpEmu.Logging/FileLogSink.cs b/src/SharpEmu.Logging/FileLogSink.cs index bc2118b..a649553 100644 --- a/src/SharpEmu.Logging/FileLogSink.cs +++ b/src/SharpEmu.Logging/FileLogSink.cs @@ -6,19 +6,15 @@ using System.Text; namespace SharpEmu.Logging; -/// -/// Writes log entries to a file. Thread-safe via an internal lock. -/// is enabled so entries survive a crash. -/// public sealed class FileLogSink : ISharpEmuLogSink, IDisposable { + private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500); + private readonly object _sync = new(); private readonly StreamWriter _writer; + private readonly Timer _flushTimer; private bool _disposed; - /// Absolute or relative file path. Parent directories are created if missing. - /// to append to an existing file; to overwrite. - /// Always recommended for file logs — entries include a full date-time prefix. public FileLogSink(string path, bool append = true, bool includeTimestamp = true) { ArgumentException.ThrowIfNullOrWhiteSpace(path); @@ -34,14 +30,20 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, - bufferSize: 4096, + bufferSize: 65536, FileOptions.SequentialScan); - _writer = new StreamWriter(fileStream, Encoding.UTF8) + _writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536) { - AutoFlush = true + AutoFlush = false }; IncludeTimestamp = includeTimestamp; + + _flushTimer = new Timer( + static state => ((FileLogSink)state!).FlushBuffered(), + this, + FlushInterval, + FlushInterval); } public bool IncludeTimestamp { get; set; } @@ -84,11 +86,31 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable { _writer.WriteLine(entry.Exception); } + + if (entry.Level >= LogLevel.Error) + { + _writer.Flush(); + } + } + } + + private void FlushBuffered() + { + lock (_sync) + { + if (_disposed) + { + return; + } + + _writer.Flush(); } } public void Dispose() { + _flushTimer.Dispose(); + lock (_sync) { if (_disposed) diff --git a/src/SharpEmu.Logging/HostSystemInfo.cs b/src/SharpEmu.Logging/HostSystemInfo.cs index e1493a6..86b954b 100644 --- a/src/SharpEmu.Logging/HostSystemInfo.cs +++ b/src/SharpEmu.Logging/HostSystemInfo.cs @@ -36,11 +36,16 @@ public static class HostSystemInfo using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name)) { - return Regex.Replace( + name = Regex.Replace( name.Trim(), @"\s+\d+-Core Processor$", string.Empty, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return Regex.Replace( + name, + @"\s+with Radeon Graphics$", + string.Empty, + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } } catch (Exception) @@ -103,8 +108,12 @@ public static class HostSystemInfo } if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) || - name.Contains("geforce", StringComparison.OrdinalIgnoreCase) || - name.Contains("amd", StringComparison.OrdinalIgnoreCase) || + name.Contains("geforce", StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + + if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) || name.Contains("radeon", StringComparison.OrdinalIgnoreCase)) { return 2;