Compare commits

...

7 Commits

Author SHA1 Message Date
Mike Saito 3d2c30b151 fix(core): lazy dlsym stub materialization, COW snapshots and deferred bootstrap logging (#94)
* fix(core): implement 4-tier lazy dlsym stub materialization and argument normalization

Enforce transactional and thread-safe resolution for standalone ELF bootstrapper pipelines. - Implement a 4-tier additive fallback cascade (T0: runtime symbols, T1: import entries scan, T2: Aerolib mapping, T3: runtime slack-pool lazy stub allocation at 0x7000_0000_0000). - Fix UnmanagedCallersOnly CLR runtime crashes on second bootstrap by adding NormalizeKernelDynlibDlsymArguments to detect and swap mirrored (symbol, handle) register inputs via rigorous pointer bounds verification. - Protect failure paths via CompleteKernelDynlibDlsymFailure, cleanly zero-filling target outputAddress buffers and returns Rax = -1 with zero managed logging execution in hot native paths.

* fix(core): harden lazy-stub diagnostics with COW snapshots and deferred bootstrap logging

Follow-up to lazy stub pool copy-on-write publishing in TryGetOrCreateLazyImportStub. - Snapshot _importEntries in ProbeReturnRip before near-call and PLT import lookup loops to prevent torn iteration during concurrent array replacement. - Defer SHARPEMU_LOG_BOOTSTRAP output: hot path records raw register slots in a ring buffer under lock; TryReadAsciiZ and Console.Error run only after import handler completion via DrainDeferredBootstrapTraces. - Normalize bootstrap dynlib register order at DispatchImport gateway entry before any logging or trace reads, so swapped RDI/RSI on Import#2 cannot fault the native gateway when bootstrap tracing is enabled. - Resolve lazy stub pool bounds from the full SelfLoader-mapped import region via VirtualQuery instead of a hardcoded 4 KiB cap. - Use ConcurrentDictionary for runtime symbol registration during concurrent dlsym. - Emit distinct [LOADER][WARN] reasons when import stub region resolution fails versus lazy stub pool exhaustion.
2026-07-13 12:25:37 +03:00
Berk 61d28e9e08 [CPU] optimize strcasecmp for hot path (#95) 2026-07-13 12:19:57 +03:00
Digote 4bd42795c7 [logging] Migrate HLE diagnostics to SharpEmuLog (#80)
Signed-off-by: Digote <45742711+Digote@users.noreply.github.com>
2026-07-13 12:11:27 +03:00
Mike Saito c03ca32a02 fix(vfs): harden getdirentries and APR filepath resolution (#77)
* fix(vfs): defer host cursor commit in getdirentries

Follow-up VFS hardening task applying the same guest-writes-first discipline established in the time subsystem to directory enumeration.

Deferred Commit in KernelGetdirentriesCore:

- Reordered guest output so the 512-byte dirent buffer is written first via TryWriteCompat, basep is updated second (when non-null) via TryWriteUInt64Compat, and directory.NextIndex is advanced only after both guest writes succeed.

- Removed the early basep write at method entry that could mutate guest memory before buffer validation and advance the host cursor before a successful dirent delivery, causing permanent entry loss on MEMORY_FAULT at bufferAddress.

- EOF handling: when currentIndex >= Entries.Length, write basep with the final offset and return 0 without mutating NextIndex, matching FreeBSD getdirentries(2) semantics and preventing infinite retry loops.

KernelGetdents path: basePointerAddress is passed as 0, so the transaction collapses to buffer write then host cursor advance with no basep side effect.

Out of scope: coalesced {id, size} writes in sceKernelAprResolveFilepathsToIdsAndFileSizes; NetCtl connected-state stubs.

Files: KernelMemoryCompatExports.cs

* fix(vfs): resolve-first bulk commit in APR filepath resolution

Refactored sceKernelAprResolveFilepathsToIdsAndFileSizes to stop writing ids and sizes into guest memory one element at a time.

- Removed the uint.MaxValue placeholder write at the start of each loop iteration.

- Path resolution and file size lookup now fill host-side buffers first; on EFAULT or NOT_FOUND the guest ids/sizes arrays are left untouched.

- ids and sizes are packed into contiguous byte buffers and written with one TryWriteCompat call per output array instead of separate TryWriteUInt32Compat / TryWriteUInt64Compat per index.

- AmprFileRegistry.Register is called only after guest writes succeed.

- AmprFileRegistry.ComputeFileId is internal so ids can be computed without registering paths during the resolve loop.

Files: KernelMemoryCompatExports.cs, AmprFileRegistry.cs
2026-07-13 12:10:20 +03:00
kuba 6e2878f2ff Add commit hash to video window title (#93) 2026-07-13 11:21:41 +03:00
ParantezTech 2e09b015bc Merge branch 'main' of https://github.com/par274/sharpemu 2026-07-13 01:10:08 +03:00
ParantezTech 7b9efd1539 [readme] update screenshot for dreaming sarah 2026-07-13 01:09:45 +03:00
15 changed files with 716 additions and 88 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+4 -1
View File
@@ -216,7 +216,10 @@
}
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
@@ -38,6 +38,71 @@ public sealed partial class DirectExecutionBackend
}
}
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
@@ -170,14 +235,15 @@ public sealed partial class DirectExecutionBackend
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != target)
if (importEntries[importIndex].Address != target)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -204,14 +270,14 @@ public sealed partial class DirectExecutionBackend
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != slotTarget)
if (importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
@@ -110,12 +111,13 @@ public sealed partial class DirectExecutionBackend
LastError = "Import dispatch called without active CPU context";
return 18446744071562199298uL;
}
if ((uint)importIndex >= (uint)_importEntries.Length)
var importEntries = _importEntries;
if ((uint)importIndex >= (uint)importEntries.Length)
{
LastError = $"Import dispatch index out of range: {importIndex}";
return 18446744071562199042uL;
}
ImportStubEntry importStubEntry = _importEntries[importIndex];
ImportStubEntry importStubEntry = importEntries[importIndex];
int num2 = Volatile.Read(in _rawSentinelRecoveries);
if (num2 != _lastReportedRawSentinelRecoveries)
{
@@ -159,6 +161,14 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
*(ulong*)(argPackPtr + 8) = cpuContext[CpuRegister.Rsi];
*(ulong*)(argPackPtr + 16) = cpuContext[CpuRegister.Rdx];
}
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
ulong num3 = cpuContext[CpuRegister.Rdx];
@@ -209,16 +219,6 @@ public sealed partial class DirectExecutionBackend
TraceGuestContext(
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
}
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
string symbolText = "<unreadable>";
if (TryReadAsciiZ(value2, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
if (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
@@ -383,6 +383,11 @@ public sealed partial class DirectExecutionBackend
{
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
if (_logBootstrap)
{
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
}
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
@@ -545,6 +550,7 @@ public sealed partial class DirectExecutionBackend
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
DrainDeferredBootstrapTraces();
return cpuContext[CpuRegister.Rax];
}
catch (Exception ex)
@@ -553,6 +559,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
DrainDeferredBootstrapTraces();
return 18446744071562199298uL;
}
}
@@ -705,6 +712,7 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -869,6 +877,7 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -1309,30 +1318,485 @@ public sealed partial class DirectExecutionBackend
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
try
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
outputAddress == 0 ||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
}
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
catch
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
cpuContext[CpuRegister.Rax] = 0uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void NormalizeKernelDynlibDlsymArguments(
CpuContext cpuContext,
out ulong symbolNameAddress,
out ulong outputAddress)
{
var handle = cpuContext[CpuRegister.Rdi];
symbolNameAddress = cpuContext[CpuRegister.Rsi];
outputAddress = cpuContext[CpuRegister.Rdx];
// Standalone bootstrap loaders sometimes call through the bridge with
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
if (symbolNameAddress < 0x10000 &&
IsPlausibleDynlibSymbolPointer(handle))
{
symbolNameAddress = handle;
handle = cpuContext[CpuRegister.Rsi];
cpuContext[CpuRegister.Rdi] = handle;
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
}
}
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
{
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
}
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
{
if (outputAddress != 0)
{
_ = TryWriteUInt64Compat(outputAddress, 0);
}
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private void ResetLazyDlsymStubState()
{
lock (_lazyDlsymStubGate)
{
_lazyDlsymStubCache.Clear();
_lazyImportStubPoolMapped = false;
_lazyImportStubPoolBase = 0;
_lazyImportStubNextSlot = 0;
_lazyImportStubPoolLimit = 0;
}
}
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
// Tier 0: ELF runtime symbols and aliases.
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
{
return true;
}
// Tier 1: existing import stub entries (duplicate prevention).
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
{
return true;
}
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
if (hasAerolibSymbol)
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
}
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
hasAerolibSymbol = true;
}
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
{
return false;
}
return TryGetOrCreateLazyImportStub(
dispatchNid,
symbolName,
hasAerolibSymbol ? hleSymbol : null,
export,
out guestAddress);
}
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(identifier))
{
return false;
}
var importEntries = _importEntries;
for (var i = 0; i < importEntries.Length; i++)
{
var entry = importEntries[i];
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
{
continue;
}
if (entry.Address >= 0x10000)
{
guestAddress = entry.Address;
return true;
}
}
return false;
}
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
{
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
{
return true;
}
if (entry.Export is not { } export)
{
return false;
}
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
}
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
{
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
}
private bool TryResolveLazyDispatchTarget(
string symbolName,
bool hasAerolibSymbol,
in SysAbiSymbol hleSymbol,
out string dispatchNid,
out ExportedFunction? export)
{
export = null;
dispatchNid = string.Empty;
if (IsKernelDynlibDlsymIdentifier(symbolName))
{
dispatchNid = KernelDynlibDlsymAerolibNid;
_ = _moduleManager.TryGetExport(dispatchNid, out export);
return true;
}
if (!hasAerolibSymbol)
{
return false;
}
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
{
dispatchNid = hleSymbol.Nid;
return true;
}
return false;
}
private bool TryGetOrCreateLazyImportStub(
string dispatchNid,
string symbolName,
SysAbiSymbol? hleSymbol,
ExportedFunction? export,
out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(dispatchNid))
{
return false;
}
lock (_lazyDlsymStubGate)
{
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
{
return true;
}
if (!EnsureLazyImportStubPoolMapped())
{
LogLazyImportStubMaterializationFailure(
"import stub region unresolved",
dispatchNid,
symbolName);
return false;
}
if (!TryAllocateLazyImportStubSlot(out guestAddress))
{
LogLazyImportStubMaterializationFailure(
"lazy import stub pool exhausted",
dispatchNid,
symbolName);
return false;
}
var previousEntries = _importEntries;
var importIndex = previousEntries.Length;
var newEntries = new ImportStubEntry[importIndex + 1];
if (importIndex > 0)
{
Array.Copy(previousEntries, newEntries, importIndex);
}
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
if (hostTrampoline == 0 ||
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
{
guestAddress = 0;
LogLazyImportStubMaterializationFailure(
"failed to patch lazy import stub trampoline",
dispatchNid,
symbolName);
return false;
}
_importEntries = newEntries;
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
return true;
}
}
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
{
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
{
return guestAddress >= 0x10000;
}
return false;
}
private void RegisterDlsymRuntimeSymbolKeys(
string symbolName,
string dispatchNid,
SysAbiSymbol? hleSymbol,
ulong guestAddress)
{
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
{
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
}
if (hleSymbol.HasValue)
{
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
}
}
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
{
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
{
return;
}
_runtimeSymbolsByName[key] = guestAddress;
_lazyDlsymStubCache[key] = guestAddress;
}
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
$"nid={dispatchNid} symbol='{symbolName}'");
}
private unsafe bool TryResolveImportStubRegionBounds(
ImportStubEntry[] importEntries,
out ulong regionBase,
out ulong regionLimit)
{
regionBase = 0;
regionLimit = 0;
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
{
var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride;
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
memoryInfo.RegionSize == 0 ||
memoryInfo.State != 4096)
{
continue;
}
var candidateLimit = candidateBase + memoryInfo.RegionSize;
var hasStub = false;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
{
continue;
}
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
{
continue;
}
hasStub = true;
break;
}
if (!hasStub)
{
continue;
}
regionBase = candidateBase;
regionLimit = candidateLimit;
return true;
}
ulong maxStubEnd = 0;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < ImportStubRegionCanonicalBase)
{
continue;
}
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > maxStubEnd)
{
maxStubEnd = entryEnd;
regionBase = ImportStubRegionCanonicalBase;
}
}
if (regionBase == 0 || maxStubEnd <= regionBase)
{
return false;
}
var spanBytes = maxStubEnd - regionBase;
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
return regionLimit > regionBase;
}
private bool EnsureLazyImportStubPoolMapped()
{
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
{
return true;
}
var importEntries = _importEntries;
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
{
return false;
}
ulong nextSlot = importStubRegionBase;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > nextSlot)
{
nextSlot = entryEnd;
}
}
if (nextSlot >= importStubRegionLimit)
{
return false;
}
_lazyImportStubPoolBase = importStubRegionBase;
_lazyImportStubNextSlot = nextSlot;
_lazyImportStubPoolLimit = importStubRegionLimit;
_lazyImportStubPoolMapped = true;
return true;
}
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
{
guestAddress = 0;
if (!_lazyImportStubPoolMapped ||
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
{
return false;
}
guestAddress = _lazyImportStubNextSlot;
_lazyImportStubNextSlot += LazyImportStubSlotSize;
return true;
}
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
{
address = 0;
@@ -1397,29 +1861,20 @@ public sealed partial class DirectExecutionBackend
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong bridgeHandle = cpuContext[CpuRegister.Rdi];
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
try
{
return result;
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
}
if (_logBootstrap)
catch
{
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
if (cpuContext[CpuRegister.Rax] == 0uL)
{
return OrbisGen2Result.ORBIS_GEN2_OK;
}
Console.Error.WriteLine(
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
return OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1460,6 +1915,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
Span<byte> destination = stackalloc byte[1];
for (int i = 0; i < maxLength; i++)
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -48,6 +49,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong Arg1,
ulong Arg2);
private readonly record struct DeferredBootstrapTraceEntry(
long DispatchIndex,
ulong Op,
ulong SymbolPointer,
ulong OutputPointer,
ulong ReturnRip);
#pragma warning disable CS0649
private struct EXCEPTION_POINTERS
{
@@ -232,7 +240,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private KeyValuePair<string, ulong>[] _runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
private readonly Dictionary<string, ulong> _runtimeSymbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ulong> _runtimeSymbolsByName =
new(StringComparer.Ordinal);
// Keep in sync with SelfLoader import-stub mapping constants.
private const ulong ImportStubRegionCanonicalBase = 0x0000_7000_0000_0000UL;
private const ulong ImportStubRegionAddressStride = 0x0000_0000_0100_0000UL;
private const ulong LazyImportStubSlotSize = 0x10;
private const ulong ImportStubRegionPageSize = 0x1000UL;
private const string KernelDynlibDlsymAerolibNid = "LwG8g3niqwA";
private readonly object _lazyDlsymStubGate = new();
private readonly Dictionary<string, ulong> _lazyDlsymStubCache = new(StringComparer.Ordinal);
private ulong _lazyImportStubPoolBase;
private ulong _lazyImportStubNextSlot;
private ulong _lazyImportStubPoolLimit;
private bool _lazyImportStubPoolMapped;
private readonly RecentImportTraceEntry[] _recentImportTrace = new RecentImportTraceEntry[64];
@@ -240,6 +272,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private int _recentImportTraceWriteIndex;
private readonly DeferredBootstrapTraceEntry[] _deferredBootstrapTrace = new DeferredBootstrapTraceEntry[32];
private int _deferredBootstrapTraceCount;
private int _deferredBootstrapTraceWriteIndex;
private readonly object _deferredBootstrapTraceGate = new();
private readonly string[] _distinctImportNidHistory = new string[128];
private int _distinctImportNidHistoryCount;
@@ -855,8 +895,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
result = OrbisGen2Result.ORBIS_GEN2_OK;
LastError = null;
InitializeRuntimeSymbolIndex(runtimeSymbols);
ResetLazyDlsymStubState();
_recentImportTraceCount = 0;
_recentImportTraceWriteIndex = 0;
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTraceCount = 0;
_deferredBootstrapTraceWriteIndex = 0;
}
_distinctImportNidHistoryCount = 0;
_distinctImportNidHistoryWriteIndex = 0;
_lastDistinctImportNid = string.Empty;
@@ -930,6 +976,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
finally
{
DrainDeferredBootstrapTraces();
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
}
@@ -4456,13 +4503,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ulong rsp = cpuContext[CpuRegister.Rsp];
Console.Error.WriteLine($"[LOADER][ERROR] Stall snapshot: rip=0x{cpuContext.Rip:X16} rsp=0x{rsp:X16} rbp=0x{cpuContext[CpuRegister.Rbp]:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16} rbx=0x{cpuContext[CpuRegister.Rbx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16}");
ulong num = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
for (int i = 0; i < _importEntries.Length; i++)
var importEntries = _importEntries;
for (int i = 0; i < importEntries.Length; i++)
{
if (_importEntries[i].Address != num)
if (importEntries[i].Address != num)
{
continue;
}
string text = _importEntries[i].Nid;
string text = importEntries[i].Nid;
if (_moduleManager.TryGetExport(text, out ExportedFunction export))
{
Console.Error.WriteLine($"[LOADER][ERROR] Stall import-stub: rip=0x{num:X16} nid={text} -> {export.LibraryName}:{export.Name}");
@@ -4653,6 +4701,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ClearImportHandlerTrampolines();
_importEntries = Array.Empty<ImportStubEntry>();
_runtimeSymbolsByName.Clear();
ResetLazyDlsymStubState();
_importNidHashCache.Clear();
StopStallWatchdog();
if (_exceptionHandler != 0)
+1 -1
View File
@@ -140,7 +140,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
+4 -1
View File
@@ -65,7 +65,10 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
+6 -4
View File
@@ -3,11 +3,13 @@
using System.Buffers.Binary;
using System.Linq;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class Aerolib : ISymbolCatalog
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
if (resourceName == null)
{
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
Log.Error("Embedded resource 'aerolib.bin' not found");
return;
}
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
Log.Error("Failed to open embedded resource stream");
return;
}
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
_byExportName[name] = symbol;
}
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
}
}
+6 -3
View File
@@ -3,11 +3,14 @@
using System.Collections.Concurrent;
using System.Reflection;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
@@ -47,7 +50,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -106,7 +109,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
Log.Warning($"NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
@@ -114,7 +117,7 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
+4
View File
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
+5 -1
View File
@@ -1,6 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
internal static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
@@ -1689,15 +1689,15 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var entryCount = (int)count;
Span<uint> localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount];
Span<ulong> localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount];
var resolvedGuestPaths = new string[entryCount];
var resolvedHostPaths = new string[entryCount];
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var index = (int)i;
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
@@ -1712,23 +1712,47 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
var fileId = AmprFileRegistry.ComputeFileId(guestPath);
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
localIds[index] = fileId;
localSizes[index] = fileSize;
resolvedGuestPaths[index] = guestPath;
resolvedHostPaths[index] = hostPath;
}
Span<byte> sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)];
for (ulong i = 0; i < count; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]);
}
if (!TryWriteCompat(ctx, sizesAddress, sizePayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (idsAddress != 0)
{
Span<byte> idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)];
for (ulong i = 0; i < count; i++)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]);
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
if (!TryWriteCompat(ctx, idsAddress, idPayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
for (var i = 0; i < entryCount; i++)
{
AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -6350,19 +6374,19 @@ public static class KernelMemoryCompatExports
}
var currentIndex = directory.NextIndex;
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (currentIndex >= directory.Entries.Length)
{
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var entryName = directory.Entries[currentIndex];
directory.NextIndex = currentIndex + 1;
var entryBytes = Encoding.UTF8.GetBytes(entryName);
var nameLength = Math.Min(entryBytes.Length, 255);
@@ -6381,6 +6405,13 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
directory.NextIndex = currentIndex + 1;
ctx[CpuRegister.Rax] = 512;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -64,7 +64,7 @@ public static class VideoOutExports
private static long _vblankSignalCount;
private static long _flipSubmitCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version, string? emulatorCommitSha)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title))
@@ -79,9 +79,10 @@ 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()}";
lock (_stateGate)
{
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
}
}
+6
View File
@@ -98,6 +98,12 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
}
}