mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be6a6a5935 | |||
| caf859cc52 | |||
| d2f3511002 | |||
| 90a5d5176f | |||
| 28a43e09c7 | |||
| 093cfa1f3e | |||
| d8397b022e | |||
| 85cc2b9892 | |||
| 293194c40b | |||
| 1f09de8896 | |||
| ddc452b4fc | |||
| 61a97baf85 | |||
| e80f96ecf5 | |||
| d49c0f1f10 | |||
| 1d33ef90fc | |||
| 26a570633c | |||
| e4f89445b9 | |||
| 1254cc1564 | |||
| 503b3f4d6b | |||
| 6b37ab54f2 | |||
| a84d2344fb | |||
| 787d3a1efb | |||
| 884584da67 | |||
| d43edc865a |
Binary file not shown.
|
After Width: | Height: | Size: 698 B |
Binary file not shown.
|
After Width: | Height: | Size: 802 B |
@@ -5,6 +5,7 @@ using SharpEmu.Core.Runtime;
|
|||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.GUI;
|
using SharpEmu.GUI;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -111,8 +112,16 @@ internal static partial class Program
|
|||||||
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
|
||||||
|
|
||||||
OrbisGen2Result result;
|
OrbisGen2Result result;
|
||||||
|
ConsoleCancelEventHandler? cancelHandler = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
cancelHandler = (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
eventArgs.Cancel = true;
|
||||||
|
VideoOutExports.NotifyHostInterrupt();
|
||||||
|
};
|
||||||
|
Console.CancelKeyPress += cancelHandler;
|
||||||
|
|
||||||
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
|
||||||
result = runtime.Run(ebootPath);
|
result = runtime.Run(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
Console.Error.WriteLine($"[DEBUG] Result: {result}");
|
||||||
@@ -123,6 +132,13 @@ internal static partial class Program
|
|||||||
Log.Error("SharpEmu failed to run.", ex);
|
Log.Error("SharpEmu failed to run.", ex);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (cancelHandler is not null)
|
||||||
|
{
|
||||||
|
Console.CancelKeyPress -= cancelHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
|
||||||
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
|
||||||
|
|||||||
@@ -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];
|
||||||
@@ -238,6 +238,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 0uL;
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
return 0uL;
|
return 0uL;
|
||||||
}
|
}
|
||||||
|
if (_hostShutdownRequested)
|
||||||
|
{
|
||||||
|
if (isGuestWorker &&
|
||||||
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "host shutdown"))
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
|
return 0uL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGuestWorker &&
|
||||||
|
TryAbortGuestForHostShutdown(argPackPtr, num, num7))
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = 1uL;
|
||||||
|
return 1uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||||
@@ -245,12 +261,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 +328,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 +366,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 +398,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 +407,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 +752,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 +767,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!expectedFileProbeMiss &&
|
if (!expectedFileProbeMiss &&
|
||||||
!expectedTimedWaitTimeout &&
|
!expectedTimedWaitTimeout &&
|
||||||
!expectedEqueueTimeout &&
|
!expectedEqueueTimeout &&
|
||||||
|
!expectedEventFlagTimeout &&
|
||||||
!expectedMutexTrylockBusy &&
|
!expectedMutexTrylockBusy &&
|
||||||
!expectedUserServiceNoEvent &&
|
!expectedUserServiceNoEvent &&
|
||||||
!expectedPrivacyInvalidParameter)
|
!expectedPrivacyInvalidParameter)
|
||||||
@@ -810,14 +830,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||||
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
|
||||||
// slows guest spinlocks enough to livelock (Demon's Souls).
|
|
||||||
return nid is
|
return nid is
|
||||||
|
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||||
|
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"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
|
||||||
@@ -876,7 +897,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"Q2V+iqvjgC0" or // vsnprintf
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
"j4ViWNHEgww" or // strlen
|
"j4ViWNHEgww" or // strlen
|
||||||
"5jNubw4vlAA" or // strnlen
|
"5jNubw4vlAA" or // strnlen
|
||||||
"LHMrG7e8G78" or // wcslen
|
"LHMrG7e8G78" or // wcsmisc
|
||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
@@ -970,6 +991,31 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryAbortGuestForHostShutdown(nint argPackPtr, long dispatchIndex, ulong returnRip)
|
||||||
|
{
|
||||||
|
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||||
|
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
*(ulong*)(argPackPtr + 96) = hostExit;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActiveForcedGuestExit = true;
|
||||||
|
if (string.IsNullOrWhiteSpace(LastError))
|
||||||
|
{
|
||||||
|
LastError = "Host shutdown requested.";
|
||||||
|
}
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Guest unwind for host shutdown at import#{dispatchIndex} ret=0x{returnRip:X16} -> host_exit=0x{hostExit:X16}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
|
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
|
||||||
{
|
{
|
||||||
ulong hostExit = ActiveEntryReturnSentinelRip;
|
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||||
@@ -1076,9 +1122,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
|
||||||
string.Equals(nid, "Zxa0VhQVTsk", StringComparison.Ordinal) ||
|
{
|
||||||
string.Equals(nid, "T72hz6ffq08", StringComparison.Ordinal);
|
"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;
|
||||||
@@ -530,6 +576,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// thread entering a freed stub).
|
// thread entering a freed stub).
|
||||||
private volatile bool _guestTeardownRequested;
|
private volatile bool _guestTeardownRequested;
|
||||||
|
|
||||||
|
private volatile bool _hostShutdownRequested;
|
||||||
|
|
||||||
|
private static volatile DirectExecutionBackend? _activeSessionBackend;
|
||||||
|
|
||||||
private int _readyGuestThreadCount;
|
private int _readyGuestThreadCount;
|
||||||
|
|
||||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||||
@@ -766,7 +816,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private bool ActiveForcedGuestExit
|
private bool ActiveForcedGuestExit
|
||||||
{
|
{
|
||||||
get => HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit;
|
get => _hostShutdownRequested ||
|
||||||
|
(HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit);
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (HasActiveExecutionThread)
|
if (HasActiveExecutionThread)
|
||||||
@@ -928,7 +979,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||||
_entryReturnSentinelRip = 0uL;
|
_entryReturnSentinelRip = 0uL;
|
||||||
_forcedGuestExit = false;
|
_forcedGuestExit = false;
|
||||||
|
_hostShutdownRequested = false;
|
||||||
_guestTeardownRequested = false;
|
_guestTeardownRequested = false;
|
||||||
|
_activeSessionBackend = this;
|
||||||
|
HostSessionControl.SetShutdownHandler(RequestHostShutdown);
|
||||||
_importLoopSignatureCount = 0;
|
_importLoopSignatureCount = 0;
|
||||||
_importLoopSignatureWriteIndex = 0;
|
_importLoopSignatureWriteIndex = 0;
|
||||||
_importLoopPatternHits = 0;
|
_importLoopPatternHits = 0;
|
||||||
@@ -976,12 +1030,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
HostSessionControl.SetShutdownHandler(null);
|
||||||
|
if (ReferenceEquals(_activeSessionBackend, this))
|
||||||
|
{
|
||||||
|
_activeSessionBackend = null;
|
||||||
|
}
|
||||||
DrainDeferredBootstrapTraces();
|
DrainDeferredBootstrapTraces();
|
||||||
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
|
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
|
||||||
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
|
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void RequestHostShutdown(string reason)
|
||||||
|
{
|
||||||
|
_hostShutdownRequested = true;
|
||||||
|
_forcedGuestExit = true;
|
||||||
|
_guestTeardownRequested = true;
|
||||||
|
LastError = string.IsNullOrWhiteSpace(reason)
|
||||||
|
? "Host shutdown requested."
|
||||||
|
: $"Host shutdown requested: {reason}";
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {LastError}");
|
||||||
|
}
|
||||||
|
|
||||||
private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs)
|
private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
|
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
|
||||||
@@ -2757,7 +2827,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 +3530,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 +3564,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;
|
||||||
@@ -4365,7 +4449,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
||||||
if (string.IsNullOrEmpty(LastError))
|
if (string.IsNullOrEmpty(LastError))
|
||||||
{
|
{
|
||||||
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
LastError = _hostShutdownRequested
|
||||||
|
? "Host shutdown requested."
|
||||||
|
: "Detected repeating import loop and forced guest unwind to host.";
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||||
RequestGuestThreadTeardown(3000);
|
RequestGuestThreadTeardown(3000);
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(data);
|
ReadOnlyMemory<byte> json = data;
|
||||||
|
if (json.Span.StartsWith("\uFEFF"u8))
|
||||||
|
{
|
||||||
|
json = json[3..];
|
||||||
|
}
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
return TryReadPs5Param(doc.RootElement);
|
return TryReadPs5Param(doc.RootElement);
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
||||||
{
|
{
|
||||||
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
|
if (root.ValueKind != JsonValueKind.Object)
|
||||||
|
return (null, null, null);
|
||||||
|
|
||||||
|
var titleId = GetString(root, "titleId");
|
||||||
|
|
||||||
string? ver =
|
string? ver =
|
||||||
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
GetString(root, "contentVersion")
|
||||||
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
?? GetString(root, "masterVersion")
|
||||||
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
?? GetString(root, "targetContentVersion");
|
||||||
|
|
||||||
string? title = ExtractTitleName(root);
|
string? title = ExtractTitleName(root);
|
||||||
|
|
||||||
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
|
|||||||
|
|
||||||
private static string? ExtractTitleName(JsonElement root)
|
private static string? ExtractTitleName(JsonElement root)
|
||||||
{
|
{
|
||||||
if (!root.TryGetProperty("localizedParameters", out var lp))
|
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
|
||||||
|
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
disc.TryGetProperty("localizedParameters", out lp);
|
||||||
{
|
|
||||||
disc.TryGetProperty("localizedParameters", out lp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lp.ValueKind != JsonValueKind.Object)
|
if (lp.ValueKind != JsonValueKind.Object)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
var defLang = GetString(lp, "defaultLanguage");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(defLang))
|
if (!string.IsNullOrEmpty(defLang))
|
||||||
{
|
{
|
||||||
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (langObj.TryGetProperty("titleName", out var tn))
|
var title = GetString(langObj, "titleName");
|
||||||
return tn.GetString();
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
if (en.TryGetProperty("titleName", out var tn2))
|
var title = GetString(en, "titleName");
|
||||||
return tn2.GetString();
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in lp.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (property.Value.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
var title = GetString(property.Value, "titleName");
|
||||||
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
return title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? GetString(JsonElement parent, string propertyName) =>
|
||||||
|
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
|
||||||
|
? value.GetString()
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -550,6 +550,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
|
||||||
|
{
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var region = FindRegion(virtualAddress, (ulong)expected.Length);
|
||||||
|
if (region is null ||
|
||||||
|
!TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)expected.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expected.IsEmpty)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (region.IsReservedOnly &&
|
||||||
|
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
var requiresExclusiveAccess = false;
|
var requiresExclusiveAccess = false;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "العربية",
|
||||||
|
|
||||||
|
"Page.Library": "المكتبة",
|
||||||
|
"Page.Options": "الخيارات",
|
||||||
|
"Page.GameCount.One": "لعبة واحدة",
|
||||||
|
"Page.GameCount.Other": "{0} لعبة",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "ابحث في المكتبة...",
|
||||||
|
"Library.AddFolder": "+ إضافة مجلد",
|
||||||
|
"Library.Rescan": "⟳ إعادة الفحص",
|
||||||
|
"Library.OpenFile": "فتح ملف...",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "تشغيل",
|
||||||
|
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
|
||||||
|
"Library.Context.CopyPath": "نسخ المسار",
|
||||||
|
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
|
||||||
|
"Library.Context.Remove": "إزالة من المكتبة",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "مكتبتك فارغة",
|
||||||
|
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
|
||||||
|
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
|
||||||
|
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
|
||||||
|
|
||||||
|
"Library.Loading": "جارٍ تحميل المكتبة...",
|
||||||
|
|
||||||
|
"Options.General": "عام",
|
||||||
|
"Options.Section.Emulation": "المحاكاة",
|
||||||
|
"Options.Section.Logging": "التسجيل",
|
||||||
|
"Options.Section.Launcher": "المُشغِّل",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "محرك المعالج",
|
||||||
|
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
|
||||||
|
"Options.CpuEngine.Native": "أصلي",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
|
||||||
|
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "مستوى التسجيل",
|
||||||
|
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
|
||||||
|
"Options.LogLevel.Trace": "تتبع",
|
||||||
|
"Options.LogLevel.Debug": "تصحيح الأخطاء",
|
||||||
|
"Options.LogLevel.Info": "معلومات",
|
||||||
|
"Options.LogLevel.Warning": "تحذير",
|
||||||
|
"Options.LogLevel.Error": "خطأ",
|
||||||
|
"Options.LogLevel.Critical": "حرج",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "حد تتبع الاستيراد",
|
||||||
|
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "التسجيل في ملف",
|
||||||
|
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "مسار ملف السجل",
|
||||||
|
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
|
||||||
|
"Options.LogFilePath.Select": "تحديد...",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
|
||||||
|
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "موسيقى اللعبة",
|
||||||
|
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "حالة دسكورد",
|
||||||
|
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "لغة المحاكي",
|
||||||
|
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
|
||||||
|
|
||||||
|
"Common.On": "تشغيل",
|
||||||
|
"Common.Off": "إيقاف",
|
||||||
|
|
||||||
|
"Console.Title": "نافذة السجلات",
|
||||||
|
"Console.SearchWatermark": "بحث...",
|
||||||
|
"Console.AutoScroll": "تمرير تلقائي",
|
||||||
|
"Console.Split": "تقسيم",
|
||||||
|
"Console.Copy": "نسخ",
|
||||||
|
"Console.Clear": "مسح",
|
||||||
|
"Console.WindowTitle": "نافذة سجلات SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
|
||||||
|
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
|
||||||
|
"Launch.Idle": "خامل",
|
||||||
|
"Launch.Console": "≡ نافذة السجلات",
|
||||||
|
"Launch.Launch": "▶ تشغيل",
|
||||||
|
"Launch.Stop": "■ إيقاف",
|
||||||
|
"Launch.Running": "قيد التشغيل — {0}",
|
||||||
|
"Launch.Stopping": "جارٍ الإيقاف...",
|
||||||
|
"Launch.Exited": "انتهى برمز {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
|
||||||
|
"Launch.LogFile": "ملف السجل: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
|
||||||
|
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "موافق",
|
||||||
|
"Exit.InvalidArguments": "معطيات غير صالحة",
|
||||||
|
"Exit.EbootNotFound": "لم يُعثر على eboot",
|
||||||
|
"Exit.RuntimeException": "استثناء وقت التشغيل",
|
||||||
|
"Exit.EmulationError": "خطأ في المحاكاة",
|
||||||
|
"Exit.Unknown": "غير معروف",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
|
||||||
|
"Status.EmulatorPath": "المحاكي: {0}",
|
||||||
|
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
|
||||||
|
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
|
||||||
|
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
|
||||||
|
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
|
||||||
|
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
|
||||||
|
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
|
||||||
|
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
|
||||||
|
"Status.Running": "جارٍ تشغيل {0}",
|
||||||
|
"Status.Stopping": "جارٍ الإيقاف...",
|
||||||
|
"Status.Idle": "خامل",
|
||||||
|
|
||||||
|
"Clipboard.Path": "المسار",
|
||||||
|
"Clipboard.TitleId": "معرف العنوان",
|
||||||
|
|
||||||
|
"Discord.Playing": "يلعب {0}",
|
||||||
|
"Discord.Browsing": "يتصفح المكتبة",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
|
||||||
|
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
|
||||||
|
"Dialog.PsExecutables": "ملفات PS التنفيذية",
|
||||||
|
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
|
||||||
|
"Dialog.PlainTextFiles": "ملفات نصية عادية",
|
||||||
|
"Dialog.LogFiles": "ملفات السجل"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Português (Brasil)",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opções",
|
||||||
|
"Page.GameCount.One": "1 Jogo",
|
||||||
|
"Page.GameCount.Other": "{0} jogos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
|
"Library.Rescan": "⟳ Atualizar biblioteca",
|
||||||
|
"Library.OpenFile": "Abrir arquivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Jogar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||||
|
"Library.Context.CopyPath": "Copiar o caminho",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||||
|
"Library.Context.Remove": "Remover da biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Sua biblioteca está vazia",
|
||||||
|
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
|
||||||
|
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
|
||||||
|
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Adicionar pasta do jogo",
|
||||||
|
|
||||||
|
"Library.Loading": "Carregando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "Opções Gerais",
|
||||||
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor da CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
|
||||||
|
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nível de log",
|
||||||
|
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warning",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Critical",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de rastreamento de importações",
|
||||||
|
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Salvar log em arquivo",
|
||||||
|
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Caminho do arquivo de log",
|
||||||
|
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Selecionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
|
||||||
|
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música de prévia",
|
||||||
|
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Status do Discord",
|
||||||
|
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma do emulador",
|
||||||
|
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Ativado",
|
||||||
|
"Common.Off": "Desativado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Pesquisar...",
|
||||||
|
"Console.AutoScroll": "Rolagem automática",
|
||||||
|
"Console.Split": "Recortar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpar",
|
||||||
|
"Console.WindowTitle": "Console do SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||||
|
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
|
||||||
|
"Launch.Idle": "Ocioso",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Parar",
|
||||||
|
"Launch.Running": "Em execução — {0}",
|
||||||
|
"Launch.Stopping": "Encerrando…",
|
||||||
|
"Launch.Exited": "Encerrado com código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Arquivo de log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos inválidos",
|
||||||
|
"Exit.EbootNotFound": "eboot.bin não encontrado",
|
||||||
|
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||||
|
"Exit.EmulationError": "erro de emulação",
|
||||||
|
"Exit.Unknown": "desconhecido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
|
||||||
|
"Status.ScanningLibrary": "Verificando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
|
||||||
|
"Status.Running": "Executando {0}",
|
||||||
|
"Status.Stopping": "Encerrando…",
|
||||||
|
"Status.Idle": "Ocioso",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Caminho",
|
||||||
|
"Clipboard.TitleId": "ID do título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jogando {0}",
|
||||||
|
"Discord.Browsing": "Navegando pela biblioteca",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Executáveis de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||||
|
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||||
|
"Dialog.LogFiles": "Arquivos de log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Deutsch",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliothek",
|
||||||
|
"Page.Options": "Optionen",
|
||||||
|
"Page.GameCount.One": "1 Spiel",
|
||||||
|
"Page.GameCount.Other": "{0} Spiele",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Bibliothek durchsuchen…",
|
||||||
|
"Library.AddFolder": "+ Spielordner hinzufügen",
|
||||||
|
"Library.Rescan": "⟳ Neu scannen",
|
||||||
|
"Library.OpenFile": "Datei öffnen…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Starten",
|
||||||
|
"Library.Context.OpenFolder": "Spielordner öffnen",
|
||||||
|
"Library.Context.CopyPath": "Pfad kopieren",
|
||||||
|
"Library.Context.CopyTitleId": "Title ID kopieren",
|
||||||
|
"Library.Context.Remove": "Aus Bibliothek entfernen",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Deine Bibliothek ist leer",
|
||||||
|
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
|
||||||
|
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
|
||||||
|
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht "{0}".",
|
||||||
|
"Library.Empty.AddFolder": "+ Spielordner hinzufügen",
|
||||||
|
|
||||||
|
"Library.Loading": "Bibliothek wird geladen…",
|
||||||
|
|
||||||
|
"Options.General": "Allgemein",
|
||||||
|
"Options.Section.Emulation": "EMULATION",
|
||||||
|
"Options.Section.Logging": "PROTOKOLLIERUNG",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-Engine",
|
||||||
|
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
|
||||||
|
"Options.CpuEngine.Native": "Nativ",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Strikte dynlib-Auflösung",
|
||||||
|
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Protokollstufe",
|
||||||
|
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Warnung",
|
||||||
|
"Options.LogLevel.Error": "Fehler",
|
||||||
|
"Options.LogLevel.Critical": "Kritisch",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Import-Trace-Limit",
|
||||||
|
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "In Datei protokollieren",
|
||||||
|
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
|
||||||
|
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad – Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
|
||||||
|
"Options.LogFilePath.Select": "Auswählen…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
|
||||||
|
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titel-Musik",
|
||||||
|
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-Präsenz",
|
||||||
|
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulator-Sprache",
|
||||||
|
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
|
||||||
|
|
||||||
|
"Common.On": "An",
|
||||||
|
"Common.Off": "Aus",
|
||||||
|
|
||||||
|
"Console.Title": "KONSOLE",
|
||||||
|
"Console.SearchWatermark": "Suchen...",
|
||||||
|
"Console.AutoScroll": "Auto-Scroll",
|
||||||
|
"Console.Split": "Teilen",
|
||||||
|
"Console.Copy": "Kopieren",
|
||||||
|
"Console.Clear": "Leeren",
|
||||||
|
"Console.WindowTitle": "SharpEmu Konsole",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
|
||||||
|
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
|
||||||
|
"Launch.Idle": "Bereit",
|
||||||
|
"Launch.Console": "≡ Konsole",
|
||||||
|
"Launch.Launch": "▶ Starten",
|
||||||
|
"Launch.Stop": "■ Stoppen",
|
||||||
|
"Launch.Running": "Läuft -- {0}",
|
||||||
|
"Launch.Stopping": "Wird beendet…",
|
||||||
|
"Launch.Exited": "Beendet mit Code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
|
||||||
|
"Launch.LogFile": "Log-Datei: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
|
||||||
|
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "Ungültige Argumente",
|
||||||
|
"Exit.EbootNotFound": "eboot nicht gefunden",
|
||||||
|
"Exit.RuntimeException": "Laufzeitfehler",
|
||||||
|
"Exit.EmulationError": "Emulationsfehler",
|
||||||
|
"Exit.Unknown": "unbekannt",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: wird gesucht…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
|
||||||
|
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
|
||||||
|
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
|
||||||
|
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
|
||||||
|
"Status.RemovedFromLibrary": ""{0}" wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
|
||||||
|
"Status.Running": "Läuft {0}",
|
||||||
|
"Status.Stopping": "Wird gestoppt…",
|
||||||
|
"Status.Idle": "Bereit",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Pfad",
|
||||||
|
"Clipboard.TitleId": "Title ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Spielt {0}",
|
||||||
|
"Discord.Browsing": "Durchsucht die Bibliothek",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Spielordner auswählen",
|
||||||
|
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
|
||||||
|
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
|
||||||
|
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
|
||||||
|
"Dialog.PlainTextFiles": "Textdateien",
|
||||||
|
"Dialog.LogFiles": "Protokolldateien"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Dansk",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliotek",
|
||||||
|
"Page.Options": "Indstillinger",
|
||||||
|
"Page.GameCount.One": "1 spil",
|
||||||
|
"Page.GameCount.Other": "{0} spil",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Søg i biblioteket…",
|
||||||
|
"Library.AddFolder": "+ Tilføj mappe",
|
||||||
|
"Library.Rescan": "⟳ Genindlæs",
|
||||||
|
"Library.OpenFile": "Åbn fil…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Start",
|
||||||
|
"Library.Context.OpenFolder": "Åbn spilmappe",
|
||||||
|
"Library.Context.CopyPath": "Kopiér sti",
|
||||||
|
"Library.Context.CopyTitleId": "Kopiér titel-ID",
|
||||||
|
"Library.Context.Remove": "Fjern fra bibliotek",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Dit bibliotek er tomt",
|
||||||
|
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
|
||||||
|
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
|
||||||
|
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Tilføj spilmappe",
|
||||||
|
|
||||||
|
"Library.Loading": "Indlæser bibliotek…",
|
||||||
|
|
||||||
|
"Options.General": "Generelt",
|
||||||
|
"Options.Section.Emulation": "EMULERING",
|
||||||
|
"Options.Section.Logging": "LOGGING",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU-engine",
|
||||||
|
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
|
||||||
|
"Options.CpuEngine.Native": "Native",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Streng dynlib-opløsning",
|
||||||
|
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Logniveau",
|
||||||
|
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
|
||||||
|
"Options.LogLevel.Trace": "Trace",
|
||||||
|
"Options.LogLevel.Debug": "Debug",
|
||||||
|
"Options.LogLevel.Info": "Info",
|
||||||
|
"Options.LogLevel.Warning": "Advarsel",
|
||||||
|
"Options.LogLevel.Error": "Fejl",
|
||||||
|
"Options.LogLevel.Critical": "Kritisk",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Grænse for import-trace",
|
||||||
|
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Log til fil",
|
||||||
|
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Sti til logfil",
|
||||||
|
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
|
||||||
|
"Options.LogFilePath.Select": "Vælg…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
|
||||||
|
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Titelmusik",
|
||||||
|
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discord-tilstedeværelse",
|
||||||
|
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Emulatorsprog",
|
||||||
|
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
|
||||||
|
|
||||||
|
"Common.On": "Til",
|
||||||
|
"Common.Off": "Fra",
|
||||||
|
|
||||||
|
"Console.Title": "KONSOL",
|
||||||
|
"Console.SearchWatermark": "Søg...",
|
||||||
|
"Console.AutoScroll": "Auto-scroll",
|
||||||
|
"Console.Split": "Opdel",
|
||||||
|
"Console.Copy": "Kopiér",
|
||||||
|
"Console.Clear": "Ryd",
|
||||||
|
"Console.WindowTitle": "SharpEmu-konsol",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Intet spil valgt",
|
||||||
|
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
|
||||||
|
"Launch.Idle": "Inaktiv",
|
||||||
|
"Launch.Console": "≡ Konsol",
|
||||||
|
"Launch.Launch": "▶ Start",
|
||||||
|
"Launch.Stop": "■ Stop",
|
||||||
|
"Launch.Running": "Kører — {0}",
|
||||||
|
"Launch.Stopping": "Stopper…",
|
||||||
|
"Launch.Exited": "Afsluttet med kode {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
|
||||||
|
"Launch.LogFile": "Logfil: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
|
||||||
|
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "ugyldige argumenter",
|
||||||
|
"Exit.EbootNotFound": "eboot ikke fundet",
|
||||||
|
"Exit.RuntimeException": "runtime-fejl",
|
||||||
|
"Exit.EmulationError": "emuleringsfejl",
|
||||||
|
"Exit.Unknown": "ukendt",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulator: lokaliserer…",
|
||||||
|
"Status.EmulatorPath": "Emulator: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
|
||||||
|
"Status.ScanningLibrary": "Skanner bibliotek…",
|
||||||
|
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
|
||||||
|
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
|
||||||
|
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
|
||||||
|
"Status.Running": "Kører {0}",
|
||||||
|
"Status.Stopping": "Stopper…",
|
||||||
|
"Status.Idle": "Inaktiv",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Sti",
|
||||||
|
"Clipboard.TitleId": "Titel-ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "Spiller {0}",
|
||||||
|
"Discord.Browsing": "Gennemser biblioteket",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
|
||||||
|
"Dialog.OpenExecutable": "Åbn et program der skal startes",
|
||||||
|
"Dialog.PsExecutables": "PS-programmer",
|
||||||
|
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
|
||||||
|
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
|
||||||
|
"Dialog.LogFiles": "Logfiler"
|
||||||
|
}
|
||||||
@@ -125,5 +125,13 @@
|
|||||||
"Dialog.PsExecutables": "PS executables",
|
"Dialog.PsExecutables": "PS executables",
|
||||||
"Dialog.SaveLogFile": "Select where to save the Log file",
|
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||||
"Dialog.PlainTextFiles": "Plain Text Files",
|
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||||
"Dialog.LogFiles": "Log Files"
|
"Dialog.LogFiles": "Log Files",
|
||||||
|
|
||||||
|
"Options.About" : "About",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Source code, issues and project development.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||||
|
"About.GithubButton": "Contribute in GitHub!",
|
||||||
|
"About.DiscordButton": "Join our Discord!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Español",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opciones",
|
||||||
|
"Page.GameCount.One": "1 juego",
|
||||||
|
"Page.GameCount.Other": "{0} juegos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Añadir carpeta",
|
||||||
|
"Library.Rescan": "⟳ Volver a escanear",
|
||||||
|
"Library.OpenFile": "Abrir archivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Iniciar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
|
||||||
|
"Library.Context.CopyPath": "Copiar ruta",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID del título",
|
||||||
|
"Library.Context.Remove": "Eliminar de la biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Tu biblioteca está vacía",
|
||||||
|
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
|
||||||
|
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
|
||||||
|
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Añadir carpeta de juegos",
|
||||||
|
|
||||||
|
"Library.Loading": "Cargando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "General",
|
||||||
|
"Options.Section.Emulation": "EMULACIÓN",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor de CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
|
||||||
|
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nivel de Log",
|
||||||
|
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Trazas",
|
||||||
|
"Options.LogLevel.Debug": "Depuración",
|
||||||
|
"Options.LogLevel.Info": "Información",
|
||||||
|
"Options.LogLevel.Warning": "Advertencia",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Crítico",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Límite de trazado de importaciones",
|
||||||
|
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Registrar log en archivo",
|
||||||
|
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Ruta del archivo de Log",
|
||||||
|
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Seleccionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música del título",
|
||||||
|
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Actividad de Discord",
|
||||||
|
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma del emulador",
|
||||||
|
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Encendido",
|
||||||
|
"Common.Off": "Apagado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLA",
|
||||||
|
"Console.SearchWatermark": "Buscar...",
|
||||||
|
"Console.AutoScroll": "Desplazamiento automático",
|
||||||
|
"Console.Split": "Desacoplar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpiar",
|
||||||
|
"Console.WindowTitle": "Consola SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
|
||||||
|
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
|
||||||
|
"Launch.Idle": "Inactivo",
|
||||||
|
"Launch.Console": "≡ Consola",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Detener",
|
||||||
|
"Launch.Running": "En ejecución — {0}",
|
||||||
|
"Launch.Stopping": "Deteniendo…",
|
||||||
|
"Launch.Exited": "Finalizó con el código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Archivo de Log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos no válidos",
|
||||||
|
"Exit.EbootNotFound": "no se encontró eboot",
|
||||||
|
"Exit.RuntimeException": "excepción en tiempo de ejecución",
|
||||||
|
"Exit.EmulationError": "error de emulación",
|
||||||
|
"Exit.Unknown": "desconocido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Escaneando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
|
||||||
|
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
|
||||||
|
"Status.Running": "Ejecutando {0}",
|
||||||
|
"Status.Stopping": "Deteniendo…",
|
||||||
|
"Status.Idle": "Inactivo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Ruta",
|
||||||
|
"Clipboard.TitleId": "ID del título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jugando a {0}",
|
||||||
|
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Ejecutables de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
|
||||||
|
"Dialog.PlainTextFiles": "Archivos en texto plano",
|
||||||
|
"Dialog.LogFiles": "Archivos de Log",
|
||||||
|
|
||||||
|
"Options.About" : "Informacion",
|
||||||
|
"About.Github.Label": "GitHub",
|
||||||
|
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
|
||||||
|
"About.Discord.Label": "Discord",
|
||||||
|
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
|
||||||
|
"About.GithubButton": "Contribuye en GitHub!",
|
||||||
|
"About.DiscordButton": "Únete a nuestro Discord!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Français",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliothèque",
|
||||||
|
"Page.Options": "Options",
|
||||||
|
"Page.GameCount.One": "1 jeu",
|
||||||
|
"Page.GameCount.Other": "{0} jeux",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||||
|
"Library.AddFolder": "+ Ajouter un dossier",
|
||||||
|
"Library.Rescan": "⟳ Analyser à nouveau",
|
||||||
|
"Library.OpenFile": "Ouvrir un fichier…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Lancer",
|
||||||
|
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||||
|
"Library.Context.CopyPath": "Copier le chemin",
|
||||||
|
"Library.Context.CopyTitleId": "Copier l’identifiant du jeu",
|
||||||
|
"Library.Context.Remove": "Retirer de la bibliothèque",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Votre bibliothèque est vide",
|
||||||
|
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
|
||||||
|
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
|
||||||
|
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
|
||||||
|
"Library.Empty.AddFolder": "+ Ajouter un dossier de jeux",
|
||||||
|
|
||||||
|
"Library.Loading": "Chargement de la bibliothèque…",
|
||||||
|
|
||||||
|
"Options.General": "Général",
|
||||||
|
"Options.Section.Emulation": "ÉMULATION",
|
||||||
|
"Options.Section.Logging": "JOURNALISATION",
|
||||||
|
"Options.Section.Launcher": "LANCEUR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Moteur CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Moteur d’exécution utilisé pour exécuter le code du jeu.",
|
||||||
|
"Options.CpuEngine.Native": "Natif",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
|
||||||
|
"Options.Strict.Desc": "Interrompre le lancement lorsqu’un symbole importé ne peut pas être résolu.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Niveau de journalisation",
|
||||||
|
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
|
||||||
|
"Options.LogLevel.Trace": "Traçage",
|
||||||
|
"Options.LogLevel.Debug": "Débogage",
|
||||||
|
"Options.LogLevel.Info": "Informations",
|
||||||
|
"Options.LogLevel.Warning": "Avertissements",
|
||||||
|
"Options.LogLevel.Error": "Erreurs",
|
||||||
|
"Options.LogLevel.Critical": "Erreurs critiques",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de traçage des imports",
|
||||||
|
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||||
|
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Chemin du fichier journal",
|
||||||
|
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
|
||||||
|
"Options.LogFilePath.Select": "Sélectionner…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu d’ajouter l’identifiant du jeu et l’horodatage.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Musique du jeu",
|
||||||
|
"Options.TitleMusic.Desc": "Lire en boucle la musique d’aperçu du jeu sélectionné dans la bibliothèque.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Présence Discord",
|
||||||
|
"Options.Discord.Desc": "Afficher le jeu en cours d’exécution sur votre profil Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Langue de l’émulateur",
|
||||||
|
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||||
|
|
||||||
|
"Common.On": "Activé",
|
||||||
|
"Common.Off": "Désactivé",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Rechercher…",
|
||||||
|
"Console.AutoScroll": "Défilement automatique",
|
||||||
|
"Console.Split": "Détacher",
|
||||||
|
"Console.Copy": "Copier",
|
||||||
|
"Console.Clear": "Effacer",
|
||||||
|
"Console.WindowTitle": "Console SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Aucun jeu sélectionné",
|
||||||
|
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
|
||||||
|
"Launch.Idle": "Inactif",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Lancer",
|
||||||
|
"Launch.Stop": "■ Arrêter",
|
||||||
|
"Launch.Running": "En cours d’exécution — {0}",
|
||||||
|
"Launch.Stopping": "Arrêt en cours…",
|
||||||
|
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "L’exécutable SharpEmu est introuvable. Compilez d’abord le projet SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Fichier journal : {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
|
||||||
|
"Launch.ProcessExited": "Le processus s’est terminé avec le code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "arguments non valides",
|
||||||
|
"Exit.EbootNotFound": "eboot introuvable",
|
||||||
|
"Exit.RuntimeException": "exception d’exécution",
|
||||||
|
"Exit.EmulationError": "erreur d’émulation",
|
||||||
|
"Exit.Unknown": "inconnu",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
|
||||||
|
"Status.EmulatorPath": "Émulateur : {0}",
|
||||||
|
"Status.EmulatorNotFound": "Émulateur : l’exécutable SharpEmu est introuvable — compilez d’abord SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
|
||||||
|
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
|
||||||
|
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Impossible d’ouvrir le dossier : {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
|
||||||
|
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
|
||||||
|
"Status.Running": "Exécution de {0}",
|
||||||
|
"Status.Stopping": "Arrêt en cours…",
|
||||||
|
"Status.Idle": "Inactif",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Chemin",
|
||||||
|
"Clipboard.TitleId": "Identifiant du jeu",
|
||||||
|
|
||||||
|
"Discord.Playing": "Joue à {0}",
|
||||||
|
"Discord.Browsing": "Parcourt la bibliothèque",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
|
||||||
|
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
|
||||||
|
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||||
|
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||||
|
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||||
|
"Dialog.LogFiles": "Fichiers journaux"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "日本語",
|
||||||
|
|
||||||
|
"Page.Library": "ライブラリ",
|
||||||
|
"Page.Options": "オプション",
|
||||||
|
"Page.GameCount.One": "ゲーム 1本",
|
||||||
|
"Page.GameCount.Other": "ゲーム {0}本",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "ライブラリを検索…",
|
||||||
|
"Library.AddFolder": "+ フォルダーを追加",
|
||||||
|
"Library.Rescan": "⟳ 再スキャン",
|
||||||
|
"Library.OpenFile": "ファイルを開く…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "起動",
|
||||||
|
"Library.Context.OpenFolder": "ゲームフォルダーを開く",
|
||||||
|
"Library.Context.CopyPath": "パスをコピー",
|
||||||
|
"Library.Context.CopyTitleId": "ゲームIDをコピー",
|
||||||
|
"Library.Context.Remove": "ライブラリから削除",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "ライブラリが空です",
|
||||||
|
"Library.Empty.Hint": "開始するには、ゲームが含まれるフォルダーを追加してください。",
|
||||||
|
"Library.Empty.SearchTitle": "検索条件に一致するゲームが見つかりません",
|
||||||
|
"Library.Empty.SearchHint": "ライブラリに「{0}」と一致する項目はありません。",
|
||||||
|
"Library.Empty.AddFolder": "+ ゲームフォルダーを追加",
|
||||||
|
|
||||||
|
"Library.Loading": "ライブラリを読み込み中…",
|
||||||
|
|
||||||
|
"Options.General": "一般",
|
||||||
|
"Options.Section.Emulation": "エミュレーション",
|
||||||
|
"Options.Section.Logging": "ロギング",
|
||||||
|
"Options.Section.Launcher": "ランチャー",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPUエンジン",
|
||||||
|
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
|
||||||
|
"Options.CpuEngine.Native": "ネイティブ",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "厳格な動的ライブラリ解決",
|
||||||
|
"Options.Strict.Desc": "インポートされたシンボルが解決できない場合、起動を中断します。",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "ログレベル",
|
||||||
|
"Options.LogLevel.Desc": "エミュレータコンソールに表示されるメッセージの詳細度。",
|
||||||
|
"Options.LogLevel.Trace": "トレース",
|
||||||
|
"Options.LogLevel.Debug": "デバッグ",
|
||||||
|
"Options.LogLevel.Info": "情報",
|
||||||
|
"Options.LogLevel.Warning": "警告",
|
||||||
|
"Options.LogLevel.Error": "エラー",
|
||||||
|
"Options.LogLevel.Critical": "致命的なエラー",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "インポートトレース制限",
|
||||||
|
"Options.TraceImports.Desc": "各モジュールの最初のN個のインポートをトレースします(0 = 無効)。",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "ファイルに保存",
|
||||||
|
"Options.LogToFile.Desc": "エミュレータの出力をログファイルにコピーします。",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "ログファイルのパス",
|
||||||
|
"Options.LogFilePath.Default": "カスタムパスなし — ログはエミュレータと同じ場所の user/logs フォルダーに保存されます。",
|
||||||
|
"Options.LogFilePath.Select": "選択…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "ログファイルを上書き",
|
||||||
|
"Options.OverrideLogFile.Desc": "ゲームIDやタイムスタンプを追加せず、指定されたパスをそのまま使用します。",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "ゲーム内音楽",
|
||||||
|
"Options.TitleMusic.Desc": "ライブラリで選択したゲームのプレビュー音楽をループ再生します。",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Discordステータス表示",
|
||||||
|
"Options.Discord.Desc": "現在プレイ中のゲームをDiscordのプロフィールに表示します。",
|
||||||
|
|
||||||
|
"Options.Language.Label": "エミュレータの言語",
|
||||||
|
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
|
||||||
|
|
||||||
|
"Common.On": "オン",
|
||||||
|
"Common.Off": "オフ",
|
||||||
|
|
||||||
|
"Console.Title": "コンソール",
|
||||||
|
"Console.SearchWatermark": "検索…",
|
||||||
|
"Console.AutoScroll": "自動スクロール",
|
||||||
|
"Console.Split": "ウィンドウを分離",
|
||||||
|
"Console.Copy": "コピー",
|
||||||
|
"Console.Clear": "消去",
|
||||||
|
"Console.WindowTitle": "SharpEmu コンソール",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "ゲームが選択されていません",
|
||||||
|
"Launch.NoGameHint": "ライブラリからゲームを選択するか、eboot.bin ファイルを直接開いてください。",
|
||||||
|
"Launch.Idle": "待機中",
|
||||||
|
"Launch.Console": "≡ コンソール",
|
||||||
|
"Launch.Launch": "▶ 起動",
|
||||||
|
"Launch.Stop": "■ 停止",
|
||||||
|
"Launch.Running": "実行中 — {0}",
|
||||||
|
"Launch.Stopping": "停止中…",
|
||||||
|
"Launch.Exited": "プロセスがコード {0} ({1}) で終了しました",
|
||||||
|
"Launch.ExeNotFound": "SharpEmuの実行ファイルが見つかりません。先に SharpEmu.CLI プロジェクトをビルドしてください(dotnet build)。",
|
||||||
|
"Launch.LogFile": "ログファイル: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "エミュレータを起動できませんでした: {0}",
|
||||||
|
"Launch.ProcessExited": "プロセスがコード {0} ({1}) で終了しました。",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "無効な引数",
|
||||||
|
"Exit.EbootNotFound": "eboot が見つかりません",
|
||||||
|
"Exit.RuntimeException": "ランタイム例外",
|
||||||
|
"Exit.EmulationError": "エミュレーションエラー",
|
||||||
|
"Exit.Unknown": "不明",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "エミュレータ: 位置を検索中…",
|
||||||
|
"Status.EmulatorPath": "エミュレータ: {0}",
|
||||||
|
"Status.EmulatorNotFound": "エミュレータ: SharpEmuの実行ファイルが見つかりません — 先に SharpEmu.CLI をビルドしてください。",
|
||||||
|
"Status.ScanningLibrary": "ライブラリをスキャン中…",
|
||||||
|
"Status.AddFolderPrompt": "ライブラリに表示するゲームフォルダーを追加してください。",
|
||||||
|
"Status.LibraryScanned": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
|
||||||
|
"Status.CouldNotOpenFolder": "フォルダーを開けませんでした: {0}",
|
||||||
|
"Status.CopiedToClipboard": "「{0}」をクリップボードにコピーしました。",
|
||||||
|
"Status.RemovedFromLibrary": "「{0}」がライブラリから削除されました。復元するにはフォルダーを再追加してください。",
|
||||||
|
"Status.Running": "{0} を実行中",
|
||||||
|
"Status.Stopping": "停止中…",
|
||||||
|
"Status.Idle": "待機中",
|
||||||
|
|
||||||
|
"Clipboard.Path": "パス",
|
||||||
|
"Clipboard.TitleId": "ゲームID",
|
||||||
|
|
||||||
|
"Discord.Playing": "{0} をプレイ中",
|
||||||
|
"Discord.Browsing": "ライブラリを閲覧中",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "ゲームが含まれるフォルダーを選択",
|
||||||
|
"Dialog.OpenExecutable": "起動する実行ファイルを開く",
|
||||||
|
"Dialog.PsExecutables": "PlayStation 実行ファイル",
|
||||||
|
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||||
|
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||||
|
"Dialog.LogFiles": "ログファイル"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "한국어",
|
||||||
|
|
||||||
|
"Page.Library": "라이브러리",
|
||||||
|
"Page.Options": "옵션",
|
||||||
|
"Page.GameCount.One": "게임 1개",
|
||||||
|
"Page.GameCount.Other": "게임 {0}개",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "라이브러리 검색…",
|
||||||
|
"Library.AddFolder": "+ 폴더 추가",
|
||||||
|
"Library.Rescan": "⟳ 다시 스캔",
|
||||||
|
"Library.OpenFile": "파일 열기…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "실행",
|
||||||
|
"Library.Context.OpenFolder": "게임 폴더 열기",
|
||||||
|
"Library.Context.CopyPath": "경로 복사",
|
||||||
|
"Library.Context.CopyTitleId": "게임 ID 복사",
|
||||||
|
"Library.Context.Remove": "라이브러리에서 제거",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "라이브러리가 비어 있습니다",
|
||||||
|
"Library.Empty.Hint": "시작하려면 게임이 포함된 폴더를 추가하세요.",
|
||||||
|
"Library.Empty.SearchTitle": "검색 결과와 일치하는 게임이 없습니다",
|
||||||
|
"Library.Empty.SearchHint": "라이브러리에 '{0}'와(과) 일치하는 항목이 없습니다.",
|
||||||
|
"Library.Empty.AddFolder": "+ 게임 폴더 추가",
|
||||||
|
|
||||||
|
"Library.Loading": "라이브러리 불러오는 중…",
|
||||||
|
|
||||||
|
"Options.General": "일반",
|
||||||
|
"Options.Section.Emulation": "에뮬레이션",
|
||||||
|
"Options.Section.Logging": "로깅",
|
||||||
|
"Options.Section.Launcher": "런처",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "CPU 엔진",
|
||||||
|
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
|
||||||
|
"Options.CpuEngine.Native": "네이티브",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
|
||||||
|
"Options.Strict.Desc": "가져온 심볼을 해석할 수 없는 경우 실행을 중단합니다.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "로그 수준",
|
||||||
|
"Options.LogLevel.Desc": "에뮬레이터 콘솔에 표시할 메시지의 세부 정보 수준입니다.",
|
||||||
|
"Options.LogLevel.Trace": "트레이스",
|
||||||
|
"Options.LogLevel.Debug": "디버그",
|
||||||
|
"Options.LogLevel.Info": "정보",
|
||||||
|
"Options.LogLevel.Warning": "경고",
|
||||||
|
"Options.LogLevel.Error": "오류",
|
||||||
|
"Options.LogLevel.Critical": "치명적 오류",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "가져오기 트레이스 한도",
|
||||||
|
"Options.TraceImports.Desc": "각 모듈의 처음 N개 가져오기를 트레이스합니다 (0 = 비활성화).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "파일로 저장",
|
||||||
|
"Options.LogToFile.Desc": "에뮬레이터 출력을 로그 파일에 복사합니다.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "로그 파일 경로",
|
||||||
|
"Options.LogFilePath.Default": "사용자 지정 경로 없음 — 로그는 에뮬레이터 옆의 user/logs 폴더에 저장됩니다.",
|
||||||
|
"Options.LogFilePath.Select": "선택…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "로그 파일 덮어쓰기",
|
||||||
|
"Options.OverrideLogFile.Desc": "게임 ID와 타임스탬프를 추가하는 대신 정확히 이 경로를 사용합니다.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "게임 음악",
|
||||||
|
"Options.TitleMusic.Desc": "라이브러리에서 선택한 게임의 미리보기 음악을 반복 재생합니다.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "디스코드 상태 표시",
|
||||||
|
"Options.Discord.Desc": "디스코드 프로필에 현재 실행 중인 게임을 표시합니다.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "에뮬레이터 언어",
|
||||||
|
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
|
||||||
|
|
||||||
|
"Common.On": "켬",
|
||||||
|
"Common.Off": "끔",
|
||||||
|
|
||||||
|
"Console.Title": "콘솔",
|
||||||
|
"Console.SearchWatermark": "검색…",
|
||||||
|
"Console.AutoScroll": "자동 스크롤",
|
||||||
|
"Console.Split": "창 분리",
|
||||||
|
"Console.Copy": "복사",
|
||||||
|
"Console.Clear": "지우기",
|
||||||
|
"Console.WindowTitle": "SharpEmu 콘솔",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "선택된 게임 없음",
|
||||||
|
"Launch.NoGameHint": "라이브러리에서 게임을 선택하거나 eboot.bin 파일을 직접 여세요.",
|
||||||
|
"Launch.Idle": "대기 중",
|
||||||
|
"Launch.Console": "≡ 콘솔",
|
||||||
|
"Launch.Launch": "▶ 실행",
|
||||||
|
"Launch.Stop": "■ 중지",
|
||||||
|
"Launch.Running": "실행 중 — {0}",
|
||||||
|
"Launch.Stopping": "중지 중…",
|
||||||
|
"Launch.Exited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다",
|
||||||
|
"Launch.ExeNotFound": "SharpEmu 실행 파일을 찾을 수 없습니다. 먼저 SharpEmu.CLI 프로젝트를 컴파일하세요 (dotnet build).",
|
||||||
|
"Launch.LogFile": "로그 파일: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "에뮬레이터를 시작할 수 없습니다: {0}",
|
||||||
|
"Launch.ProcessExited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다.",
|
||||||
|
|
||||||
|
"Exit.Ok": "확인",
|
||||||
|
"Exit.InvalidArguments": "잘못된 인수",
|
||||||
|
"Exit.EbootNotFound": "eboot을 찾을 수 없음",
|
||||||
|
"Exit.RuntimeException": "런타임 예외",
|
||||||
|
"Exit.EmulationError": "에뮬레이션 오류",
|
||||||
|
"Exit.Unknown": "알 수 없음",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "에뮬레이터: 위치 검색 중…",
|
||||||
|
"Status.EmulatorPath": "에뮬레이터: {0}",
|
||||||
|
"Status.EmulatorNotFound": "에뮬레이터: SharpEmu 실행 파일을 찾을 수 없습니다 — 먼저 SharpEmu.CLI를 컴파일하세요.",
|
||||||
|
"Status.ScanningLibrary": "라이브러리 스캔 중…",
|
||||||
|
"Status.AddFolderPrompt": "라이브러리를 채우려면 게임 폴더를 추가하세요.",
|
||||||
|
"Status.LibraryScanned": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
|
||||||
|
"Status.CouldNotOpenFolder": "폴더를 열 수 없습니다: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0}이(가) 클립보드에 복사되었습니다.",
|
||||||
|
"Status.RemovedFromLibrary": "'{0}'이(가) 라이브러리에서 제거되었습니다. 복구하려면 폴더를 다시 추가하세요.",
|
||||||
|
"Status.Running": "{0} 실행 중",
|
||||||
|
"Status.Stopping": "중지 중…",
|
||||||
|
"Status.Idle": "대기 중",
|
||||||
|
|
||||||
|
"Clipboard.Path": "경로",
|
||||||
|
"Clipboard.TitleId": "게임 ID",
|
||||||
|
|
||||||
|
"Discord.Playing": "{0} 플레이 중",
|
||||||
|
"Discord.Browsing": "라이브러리 둘러보는 중",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "게임이 포함된 폴더 선택",
|
||||||
|
"Dialog.OpenExecutable": "실행할 파일 열기",
|
||||||
|
"Dialog.PsExecutables": "PlayStation 실행 파일",
|
||||||
|
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||||
|
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||||
|
"Dialog.LogFiles": "로그 파일"
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ public sealed class Localization
|
|||||||
private const string EmbeddedResourceSuffix = ".json";
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
|
private Dictionary<string, string> _fallbackStrings = new();
|
||||||
|
|
||||||
|
|
||||||
private Localization()
|
private Localization()
|
||||||
{
|
{
|
||||||
@@ -32,7 +34,16 @@ public sealed class Localization
|
|||||||
|
|
||||||
public string CurrentCode { get; private set; } = "en";
|
public string CurrentCode { get; private set; } = "en";
|
||||||
|
|
||||||
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
|
public string Get(string key)
|
||||||
|
{
|
||||||
|
if (_strings.TryGetValue(key, out var value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
|
||||||
|
return fallbackValue;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||||
|
|
||||||
@@ -74,13 +85,46 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||||
|
/// english is the fallback language
|
||||||
public void Load(string code)
|
public void Load(string code)
|
||||||
{
|
{
|
||||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
||||||
|
{
|
||||||
|
fallback = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
_fallbackStrings = fallback;
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
||||||
|
{
|
||||||
|
_strings = enDict;
|
||||||
|
_fallbackStrings = enDict;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
_fallbackStrings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = "en";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the requested language
|
||||||
|
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
||||||
|
{
|
||||||
|
_strings = loaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_fallbackStrings.Count > 0)
|
||||||
|
_strings = new Dictionary<string, string>(_fallbackStrings);
|
||||||
|
else
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||||
@@ -149,16 +193,64 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoad(string code, string json)
|
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = OpenEmbeddedLanguageStream(code);
|
||||||
|
if (stream is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoad(string json, out Dictionary<string, string> result)
|
||||||
{
|
{
|
||||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_strings = loaded;
|
result = loaded;
|
||||||
CurrentCode = code;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private bool TryLoad(string code, string json)
|
||||||
|
{
|
||||||
|
if (TryLoad(json, out var dict))
|
||||||
|
{
|
||||||
|
_strings = dict;
|
||||||
|
CurrentCode = code;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,7 +317,73 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
<Border Classes="card">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock x:Name="AboutSectionTitle"
|
||||||
|
Classes="sectionTitle"
|
||||||
|
Text="ABOUT" />
|
||||||
|
|
||||||
|
<!--Github-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/github.png"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock x:Name="GithubLabel"
|
||||||
|
Text="GitHub"
|
||||||
|
FontSize="13" />
|
||||||
|
<TextBlock x:Name="GithubDesc"
|
||||||
|
Text="Source code, issues and project development."
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource MutedBrush}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
x:Name="GithubButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="Open"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!--Discord-->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image Source="avares://SharpEmu.GUI/Assets/discord.png"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock x:Name="DiscordServerLabel"
|
||||||
|
Text="Discord"
|
||||||
|
FontSize="13" />
|
||||||
|
<TextBlock x:Name="DiscordServerDesc"
|
||||||
|
Text="Join the community, get support and follow development."
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource MutedBrush}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
x:Name="DiscordButton"
|
||||||
|
Classes="ghost"
|
||||||
|
Content="Join"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -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"));
|
||||||
@@ -146,6 +147,25 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
||||||
_gamepadTimer.Start();
|
_gamepadTimer.Start();
|
||||||
|
|
||||||
|
|
||||||
|
GithubButton.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "https://github.com/par274/sharpemu",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DiscordButton.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "https://discord.com/invite/6GejPEDqpc",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -442,6 +462,14 @@ public partial class MainWindow : Window
|
|||||||
LaunchButton.Content = loc.Get("Launch.Launch");
|
LaunchButton.Content = loc.Get("Launch.Launch");
|
||||||
StopButton.Content = loc.Get("Launch.Stop");
|
StopButton.Content = loc.Get("Launch.Stop");
|
||||||
|
|
||||||
|
AboutSectionTitle.Text = loc.Get("Options.About");
|
||||||
|
GithubLabel.Text = loc.Get("About.Github.Label");
|
||||||
|
GithubDesc.Text = loc.Get("About.Github.Desc");
|
||||||
|
DiscordServerLabel.Text = loc.Get("About.Discord.Label");
|
||||||
|
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||||
|
GithubButton.Content = loc.Get("About.GithubButton");
|
||||||
|
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||||
|
|
||||||
UpdateEmptyStateTexts();
|
UpdateEmptyStateTexts();
|
||||||
UpdateSelectedGameTexts();
|
UpdateSelectedGameTexts();
|
||||||
}
|
}
|
||||||
@@ -1504,7 +1532,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)));
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
|
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
|
||||||
|
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets host-facing libraries (VideoOut, AudioOut) request cooperative guest
|
||||||
|
/// shutdown without taking a dependency on SharpEmu.Core.
|
||||||
|
/// </summary>
|
||||||
|
public static class HostSessionControl
|
||||||
|
{
|
||||||
|
private static Action<string>? _shutdownHandler;
|
||||||
|
|
||||||
|
public static void SetShutdownHandler(Action<string>? handler)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _shutdownHandler, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequestShutdown(string reason)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Volatile.Read(ref _shutdownHandler)?.Invoke(reason);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Host shutdown handler failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,6 @@ public interface ICpuMemory
|
|||||||
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||||
|
|
||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ public static class AgcExports
|
|||||||
private const uint RegisterDefaultsVersion13 = 13;
|
private const uint RegisterDefaultsVersion13 = 13;
|
||||||
private const int RegisterDefaultsSize = 0x40;
|
private const int RegisterDefaultsSize = 0x40;
|
||||||
private const int RegisterDefaultBlockSize = 16 * 8;
|
private const int RegisterDefaultBlockSize = 16 * 8;
|
||||||
|
private const ulong ResourceRegistrationBytesPerResource = 0x118;
|
||||||
|
private const ulong ResourceRegistrationBytesPerOwner = 0x1E0;
|
||||||
|
private const int ResourceRegistrationMaxNameLength = 256;
|
||||||
|
|
||||||
private const ulong ShaderUserDataOffset = 0x08;
|
private const ulong ShaderUserDataOffset = 0x08;
|
||||||
private const ulong ShaderCodeOffset = 0x10;
|
private const ulong ShaderCodeOffset = 0x10;
|
||||||
@@ -154,7 +157,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
|
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
|
||||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||||
@@ -388,9 +391,26 @@ public static class AgcExports
|
|||||||
public SubmittedDcbState Graphics { get; } = new();
|
public SubmittedDcbState Graphics { get; } = new();
|
||||||
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
|
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
|
||||||
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
|
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
|
||||||
|
public Dictionary<uint, string> ResourceOwners { get; } = new();
|
||||||
|
public Dictionary<uint, RegisteredAgcResource> RegisteredResources { get; } = new();
|
||||||
|
public bool ResourceRegistrationInitialized { get; set; }
|
||||||
|
public ulong ResourceRegistrationMemory { get; set; }
|
||||||
|
public ulong ResourceRegistrationMemorySize { get; set; }
|
||||||
|
public uint ResourceRegistrationMaxOwners { get; set; }
|
||||||
|
public uint DefaultOwner { get; set; } = DefaultAgcOwner;
|
||||||
|
public uint NextOwner { get; set; } = 1;
|
||||||
|
public uint NextResource { get; set; } = 1;
|
||||||
public ulong WorkSequence { get; set; }
|
public ulong WorkSequence { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct RegisteredAgcResource(
|
||||||
|
uint Owner,
|
||||||
|
ulong Address,
|
||||||
|
ulong Size,
|
||||||
|
string Name,
|
||||||
|
uint Type,
|
||||||
|
uint Flags);
|
||||||
|
|
||||||
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
|
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
|
||||||
|
|
||||||
private readonly record struct RegisterDefaultGroup(
|
private readonly record struct RegisterDefaultGroup(
|
||||||
@@ -1999,8 +2019,16 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2030,27 +2058,35 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
for (uint i = 0; i < bufferCount; i++)
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
for (uint i = 0; i < bufferCount; i++)
|
||||||
commandAddress == 0 ||
|
|
||||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
|
||||||
dwordCount == 0)
|
|
||||||
{
|
{
|
||||||
continue;
|
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||||
|
commandAddress == 0 ||
|
||||||
|
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||||
|
dwordCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||||
|
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracePackets)
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
{
|
}
|
||||||
TraceAgc(
|
finally
|
||||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
{
|
||||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
}
|
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2098,8 +2134,16 @@ public static class AgcExports
|
|||||||
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2120,12 +2164,87 @@ public static class AgcExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(outAddress, 256))
|
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
TraceAgc(
|
||||||
|
$"agc.driver_get_resource_registration_max_name_length " +
|
||||||
|
$"out=0x{outAddress:X16} value={ResourceRegistrationMaxNameLength}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AOLcoIkQDgM",
|
||||||
|
ExportName = "sceAgcDriverQueryResourceRegistrationUserMemoryRequirements",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverQueryResourceRegistrationUserMemoryRequirements(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var sizeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resourceCount = ctx[CpuRegister.Rsi];
|
||||||
|
var ownerCount = ctx[CpuRegister.Rdx];
|
||||||
|
if (sizeAddress == 0 || resourceCount == 0 || ownerCount == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong requiredSize;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
requiredSize = checked(
|
||||||
|
resourceCount * ResourceRegistrationBytesPerResource +
|
||||||
|
ownerCount * ResourceRegistrationBytesPerOwner);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(sizeAddress, requiredSize))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_query_resource_registration_memory resources={resourceCount} " +
|
||||||
|
$"owners={ownerCount} bytes=0x{requiredSize:X}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "F0Y42t-3e18",
|
||||||
|
ExportName = "sceAgcDriverInitResourceRegistration",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverInitResourceRegistration(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var memoryAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var memorySize = ctx[CpuRegister.Rsi];
|
||||||
|
var ownerCount = ctx[CpuRegister.Rdx];
|
||||||
|
if (memoryAddress == 0 || memorySize == 0 || ownerCount == 0 || ownerCount > uint.MaxValue)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.ResourceRegistrationInitialized = true;
|
||||||
|
state.ResourceRegistrationMemory = memoryAddress;
|
||||||
|
state.ResourceRegistrationMemorySize = memorySize;
|
||||||
|
state.ResourceRegistrationMaxOwners = (uint)ownerCount;
|
||||||
|
state.ResourceOwners.Clear();
|
||||||
|
state.RegisteredResources.Clear();
|
||||||
|
state.DefaultOwner = DefaultAgcOwner;
|
||||||
|
state.NextOwner = 1;
|
||||||
|
state.NextResource = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_init_resource_registration memory=0x{memoryAddress:X16} " +
|
||||||
|
$"bytes=0x{memorySize:X} owners={ownerCount}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2144,12 +2263,97 @@ public static class AgcExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
uint owner;
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
owner = state.DefaultOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(ownerAddress, owner))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={owner}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "U9ueyEhSkF4",
|
||||||
|
ExportName = "sceAgcDriverRegisterDefaultOwner",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverRegisterDefaultOwner(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var owner = (uint)ctx[CpuRegister.Rdi];
|
||||||
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.DefaultOwner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_register_default_owner owner={owner}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "X-Nm5KLREeg",
|
||||||
|
ExportName = "sceAgcDriverRegisterOwner",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverRegisterOwner(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var ownerAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (ownerAddress == 0 || nameAddress == 0 ||
|
||||||
|
!TryReadGuestCString(
|
||||||
|
ctx,
|
||||||
|
nameAddress,
|
||||||
|
ResourceRegistrationMaxNameLength,
|
||||||
|
out var nameBytes))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
uint owner;
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (!state.ResourceRegistrationInitialized ||
|
||||||
|
state.ResourceRegistrationMaxOwners != 0 &&
|
||||||
|
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = state.NextOwner;
|
||||||
|
while (owner == state.DefaultOwner || state.ResourceOwners.ContainsKey(owner))
|
||||||
|
{
|
||||||
|
owner++;
|
||||||
|
if (owner == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.NextOwner = owner + 1;
|
||||||
|
state.ResourceOwners.Add(owner, System.Text.Encoding.UTF8.GetString(nameBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(ownerAddress, owner))
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.ResourceOwners.Remove(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_register_owner out=0x{ownerAddress:X16} owner={owner} " +
|
||||||
|
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2160,19 +2364,90 @@ public static class AgcExports
|
|||||||
LibraryName = "libSceAgc")]
|
LibraryName = "libSceAgc")]
|
||||||
public static int DriverRegisterResource(CpuContext ctx)
|
public static int DriverRegisterResource(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var resourceAddress = ctx[CpuRegister.Rdi];
|
var resourceHandleAddress = ctx[CpuRegister.Rdi];
|
||||||
var owner = (uint)ctx[CpuRegister.Rsi];
|
var owner = (uint)ctx[CpuRegister.Rsi];
|
||||||
var nameAddress = ctx[CpuRegister.Rdx];
|
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||||
var type = (uint)ctx[CpuRegister.R8];
|
var resourceSize = ctx[CpuRegister.Rcx];
|
||||||
var flags = (uint)ctx[CpuRegister.R9];
|
var nameAddress = ctx[CpuRegister.R8];
|
||||||
|
var type = (uint)ctx[CpuRegister.R9];
|
||||||
|
if (resourceHandleAddress == 0 || resourceAddress == 0 || resourceSize == 0 ||
|
||||||
|
!ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var flags) ||
|
||||||
|
!TryReadGuestCString(
|
||||||
|
ctx,
|
||||||
|
nameAddress,
|
||||||
|
ResourceRegistrationMaxNameLength,
|
||||||
|
out var nameBytes))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
uint resourceHandle;
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (!state.ResourceRegistrationInitialized ||
|
||||||
|
owner != state.DefaultOwner &&
|
||||||
|
!state.ResourceOwners.ContainsKey(owner))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceHandle = state.NextResource++;
|
||||||
|
if (resourceHandle == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.RegisteredResources.Add(
|
||||||
|
resourceHandle,
|
||||||
|
new RegisteredAgcResource(
|
||||||
|
owner,
|
||||||
|
resourceAddress,
|
||||||
|
resourceSize,
|
||||||
|
System.Text.Encoding.UTF8.GetString(nameBytes),
|
||||||
|
type,
|
||||||
|
flags));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(resourceHandleAddress, resourceHandle))
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.RegisteredResources.Remove(resourceHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
TraceAgc(
|
TraceAgc(
|
||||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
|
||||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
$"resource=0x{resourceAddress:X16} bytes=0x{resourceSize:X} " +
|
||||||
|
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)} type={type} flags={flags}");
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pWLG7WOpVcw",
|
||||||
|
ExportName = "sceAgcDriverUnregisterResource",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverUnregisterResource(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var resourceHandle = (uint)ctx[CpuRegister.Rdi];
|
||||||
|
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (!state.RegisteredResources.Remove(resourceHandle))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-KRzWekV120",
|
Nid = "-KRzWekV120",
|
||||||
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||||
@@ -3167,7 +3442,8 @@ public static class AgcExports
|
|||||||
exportState,
|
exportState,
|
||||||
out var exportEvaluation,
|
out var exportEvaluation,
|
||||||
out error,
|
out error,
|
||||||
resolveVertexInputs: true) ||
|
resolveVertexInputs: true,
|
||||||
|
vertexRecordLimit: indexed ? null : vertexCount) ||
|
||||||
!Gen5ShaderTranslator.TryCreateState(
|
!Gen5ShaderTranslator.TryCreateState(
|
||||||
ctx,
|
ctx,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
@@ -3191,14 +3467,19 @@ public static class AgcExports
|
|||||||
HasPixelColorExport(pixelState, target.Slot))
|
HasPixelColorExport(pixelState, target.Slot))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
||||||
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
|
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||||
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
|
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||||
|
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||||
var shaderKey = (
|
var shaderKey = (
|
||||||
exportShaderAddress,
|
exportShaderAddress,
|
||||||
exportStateFingerprint,
|
exportStateFingerprint,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
pixelStateFingerprint,
|
pixelStateFingerprint,
|
||||||
outputKind);
|
outputKind,
|
||||||
|
attributeCount);
|
||||||
|
var totalGlobalBuffers =
|
||||||
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
|
exportEvaluation.GlobalMemoryBindings.Count;
|
||||||
(byte[] Vertex, byte[] Pixel) compiled;
|
(byte[] Vertex, byte[] Pixel) compiled;
|
||||||
lock (_submitTraceGate)
|
lock (_submitTraceGate)
|
||||||
{
|
{
|
||||||
@@ -3207,9 +3488,6 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||||
{
|
{
|
||||||
var totalGlobalBuffers =
|
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
|
||||||
exportEvaluation.GlobalMemoryBindings.Count;
|
|
||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
pixelEvaluation,
|
pixelEvaluation,
|
||||||
@@ -3217,16 +3495,18 @@ public static class AgcExports
|
|||||||
out var pixelShader,
|
out var pixelShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: 0,
|
globalBufferBase: 0,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: 0) ||
|
imageBindingBase: 0,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||||
exportState,
|
exportState,
|
||||||
exportEvaluation,
|
exportEvaluation,
|
||||||
out var vertexShader,
|
out var vertexShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: pixelEvaluation.ImageBindings.Count))
|
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3278,6 +3558,8 @@ public static class AgcExports
|
|||||||
|
|
||||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||||
|
.Append(CreateScalarRegisterBinding(pixelEvaluation))
|
||||||
|
.Append(CreateScalarRegisterBinding(exportEvaluation))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||||
exportEvaluation.VertexInputs ?? [];
|
exportEvaluation.VertexInputs ?? [];
|
||||||
@@ -3288,7 +3570,7 @@ public static class AgcExports
|
|||||||
primitiveType,
|
primitiveType,
|
||||||
compiled.Vertex,
|
compiled.Vertex,
|
||||||
compiled.Pixel,
|
compiled.Pixel,
|
||||||
GetInterpolatedAttributeCount(pixelState),
|
attributeCount,
|
||||||
vertexCount,
|
vertexCount,
|
||||||
state.InstanceCount,
|
state.InstanceCount,
|
||||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||||
@@ -3400,6 +3682,88 @@ public static class AgcExports
|
|||||||
return (uint)(maxAttribute + 1);
|
return (uint)(maxAttribute + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int ShaderScalarRegisterCount = 256;
|
||||||
|
|
||||||
|
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
|
||||||
|
Gen5ShaderEvaluation evaluation)
|
||||||
|
{
|
||||||
|
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
|
||||||
|
var registers = evaluation.InitialScalarRegisters;
|
||||||
|
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var value = registers[index];
|
||||||
|
var offset = index * sizeof(uint);
|
||||||
|
data[offset] = (byte)value;
|
||||||
|
data[offset + 1] = (byte)(value >> 8);
|
||||||
|
data[offset + 2] = (byte)(value >> 16);
|
||||||
|
data[offset + 3] = (byte)(value >> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Gen5GlobalMemoryBinding(0, 0, [], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
|
||||||
|
{
|
||||||
|
const ulong offsetBasis = 14695981039346656037UL;
|
||||||
|
const ulong prime = 1099511628211UL;
|
||||||
|
var hash = offsetBasis;
|
||||||
|
void Mix(ulong value) => hash = (hash ^ value) * prime;
|
||||||
|
|
||||||
|
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
|
||||||
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
||||||
|
{
|
||||||
|
Mix(binding.ScalarAddress);
|
||||||
|
Mix((ulong)binding.InstructionPcs.Count);
|
||||||
|
foreach (var pc in binding.InstructionPcs)
|
||||||
|
{
|
||||||
|
Mix(pc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mix((ulong)evaluation.ImageBindings.Count);
|
||||||
|
foreach (var image in evaluation.ImageBindings)
|
||||||
|
{
|
||||||
|
Mix(image.Pc);
|
||||||
|
Mix((ulong)(uint)image.Opcode.GetHashCode());
|
||||||
|
foreach (var word in image.ResourceDescriptor)
|
||||||
|
{
|
||||||
|
Mix(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var word in image.SamplerDescriptor)
|
||||||
|
{
|
||||||
|
Mix(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mix(image.MipLevel ?? uint.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluation.VertexInputs is { } vertexInputs)
|
||||||
|
{
|
||||||
|
Mix((ulong)vertexInputs.Count);
|
||||||
|
foreach (var input in vertexInputs)
|
||||||
|
{
|
||||||
|
Mix(input.Pc);
|
||||||
|
Mix(input.Location);
|
||||||
|
Mix(input.ComponentCount);
|
||||||
|
Mix(input.DataFormat);
|
||||||
|
Mix(input.NumberFormat);
|
||||||
|
Mix(input.Stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
|
||||||
|
{
|
||||||
|
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
||||||
{
|
{
|
||||||
const ulong offsetBasis = 14695981039346656037UL;
|
const ulong offsetBasis = 14695981039346656037UL;
|
||||||
@@ -4250,7 +4614,7 @@ public static class AgcExports
|
|||||||
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
||||||
var gpuDispatch = false;
|
var gpuDispatch = false;
|
||||||
var computeError = string.Empty;
|
var computeError = string.Empty;
|
||||||
if (hasStorageBinding &&
|
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
|
||||||
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
||||||
{
|
{
|
||||||
var shaderKey = (
|
var shaderKey = (
|
||||||
@@ -4320,7 +4684,8 @@ public static class AgcExports
|
|||||||
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
||||||
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
||||||
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
||||||
$"gpu={gpuDispatch} blits={blitCount}" +
|
$"gpu={gpuDispatch} blits={blitCount} " +
|
||||||
|
$"globals={evaluation.GlobalMemoryBindings.Count}" +
|
||||||
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
||||||
$" bindings=[{string.Join(',', descriptions)}]");
|
$" bindings=[{string.Join(',', descriptions)}]");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
private const int ImageDescriptorDwords = 8;
|
private const int ImageDescriptorDwords = 8;
|
||||||
private const int SamplerDescriptorDwords = 4;
|
private const int SamplerDescriptorDwords = 4;
|
||||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
||||||
|
private static readonly int DefaultGlobalMemoryBindingBytes =
|
||||||
|
int.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
|
||||||
|
out var configured) && configured >= sizeof(uint)
|
||||||
|
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
||||||
|
: 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
internal static long GlobalMemoryReadCount;
|
||||||
|
internal static long GlobalMemoryReadBytes;
|
||||||
|
internal static long GlobalMemoryReadCacheHits;
|
||||||
|
internal static long GlobalMemoryReadPvmBytes;
|
||||||
|
internal static long GlobalMemoryReadLibcBytes;
|
||||||
|
internal static long GlobalMemoryReadReuses;
|
||||||
|
|
||||||
|
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
||||||
|
private static readonly object _crossFrameReadGate = new();
|
||||||
|
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
|
||||||
|
private static long _crossFrameReadCacheBytes;
|
||||||
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||||
|
|
||||||
private readonly record struct BufferDescriptor(
|
private readonly record struct BufferDescriptor(
|
||||||
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
out Gen5ShaderEvaluation evaluation,
|
out Gen5ShaderEvaluation evaluation,
|
||||||
out string error,
|
out string error,
|
||||||
bool resolveVertexInputs = false)
|
bool resolveVertexInputs = false,
|
||||||
|
uint? vertexRecordLimit = null)
|
||||||
{
|
{
|
||||||
evaluation = default!;
|
evaluation = default!;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
if (resolveVertexInputs &&
|
if (resolveVertexInputs &&
|
||||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
||||||
{
|
{
|
||||||
|
var vertexReadSize = bufferDescriptor.SizeBytes;
|
||||||
|
if (vertexRecordLimit is { } recordLimit &&
|
||||||
|
instruction.Sources.Count > 2 &&
|
||||||
|
TryEvaluateScalarOperand(
|
||||||
|
instruction.Sources[2],
|
||||||
|
scalarRegisters,
|
||||||
|
out var scalarOffset))
|
||||||
|
{
|
||||||
|
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
|
||||||
|
var requiredBytes =
|
||||||
|
(ulong)bindingOffset +
|
||||||
|
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
|
||||||
|
(ulong)bufferMemory.DwordCount * sizeof(uint);
|
||||||
|
vertexReadSize = Math.Min(
|
||||||
|
bufferDescriptor.SizeBytes,
|
||||||
|
Math.Max(requiredBytes, sizeof(uint)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryReadGlobalMemory(
|
if (!TryReadGlobalMemory(
|
||||||
ctx,
|
ctx,
|
||||||
bufferDescriptor.BaseAddress,
|
bufferDescriptor.BaseAddress,
|
||||||
bufferDescriptor.SizeBytes,
|
vertexReadSize,
|
||||||
out var vertexData))
|
out var vertexData))
|
||||||
{
|
{
|
||||||
error =
|
error =
|
||||||
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
||||||
|
|
||||||
|
internal static void BeginGlobalMemoryReadScope()
|
||||||
|
{
|
||||||
|
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EndGlobalMemoryReadScope()
|
||||||
|
{
|
||||||
|
_globalMemoryReadCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadGlobalMemory(
|
private static bool TryReadGlobalMemory(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
out byte[] data)
|
out byte[] data)
|
||||||
{
|
{
|
||||||
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
|
return TryReadGlobalMemory(
|
||||||
{
|
ctx,
|
||||||
data = GC.AllocateUninitializedArray<byte>(size);
|
baseAddress,
|
||||||
if (ctx.Memory.TryRead(baseAddress, data))
|
(ulong)DefaultGlobalMemoryBindingBytes,
|
||||||
{
|
out data);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [];
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadGlobalMemory(
|
private static bool TryReadGlobalMemory(
|
||||||
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cache = _globalMemoryReadCache;
|
||||||
|
var cacheKey = (baseAddress, (int)cappedSize);
|
||||||
|
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
|
||||||
|
data = cached;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? previous;
|
||||||
|
lock (_crossFrameReadGate)
|
||||||
|
{
|
||||||
|
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadReuses);
|
||||||
|
if (cache is not null)
|
||||||
|
{
|
||||||
|
cache[cacheKey] = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = previous;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var candidateSize = (int)cappedSize;
|
var candidateSize = (int)cappedSize;
|
||||||
while (candidateSize >= sizeof(uint))
|
while (candidateSize >= sizeof(uint))
|
||||||
{
|
{
|
||||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||||
if (ctx.Memory.TryRead(baseAddress, data) ||
|
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||||
|
if (readFromPvm ||
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
||||||
{
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadCount);
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
||||||
|
if (readFromPvm)
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache is not null)
|
||||||
|
{
|
||||||
|
cache[cacheKey] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_crossFrameReadGate)
|
||||||
|
{
|
||||||
|
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
|
||||||
|
{
|
||||||
|
_crossFrameReadCacheBytes -= replaced.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
|
||||||
|
{
|
||||||
|
_crossFrameReadCache.Clear();
|
||||||
|
_crossFrameReadCacheBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_crossFrameReadCache[cacheKey] = data;
|
||||||
|
_crossFrameReadCacheBytes += data.Length;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -802,8 +802,15 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x14 => "VCmpxGtF32",
|
0x14 => "VCmpxGtF32",
|
||||||
0x15 => "VCmpxLgF32",
|
0x15 => "VCmpxLgF32",
|
||||||
0x16 => "VCmpxGeF32",
|
0x16 => "VCmpxGeF32",
|
||||||
|
0x17 => "VCmpxOF32",
|
||||||
|
0x18 => "VCmpxUF32",
|
||||||
|
0x19 => "VCmpxNgeF32",
|
||||||
|
0x1A => "VCmpxNlgF32",
|
||||||
|
0x1B => "VCmpxNgtF32",
|
||||||
|
0x1C => "VCmpxNleF32",
|
||||||
0x1D => "VCmpxNeqF32",
|
0x1D => "VCmpxNeqF32",
|
||||||
0x1E => "VCmpxNltF32",
|
0x1E => "VCmpxNltF32",
|
||||||
|
0x1F => "VCmpxTruF32",
|
||||||
0x80 => "VCmpFI32",
|
0x80 => "VCmpFI32",
|
||||||
0x81 => "VCmpLtI32",
|
0x81 => "VCmpLtI32",
|
||||||
0x82 => "VCmpEqI32",
|
0x82 => "VCmpEqI32",
|
||||||
|
|||||||
@@ -396,6 +396,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_module.Constant64(_ulongType, 32)));
|
_module.Constant64(_ulongType, 32)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VBcntU32B32":
|
||||||
|
result = IAdd(
|
||||||
|
_module.AddInstruction(
|
||||||
|
SpirvOp.BitCount,
|
||||||
|
_uintType,
|
||||||
|
GetRawSource(instruction, 0)),
|
||||||
|
GetRawSource(instruction, 1));
|
||||||
|
break;
|
||||||
case "VMadU32U24":
|
case "VMadU32U24":
|
||||||
{
|
{
|
||||||
var left = BitwiseAnd(
|
var left = BitwiseAnd(
|
||||||
@@ -593,6 +601,18 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Ext(38, _uintType, high, right));
|
Ext(38, _uintType, high, right));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VSadU32":
|
||||||
|
{
|
||||||
|
var left = GetRawSource(instruction, 0);
|
||||||
|
var right = GetRawSource(instruction, 1);
|
||||||
|
var difference = _module.AddInstruction(
|
||||||
|
SpirvOp.ISub,
|
||||||
|
_uintType,
|
||||||
|
Ext(41, _uintType, left, right),
|
||||||
|
Ext(38, _uintType, left, right));
|
||||||
|
result = IAdd(difference, GetRawSource(instruction, 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "VMed3I32":
|
case "VMed3I32":
|
||||||
{
|
{
|
||||||
var left = Bitcast(_intType, GetRawSource(instruction, 0));
|
var left = Bitcast(_intType, GetRawSource(instruction, 0));
|
||||||
@@ -702,6 +722,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
result = Ext(58, _uintType, vector);
|
result = Ext(58, _uintType, vector);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VCvtPkU16U32":
|
||||||
|
case "VCvtPkI16I32":
|
||||||
|
result = BitwiseOr(
|
||||||
|
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
|
||||||
|
ShiftLeftLogical(
|
||||||
|
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
|
||||||
|
UInt(16)));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||||
return false;
|
return false;
|
||||||
@@ -833,6 +861,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
{
|
{
|
||||||
condition = _module.ConstantBool(true);
|
condition = _module.ConstantBool(true);
|
||||||
}
|
}
|
||||||
|
else if (opcode is "VCmpOF32" or "VCmpxOF32" or "VCmpUF32" or "VCmpxUF32")
|
||||||
|
{
|
||||||
|
// The ordered/unordered predicates only test whether either
|
||||||
|
// operand is NaN. SPIR-V's OpOrdered/OpUnordered are Kernel-only,
|
||||||
|
// so build the same result from OpIsNan, which needs no extra
|
||||||
|
// capability: unordered = isnan(a) || isnan(b), ordered = !that.
|
||||||
|
var left = GetFloatSource(instruction, 0);
|
||||||
|
var right = GetFloatSource(instruction, 1);
|
||||||
|
var nanLeft = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
|
||||||
|
var nanRight = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
|
||||||
|
var unordered = _module.AddInstruction(
|
||||||
|
SpirvOp.LogicalOr,
|
||||||
|
_boolType,
|
||||||
|
nanLeft,
|
||||||
|
nanRight);
|
||||||
|
condition = opcode is "VCmpUF32" or "VCmpxUF32"
|
||||||
|
? unordered
|
||||||
|
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
|
||||||
|
}
|
||||||
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
||||||
opcode.EndsWith("F32", StringComparison.Ordinal))
|
opcode.EndsWith("F32", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -847,6 +894,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
||||||
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
||||||
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
||||||
|
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
|
||||||
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
||||||
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
||||||
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
||||||
@@ -897,7 +945,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition = _module.AddInstruction(operation, _boolType, left, right);
|
condition = _module.AddInstruction(operation, _boolType, left, right);
|
||||||
}
|
}
|
||||||
|
|
||||||
StoreWaveMask(106, condition);
|
// On gfx10, VCmpx writes EXEC only and preserves VCC; the sdst
|
||||||
|
// operand was removed from the cmpx encodings on this generation.
|
||||||
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var active = _module.AddInstruction(
|
var active = _module.AddInstruction(
|
||||||
@@ -907,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition);
|
condition);
|
||||||
StoreWaveMask(126, active);
|
StoreWaveMask(126, active);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StoreWaveMask(106, condition);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1303,6 +1356,43 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Store(_scc, IsNotZero(result));
|
Store(_scc, IsNotZero(result));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "SAbsdiffI32":
|
||||||
|
{
|
||||||
|
var wideLeft = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, left));
|
||||||
|
var wideRight = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, right));
|
||||||
|
var difference = _module.AddInstruction(
|
||||||
|
SpirvOp.ISub,
|
||||||
|
_longType,
|
||||||
|
wideLeft,
|
||||||
|
wideRight);
|
||||||
|
result = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_uintType,
|
||||||
|
Ext(5, _longType, difference));
|
||||||
|
Store(_scc, IsNotZero(result));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SPackLlB32B16":
|
||||||
|
result = BitwiseOr(
|
||||||
|
BitwiseAnd(left, UInt(0xFFFF)),
|
||||||
|
ShiftLeftLogical(right, UInt(16)));
|
||||||
|
break;
|
||||||
|
case "SPackLhB32B16":
|
||||||
|
result = BitwiseOr(
|
||||||
|
BitwiseAnd(left, UInt(0xFFFF)),
|
||||||
|
BitwiseAnd(right, UInt(0xFFFF0000)));
|
||||||
|
break;
|
||||||
|
case "SPackHhB32B16":
|
||||||
|
result = BitwiseOr(
|
||||||
|
ShiftRightLogical(left, UInt(16)),
|
||||||
|
BitwiseAnd(right, UInt(0xFFFF0000)));
|
||||||
|
break;
|
||||||
case "SCselectB32":
|
case "SCselectB32":
|
||||||
result = _module.AddInstruction(
|
result = _module.AddInstruction(
|
||||||
SpirvOp.Select,
|
SpirvOp.Select,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0)
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Pixel,
|
Gen5SpirvStage.Pixel,
|
||||||
@@ -30,7 +31,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0)
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Vertex,
|
Gen5SpirvStage.Vertex,
|
||||||
@@ -53,7 +56,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Math.Max(localSizeZ, 1),
|
Math.Max(localSizeZ, 1),
|
||||||
0,
|
0,
|
||||||
-1,
|
-1,
|
||||||
0);
|
0,
|
||||||
|
-1);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +98,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly int _globalBufferBase;
|
private readonly int _globalBufferBase;
|
||||||
private readonly int _totalGlobalBufferCount;
|
private readonly int _totalGlobalBufferCount;
|
||||||
private readonly int _imageBindingBase;
|
private readonly int _imageBindingBase;
|
||||||
|
private readonly int _scalarRegisterBufferIndex;
|
||||||
private readonly List<uint> _interfaces = [];
|
private readonly List<uint> _interfaces = [];
|
||||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||||
@@ -167,7 +173,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
int globalBufferBase,
|
int globalBufferBase,
|
||||||
int totalGlobalBufferCount,
|
int totalGlobalBufferCount,
|
||||||
int imageBindingBase)
|
int imageBindingBase,
|
||||||
|
int scalarRegisterBufferIndex)
|
||||||
{
|
{
|
||||||
_stage = stage;
|
_stage = stage;
|
||||||
_state = state;
|
_state = state;
|
||||||
@@ -181,6 +188,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
? evaluation.GlobalMemoryBindings.Count
|
? evaluation.GlobalMemoryBindings.Count
|
||||||
: totalGlobalBufferCount;
|
: totalGlobalBufferCount;
|
||||||
_imageBindingBase = imageBindingBase;
|
_imageBindingBase = imageBindingBase;
|
||||||
|
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
||||||
@@ -767,15 +775,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
private void EmitInitialState()
|
private void EmitInitialState()
|
||||||
{
|
{
|
||||||
for (uint index = 0;
|
if (_scalarRegisterBufferIndex >= 0)
|
||||||
index < _evaluation.InitialScalarRegisters.Count &&
|
|
||||||
index < ScalarRegisterCount;
|
|
||||||
index++)
|
|
||||||
{
|
{
|
||||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
for (uint index = 0; index < ScalarRegisterCount; index++)
|
||||||
if (value != 0)
|
|
||||||
{
|
{
|
||||||
StoreS(index, UInt(value));
|
StoreS(index, LoadBufferWord(_scalarRegisterBufferIndex, UInt(index)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (uint index = 0;
|
||||||
|
index < _evaluation.InitialScalarRegisters.Count &&
|
||||||
|
index < ScalarRegisterCount;
|
||||||
|
index++)
|
||||||
|
{
|
||||||
|
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||||
|
if (value != 0)
|
||||||
|
{
|
||||||
|
StoreS(index, UInt(value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,17 @@ public static class AudioOutExports
|
|||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ShutdownAllPorts()
|
||||||
|
{
|
||||||
|
foreach (var handle in Ports.Keys)
|
||||||
|
{
|
||||||
|
if (Ports.TryRemove(handle, out var port))
|
||||||
|
{
|
||||||
|
port.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
private static bool TryGetFormat(
|
||||||
int rawFormat,
|
int rawFormat,
|
||||||
out int channels,
|
out int channels,
|
||||||
|
|||||||
@@ -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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WSOuge5IsCg",
|
||||||
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
|
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 = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
|
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 = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
|
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 = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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 System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
@@ -33,6 +34,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class EventFlagWaiter
|
||||||
|
{
|
||||||
|
public OrbisGen2Result? Result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -233,9 +239,47 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
|
if (timeoutUsec == 0)
|
||||||
|
{
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||||
|
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||||
|
var timedWaiter = new EventFlagWaiter();
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
resumeHandler: () => CompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
timeoutAddress,
|
||||||
|
deadline),
|
||||||
|
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress),
|
||||||
|
blockDeadlineTimestamp: deadline))
|
||||||
|
{
|
||||||
|
state.WaitingThreads++;
|
||||||
|
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +490,92 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryCompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
long deadlineTimestamp)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is null)
|
||||||
|
{
|
||||||
|
if (IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiter.Result == 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 (int)waiter.Result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
$"event_flag:0x{handle:X16}";
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "LHMrG7e8G78",
|
Nid = "LHMrG7e8G78",
|
||||||
ExportName = "wcslen",
|
ExportName = "wcsmisc",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
public static int Wcslen(CpuContext ctx)
|
public static int Wcslen(CpuContext ctx)
|
||||||
@@ -1488,6 +1488,214 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ob5xAW4ln-0",
|
||||||
|
ExportName = "strchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The terminator counts as part of the scanned range, so strchr(s, '\0')
|
||||||
|
// returns a pointer to the string's null byte just like a native libc.
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < 1_048_576; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = address + index;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9yDWMxEFdJU",
|
||||||
|
ExportName = "strrchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strrchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong match = 0;
|
||||||
|
var found = false;
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < 1_048_576; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
match = address + index;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = found ? match : 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8u8lPzUEq+U",
|
||||||
|
ExportName = "memchr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Memchr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
|
||||||
|
var count = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (ulong index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, address + index, current))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == needle)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = address + index;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ls4tzzhimqQ",
|
||||||
|
ExportName = "strcat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strcat(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var source = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadCString(ctx, source, 1_048_576, out var sourceBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the destination terminator and re-terminate after the copied bytes.
|
||||||
|
var appendAddress = destination + (ulong)destinationBytes.Length;
|
||||||
|
var payload = new byte[sourceBytes.Length + 1];
|
||||||
|
sourceBytes.CopyTo(payload.AsSpan());
|
||||||
|
if (!TryWriteCompat(ctx, appendAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kHg45qPC6f0",
|
||||||
|
ExportName = "strncat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strncat(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var source = ctx[CpuRegister.Rsi];
|
||||||
|
var limit = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
// Bounding the source read by the count yields strncat's "at most n bytes"
|
||||||
|
// semantics while still stopping early at the source terminator.
|
||||||
|
if (!TryReadCString(ctx, source, limit, out var sourceBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appendAddress = destination + (ulong)destinationBytes.Length;
|
||||||
|
var payload = new byte[sourceBytes.Length + 1];
|
||||||
|
sourceBytes.CopyTo(payload.AsSpan());
|
||||||
|
if (!TryWriteCompat(ctx, appendAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "viiwFMaNamA",
|
||||||
|
ExportName = "strstr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strstr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var haystack = ctx[CpuRegister.Rdi];
|
||||||
|
var needle = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadCString(ctx, haystack, 1_048_576, out var haystackBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, needle, 1_048_576, out var needleBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty needle matches at the start of the haystack.
|
||||||
|
if (needleBytes.Length == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = haystack;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchIndex = haystackBytes.AsSpan().IndexOf(needleBytes.AsSpan());
|
||||||
|
ctx[CpuRegister.Rax] = matchIndex >= 0 ? haystack + (ulong)matchIndex : 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QrZZdJ8XsX0",
|
Nid = "QrZZdJ8XsX0",
|
||||||
ExportName = "fputs",
|
ExportName = "fputs",
|
||||||
@@ -1683,6 +1891,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",
|
||||||
@@ -3013,7 +3245,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "4h6F1LLbTiw",
|
Nid = "4h6F1LLbTiw",
|
||||||
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
ExportName = "sceKernelMapNamedFlexibleMemoryInternal",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
||||||
@@ -3965,7 +4197,7 @@ public static class KernelMemoryCompatExports
|
|||||||
_ => unchecked((int)argumentSource.NextGpArg())
|
_ => unchecked((int)argumentSource.NextGpArg())
|
||||||
};
|
};
|
||||||
|
|
||||||
var formatted = value.ToString();
|
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||||
if (showSign && value >= 0)
|
if (showSign && value >= 0)
|
||||||
formatted = "+" + formatted;
|
formatted = "+" + formatted;
|
||||||
else if (spaceForSign && value >= 0)
|
else if (spaceForSign && value >= 0)
|
||||||
@@ -3989,7 +4221,7 @@ public static class KernelMemoryCompatExports
|
|||||||
_ => (uint)argumentSource.NextGpArg()
|
_ => (uint)argumentSource.NextGpArg()
|
||||||
};
|
};
|
||||||
|
|
||||||
var formatted = value.ToString();
|
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||||
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -4010,8 +4242,8 @@ public static class KernelMemoryCompatExports
|
|||||||
};
|
};
|
||||||
|
|
||||||
var formatted = specifier == 'x'
|
var formatted = specifier == 'x'
|
||||||
? value.ToString("x")
|
? value.ToString("x", CultureInfo.InvariantCulture)
|
||||||
: value.ToString("X");
|
: value.ToString("X", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (alternateForm && value != 0)
|
if (alternateForm && value != 0)
|
||||||
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
||||||
@@ -4119,7 +4351,10 @@ public static class KernelMemoryCompatExports
|
|||||||
var formatStr = precision >= 0
|
var formatStr = precision >= 0
|
||||||
? $"{{0:{specifier}{precision}}}"
|
? $"{{0:{specifier}{precision}}}"
|
||||||
: $"{{0:{specifier}}}";
|
: $"{{0:{specifier}}}";
|
||||||
var formatted = string.Format(formatStr, value);
|
var formatted = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
formatStr,
|
||||||
|
value);
|
||||||
|
|
||||||
if (showSign && value >= 0)
|
if (showSign && value >= 0)
|
||||||
formatted = "+" + formatted;
|
formatted = "+" + formatted;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,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)
|
||||||
@@ -424,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",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public static class LibcInternalExports
|
|||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "NWtTN10cJzE",
|
Nid = "NWtTN10cJzE",
|
||||||
ExportName = "LibcHeapGetTraceInfo",
|
ExportName = "sceLibcHeapGetTraceInfo",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "LibcInternalExt")]
|
LibraryName = "LibcInternalExt")]
|
||||||
public static int LibcHeapGetTraceInfo(CpuContext ctx)
|
public static int LibcHeapGetTraceInfo(CpuContext ctx)
|
||||||
|
|||||||
@@ -237,6 +237,27 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1WsleK-MTkE",
|
||||||
|
ExportName = "sceNgs2GeomCalcListener",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0lbbayqDNoE",
|
||||||
|
ExportName = "sceNgs2GeomResetSourceParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Lcfo8SmpsU",
|
||||||
|
ExportName = "sceNgs2GeomResetListenerParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
|
|||||||
@@ -53,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(
|
||||||
@@ -169,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -14,13 +14,21 @@ 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;
|
||||||
|
private static int _controlsAnnouncementLogged;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hv1luiJrqQM",
|
Nid = "hv1luiJrqQM",
|
||||||
@@ -63,11 +71,15 @@ public static class PadExports
|
|||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
XInputReader.EnsureStarted();
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
{
|
||||||
: XInputReader.TryGetState(out _)
|
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
: XInputReader.TryGetState(out _)
|
||||||
|
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||||
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +197,43 @@ public static class PadExports
|
|||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadSetTriggerEffect(CpuContext ctx)
|
public static int PadSetTriggerEffect(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> parameter = stackalloc byte[120];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggerMask = parameter[0];
|
||||||
|
XInputReader.SetTriggerRumble(
|
||||||
|
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||||
|
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte DecodeTriggerVibration(ReadOnlySpan<byte> command)
|
||||||
|
{
|
||||||
|
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
|
||||||
|
var amplitude = mode switch
|
||||||
|
{
|
||||||
|
3 when command[10] != 0 => command[9],
|
||||||
|
6 when command[8] != 0 => command[9..19].ToArray().Max(),
|
||||||
|
_ => (byte)0,
|
||||||
|
};
|
||||||
|
return (byte)(Math.Min(amplitude, (byte)8) * 255 / 8);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yFVnOdGxvZY",
|
Nid = "yFVnOdGxvZY",
|
||||||
ExportName = "scePadSetVibration",
|
ExportName = "scePadSetVibration",
|
||||||
@@ -270,6 +316,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;
|
||||||
@@ -303,25 +387,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")]
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ internal static class XInputReader
|
|||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
private static byte _motorLeft;
|
private static byte _motorLeft;
|
||||||
private static byte _motorRight;
|
private static byte _motorRight;
|
||||||
|
private static byte _triggerLeft;
|
||||||
|
private static byte _triggerRight;
|
||||||
|
|
||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
internal static void EnsureStarted()
|
||||||
@@ -99,6 +101,31 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Approximates per-trigger vibration on the two XInput body motors.</summary>
|
||||||
|
internal static void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (leftTrigger is { } left)
|
||||||
|
{
|
||||||
|
changed |= _triggerLeft != left;
|
||||||
|
_triggerLeft = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTrigger is { } right)
|
||||||
|
{
|
||||||
|
changed |= _triggerRight != right;
|
||||||
|
_triggerRight = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
SendRumbleLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SendRumbleLocked()
|
private static void SendRumbleLocked()
|
||||||
{
|
{
|
||||||
if (_slot < 0)
|
if (_slot < 0)
|
||||||
@@ -108,8 +135,8 @@ internal static class XInputReader
|
|||||||
|
|
||||||
var vibration = new XInputVibration
|
var vibration = new XInputVibration
|
||||||
{
|
{
|
||||||
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
|
LeftMotorSpeed = (ushort)(Math.Max(_motorLeft, _triggerLeft) * 257),
|
||||||
RightMotorSpeed = (ushort)(_motorRight * 257),
|
RightMotorSpeed = (ushort)(Math.Max(_motorRight, _triggerRight) * 257),
|
||||||
};
|
};
|
||||||
_ = XInputSetState((uint)_slot, ref vibration);
|
_ = XInputSetState((uint)_slot, ref vibration);
|
||||||
}
|
}
|
||||||
@@ -147,6 +174,8 @@ internal static class XInputReader
|
|||||||
_slot = -1;
|
_slot = -1;
|
||||||
_motorLeft = 0;
|
_motorLeft = 0;
|
||||||
_motorRight = 0;
|
_motorRight = 0;
|
||||||
|
_triggerLeft = 0;
|
||||||
|
_triggerRight = 0;
|
||||||
_state = default;
|
_state = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
@@ -48,54 +49,161 @@ 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 volatile int _vblankPumpStopRequested;
|
||||||
|
private static volatile int _presentationWindowCloseNotified;
|
||||||
|
|
||||||
|
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 (_vblankPumpStopRequested == 0)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyPresentationWindowClosed()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] VideoOut presentation window closed");
|
||||||
|
RequestHostShutdown("videoout-window-closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyHostInterrupt()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Host interrupt requested");
|
||||||
|
RequestHostShutdown("host-interrupt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequestHostShutdown(string reason)
|
||||||
|
{
|
||||||
|
AudioOutExports.ShutdownAllPorts();
|
||||||
|
StopVblankPump();
|
||||||
|
HostSessionControl.RequestShutdown(reason);
|
||||||
|
ScheduleProcessExitIfGuestDoesNotStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScheduleProcessExitIfGuestDoesNotStop()
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(static _ =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(2000);
|
||||||
|
Environment.Exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void StopVblankPump()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _vblankPumpStopRequested, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var thread = _vblankPumpThread;
|
||||||
|
if (thread is { IsAlive: true })
|
||||||
|
{
|
||||||
|
thread.Join(TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +211,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 +221,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 +247,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 +503,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 +1127,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 +1187,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)
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ using Silk.NET.Vulkan;
|
|||||||
using Silk.NET.Vulkan.Extensions.KHR;
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
using Silk.NET.Vulkan.Extensions.EXT;
|
using Silk.NET.Vulkan.Extensions.EXT;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
using System.Diagnostics;
|
||||||
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;
|
||||||
@@ -276,6 +278,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,6 +312,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -358,6 +362,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -428,6 +433,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
renderState ?? VulkanGuestRenderState.Default),
|
renderState ?? VulkanGuestRenderState.Default),
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -600,7 +606,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 +629,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;
|
||||||
@@ -641,6 +648,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RequiredGuestWorkSequence: 0,
|
RequiredGuestWorkSequence: 0,
|
||||||
IsSplash: false,
|
IsSplash: false,
|
||||||
GuestImageAddress: address);
|
GuestImageAddress: address);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -881,6 +889,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_pendingGuestWork.Enqueue(work);
|
_pendingGuestWork.Enqueue(work);
|
||||||
_enqueuedGuestWorkSequence++;
|
_enqueuedGuestWorkSequence++;
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryTakeGuestWork(out object work)
|
private static bool TryTakeGuestWork(out object work)
|
||||||
@@ -921,6 +930,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private readonly IWindow _window;
|
private readonly IWindow _window;
|
||||||
private const int MaxInFlightGuestSubmissions = 8;
|
private const int MaxInFlightGuestSubmissions = 8;
|
||||||
|
private const double PerformanceHudSampleSeconds = 0.5;
|
||||||
|
private const uint ThreadQueryLimitedInformation = 0x0800;
|
||||||
|
private static readonly bool _performanceHudEnabled =
|
||||||
|
!string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_PERF_HUD"),
|
||||||
|
"0",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private Vk _vk = null!;
|
private Vk _vk = null!;
|
||||||
private KhrSurface _surfaceApi = null!;
|
private KhrSurface _surfaceApi = null!;
|
||||||
private KhrSwapchain _swapchainApi = null!;
|
private KhrSwapchain _swapchainApi = null!;
|
||||||
@@ -933,6 +949,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,10 +968,24 @@ 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;
|
||||||
private long _presentedSequence;
|
private long _presentedSequence;
|
||||||
|
private long _performanceHudLastTimestamp;
|
||||||
|
private TimeSpan _performanceHudLastProcessCpu;
|
||||||
|
private long _performanceHudPresentedFrames;
|
||||||
|
private long _performanceHudLastPresentedFrames;
|
||||||
|
private long _performanceHudLastReadCount;
|
||||||
|
private long _performanceHudLastReadBytes;
|
||||||
|
private long _performanceHudLastReadHits;
|
||||||
|
private long _performanceHudLastReadPvmBytes;
|
||||||
|
private long _performanceHudLastReadLibcBytes;
|
||||||
|
private readonly Dictionary<int, TimeSpan> _performanceHudThreadCpu = [];
|
||||||
|
private readonly Dictionary<int, string> _performanceHudThreadNames = [];
|
||||||
private bool _vulkanReady;
|
private bool _vulkanReady;
|
||||||
private bool _firstFramePresented;
|
private bool _firstFramePresented;
|
||||||
private bool _firstGuestDrawPresented;
|
private bool _firstGuestDrawPresented;
|
||||||
@@ -966,6 +998,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 +1024,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,
|
||||||
@@ -1110,13 +1146,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
||||||
options.Title = VideoOutExports.GetWindowTitle();
|
options.Title = VideoOutExports.GetWindowTitle();
|
||||||
options.WindowBorder = WindowBorder.Fixed;
|
options.WindowBorder = WindowBorder.Fixed;
|
||||||
options.VSync = true;
|
// FIFO already provides the presentation clock. Throttling Silk's render loop
|
||||||
options.FramesPerSecond = 60;
|
// as well can miss alternating vblanks and collapse delivery to 30 FPS or less.
|
||||||
options.UpdatesPerSecond = 60;
|
options.VSync = false;
|
||||||
|
options.FramesPerSecond = 0;
|
||||||
|
options.UpdatesPerSecond = 0;
|
||||||
_window = Window.Create(options);
|
_window = Window.Create(options);
|
||||||
_window.Load += Initialize;
|
_window.Load += Initialize;
|
||||||
_window.Render += Render;
|
_window.Render += Render;
|
||||||
_window.Closing += DisposeVulkan;
|
_window.Closing += OnWindowClosing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowClosing()
|
||||||
|
{
|
||||||
|
VideoOutExports.NotifyPresentationWindowClosed();
|
||||||
|
DisposeVulkan();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run() => _window.Run();
|
public void Run() => _window.Run();
|
||||||
@@ -1150,6 +1194,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CreateSurface();
|
CreateSurface();
|
||||||
SelectPhysicalDevice();
|
SelectPhysicalDevice();
|
||||||
CreateDevice();
|
CreateDevice();
|
||||||
|
CreatePipelineCache();
|
||||||
CreateSwapchain();
|
CreateSwapchain();
|
||||||
CreateCommandResources();
|
CreateCommandResources();
|
||||||
CreateGuestDrawResources();
|
CreateGuestDrawResources();
|
||||||
@@ -1559,8 +1604,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 +1791,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(
|
||||||
@@ -1764,6 +1916,30 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var surfaceFormat = ChooseSurfaceFormat(formats);
|
var surfaceFormat = ChooseSurfaceFormat(formats);
|
||||||
_swapchainFormat = surfaceFormat.Format;
|
_swapchainFormat = surfaceFormat.Format;
|
||||||
_extent = ChooseExtent(capabilities);
|
_extent = ChooseExtent(capabilities);
|
||||||
|
uint presentModeCount = 0;
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||||
|
_physicalDevice,
|
||||||
|
_surface,
|
||||||
|
&presentModeCount,
|
||||||
|
null),
|
||||||
|
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||||
|
var presentModes = new PresentModeKHR[presentModeCount];
|
||||||
|
fixed (PresentModeKHR* presentModePointer = presentModes)
|
||||||
|
{
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||||
|
_physicalDevice,
|
||||||
|
_surface,
|
||||||
|
&presentModeCount,
|
||||||
|
presentModePointer),
|
||||||
|
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||||
|
}
|
||||||
|
|
||||||
|
var presentMode = presentModes.Contains(PresentModeKHR.MailboxKhr)
|
||||||
|
? PresentModeKHR.MailboxKhr
|
||||||
|
: PresentModeKHR.FifoKhr;
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Vulkan present mode: {presentMode}");
|
||||||
var imageCount = capabilities.MinImageCount + 1;
|
var imageCount = capabilities.MinImageCount + 1;
|
||||||
if (capabilities.MaxImageCount != 0)
|
if (capabilities.MaxImageCount != 0)
|
||||||
{
|
{
|
||||||
@@ -1787,7 +1963,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageSharingMode = SharingMode.Exclusive,
|
ImageSharingMode = SharingMode.Exclusive,
|
||||||
PreTransform = capabilities.CurrentTransform,
|
PreTransform = capabilities.CurrentTransform,
|
||||||
CompositeAlpha = compositeAlpha,
|
CompositeAlpha = compositeAlpha,
|
||||||
PresentMode = PresentModeKHR.FifoKhr,
|
PresentMode = presentMode,
|
||||||
Clipped = true,
|
Clipped = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1835,9 +2011,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 +2395,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(
|
Check(
|
||||||
_vk.CreateGraphicsPipelines(
|
_vk.CreateGraphicsPipelines(
|
||||||
_device,
|
_device,
|
||||||
default,
|
_pipelineCache,
|
||||||
1,
|
1,
|
||||||
&pipelineInfo,
|
&pipelineInfo,
|
||||||
null,
|
null,
|
||||||
@@ -2219,6 +2430,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 +2497,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 +2799,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 +2961,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(
|
Check(
|
||||||
_vk.CreateGraphicsPipelines(
|
_vk.CreateGraphicsPipelines(
|
||||||
_device,
|
_device,
|
||||||
default,
|
_pipelineCache,
|
||||||
1,
|
1,
|
||||||
&pipelineInfo,
|
&pipelineInfo,
|
||||||
null,
|
null,
|
||||||
@@ -2922,7 +3141,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(
|
Check(
|
||||||
_vk.CreateComputePipelines(
|
_vk.CreateComputePipelines(
|
||||||
_device,
|
_device,
|
||||||
default,
|
_pipelineCache,
|
||||||
1,
|
1,
|
||||||
&pipelineInfo,
|
&pipelineInfo,
|
||||||
null,
|
null,
|
||||||
@@ -3448,11 +3667,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 +3709,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 +4730,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} " +
|
||||||
@@ -5075,6 +5317,219 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void UpdatePerformanceHud()
|
||||||
|
{
|
||||||
|
if (!_performanceHudEnabled || !OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_performanceHudLastTimestamp != 0 &&
|
||||||
|
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds <
|
||||||
|
PerformanceHudSampleSeconds)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = Process.GetCurrentProcess();
|
||||||
|
var processCpu = process.TotalProcessorTime;
|
||||||
|
var currentThreadCpu = new Dictionary<int, TimeSpan>();
|
||||||
|
var currentThreadIds = new HashSet<int>();
|
||||||
|
var hottestThreadId = 0;
|
||||||
|
var hottestThreadCpuSeconds = 0.0;
|
||||||
|
|
||||||
|
foreach (ProcessThread thread in process.Threads)
|
||||||
|
{
|
||||||
|
using (thread)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var threadId = thread.Id;
|
||||||
|
var cpu = thread.TotalProcessorTime;
|
||||||
|
currentThreadIds.Add(threadId);
|
||||||
|
currentThreadCpu[threadId] = cpu;
|
||||||
|
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
|
||||||
|
{
|
||||||
|
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
|
||||||
|
if (deltaSeconds > hottestThreadCpuSeconds)
|
||||||
|
{
|
||||||
|
hottestThreadCpuSeconds = deltaSeconds;
|
||||||
|
hottestThreadId = threadId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_performanceHudLastTimestamp != 0)
|
||||||
|
{
|
||||||
|
var elapsedSeconds = Math.Max(
|
||||||
|
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds,
|
||||||
|
0.001);
|
||||||
|
var processCpuPercent = Math.Max(
|
||||||
|
0.0,
|
||||||
|
(processCpu - _performanceHudLastProcessCpu).TotalSeconds /
|
||||||
|
elapsedSeconds /
|
||||||
|
Math.Max(Environment.ProcessorCount, 1) *
|
||||||
|
100.0);
|
||||||
|
var hottestThreadPercent = hottestThreadCpuSeconds / elapsedSeconds * 100.0;
|
||||||
|
var presentedFrames = _performanceHudPresentedFrames;
|
||||||
|
var fps = (presentedFrames - _performanceHudLastPresentedFrames) / elapsedSeconds;
|
||||||
|
var hotName = hottestThreadId == 0
|
||||||
|
? "idle"
|
||||||
|
: GetPerformanceThreadName(hottestThreadId);
|
||||||
|
long guestBacklog;
|
||||||
|
int queuedGuestWork;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
guestBacklog = Math.Max(
|
||||||
|
0,
|
||||||
|
_enqueuedGuestWorkSequence - _completedGuestWorkSequence);
|
||||||
|
queuedGuestWork = _pendingGuestWork.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gpuInFlight = _pendingGuestSubmissions.Count +
|
||||||
|
(_presentationInFlight ? 1 : 0);
|
||||||
|
var readCount = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
|
||||||
|
var readBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
|
||||||
|
var readHits = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
|
||||||
|
var readPvmBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
|
||||||
|
var readLibcBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
|
||||||
|
var readsPerSecond =
|
||||||
|
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
|
||||||
|
var readMbPerSecond =
|
||||||
|
(readBytes - _performanceHudLastReadBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
var readHitsPerSecond =
|
||||||
|
(readHits - _performanceHudLastReadHits) / elapsedSeconds;
|
||||||
|
var readPvmMbPerSecond =
|
||||||
|
(readPvmBytes - _performanceHudLastReadPvmBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
var readLibcMbPerSecond =
|
||||||
|
(readLibcBytes - _performanceHudLastReadLibcBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
_performanceHudLastReadCount = readCount;
|
||||||
|
_performanceHudLastReadBytes = readBytes;
|
||||||
|
_performanceHudLastReadHits = readHits;
|
||||||
|
_performanceHudLastReadPvmBytes = readPvmBytes;
|
||||||
|
_performanceHudLastReadLibcBytes = readLibcBytes;
|
||||||
|
_window.Title =
|
||||||
|
$"FPS {fps:0.0} CPU {processCpuPercent:0}% | " +
|
||||||
|
$"HOT {hotName}#{hottestThreadId} {hottestThreadPercent:0}% | " +
|
||||||
|
$"WORK {guestBacklog} (q{queuedGuestWork}/gpu{gpuInFlight}) | " +
|
||||||
|
$"RD {readsPerSecond:0}/s {readMbPerSecond:0}MB/s h{readHitsPerSecond:0}/s " +
|
||||||
|
$"P{readPvmMbPerSecond:0} L{readLibcMbPerSecond:0} | " +
|
||||||
|
VideoOutExports.GetWindowTitle();
|
||||||
|
_performanceHudLastPresentedFrames = presentedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudThreadCpu.Clear();
|
||||||
|
foreach (var (threadId, cpu) in currentThreadCpu)
|
||||||
|
{
|
||||||
|
_performanceHudThreadCpu[threadId] = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var staleThreadId in _performanceHudThreadNames.Keys
|
||||||
|
.Where(threadId => !currentThreadIds.Contains(threadId))
|
||||||
|
.ToArray())
|
||||||
|
{
|
||||||
|
_performanceHudThreadNames.Remove(staleThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudLastProcessCpu = processCpu;
|
||||||
|
_performanceHudLastTimestamp = now;
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is InvalidOperationException or System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
_performanceHudLastTimestamp = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPerformanceThreadName(int threadId)
|
||||||
|
{
|
||||||
|
if (_performanceHudThreadNames.TryGetValue(threadId, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = "tid";
|
||||||
|
var handle = OpenThread(ThreadQueryLimitedInformation, false, (uint)threadId);
|
||||||
|
if (handle != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (GetThreadDescription(handle, out var description) >= 0 && description != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var described = Marshal.PtrToStringUni(description);
|
||||||
|
if (!string.IsNullOrWhiteSpace(described))
|
||||||
|
{
|
||||||
|
name = described.Length <= 28 ? described : described[..28];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LocalFree(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudThreadNames[threadId] = name;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern nint OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern int GetThreadDescription(nint thread, out nint description);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern nint LocalFree(nint memory);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||||
|
private static extern bool CloseHandle(nint handle);
|
||||||
|
|
||||||
|
private void WaitForRenderWork()
|
||||||
|
{
|
||||||
|
var gpuWorkInFlight = _pendingGuestSubmissions.Count > 0 || _presentationInFlight;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed ||
|
||||||
|
_pendingGuestWork.Count > 0 ||
|
||||||
|
(_latestPresentation is { } latest &&
|
||||||
|
latest.Sequence != _presentedSequence &&
|
||||||
|
latest.RequiredGuestWorkSequence <= _completedGuestWorkSequence))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Monitor.Wait(_gate, gpuWorkInFlight ? 1 : 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Render(double _)
|
private void Render(double _)
|
||||||
{
|
{
|
||||||
if (!_vulkanReady)
|
if (!_vulkanReady)
|
||||||
@@ -5082,6 +5537,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WaitForRenderWork();
|
||||||
|
UpdatePerformanceHud();
|
||||||
|
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
if (!_deviceLost)
|
if (!_deviceLost)
|
||||||
{
|
{
|
||||||
@@ -5125,6 +5583,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 +5632,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 +5764,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 +5789,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 +5802,14 @@ 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();
|
||||||
|
_performanceHudPresentedFrames++;
|
||||||
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;
|
||||||
@@ -6027,59 +6493,34 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
offsets);
|
offsets);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint maxPixelsPerDraw = 512 * 512;
|
var scissor = new Rect2D(
|
||||||
var rowsPerDraw = Math.Max(
|
new Offset2D(drawScissor.X, drawScissor.Y),
|
||||||
1u,
|
new Extent2D(drawScissor.Width, drawScissor.Height));
|
||||||
Math.Min(drawScissor.Height, maxPixelsPerDraw / Math.Max(drawScissor.Width, 1u)));
|
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
||||||
var drawCount = 0u;
|
|
||||||
for (var y = 0u; y < drawScissor.Height; y += rowsPerDraw)
|
if (resources.IndexBuffer.Handle != 0)
|
||||||
{
|
{
|
||||||
var scissor = new Rect2D(
|
_vk.CmdBindIndexBuffer(
|
||||||
new Offset2D(
|
_commandBuffer,
|
||||||
drawScissor.X,
|
resources.IndexBuffer,
|
||||||
checked(drawScissor.Y + (int)y)),
|
0,
|
||||||
new Extent2D(
|
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
||||||
drawScissor.Width,
|
_vk.CmdDrawIndexed(
|
||||||
Math.Min(rowsPerDraw, drawScissor.Height - y)));
|
_commandBuffer,
|
||||||
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
resources.VertexCount,
|
||||||
|
resources.InstanceCount,
|
||||||
if (resources.IndexBuffer.Handle != 0)
|
0,
|
||||||
{
|
0,
|
||||||
_vk.CmdBindIndexBuffer(
|
0);
|
||||||
_commandBuffer,
|
|
||||||
resources.IndexBuffer,
|
|
||||||
0,
|
|
||||||
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
|
||||||
_vk.CmdDrawIndexed(
|
|
||||||
_commandBuffer,
|
|
||||||
resources.VertexCount,
|
|
||||||
resources.InstanceCount,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_vk.CmdDraw(
|
|
||||||
_commandBuffer,
|
|
||||||
resources.VertexCount,
|
|
||||||
resources.InstanceCount,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCount++;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (drawCount > 1)
|
|
||||||
{
|
{
|
||||||
TraceVulkanShader(
|
_vk.CmdDraw(
|
||||||
$"vk.graphics_chunked target={extent.Width}x{extent.Height} " +
|
_commandBuffer,
|
||||||
$"draws={drawCount} rows={rowsPerDraw} " +
|
resources.VertexCount,
|
||||||
$"scissor={drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height} " +
|
resources.InstanceCount,
|
||||||
$"viewport={drawViewport.X:0.###},{drawViewport.Y:0.###}," +
|
0,
|
||||||
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###} " +
|
0);
|
||||||
$"name={resources.DebugName}");
|
|
||||||
}
|
}
|
||||||
_vk.CmdEndRenderPass(_commandBuffer);
|
_vk.CmdEndRenderPass(_commandBuffer);
|
||||||
}
|
}
|
||||||
@@ -6597,7 +7038,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 +7051,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 +7147,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 +7181,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// Synthetic-shader conformance dumper.
|
||||||
|
//
|
||||||
|
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
|
||||||
|
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
|
||||||
|
// reflection so no emulator source changes are required) and writes the
|
||||||
|
// resulting vertex and compute SPIR-V blobs to disk. The blobs can then be
|
||||||
|
// checked with spirv-val / spirv-dis.
|
||||||
|
//
|
||||||
|
// Programs that contain buffer_store_dword automatically get a single
|
||||||
|
// global-memory binding covering every store, which the emitter exposes as
|
||||||
|
// guestBuffers[0] (descriptor set 0, binding 0).
|
||||||
|
//
|
||||||
|
// Each program carries an expectation: ExpectTranslate=true programs must
|
||||||
|
// decode and emit both stages; ExpectTranslate=false programs pin a decode
|
||||||
|
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
||||||
|
// non-zero, so it can gate scripts/CI.
|
||||||
|
//
|
||||||
|
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Reflection;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.CxxAbi;
|
||||||
|
|
||||||
|
const ulong ProgramAddress = 0x100000;
|
||||||
|
|
||||||
|
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
|
||||||
|
[
|
||||||
|
("fmac", true, [
|
||||||
|
0x560A0501, // v_fmac_f32 v5, v1, v2
|
||||||
|
0x580A0501, 0x42280000, // v_fmamk_f32 v5, v1, 42.0, v2
|
||||||
|
0x5A0A0501, 0x42280000, // v_fmaak_f32 v5, v1, v2, 42.0
|
||||||
|
0xD52B0005, 0x00020501, // v_fmac_f32_e64 v5, v1, v2
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("muls", true, [
|
||||||
|
0xD5690005, 0x00020501, // v_mul_lo_u32 v5, v1, v2
|
||||||
|
0xD56A0005, 0x00020501, // v_mul_hi_u32 v5, v1, v2
|
||||||
|
0xD56B0005, 0x00020501, // v_mul_lo_i32 v5, v1, v2
|
||||||
|
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
("sopp-hints", true, [
|
||||||
|
0xBFA10001, // s_clause 0x1
|
||||||
|
0xBFA30000, // s_waitcnt_depctr 0x0
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
// s_round_mode / s_denorm_mode write the FP MODE state and must keep
|
||||||
|
// failing decode loudly until their semantics are modeled (see #108);
|
||||||
|
// this program pins that behavior.
|
||||||
|
("sopp-mode", false, [
|
||||||
|
0xBFA40000, // s_round_mode 0x0
|
||||||
|
0xBFA50000, // s_denorm_mode 0x0
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
// Executable end-to-end test: compute with real ALU instructions, then
|
||||||
|
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
|
||||||
|
// that a store with EXEC=0 does not land (offset 12 stays sentinel), and
|
||||||
|
// that stores work again after EXEC is restored (offset 16).
|
||||||
|
("exec", true, [
|
||||||
|
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
||||||
|
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
||||||
|
0x7E0202FF, 0x40100000, // v_mov_b32 v1, 2.25f
|
||||||
|
0x7E0402FF, 0x41200000, // v_mov_b32 v2, 10.0f
|
||||||
|
0x56040300, // v_fmac_f32 v2, v0, v1 -> v2 = fma(1.5, 2.25, 10.0)
|
||||||
|
0x7E0602FF, 0x7FFFFFFF, // v_mov_b32 v3, 0x7FFFFFFF
|
||||||
|
0x7E0802FF, 0x00010003, // v_mov_b32 v4, 0x00010003
|
||||||
|
0xD56C0005, 0x00020903, // v_mul_hi_i32 v5, v3, v4
|
||||||
|
0xD56B0006, 0x00020903, // v_mul_lo_i32 v6, v3, v4
|
||||||
|
0xE0700000, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0
|
||||||
|
0xE0700004, 0x80020500, // buffer_store_dword v5, off, s[8:11], 0 offset:4
|
||||||
|
0xE0700008, 0x80020600, // buffer_store_dword v6, off, s[8:11], 0 offset:8
|
||||||
|
0xBEFE0380, // s_mov_b32 exec_lo, 0 -> lane inactive
|
||||||
|
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
|
||||||
|
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
|
||||||
|
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
|
||||||
|
0xBF810000, // s_endpgm
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
var assembly = typeof(CxaGuardExports).Assembly;
|
||||||
|
var shaderTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderTranslator")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator not found");
|
||||||
|
var spirvTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5SpirvTranslator")
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator not found");
|
||||||
|
var describe = shaderTranslator.GetMethod(
|
||||||
|
"Describe",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator.Describe not found");
|
||||||
|
var tryDecode = shaderTranslator.GetMethod(
|
||||||
|
"TryDecodeProgram",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderTranslator.TryDecodeProgram not found");
|
||||||
|
var stateType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderState")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderState not found");
|
||||||
|
var evaluationType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderEvaluation")
|
||||||
|
?? throw new InvalidOperationException("Gen5ShaderEvaluation not found");
|
||||||
|
var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5ImageBinding not found");
|
||||||
|
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
|
||||||
|
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
|
||||||
|
var tryCompile = spirvTranslator.GetMethod(
|
||||||
|
"TryCompileVertexShader",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
|
||||||
|
var tryCompileCompute = spirvTranslator.GetMethod(
|
||||||
|
"TryCompileComputeShader",
|
||||||
|
BindingFlags.Public | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileComputeShader not found");
|
||||||
|
|
||||||
|
var outputDirectory = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: Path.Combine(AppContext.BaseDirectory, "spv");
|
||||||
|
Directory.CreateDirectory(outputDirectory);
|
||||||
|
|
||||||
|
var failures = 0;
|
||||||
|
foreach (var (name, expectTranslate, words) in testPrograms)
|
||||||
|
{
|
||||||
|
var memory = new FakeMemory();
|
||||||
|
memory.AddRegion(ProgramAddress, words);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[{name}] decode: " +
|
||||||
|
(string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!);
|
||||||
|
|
||||||
|
object?[] decodeArgs = [ctx, ProgramAddress, null, null];
|
||||||
|
if (!(bool)tryDecode.Invoke(null, decodeArgs)!)
|
||||||
|
{
|
||||||
|
if (expectTranslate)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectTranslate)
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[{name}] FAILED: decoded successfully but is pinned as a decode failure — " +
|
||||||
|
"if the new decode support is intentional, its semantics need verifying here first");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer stores need a global-memory binding; the emitter resolves them by
|
||||||
|
// instruction PC, so collect store PCs from the decoded program itself.
|
||||||
|
var programObj = decodeArgs[2]!;
|
||||||
|
var instructions = (System.Collections.IEnumerable)programObj
|
||||||
|
.GetType().GetProperty("Instructions")!.GetValue(programObj)!;
|
||||||
|
var storePcs = new List<uint>();
|
||||||
|
foreach (var instruction in instructions)
|
||||||
|
{
|
||||||
|
var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!;
|
||||||
|
if (op.StartsWith("BufferStore", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The binding's scalar base (8 -> s[8:11]) must match the srsrc field of
|
||||||
|
// the hand-assembled buffer_store words, and the 64-byte backing store
|
||||||
|
// must cover every hand-assembled store offset.
|
||||||
|
var globalBindings = Array.CreateInstance(globalBindingType, storePcs.Count > 0 ? 1 : 0);
|
||||||
|
if (storePcs.Count > 0)
|
||||||
|
{
|
||||||
|
globalBindings.SetValue(
|
||||||
|
Activator.CreateInstance(
|
||||||
|
globalBindingType,
|
||||||
|
8u,
|
||||||
|
0UL,
|
||||||
|
(IReadOnlyList<uint>)storePcs,
|
||||||
|
new byte[64]),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = Activator.CreateInstance(
|
||||||
|
stateType,
|
||||||
|
programObj,
|
||||||
|
new uint[16],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0u)!;
|
||||||
|
var evaluation = Activator.CreateInstance(
|
||||||
|
evaluationType,
|
||||||
|
new uint[256],
|
||||||
|
new uint[256],
|
||||||
|
new Dictionary<uint, IReadOnlyList<uint>>(),
|
||||||
|
Array.CreateInstance(imageBindingType, 0),
|
||||||
|
globalBindings,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)!;
|
||||||
|
|
||||||
|
object?[] compileArgs = [state, evaluation, null, null, 0, -1, 0];
|
||||||
|
if ((bool)tryCompile.Invoke(null, compileArgs)!)
|
||||||
|
{
|
||||||
|
var shader = compileArgs[2]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
object?[] computeArgs = [state, evaluation, 1u, 1u, 1u, null, null];
|
||||||
|
if ((bool)tryCompileCompute.Invoke(null, computeArgs)!)
|
||||||
|
{
|
||||||
|
var shader = computeArgs[5]!;
|
||||||
|
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
|
||||||
|
var path = Path.Combine(outputDirectory, $"{name}-cs.spv");
|
||||||
|
File.WriteAllBytes(path, spirv);
|
||||||
|
Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failures++;
|
||||||
|
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(failures == 0
|
||||||
|
? "RESULT: all programs behaved as expected"
|
||||||
|
: $"RESULT: {failures} unexpected outcome(s)");
|
||||||
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
||||||
|
|
||||||
|
internal sealed class FakeMemory : ICpuMemory
|
||||||
|
{
|
||||||
|
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
||||||
|
|
||||||
|
public void AddRegion(ulong baseAddress, uint[] words)
|
||||||
|
{
|
||||||
|
var bytes = new byte[words.Length * sizeof(uint)];
|
||||||
|
for (var index = 0; index < words.Length; index++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
|
bytes.AsSpan(index * sizeof(uint)),
|
||||||
|
words[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_regions.Add((baseAddress, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
foreach (var (baseAddress, data) in _regions)
|
||||||
|
{
|
||||||
|
if (virtualAddress >= baseAddress &&
|
||||||
|
virtualAddress + (ulong)destination.Length <= baseAddress + (ulong)data.Length)
|
||||||
|
{
|
||||||
|
data.AsSpan(
|
||||||
|
(int)(virtualAddress - baseAddress),
|
||||||
|
destination.Length).CopyTo(destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<!-- Standalone dev tool: opt out of the repo-wide lock-file requirement
|
||||||
|
so no packages.lock.json is generated or committed for it. -->
|
||||||
|
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user