Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech 6c8c98ed7d [GUI] now languages are embedded in a single assembly 2026-07-14 02:07:31 +03:00
40 changed files with 307 additions and 9189 deletions
+6 -7
View File
@@ -3,8 +3,8 @@
#!/usr/bin/env python3
import hashlib
import struct
import hashlib
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,11 +42,10 @@ 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
-1
View File
@@ -95,7 +95,6 @@ internal static partial class Program
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
-3
View File
@@ -24,9 +24,6 @@ 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 (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
@@ -245,13 +245,12 @@ 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 ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
@@ -312,15 +311,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
if (importStubEntry.Nid == "8zTFvBIAIN8" && 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 ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
if (importStubEntry.Nid == "tsvEmnenz48" && 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 ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
{
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}");
}
@@ -350,7 +349,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
if (importStubEntry.Nid == "Ou3iL1abvng")
{
if (_logStackCheck)
{
@@ -382,7 +381,7 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress);
try
{
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
if (_logBootstrap)
{
@@ -391,11 +390,12 @@ public sealed partial class DirectExecutionBackend
orbisGen2Result = DispatchBootstrapBridge();
}
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
{
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
}
@@ -736,9 +736,6 @@ 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;
@@ -751,7 +748,6 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedEventFlagTimeout &&
!expectedMutexTrylockBusy &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -822,7 +818,6 @@ 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
@@ -1081,15 +1076,7 @@ public sealed partial class DirectExecutionBackend
}
private static bool IsImportLoopGuardBoundary(string nid) =>
nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
private void ResetImportLoopPattern()
{
@@ -25,26 +25,6 @@ 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; }
@@ -53,36 +33,12 @@ 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(
@@ -169,8 +125,6 @@ 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;
@@ -2803,21 +2757,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (hostThread is not null &&
!ReferenceEquals(hostThread, Thread.CurrentThread))
{
// 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);
}
hostThread.Join(1);
}
else
{
@@ -3506,7 +3446,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
out ulong mappedBase,
out string? error)
{
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
for (int i = 0; i < 64; i++)
{
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
@@ -3540,7 +3480,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
out ulong tlsBase,
out string? error)
{
for (int i = 0; i < GuestThreadRegionSlotCount; i++)
for (int i = 0; i < 64; i++)
{
var candidateBase = GuestThreadTlsBaseAddress - ((ulong)i * GuestThreadRegionStride);
var mappedBase = candidateBase - GuestThreadTlsPrefixSize;
-129
View File
@@ -1,129 +0,0 @@
{
"_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": "ملفات السجل"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_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"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_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"
}
-134
View File
@@ -1,134 +0,0 @@
{
"_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"
}
-129
View File
@@ -1,129 +0,0 @@
{
"_languageName": "Русский",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
"Page.GameCount.One": "1 игра",
"Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "+ Добавить папку",
"Library.Rescan": "⟳ Сканировать",
"Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить",
"Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста",
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
"Library.Empty.AddFolder": "+ Добавить папку с играми",
"Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
"Options.CpuEngine.Native": "Нативный",
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
"Options.LogLevel.Label": "Уровень логгирования",
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
"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": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
"Options.LogFilePath.Label": "Путь к файлу лога",
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
"Options.LogFilePath.Select": "Выбрать…",
"Options.OverrideLogFile.Label": "Переопределить файл лога",
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
"Options.TitleMusic.Label": "Музыка игры",
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
"Options.Discord.Label": "Статус Discord",
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Common.On": "Включено",
"Common.Off": "Выключено",
"Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...",
"Console.AutoScroll": "Авто-прокрутка",
"Console.Split": "Разделить",
"Console.Copy": "Скопировать",
"Console.Clear": "Очистить",
"Console.WindowTitle": "Консоль SharpEmu",
"Launch.NoGameSelected": "Ничего не выбрано",
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
"Launch.Exited": "Завершено с кодом {0} ({1})",
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Лог: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "некорректные аргументы",
"Exit.EbootNotFound": "eboot не найден",
"Exit.RuntimeException": "ошибка выполнения",
"Exit.EmulationError": "ошибка эмуляции",
"Exit.Unknown": "неизвестная ошибка",
"Status.EmulatorLocating": "Эмулятор: поиск…",
"Status.EmulatorPath": "Эмулятор: {0}",
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
"Status.ScanningLibrary": "Сканирование библиотеки…",
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
"Status.Running": "Запущено: {0}",
"Status.Stopping": "Остановка…",
"Status.Idle": "Ожидание",
"Clipboard.Path": "Путь",
"Clipboard.TitleId": "ID игры",
"Discord.Playing": "Играет в {0}",
"Discord.Browsing": "Просматривает библиотеку",
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
"Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи"
}
+1 -3
View File
@@ -25,7 +25,6 @@ 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"));
@@ -1505,8 +1504,7 @@ public partial class MainWindow : Window
}
var incoming = new List<LogLine>();
while (incoming.Count < MaxConsoleLinesPerFlush &&
_pendingLines.TryDequeue(out var pending))
while (_pendingLines.TryDequeue(out var pending))
{
WriteFileLog(pending.Line);
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
Binary file not shown.
+12 -264
View File
@@ -120,9 +120,6 @@ 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;
@@ -391,26 +388,9 @@ 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(
@@ -2140,87 +2120,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
if (!ctx.TryWriteUInt32(outAddress, 256))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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}");
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2239,97 +2144,12 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
owner = state.DefaultOwner;
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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)}");
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2340,90 +2160,19 @@ public static class AgcExports
LibraryName = "libSceAgc")]
public static int DriverRegisterResource(CpuContext ctx)
{
var resourceHandleAddress = ctx[CpuRegister.Rdi];
var resourceAddress = ctx[CpuRegister.Rdi];
var owner = (uint)ctx[CpuRegister.Rsi];
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);
}
var nameAddress = ctx[CpuRegister.Rdx];
var type = (uint)ctx[CpuRegister.R8];
var flags = (uint)ctx[CpuRegister.R9];
TraceAgc(
$"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}");
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
$"name=0x{nameAddress:X16} 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",
@@ -4501,7 +4250,7 @@ public static class AgcExports
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
var gpuDispatch = false;
var computeError = string.Empty;
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
if (hasStorageBinding &&
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
{
var shaderKey = (
@@ -4571,8 +4320,7 @@ 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} " +
$"globals={evaluation.GlobalMemoryBindings.Count}" +
$"gpu={gpuDispatch} blits={blitCount}" +
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
$" bindings=[{string.Join(',', descriptions)}]");
}
-37
View File
@@ -1,37 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
}
+1 -1
View File
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
-46
View File
@@ -1,46 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace SharpEmu.Libs;
public static class HostTimerResolution
{
private const uint TargetPeriodMilliseconds = 1;
private static int _requested;
public static void Request()
{
if (Interlocked.Exchange(ref _requested, 1) != 0)
{
return;
}
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
[SupportedOSPlatform("windows")]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
private static extern uint TimeBeginPeriod(uint uPeriod);
}
-52
View File
@@ -78,58 +78,6 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json15InitParameter2C2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json15InitParameter212setAllocatorERNS0_12MemAllocatorE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json15InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json12Initializer2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
@@ -80,8 +80,14 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// Completion output was written when the command was submitted.
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
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);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -163,6 +169,27 @@ 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,7 +11,6 @@ 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;
@@ -90,16 +89,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
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;
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -109,16 +100,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
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;
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -128,15 +111,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
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;
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -146,18 +122,8 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
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;
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -509,43 +475,6 @@ 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,
@@ -727,6 +656,6 @@ public static class KernelEventQueueCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine(
$"[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}");
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
}
+56
View File
@@ -461,10 +461,66 @@ 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,15 +110,6 @@ 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;
@@ -1564,10 +1555,9 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
int directoryFd;
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
lock (_fdGate)
{
directoryFd = AllocateGuestFileDescriptor();
_openDirectories[directoryFd] = new OpenDirectory
{
Path = hostPath,
@@ -1588,10 +1578,9 @@ public static class KernelMemoryCompatExports
stream.Seek(0, SeekOrigin.End);
}
int fd;
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
lock (_fdGate)
{
fd = AllocateGuestFileDescriptor();
_openFiles[fd] = stream;
}
@@ -1683,30 +1672,6 @@ 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",
@@ -1988,12 +1953,6 @@ 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)
{
@@ -2037,17 +1996,6 @@ 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)
{
@@ -2277,17 +2225,6 @@ 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)
{
@@ -3504,18 +3441,6 @@ 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,
@@ -3989,7 +3914,7 @@ public static class KernelMemoryCompatExports
_ => unchecked((int)argumentSource.NextGpArg())
};
var formatted = value.ToString(CultureInfo.InvariantCulture);
var formatted = value.ToString();
if (showSign && value >= 0)
formatted = "+" + formatted;
else if (spaceForSign && value >= 0)
@@ -4013,7 +3938,7 @@ public static class KernelMemoryCompatExports
_ => (uint)argumentSource.NextGpArg()
};
var formatted = value.ToString(CultureInfo.InvariantCulture);
var formatted = value.ToString();
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
}
break;
@@ -4034,8 +3959,8 @@ public static class KernelMemoryCompatExports
};
var formatted = specifier == 'x'
? value.ToString("x", CultureInfo.InvariantCulture)
: value.ToString("X", CultureInfo.InvariantCulture);
? value.ToString("x")
: value.ToString("X");
if (alternateForm && value != 0)
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
@@ -4143,10 +4068,7 @@ public static class KernelMemoryCompatExports
var formatStr = precision >= 0
? $"{{0:{specifier}{precision}}}"
: $"{{0:{specifier}}}";
var formatted = string.Format(
CultureInfo.InvariantCulture,
formatStr,
value);
var formatted = string.Format(formatStr, value);
if (showSign && value >= 0)
formatted = "+" + formatted;
@@ -3,7 +3,6 @@
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -45,8 +44,6 @@ 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
{
@@ -140,7 +137,7 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx)
{
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
_ = ctx;
Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -354,95 +351,29 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx)
{
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
if (absoluteTimeAddress == 0 ||
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
// POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113).
var abstimeAddress = ctx[CpuRegister.Rdx];
if (abstimeAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var deadlineSeconds = unchecked((long)secondsRaw);
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
if (!ctx.TryReadUInt64(abstimeAddress, out var deadlineSeconds) ||
!ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
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"}");
}
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);
}
[SysAbiExport(
@@ -723,12 +654,11 @@ public static class KernelPthreadCompatExports
var acquired = state.Semaphore.Wait(0);
if (!acquired)
{
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Fibers retain the synchronous fallback to preserve switch state.
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
if (canCooperativelyBlock &&
// 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 &&
!tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
@@ -767,53 +697,6 @@ 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)
@@ -1193,13 +1076,6 @@ 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;
}
@@ -1296,13 +1172,7 @@ 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,
bool posixResult = false)
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
{
if (condAddress == 0 || mutexAddress == 0)
{
@@ -1340,10 +1210,9 @@ 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
{
@@ -1352,7 +1221,6 @@ public static class KernelPthreadCompatExports
}
finally
{
// Balance the surrounding lock statement.
Monitor.Enter(state.SyncRoot);
}
}
@@ -1363,7 +1231,7 @@ public static class KernelPthreadCompatExports
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
() => state.SignalEpoch != observedEpoch,
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
{
@@ -1447,7 +1315,7 @@ public static class KernelPthreadCompatExports
state,
timed,
waitResult);
return TranslatePthreadResult(waitResult, posixResult);
return waitResult;
}
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
@@ -1504,8 +1372,7 @@ public static class KernelPthreadCompatExports
PthreadCondState state,
ulong observedEpoch,
bool timed,
int releasedRecursion,
bool posixResult)
int releasedRecursion)
{
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
lock (state.SyncRoot)
@@ -1526,32 +1393,7 @@ public static class KernelPthreadCompatExports
}
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
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
};
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
}
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
@@ -1717,10 +1559,7 @@ public static class KernelPthreadCompatExports
return TimeSpan.Zero;
}
// Round positive sub-millisecond waits to the host sleep quantum.
return timeoutUsec < 1_000
? TimeSpan.FromMilliseconds(1)
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
}
private static TimeSpan GetCondSpuriousWakeTimeout()
@@ -1862,8 +1701,7 @@ public static class KernelPthreadCompatExports
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
}
@@ -85,29 +85,22 @@ public static class KernelRuntimeCompatExports
ExportName = "sceKernelNanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
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)
public static int KernelNanosleep(CpuContext ctx)
{
var requestAddress = ctx[CpuRegister.Rdi];
var remainAddress = ctx[CpuRegister.Rsi];
if (requestAddress == 0)
{
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
ctx[CpuRegister.Rax] = Einval;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> timespecBuffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
{
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
ctx[CpuRegister.Rax] = Efault;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
@@ -115,7 +108,8 @@ public static class KernelRuntimeCompatExports
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
{
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
ctx[CpuRegister.Rax] = Einval;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (tvSec == 0 && tvNsec == 0)
@@ -125,7 +119,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
GuestThreadExecution.Scheduler?.Pump(ctx, "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.
@@ -144,21 +138,6 @@ 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)
@@ -213,13 +192,6 @@ 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)
@@ -431,18 +403,6 @@ 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,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -13,9 +12,6 @@ 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; }
@@ -32,7 +28,6 @@ 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; }
@@ -125,7 +120,6 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
@@ -137,82 +131,36 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
if (!ctx.TryReadUInt32(timeoutAddress, out _))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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;
}
_ = 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 (!pollTimedOut)
var waiter = new SemaphoreWaiter
{
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);
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);
}
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
{
Thread.Sleep(0);
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);
}
else
{
Thread.Yield();
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
[SysAbiExport(
@@ -420,42 +368,6 @@ 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))
@@ -1,558 +0,0 @@
// 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;
}
}
-353
View File
@@ -338,359 +338,6 @@ 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",
-48
View File
@@ -95,54 +95,6 @@ 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",
+2 -39
View File
@@ -22,17 +22,6 @@ 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",
@@ -53,19 +42,8 @@ public static class NpManagerExports
LibraryName = "libSceNpManager")]
public static int NpGetOnlineId(CpuContext ctx)
{
// 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]);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -180,19 +158,4 @@ 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);
}
}
-12
View File
@@ -31,18 +31,6 @@ 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",
+20 -67
View File
@@ -14,18 +14,11 @@ 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 = 1000;
private const int PrimaryUserId = 1;
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;
@@ -185,16 +178,6 @@ 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",
@@ -277,44 +260,6 @@ 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;
@@ -348,17 +293,25 @@ public static class PadExports
r2 = Math.Max(r2, xpad.R2);
}
_cachedInputState = new PadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
L2: l2,
R2: r2);
_lastInputSampleTicks = now;
return _cachedInputState;
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);
}
[DllImport("user32.dll")]
+1 -3
View File
@@ -682,9 +682,7 @@ public static class PlayGoExports
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
// Full installs may omit PlayGo sidecar metadata.
TracePlayGo("metadata_missing; using fully-installed default chunk");
return new PlayGoMetadata(true, [(ushort)0]);
return PlayGoMetadata.Empty;
}
var chunkIds = LoadChunkIds(chunkDefsXml);
+13 -23
View File
@@ -30,17 +30,13 @@ 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;
}
}
@@ -248,6 +244,7 @@ public static class SaveDataExports
}
}
private static int _nextTransactionResource;
[SysAbiExport(
Nid = "gjRZNnw0JPE",
ExportName = "sceSaveDataCreateTransactionResource",
@@ -255,32 +252,25 @@ public static class SaveDataExports
LibraryName = "libSceSaveData")]
public static int SaveDataCreateTransactionResource(CpuContext ctx)
{
var memorySize = ctx[CpuRegister.Rdi];
int resource;
lock (_stateGate)
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var reserved = ctx[CpuRegister.Rsi];
var resourceAddress = ctx[CpuRegister.Rdx];
if (resourceAddress == 0)
{
resource = ++_nextTransactionResource;
_transactionResources.Add(resource);
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
return ctx.SetReturn(resource);
}
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
[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)
if (!ctx.TryWriteUInt32(resourceAddress, id))
{
_transactionResources.Remove(resource);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSaveData($"delete_transaction_resource resource={resource}");
TraceSaveData(
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
return ctx.SetReturn(0);
}
@@ -13,8 +13,7 @@ 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);
// Retail user-service IDs begin at 0x3E8.
private const int PrimaryUserId = 1000;
private const int PrimaryUserId = 1;
private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
@@ -112,8 +111,7 @@ public static class UserServiceExports
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx];
// Zero selects the current user.
if (userId != 0 && userId != PrimaryUserId)
if (userId != PrimaryUserId)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
}
@@ -139,28 +137,6 @@ 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",
+5 -26
View File
@@ -14,17 +14,6 @@ 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;
@@ -38,18 +27,13 @@ internal static class PngSplashLoader
return false;
}
var path = Path.Combine(app0Root, "sce_sys", fileName);
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
if (!File.Exists(path))
{
return false;
}
return TryDecode(
File.ReadAllBytes(path),
out pixels,
out width,
out height,
requestRgba);
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
}
catch
{
@@ -64,8 +48,7 @@ internal static class PngSplashLoader
ReadOnlySpan<byte> png,
out byte[] pixels,
out uint width,
out uint height,
bool requestRgba)
out uint height)
{
pixels = [];
width = 0;
@@ -171,13 +154,9 @@ internal static class PngSplashLoader
sourceOffset < reconstructed.Length;
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
{
pixels[targetOffset] = requestRgba
? reconstructed[sourceOffset]
: reconstructed[sourceOffset + 2];
pixels[targetOffset] = reconstructed[sourceOffset + 2];
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
pixels[targetOffset + 2] = requestRgba
? reconstructed[sourceOffset + 2]
: reconstructed[sourceOffset];
pixels[targetOffset + 2] = reconstructed[sourceOffset];
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
? reconstructed[sourceOffset + 3]
: (byte)0xFF;
+35 -120
View File
@@ -3,7 +3,6 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using SharpEmu.Logging;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -48,106 +47,54 @@ public static class VideoOutExports
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
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 Timer? _vblankPumpTimer;
private static int _vblankPumpActive;
private const int VblankPumpIntervalMs = 16;
private static void EnsureVblankPumpStarted()
{
if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0)
if (_vblankPumpTimer is not null)
{
return;
}
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);
}
_vblankPumpTimer = new Timer(
static _ => PumpVblanks(),
null,
VblankPumpIntervalMs,
VblankPumpIntervalMs);
}
private static void PumpVblanks()
{
lock (_vblankEdgeGate)
if (Interlocked.Exchange(ref _vblankPumpActive, 1) != 0)
{
_vblankEdgeSequence++;
Monitor.PulseAll(_vblankEdgeGate);
return;
}
VideoOutPortState[] ports;
lock (_stateGate)
try
{
if (_ports.Count == 0)
VideoOutPortState[] ports;
lock (_stateGate)
{
return;
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();
}
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
foreach (var port in ports)
{
SignalVblank(port);
}
}
foreach (var port in ports)
finally
{
SignalVblank(port);
Volatile.Write(ref _vblankPumpActive, 0);
}
}
@@ -155,7 +102,6 @@ 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"),
@@ -165,10 +111,6 @@ 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;
@@ -191,24 +133,9 @@ 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}";
lock (_stateGate)
{
_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()}";
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
}
}
@@ -447,20 +374,7 @@ public static class VideoOutExports
return OrbisVideoOutErrorInvalidHandle;
}
EnsureVblankPumpStarted();
lock (_vblankEdgeGate)
{
var entryEdge = _vblankEdgeSequence;
while (_vblankEdgeSequence == entryEdge)
{
if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds))
{
break;
}
}
}
Thread.Sleep(1);
SignalVblank(port);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -1071,7 +985,10 @@ public static class VideoOutExports
displayBuffer.PitchInPixel);
}
if (_dumpVideoOut)
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
StringComparison.Ordinal))
{
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
}
@@ -1131,11 +1048,9 @@ 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} " +
$"vblank_missed={missedEdges}");
$"presented_fps={presentedCount / elapsedSeconds:F1}");
}
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
@@ -11,7 +11,6 @@ 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;
@@ -601,8 +600,7 @@ internal static unsafe class VulkanVideoPresenter
var traceSubmission = false;
lock (_gate)
{
// VideoOut registration does not imply a rendered Vulkan image.
var known = _gpuGuestImages.ContainsKey(address);
var known = _availableGuestImages.ContainsKey(address);
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
@@ -624,7 +622,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 has no completed GPU render output");
$"{width}x{height} - flip target was never registered as a render output");
}
return false;
@@ -935,8 +933,6 @@ 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;
@@ -954,9 +950,6 @@ 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;
@@ -973,10 +966,6 @@ 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();
@@ -999,7 +988,7 @@ internal static unsafe class VulkanVideoPresenter
private readonly record struct GraphicsPipelineKey(
string VertexShader,
string FragmentShader,
Format RenderTargetFormat,
ulong RenderPass,
PrimitiveTopology Topology,
VulkanGuestBlendState Blend,
string ResourceLayout,
@@ -1149,19 +1138,12 @@ 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();
@@ -1571,11 +1553,8 @@ 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: {selectedName} ({selected.DeviceType})");
VideoOutExports.SetSelectedGpuName(selectedName);
_window.Title = VideoOutExports.GetWindowTitle();
$"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})");
}
private static int ScorePhysicalDevice(
@@ -1758,110 +1737,6 @@ 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(
@@ -1954,44 +1829,9 @@ 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
@@ -2338,7 +2178,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateGraphicsPipelines(
_device,
_pipelineCache,
default,
1,
&pipelineInfo,
null,
@@ -2373,7 +2213,6 @@ internal static unsafe class VulkanVideoPresenter
private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var vertexSpirv = draw.VertexSpirv;
@@ -2440,13 +2279,7 @@ internal static unsafe class VulkanVideoPresenter
CreateTranslatedDescriptorResources(
resources,
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit);
CreateTranslatedPipeline(
resources,
vertexSpirv,
draw.PixelSpirv,
renderPass,
renderTargetFormat,
extent);
CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent);
return resources;
}
catch
@@ -2742,13 +2575,12 @@ internal static unsafe class VulkanVideoPresenter
byte[] vertexSpirv,
byte[] fragmentSpirv,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv),
renderTargetFormat,
renderPass.Handle,
resources.Topology,
resources.Blend,
GetResourceLayoutKey(resources),
@@ -2904,7 +2736,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateGraphicsPipelines(
_device,
_pipelineCache,
default,
1,
&pipelineInfo,
null,
@@ -3084,7 +2916,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateComputePipelines(
_device,
_pipelineCache,
default,
1,
&pipelineInfo,
null,
@@ -3610,36 +3442,11 @@ internal static unsafe class VulkanVideoPresenter
{
const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211;
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)
var hash = offsetBasis;
foreach (var value in pixels)
{
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;
hash ^= value;
hash *= prime;
}
return hash;
@@ -3652,7 +3459,10 @@ internal static unsafe class VulkanVideoPresenter
uint width,
uint height)
{
if (!_dumpTextures ||
if (!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal) ||
texture.IsFallback ||
texture.IsStorage ||
GetTextureBytesPerPixel(texture.Format) != 4 ||
@@ -4673,7 +4483,6 @@ internal static unsafe class VulkanVideoPresenter
resources = CreateTranslatedDrawResources(
work.Draw,
target.RenderPass,
target.Format,
extent);
resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
@@ -5310,8 +5119,6 @@ internal static unsafe class VulkanVideoPresenter
return;
}
CompletePendingPresentation(wait: true);
byte[]? pixels = null;
if (presentation.Pixels is { } sourcePixels)
{
@@ -5359,7 +5166,6 @@ internal static unsafe class VulkanVideoPresenter
translatedResources = CreateTranslatedDrawResources(
translatedDraw,
_renderPass,
_swapchainFormat,
_extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented)
@@ -5491,16 +5297,7 @@ internal static unsafe class VulkanVideoPresenter
SignalSemaphoreCount = 1,
PSignalSemaphores = &renderFinished,
};
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;
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
var swapchain = _swapchain;
var presentInfo = new PresentInfoKHR
@@ -5516,7 +5313,6 @@ 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)
{
@@ -5529,13 +5325,19 @@ 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;
@@ -6789,9 +6591,7 @@ 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);
@@ -6802,11 +6602,6 @@ 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);
@@ -6898,7 +6693,6 @@ 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();
@@ -6932,11 +6726,6 @@ 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);
@@ -1,19 +0,0 @@
// 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);
}
}
+10 -32
View File
@@ -6,15 +6,19 @@ 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);
@@ -30,20 +34,14 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
append ? FileMode.Append : FileMode.Create,
FileAccess.Write,
FileShare.Read,
bufferSize: 65536,
bufferSize: 4096,
FileOptions.SequentialScan);
_writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536)
_writer = new StreamWriter(fileStream, Encoding.UTF8)
{
AutoFlush = false
AutoFlush = true
};
IncludeTimestamp = includeTimestamp;
_flushTimer = new Timer(
static state => ((FileLogSink)state!).FlushBuffered(),
this,
FlushInterval,
FlushInterval);
}
public bool IncludeTimestamp { get; set; }
@@ -86,31 +84,11 @@ 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)
-187
View File
@@ -1,187 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace SharpEmu.Logging;
/// <summary>Provides best-effort information about the host system.</summary>
public static class HostSystemInfo
{
private static readonly Lazy<string> CpuNameValue = new(GetCpuName);
private static readonly Lazy<string> GpuNameValue = new(GetPreferredGpuName);
private static readonly Lazy<string> MemoryDescriptionValue = new(GetMemoryDescription);
/// <summary>Host CPU name, or a safe fallback when it cannot be determined.</summary>
public static string CpuName => CpuNameValue.Value;
/// <summary>Preferred physical GPU name, or a safe fallback when it cannot be determined.</summary>
public static string GpuName => GpuNameValue.Value;
/// <summary>Returns a concise description of the host hardware for diagnostic logs.</summary>
public static string Summary =>
$"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
private static string GetCpuName()
{
if (!OperatingSystem.IsWindows())
{
return $"{Environment.ProcessorCount} logical processors";
}
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
{
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)
{
// Hardware information is diagnostic only.
}
return $"{Environment.ProcessorCount} logical processors";
}
private static string GetPreferredGpuName()
{
if (!OperatingSystem.IsWindows())
{
return "unknown";
}
try
{
var preferredName = "unknown";
var preferredScore = int.MinValue;
for (uint index = 0; ; index++)
{
var device = new DisplayDevice
{
cb = Marshal.SizeOf<DisplayDevice>(),
};
if (!EnumDisplayDevices(null, index, ref device, 0))
{
break;
}
var name = device.DeviceString?.Trim();
var score = ScoreGpu(name);
if (score > preferredScore)
{
preferredName = name!;
preferredScore = score;
}
}
return preferredScore > 0 ? preferredName : "unknown";
}
catch (Exception)
{
// Hardware information is diagnostic only.
return "unknown";
}
}
private static int ScoreGpu(string? name)
{
if (string.IsNullOrWhiteSpace(name) ||
name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
{
return -1;
}
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
{
return 2;
}
return 1;
}
private static string GetMemoryDescription()
{
if (OperatingSystem.IsWindows())
{
try
{
var status = new MemoryStatusEx
{
dwLength = (uint)Marshal.SizeOf<MemoryStatusEx>(),
};
if (GlobalMemoryStatusEx(ref status))
{
var megabytes = status.ullTotalPhys / (1024 * 1024);
var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
}
}
catch (Exception)
{
// Hardware information is diagnostic only.
}
}
return "unknown";
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DisplayDevice
{
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string? DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceKey;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MemoryStatusEx
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
}