Compare commits

..

4 Commits

Author SHA1 Message Date
Spooks 787d3a1efb Fix Gen5 boot and restore stable AGC rendering (#139)
Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-14 16:13:42 +03:00
Kushida 884584da67 fix: restore WaitSema loop guard boundary (#133) 2026-07-14 15:07:25 +03:00
Spooks d43edc865a 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 <Spooks4576@users.noreply.github.com>
2026-07-14 14:41:42 +03:00
Berk cf6964710a [emulator] Improve emulator performance by optimizing memory access and reducing unnecessary overhead in kernel and CPU execution paths (#131) 2026-07-14 14:28:44 +03:00
26 changed files with 1632 additions and 226 deletions
+3
View File
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version> <Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'"> <PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -161,7 +161,7 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8)); *(ulong*)(xmmSlot + 8));
} }
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal)) if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{ {
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _); NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi]; *(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
@@ -245,12 +245,13 @@ public sealed partial class DirectExecutionBackend
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter); bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false; bool flag5 = false;
ExportedFunction? matchedExport = importStubEntry.Export; ExportedFunction? matchedExport = importStubEntry.Export;
var traceFlags = importStubEntry.TraceFlags;
bool periodicTrace = num <= 128 || bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) || (num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) || (num >= 900 && num <= 1300) ||
num % 100000 == 0L || num % 100000 == 0L ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || ((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || ((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
flag || flag ||
flag2 || flag2 ||
flag3; flag3;
@@ -311,15 +312,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi], cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]); 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}"); 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}"); 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}"); 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(); Console.Error.Flush();
} }
} }
if (importStubEntry.Nid == "Ou3iL1abvng") if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
{ {
if (_logStackCheck) if (_logStackCheck)
{ {
@@ -381,7 +382,7 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress); ActiveGuestReturnSlotAddress);
try try
{ {
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal)) if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{ {
if (_logBootstrap) if (_logBootstrap)
{ {
@@ -390,12 +391,11 @@ public sealed partial class DirectExecutionBackend
orbisGen2Result = DispatchBootstrapBridge(); orbisGen2Result = DispatchBootstrapBridge();
} }
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) || else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
{ {
orbisGen2Result = DispatchKernelDynlibDlsym(); orbisGen2Result = DispatchKernelDynlibDlsym();
} }
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal)) else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
{ {
orbisGen2Result = DispatchIl2CppApiLookupSymbol(); orbisGen2Result = DispatchIl2CppApiLookupSymbol();
} }
@@ -736,6 +736,9 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout = var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) && string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedEventFlagTimeout =
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedMutexTrylockBusy = var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) && string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@@ -748,6 +751,7 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss && if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout && !expectedTimedWaitTimeout &&
!expectedEqueueTimeout && !expectedEqueueTimeout &&
!expectedEventFlagTimeout &&
!expectedMutexTrylockBusy && !expectedMutexTrylockBusy &&
!expectedUserServiceNoEvent && !expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter) !expectedPrivacyInvalidParameter)
@@ -818,6 +822,7 @@ public sealed partial class DirectExecutionBackend
"2Z+PpY6CaJg" or // pthread_mutex_unlock "2Z+PpY6CaJg" or // pthread_mutex_unlock
"EgmLo6EWgso" or // pthread_rwlock_unlock "EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock "+L98PIbGttk" or // scePthreadRwlockUnlock
"q1cHNfGycLI" or // scePadRead
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending "zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress "V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
@@ -1076,7 +1081,15 @@ public sealed partial class DirectExecutionBackend
} }
private static bool IsImportLoopGuardBoundary(string nid) => private static bool IsImportLoopGuardBoundary(string nid) =>
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal); nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
private void ResetImportLoopPattern() private void ResetImportLoopPattern()
{ {
@@ -25,6 +25,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private const int DefaultImportLoopGuardSeconds = 5; 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 private readonly struct ImportStubEntry
{ {
public ulong Address { get; } public ulong Address { get; }
@@ -33,12 +53,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public ExportedFunction? Export { get; } public ExportedFunction? Export { get; }
public ImportStubKind Kind { get; }
public ImportStubTraceFlags TraceFlags { get; }
public ImportStubEntry(ulong address, string nid, ExportedFunction? export) public ImportStubEntry(ulong address, string nid, ExportedFunction? export)
{ {
Address = address; Address = address;
Nid = nid; Nid = nid;
Export = export; 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( 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 ulong GuestThreadRegionStride = 0x0100_0000UL;
private const int GuestThreadRegionSlotCount = 256;
private const uint PAGE_EXECUTE_READWRITE = 64u; private const uint PAGE_EXECUTE_READWRITE = 64u;
private const uint PAGE_READWRITE = 4u; private const uint PAGE_READWRITE = 4u;
@@ -2757,7 +2803,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (hostThread is not null && if (hostThread is not null &&
!ReferenceEquals(hostThread, Thread.CurrentThread)) !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 else
{ {
@@ -3446,7 +3506,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
out ulong mappedBase, out ulong mappedBase,
out string? error) out string? error)
{ {
for (int i = 0; i < 64; i++) for (int i = 0; i < GuestThreadRegionSlotCount; i++)
{ {
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride); var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size)) if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
@@ -3480,7 +3540,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
out ulong tlsBase, out ulong tlsBase,
out string? error) out string? error)
{ {
for (int i = 0; i < 64; i++) for (int i = 0; i < GuestThreadRegionSlotCount; i++)
{ {
var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride); var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride);
var mappedBase = candidateBase - GuestThreadTlsPrefixSize; var mappedBase = candidateBase - GuestThreadTlsPrefixSize;
+3 -1
View File
@@ -25,6 +25,7 @@ namespace SharpEmu.GUI;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private const int MaxConsoleLines = 4000; private const int MaxConsoleLines = 4000;
private const int MaxConsoleLinesPerFlush = 500;
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE")); private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488")); private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
@@ -1504,7 +1505,8 @@ public partial class MainWindow : Window
} }
var incoming = new List<LogLine>(); var incoming = new List<LogLine>();
while (_pendingLines.TryDequeue(out var pending)) while (incoming.Count < MaxConsoleLinesPerFlush &&
_pendingLines.TryDequeue(out var pending))
{ {
WriteFileLog(pending.Line); WriteFileLog(pending.Line);
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line))); incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
+37
View File
@@ -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);
}
}
+1 -1
View File
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
{ {
var userId = unchecked((int)ctx[CpuRegister.Rdi]); var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi]; 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); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
+46
View File
@@ -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);
}
+52
View File
@@ -78,6 +78,58 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal)) if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
@@ -80,14 +80,8 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress); // Completion output was written when the command was submitted.
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
{
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);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -169,27 +163,6 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result); 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( private static void TraceApr(
CpuContext ctx, CpuContext ctx,
string operation, string operation,
@@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports
{ {
private const int KernelEventSize = 0x20; private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14; public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterUser = -11;
public const short KernelEventFilterAmpr = -16; public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17; public const short KernelEventFilterAmprSystem = -17;
@@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx) public static int KernelAddUserEventEdge(CpuContext ctx)
{ {
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx) public static int KernelAddUserEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx) public static int KernelDeleteUserEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx) public static int KernelTriggerUserEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports
return triggeredCount; 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<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
flags,
fflags,
data,
registration.UserData));
}
WakeEventQueue(handle);
return true;
}
public static bool TriggerDisplayEvent( public static bool TriggerDisplayEvent(
ulong handle, ulong handle,
ulong ident, ulong ident,
@@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip); _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine( 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}");
} }
} }
@@ -1683,6 +1683,30 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "E6ao34wPw+U",
ExportName = "stat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixStat(CpuContext ctx)
{
var result = KernelStat(ctx);
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return 0;
}
var errno = result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
_ => 2,
};
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
[SysAbiExport( [SysAbiExport(
Nid = "gEpBkcwxUjw", Nid = "gEpBkcwxUjw",
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes", ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
@@ -3480,6 +3504,18 @@ public static class KernelMemoryCompatExports
return true; return true;
} }
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
{
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
{
rendered = format;
return false;
}
rendered = FormatString(ctx, format, ref vaCursor);
return true;
}
private static int WriteSnprintfOutput( private static int WriteSnprintfOutput(
CpuContext ctx, CpuContext ctx,
ulong destination, ulong destination,
@@ -3,6 +3,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel; 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. // type initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextMutexWakeId; private static long _nextMutexWakeId;
private static long _nextCondWakeId; private static long _nextCondWakeId;
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
private sealed class PthreadMutexState private sealed class PthreadMutexState
{ {
@@ -137,7 +140,7 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx) public static int PthreadYield(CpuContext ctx)
{ {
_ = ctx; GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
Thread.Yield(); Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -351,29 +354,95 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx) public static int PosixPthreadCondTimedwait(CpuContext ctx)
{ {
// POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113). var absoluteTimeAddress = ctx[CpuRegister.Rdx];
var abstimeAddress = ctx[CpuRegister.Rdx]; if (absoluteTimeAddress == 0 ||
if (abstimeAddress == 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) || var deadlineSeconds = unchecked((long)secondsRaw);
!ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds)) 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; long nowSeconds;
var deadlineMicroseconds = long nowNanoseconds;
deadlineSeconds >= long.MaxValue / 1_000_000UL if (deadlineSeconds >= 1_000_000_000)
? long.MaxValue {
: (long)(deadlineSeconds * 1_000_000UL + Math.Min(deadlineNanoseconds, 999_999_999UL) / 1000UL); var now = DateTimeOffset.UtcNow;
var remainingMicroseconds = deadlineMicroseconds - nowMicroseconds; nowSeconds = now.ToUnixTimeSeconds();
var timeoutUsec = remainingMicroseconds <= 0 nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
? 1u }
: (uint)Math.Min(remainingMicroseconds, uint.MaxValue); else
return PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: timeoutUsec); {
// 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( [SysAbiExport(
@@ -654,11 +723,12 @@ public static class KernelPthreadCompatExports
var acquired = state.Semaphore.Wait(0); var acquired = state.Semaphore.Wait(0);
if (!acquired) if (!acquired)
{ {
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId }; var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Cooperative deschedule on a contended lock corrupts fiber state // Fibers retain the synchronous fallback to preserve switch state.
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
// synchronous host-thread wait below. var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
if (_enableMutexLockBlocking && if (canCooperativelyBlock &&
!tryOnly && !tryOnly &&
GuestThreadExecution.IsGuestThread && GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) && GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
@@ -697,6 +767,53 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
{ {
if (mutexAddress == 0) if (mutexAddress == 0)
@@ -1076,6 +1193,13 @@ public static class KernelPthreadCompatExports
lock (_stateGate) lock (_stateGate)
{ {
if (_condStates.TryGetValue(condAddress, out var raced))
{
resolvedAddress = condAddress;
state = raced;
return true;
}
_condStates[condAddress] = createdState; _condStates[condAddress] = createdState;
_condStates[handle] = createdState; _condStates[handle] = createdState;
} }
@@ -1172,7 +1296,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) if (condAddress == 0 || mutexAddress == 0)
{ {
@@ -1210,9 +1340,10 @@ public static class KernelPthreadCompatExports
if (consumedPendingSignal) 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); state.Waiters = Math.Max(0, state.Waiters - 1);
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult); TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
// Relock outside SyncRoot to preserve lock ordering.
Monitor.Exit(state.SyncRoot); Monitor.Exit(state.SyncRoot);
try try
{ {
@@ -1221,6 +1352,7 @@ public static class KernelPthreadCompatExports
} }
finally finally
{ {
// Balance the surrounding lock statement.
Monitor.Enter(state.SyncRoot); Monitor.Enter(state.SyncRoot);
} }
} }
@@ -1231,7 +1363,7 @@ public static class KernelPthreadCompatExports
ctx, ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait", timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey, state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion), () => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
() => state.SignalEpoch != observedEpoch, () => state.SignalEpoch != observedEpoch,
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0)) timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
{ {
@@ -1315,7 +1447,7 @@ public static class KernelPthreadCompatExports
state, state,
timed, timed,
waitResult); waitResult);
return waitResult; return TranslatePthreadResult(waitResult, posixResult);
} }
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast) private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
@@ -1372,7 +1504,8 @@ public static class KernelPthreadCompatExports
PthreadCondState state, PthreadCondState state,
ulong observedEpoch, ulong observedEpoch,
bool timed, bool timed,
int releasedRecursion) int releasedRecursion,
bool posixResult)
{ {
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK; var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
lock (state.SyncRoot) lock (state.SyncRoot)
@@ -1393,7 +1526,32 @@ public static class KernelPthreadCompatExports
} }
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion); 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) => private static string GetCondWakeKey(ulong resolvedCondAddress) =>
@@ -1559,7 +1717,10 @@ public static class KernelPthreadCompatExports
return TimeSpan.Zero; 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() private static TimeSpan GetCondSpuriousWakeTimeout()
@@ -1701,7 +1862,8 @@ public static class KernelPthreadCompatExports
} }
Console.Error.WriteLine( 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}"); $"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
} }
@@ -85,22 +85,29 @@ public static class KernelRuntimeCompatExports
ExportName = "sceKernelNanosleep", ExportName = "sceKernelNanosleep",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelNanosleep(CpuContext ctx) public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
[SysAbiExport(
Nid = "yS8U2TGCe1A",
ExportName = "nanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
private static int NanosleepCore(CpuContext ctx, bool posix)
{ {
var requestAddress = ctx[CpuRegister.Rdi]; var requestAddress = ctx[CpuRegister.Rdi];
var remainAddress = ctx[CpuRegister.Rsi]; var remainAddress = ctx[CpuRegister.Rsi];
if (requestAddress == 0) if (requestAddress == 0)
{ {
ctx[CpuRegister.Rax] = Einval; return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
Span<byte> timespecBuffer = stackalloc byte[16]; Span<byte> timespecBuffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer)) if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
{ {
ctx[CpuRegister.Rax] = Efault; return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer); var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
@@ -108,8 +115,7 @@ public static class KernelRuntimeCompatExports
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L) if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
{ {
ctx[CpuRegister.Rax] = Einval; return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
if (tvSec == 0 && tvNsec == 0) if (tvSec == 0 && tvNsec == 0)
@@ -119,7 +125,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep"); GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to // TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
// a single tick rather than collapsing to a zero-length (no-op) sleep. // a single tick rather than collapsing to a zero-length (no-op) sleep.
@@ -138,6 +144,21 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
{
if (posix)
{
TrySetErrno(ctx, errnoValue);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
}
else
{
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
}
return (int)sceResult;
}
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds) private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
{ {
if (remainAddress == 0) if (remainAddress == 0)
@@ -192,6 +213,13 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{ {
if (!_traceUsleep) if (!_traceUsleep)
@@ -403,6 +431,18 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "HoLVWNanBBc",
ExportName = "getpid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetProcessId(CpuContext ctx)
{
var processId = Environment.ProcessId;
ctx[CpuRegister.Rax] = unchecked((uint)processId);
return processId;
}
[SysAbiExport( [SysAbiExport(
Nid = "fgxnMeTNUtY", Nid = "fgxnMeTNUtY",
ExportName = "sceKernelGetProcessTimeCounter", ExportName = "sceKernelGetProcessTimeCounter",
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new(); private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1; private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState private sealed class KernelSemaphoreState
{ {
public required string Name { get; init; } public required string Name { get; init; }
@@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports
{ {
public required int NeedCount { get; init; } public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; } public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Written and read only under the owning semaphore's Gate. // Written and read only under the owning semaphore's Gate.
public int? Result { get; set; } public int? Result { get; set; }
@@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var pollTimedOut = false;
lock (semaphore.Gate) lock (semaphore.Gate)
{ {
if (semaphore.Count >= needCount) if (semaphore.Count >= needCount)
@@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0) if (timeoutAddress != 0)
{ {
if (!ctx.TryReadUInt32(timeoutAddress, out _)) if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
{ {
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
_ = ctx.TryWriteUInt32(timeoutAddress, 0); if (timeoutMicros == 0)
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); {
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); _ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
} }
var waiter = new SemaphoreWaiter if (!pollTimedOut)
{ {
NeedCount = needCount, var waiter = new SemaphoreWaiter
CancelEpochAtBlock = semaphore.CancelEpoch, {
}; NeedCount = needCount,
if (!GuestThreadExecution.RequestCurrentThreadBlock( CancelEpochAtBlock = semaphore.CancelEpoch,
ctx, };
"sceKernelWaitSema", if (!GuestThreadExecution.RequestCurrentThreadBlock(
GetSemaphoreWakeKey(handle), ctx,
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter), "sceKernelWaitSema",
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter))) GetSemaphoreWakeKey(handle),
{ resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); {
} TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++; semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
} }
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
} }
[SysAbiExport( [SysAbiExport(
@@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports
} }
} }
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
{
int result;
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
result = waiter.Result!.Value;
}
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return result;
}
private static void TraceSemaphore(string message) private static void TraceSemaphore(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
+353
View File
@@ -338,6 +338,359 @@ public static class LibcStdioExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "8Q60JLJ6Rv4",
ExportName = "getc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
[SysAbiExport(
Nid = "AEuF3F2f8TA",
ExportName = "fgetc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
private static int FgetcCore(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
var value = stream.ReadByte();
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "LxcEU+ICu8U",
ExportName = "feof",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Feof(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
stream.CanRead && stream.Position >= stream.Length;
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AHxyhN96dy4",
ExportName = "ferror",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ferror(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3QIPIh-GDjw",
ExportName = "rewind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Rewind(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Seek(0, SeekOrigin.Begin);
}
catch (IOException)
{
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "aZK8lNei-Qw",
ExportName = "fputc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fputc(CpuContext ctx)
{
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
var handle = ctx[CpuRegister.Rsi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.WriteByte(character);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
// mirror the fputs HLE and forward them to the host console.
Console.Out.Write((char)character);
}
ctx[CpuRegister.Rax] = character;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MpxhMh8QFro",
ExportName = "fwrite",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fwrite(CpuContext ctx)
{
var source = ctx[CpuRegister.Rdi];
var elementSize = ctx[CpuRegister.Rsi];
var elementCount = ctx[CpuRegister.Rdx];
var handle = ctx[CpuRegister.Rcx];
if (elementSize == 0 || elementCount == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
if (source == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var totalRequested = elementSize * elementCount;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
ulong totalWritten = 0;
try
{
while (totalWritten < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (knownHandle)
{
stream!.Write(buffer, 0, request);
}
else
{
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
}
totalWritten += (ulong)request;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MUjC4lbHrK4",
ExportName = "fflush",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fflush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
try
{
if (handle == 0)
{
foreach (var stream in _fileHandles.Values)
{
stream.Flush();
}
}
else if (_fileHandles.TryGetValue(handle, out var stream))
{
stream.Flush();
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fffwELXNVFA",
ExportName = "fprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteRenderedText(ctx, handle, rendered);
}
[SysAbiExport(
Nid = "pDBDcY6uLSA",
ExportName = "vfprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vfprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
return WriteRenderedText(ctx, handle, rendered);
}
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
{
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Write(payload, 0, payload.Length);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
Console.Out.Write(rendered);
}
ctx[CpuRegister.Rax] = (ulong)payload.Length;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gkWgn0p1AfU",
ExportName = "freopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Freopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
var handle = ctx[CpuRegister.Rdx];
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (_fileHandles.TryRemove(handle, out var previousStream))
{
previousStream.Dispose();
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (fileAccess != FileAccess.Read)
{
var parentDirectory = Path.GetDirectoryName(hostPath);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
Directory.CreateDirectory(parentDirectory);
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
}
ctx[CpuRegister.Rax] = handle;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
}
ctx[CpuRegister.Rax] = 0;
return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
[SysAbiExport( [SysAbiExport(
Nid = "sUP1hBaouOw", Nid = "sUP1hBaouOw",
ExportName = "_Getpctype", ExportName = "_Getpctype",
+39 -2
View File
@@ -22,6 +22,17 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "S7QTn72PrDw",
ExportName = "sceNpDeleteRequest",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpDeleteRequest(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "JELHf4xPufo", Nid = "JELHf4xPufo",
ExportName = "sceNpCheckCallbackForLib", ExportName = "sceNpCheckCallbackForLib",
@@ -42,8 +53,19 @@ public static class NpManagerExports
LibraryName = "libSceNpManager")] LibraryName = "libSceNpManager")]
public static int NpGetOnlineId(CpuContext ctx) public static int NpGetOnlineId(CpuContext ctx)
{ {
ctx[CpuRegister.Rax] = 0; // Gen5 ABI: user ID, then output structure.
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -158,4 +180,19 @@ public static class NpManagerExports
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}"); 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<byte> 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);
}
} }
+12
View File
@@ -31,6 +31,18 @@ public static class NpWebApi2Exports
return ctx.SetReturn(0); 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( [SysAbiExport(
Nid = "bEvXpcEk200", Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate", ExportName = "sceNpWebApi2Terminate",
+67 -20
View File
@@ -14,11 +14,18 @@ public static class PadExports
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005); private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007); private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008); 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 StandardPortType = 0;
private const int PrimaryPadHandle = 1; private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C; private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78; 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; private static bool _initialized;
@@ -178,6 +185,16 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport(
Nid = "2JgFB2n9oUM",
ExportName = "scePadSetTriggerEffect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetTriggerEffect(CpuContext ctx)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport( [SysAbiExport(
Nid = "yFVnOdGxvZY", Nid = "yFVnOdGxvZY",
ExportName = "scePadSetVibration", ExportName = "scePadSetVibration",
@@ -260,6 +277,44 @@ public static class PadExports
{ {
Span<byte> data = stackalloc byte[PadDataSize]; Span<byte> data = stackalloc byte[PadDataSize];
data.Clear(); 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 acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0; var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128; var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
@@ -293,25 +348,17 @@ public static class PadExports
r2 = Math.Max(r2, xpad.R2); r2 = Math.Max(r2, xpad.R2);
} }
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons); _cachedInputState = new PadState(
data[0x04] = leftX; Connected: true,
data[0x05] = leftY; Buttons: buttons,
data[0x06] = rightX; LeftX: leftX,
data[0x07] = rightY; LeftY: leftY,
data[0x08] = l2; RightX: rightX,
data[0x09] = r2; RightY: rightY,
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f); L2: l2,
data[0x4C] = 1; R2: r2);
var timestampTicks = Stopwatch.GetTimestamp(); _lastInputSampleTicks = now;
var timestampMicroseconds = return _cachedInputState;
((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);
} }
[DllImport("user32.dll")] [DllImport("user32.dll")]
+3 -1
View File
@@ -682,7 +682,9 @@ public static class PlayGoExports
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml); var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata) 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); var chunkIds = LoadChunkIds(chunkDefsXml);
+23 -13
View File
@@ -30,13 +30,17 @@ public static class SaveDataExports
private const uint MountModeCreate2 = 1u << 5; private const uint MountModeCreate2 = 1u << 5;
private const int MountResultSize = 0x40; private const int MountResultSize = 0x40;
private static readonly object _stateGate = new(); private static readonly object _stateGate = new();
private static readonly HashSet<int> _transactionResources = [];
private static string? _titleId; private static string? _titleId;
private static int _nextTransactionResource;
public static void ConfigureApplicationInfo(string? titleId) public static void ConfigureApplicationInfo(string? titleId)
{ {
lock (_stateGate) lock (_stateGate)
{ {
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim()); _titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
_transactionResources.Clear();
_nextTransactionResource = 0;
} }
} }
@@ -244,7 +248,6 @@ public static class SaveDataExports
} }
} }
private static int _nextTransactionResource;
[SysAbiExport( [SysAbiExport(
Nid = "gjRZNnw0JPE", Nid = "gjRZNnw0JPE",
ExportName = "sceSaveDataCreateTransactionResource", ExportName = "sceSaveDataCreateTransactionResource",
@@ -252,25 +255,32 @@ public static class SaveDataExports
LibraryName = "libSceSaveData")] LibraryName = "libSceSaveData")]
public static int SaveDataCreateTransactionResource(CpuContext ctx) public static int SaveDataCreateTransactionResource(CpuContext ctx)
{ {
var userId = unchecked((int)ctx[CpuRegister.Rdi]); var memorySize = ctx[CpuRegister.Rdi];
var reserved = ctx[CpuRegister.Rsi]; int resource;
var resourceAddress = ctx[CpuRegister.Rdx]; lock (_stateGate)
if (resourceAddress == 0)
{ {
return ctx.SetReturn(OrbisSaveDataErrorParameter); resource = ++_nextTransactionResource;
_transactionResources.Add(resource);
} }
var id = (uint)Interlocked.Increment(ref _nextTransactionResource); TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
return ctx.SetReturn(resource);
}
if (!ctx.TryWriteUInt32(resourceAddress, id)) [SysAbiExport(
Nid = "lJUQuaKqoKY",
ExportName = "sceSaveDataDeleteTransactionResource",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataDeleteTransactionResource(CpuContext ctx)
{
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{ {
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); _transactionResources.Remove(resource);
} }
TraceSaveData( TraceSaveData($"delete_transaction_resource resource={resource}");
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
@@ -13,7 +13,8 @@ public static class UserServiceExports
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007); private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009); private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A); 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 int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu"; private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered; private static int _loginEventDelivered;
@@ -111,7 +112,8 @@ public static class UserServiceExports
var userId = unchecked((int)ctx[CpuRegister.Rdi]); var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi]; var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx]; var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId) // Zero selects the current user.
if (userId != 0 && userId != PrimaryUserId)
{ {
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter); return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
} }
@@ -137,6 +139,28 @@ public static class UserServiceExports
return ctx.SetReturn(result); 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<byte> 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( [SysAbiExport(
Nid = "D-CzAxQL0XI", Nid = "D-CzAxQL0XI",
ExportName = "sceUserServiceGetPlatformPrivacySetting", ExportName = "sceUserServiceGetPlatformPrivacySetting",
+119 -36
View File
@@ -48,54 +48,106 @@ public static class VideoOutExports
private static readonly Dictionary<int, VideoOutPortState> _ports = new(); private static readonly Dictionary<int, VideoOutPortState> _ports = new();
// Hardware raises vblank autonomously; UE blocks its frame loop on it. // Hardware raises vblank autonomously; UE blocks its frame loop on it.
private static Timer? _vblankPumpTimer; private const double VblankHz = 60.0;
private static int _vblankPumpActive; private const int VblankWaitTimeoutMilliseconds = 100;
private const int VblankPumpIntervalMs = 16; 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() private static void EnsureVblankPumpStarted()
{ {
if (_vblankPumpTimer is not null) if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0)
{ {
return; return;
} }
_vblankPumpTimer = new Timer( HostTimerResolution.Request();
static _ => PumpVblanks(),
null, _vblankPumpThread = new Thread(VblankPumpLoop)
VblankPumpIntervalMs, {
VblankPumpIntervalMs); 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() 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; if (_ports.Count == 0)
lock (_stateGate)
{ {
if (_ports.Count == 0) return;
{
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();
} }
foreach (var port in ports) // Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
{ // flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
SignalVblank(port); 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 _nextHandle = 1;
private static int _frameDumpCount; private static int _frameDumpCount;
private static long _nextFrameDumpIndex; private static long _nextFrameDumpIndex;
private static string _windowTitleBase = "SharpEmu VideoOut";
private static string _windowTitle = "SharpEmu VideoOut"; private static string _windowTitle = "SharpEmu VideoOut";
private static readonly bool _logFrameRate = string.Equals( private static readonly bool _logFrameRate = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"), Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
@@ -112,6 +165,10 @@ public static class VideoOutExports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"), Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
"1", "1",
StringComparison.Ordinal); 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 _frameRateWindowStart = Stopwatch.GetTimestamp();
private static long _submittedFrameCount; private static long _submittedFrameCount;
private static long _presentedFrameCount; private static long _presentedFrameCount;
@@ -134,10 +191,24 @@ public static class VideoOutExports
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts); var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}"; var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.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) 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; return OrbisVideoOutErrorInvalidHandle;
} }
Thread.Sleep(1); EnsureVblankPumpStarted();
lock (_vblankEdgeGate)
{
var entryEdge = _vblankEdgeSequence;
while (_vblankEdgeSequence == entryEdge)
{
if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds))
{
break;
}
}
}
SignalVblank(port); SignalVblank(port);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -987,10 +1071,7 @@ public static class VideoOutExports
displayBuffer.PitchInPixel); displayBuffer.PitchInPixel);
} }
if (string.Equals( if (_dumpVideoOut)
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
StringComparison.Ordinal))
{ {
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg); _ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
} }
@@ -1050,9 +1131,11 @@ public static class VideoOutExports
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency; var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0); var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0); var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
var missedEdges = Interlocked.Exchange(ref _vblankMissedEdges, 0);
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " + $"[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<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1) private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
@@ -11,6 +11,7 @@ using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Windowing; using Silk.NET.Windowing;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using VkBuffer = Silk.NET.Vulkan.Buffer; using VkBuffer = Silk.NET.Vulkan.Buffer;
@@ -600,7 +601,8 @@ internal static unsafe class VulkanVideoPresenter
var traceSubmission = false; var traceSubmission = false;
lock (_gate) lock (_gate)
{ {
var known = _availableGuestImages.ContainsKey(address); // VideoOut registration does not imply a rendered Vulkan image.
var known = _gpuGuestImages.ContainsKey(address);
if (ShouldTracePresentedGuestImageContentsForDiagnostics()) if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -622,7 +624,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " + $"[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; return false;
@@ -933,6 +935,8 @@ internal static unsafe class VulkanVideoPresenter
private ExtDebugUtils? _debugUtils; private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice; private PhysicalDevice _physicalDevice;
private Device _device; private Device _device;
private PipelineCache _pipelineCache;
private string? _pipelineCachePath;
private Queue _queue; private Queue _queue;
private uint _queueFamilyIndex; private uint _queueFamilyIndex;
private SwapchainKHR _swapchain; private SwapchainKHR _swapchain;
@@ -950,6 +954,9 @@ internal static unsafe class VulkanVideoPresenter
private CommandBuffer _presentationCommandBuffer; private CommandBuffer _presentationCommandBuffer;
private VkSemaphore _imageAvailable; private VkSemaphore _imageAvailable;
private VkSemaphore _renderFinished; private VkSemaphore _renderFinished;
private Fence _presentationFence;
private bool _presentationInFlight;
private TranslatedDrawResources? _pendingPresentationResources;
private VkBuffer _stagingBuffer; private VkBuffer _stagingBuffer;
private DeviceMemory _stagingMemory; private DeviceMemory _stagingMemory;
private ulong _stagingSize; private ulong _stagingSize;
@@ -966,6 +973,10 @@ internal static unsafe class VulkanVideoPresenter
private int _directPresentationCount; private int _directPresentationCount;
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new(); private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = 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, Format Format)> _tracedTextureUploads = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new();
private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new(); private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new();
@@ -988,7 +999,7 @@ internal static unsafe class VulkanVideoPresenter
private readonly record struct GraphicsPipelineKey( private readonly record struct GraphicsPipelineKey(
string VertexShader, string VertexShader,
string FragmentShader, string FragmentShader,
ulong RenderPass, Format RenderTargetFormat,
PrimitiveTopology Topology, PrimitiveTopology Topology,
VulkanGuestBlendState Blend, VulkanGuestBlendState Blend,
string ResourceLayout, string ResourceLayout,
@@ -1150,6 +1161,7 @@ internal static unsafe class VulkanVideoPresenter
CreateSurface(); CreateSurface();
SelectPhysicalDevice(); SelectPhysicalDevice();
CreateDevice(); CreateDevice();
CreatePipelineCache();
CreateSwapchain(); CreateSwapchain();
CreateCommandResources(); CreateCommandResources();
CreateGuestDrawResources(); CreateGuestDrawResources();
@@ -1559,8 +1571,11 @@ internal static unsafe class VulkanVideoPresenter
} }
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected); _vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine( 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( 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() private void CreateSwapchain()
{ {
Check( 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 _imageAvailable), "vkCreateSemaphore");
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "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); 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() private CommandBuffer AllocateGuestCommandBuffer()
{ {
var allocateInfo = new CommandBufferAllocateInfo var allocateInfo = new CommandBufferAllocateInfo
@@ -2184,7 +2338,7 @@ internal static unsafe class VulkanVideoPresenter
Check( Check(
_vk.CreateGraphicsPipelines( _vk.CreateGraphicsPipelines(
_device, _device,
default, _pipelineCache,
1, 1,
&pipelineInfo, &pipelineInfo,
null, null,
@@ -2219,6 +2373,7 @@ internal static unsafe class VulkanVideoPresenter
private TranslatedDrawResources CreateTranslatedDrawResources( private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw, VulkanTranslatedGuestDraw draw,
RenderPass renderPass, RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent) Extent2D extent)
{ {
var vertexSpirv = draw.VertexSpirv; var vertexSpirv = draw.VertexSpirv;
@@ -2285,7 +2440,13 @@ internal static unsafe class VulkanVideoPresenter
CreateTranslatedDescriptorResources( CreateTranslatedDescriptorResources(
resources, resources,
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit); ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit);
CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent); CreateTranslatedPipeline(
resources,
vertexSpirv,
draw.PixelSpirv,
renderPass,
renderTargetFormat,
extent);
return resources; return resources;
} }
catch catch
@@ -2581,12 +2742,13 @@ internal static unsafe class VulkanVideoPresenter
byte[] vertexSpirv, byte[] vertexSpirv,
byte[] fragmentSpirv, byte[] fragmentSpirv,
RenderPass renderPass, RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent) Extent2D extent)
{ {
var pipelineKey = new GraphicsPipelineKey( var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv), GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv), GetShaderDigest(fragmentSpirv),
renderPass.Handle, renderTargetFormat,
resources.Topology, resources.Topology,
resources.Blend, resources.Blend,
GetResourceLayoutKey(resources), GetResourceLayoutKey(resources),
@@ -2742,7 +2904,7 @@ internal static unsafe class VulkanVideoPresenter
Check( Check(
_vk.CreateGraphicsPipelines( _vk.CreateGraphicsPipelines(
_device, _device,
default, _pipelineCache,
1, 1,
&pipelineInfo, &pipelineInfo,
null, null,
@@ -2922,7 +3084,7 @@ internal static unsafe class VulkanVideoPresenter
Check( Check(
_vk.CreateComputePipelines( _vk.CreateComputePipelines(
_device, _device,
default, _pipelineCache,
1, 1,
&pipelineInfo, &pipelineInfo,
null, null,
@@ -3448,11 +3610,36 @@ internal static unsafe class VulkanVideoPresenter
{ {
const ulong offsetBasis = 14695981039346656037; const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211; 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<byte, ulong>(pixels);
var index = 0;
var blockEnd = words.Length - (words.Length & 3);
for (; index < blockEnd; index += 4)
{ {
hash ^= value; h0 = (h0 ^ words[index]) * prime;
hash *= 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; return hash;
@@ -3465,10 +3652,7 @@ internal static unsafe class VulkanVideoPresenter
uint width, uint width,
uint height) uint height)
{ {
if (!string.Equals( if (!_dumpTextures ||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal) ||
texture.IsFallback || texture.IsFallback ||
texture.IsStorage || texture.IsStorage ||
GetTextureBytesPerPixel(texture.Format) != 4 || GetTextureBytesPerPixel(texture.Format) != 4 ||
@@ -4489,6 +4673,7 @@ internal static unsafe class VulkanVideoPresenter
resources = CreateTranslatedDrawResources( resources = CreateTranslatedDrawResources(
work.Draw, work.Draw,
target.RenderPass, target.RenderPass,
target.Format,
extent); extent);
resources.DebugName = resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " + $"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
@@ -5125,6 +5310,8 @@ internal static unsafe class VulkanVideoPresenter
return; return;
} }
CompletePendingPresentation(wait: true);
byte[]? pixels = null; byte[]? pixels = null;
if (presentation.Pixels is { } sourcePixels) if (presentation.Pixels is { } sourcePixels)
{ {
@@ -5172,6 +5359,7 @@ internal static unsafe class VulkanVideoPresenter
translatedResources = CreateTranslatedDrawResources( translatedResources = CreateTranslatedDrawResources(
translatedDraw, translatedDraw,
_renderPass, _renderPass,
_swapchainFormat,
_extent); _extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() && if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented) !_firstGuestDrawPresented)
@@ -5303,7 +5491,16 @@ internal static unsafe class VulkanVideoPresenter
SignalSemaphoreCount = 1, SignalSemaphoreCount = 1,
PSignalSemaphores = &renderFinished, 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 swapchain = _swapchain;
var presentInfo = new PresentInfoKHR var presentInfo = new PresentInfoKHR
@@ -5319,6 +5516,7 @@ internal static unsafe class VulkanVideoPresenter
if (presentResult == Result.ErrorOutOfDateKhr) if (presentResult == Result.ErrorOutOfDateKhr)
{ {
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null) if (translatedResources is not null)
{ {
@@ -5331,19 +5529,13 @@ internal static unsafe class VulkanVideoPresenter
CheckSwapchainResult(presentResult, "vkQueuePresentKHR"); CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
recreateAfterPresent |= presentResult == Result.SuboptimalKhr; recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
VideoOutExports.ReportPresentedFrame(); VideoOutExports.ReportPresentedFrame();
if (_swapchainReadbackPending) if (_swapchainReadbackPending)
{ {
CompletePendingPresentation(wait: true);
TraceSwapchainReadback(); TraceSwapchainReadback();
} }
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null)
{
MarkSampledImagesInitialized(translatedResources);
MarkStorageImagesInitialized(translatedResources);
DestroyTranslatedDrawResources(translatedResources);
}
_imageInitialized[imageIndex] = true; _imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence; _presentedSequence = presentation.Sequence;
@@ -6597,7 +6789,9 @@ internal static unsafe class VulkanVideoPresenter
} }
_vulkanReady = false; _vulkanReady = false;
_vk.DeviceWaitIdle(_device); _vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
SavePipelineCache();
foreach (var pipeline in _computePipelines.Values) foreach (var pipeline in _computePipelines.Values)
{ {
_vk.DestroyPipeline(_device, pipeline, null); _vk.DestroyPipeline(_device, pipeline, null);
@@ -6608,6 +6802,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.DestroyPipeline(_device, pipeline, null); _vk.DestroyPipeline(_device, pipeline, null);
} }
_graphicsPipelines.Clear(); _graphicsPipelines.Clear();
if (_pipelineCache.Handle != 0)
{
_vk.DestroyPipelineCache(_device, _pipelineCache, null);
_pipelineCache = default;
}
foreach (var layout in _descriptorLayouts.Values) foreach (var layout in _descriptorLayouts.Values)
{ {
_vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null); _vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null);
@@ -6699,6 +6898,7 @@ internal static unsafe class VulkanVideoPresenter
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}"); $"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}");
_vk.DeviceWaitIdle(_device); _vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
DestroySwapchainResources(); DestroySwapchainResources();
CreateSwapchain(); CreateSwapchain();
@@ -6732,6 +6932,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.DestroySemaphore(_device, _renderFinished, null); _vk.DestroySemaphore(_device, _renderFinished, null);
_renderFinished = default; _renderFinished = default;
} }
if (_presentationFence.Handle != 0)
{
_vk.DestroyFence(_device, _presentationFence, null);
_presentationFence = default;
}
if (_barycentricPipeline.Handle != 0) if (_barycentricPipeline.Handle != 0)
{ {
_vk.DestroyPipeline(_device, _barycentricPipeline, null); _vk.DestroyPipeline(_device, _barycentricPipeline, null);
@@ -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);
}
}
+32 -10
View File
@@ -6,19 +6,15 @@ using System.Text;
namespace SharpEmu.Logging; namespace SharpEmu.Logging;
/// <summary>
/// Writes log entries to a file. Thread-safe via an internal lock.
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
/// </summary>
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
{ {
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
private readonly object _sync = new(); private readonly object _sync = new();
private readonly StreamWriter _writer; private readonly StreamWriter _writer;
private readonly Timer _flushTimer;
private bool _disposed; private bool _disposed;
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
public FileLogSink(string path, bool append = true, bool includeTimestamp = true) public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(path); ArgumentException.ThrowIfNullOrWhiteSpace(path);
@@ -34,14 +30,20 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
append ? FileMode.Append : FileMode.Create, append ? FileMode.Append : FileMode.Create,
FileAccess.Write, FileAccess.Write,
FileShare.Read, FileShare.Read,
bufferSize: 4096, bufferSize: 65536,
FileOptions.SequentialScan); FileOptions.SequentialScan);
_writer = new StreamWriter(fileStream, Encoding.UTF8) _writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536)
{ {
AutoFlush = true AutoFlush = false
}; };
IncludeTimestamp = includeTimestamp; IncludeTimestamp = includeTimestamp;
_flushTimer = new Timer(
static state => ((FileLogSink)state!).FlushBuffered(),
this,
FlushInterval,
FlushInterval);
} }
public bool IncludeTimestamp { get; set; } public bool IncludeTimestamp { get; set; }
@@ -84,11 +86,31 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
{ {
_writer.WriteLine(entry.Exception); _writer.WriteLine(entry.Exception);
} }
if (entry.Level >= LogLevel.Error)
{
_writer.Flush();
}
}
}
private void FlushBuffered()
{
lock (_sync)
{
if (_disposed)
{
return;
}
_writer.Flush();
} }
} }
public void Dispose() public void Dispose()
{ {
_flushTimer.Dispose();
lock (_sync) lock (_sync)
{ {
if (_disposed) if (_disposed)
+12 -3
View File
@@ -36,11 +36,16 @@ public static class HostSystemInfo
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name)) if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
{ {
return Regex.Replace( name = Regex.Replace(
name.Trim(), name.Trim(),
@"\s+\d+-Core Processor$", @"\s+\d+-Core Processor$",
string.Empty, string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return Regex.Replace(
name,
@"\s+with Radeon Graphics$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
} }
} }
catch (Exception) catch (Exception)
@@ -103,8 +108,12 @@ public static class HostSystemInfo
} }
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) || if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase) || name.Contains("geforce", StringComparison.OrdinalIgnoreCase))
name.Contains("amd", StringComparison.OrdinalIgnoreCase) || {
return 3;
}
if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase)) name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
{ {
return 2; return 2;