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