mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61a97baf85 | |||
| e80f96ecf5 | |||
| d49c0f1f10 | |||
| 1d33ef90fc | |||
| 26a570633c | |||
| e4f89445b9 | |||
| 1254cc1564 | |||
| 503b3f4d6b | |||
| 6b37ab54f2 | |||
| a84d2344fb | |||
| 787d3a1efb | |||
| 884584da67 | |||
| d43edc865a | |||
| cf6964710a | |||
| 4f028d0483 | |||
| 4db98bd8fe | |||
| 4600a2ed1f |
@@ -3,8 +3,8 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
import struct
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
@@ -21,7 +21,7 @@ def name2nid(name):
|
||||
def generate():
|
||||
names_path = Path(NAMES)
|
||||
output_path = Path(OUTPUT)
|
||||
|
||||
|
||||
entries = []
|
||||
with open(names_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -29,12 +29,12 @@ def generate():
|
||||
if name:
|
||||
nid = name2nid(name)
|
||||
entries.append((nid, name))
|
||||
|
||||
|
||||
print(f"Found {len(entries)} entries")
|
||||
|
||||
data = bytearray()
|
||||
data.extend(struct.pack('<I', len(entries)))
|
||||
|
||||
|
||||
for nid, name in entries:
|
||||
nid_bytes = nid.encode('utf-8')
|
||||
name_bytes = name.encode('utf-8')
|
||||
@@ -42,10 +42,11 @@ def generate():
|
||||
data.extend(nid_bytes)
|
||||
data.extend(struct.pack('<H', len(name_bytes)))
|
||||
data.extend(name_bytes)
|
||||
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||
print(f"Total entries: {len(entries)}")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>0.0.1</Version>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
|
||||
@@ -161,7 +161,7 @@ public sealed partial class DirectExecutionBackend
|
||||
*(ulong*)(xmmSlot + 8));
|
||||
}
|
||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
|
||||
{
|
||||
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
|
||||
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
|
||||
@@ -245,12 +245,13 @@ public sealed partial class DirectExecutionBackend
|
||||
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
|
||||
bool flag5 = false;
|
||||
ExportedFunction? matchedExport = importStubEntry.Export;
|
||||
var traceFlags = importStubEntry.TraceFlags;
|
||||
bool periodicTrace = num <= 128 ||
|
||||
(num >= 240 && num <= 400) ||
|
||||
(num >= 900 && num <= 1300) ||
|
||||
num % 100000 == 0L ||
|
||||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
|
||||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
|
||||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
|
||||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
|
||||
flag ||
|
||||
flag2 ||
|
||||
flag3;
|
||||
@@ -311,15 +312,15 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rsi],
|
||||
cpuContext[CpuRegister.Rdx]);
|
||||
}
|
||||
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
||||
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
|
||||
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
|
||||
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
|
||||
}
|
||||
@@ -349,7 +350,7 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
if (importStubEntry.Nid == "Ou3iL1abvng")
|
||||
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
|
||||
{
|
||||
if (_logStackCheck)
|
||||
{
|
||||
@@ -381,7 +382,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ActiveGuestReturnSlotAddress);
|
||||
try
|
||||
{
|
||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
|
||||
{
|
||||
if (_logBootstrap)
|
||||
{
|
||||
@@ -390,12 +391,11 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
orbisGen2Result = DispatchBootstrapBridge();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
||||
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
|
||||
{
|
||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
|
||||
{
|
||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||
}
|
||||
@@ -736,6 +736,9 @@ public sealed partial class DirectExecutionBackend
|
||||
var expectedEqueueTimeout =
|
||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
var expectedEventFlagTimeout =
|
||||
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
var expectedMutexTrylockBusy =
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
@@ -748,6 +751,7 @@ public sealed partial class DirectExecutionBackend
|
||||
if (!expectedFileProbeMiss &&
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedEventFlagTimeout &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter)
|
||||
@@ -818,6 +822,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
@@ -876,7 +881,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"j4ViWNHEgww" or // strlen
|
||||
"5jNubw4vlAA" or // strnlen
|
||||
"LHMrG7e8G78" or // wcslen
|
||||
"LHMrG7e8G78" or // wcsmisc
|
||||
"WkkeywLJcgU" or // wcslen
|
||||
"Ovb2dSJOAuE" or // strcmp
|
||||
"aesyjrHVWy4" or // strncmp
|
||||
@@ -1076,7 +1081,15 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
|
||||
nid switch
|
||||
{
|
||||
"1jfXLRVzisc" => true, // sceKernelUsleep
|
||||
"QcteRwbsnV0" => true, // usleep
|
||||
"n88vx3C5nW8" => true, // gettimeofday
|
||||
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
|
||||
"T72hz6ffq08" => true, // scePthreadYield
|
||||
_ => false
|
||||
};
|
||||
|
||||
private void ResetImportLoopPattern()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private const int DefaultImportLoopGuardSeconds = 5;
|
||||
|
||||
private enum ImportStubKind : byte
|
||||
{
|
||||
Normal = 0,
|
||||
BootstrapBridge = 1,
|
||||
KernelDynlibDlsym = 2,
|
||||
Il2CppApiLookupSymbol = 3,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum ImportStubTraceFlags : byte
|
||||
{
|
||||
None = 0,
|
||||
Memset = 1 << 0,
|
||||
CxaAtexit = 1 << 1,
|
||||
RawArgs = 1 << 2,
|
||||
StackChkFail = 1 << 3,
|
||||
PeriodicEvery1000 = 1 << 4,
|
||||
PeriodicEvery128 = 1 << 5,
|
||||
}
|
||||
|
||||
private readonly struct ImportStubEntry
|
||||
{
|
||||
public ulong Address { get; }
|
||||
@@ -33,12 +53,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public ExportedFunction? Export { get; }
|
||||
|
||||
public ImportStubKind Kind { get; }
|
||||
|
||||
public ImportStubTraceFlags TraceFlags { get; }
|
||||
|
||||
public ImportStubEntry(ulong address, string nid, ExportedFunction? export)
|
||||
{
|
||||
Address = address;
|
||||
Nid = nid;
|
||||
Export = export;
|
||||
Kind = ClassifyKind(nid);
|
||||
TraceFlags = ClassifyTraceFlags(nid);
|
||||
}
|
||||
|
||||
private static ImportStubKind ClassifyKind(string nid) => nid switch
|
||||
{
|
||||
RuntimeStubNids.BootstrapBridge => ImportStubKind.BootstrapBridge,
|
||||
RuntimeStubNids.KernelDynlibDlsym or "LwG8g3niqwA" => ImportStubKind.KernelDynlibDlsym,
|
||||
"r8mvOaWdi28" => ImportStubKind.Il2CppApiLookupSymbol,
|
||||
_ => ImportStubKind.Normal,
|
||||
};
|
||||
|
||||
private static ImportStubTraceFlags ClassifyTraceFlags(string nid) => nid switch
|
||||
{
|
||||
"8zTFvBIAIN8" => ImportStubTraceFlags.Memset,
|
||||
"tsvEmnenz48" => ImportStubTraceFlags.CxaAtexit | ImportStubTraceFlags.PeriodicEvery1000,
|
||||
"bzQExy189ZI" or "8G2LB+A3rzg" => ImportStubTraceFlags.RawArgs,
|
||||
"Ou3iL1abvng" => ImportStubTraceFlags.StackChkFail,
|
||||
"rTXw65xmLIA" => ImportStubTraceFlags.PeriodicEvery128,
|
||||
_ => ImportStubTraceFlags.None,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly record struct RecentImportTraceEntry(
|
||||
@@ -125,6 +169,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
|
||||
|
||||
private const int GuestThreadRegionSlotCount = 256;
|
||||
|
||||
private const uint PAGE_EXECUTE_READWRITE = 64u;
|
||||
|
||||
private const uint PAGE_READWRITE = 4u;
|
||||
@@ -2757,7 +2803,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
if (hostThread is not null &&
|
||||
!ReferenceEquals(hostThread, Thread.CurrentThread))
|
||||
{
|
||||
hostThread.Join(1);
|
||||
// The handle is published before the host thread starts.
|
||||
if ((hostThread.ThreadState & System.Threading.ThreadState.Unstarted) != 0)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
hostThread.Join(1);
|
||||
}
|
||||
catch (ThreadStateException)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3446,7 +3506,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
out ulong mappedBase,
|
||||
out string? error)
|
||||
{
|
||||
for (int i = 0; i < 64; i++)
|
||||
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
|
||||
{
|
||||
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
|
||||
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
|
||||
@@ -3480,7 +3540,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
out ulong tlsBase,
|
||||
out string? error)
|
||||
{
|
||||
for (int i = 0; i < 64; i++)
|
||||
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
|
||||
{
|
||||
var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride);
|
||||
var mappedBase = candidateBase - GuestThreadTlsPrefixSize;
|
||||
|
||||
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
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);
|
||||
}
|
||||
catch (JsonException)
|
||||
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
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 =
|
||||
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
||||
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
||||
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
||||
GetString(root, "contentVersion")
|
||||
?? GetString(root, "masterVersion")
|
||||
?? GetString(root, "targetContentVersion");
|
||||
|
||||
string? title = ExtractTitleName(root);
|
||||
|
||||
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
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)
|
||||
return null;
|
||||
|
||||
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
||||
var defLang = GetString(lp, "defaultLanguage");
|
||||
|
||||
if (!string.IsNullOrEmpty(defLang))
|
||||
{
|
||||
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (langObj.TryGetProperty("titleName", out var tn))
|
||||
return tn.GetString();
|
||||
var title = GetString(langObj, "titleName");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (en.TryGetProperty("titleName", out var tn2))
|
||||
return tn2.GetString();
|
||||
var title = GetString(en, "titleName");
|
||||
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;
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement parent, string propertyName) =>
|
||||
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -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": "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"
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"_languageName": "Italiano",
|
||||
|
||||
"Page.Library": "Libreria",
|
||||
"Page.Options": "Opzioni",
|
||||
"Page.GameCount.One": "1 gioco",
|
||||
"Page.GameCount.Other": "{0} giochi",
|
||||
|
||||
"Library.SearchWatermark": "Cerca nella libreria…",
|
||||
"Library.AddFolder": "+ Aggiungi cartella",
|
||||
"Library.Rescan": "⟳ Riscansiona",
|
||||
"Library.OpenFile": "Apri file…",
|
||||
|
||||
"Library.Context.Launch": "Avvia",
|
||||
"Library.Context.OpenFolder": "Apri cartella gioco",
|
||||
"Library.Context.CopyPath": "Copia percorso",
|
||||
"Library.Context.CopyTitleId": "Copia ID titolo",
|
||||
"Library.Context.Remove": "Rimuovi dalla libreria",
|
||||
|
||||
"Library.Empty.Title": "La tua libreria è vuota",
|
||||
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
|
||||
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
|
||||
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Aggiungi cartella giochi",
|
||||
|
||||
"Library.Loading": "Caricamento libreria…",
|
||||
|
||||
"Options.General": "Generale",
|
||||
"Options.Section.Emulation": "EMULAZIONE",
|
||||
"Options.Section.Logging": "LOG",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU engine",
|
||||
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
|
||||
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
|
||||
|
||||
"Options.LogLevel.Label": "Livello Log",
|
||||
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
|
||||
"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 tracciamento import",
|
||||
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
|
||||
|
||||
|
||||
"Options.LogToFile.Label": "Salva log su file",
|
||||
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
|
||||
|
||||
|
||||
"Options.LogFilePath.Label": "Percorso file di log",
|
||||
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
|
||||
"Options.LogFilePath.Select": "Seleziona…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
|
||||
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
|
||||
|
||||
|
||||
"Options.TitleMusic.Label": "Musica del titolo",
|
||||
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
|
||||
|
||||
"Options.Discord.Label": "Presenza Discord",
|
||||
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
|
||||
|
||||
"Options.Language.Label": "Lingua dell'emulatore",
|
||||
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
|
||||
|
||||
"Common.On": "On",
|
||||
"Common.Off": "Off",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Cerca...",
|
||||
"Console.AutoScroll": "Scorrimento automatico",
|
||||
"Console.Split": "Dividi",
|
||||
"Console.Copy": "Copia",
|
||||
"Console.Clear": "Cancella",
|
||||
"Console.WindowTitle": "Console SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nessun gioco selezionato",
|
||||
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
|
||||
"Launch.Idle": "Inattivo",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Avvia",
|
||||
"Launch.Stop": "■ Ferma",
|
||||
"Launch.Running": "In esecuzione — {0}",
|
||||
"Launch.Stopping": "Arresto in corso…",
|
||||
"Launch.Exited": "Terminato con codice {0} ({1})",
|
||||
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "File di log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
|
||||
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
|
||||
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argomenti non validi",
|
||||
"Exit.EbootNotFound": "eboot non trovato",
|
||||
"Exit.RuntimeException": "errore di runtime",
|
||||
"Exit.EmulationError": "errore di emulazione",
|
||||
"Exit.Unknown": "sconosciuto",
|
||||
|
||||
|
||||
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
|
||||
"Status.EmulatorPath": "Emulatore: {0}",
|
||||
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Scansione della libreria…",
|
||||
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
|
||||
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
|
||||
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
|
||||
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
|
||||
"Status.Running": "In esecuzione: {0}",
|
||||
"Status.Stopping": "Arresto in corso…",
|
||||
"Status.Idle": "Inattivo",
|
||||
|
||||
"Clipboard.Path": "Percorso",
|
||||
"Clipboard.TitleId": "ID Titolo",
|
||||
|
||||
"Discord.Playing": "Sta giocando a {0}",
|
||||
"Discord.Browsing": "Sta esplorando la libreria",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
|
||||
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
|
||||
"Dialog.PsExecutables": "Eseguibili PS",
|
||||
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
||||
"Dialog.PlainTextFiles": "File di testo semplice",
|
||||
"Dialog.LogFiles": "File di log"
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace SharpEmu.GUI;
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const int MaxConsoleLines = 4000;
|
||||
private const int MaxConsoleLinesPerFlush = 500;
|
||||
|
||||
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
||||
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
||||
@@ -1504,7 +1505,8 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
var incoming = new List<LogLine>();
|
||||
while (_pendingLines.TryDequeue(out var pending))
|
||||
while (incoming.Count < MaxConsoleLinesPerFlush &&
|
||||
_pendingLines.TryDequeue(out var pending))
|
||||
{
|
||||
WriteFileLog(pending.Line);
|
||||
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
||||
|
||||
Binary file not shown.
@@ -120,6 +120,9 @@ public static class AgcExports
|
||||
private const uint RegisterDefaultsVersion13 = 13;
|
||||
private const int RegisterDefaultsSize = 0x40;
|
||||
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 ShaderCodeOffset = 0x10;
|
||||
@@ -388,9 +391,26 @@ public static class AgcExports
|
||||
public SubmittedDcbState Graphics { get; } = new();
|
||||
public Dictionary<uint, SubmittedDcbState> ComputeQueues { 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; }
|
||||
}
|
||||
|
||||
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 RegisterDefaultGroup(
|
||||
@@ -2120,12 +2140,87 @@ public static class AgcExports
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2144,12 +2239,97 @@ public static class AgcExports
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2160,19 +2340,90 @@ public static class AgcExports
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DriverRegisterResource(CpuContext ctx)
|
||||
{
|
||||
var resourceAddress = ctx[CpuRegister.Rdi];
|
||||
var resourceHandleAddress = ctx[CpuRegister.Rdi];
|
||||
var owner = (uint)ctx[CpuRegister.Rsi];
|
||||
var nameAddress = ctx[CpuRegister.Rdx];
|
||||
var type = (uint)ctx[CpuRegister.R8];
|
||||
var flags = (uint)ctx[CpuRegister.R9];
|
||||
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||
var resourceSize = ctx[CpuRegister.Rcx];
|
||||
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(
|
||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
||||
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
|
||||
$"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);
|
||||
}
|
||||
|
||||
[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(
|
||||
Nid = "-KRzWekV120",
|
||||
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||
@@ -4250,7 +4501,7 @@ public static class AgcExports
|
||||
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
||||
var gpuDispatch = false;
|
||||
var computeError = string.Empty;
|
||||
if (hasStorageBinding &&
|
||||
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
|
||||
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
||||
{
|
||||
var shaderKey = (
|
||||
@@ -4320,7 +4571,8 @@ public static class AgcExports
|
||||
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
||||
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
||||
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
||||
$"gpu={gpuDispatch} blits={blitCount}" +
|
||||
$"gpu={gpuDispatch} blits={blitCount} " +
|
||||
$"globals={evaluation.GlobalMemoryBindings.Count}" +
|
||||
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
||||
$" bindings=[{string.Join(',', descriptions)}]");
|
||||
}
|
||||
|
||||
@@ -396,6 +396,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
}
|
||||
case "VBcntU32B32":
|
||||
result = IAdd(
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitCount,
|
||||
_uintType,
|
||||
GetRawSource(instruction, 0)),
|
||||
GetRawSource(instruction, 1));
|
||||
break;
|
||||
case "VMadU32U24":
|
||||
{
|
||||
var left = BitwiseAnd(
|
||||
@@ -593,6 +601,18 @@ internal static partial class Gen5SpirvTranslator
|
||||
Ext(38, _uintType, high, right));
|
||||
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":
|
||||
{
|
||||
var left = Bitcast(_intType, GetRawSource(instruction, 0));
|
||||
@@ -702,6 +722,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
result = Ext(58, _uintType, vector);
|
||||
break;
|
||||
}
|
||||
case "VCvtPkU16U32":
|
||||
case "VCvtPkI16I32":
|
||||
result = BitwiseOr(
|
||||
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
|
||||
ShiftLeftLogical(
|
||||
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
|
||||
UInt(16)));
|
||||
break;
|
||||
default:
|
||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||
return false;
|
||||
@@ -1303,6 +1331,43 @@ internal static partial class Gen5SpirvTranslator
|
||||
Store(_scc, IsNotZero(result));
|
||||
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":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ajm;
|
||||
|
||||
public static class AjmExports
|
||||
{
|
||||
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
|
||||
private static int _nextContextId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dl+4eHSzUu4",
|
||||
ExportName = "sceAjmInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int Initialize(CpuContext ctx)
|
||||
{
|
||||
var reserved = ctx[CpuRegister.Rdi];
|
||||
var outContextAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
// AJM requires a zero reserved argument.
|
||||
if (reserved != 0 || outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace SharpEmu.Libs;
|
||||
|
||||
public static class HostTimerResolution
|
||||
{
|
||||
private const uint TargetPeriodMilliseconds = 1;
|
||||
|
||||
private static int _requested;
|
||||
|
||||
public static void Request()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _requested, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Host timer resolution request rejected; " +
|
||||
"timed waits keep the default ~15.6 ms granularity.");
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
|
||||
private static extern uint TimeBeginPeriod(uint uPeriod);
|
||||
}
|
||||
@@ -78,6 +78,58 @@ public static class JsonExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_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)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -80,14 +80,8 @@ public static class KernelAprCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
|
||||
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
||||
{
|
||||
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
|
||||
// Completion output was written when the command was submitted.
|
||||
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -169,27 +163,6 @@ public static class KernelAprCompatExports
|
||||
return ctx.Memory.TryWrite(resultAddress, result);
|
||||
}
|
||||
|
||||
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
||||
{
|
||||
if (waitArg2 == 0)
|
||||
{
|
||||
return submittedResultAddress;
|
||||
}
|
||||
|
||||
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
|
||||
{
|
||||
return submittedResultAddress;
|
||||
}
|
||||
|
||||
return waitArg2;
|
||||
}
|
||||
|
||||
private static bool IsAmprCompletionToken(ulong value)
|
||||
{
|
||||
var tag = value >> 56;
|
||||
return tag is 0x0C or 0x10;
|
||||
}
|
||||
|
||||
private static void TraceApr(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports
|
||||
{
|
||||
private const int KernelEventSize = 0x20;
|
||||
public const short KernelEventFilterGraphics = -14;
|
||||
public const short KernelEventFilterUser = -11;
|
||||
public const short KernelEventFilterAmpr = -16;
|
||||
public const short KernelEventFilterAmprSystem = -17;
|
||||
|
||||
@@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAddUserEventEdge(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var registered = RegisterEvent(
|
||||
handle,
|
||||
ctx[CpuRegister.Rsi],
|
||||
KernelEventFilterUser,
|
||||
0);
|
||||
TraceEventQueue(ctx, "add_user_edge", handle);
|
||||
return registered
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAddUserEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var registered = RegisterEvent(
|
||||
handle,
|
||||
ctx[CpuRegister.Rsi],
|
||||
KernelEventFilterUser,
|
||||
0);
|
||||
TraceEventQueue(ctx, "add_user", handle);
|
||||
return registered
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelDeleteUserEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var deleted = DeleteRegisteredEvent(
|
||||
handle,
|
||||
ctx[CpuRegister.Rsi],
|
||||
KernelEventFilterUser);
|
||||
TraceEventQueue(ctx, "delete_user", handle);
|
||||
return deleted
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelTriggerUserEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var triggered = TriggerRegisteredEvent(
|
||||
handle,
|
||||
ctx[CpuRegister.Rsi],
|
||||
KernelEventFilterUser,
|
||||
flags: 0x21,
|
||||
fflags: 0,
|
||||
data: ctx[CpuRegister.Rdx]);
|
||||
TraceEventQueue(ctx, "trigger_user", handle);
|
||||
return triggered
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
private static bool TriggerRegisteredEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
short filter,
|
||||
ushort flags,
|
||||
uint fflags,
|
||||
ulong data)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_registeredEvents.TryGetValue(handle, out var registrations) ||
|
||||
!registrations.TryGetValue((ident, filter), out var registration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
QueueOrUpdateEvent(
|
||||
queue,
|
||||
new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
flags,
|
||||
fflags,
|
||||
data,
|
||||
registration.UserData));
|
||||
}
|
||||
|
||||
WakeEventQueue(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TriggerDisplayEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
@@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,66 +461,10 @@ public static class KernelExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
||||
// resolved against scripts/ps5_names.txt.
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceKernelUnknown9T2p",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "sceKernelUnknownTU",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceKernelUnknowniWQW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "sceKernelUnknownKuOmg",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "sceKernelUnknownRenI",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceKernelUnknownpQGp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceKernelUnknownRbvt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||
// shadowing them breaks every C++ static-init guard in the game.
|
||||
|
||||
private static int SetZeroReturn(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ public static class KernelMemoryCompatExports
|
||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
|
||||
internal static int AllocateGuestFileDescriptor()
|
||||
{
|
||||
lock (_fdGate)
|
||||
{
|
||||
return (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong _nextPhysicalAddress;
|
||||
private static ulong _nextVirtualAddress;
|
||||
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
||||
@@ -469,7 +478,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LHMrG7e8G78",
|
||||
ExportName = "wcslen",
|
||||
ExportName = "wcsmisc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Wcslen(CpuContext ctx)
|
||||
@@ -1479,6 +1488,214 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "QrZZdJ8XsX0",
|
||||
ExportName = "fputs",
|
||||
@@ -1555,9 +1772,10 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
int directoryFd;
|
||||
lock (_fdGate)
|
||||
{
|
||||
directoryFd = AllocateGuestFileDescriptor();
|
||||
_openDirectories[directoryFd] = new OpenDirectory
|
||||
{
|
||||
Path = hostPath,
|
||||
@@ -1578,9 +1796,10 @@ public static class KernelMemoryCompatExports
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
}
|
||||
|
||||
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
int fd;
|
||||
lock (_fdGate)
|
||||
{
|
||||
fd = AllocateGuestFileDescriptor();
|
||||
_openFiles[fd] = stream;
|
||||
}
|
||||
|
||||
@@ -1672,6 +1891,30 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "gEpBkcwxUjw",
|
||||
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
|
||||
@@ -1953,6 +2196,12 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryCloseSocketFd(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -1996,6 +2245,17 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryReadSocketFd(ctx, fd, bufferAddress, requested, out var socketBytesRead, out var socketError))
|
||||
{
|
||||
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return (int)socketError;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = socketBytesRead;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -2225,6 +2485,17 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryWriteSocketFd(ctx, fd, payload, out var socketError))
|
||||
{
|
||||
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return (int)socketError;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -2974,7 +3245,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4h6F1LLbTiw",
|
||||
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
||||
ExportName = "sceKernelMapNamedFlexibleMemoryInternal",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
||||
@@ -3441,6 +3712,18 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
|
||||
{
|
||||
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||
{
|
||||
rendered = format;
|
||||
return false;
|
||||
}
|
||||
|
||||
rendered = FormatString(ctx, format, ref vaCursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int WriteSnprintfOutput(
|
||||
CpuContext ctx,
|
||||
ulong destination,
|
||||
@@ -3914,7 +4197,7 @@ public static class KernelMemoryCompatExports
|
||||
_ => unchecked((int)argumentSource.NextGpArg())
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
else if (spaceForSign && value >= 0)
|
||||
@@ -3938,7 +4221,7 @@ public static class KernelMemoryCompatExports
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
||||
}
|
||||
break;
|
||||
@@ -3959,8 +4242,8 @@ public static class KernelMemoryCompatExports
|
||||
};
|
||||
|
||||
var formatted = specifier == 'x'
|
||||
? value.ToString("x")
|
||||
: value.ToString("X");
|
||||
? value.ToString("x", CultureInfo.InvariantCulture)
|
||||
: value.ToString("X", CultureInfo.InvariantCulture);
|
||||
|
||||
if (alternateForm && value != 0)
|
||||
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
||||
@@ -4068,7 +4351,10 @@ public static class KernelMemoryCompatExports
|
||||
var formatStr = precision >= 0
|
||||
? $"{{0:{specifier}{precision}}}"
|
||||
: $"{{0:{specifier}}}";
|
||||
var formatted = string.Format(formatStr, value);
|
||||
var formatted = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
formatStr,
|
||||
value);
|
||||
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -44,6 +45,8 @@ public static class KernelPthreadCompatExports
|
||||
// type initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||
private static long _nextMutexWakeId;
|
||||
private static long _nextCondWakeId;
|
||||
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
|
||||
|
||||
private sealed class PthreadMutexState
|
||||
{
|
||||
@@ -137,7 +140,7 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadYield(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
|
||||
Thread.Yield();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -351,29 +354,95 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx)
|
||||
{
|
||||
// POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113).
|
||||
var abstimeAddress = ctx[CpuRegister.Rdx];
|
||||
if (abstimeAddress == 0)
|
||||
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
|
||||
if (absoluteTimeAddress == 0 ||
|
||||
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
|
||||
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(abstimeAddress, out var deadlineSeconds) ||
|
||||
!ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds))
|
||||
var deadlineSeconds = unchecked((long)secondsRaw);
|
||||
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
|
||||
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var nowMicroseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000L;
|
||||
var deadlineMicroseconds =
|
||||
deadlineSeconds >= long.MaxValue / 1_000_000UL
|
||||
? long.MaxValue
|
||||
: (long)(deadlineSeconds * 1_000_000UL + Math.Min(deadlineNanoseconds, 999_999_999UL) / 1000UL);
|
||||
var remainingMicroseconds = deadlineMicroseconds - nowMicroseconds;
|
||||
var timeoutUsec = remainingMicroseconds <= 0
|
||||
? 1u
|
||||
: (uint)Math.Min(remainingMicroseconds, uint.MaxValue);
|
||||
return PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: timeoutUsec);
|
||||
long nowSeconds;
|
||||
long nowNanoseconds;
|
||||
if (deadlineSeconds >= 1_000_000_000)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
nowSeconds = now.ToUnixTimeSeconds();
|
||||
nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Engine condition variables use monotonic deadlines.
|
||||
KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds);
|
||||
}
|
||||
|
||||
var remainingNanoseconds =
|
||||
((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) -
|
||||
((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds);
|
||||
var timeoutUsec = remainingNanoseconds <= 0
|
||||
? 0u
|
||||
: (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000);
|
||||
|
||||
TracePosixTimedWait(
|
||||
ctx,
|
||||
deadlineSeconds,
|
||||
deadlineNanoseconds,
|
||||
nowSeconds,
|
||||
nowNanoseconds,
|
||||
remainingNanoseconds,
|
||||
timeoutUsec);
|
||||
|
||||
return PthreadCondWaitCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rsi],
|
||||
timed: true,
|
||||
timeoutUsec,
|
||||
posixResult: true);
|
||||
}
|
||||
|
||||
private static void TracePosixTimedWait(
|
||||
CpuContext ctx,
|
||||
long deadlineSeconds,
|
||||
long deadlineNanoseconds,
|
||||
long nowSeconds,
|
||||
long nowNanoseconds,
|
||||
Int128 remainingNanoseconds,
|
||||
uint timeoutUsec)
|
||||
{
|
||||
var condAddress = ctx[CpuRegister.Rdi];
|
||||
var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1);
|
||||
if (count != 1 && count % 1_000_000 != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
|
||||
$"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||
$"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " +
|
||||
$"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { } scheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var snapshot in scheduler.SnapshotThreads())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
|
||||
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
|
||||
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -654,11 +723,12 @@ public static class KernelPthreadCompatExports
|
||||
var acquired = state.Semaphore.Wait(0);
|
||||
if (!acquired)
|
||||
{
|
||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
// Cooperative deschedule on a contended lock corrupts fiber state
|
||||
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
|
||||
// synchronous host-thread wait below.
|
||||
if (_enableMutexLockBlocking &&
|
||||
// Fibers retain the synchronous fallback to preserve switch state.
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||
if (canCooperativelyBlock &&
|
||||
!tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
@@ -697,6 +767,53 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void TraceContendedMutex(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
ulong currentThreadId)
|
||||
{
|
||||
if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ulong ownerThreadId;
|
||||
int recursionCount;
|
||||
int type;
|
||||
lock (state)
|
||||
{
|
||||
ownerThreadId = state.OwnerThreadId;
|
||||
recursionCount = state.RecursionCount;
|
||||
type = state.Type;
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " +
|
||||
$"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " +
|
||||
$"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}");
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { } scheduler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var snapshot in scheduler.SnapshotThreads())
|
||||
{
|
||||
if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
|
||||
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
|
||||
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
@@ -1076,6 +1193,13 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||
{
|
||||
resolvedAddress = condAddress;
|
||||
state = raced;
|
||||
return true;
|
||||
}
|
||||
|
||||
_condStates[condAddress] = createdState;
|
||||
_condStates[handle] = createdState;
|
||||
}
|
||||
@@ -1172,7 +1296,13 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
|
||||
private static int PthreadCondWaitCore(
|
||||
CpuContext ctx,
|
||||
ulong condAddress,
|
||||
ulong mutexAddress,
|
||||
bool timed,
|
||||
uint timeoutUsec = 0,
|
||||
bool posixResult = false)
|
||||
{
|
||||
if (condAddress == 0 || mutexAddress == 0)
|
||||
{
|
||||
@@ -1210,9 +1340,10 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
if (consumedPendingSignal)
|
||||
{
|
||||
// Leave SyncRoot before relocking the mutex to avoid lock-order inversion with cond-signal (#113).
|
||||
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||
|
||||
// Relock outside SyncRoot to preserve lock ordering.
|
||||
Monitor.Exit(state.SyncRoot);
|
||||
try
|
||||
{
|
||||
@@ -1221,6 +1352,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Balance the surrounding lock statement.
|
||||
Monitor.Enter(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
@@ -1231,7 +1363,7 @@ public static class KernelPthreadCompatExports
|
||||
ctx,
|
||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||
state.WakeKey,
|
||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
|
||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
||||
() => state.SignalEpoch != observedEpoch,
|
||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||
{
|
||||
@@ -1315,7 +1447,7 @@ public static class KernelPthreadCompatExports
|
||||
state,
|
||||
timed,
|
||||
waitResult);
|
||||
return waitResult;
|
||||
return TranslatePthreadResult(waitResult, posixResult);
|
||||
}
|
||||
|
||||
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
|
||||
@@ -1372,7 +1504,8 @@ public static class KernelPthreadCompatExports
|
||||
PthreadCondState state,
|
||||
ulong observedEpoch,
|
||||
bool timed,
|
||||
int releasedRecursion)
|
||||
int releasedRecursion,
|
||||
bool posixResult)
|
||||
{
|
||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
lock (state.SyncRoot)
|
||||
@@ -1393,7 +1526,32 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
|
||||
return TranslatePthreadResult(
|
||||
lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult,
|
||||
posixResult);
|
||||
}
|
||||
|
||||
private static int TranslatePthreadResult(int result, bool posixResult)
|
||||
{
|
||||
if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return result switch
|
||||
{
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT
|
||||
_ => result
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
|
||||
@@ -1559,7 +1717,10 @@ public static class KernelPthreadCompatExports
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
|
||||
// Round positive sub-millisecond waits to the host sleep quantum.
|
||||
return timeoutUsec < 1_000
|
||||
? TimeSpan.FromMilliseconds(1)
|
||||
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
|
||||
}
|
||||
|
||||
private static TimeSpan GetCondSpuriousWakeTimeout()
|
||||
@@ -1701,7 +1862,8 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
|
||||
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
|
||||
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
|
||||
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
|
||||
}
|
||||
|
||||
|
||||
@@ -85,22 +85,29 @@ public static class KernelRuntimeCompatExports
|
||||
ExportName = "sceKernelNanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelNanosleep(CpuContext ctx)
|
||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yS8U2TGCe1A",
|
||||
ExportName = "nanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
||||
|
||||
private static int NanosleepCore(CpuContext ctx, bool posix)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
var remainAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Efault;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||
@@ -108,8 +115,7 @@ public static class KernelRuntimeCompatExports
|
||||
|
||||
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (tvSec == 0 && tvNsec == 0)
|
||||
@@ -119,7 +125,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
|
||||
|
||||
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||
@@ -138,6 +144,21 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
|
||||
{
|
||||
if (posix)
|
||||
{
|
||||
TrySetErrno(ctx, errnoValue);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
|
||||
}
|
||||
|
||||
return (int)sceResult;
|
||||
}
|
||||
|
||||
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||
{
|
||||
if (remainAddress == 0)
|
||||
@@ -192,6 +213,13 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QcteRwbsnV0",
|
||||
ExportName = "usleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx);
|
||||
|
||||
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
|
||||
{
|
||||
if (!_traceUsleep)
|
||||
@@ -403,6 +431,18 @@ public static class KernelRuntimeCompatExports
|
||||
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(
|
||||
Nid = "fgxnMeTNUtY",
|
||||
ExportName = "sceKernelGetProcessTimeCounter",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
|
||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||
private static int _nextSemaphoreHandle = 1;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _semaPollBackoffCount;
|
||||
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
@@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
public required int NeedCount { get; init; }
|
||||
public required int CancelEpochAtBlock { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Written and read only under the owning semaphore's Gate.
|
||||
public int? Result { get; set; }
|
||||
@@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var pollTimedOut = false;
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
@@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
if (timeoutMicros == 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
pollTimedOut = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||
var timedWaiter = new SemaphoreWaiter
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
Timed = true,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||
// immediate-timeout poll the callers already tolerate.
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
pollTimedOut = true;
|
||||
}
|
||||
}
|
||||
|
||||
var waiter = new SemaphoreWaiter
|
||||
if (!pollTimedOut)
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
var waiter = new SemaphoreWaiter
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
|
||||
if ((++_semaPollBackoffCount & 255) == 0)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Yield();
|
||||
}
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompleteBlockedTimedSemaWait(
|
||||
CpuContext ctx,
|
||||
KernelSemaphoreState semaphore,
|
||||
SemaphoreWaiter waiter,
|
||||
ulong timeoutAddress,
|
||||
long deadlineTimestamp)
|
||||
{
|
||||
int result;
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
|
||||
result = waiter.Result!.Value;
|
||||
}
|
||||
|
||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||
var remainingMicros = remainingTicks <= 0
|
||||
? 0u
|
||||
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
internal static class KernelSocketCompatExports
|
||||
{
|
||||
private sealed class EmulatedSocketState
|
||||
{
|
||||
public TcpClient? Client;
|
||||
public NetworkStream? Stream;
|
||||
public IPAddress BoundAddress = IPAddress.Any;
|
||||
public int BoundPort;
|
||||
public bool Bound;
|
||||
public bool Connected;
|
||||
}
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
|
||||
|
||||
internal static bool IsEmulatedSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Sockets.ContainsKey(fd);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryCloseSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Sockets.Remove(fd, out var state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DisposeEmulatedSocket(state);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadSocketFd(
|
||||
CpuContext ctx,
|
||||
int fd,
|
||||
ulong bufferAddress,
|
||||
int requested,
|
||||
out ulong bytesRead,
|
||||
out OrbisGen2Result error)
|
||||
{
|
||||
bytesRead = 0;
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||
state is null ||
|
||||
!state.Connected ||
|
||||
state.Stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||
int socketRead;
|
||||
try
|
||||
{
|
||||
socketRead = state.Stream.Read(socketBuffer, 0, requested);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
|
||||
{
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesRead = unchecked((ulong)socketRead);
|
||||
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryWriteSocketFd(
|
||||
CpuContext ctx,
|
||||
int fd,
|
||||
byte[] payload,
|
||||
out OrbisGen2Result error)
|
||||
{
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||
state is null ||
|
||||
!state.Connected ||
|
||||
state.Stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
state.Stream.Write(payload, 0, payload.Length);
|
||||
state.Stream.Flush();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Out.Write(Encoding.UTF8.GetString(payload));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
|
||||
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "socket",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Socket(CpuContext ctx)
|
||||
{
|
||||
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
|
||||
lock (Gate)
|
||||
{
|
||||
Sockets[fd] = new EmulatedSocketState();
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XVL8So3QJUk",
|
||||
ExportName = "connect",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Connect(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out _))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||
{
|
||||
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
|
||||
if (redirectApplied)
|
||||
{
|
||||
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
|
||||
}
|
||||
|
||||
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
|
||||
{
|
||||
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
|
||||
{
|
||||
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Sockets.TryGetValue(fd, out var state) || state is null)
|
||||
{
|
||||
try { stream.Dispose(); } catch (IOException) { }
|
||||
try { client.Dispose(); } catch (IOException) { }
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
DisposeEmulatedSocket(state);
|
||||
state.Client = client;
|
||||
state.Stream = stream;
|
||||
state.Connected = true;
|
||||
state.BoundAddress = ipAddress;
|
||||
state.BoundPort = port;
|
||||
state.Bound = true;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "bind",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Bind(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
state.BoundAddress = ipAddress;
|
||||
state.BoundPort = port;
|
||||
state.Bound = true;
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "getsockname",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Getsockname(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlenAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> addrlenBuffer = stackalloc byte[4];
|
||||
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
|
||||
if (addrlen < 8)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> sockaddr = stackalloc byte[16];
|
||||
sockaddr[0] = 16;
|
||||
sockaddr[1] = 2;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
|
||||
var addressBytes = state.BoundAddress.GetAddressBytes();
|
||||
if (addressBytes.Length == 4)
|
||||
{
|
||||
addressBytes.CopyTo(sockaddr.Slice(4, 4));
|
||||
}
|
||||
|
||||
var writeLength = Math.Min(addrlen, 16);
|
||||
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
|
||||
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9oiX1kyeedA",
|
||||
ExportName = "bzero",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Bzero(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var length = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (length > 0 && address != 0)
|
||||
{
|
||||
var zeros = new byte[length];
|
||||
if (!ctx.Memory.TryWrite(address, zeros))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4n51s0zEf0c",
|
||||
ExportName = "inet_pton",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int InetPton(CpuContext ctx)
|
||||
{
|
||||
var af = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var srcAddress = ctx[CpuRegister.Rsi];
|
||||
var dstAddress = ctx[CpuRegister.Rdx];
|
||||
if (af != 2 || srcAddress == 0 || dstAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryReadCString(srcAddress, ctx, out var text) ||
|
||||
!TryParseIpv4Address(text, out var octets))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> packed = stackalloc byte[4];
|
||||
packed[0] = octets[0];
|
||||
packed[1] = octets[1];
|
||||
packed[2] = octets[2];
|
||||
packed[3] = octets[3];
|
||||
if (!ctx.Memory.TryWrite(dstAddress, packed))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jogUIsOV3-U",
|
||||
ExportName = "htons",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Htons(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
|
||||
ctx[CpuRegister.Rax] = swapped;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Sockets.TryGetValue(fd, out state);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseGuestSockaddrIn(
|
||||
ulong address,
|
||||
int addrlen,
|
||||
CpuContext ctx,
|
||||
out IPAddress ipAddress,
|
||||
out int port)
|
||||
{
|
||||
ipAddress = IPAddress.None;
|
||||
port = 0;
|
||||
if (address == 0 || addrlen < 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
var readLength = Math.Min(addrlen, buffer.Length);
|
||||
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[1] != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
|
||||
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DisposeEmulatedSocket(EmulatedSocketState state)
|
||||
{
|
||||
try { state.Stream?.Dispose(); } catch (IOException) { }
|
||||
try { state.Client?.Dispose(); } catch (IOException) { }
|
||||
state.Stream = null;
|
||||
state.Client = null;
|
||||
state.Connected = false;
|
||||
}
|
||||
|
||||
private static void RemoveEmulatedSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (Sockets.Remove(fd, out var socketState))
|
||||
{
|
||||
DisposeEmulatedSocket(socketState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogNet(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
|
||||
{
|
||||
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||
if (string.IsNullOrWhiteSpace(redirect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ipAddress = redirectAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsNetRedirectConfigured()
|
||||
{
|
||||
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||
return !string.IsNullOrWhiteSpace(redirect);
|
||||
}
|
||||
|
||||
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
|
||||
{
|
||||
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
|
||||
}
|
||||
|
||||
private static bool TryEstablishHostTcpConnection(
|
||||
IPAddress ipAddress,
|
||||
int port,
|
||||
out TcpClient client,
|
||||
out NetworkStream stream)
|
||||
{
|
||||
client = null!;
|
||||
stream = null!;
|
||||
if (!TryConnectTcpClient(ipAddress, port, out client))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stream = client.GetStream();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
|
||||
{
|
||||
client = new TcpClient();
|
||||
try
|
||||
{
|
||||
var connectTask = client.ConnectAsync(ipAddress, port);
|
||||
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
|
||||
{
|
||||
const int maxLength = 64;
|
||||
var buffer = new byte[maxLength];
|
||||
var length = 0;
|
||||
for (; length < maxLength; length++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
|
||||
{
|
||||
text = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[length] == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
text = Encoding.ASCII.GetString(buffer, 0, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseIpv4Address(string text, out byte[] octets)
|
||||
{
|
||||
octets = Array.Empty<byte>();
|
||||
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsed = new byte[4];
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if (!byte.TryParse(parts[i], out parsed[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
octets = parsed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public static class LibcInternalExports
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NWtTN10cJzE",
|
||||
ExportName = "LibcHeapGetTraceInfo",
|
||||
ExportName = "sceLibcHeapGetTraceInfo",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "LibcInternalExt")]
|
||||
public static int LibcHeapGetTraceInfo(CpuContext ctx)
|
||||
|
||||
@@ -338,6 +338,359 @@ public static class LibcStdioExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Q60JLJ6Rv4",
|
||||
ExportName = "getc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AEuF3F2f8TA",
|
||||
ExportName = "fgetc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
|
||||
|
||||
private static int FgetcCore(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var value = stream.ReadByte();
|
||||
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LxcEU+ICu8U",
|
||||
ExportName = "feof",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Feof(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
|
||||
stream.CanRead && stream.Position >= stream.Length;
|
||||
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AHxyhN96dy4",
|
||||
ExportName = "ferror",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Ferror(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3QIPIh-GDjw",
|
||||
ExportName = "rewind",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Rewind(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aZK8lNei-Qw",
|
||||
ExportName = "fputc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fputc(CpuContext ctx)
|
||||
{
|
||||
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
|
||||
var handle = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.WriteByte(character);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
|
||||
// mirror the fputs HLE and forward them to the host console.
|
||||
Console.Out.Write((char)character);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = character;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MpxhMh8QFro",
|
||||
ExportName = "fwrite",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fwrite(CpuContext ctx)
|
||||
{
|
||||
var source = ctx[CpuRegister.Rdi];
|
||||
var elementSize = ctx[CpuRegister.Rsi];
|
||||
var elementCount = ctx[CpuRegister.Rdx];
|
||||
var handle = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (elementSize == 0 || elementCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
|
||||
if (source == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var totalRequested = elementSize * elementCount;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||
ulong totalWritten = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (totalWritten < totalRequested)
|
||||
{
|
||||
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
|
||||
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (knownHandle)
|
||||
{
|
||||
stream!.Write(buffer, 0, request);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
|
||||
}
|
||||
|
||||
totalWritten += (ulong)request;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MUjC4lbHrK4",
|
||||
ExportName = "fflush",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fflush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
try
|
||||
{
|
||||
if (handle == 0)
|
||||
{
|
||||
foreach (var stream in _fileHandles.Values)
|
||||
{
|
||||
stream.Flush();
|
||||
}
|
||||
}
|
||||
else if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
stream.Flush();
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fffwELXNVFA",
|
||||
ExportName = "fprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fprintf(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||
return WriteRenderedText(ctx, handle, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pDBDcY6uLSA",
|
||||
ExportName = "vfprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Vfprintf(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
|
||||
return WriteRenderedText(ctx, handle, rendered);
|
||||
}
|
||||
|
||||
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
|
||||
{
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
|
||||
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.Write(rendered);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = (ulong)payload.Length;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gkWgn0p1AfU",
|
||||
ExportName = "freopen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Freopen(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var modeAddress = ctx[CpuRegister.Rsi];
|
||||
var handle = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (_fileHandles.TryRemove(handle, out var previousStream))
|
||||
{
|
||||
previousStream.Dispose();
|
||||
}
|
||||
|
||||
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (fileAccess != FileAccess.Read)
|
||||
{
|
||||
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
|
||||
_fileHandles[handle] = stream;
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return ex is UnauthorizedAccessException
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUP1hBaouOw",
|
||||
ExportName = "_Getpctype",
|
||||
|
||||
@@ -95,6 +95,54 @@ public static class NetExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceNetHtonl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetHtonl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceNetHtons",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetHtons(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceNetNtohl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetNtohl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceNetNtohs",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetNtohs(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C4UgDHHPvdw",
|
||||
ExportName = "sceNetResolverCreate",
|
||||
|
||||
@@ -22,6 +22,17 @@ public static class NpManagerExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "S7QTn72PrDw",
|
||||
ExportName = "sceNpDeleteRequest",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpDeleteRequest(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JELHf4xPufo",
|
||||
ExportName = "sceNpCheckCallbackForLib",
|
||||
@@ -42,8 +53,19 @@ public static class NpManagerExports
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetOnlineId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
// Gen5 ABI: user ID, then output structure.
|
||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rbknaUjpqWo",
|
||||
ExportName = "sceNpGetOnlineIdA",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetOnlineIdA(CpuContext ctx)
|
||||
{
|
||||
// The A variant takes a user ID before OnlineId.
|
||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -158,4 +180,19 @@ public static class NpManagerExports
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
|
||||
}
|
||||
|
||||
private static int WriteOfflineOnlineId(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (address == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// SceNpOnlineId is a 16-byte handle plus four trailing bytes.
|
||||
Span<byte> onlineId = stackalloc byte[20];
|
||||
"Player"u8.CopyTo(onlineId);
|
||||
return ctx.Memory.TryWrite(address, onlineId)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,18 @@ public static class NpWebApi2Exports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WV1GwM32NgY",
|
||||
ExportName = "sceNpWebApi2InitializeForToolkit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2InitializeAlt(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bEvXpcEk200",
|
||||
ExportName = "sceNpWebApi2Terminate",
|
||||
|
||||
@@ -14,11 +14,18 @@ public static class PadExports
|
||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||
private const int PrimaryUserId = 1;
|
||||
private const int PrimaryUserId = 1000;
|
||||
private const int StandardPortType = 0;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _lastInputSampleTicks;
|
||||
|
||||
[ThreadStatic]
|
||||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
@@ -178,6 +185,16 @@ public static class PadExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2JgFB2n9oUM",
|
||||
ExportName = "scePadSetTriggerEffect",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetTriggerEffect(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yFVnOdGxvZY",
|
||||
ExportName = "scePadSetVibration",
|
||||
@@ -260,6 +277,44 @@ public static class PadExports
|
||||
{
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
data.Clear();
|
||||
var input = ReadHostInputState();
|
||||
var buttons = input.Buttons;
|
||||
var leftX = input.LeftX;
|
||||
var leftY = input.LeftY;
|
||||
var rightX = input.RightX;
|
||||
var rightY = input.RightY;
|
||||
var l2 = input.L2;
|
||||
var r2 = input.R2;
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
data[0x04] = leftX;
|
||||
data[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = l2;
|
||||
data[0x09] = r2;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
var timestampMicroseconds =
|
||||
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
data[0x50..],
|
||||
timestampMicroseconds);
|
||||
data[0x68] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(dataAddress, data);
|
||||
}
|
||||
|
||||
private static PadState ReadHostInputState()
|
||||
{
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (_lastInputSampleTicks != 0 && now - _lastInputSampleTicks < InputSampleIntervalTicks)
|
||||
{
|
||||
return _cachedInputState;
|
||||
}
|
||||
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||
@@ -293,25 +348,17 @@ public static class PadExports
|
||||
r2 = Math.Max(r2, xpad.R2);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
data[0x04] = leftX;
|
||||
data[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = l2;
|
||||
data[0x09] = r2;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
var timestampMicroseconds =
|
||||
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
data[0x50..],
|
||||
timestampMicroseconds);
|
||||
data[0x68] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(dataAddress, data);
|
||||
_cachedInputState = new PadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: leftX,
|
||||
LeftY: leftY,
|
||||
RightX: rightX,
|
||||
RightY: rightY,
|
||||
L2: l2,
|
||||
R2: r2);
|
||||
_lastInputSampleTicks = now;
|
||||
return _cachedInputState;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
|
||||
@@ -682,7 +682,9 @@ public static class PlayGoExports
|
||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||
if (!hasMetadata)
|
||||
{
|
||||
return PlayGoMetadata.Empty;
|
||||
// Full installs may omit PlayGo sidecar metadata.
|
||||
TracePlayGo("metadata_missing; using fully-installed default chunk");
|
||||
return new PlayGoMetadata(true, [(ushort)0]);
|
||||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
|
||||
@@ -30,13 +30,17 @@ public static class SaveDataExports
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly HashSet<int> _transactionResources = [];
|
||||
private static string? _titleId;
|
||||
private static int _nextTransactionResource;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? titleId)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
_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(
|
||||
Nid = "gjRZNnw0JPE",
|
||||
ExportName = "sceSaveDataCreateTransactionResource",
|
||||
@@ -252,25 +255,32 @@ public static class SaveDataExports
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var reserved = ctx[CpuRegister.Rsi];
|
||||
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (resourceAddress == 0)
|
||||
var memorySize = ctx[CpuRegister.Rdi];
|
||||
int resource;
|
||||
lock (_stateGate)
|
||||
{
|
||||
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(
|
||||
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||
|
||||
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ public static class UserServiceExports
|
||||
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||
private const int PrimaryUserId = 1;
|
||||
// Retail user-service IDs begin at 0x3E8.
|
||||
private const int PrimaryUserId = 1000;
|
||||
private const int InvalidUserId = -1;
|
||||
private const string PrimaryUserName = "SharpEmu";
|
||||
private static int _loginEventDelivered;
|
||||
@@ -111,7 +112,8 @@ public static class UserServiceExports
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var capacity = ctx[CpuRegister.Rdx];
|
||||
if (userId != PrimaryUserId)
|
||||
// Zero selects the current user.
|
||||
if (userId != 0 && userId != PrimaryUserId)
|
||||
{
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
@@ -137,6 +139,28 @@ public static class UserServiceExports
|
||||
return ctx.SetReturn(result);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-sD02mFDBh4",
|
||||
ExportName = "sceUserServiceGetGamePresets",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetGamePresets(CpuContext ctx)
|
||||
{
|
||||
// Return deterministic defaults for the offline profile.
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var resultAddress = ctx[CpuRegister.Rcx];
|
||||
Span<byte> defaults = stackalloc byte[0x18];
|
||||
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
|
||||
{
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
TraceUserService(
|
||||
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
|
||||
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D-CzAxQL0XI",
|
||||
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||
|
||||
@@ -14,6 +14,17 @@ internal static class PngSplashLoader
|
||||
];
|
||||
|
||||
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
|
||||
=> TryLoad("pic0.png", requestRgba: false, out pixels, out width, out height);
|
||||
|
||||
public static bool TryLoadIcon(out byte[] pixels, out uint width, out uint height)
|
||||
=> TryLoad("icon0.png", requestRgba: true, out pixels, out width, out height);
|
||||
|
||||
private static bool TryLoad(
|
||||
string fileName,
|
||||
bool requestRgba,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
@@ -27,13 +38,18 @@ internal static class PngSplashLoader
|
||||
return false;
|
||||
}
|
||||
|
||||
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
|
||||
var path = Path.Combine(app0Root, "sce_sys", fileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
|
||||
return TryDecode(
|
||||
File.ReadAllBytes(path),
|
||||
out pixels,
|
||||
out width,
|
||||
out height,
|
||||
requestRgba);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -48,7 +64,8 @@ internal static class PngSplashLoader
|
||||
ReadOnlySpan<byte> png,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
out uint height,
|
||||
bool requestRgba)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
@@ -154,9 +171,13 @@ internal static class PngSplashLoader
|
||||
sourceOffset < reconstructed.Length;
|
||||
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
|
||||
{
|
||||
pixels[targetOffset] = reconstructed[sourceOffset + 2];
|
||||
pixels[targetOffset] = requestRgba
|
||||
? reconstructed[sourceOffset]
|
||||
: reconstructed[sourceOffset + 2];
|
||||
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
|
||||
pixels[targetOffset + 2] = reconstructed[sourceOffset];
|
||||
pixels[targetOffset + 2] = requestRgba
|
||||
? reconstructed[sourceOffset + 2]
|
||||
: reconstructed[sourceOffset];
|
||||
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
|
||||
? reconstructed[sourceOffset + 3]
|
||||
: (byte)0xFF;
|
||||
|
||||
@@ -48,54 +48,106 @@ public static class VideoOutExports
|
||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||
|
||||
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
|
||||
private static Timer? _vblankPumpTimer;
|
||||
private static int _vblankPumpActive;
|
||||
private const int VblankPumpIntervalMs = 16;
|
||||
private const double VblankHz = 60.0;
|
||||
private const int VblankWaitTimeoutMilliseconds = 100;
|
||||
private static Thread? _vblankPumpThread;
|
||||
private static int _vblankPumpStarted;
|
||||
|
||||
private static readonly object _vblankEdgeGate = new();
|
||||
private static ulong _vblankEdgeSequence;
|
||||
private static long _vblankMissedEdges;
|
||||
|
||||
private static void EnsureVblankPumpStarted()
|
||||
{
|
||||
if (_vblankPumpTimer is not null)
|
||||
if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vblankPumpTimer = new Timer(
|
||||
static _ => PumpVblanks(),
|
||||
null,
|
||||
VblankPumpIntervalMs,
|
||||
VblankPumpIntervalMs);
|
||||
HostTimerResolution.Request();
|
||||
|
||||
_vblankPumpThread = new Thread(VblankPumpLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu VideoOut vblank",
|
||||
Priority = ThreadPriority.AboveNormal,
|
||||
};
|
||||
_vblankPumpThread.Start();
|
||||
}
|
||||
|
||||
private static void VblankPumpLoop()
|
||||
{
|
||||
var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz));
|
||||
var nextEdge = Stopwatch.GetTimestamp() + intervalTicks;
|
||||
|
||||
while (true)
|
||||
{
|
||||
WaitUntilTimestamp(nextEdge);
|
||||
PumpVblanks();
|
||||
|
||||
nextEdge += intervalTicks;
|
||||
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (nextEdge < now)
|
||||
{
|
||||
var missed = (now - nextEdge) / intervalTicks + 1;
|
||||
Interlocked.Add(ref _vblankMissedEdges, missed);
|
||||
nextEdge = now + intervalTicks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WaitUntilTimestamp(long deadlineTicks)
|
||||
{
|
||||
var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var remaining = deadlineTicks - Stopwatch.GetTimestamp();
|
||||
if (remaining <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (remaining > spinThresholdTicks)
|
||||
{
|
||||
var sleepMilliseconds =
|
||||
(int)((remaining - spinThresholdTicks) * 1000L / Stopwatch.Frequency);
|
||||
if (sleepMilliseconds > 0)
|
||||
{
|
||||
Thread.Sleep(sleepMilliseconds);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Thread.SpinWait(64);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PumpVblanks()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _vblankPumpActive, 1) != 0)
|
||||
lock (_vblankEdgeGate)
|
||||
{
|
||||
return;
|
||||
_vblankEdgeSequence++;
|
||||
Monitor.PulseAll(_vblankEdgeGate);
|
||||
}
|
||||
|
||||
try
|
||||
VideoOutPortState[] ports;
|
||||
lock (_stateGate)
|
||||
{
|
||||
VideoOutPortState[] ports;
|
||||
lock (_stateGate)
|
||||
if (_ports.Count == 0)
|
||||
{
|
||||
if (_ports.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var port in ports)
|
||||
{
|
||||
SignalVblank(port);
|
||||
}
|
||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||
}
|
||||
finally
|
||||
|
||||
foreach (var port in ports)
|
||||
{
|
||||
Volatile.Write(ref _vblankPumpActive, 0);
|
||||
SignalVblank(port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +155,7 @@ public static class VideoOutExports
|
||||
private static int _nextHandle = 1;
|
||||
private static int _frameDumpCount;
|
||||
private static long _nextFrameDumpIndex;
|
||||
private static string _windowTitleBase = "SharpEmu VideoOut";
|
||||
private static string _windowTitle = "SharpEmu VideoOut";
|
||||
private static readonly bool _logFrameRate = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||
@@ -112,6 +165,10 @@ public static class VideoOutExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _dumpVideoOut = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||
private static long _submittedFrameCount;
|
||||
private static long _presentedFrameCount;
|
||||
@@ -134,10 +191,24 @@ public static class VideoOutExports
|
||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName} \u00b7 {HostSystemInfo.GpuName}";
|
||||
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName}";
|
||||
lock (_stateGate)
|
||||
{
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
|
||||
_windowTitleBase = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
|
||||
_windowTitle = $"{_windowTitleBase} \u00b7 {HostSystemInfo.GpuName}";
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetSelectedGpuName(string gpuName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(gpuName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
_windowTitle = $"{_windowTitleBase} \u00b7 {gpuName.Trim()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +447,20 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
EnsureVblankPumpStarted();
|
||||
|
||||
lock (_vblankEdgeGate)
|
||||
{
|
||||
var entryEdge = _vblankEdgeSequence;
|
||||
while (_vblankEdgeSequence == entryEdge)
|
||||
{
|
||||
if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignalVblank(port);
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -987,10 +1071,7 @@ public static class VideoOutExports
|
||||
displayBuffer.PitchInPixel);
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
if (_dumpVideoOut)
|
||||
{
|
||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
@@ -1050,9 +1131,11 @@ public static class VideoOutExports
|
||||
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||
var missedEdges = Interlocked.Exchange(ref _vblankMissedEdges, 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||
$"presented_fps={presentedCount / elapsedSeconds:F1}");
|
||||
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
|
||||
$"vblank_missed={missedEdges}");
|
||||
}
|
||||
|
||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Silk.NET.Vulkan.Extensions.EXT;
|
||||
using Silk.NET.Windowing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
@@ -600,7 +601,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var traceSubmission = false;
|
||||
lock (_gate)
|
||||
{
|
||||
var known = _availableGuestImages.ContainsKey(address);
|
||||
// VideoOut registration does not imply a rendered Vulkan image.
|
||||
var known = _gpuGuestImages.ContainsKey(address);
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -622,7 +624,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
|
||||
$"{width}x{height} - flip target was never registered as a render output");
|
||||
$"{width}x{height} - flip target has no completed GPU render output");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -933,6 +935,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private ExtDebugUtils? _debugUtils;
|
||||
private PhysicalDevice _physicalDevice;
|
||||
private Device _device;
|
||||
private PipelineCache _pipelineCache;
|
||||
private string? _pipelineCachePath;
|
||||
private Queue _queue;
|
||||
private uint _queueFamilyIndex;
|
||||
private SwapchainKHR _swapchain;
|
||||
@@ -950,6 +954,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private CommandBuffer _presentationCommandBuffer;
|
||||
private VkSemaphore _imageAvailable;
|
||||
private VkSemaphore _renderFinished;
|
||||
private Fence _presentationFence;
|
||||
private bool _presentationInFlight;
|
||||
private TranslatedDrawResources? _pendingPresentationResources;
|
||||
private VkBuffer _stagingBuffer;
|
||||
private DeviceMemory _stagingMemory;
|
||||
private ulong _stagingSize;
|
||||
@@ -966,6 +973,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private int _directPresentationCount;
|
||||
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
||||
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
|
||||
private static readonly bool _dumpTextures = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new();
|
||||
private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new();
|
||||
private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new();
|
||||
@@ -988,7 +999,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private readonly record struct GraphicsPipelineKey(
|
||||
string VertexShader,
|
||||
string FragmentShader,
|
||||
ulong RenderPass,
|
||||
Format RenderTargetFormat,
|
||||
PrimitiveTopology Topology,
|
||||
VulkanGuestBlendState Blend,
|
||||
string ResourceLayout,
|
||||
@@ -1138,12 +1149,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
|
||||
{
|
||||
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
|
||||
_window.SetWindowIcon(ref icon);
|
||||
}
|
||||
|
||||
WaitForRenderDocAttachIfRequested();
|
||||
_vk = Vk.GetApi();
|
||||
CreateInstance();
|
||||
CreateSurface();
|
||||
SelectPhysicalDevice();
|
||||
CreateDevice();
|
||||
CreatePipelineCache();
|
||||
CreateSwapchain();
|
||||
CreateCommandResources();
|
||||
CreateGuestDrawResources();
|
||||
@@ -1553,8 +1571,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})");
|
||||
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
|
||||
VideoOutExports.SetSelectedGpuName(selectedName);
|
||||
_window.Title = VideoOutExports.GetWindowTitle();
|
||||
}
|
||||
|
||||
private static int ScorePhysicalDevice(
|
||||
@@ -1737,6 +1758,110 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePipelineCache()
|
||||
{
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "pipeline-cache");
|
||||
_pipelineCachePath = Path.Combine(
|
||||
directory,
|
||||
$"vulkan-{properties.VendorID:X4}-{properties.DeviceID:X4}-{properties.DriverVersion:X8}.bin");
|
||||
|
||||
byte[] initialData = [];
|
||||
try
|
||||
{
|
||||
if (File.Exists(_pipelineCachePath))
|
||||
{
|
||||
initialData = File.ReadAllBytes(_pipelineCachePath);
|
||||
}
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Vulkan pipeline cache could not be read: {exception.Message}");
|
||||
}
|
||||
|
||||
fixed (byte* initialDataPointer = initialData)
|
||||
{
|
||||
var createInfo = new PipelineCacheCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineCacheCreateInfo,
|
||||
InitialDataSize = (nuint)initialData.Length,
|
||||
PInitialData = initialData.Length == 0 ? null : initialDataPointer,
|
||||
};
|
||||
var result = _vk.CreatePipelineCache(
|
||||
_device,
|
||||
&createInfo,
|
||||
null,
|
||||
out _pipelineCache);
|
||||
if (result != Result.Success && initialData.Length != 0)
|
||||
{
|
||||
createInfo.InitialDataSize = 0;
|
||||
createInfo.PInitialData = null;
|
||||
Check(
|
||||
_vk.CreatePipelineCache(
|
||||
_device,
|
||||
&createInfo,
|
||||
null,
|
||||
out _pipelineCache),
|
||||
"vkCreatePipelineCache(empty)");
|
||||
initialData = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
Check(result, "vkCreatePipelineCache");
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan pipeline cache: " +
|
||||
$"{(initialData.Length == 0 ? "new" : $"loaded {initialData.Length} bytes")}");
|
||||
}
|
||||
|
||||
private void SavePipelineCache()
|
||||
{
|
||||
if (_pipelineCache.Handle == 0 || string.IsNullOrWhiteSpace(_pipelineCachePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
nuint size = 0;
|
||||
Check(
|
||||
_vk.GetPipelineCacheData(_device, _pipelineCache, &size, null),
|
||||
"vkGetPipelineCacheData(size)");
|
||||
if (size == 0 || size > int.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new byte[(int)size];
|
||||
fixed (byte* dataPointer = data)
|
||||
{
|
||||
Check(
|
||||
_vk.GetPipelineCacheData(
|
||||
_device,
|
||||
_pipelineCache,
|
||||
&size,
|
||||
dataPointer),
|
||||
"vkGetPipelineCacheData");
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(_pipelineCachePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
File.WriteAllBytes(_pipelineCachePath, data.AsSpan(0, (int)size).ToArray());
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException or UnauthorizedAccessException or InvalidOperationException)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Vulkan pipeline cache could not be saved: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSwapchain()
|
||||
{
|
||||
Check(
|
||||
@@ -1829,9 +1954,44 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _imageAvailable), "vkCreateSemaphore");
|
||||
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "vkCreateSemaphore");
|
||||
|
||||
var fenceInfo = new FenceCreateInfo
|
||||
{
|
||||
SType = StructureType.FenceCreateInfo,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateFence(_device, &fenceInfo, null, out _presentationFence),
|
||||
"vkCreateFence(presentation)");
|
||||
|
||||
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
||||
}
|
||||
|
||||
private void CompletePendingPresentation(bool wait)
|
||||
{
|
||||
if (!_presentationInFlight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fence = _presentationFence;
|
||||
var result = wait
|
||||
? _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue)
|
||||
: _vk.GetFenceStatus(_device, fence);
|
||||
if (!wait && result == Result.NotReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Check(result, "vkWaitForFences(presentation)");
|
||||
_presentationInFlight = false;
|
||||
if (_pendingPresentationResources is not null)
|
||||
{
|
||||
MarkSampledImagesInitialized(_pendingPresentationResources);
|
||||
MarkStorageImagesInitialized(_pendingPresentationResources);
|
||||
DestroyTranslatedDrawResources(_pendingPresentationResources);
|
||||
_pendingPresentationResources = null;
|
||||
}
|
||||
}
|
||||
|
||||
private CommandBuffer AllocateGuestCommandBuffer()
|
||||
{
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
@@ -2178,7 +2338,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(
|
||||
_vk.CreateGraphicsPipelines(
|
||||
_device,
|
||||
default,
|
||||
_pipelineCache,
|
||||
1,
|
||||
&pipelineInfo,
|
||||
null,
|
||||
@@ -2213,6 +2373,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private TranslatedDrawResources CreateTranslatedDrawResources(
|
||||
VulkanTranslatedGuestDraw draw,
|
||||
RenderPass renderPass,
|
||||
Format renderTargetFormat,
|
||||
Extent2D extent)
|
||||
{
|
||||
var vertexSpirv = draw.VertexSpirv;
|
||||
@@ -2279,7 +2440,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CreateTranslatedDescriptorResources(
|
||||
resources,
|
||||
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit);
|
||||
CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent);
|
||||
CreateTranslatedPipeline(
|
||||
resources,
|
||||
vertexSpirv,
|
||||
draw.PixelSpirv,
|
||||
renderPass,
|
||||
renderTargetFormat,
|
||||
extent);
|
||||
return resources;
|
||||
}
|
||||
catch
|
||||
@@ -2575,12 +2742,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
byte[] vertexSpirv,
|
||||
byte[] fragmentSpirv,
|
||||
RenderPass renderPass,
|
||||
Format renderTargetFormat,
|
||||
Extent2D extent)
|
||||
{
|
||||
var pipelineKey = new GraphicsPipelineKey(
|
||||
GetShaderDigest(vertexSpirv),
|
||||
GetShaderDigest(fragmentSpirv),
|
||||
renderPass.Handle,
|
||||
renderTargetFormat,
|
||||
resources.Topology,
|
||||
resources.Blend,
|
||||
GetResourceLayoutKey(resources),
|
||||
@@ -2736,7 +2904,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(
|
||||
_vk.CreateGraphicsPipelines(
|
||||
_device,
|
||||
default,
|
||||
_pipelineCache,
|
||||
1,
|
||||
&pipelineInfo,
|
||||
null,
|
||||
@@ -2916,7 +3084,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(
|
||||
_vk.CreateComputePipelines(
|
||||
_device,
|
||||
default,
|
||||
_pipelineCache,
|
||||
1,
|
||||
&pipelineInfo,
|
||||
null,
|
||||
@@ -3442,11 +3610,36 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037;
|
||||
const ulong prime = 1099511628211;
|
||||
var hash = offsetBasis;
|
||||
foreach (var value in pixels)
|
||||
|
||||
var h0 = offsetBasis ^ (ulong)pixels.Length;
|
||||
var h1 = offsetBasis;
|
||||
var h2 = offsetBasis;
|
||||
var h3 = offsetBasis;
|
||||
|
||||
var words = MemoryMarshal.Cast<byte, ulong>(pixels);
|
||||
var index = 0;
|
||||
var blockEnd = words.Length - (words.Length & 3);
|
||||
for (; index < blockEnd; index += 4)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= prime;
|
||||
h0 = (h0 ^ words[index]) * prime;
|
||||
h1 = (h1 ^ words[index + 1]) * prime;
|
||||
h2 = (h2 ^ words[index + 2]) * prime;
|
||||
h3 = (h3 ^ words[index + 3]) * prime;
|
||||
}
|
||||
|
||||
for (; index < words.Length; index++)
|
||||
{
|
||||
h0 = (h0 ^ words[index]) * prime;
|
||||
}
|
||||
|
||||
var hash = h0;
|
||||
hash = (hash ^ h1) * prime;
|
||||
hash = (hash ^ h2) * prime;
|
||||
hash = (hash ^ h3) * prime;
|
||||
|
||||
foreach (var value in pixels[(words.Length * sizeof(ulong))..])
|
||||
{
|
||||
hash = (hash ^ value) * prime;
|
||||
}
|
||||
|
||||
return hash;
|
||||
@@ -3459,10 +3652,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
uint width,
|
||||
uint height)
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
|
||||
"1",
|
||||
StringComparison.Ordinal) ||
|
||||
if (!_dumpTextures ||
|
||||
texture.IsFallback ||
|
||||
texture.IsStorage ||
|
||||
GetTextureBytesPerPixel(texture.Format) != 4 ||
|
||||
@@ -4483,6 +4673,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
resources = CreateTranslatedDrawResources(
|
||||
work.Draw,
|
||||
target.RenderPass,
|
||||
target.Format,
|
||||
extent);
|
||||
resources.DebugName =
|
||||
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
|
||||
@@ -5119,6 +5310,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
CompletePendingPresentation(wait: true);
|
||||
|
||||
byte[]? pixels = null;
|
||||
if (presentation.Pixels is { } sourcePixels)
|
||||
{
|
||||
@@ -5166,6 +5359,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
translatedResources = CreateTranslatedDrawResources(
|
||||
translatedDraw,
|
||||
_renderPass,
|
||||
_swapchainFormat,
|
||||
_extent);
|
||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
||||
!_firstGuestDrawPresented)
|
||||
@@ -5297,7 +5491,16 @@ internal static unsafe class VulkanVideoPresenter
|
||||
SignalSemaphoreCount = 1,
|
||||
PSignalSemaphores = &renderFinished,
|
||||
};
|
||||
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
|
||||
var presentationFence = _presentationFence;
|
||||
Check(
|
||||
_vk.ResetFences(_device, 1, &presentationFence),
|
||||
"vkResetFences(presentation)");
|
||||
Check(
|
||||
_vk.QueueSubmit(_queue, 1, &submitInfo, presentationFence),
|
||||
"vkQueueSubmit");
|
||||
_presentationInFlight = true;
|
||||
_pendingPresentationResources = translatedResources;
|
||||
translatedResources = null;
|
||||
|
||||
var swapchain = _swapchain;
|
||||
var presentInfo = new PresentInfoKHR
|
||||
@@ -5313,6 +5516,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (presentResult == Result.ErrorOutOfDateKhr)
|
||||
{
|
||||
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
|
||||
CompletePendingPresentation(wait: false);
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
if (translatedResources is not null)
|
||||
{
|
||||
@@ -5325,19 +5529,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
|
||||
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
|
||||
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
|
||||
VideoOutExports.ReportPresentedFrame();
|
||||
if (_swapchainReadbackPending)
|
||||
{
|
||||
CompletePendingPresentation(wait: true);
|
||||
TraceSwapchainReadback();
|
||||
}
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
if (translatedResources is not null)
|
||||
{
|
||||
MarkSampledImagesInitialized(translatedResources);
|
||||
MarkStorageImagesInitialized(translatedResources);
|
||||
DestroyTranslatedDrawResources(translatedResources);
|
||||
}
|
||||
|
||||
_imageInitialized[imageIndex] = true;
|
||||
_presentedSequence = presentation.Sequence;
|
||||
@@ -6591,7 +6789,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
_vulkanReady = false;
|
||||
_vk.DeviceWaitIdle(_device);
|
||||
CompletePendingPresentation(wait: false);
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
SavePipelineCache();
|
||||
foreach (var pipeline in _computePipelines.Values)
|
||||
{
|
||||
_vk.DestroyPipeline(_device, pipeline, null);
|
||||
@@ -6602,6 +6802,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.DestroyPipeline(_device, pipeline, null);
|
||||
}
|
||||
_graphicsPipelines.Clear();
|
||||
if (_pipelineCache.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipelineCache(_device, _pipelineCache, null);
|
||||
_pipelineCache = default;
|
||||
}
|
||||
foreach (var layout in _descriptorLayouts.Values)
|
||||
{
|
||||
_vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null);
|
||||
@@ -6693,6 +6898,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}");
|
||||
_vk.DeviceWaitIdle(_device);
|
||||
CompletePendingPresentation(wait: false);
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
DestroySwapchainResources();
|
||||
CreateSwapchain();
|
||||
@@ -6726,6 +6932,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.DestroySemaphore(_device, _renderFinished, null);
|
||||
_renderFinished = default;
|
||||
}
|
||||
if (_presentationFence.Handle != 0)
|
||||
{
|
||||
_vk.DestroyFence(_device, _presentationFence, null);
|
||||
_presentationFence = default;
|
||||
}
|
||||
if (_barycentricPipeline.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipeline(_device, _barycentricPipeline, null);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Voice;
|
||||
|
||||
public static class VoiceQoSExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "U8IfNl6-Css",
|
||||
ExportName = "sceVoiceQoSInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSInit(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,15 @@ using System.Text;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Writes log entries to a file. Thread-safe via an internal lock.
|
||||
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
|
||||
/// </summary>
|
||||
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private readonly object _sync = new();
|
||||
private readonly StreamWriter _writer;
|
||||
private readonly Timer _flushTimer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
|
||||
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
|
||||
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
|
||||
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||
@@ -34,14 +30,20 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
||||
append ? FileMode.Append : FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
bufferSize: 4096,
|
||||
bufferSize: 65536,
|
||||
FileOptions.SequentialScan);
|
||||
_writer = new StreamWriter(fileStream, Encoding.UTF8)
|
||||
_writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536)
|
||||
{
|
||||
AutoFlush = true
|
||||
AutoFlush = false
|
||||
};
|
||||
|
||||
IncludeTimestamp = includeTimestamp;
|
||||
|
||||
_flushTimer = new Timer(
|
||||
static state => ((FileLogSink)state!).FlushBuffered(),
|
||||
this,
|
||||
FlushInterval,
|
||||
FlushInterval);
|
||||
}
|
||||
|
||||
public bool IncludeTimestamp { get; set; }
|
||||
@@ -84,11 +86,31 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
||||
{
|
||||
_writer.WriteLine(entry.Exception);
|
||||
}
|
||||
|
||||
if (entry.Level >= LogLevel.Error)
|
||||
{
|
||||
_writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushBuffered()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_writer.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_flushTimer.Dispose();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -36,11 +36,16 @@ public static class HostSystemInfo
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return Regex.Replace(
|
||||
name = Regex.Replace(
|
||||
name.Trim(),
|
||||
@"\s+\d+-Core Processor$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return Regex.Replace(
|
||||
name,
|
||||
@"\s+with Radeon Graphics$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -103,8 +108,12 @@ public static class HostSystemInfo
|
||||
}
|
||||
|
||||
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 2;
|
||||
|
||||
@@ -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