mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a828f8ed3 | |||
| a63a7de35a | |||
| fe1a592496 | |||
| 9d4dad2c24 | |||
| 38d94d8e54 | |||
| 61a1abdb13 | |||
| 88452218cb | |||
| 427a511e48 | |||
| e103795768 | |||
| 24c6e44800 | |||
| 3a30d6419f | |||
| 2d8a795f23 | |||
| adb2acdb39 | |||
| 5e5bead02c | |||
| e98f02966a |
@@ -2,7 +2,6 @@
|
|||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
@@ -10,7 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||||
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -81,6 +81,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -103,6 +104,16 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"Silk.NET.Core": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
@@ -338,10 +338,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
orbisGen2Result = DispatchBootstrapBridge();
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
|
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||||
|
}
|
||||||
else if (importStubEntry.Export is { } cachedExport &&
|
else if (importStubEntry.Export is { } cachedExport &&
|
||||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||||
{
|
{
|
||||||
@@ -628,25 +633,56 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
|
"q1cHNfGycLI" or // scePadRead
|
||||||
|
"xk0AcarP3V4" or // scePadOpen
|
||||||
|
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||||
|
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||||
|
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
||||||
|
|
||||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
|
var resultValue = unchecked((int)result);
|
||||||
|
if (resultValue > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var expectedFileProbeMiss =
|
var expectedFileProbeMiss =
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||||
IsExpectedFileProbeNotFoundNid(nid);
|
IsExpectedFileProbeNotFoundNid(nid);
|
||||||
var expectedTimedWaitTimeout =
|
var expectedTimedWaitTimeout =
|
||||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||||
unchecked((int)result) == 60;
|
unchecked((int)result) == 60;
|
||||||
|
var expectedEqueueTimeout =
|
||||||
|
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
|
var expectedUserServiceNoEvent =
|
||||||
|
string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) &&
|
||||||
|
resultValue == unchecked((int)0x80960007);
|
||||||
|
var expectedPrivacyInvalidParameter =
|
||||||
|
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
||||||
|
resultValue == unchecked((int)0x80960009);
|
||||||
|
if (!expectedFileProbeMiss &&
|
||||||
|
!expectedTimedWaitTimeout &&
|
||||||
|
!expectedEqueueTimeout &&
|
||||||
|
!expectedMutexTrylockBusy &&
|
||||||
|
!expectedUserServiceNoEvent &&
|
||||||
|
!expectedPrivacyInvalidParameter)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = nid + "\0" + (int)result;
|
if (!ShouldLogExpectedImportResults())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = nid + "\0" + resultValue;
|
||||||
int count;
|
int count;
|
||||||
lock (_importResultLogSampleGate)
|
lock (_importResultLogSampleGate)
|
||||||
{
|
{
|
||||||
@@ -658,6 +694,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return count <= 8 || count % 10000 == 0;
|
return count <= 8 || count % 10000 == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldLogExpectedImportResults() =>
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"eV9wAD2riIA" or // sceKernelStat
|
"eV9wAD2riIA" or // sceKernelStat
|
||||||
@@ -1180,8 +1222,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
|
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
||||||
|
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1194,6 +1239,62 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
var alias = symbolName switch
|
||||||
|
{
|
||||||
|
"scriptingGetMem" => "malloc",
|
||||||
|
"scriptingFreeMem" => "free",
|
||||||
|
"scriptingRealloc" => "realloc",
|
||||||
|
"scriptingCalloc" => "calloc",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return alias != null && TryResolveRuntimeSymbolAddress(alias, out address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrbisGen2Result DispatchIl2CppApiLookupSymbol()
|
||||||
|
{
|
||||||
|
var cpuContext = ActiveCpuContext;
|
||||||
|
if (cpuContext == null)
|
||||||
|
{
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
||||||
|
var outputAddress = cpuContext[CpuRegister.Rsi];
|
||||||
|
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||||
|
outputAddress == 0 ||
|
||||||
|
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
||||||
|
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
||||||
|
if (outputAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuContext[CpuRegister.Rax] = 0;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
||||||
|
{
|
||||||
|
if (TryResolveRuntimeSymbolAddress(symbolName, out address))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||||
|
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
||||||
|
}
|
||||||
|
|
||||||
private OrbisGen2Result DispatchBootstrapBridge()
|
private OrbisGen2Result DispatchBootstrapBridge()
|
||||||
{
|
{
|
||||||
var cpuContext = ActiveCpuContext;
|
var cpuContext = ActiveCpuContext;
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
|
private readonly object _allocationSearchHintGate = new();
|
||||||
private readonly List<MemoryRegion> _regions = new();
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
|
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
@@ -19,7 +21,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
@@ -35,6 +38,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private ulong _guestAllocationOffset;
|
||||||
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
@@ -96,7 +100,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +114,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
var preferReserveOnly = !executable &&
|
||||||
|
alignedSize >= LargeDataReserveThreshold &&
|
||||||
|
alignedSize > FullCommitRegionLimit;
|
||||||
|
|
||||||
void* result = null;
|
void* result = null;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
@@ -139,7 +145,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -193,12 +199,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
lazyPrimeState = committedBytes == primeBytes
|
lazyPrimeState = committedBytes == primeBytes
|
||||||
? $"ok:{committedBytes:X}"
|
? $"ok:{committedBytes:X}"
|
||||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||||
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||||
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -227,7 +233,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var allocationKind = reservedOnly
|
var allocationKind = reservedOnly
|
||||||
? "reserved data memory (lazy commit)"
|
? "reserved data memory (lazy commit)"
|
||||||
: (executable ? "executable memory" : "data memory");
|
: (executable ? "executable memory" : "data memory");
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||||
|
|
||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
@@ -247,7 +253,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = AlignUp(size, PageSize);
|
var alignedSize = AlignUp(size, PageSize);
|
||||||
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||||
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||||
{
|
{
|
||||||
@@ -267,6 +274,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||||
if (actualAddress == cursor)
|
if (actualAddress == cursor)
|
||||||
{
|
{
|
||||||
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +342,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
|
lock (_allocationSearchHintGate)
|
||||||
|
{
|
||||||
|
_allocationSearchHints.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -390,7 +402,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
ApplySegmentProtection(mapStart, mapEnd, protection);
|
||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -793,6 +805,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
var regionEnd = region.VirtualAddress + region.Size;
|
var regionEnd = region.VirtualAddress + region.Size;
|
||||||
|
if (region.VirtualAddress >= end)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionEnd <= address)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (address < regionEnd && region.VirtualAddress < end)
|
if (address < regionEnd && region.VirtualAddress < end)
|
||||||
{
|
{
|
||||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||||
@@ -807,6 +829,37 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return overlapEnd != 0;
|
return overlapEnd != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ulong GetAllocationSearchCursor(
|
||||||
|
ulong desiredAddress,
|
||||||
|
ulong requestedCursor,
|
||||||
|
ulong alignment,
|
||||||
|
bool executable)
|
||||||
|
{
|
||||||
|
lock (_allocationSearchHintGate)
|
||||||
|
{
|
||||||
|
var key = (desiredAddress, alignment, executable);
|
||||||
|
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
|
||||||
|
hintedCursor > requestedCursor)
|
||||||
|
{
|
||||||
|
return AlignUp(hintedCursor, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestedCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllocationSearchCursor(
|
||||||
|
ulong desiredAddress,
|
||||||
|
ulong alignment,
|
||||||
|
bool executable,
|
||||||
|
ulong nextCursor)
|
||||||
|
{
|
||||||
|
lock (_allocationSearchHintGate)
|
||||||
|
{
|
||||||
|
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||||
{
|
{
|
||||||
offset = 0;
|
offset = 0;
|
||||||
@@ -975,6 +1028,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return checked((value + mask) & ~mask);
|
return checked((value + mask) & ~mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ResolveLazyReservePrimeBytes()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
|
||||||
|
if (ulong.TryParse(configured, out var megabytes))
|
||||||
|
{
|
||||||
|
return megabytes == 0
|
||||||
|
? 0
|
||||||
|
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultLazyReservePrimeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceVmem(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[VMEM] {message}");
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -543,6 +543,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
Path.Combine(ebootDirectory, "sce_module"),
|
Path.Combine(ebootDirectory, "sce_module"),
|
||||||
Path.Combine(ebootDirectory, "sce_modules"),
|
Path.Combine(ebootDirectory, "sce_modules"),
|
||||||
|
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||||
}
|
}
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(Directory.Exists)
|
.Where(Directory.Exists)
|
||||||
@@ -554,7 +555,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(Directory.EnumerateFiles)
|
.SelectMany(directory => Directory
|
||||||
|
.EnumerateFiles(directory)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||||
.Where(path =>
|
.Where(path =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
@@ -562,7 +565,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
})
|
})
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var modulePaths = allModulePaths
|
var modulePaths = allModulePaths
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -88,6 +89,16 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"Silk.NET.Core": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
@@ -1204,6 +1204,51 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "q88lQ+GP5Yk",
|
||||||
|
ExportName = "sceAgcDcbDrawIndex",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbDrawIndex(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
var indexAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var modifier = (uint)ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
|
||||||
|
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
||||||
|
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, baseCommand + 8, (uint)(indexAddress >> 32)) ||
|
||||||
|
!TryWriteUInt32(ctx, baseCommand + 12, Pm4(2, ItIndexBufferSize, 0)) ||
|
||||||
|
!TryWriteUInt32(ctx, baseCommand + 16, indexCount))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) ||
|
||||||
|
!TryWriteUInt32(ctx, drawCommand, Pm4(5, ItDrawIndex2, 0)) ||
|
||||||
|
!TryWriteUInt32(ctx, drawCommand + 4, indexCount) ||
|
||||||
|
!TryWriteUInt32(ctx, drawCommand + 8, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, drawCommand + 12, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, drawCommand + 16, 0))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb_draw_index buf=0x{commandBufferAddress:X16} " +
|
||||||
|
$"base=0x{baseCommand:X16} draw=0x{drawCommand:X16} " +
|
||||||
|
$"count={indexCount} index=0x{indexAddress:X16}");
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, drawCommand);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Yw0jKSqop+E",
|
Nid = "Yw0jKSqop+E",
|
||||||
ExportName = "sceAgcDcbDrawIndexAuto",
|
ExportName = "sceAgcDcbDrawIndexAuto",
|
||||||
@@ -2006,6 +2051,88 @@ public static class AgcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uJziRsODk1c",
|
||||||
|
ExportName = "sceAgcDriverGetResourceRegistrationMaxNameLength",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverGetResourceRegistrationMaxNameLength(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
if (outAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteUInt32(ctx, outAddress, 256))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const uint DefaultAgcOwner = 1;
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "F0ZXt5q0ZTA",
|
||||||
|
ExportName = "sceAgcDriverGetDefaultOwner",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverGetDefaultOwner(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var ownerAddress = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
if (ownerAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteUInt32(ctx, ownerAddress, DefaultAgcOwner))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "W5z4eZrjEas",
|
||||||
|
ExportName = "sceAgcDriverRegisterResource",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverRegisterResource(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var resourceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var owner = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var type = (uint)ctx[CpuRegister.R8];
|
||||||
|
var flags = (uint)ctx[CpuRegister.R9];
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
||||||
|
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-KRzWekV120",
|
||||||
|
ExportName = "sceAgcDriverUnknown_KRzWekV120",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DriverUnknownKRzWekV120(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_unknown_krz rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||||
|
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||||
|
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "h9z6+0hEydk",
|
Nid = "h9z6+0hEydk",
|
||||||
ExportName = "sceAgcSuspendPoint",
|
ExportName = "sceAgcSuspendPoint",
|
||||||
@@ -3482,7 +3609,7 @@ public static class AgcExports
|
|||||||
',',
|
',',
|
||||||
draw.GlobalMemoryBindings.Select((binding, index) =>
|
draw.GlobalMemoryBindings.Select((binding, index) =>
|
||||||
$"{index}:0x{binding.BaseAddress:X16}:{binding.Data.Length}:" +
|
$"{index}:0x{binding.BaseAddress:X16}:{binding.Data.Length}:" +
|
||||||
Convert.ToHexString(binding.Data.AsSpan(0, Math.Min(binding.Data.Length, 32)))));
|
Convert.ToHexString(binding.Data.AsSpan(0, Math.Min(binding.Data.Length, 256)))));
|
||||||
var indices = draw.IndexBuffer is { } indexBuffer
|
var indices = draw.IndexBuffer is { } indexBuffer
|
||||||
? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" +
|
? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" +
|
||||||
Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Data.Length, 32)))
|
Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Data.Length, 32)))
|
||||||
@@ -3617,7 +3744,7 @@ public static class AgcExports
|
|||||||
descriptor.Width > 8192 ||
|
descriptor.Width > 8192 ||
|
||||||
descriptor.Height > 8192)
|
descriptor.Height > 8192)
|
||||||
{
|
{
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3635,7 +3762,7 @@ public static class AgcExports
|
|||||||
sourceByteCount > MaxPresentedTextureBytes ||
|
sourceByteCount > MaxPresentedTextureBytes ||
|
||||||
sourceByteCount > int.MaxValue)
|
sourceByteCount > int.MaxValue)
|
||||||
{
|
{
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3698,7 +3825,7 @@ public static class AgcExports
|
|||||||
var source = new byte[(int)sourceByteCount];
|
var source = new byte[(int)sourceByteCount];
|
||||||
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
||||||
{
|
{
|
||||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3741,18 +3868,25 @@ public static class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(bool isStorage = false) =>
|
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||||
new(
|
bool isStorage,
|
||||||
|
uint format,
|
||||||
|
uint numberType)
|
||||||
|
{
|
||||||
|
var fallbackFormat = format == 0 ? 10u : format;
|
||||||
|
var fallbackNumberType = numberType;
|
||||||
|
return new(
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
56,
|
fallbackFormat,
|
||||||
NumberType: 0,
|
fallbackNumberType,
|
||||||
[0, 0, 0, 255],
|
[0, 0, 0, 255],
|
||||||
IsFallback: true,
|
IsFallback: true,
|
||||||
IsStorage: isStorage,
|
IsStorage: isStorage,
|
||||||
MipLevels: 1,
|
MipLevels: 1,
|
||||||
MipLevel: 0);
|
MipLevel: 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||||
descriptor.Count >= 4
|
descriptor.Count >= 4
|
||||||
|
|||||||
@@ -467,9 +467,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
_imageBindingByPc.TryAdd(binding.Pc, index);
|
||||||
var isStorage =
|
var isStorage =
|
||||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
||||||
var isDepth =
|
|
||||||
binding.Opcode.Contains("SampleC", StringComparison.Ordinal) ||
|
|
||||||
binding.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
|
||||||
var (format, componentKind) =
|
var (format, componentKind) =
|
||||||
DecodeImageFormat(binding.ResourceDescriptor);
|
DecodeImageFormat(binding.ResourceDescriptor);
|
||||||
var componentType = componentKind switch
|
var componentType = componentKind switch
|
||||||
@@ -494,7 +491,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var imageType = _module.TypeImage(
|
var imageType = _module.TypeImage(
|
||||||
componentType,
|
componentType,
|
||||||
SpirvImageDim.Dim2D,
|
SpirvImageDim.Dim2D,
|
||||||
depth: isDepth,
|
depth: false,
|
||||||
arrayed: false,
|
arrayed: false,
|
||||||
multisampled: false,
|
multisampled: false,
|
||||||
sampled: isStorage ? 2u : 1u,
|
sampled: isStorage ? 2u : 1u,
|
||||||
@@ -1661,18 +1658,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
var imageOperands =
|
var imageOperands =
|
||||||
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
||||||
|
var reference = hasCompare
|
||||||
|
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||||
|
: 0u;
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
coordinates,
|
coordinates,
|
||||||
};
|
};
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
operands.Add(
|
|
||||||
Bitcast(
|
|
||||||
_floatType,
|
|
||||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageOperands != 0)
|
if (imageOperands != 0)
|
||||||
{
|
{
|
||||||
@@ -1689,29 +1682,14 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
hasCompare
|
explicitLod
|
||||||
? explicitLod
|
? SpirvOp.ImageSampleExplicitLod
|
||||||
? SpirvOp.ImageSampleDrefExplicitLod
|
: SpirvOp.ImageSampleImplicitLod,
|
||||||
: SpirvOp.ImageSampleDrefImplicitLod
|
resource.VectorType,
|
||||||
: explicitLod
|
|
||||||
? SpirvOp.ImageSampleExplicitLod
|
|
||||||
: SpirvOp.ImageSampleImplicitLod,
|
|
||||||
hasCompare ? resource.ComponentType : resource.VectorType,
|
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
var scalar = sampled;
|
sampled = EmitManualDepthCompare(resource, sampled, reference);
|
||||||
sampled = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
resource.VectorType,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
resource.ComponentKind == ImageComponentKind.Float
|
|
||||||
? Float(1)
|
|
||||||
: resource.ComponentKind == ImageComponentKind.Uint
|
|
||||||
? UInt(1)
|
|
||||||
: _module.Constant(_intType, 1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (instruction.Opcode.StartsWith(
|
else if (instruction.Opcode.StartsWith(
|
||||||
@@ -1725,6 +1703,9 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
||||||
var coordinates = BuildFloatCoordinates(image, start);
|
var coordinates = BuildFloatCoordinates(image, start);
|
||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
|
var reference = hasCompare
|
||||||
|
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||||
|
: 0u;
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
@@ -1732,10 +1713,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
};
|
};
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
operands.Add(
|
operands.Add(UInt(0));
|
||||||
Bitcast(
|
|
||||||
_floatType,
|
|
||||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1756,11 +1734,28 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
hasCompare
|
SpirvOp.ImageGather,
|
||||||
? SpirvOp.ImageDrefGather
|
|
||||||
: SpirvOp.ImageGather,
|
|
||||||
resource.VectorType,
|
resource.VectorType,
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
|
if (hasCompare)
|
||||||
|
{
|
||||||
|
var compared = new uint[4];
|
||||||
|
for (var component = 0u; component < 4; component++)
|
||||||
|
{
|
||||||
|
var texel = _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeExtract,
|
||||||
|
resource.ComponentType,
|
||||||
|
sampled,
|
||||||
|
component);
|
||||||
|
compared[component] = EmitDepthCompareScalar(resource, texel, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
sampled = _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
resource.VectorType,
|
||||||
|
compared);
|
||||||
|
}
|
||||||
|
|
||||||
writeAllComponents = true;
|
writeAllComponents = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1794,6 +1789,67 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private uint EmitDepthCompareScalar(
|
||||||
|
SpirvImageResource resource,
|
||||||
|
uint texel,
|
||||||
|
uint reference)
|
||||||
|
{
|
||||||
|
var texelAsFloat = resource.ComponentKind switch
|
||||||
|
{
|
||||||
|
ImageComponentKind.Uint => _module.AddInstruction(
|
||||||
|
SpirvOp.ConvertUToF, _floatType, texel),
|
||||||
|
ImageComponentKind.Sint => _module.AddInstruction(
|
||||||
|
SpirvOp.ConvertSToF, _floatType, texel),
|
||||||
|
_ => texel,
|
||||||
|
};
|
||||||
|
var passes = _module.AddInstruction(
|
||||||
|
SpirvOp.FOrdLessThanEqual,
|
||||||
|
_boolType,
|
||||||
|
reference,
|
||||||
|
texelAsFloat);
|
||||||
|
return _module.AddInstruction(
|
||||||
|
SpirvOp.Select,
|
||||||
|
resource.ComponentType,
|
||||||
|
passes,
|
||||||
|
resource.ComponentKind switch
|
||||||
|
{
|
||||||
|
ImageComponentKind.Uint => UInt(1),
|
||||||
|
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
||||||
|
_ => Float(1),
|
||||||
|
},
|
||||||
|
resource.ComponentKind switch
|
||||||
|
{
|
||||||
|
ImageComponentKind.Uint => UInt(0),
|
||||||
|
ImageComponentKind.Sint => _module.Constant(_intType, 0),
|
||||||
|
_ => Float(0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint EmitManualDepthCompare(
|
||||||
|
SpirvImageResource resource,
|
||||||
|
uint sampledVector,
|
||||||
|
uint reference)
|
||||||
|
{
|
||||||
|
var texel = _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeExtract,
|
||||||
|
resource.ComponentType,
|
||||||
|
sampledVector,
|
||||||
|
0u);
|
||||||
|
var scalar = EmitDepthCompareScalar(resource, texel, reference);
|
||||||
|
return _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
resource.VectorType,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
resource.ComponentKind switch
|
||||||
|
{
|
||||||
|
ImageComponentKind.Uint => UInt(1),
|
||||||
|
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
||||||
|
_ => Float(1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
||||||
{
|
{
|
||||||
var x = Bitcast(
|
var x = Bitcast(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public static class AmprExports
|
|||||||
private const uint KernelEventQueueRecordType = 2;
|
private const uint KernelEventQueueRecordType = 2;
|
||||||
private const uint WriteAddressRecordType = 3;
|
private const uint WriteAddressRecordType = 3;
|
||||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||||
|
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly bool _traceAmpr =
|
private static readonly bool _traceAmpr =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
private static readonly bool _traceAmprReads =
|
private static readonly bool _traceAmprReads =
|
||||||
@@ -37,6 +38,23 @@ public static class AmprExports
|
|||||||
public ulong WriteOffset;
|
public ulong WriteOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class CachedHostFile
|
||||||
|
{
|
||||||
|
public CachedHostFile(string path)
|
||||||
|
{
|
||||||
|
Stream = new FileStream(
|
||||||
|
path,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
|
bufferSize: 1024 * 1024,
|
||||||
|
FileOptions.RandomAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Gate { get; } = new();
|
||||||
|
public FileStream Stream { get; }
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8aI7R7WaOlc",
|
Nid = "8aI7R7WaOlc",
|
||||||
ExportName = "sceAmprCommandBufferConstructor",
|
ExportName = "sceAmprCommandBufferConstructor",
|
||||||
@@ -249,7 +267,7 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
@@ -634,24 +652,43 @@ public static class AmprExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = new FileStream(
|
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
||||||
hostPath,
|
{
|
||||||
FileMode.Open,
|
return openResult;
|
||||||
FileAccess.Read,
|
}
|
||||||
FileShare.ReadWrite | FileShare.Delete,
|
|
||||||
ChunkSize,
|
long fileLength;
|
||||||
FileOptions.SequentialScan);
|
lock (cachedFile.Gate)
|
||||||
if (fileOffset >= (ulong)stream.Length)
|
{
|
||||||
|
fileLength = cachedFile.Stream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileOffset >= (ulong)fileLength)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.Position = unchecked((long)fileOffset);
|
|
||||||
|
|
||||||
while (bytesRead < size)
|
while (bytesRead < size)
|
||||||
{
|
{
|
||||||
|
if (bytesRead > ulong.MaxValue - fileOffset)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var absoluteOffset = fileOffset + bytesRead;
|
||||||
|
if (absoluteOffset > long.MaxValue)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
var read = stream.Read(buffer, 0, request);
|
int read;
|
||||||
|
lock (cachedFile.Gate)
|
||||||
|
{
|
||||||
|
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
||||||
|
read = cachedFile.Stream.Read(buffer, 0, request);
|
||||||
|
}
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -681,6 +718,44 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result)
|
||||||
|
{
|
||||||
|
file = null!;
|
||||||
|
result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
string cachePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cachePath = Path.GetFullPath(hostPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
cachePath = hostPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lazy = _hostFileCache.GetOrAdd(
|
||||||
|
cachePath,
|
||||||
|
static path => new Lazy<CachedHostFile>(() => new CachedHostFile(path), isThreadSafe: true));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = lazy.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
_hostFileCache.TryRemove(cachePath, out _);
|
||||||
|
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
_hostFileCache.TryRemove(cachePath, out _);
|
||||||
|
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool AppendReadFileRecord(
|
private static bool AppendReadFileRecord(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class AudioOutExports
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||||
|
private static int _nextPortHandle;
|
||||||
|
|
||||||
|
private sealed class PortState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _paceGate = new();
|
||||||
|
private long _nextSilentOutput;
|
||||||
|
|
||||||
|
public PortState(
|
||||||
|
int userId,
|
||||||
|
int type,
|
||||||
|
uint bufferLength,
|
||||||
|
uint frequency,
|
||||||
|
int format,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat,
|
||||||
|
WinMmAudioPort? backend)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
Type = type;
|
||||||
|
BufferLength = bufferLength;
|
||||||
|
Frequency = frequency;
|
||||||
|
Format = format;
|
||||||
|
Channels = channels;
|
||||||
|
BytesPerSample = bytesPerSample;
|
||||||
|
IsFloat = isFloat;
|
||||||
|
Backend = backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int UserId { get; }
|
||||||
|
public int Type { get; }
|
||||||
|
public uint BufferLength { get; }
|
||||||
|
public uint Frequency { get; }
|
||||||
|
public int Format { get; }
|
||||||
|
public int Channels { get; }
|
||||||
|
public int BytesPerSample { get; }
|
||||||
|
public bool IsFloat { get; }
|
||||||
|
public WinMmAudioPort? Backend { get; }
|
||||||
|
public int BufferByteLength =>
|
||||||
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
|
public void PaceSilence()
|
||||||
|
{
|
||||||
|
long delay;
|
||||||
|
lock (_paceGate)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_nextSilentOutput < now)
|
||||||
|
{
|
||||||
|
_nextSilentOutput = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = _nextSilentOutput - now;
|
||||||
|
_nextSilentOutput += checked(
|
||||||
|
(long)Math.Ceiling(
|
||||||
|
Stopwatch.Frequency * (double)BufferLength / Frequency));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Backend?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JfEPXVxhFqA",
|
||||||
|
ExportName = "sceAudioOutInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ekNvsT22rsY",
|
||||||
|
ExportName = "sceAudioOutOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||||
|
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
|
||||||
|
var format = unchecked((int)ctx[CpuRegister.R9]);
|
||||||
|
if (bufferLength == 0 || frequency == 0 ||
|
||||||
|
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
WinMmAudioPort? backend = null;
|
||||||
|
string backendName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
backend = new WinMmAudioPort(frequency);
|
||||||
|
backendName = "winmm";
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
backendName = "silent";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = Interlocked.Increment(ref _nextPortHandle);
|
||||||
|
Ports[handle] = new PortState(
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
bufferLength,
|
||||||
|
frequency,
|
||||||
|
format,
|
||||||
|
channels,
|
||||||
|
bytesPerSample,
|
||||||
|
isFloat,
|
||||||
|
backend);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||||
|
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||||
|
$"{bufferLength} frames, backend={backendName}");
|
||||||
|
return SetReturn(ctx, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "s1--uE9mBFw",
|
||||||
|
ExportName = "sceAudioOutClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!Ports.TryRemove(handle, out var port))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
port.Dispose();
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "QOQtbeDqsT4",
|
||||||
|
ExportName = "sceAudioOutOutput",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutOutput(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||||
|
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port.Backend is null ||
|
||||||
|
!port.Backend.Submit(
|
||||||
|
source,
|
||||||
|
port.BufferLength,
|
||||||
|
port.Channels,
|
||||||
|
port.BytesPerSample,
|
||||||
|
port.IsFloat))
|
||||||
|
{
|
||||||
|
port.PaceSilence();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "b+uAV89IlxE",
|
||||||
|
ExportName = "sceAudioOutSetVolume",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutSetVolume(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
Ports.ContainsKey(handle)
|
||||||
|
? 0
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetFormat(
|
||||||
|
int rawFormat,
|
||||||
|
out int channels,
|
||||||
|
out int bytesPerSample,
|
||||||
|
out bool isFloat)
|
||||||
|
{
|
||||||
|
var format = rawFormat & 0xFF;
|
||||||
|
channels = format switch
|
||||||
|
{
|
||||||
|
0 or 3 => 1,
|
||||||
|
1 or 4 => 2,
|
||||||
|
2 or 5 or 6 or 7 => 8,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
|
||||||
|
isFloat = bytesPerSample == 4;
|
||||||
|
return channels != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
internal sealed class WinMmAudioPort : IDisposable
|
||||||
|
{
|
||||||
|
private const uint WaveMapper = uint.MaxValue;
|
||||||
|
private const uint CallbackEvent = 0x0005_0000;
|
||||||
|
private const ushort WaveFormatPcm = 1;
|
||||||
|
private const uint WaveHeaderDone = 0x0000_0001;
|
||||||
|
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly AutoResetEvent _completion = new(false);
|
||||||
|
private readonly Queue<NativeBuffer> _buffers = new();
|
||||||
|
private IntPtr _device;
|
||||||
|
private int _queuedPcmBytes;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public WinMmAudioPort(uint sampleRate)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = new WaveFormat
|
||||||
|
{
|
||||||
|
FormatTag = WaveFormatPcm,
|
||||||
|
Channels = 2,
|
||||||
|
SamplesPerSecond = sampleRate,
|
||||||
|
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||||
|
BlockAlign = 4,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
ExtraSize = 0,
|
||||||
|
};
|
||||||
|
var result = WaveOutOpen(
|
||||||
|
out _device,
|
||||||
|
WaveMapper,
|
||||||
|
ref format,
|
||||||
|
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||||
|
IntPtr.Zero,
|
||||||
|
CallbackEvent);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
uint frames,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputLength = checked((int)frames * 4);
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
while (_queuedPcmBytes != 0 &&
|
||||||
|
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
||||||
|
{
|
||||||
|
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ConvertToStereoPcm16(
|
||||||
|
source,
|
||||||
|
output.AsSpan(0, outputLength),
|
||||||
|
checked((int)frames),
|
||||||
|
channels,
|
||||||
|
bytesPerSample,
|
||||||
|
isFloat);
|
||||||
|
return QueueBuffer(output.AsSpan(0, outputLength));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_device != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
WaveOutReset(_device);
|
||||||
|
while (_buffers.TryDequeue(out var buffer))
|
||||||
|
{
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
WaveOutClose(_device);
|
||||||
|
_device = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
_completion.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||||
|
var headerAddress = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = new WaveHeader
|
||||||
|
{
|
||||||
|
Data = dataAddress,
|
||||||
|
BufferLength = checked((uint)data.Length),
|
||||||
|
};
|
||||||
|
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||||
|
Marshal.StructureToPtr(header, headerAddress, false);
|
||||||
|
|
||||||
|
var result = WaveOutPrepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WaveOutWrite(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||||
|
_queuedPcmBytes += data.Length;
|
||||||
|
dataAddress = IntPtr.Zero;
|
||||||
|
headerAddress = IntPtr.Zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (headerAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(headerAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dataAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReapCompletedBuffers()
|
||||||
|
{
|
||||||
|
while (_buffers.TryPeek(out var buffer))
|
||||||
|
{
|
||||||
|
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||||
|
if ((header.Flags & WaveHeaderDone) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Dequeue();
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseBuffer(NativeBuffer buffer)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
buffer.Header,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
_queuedPcmBytes -= buffer.Length;
|
||||||
|
Marshal.FreeHGlobal(buffer.Header);
|
||||||
|
Marshal.FreeHGlobal(buffer.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConvertToStereoPcm16(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
Span<byte> destination,
|
||||||
|
int frames,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||||
|
for (var frame = 0; frame < frames; frame++)
|
||||||
|
{
|
||||||
|
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||||
|
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||||
|
var right = channels == 1
|
||||||
|
? left
|
||||||
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short ReadSample(
|
||||||
|
ReadOnlySpan<byte> frame,
|
||||||
|
int channel,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||||
|
if (!isFloat)
|
||||||
|
{
|
||||||
|
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||||
|
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||||
|
return checked((short)MathF.Round(value * short.MaxValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||||
|
private struct WaveFormat
|
||||||
|
{
|
||||||
|
public ushort FormatTag;
|
||||||
|
public ushort Channels;
|
||||||
|
public uint SamplesPerSecond;
|
||||||
|
public uint AverageBytesPerSecond;
|
||||||
|
public ushort BlockAlign;
|
||||||
|
public ushort BitsPerSample;
|
||||||
|
public ushort ExtraSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WaveHeader
|
||||||
|
{
|
||||||
|
public IntPtr Data;
|
||||||
|
public uint BufferLength;
|
||||||
|
public uint BytesRecorded;
|
||||||
|
public nuint User;
|
||||||
|
public uint Flags;
|
||||||
|
public uint Loops;
|
||||||
|
public IntPtr Next;
|
||||||
|
public nuint Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||||
|
private static extern uint WaveOutOpen(
|
||||||
|
out IntPtr device,
|
||||||
|
uint deviceId,
|
||||||
|
ref WaveFormat format,
|
||||||
|
IntPtr callback,
|
||||||
|
IntPtr instance,
|
||||||
|
uint flags);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||||
|
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||||
|
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||||
|
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||||
|
private static extern uint WaveOutReset(IntPtr device);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||||
|
private static extern uint WaveOutClose(IntPtr device);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.AvPlayer;
|
||||||
|
|
||||||
|
public static class AvPlayerExports
|
||||||
|
{
|
||||||
|
private const int InvalidParameters = unchecked((int)0x806A0001);
|
||||||
|
private static readonly object StateGate = new();
|
||||||
|
private static readonly HashSet<ulong> Players = new();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aS66RI0gGgo",
|
||||||
|
ExportName = "sceAvPlayerInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
public static int AvPlayerInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] == 0 ||
|
||||||
|
!KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Players.Add(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = handle;
|
||||||
|
return unchecked((int)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HD1YKVU26-M",
|
||||||
|
ExportName = "sceAvPlayerPostInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
public static int AvPlayerPostInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
||||||
|
? 0
|
||||||
|
: InvalidParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "NkJwDzKmIlw",
|
||||||
|
ExportName = "sceAvPlayerClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAvPlayer")]
|
||||||
|
public static int AvPlayerClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
|
|
||||||
|
public static class MsgDialogExports
|
||||||
|
{
|
||||||
|
private const int AlreadyInitialized = unchecked((int)0x80B80002);
|
||||||
|
private static int _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lDqxaY1UbEo",
|
||||||
|
ExportName = "sceMsgDialogInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMsgDialog")]
|
||||||
|
public static int MsgDialogInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||||
|
? 0
|
||||||
|
: AlreadyInitialized;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -276,6 +276,22 @@ public static class KernelEventQueueCompatExports
|
|||||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||||
var timeoutAddress = ctx[CpuRegister.R8];
|
var timeoutAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (!IsValidEqueue(handle))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventsAddress == 0 || eventCapacity < 1)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint timeoutUsec = 0;
|
||||||
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
@@ -300,6 +316,41 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
|
||||||
|
{
|
||||||
|
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
|
||||||
|
var deadline = Environment.TickCount64 +
|
||||||
|
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
while (!HasPendingEvents(handle))
|
||||||
|
{
|
||||||
|
var remaining = deadline - Environment.TickCount64;
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deliveredCount > 0)
|
||||||
|
{
|
||||||
|
TraceEventQueue(ctx, "wait-timed-deliver", handle);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceEventQueue(ctx, "wait-timeout", handle);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
TraceEventQueue(ctx, "wait", handle);
|
TraceEventQueue(ctx, "wait", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -330,6 +381,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
queued = true;
|
queued = true;
|
||||||
|
Monitor.PulseAll(_eventQueueGate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queued)
|
if (queued)
|
||||||
@@ -494,7 +546,9 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return deliveredCount > 0
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasPendingEvents(ulong handle)
|
private static bool HasPendingEvents(ulong handle)
|
||||||
@@ -612,4 +666,17 @@ public static class KernelEventQueueCompatExports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
return ctx.Memory.TryWrite(address, buffer);
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelExceptionCompatExports
|
||||||
|
{
|
||||||
|
private static readonly HashSet<int> AllowedSignals = new() { 1, 4, 8, 10, 11, 30 };
|
||||||
|
private static readonly Dictionary<int, ulong> _installedHandlers = new();
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WkwEd3N7w0Y",
|
||||||
|
ExportName = "sceKernelInstallExceptionHandler",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int InstallExceptionHandler(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var handler = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (!AllowedSignals.Contains(signum))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_installedHandlers.ContainsKey(signum))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
_installedHandlers[signum] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Qhv5ARAoOEc",
|
||||||
|
ExportName = "sceKernelRemoveExceptionHandler",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int RemoveExceptionHandler(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
|
||||||
|
if (!AllowedSignals.Contains(signum))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_installedHandlers.Remove(signum);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -365,6 +365,13 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mqQMh1zPPT8",
|
||||||
|
ExportName = "fstat",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
ExportName = "printf",
|
ExportName = "printf",
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -103,10 +103,12 @@ public static class KernelMemoryCompatExports
|
|||||||
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
||||||
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
||||||
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
||||||
|
private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
|
||||||
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
|
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
|
||||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static long _nextFileDescriptor = 2;
|
private static long _nextFileDescriptor = 2;
|
||||||
private static ulong _nextPhysicalAddress;
|
private static ulong _nextPhysicalAddress;
|
||||||
private static ulong _nextVirtualAddress;
|
private static ulong _nextVirtualAddress;
|
||||||
@@ -231,6 +233,25 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
||||||
|
{
|
||||||
|
if (address == 0 || length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
_mappedRegions[address] = new MappedRegion(
|
||||||
|
address,
|
||||||
|
length,
|
||||||
|
Protection: 0,
|
||||||
|
IsFlexible: false,
|
||||||
|
IsDirect: false,
|
||||||
|
DirectStart: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8zTFvBIAIN8",
|
Nid = "8zTFvBIAIN8",
|
||||||
ExportName = "memset",
|
ExportName = "memset",
|
||||||
@@ -1322,6 +1343,7 @@ public static class KernelMemoryCompatExports
|
|||||||
if (IsMutatingOpen(flags))
|
if (IsMutatingOpen(flags))
|
||||||
{
|
{
|
||||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||||
|
InvalidateAprFileSizeCache(hostPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
||||||
@@ -1530,6 +1552,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
File.Delete(hostPath);
|
File.Delete(hostPath);
|
||||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||||
|
InvalidateAprFileSizeCache(hostPath);
|
||||||
AddNegativeStatCacheForGuestPath(guestPath);
|
AddNegativeStatCacheForGuestPath(guestPath);
|
||||||
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2435,6 +2458,36 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hwVSPCmp5tM",
|
||||||
|
ExportName = "sceKernelCheckedReleaseDirectMemory",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelCheckedReleaseDirectMemory(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var start = ctx[CpuRegister.Rdi];
|
||||||
|
var length = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
if (!_directAllocations.TryGetValue(start, out var allocation) ||
|
||||||
|
allocation.Length != length)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
_directAllocations.Remove(start);
|
||||||
|
_nextPhysicalAddress = GetDirectMemoryHighWaterMarkLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "L-Q3LEjIbgA",
|
Nid = "L-Q3LEjIbgA",
|
||||||
ExportName = "sceKernelMapDirectMemory",
|
ExportName = "sceKernelMapDirectMemory",
|
||||||
@@ -2546,8 +2599,11 @@ public static class KernelMemoryCompatExports
|
|||||||
var length = ctx[CpuRegister.Rsi];
|
var length = ctx[CpuRegister.Rsi];
|
||||||
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
var flags = ctx[CpuRegister.Rcx];
|
var flags = ctx[CpuRegister.Rcx];
|
||||||
Console.Error.WriteLine(
|
if (ShouldTraceDirectMemory())
|
||||||
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
||||||
|
}
|
||||||
if (inOutAddressPointer == 0 || length == 0)
|
if (inOutAddressPointer == 0 || length == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
@@ -2577,8 +2633,11 @@ public static class KernelMemoryCompatExports
|
|||||||
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
if (ShouldTraceDirectMemory())
|
||||||
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedAddress == 0)
|
if (mappedAddress == 0)
|
||||||
{
|
{
|
||||||
@@ -2614,6 +2673,16 @@ public static class KernelMemoryCompatExports
|
|||||||
return KernelMapNamedFlexibleMemory(ctx);
|
return KernelMapNamedFlexibleMemory(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4h6F1LLbTiw",
|
||||||
|
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return KernelMapNamedFlexibleMemory(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "2SKEx6bSq-4",
|
Nid = "2SKEx6bSq-4",
|
||||||
ExportName = "sceKernelBatchMap",
|
ExportName = "sceKernelBatchMap",
|
||||||
@@ -2667,6 +2736,44 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DGMG3JshrZU",
|
||||||
|
ExportName = "sceKernelSetVirtualRangeName",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelSetVirtualRangeName(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var address = ctx[CpuRegister.Rdi];
|
||||||
|
var length = ctx[CpuRegister.Rsi];
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (nameAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, nameAddress, OrbisKernelMaximumNameLength, out var nameBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Encoding.UTF8.GetString(nameBytes);
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
if (!TryFindVirtualQueryRegionLocked(address, findNext: false, out var region) ||
|
||||||
|
length > region.Length ||
|
||||||
|
address < region.Address ||
|
||||||
|
length > region.Address + region.Length - address)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mappedRegionNames[region.Address] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rVjRvHJ0X6c",
|
Nid = "rVjRvHJ0X6c",
|
||||||
ExportName = "sceKernelVirtualQuery",
|
ExportName = "sceKernelVirtualQuery",
|
||||||
@@ -2724,11 +2831,16 @@ public static class KernelMemoryCompatExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
|
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
|
||||||
payload[32] = unchecked((byte)stateFlags);
|
payload[32] = unchecked((byte)stateFlags);
|
||||||
|
|
||||||
var name = region.IsDirect
|
string name;
|
||||||
? "direct"
|
lock (_memoryGate)
|
||||||
: region.IsFlexible
|
{
|
||||||
? "flexible"
|
name = _mappedRegionNames.GetValueOrDefault(region.Address)
|
||||||
: string.Empty;
|
?? (region.IsDirect
|
||||||
|
? "direct"
|
||||||
|
: region.IsFlexible
|
||||||
|
? "flexible"
|
||||||
|
: string.Empty);
|
||||||
|
}
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
var nameBytes = Encoding.ASCII.GetBytes(name);
|
var nameBytes = Encoding.ASCII.GetBytes(name);
|
||||||
@@ -3870,118 +3982,17 @@ public static class KernelMemoryCompatExports
|
|||||||
ulong alignment,
|
ulong alignment,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
var executable = (protection & OrbisProtCpuExec) != 0;
|
||||||
if (length == 0)
|
return KernelVirtualRangeAllocator.TryReserve(
|
||||||
{
|
ctx,
|
||||||
return false;
|
desiredAddress,
|
||||||
}
|
length,
|
||||||
|
executable,
|
||||||
try
|
alignment,
|
||||||
{
|
allowSearch: true,
|
||||||
object memoryObject = ctx.Memory;
|
allowAllocateAtAlternative: false,
|
||||||
MethodInfo? allocateAt = null;
|
"reserve range",
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
out mappedAddress);
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
|
||||||
{
|
|
||||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAtOrAbove is not null && allocateAt is not null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAtOrAbove is not null || allocateAt is not null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
if (innerProperty is null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerValue = innerProperty.GetValue(memoryObject);
|
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, memoryObject))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryObject = innerValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
|
||||||
if (allocateAtOrAbove is not null)
|
|
||||||
{
|
|
||||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
|
||||||
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
|
||||||
mappedAddress = searchedAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAt is null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, executable, false }
|
|
||||||
: new object[] { desiredAddress, length, executable };
|
|
||||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedAddress = allocated;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][TRACE] reserve range threw while invoking AllocateAt");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsMappedGuestRangeAvailable(
|
private static bool IsMappedGuestRangeAvailable(
|
||||||
@@ -4134,6 +4145,20 @@ public static class KernelMemoryCompatExports
|
|||||||
var app0Root = ResolveApp0Root();
|
var app0Root = ResolveApp0Root();
|
||||||
if (!string.IsNullOrWhiteSpace(app0Root))
|
if (!string.IsNullOrWhiteSpace(app0Root))
|
||||||
{
|
{
|
||||||
|
if (string.Equals(guestPath, "$", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(guestPath, "$/", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(guestPath, "$\\", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return app0Root;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guestPath.StartsWith("$/", StringComparison.Ordinal) ||
|
||||||
|
guestPath.StartsWith("$\\", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var relative = NormalizeMountRelativePath(guestPath[2..]);
|
||||||
|
return Path.Combine(app0Root, relative);
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -5796,22 +5821,40 @@ public static class KernelMemoryCompatExports
|
|||||||
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
||||||
{
|
{
|
||||||
size = 0;
|
size = 0;
|
||||||
|
|
||||||
|
string cachePath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(hostPath);
|
cachePath = Path.GetFullPath(hostPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
cachePath = hostPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(cachePath);
|
||||||
if (fileInfo.Exists)
|
if (fileInfo.Exists)
|
||||||
{
|
{
|
||||||
var length = fileInfo.Length;
|
var length = fileInfo.Length;
|
||||||
size = length < 0 ? 0UL : unchecked((ulong)length);
|
size = length < 0 ? 0UL : unchecked((ulong)length);
|
||||||
|
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!new DirectoryInfo(hostPath).Exists)
|
if (!new DirectoryInfo(cachePath).Exists)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size = 65536;
|
size = 65536;
|
||||||
|
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6110,6 +6153,20 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void InvalidateAprFileSizeCache(string hostPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hostPath = Path.GetFullPath(hostPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// The cache key remains the original path when normalization fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
_aprFileSizeCache.TryRemove(hostPath, out _);
|
||||||
|
}
|
||||||
|
|
||||||
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
|
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
|
||||||
{
|
{
|
||||||
if (count <= 0)
|
if (count <= 0)
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Xl257M4VNI",
|
||||||
|
ExportName = "pthread_equal",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int PosixPthreadEqual(CpuContext ctx) => PthreadEqual(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "T72hz6ffq08",
|
Nid = "T72hz6ffq08",
|
||||||
ExportName = "scePthreadYield",
|
ExportName = "scePthreadYield",
|
||||||
|
|||||||
@@ -717,6 +717,35 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FXPWHNk8Of0",
|
||||||
|
ExportName = "scePthreadAttrGetschedparam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetschedparam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var schedParamAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (attrAddress == 0 || schedParamAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "DzES9hQF4f4",
|
Nid = "DzES9hQF4f4",
|
||||||
ExportName = "scePthreadAttrSetschedparam",
|
ExportName = "scePthreadAttrSetschedparam",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -87,11 +86,11 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
if (micros < 1000)
|
if (micros < 1000)
|
||||||
{
|
{
|
||||||
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
|
||||||
// relinquish a full host time slice so spin workers cannot starve producers.
|
// PS microsecond waits into Windows millisecond sleeps on hot paths.
|
||||||
if ((++_shortUsleepCount & 31) == 0)
|
if ((++_shortUsleepCount & 255) == 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -739,8 +738,11 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
if (ShouldTraceVirtualMemory())
|
||||||
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
{
|
{
|
||||||
@@ -752,6 +754,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1732,117 +1735,16 @@ public static class KernelRuntimeCompatExports
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
return KernelVirtualRangeAllocator.TryReserve(
|
||||||
if (length == 0)
|
ctx,
|
||||||
{
|
desiredAddress,
|
||||||
return false;
|
length,
|
||||||
}
|
executable: false,
|
||||||
|
alignment,
|
||||||
try
|
allowSearch,
|
||||||
{
|
allowAllocateAtAlternative: allowSearch,
|
||||||
object memoryObject = ctx.Memory;
|
"reserve_virtual_range",
|
||||||
MethodInfo? allocateAt = null;
|
out mappedAddress);
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
|
||||||
{
|
|
||||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAtOrAbove is not null && allocateAt is not null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAtOrAbove is not null || allocateAt is not null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
if (innerProperty is null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerValue = innerProperty.GetValue(memoryObject);
|
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, memoryObject))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryObject = innerValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowSearch && allocateAtOrAbove is not null)
|
|
||||||
{
|
|
||||||
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
|
|
||||||
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
|
||||||
mappedAddress = searchedAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allocateAt is null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, false, allowSearch }
|
|
||||||
: new object[] { desiredAddress, length, false };
|
|
||||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedAddress = allocated;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][TRACE] reserve_virtual_range: AllocateAt invocation threw");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
@@ -1855,4 +1757,9 @@ public static class KernelRuntimeCompatExports
|
|||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
return (value + mask) & ~mask;
|
return (value + mask) & ~mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceVirtualMemory()
|
||||||
|
{
|
||||||
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIRTUAL_MEMORY"), "1", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
internal static class KernelVirtualRangeAllocator
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
||||||
|
|
||||||
|
public static bool TryReserve(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong desiredAddress,
|
||||||
|
ulong length,
|
||||||
|
bool executable,
|
||||||
|
ulong alignment,
|
||||||
|
bool allowSearch,
|
||||||
|
bool allowAllocateAtAlternative,
|
||||||
|
string traceName,
|
||||||
|
out ulong mappedAddress)
|
||||||
|
{
|
||||||
|
mappedAddress = 0;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
||||||
|
{
|
||||||
|
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
||||||
|
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
||||||
|
if (searchResult is bool trueValue && trueValue &&
|
||||||
|
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||||
|
{
|
||||||
|
mappedAddress = searchedAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor.AllocateAt is null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
||||||
|
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
||||||
|
: new object[] { desiredAddress, length, executable };
|
||||||
|
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
||||||
|
if (result is not ulong allocated || allocated == 0)
|
||||||
|
{
|
||||||
|
var resultType = result?.GetType().FullName ?? "null";
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedAddress = allocated;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
||||||
|
{
|
||||||
|
target = rootMemory;
|
||||||
|
accessor = default;
|
||||||
|
|
||||||
|
for (var depth = 0; depth < 4; depth++)
|
||||||
|
{
|
||||||
|
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
||||||
|
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor.InnerProperty is null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerValue = accessor.InnerProperty.GetValue(target);
|
||||||
|
if (innerValue is null || ReferenceEquals(innerValue, target))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = innerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Accessor DiscoverAccessor(Type type)
|
||||||
|
{
|
||||||
|
MethodInfo? allocateAt = null;
|
||||||
|
MethodInfo? allocateAtOrAbove = null;
|
||||||
|
var allocateAtHasAllowAlternativeArg = false;
|
||||||
|
|
||||||
|
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
var parameters = candidate.GetParameters();
|
||||||
|
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
||||||
|
parameters.Length == 5 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
|
parameters[3].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
||||||
|
{
|
||||||
|
allocateAtOrAbove = candidate;
|
||||||
|
}
|
||||||
|
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (parameters.Length == 3 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = false;
|
||||||
|
}
|
||||||
|
else if (parameters.Length == 4 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
|
parameters[3].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct Accessor(
|
||||||
|
MethodInfo? AllocateAt,
|
||||||
|
MethodInfo? AllocateAtOrAbove,
|
||||||
|
bool AllocateAtHasAllowAlternativeArg,
|
||||||
|
PropertyInfo? InnerProperty);
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Ngs2;
|
||||||
|
|
||||||
|
public static class Ngs2Exports
|
||||||
|
{
|
||||||
|
private const int OrbisNgs2ErrorInvalidOutAddress = unchecked((int)0x804A0053);
|
||||||
|
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||||
|
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||||
|
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||||
|
private const ulong HandleStorageSize = 0x20;
|
||||||
|
private const int RenderBufferInfoSize = 0x18;
|
||||||
|
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
private static readonly object StateGate = new();
|
||||||
|
private static readonly Dictionary<ulong, SystemState> Systems = new();
|
||||||
|
private static readonly Dictionary<ulong, RackState> Racks = new();
|
||||||
|
private static readonly Dictionary<ulong, VoiceState> Voices = new();
|
||||||
|
private static long _nextUid;
|
||||||
|
private static long _renderCount;
|
||||||
|
|
||||||
|
private sealed record SystemState(uint Uid);
|
||||||
|
private sealed record RackState(ulong SystemHandle, uint RackId);
|
||||||
|
private sealed record VoiceState(ulong RackHandle, uint VoiceIndex);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mPYgU4oYpuY",
|
||||||
|
ExportName = "sceNgs2SystemCreateWithAllocator",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemCreateWithAllocator(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||||
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "u-WrYDaJA3k",
|
||||||
|
ExportName = "sceNgs2SystemDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Systems.Remove(handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rackHandles = Racks
|
||||||
|
.Where(pair => pair.Value.SystemHandle == handle)
|
||||||
|
.Select(pair => pair.Key)
|
||||||
|
.ToArray();
|
||||||
|
foreach (var rackHandle in rackHandles)
|
||||||
|
{
|
||||||
|
RemoveRackLocked(rackHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "U546k6orxQo",
|
||||||
|
ExportName = "sceNgs2RackCreateWithAllocator",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackCreateWithAllocator(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var systemHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var rackId = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var outHandleAddress = ctx[CpuRegister.R8];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||||
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Racks[handle] = new RackState(systemHandle, rackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lCqD7oycmIM",
|
||||||
|
ExportName = "sceNgs2RackDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Racks.ContainsKey(handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveRackLocked(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MwmHz8pAdAo",
|
||||||
|
ExportName = "sceNgs2RackGetVoiceHandle",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackGetVoiceHandle(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var rackHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var voiceIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Racks.ContainsKey(rackHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = Voices.FirstOrDefault(
|
||||||
|
pair => pair.Value.RackHandle == rackHandle && pair.Value.VoiceIndex == voiceIndex);
|
||||||
|
if (existing.Key != 0)
|
||||||
|
{
|
||||||
|
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||||
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uu94irFOGpA",
|
||||||
|
ExportName = "sceNgs2VoiceControl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceControl(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AbYvTOZ8Pts",
|
||||||
|
ExportName = "sceNgs2VoiceRunCommands",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "i0VnXM-C9fc",
|
||||||
|
ExportName = "sceNgs2SystemRender",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemRender(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var systemHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var bufferInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferInfoCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < bufferInfoCount; i++)
|
||||||
|
{
|
||||||
|
var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize);
|
||||||
|
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||||
|
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferAddress != 0 && bufferSize != 0)
|
||||||
|
{
|
||||||
|
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Interlocked.Increment(ref _renderCount);
|
||||||
|
if (ShouldTrace() && (count <= 4 || count % 10_000 == 0))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||||
|
{
|
||||||
|
handle = 0;
|
||||||
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||||
|
data.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||||
|
return ctx.Memory.TryWrite(handle, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||||
|
{
|
||||||
|
Span<byte> zeroes = stackalloc byte[4096];
|
||||||
|
zeroes.Clear();
|
||||||
|
for (ulong offset = 0; offset < length;)
|
||||||
|
{
|
||||||
|
var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset);
|
||||||
|
if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += unchecked((uint)chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveRackLocked(ulong rackHandle)
|
||||||
|
{
|
||||||
|
Racks.Remove(rackHandle);
|
||||||
|
foreach (var voiceHandle in Voices
|
||||||
|
.Where(pair => pair.Value.RackHandle == rackHandle)
|
||||||
|
.Select(pair => pair.Key)
|
||||||
|
.ToArray())
|
||||||
|
{
|
||||||
|
Voices.Remove(voiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTrace() =>
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,16 @@ public static class PadExports
|
|||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "W2G-yoyMF5U",
|
||||||
|
ExportName = "scePadSetVibrationMode",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadSetVibrationMode(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||||
{
|
{
|
||||||
Span<byte> data = stackalloc byte[PadDataSize];
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
|
|||||||
@@ -244,6 +244,36 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int _nextTransactionResource;
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gjRZNnw0JPE",
|
||||||
|
ExportName = "sceSaveDataCreateTransactionResource",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var reserved = ctx[CpuRegister.Rsi];
|
||||||
|
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (resourceAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
|
if (!TryWriteUInt32(ctx, resourceAddress, id))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData(
|
||||||
|
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||||
<PackageReference Include="Silk.NET.Windowing" />
|
<PackageReference Include="Silk.NET.Windowing" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Silk.NET.Maths;
|
|||||||
using SharpEmu.Libs.Agc;
|
using SharpEmu.Libs.Agc;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using Silk.NET.Vulkan.Extensions.KHR;
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
|
using Silk.NET.Vulkan.Extensions.EXT;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
@@ -846,6 +847,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
|
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
|
||||||
private Instance _instance;
|
private Instance _instance;
|
||||||
private SurfaceKHR _surface;
|
private SurfaceKHR _surface;
|
||||||
|
private DebugUtilsMessengerEXT _debugMessenger;
|
||||||
|
private ExtDebugUtils? _debugUtils;
|
||||||
private PhysicalDevice _physicalDevice;
|
private PhysicalDevice _physicalDevice;
|
||||||
private Device _device;
|
private Device _device;
|
||||||
private Queue _queue;
|
private Queue _queue;
|
||||||
@@ -876,6 +879,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private bool _swapchainRecreateDeferred;
|
private bool _swapchainRecreateDeferred;
|
||||||
private bool _tracedPresentedSwapchain;
|
private bool _tracedPresentedSwapchain;
|
||||||
private bool _swapchainReadbackPending;
|
private bool _swapchainReadbackPending;
|
||||||
|
private bool _deviceLost;
|
||||||
|
private bool _deviceLostLogged;
|
||||||
private int _directPresentationCount;
|
private int _directPresentationCount;
|
||||||
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
||||||
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
|
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
|
||||||
@@ -1241,6 +1246,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private void CreateInstance()
|
private void CreateInstance()
|
||||||
{
|
{
|
||||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
|
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||||
|
byte* validationLayerName = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var applicationInfo = new ApplicationInfo
|
var applicationInfo = new ApplicationInfo
|
||||||
@@ -1268,12 +1276,29 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
||||||
|
{
|
||||||
|
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
||||||
|
}
|
||||||
|
else if (enableValidation)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
|
||||||
|
}
|
||||||
|
|
||||||
|
var layers = stackalloc byte*[1];
|
||||||
|
if (validationLayerName is not null)
|
||||||
|
{
|
||||||
|
layers[0] = validationLayerName;
|
||||||
|
}
|
||||||
|
|
||||||
var createInfo = new InstanceCreateInfo
|
var createInfo = new InstanceCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.InstanceCreateInfo,
|
SType = StructureType.InstanceCreateInfo,
|
||||||
PApplicationInfo = &applicationInfo,
|
PApplicationInfo = &applicationInfo,
|
||||||
EnabledExtensionCount = (uint)enabledExtensionCount,
|
EnabledExtensionCount = (uint)enabledExtensionCount,
|
||||||
PpEnabledExtensionNames = enabledExtensions,
|
PpEnabledExtensionNames = enabledExtensions,
|
||||||
|
EnabledLayerCount = validationLayerName is not null ? 1u : 0u,
|
||||||
|
PpEnabledLayerNames = validationLayerName is not null ? layers : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -1283,6 +1308,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
||||||
|
{
|
||||||
|
_debugUtils = debugUtils;
|
||||||
|
RegisterDebugMessenger(debugUtils);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Vulkan Validation Layers active (SHARPEMU_VK_VALIDATION=1).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1295,9 +1327,74 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
SilkMarshal.Free((nint)applicationName);
|
SilkMarshal.Free((nint)applicationName);
|
||||||
|
if (validationLayerName is not null)
|
||||||
|
{
|
||||||
|
SilkMarshal.Free((nint)validationLayerName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsInstanceLayerAvailable(string layerName)
|
||||||
|
{
|
||||||
|
uint layerCount = 0;
|
||||||
|
if (_vk.EnumerateInstanceLayerProperties(&layerCount, null) != Result.Success || layerCount == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = new LayerProperties[layerCount];
|
||||||
|
fixed (LayerProperties* propertyPointer = properties)
|
||||||
|
{
|
||||||
|
if (_vk.EnumerateInstanceLayerProperties(&layerCount, propertyPointer) != Result.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = Encoding.UTF8.GetBytes(layerName);
|
||||||
|
for (var index = 0; index < layerCount; index++)
|
||||||
|
{
|
||||||
|
if (Utf8NullTerminatedEquals(propertyPointer[index].LayerName, expected))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private void RegisterDebugMessenger(ExtDebugUtils debugUtils)
|
||||||
|
{
|
||||||
|
var messengerInfo = new DebugUtilsMessengerCreateInfoEXT
|
||||||
|
{
|
||||||
|
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
|
||||||
|
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt
|
||||||
|
| DebugUtilsMessageSeverityFlagsEXT.WarningBitExt,
|
||||||
|
MessageType = DebugUtilsMessageTypeFlagsEXT.ValidationBitExt
|
||||||
|
| DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt
|
||||||
|
| DebugUtilsMessageTypeFlagsEXT.GeneralBitExt,
|
||||||
|
PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(DebugCallback),
|
||||||
|
};
|
||||||
|
|
||||||
|
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
||||||
|
"vkCreateDebugUtilsMessengerEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe uint DebugCallback(
|
||||||
|
DebugUtilsMessageSeverityFlagsEXT severity,
|
||||||
|
DebugUtilsMessageTypeFlagsEXT type,
|
||||||
|
DebugUtilsMessengerCallbackDataEXT* callbackData,
|
||||||
|
void* userData)
|
||||||
|
{
|
||||||
|
var message = SilkMarshal.PtrToString((nint)callbackData->PMessage);
|
||||||
|
var prefix = severity switch
|
||||||
|
{
|
||||||
|
DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt => "[VULKAN][ERROR]",
|
||||||
|
DebugUtilsMessageSeverityFlagsEXT.WarningBitExt => "[VULKAN][WARN]",
|
||||||
|
_ => "[VULKAN][INFO]",
|
||||||
|
};
|
||||||
|
Console.Error.WriteLine($"{prefix} {message}");
|
||||||
|
return Vk.False;
|
||||||
|
}
|
||||||
private void CreateSurface()
|
private void CreateSurface()
|
||||||
{
|
{
|
||||||
var instanceHandle = new VkHandle(_instance.Handle);
|
var instanceHandle = new VkHandle(_instance.Handle);
|
||||||
@@ -1358,17 +1455,129 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
QueueCount = 1,
|
QueueCount = 1,
|
||||||
PQueuePriorities = &priority,
|
PQueuePriorities = &priority,
|
||||||
};
|
};
|
||||||
|
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
|
||||||
|
var enabledFeatures = new PhysicalDeviceFeatures
|
||||||
|
{
|
||||||
|
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
|
||||||
|
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
||||||
|
ShaderInt64 = supportedFeatures.ShaderInt64,
|
||||||
|
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
|
||||||
|
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
|
||||||
|
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
|
||||||
|
ShaderStorageImageWriteWithoutFormat = supportedFeatures.ShaderStorageImageWriteWithoutFormat,
|
||||||
|
RobustBufferAccess = supportedFeatures.RobustBufferAccess,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!supportedFeatures.RobustBufferAccess)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support robustBufferAccess " +
|
||||||
|
"translated shaders performing out-of-bounds buffer access may cause device loss.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedFeatures.ShaderInt64)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support shaderInt64 " +
|
||||||
|
"translated shaders using 64-bit integers will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support vertexPipelineStoresAndAtomics/fragmentStoresAndAtomics " +
|
||||||
|
"translated shaders using storage buffers in vertex/fragment stages may fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedFeatures.ShaderImageGatherExtended)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support shaderImageGatherExtended " +
|
||||||
|
"translated shaders using image gather with offsets/LOD/bias will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedFeatures.ShaderStorageImageReadWithoutFormat ||
|
||||||
|
!supportedFeatures.ShaderStorageImageWriteWithoutFormat)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support shaderStorageImage(Read|Write)WithoutFormat " +
|
||||||
|
"translated shaders using unformatted storage image load/store will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var maintenance8Features = new PhysicalDeviceMaintenance8FeaturesKHR
|
||||||
|
{
|
||||||
|
SType = StructureType.PhysicalDeviceMaintenance8FeaturesKhr,
|
||||||
|
};
|
||||||
|
var robustness2Features = new PhysicalDeviceRobustness2FeaturesEXT
|
||||||
|
{
|
||||||
|
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
|
||||||
|
PNext = &maintenance8Features,
|
||||||
|
};
|
||||||
|
var featuresQuery = new PhysicalDeviceFeatures2
|
||||||
|
{
|
||||||
|
SType = StructureType.PhysicalDeviceFeatures2,
|
||||||
|
PNext = &robustness2Features,
|
||||||
|
};
|
||||||
|
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
||||||
|
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
||||||
|
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
||||||
|
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
||||||
|
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
||||||
|
if (!supportsMaintenance8)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support VK_KHR_maintenance8 " +
|
||||||
|
"translated shaders using a dynamic texel offset on non-gather image samples will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportsRobustImageAccess2)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] GPU does not support VK_EXT_robustness2 robustImageAccess2 " +
|
||||||
|
"translated shaders performing out-of-bounds image access may cause device loss.");
|
||||||
|
}
|
||||||
|
|
||||||
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
||||||
|
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
|
||||||
|
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var extensions = stackalloc byte*[3];
|
||||||
|
var extensionCount = 0u;
|
||||||
|
extensions[extensionCount++] = swapchainExtension;
|
||||||
|
if (supportsMaintenance8)
|
||||||
|
{
|
||||||
|
extensions[extensionCount++] = maintenance8Extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsRobustness2)
|
||||||
|
{
|
||||||
|
extensions[extensionCount++] = robustness2Extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenance8Features.Maintenance8 = supportsMaintenance8;
|
||||||
|
maintenance8Features.PNext = null;
|
||||||
|
robustness2Features.RobustBufferAccess2 =
|
||||||
|
supportsRobustImageAccess2 && supportedFeatures.RobustBufferAccess;
|
||||||
|
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
|
||||||
|
robustness2Features.NullDescriptor = supportsNullDescriptor;
|
||||||
|
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
|
||||||
|
var features2 = new PhysicalDeviceFeatures2
|
||||||
|
{
|
||||||
|
SType = StructureType.PhysicalDeviceFeatures2,
|
||||||
|
PNext = supportsRobustness2
|
||||||
|
? &robustness2Features
|
||||||
|
: (supportsMaintenance8 ? &maintenance8Features : null),
|
||||||
|
Features = enabledFeatures,
|
||||||
|
};
|
||||||
var createInfo = new DeviceCreateInfo
|
var createInfo = new DeviceCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.DeviceCreateInfo,
|
SType = StructureType.DeviceCreateInfo,
|
||||||
|
PNext = &features2,
|
||||||
QueueCreateInfoCount = 1,
|
QueueCreateInfoCount = 1,
|
||||||
PQueueCreateInfos = &queueInfo,
|
PQueueCreateInfos = &queueInfo,
|
||||||
EnabledExtensionCount = 1,
|
EnabledExtensionCount = extensionCount,
|
||||||
PpEnabledExtensionNames = &swapchainExtension,
|
PpEnabledExtensionNames = extensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
|
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
|
||||||
@@ -1376,6 +1585,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
SilkMarshal.Free((nint)swapchainExtension);
|
SilkMarshal.Free((nint)swapchainExtension);
|
||||||
|
SilkMarshal.Free((nint)maintenance8Extension);
|
||||||
|
SilkMarshal.Free((nint)robustness2Extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||||
@@ -1540,6 +1751,39 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
resources.DebugName));
|
resources.DebugName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SubmitGuestCommandBufferAndWait(CommandBuffer commandBuffer)
|
||||||
|
{
|
||||||
|
var fenceInfo = new FenceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.FenceCreateInfo,
|
||||||
|
};
|
||||||
|
Fence fence;
|
||||||
|
Check(
|
||||||
|
_vk.CreateFence(_device, &fenceInfo, null, out fence),
|
||||||
|
"vkCreateFence(guest chunk)");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var submitInfo = new SubmitInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SubmitInfo,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
PCommandBuffers = &commandBuffer,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
|
||||||
|
"vkQueueSubmit(guest chunk)");
|
||||||
|
Check(
|
||||||
|
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
|
||||||
|
"vkWaitForFences(guest chunk)");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_vk.DestroyFence(_device, fence, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_vk.FreeCommandBuffers(_device, _commandPool, 1, &commandBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
private void EnsureGuestSubmissionCapacity()
|
private void EnsureGuestSubmissionCapacity()
|
||||||
{
|
{
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
@@ -2524,6 +2768,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var pipelineInfo = new ComputePipelineCreateInfo
|
var pipelineInfo = new ComputePipelineCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.ComputePipelineCreateInfo,
|
SType = StructureType.ComputePipelineCreateInfo,
|
||||||
|
Flags = PipelineCreateFlags.CreateDispatchBaseBit,
|
||||||
Stage = stage,
|
Stage = stage,
|
||||||
Layout = resources.PipelineLayout,
|
Layout = resources.PipelineLayout,
|
||||||
};
|
};
|
||||||
@@ -2573,7 +2818,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
dstSelect: texture.DstSelect,
|
dstSelect: texture.DstSelect,
|
||||||
out var view))
|
out var view))
|
||||||
{
|
{
|
||||||
if (_tracedTextureCacheHits.Add(
|
if (ShouldTraceVulkanResources() &&
|
||||||
|
_tracedTextureCacheHits.Add(
|
||||||
(texture.Address, texture.Width, texture.Height, vkFormat)))
|
(texture.Address, texture.Width, texture.Height, vkFormat)))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -2869,9 +3115,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
_vk.UnmapMemory(_device, stagingMemory);
|
_vk.UnmapMemory(_device, stagingMemory);
|
||||||
|
|
||||||
|
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
||||||
var imageInfo = new ImageCreateInfo
|
var imageInfo = new ImageCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageCreateInfo,
|
SType = StructureType.ImageCreateInfo,
|
||||||
|
Flags = supportsAttachmentUsage
|
||||||
|
? ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit
|
||||||
|
: 0,
|
||||||
ImageType = ImageType.Type2D,
|
ImageType = ImageType.Type2D,
|
||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
Extent = new Extent3D(width, height, 1),
|
Extent = new Extent3D(width, height, 1),
|
||||||
@@ -2879,7 +3129,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ArrayLayers = 1,
|
ArrayLayers = 1,
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
Tiling = ImageTiling.Optimal,
|
Tiling = ImageTiling.Optimal,
|
||||||
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
|
Usage = supportsAttachmentUsage
|
||||||
|
? ImageUsageFlags.TransferDstBit |
|
||||||
|
ImageUsageFlags.SampledBit |
|
||||||
|
ImageUsageFlags.ColorAttachmentBit |
|
||||||
|
ImageUsageFlags.StorageBit |
|
||||||
|
ImageUsageFlags.TransferSrcBit
|
||||||
|
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
|
||||||
SharingMode = SharingMode.Exclusive,
|
SharingMode = SharingMode.Exclusive,
|
||||||
InitialLayout = ImageLayout.Undefined,
|
InitialLayout = ImageLayout.Undefined,
|
||||||
};
|
};
|
||||||
@@ -3142,7 +3398,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
out var memory);
|
out var memory);
|
||||||
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
|
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
|
||||||
|
|
||||||
if (_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length)))
|
if (ShouldTraceVulkanResources() &&
|
||||||
|
_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length)))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
||||||
@@ -3714,6 +3971,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_ => Format.Undefined,
|
_ => Format.Undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static bool IsBlockCompressedFormat(Format format) =>
|
||||||
|
format is Format.BC1RgbaUnormBlock or
|
||||||
|
Format.BC1RgbaSrgbBlock or
|
||||||
|
Format.BC7UnormBlock or
|
||||||
|
Format.BC7SrgbBlock;
|
||||||
|
|
||||||
private VkBuffer CreateBuffer(
|
private VkBuffer CreateBuffer(
|
||||||
ulong size,
|
ulong size,
|
||||||
BufferUsageFlags usage,
|
BufferUsageFlags usage,
|
||||||
@@ -3767,8 +4030,24 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
|
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const uint MaxComputeZSlicesPerSubmission = 8;
|
||||||
|
|
||||||
private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work)
|
private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work)
|
||||||
{
|
{
|
||||||
|
if (_deviceLost)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress))
|
||||||
|
{
|
||||||
|
TraceVulkanShader(
|
||||||
|
$"vk.compute_skip cs=0x{work.ShaderAddress:X16} " +
|
||||||
|
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
|
$"textures={work.Textures.Count}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TranslatedDrawResources? resources = null;
|
TranslatedDrawResources? resources = null;
|
||||||
CommandBuffer commandBuffer = default;
|
CommandBuffer commandBuffer = default;
|
||||||
var submitted = false;
|
var submitted = false;
|
||||||
@@ -3776,59 +4055,100 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
EnsureGuestSubmissionCapacity();
|
EnsureGuestSubmissionCapacity();
|
||||||
resources = CreateComputeDispatchResources(work);
|
resources = CreateComputeDispatchResources(work);
|
||||||
commandBuffer = AllocateGuestCommandBuffer();
|
|
||||||
_commandBuffer = commandBuffer;
|
|
||||||
var beginInfo = new CommandBufferBeginInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.CommandBufferBeginInfo,
|
|
||||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
|
||||||
};
|
|
||||||
Check(
|
|
||||||
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
|
|
||||||
"vkBeginCommandBuffer(compute)");
|
|
||||||
|
|
||||||
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
var batchCount = Math.Max(
|
||||||
RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit);
|
1u,
|
||||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit);
|
(uint)Math.Ceiling(work.GroupCountZ / (double)MaxComputeZSlicesPerSubmission));
|
||||||
_vk.CmdBindPipeline(
|
|
||||||
_commandBuffer,
|
for (var batchIndex = 0u; batchIndex < batchCount; batchIndex++)
|
||||||
PipelineBindPoint.Compute,
|
|
||||||
resources.Pipeline);
|
|
||||||
if (resources.DescriptorSet.Handle != 0)
|
|
||||||
{
|
{
|
||||||
var descriptorSet = resources.DescriptorSet;
|
var zStart = batchIndex * MaxComputeZSlicesPerSubmission;
|
||||||
_vk.CmdBindDescriptorSets(
|
var zCount = Math.Min(MaxComputeZSlicesPerSubmission, work.GroupCountZ - zStart);
|
||||||
|
var isFirstBatch = batchIndex == 0;
|
||||||
|
var isLastBatch = batchIndex == batchCount - 1;
|
||||||
|
|
||||||
|
commandBuffer = AllocateGuestCommandBuffer();
|
||||||
|
_commandBuffer = commandBuffer;
|
||||||
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferBeginInfo,
|
||||||
|
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
|
||||||
|
"vkBeginCommandBuffer(compute)");
|
||||||
|
|
||||||
|
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
||||||
|
if (isFirstBatch)
|
||||||
|
{
|
||||||
|
RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
|
RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
_vk.CmdBindPipeline(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineBindPoint.Compute,
|
PipelineBindPoint.Compute,
|
||||||
resources.PipelineLayout,
|
resources.Pipeline);
|
||||||
0,
|
if (resources.DescriptorSet.Handle != 0)
|
||||||
1,
|
{
|
||||||
&descriptorSet,
|
var descriptorSet = resources.DescriptorSet;
|
||||||
0,
|
_vk.CmdBindDescriptorSets(
|
||||||
null);
|
_commandBuffer,
|
||||||
|
PipelineBindPoint.Compute,
|
||||||
|
resources.PipelineLayout,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
&descriptorSet,
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordChunkedComputeDispatch(_commandBuffer, work, zStart, zCount);
|
||||||
|
|
||||||
|
if (isLastBatch)
|
||||||
|
{
|
||||||
|
RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
EndDebugLabel(_commandBuffer);
|
||||||
|
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)");
|
||||||
|
|
||||||
|
TraceVulkanShader(
|
||||||
|
$"vk.compute_submit cs=0x{work.ShaderAddress:X16} " +
|
||||||
|
$"batch={batchIndex}/{batchCount} z={zStart}..{zStart + zCount}");
|
||||||
|
if (isLastBatch)
|
||||||
|
{
|
||||||
|
SubmitGuestCommandBuffer(
|
||||||
|
commandBuffer,
|
||||||
|
resources,
|
||||||
|
GetTraceImages(resources));
|
||||||
|
submitted = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SubmitGuestCommandBufferAndWait(commandBuffer);
|
||||||
|
commandBuffer = default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordChunkedComputeDispatch(_commandBuffer, work);
|
|
||||||
RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit);
|
|
||||||
EndDebugLabel(_commandBuffer);
|
|
||||||
|
|
||||||
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)");
|
|
||||||
SubmitGuestCommandBuffer(
|
|
||||||
commandBuffer,
|
|
||||||
resources,
|
|
||||||
GetTraceImages(resources));
|
|
||||||
submitted = true;
|
|
||||||
MarkSampledImagesInitialized(resources);
|
MarkSampledImagesInitialized(resources);
|
||||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
||||||
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
$"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16}");
|
$"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16} " +
|
||||||
|
$"batches={batchCount}");
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
if (TryMarkDeviceLost(exception))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][ERROR] Vulkan compute dispatch failed: {exception.Message}");
|
$"[LOADER][ERROR] Vulkan compute dispatch failed " +
|
||||||
|
$"cs=0x{work.ShaderAddress:X16}: {exception.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -3851,7 +4171,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void RecordChunkedComputeDispatch(
|
private void RecordChunkedComputeDispatch(
|
||||||
CommandBuffer commandBuffer,
|
CommandBuffer commandBuffer,
|
||||||
VulkanComputeGuestDispatch work)
|
VulkanComputeGuestDispatch work,
|
||||||
|
uint zStart,
|
||||||
|
uint zCount)
|
||||||
{
|
{
|
||||||
const uint maxWorkgroupsPerCommand = 4096;
|
const uint maxWorkgroupsPerCommand = 4096;
|
||||||
var yChunk = Math.Max(
|
var yChunk = Math.Max(
|
||||||
@@ -3861,7 +4183,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u)));
|
maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u)));
|
||||||
var commandCount = 0u;
|
var commandCount = 0u;
|
||||||
|
|
||||||
for (var z = 0u; z < work.GroupCountZ; z++)
|
for (var z = zStart; z < zStart + zCount; z++)
|
||||||
{
|
{
|
||||||
for (var y = 0u; y < work.GroupCountY; y += yChunk)
|
for (var y = 0u; y < work.GroupCountY; y += yChunk)
|
||||||
{
|
{
|
||||||
@@ -3883,12 +4205,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " +
|
$"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " +
|
||||||
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
$"commands={commandCount} y_chunk={yChunk}");
|
$"z_range={zStart}..{zStart + zCount} commands={commandCount} y_chunk={yChunk}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
||||||
{
|
{
|
||||||
|
if (_deviceLost)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
|
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
|
||||||
if (format == Format.Undefined)
|
if (format == Format.Undefined)
|
||||||
{
|
{
|
||||||
@@ -4046,6 +4373,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
if (TryMarkDeviceLost(exception))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
|
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
|
||||||
!failedTarget.Initialized)
|
!failedTarget.Initialized)
|
||||||
{
|
{
|
||||||
@@ -4091,6 +4423,23 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
existing.Format == format)
|
existing.Format == format)
|
||||||
{
|
{
|
||||||
|
if (existing.RenderPass.Handle == 0)
|
||||||
|
{
|
||||||
|
var attachmentView = existing.MipViews.Length > 0
|
||||||
|
? existing.MipViews[0]
|
||||||
|
: existing.View;
|
||||||
|
var (promotedRenderPass, promotedFramebuffer) = CreateRenderPassAndFramebuffer(
|
||||||
|
existing.Format,
|
||||||
|
attachmentView,
|
||||||
|
existing.Width,
|
||||||
|
existing.Height);
|
||||||
|
existing.RenderPass = promotedRenderPass;
|
||||||
|
existing.Framebuffer = promotedFramebuffer;
|
||||||
|
var promotedName = GuestImageDebugName(target, format);
|
||||||
|
SetDebugName(ObjectType.RenderPass, promotedRenderPass.Handle, $"{promotedName} renderpass");
|
||||||
|
SetDebugName(ObjectType.Framebuffer, promotedFramebuffer.Handle, $"{promotedName} framebuffer");
|
||||||
|
}
|
||||||
|
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4171,6 +4520,48 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
mipViews[mipLevel] = mipView;
|
mipViews[mipLevel] = mipView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
|
||||||
|
format,
|
||||||
|
mipViews[0],
|
||||||
|
target.Width,
|
||||||
|
target.Height);
|
||||||
|
|
||||||
|
var resource = new GuestImageResource
|
||||||
|
{
|
||||||
|
Address = target.Address,
|
||||||
|
Width = target.Width,
|
||||||
|
Height = target.Height,
|
||||||
|
MipLevels = mipLevels,
|
||||||
|
Format = format,
|
||||||
|
Image = image,
|
||||||
|
Memory = memory,
|
||||||
|
View = view,
|
||||||
|
MipViews = mipViews,
|
||||||
|
RenderPass = renderPass,
|
||||||
|
Framebuffer = framebuffer,
|
||||||
|
};
|
||||||
|
var debugName = GuestImageDebugName(target, format);
|
||||||
|
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||||
|
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||||
|
for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++)
|
||||||
|
{
|
||||||
|
SetDebugName(
|
||||||
|
ObjectType.ImageView,
|
||||||
|
mipViews[mipLevel].Handle,
|
||||||
|
$"{debugName} mip{mipLevel}");
|
||||||
|
}
|
||||||
|
SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass");
|
||||||
|
SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer");
|
||||||
|
_guestImages.Add(target.Address, resource);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
|
||||||
|
Format format,
|
||||||
|
ImageView attachmentView,
|
||||||
|
uint width,
|
||||||
|
uint height)
|
||||||
|
{
|
||||||
var colorAttachment = new AttachmentDescription
|
var colorAttachment = new AttachmentDescription
|
||||||
{
|
{
|
||||||
Format = format,
|
Format = format,
|
||||||
@@ -4205,49 +4596,22 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
||||||
"vkCreateRenderPass(offscreen)");
|
"vkCreateRenderPass(offscreen)");
|
||||||
|
|
||||||
var attachment = mipViews[0];
|
var attachment = attachmentView;
|
||||||
var framebufferInfo = new FramebufferCreateInfo
|
var framebufferInfo = new FramebufferCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.FramebufferCreateInfo,
|
SType = StructureType.FramebufferCreateInfo,
|
||||||
RenderPass = renderPass,
|
RenderPass = renderPass,
|
||||||
AttachmentCount = 1,
|
AttachmentCount = 1,
|
||||||
PAttachments = &attachment,
|
PAttachments = &attachment,
|
||||||
Width = target.Width,
|
Width = width,
|
||||||
Height = target.Height,
|
Height = height,
|
||||||
Layers = 1,
|
Layers = 1,
|
||||||
};
|
};
|
||||||
Check(
|
Check(
|
||||||
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer),
|
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer),
|
||||||
"vkCreateFramebuffer(offscreen)");
|
"vkCreateFramebuffer(offscreen)");
|
||||||
|
|
||||||
var resource = new GuestImageResource
|
return (renderPass, framebuffer);
|
||||||
{
|
|
||||||
Address = target.Address,
|
|
||||||
Width = target.Width,
|
|
||||||
Height = target.Height,
|
|
||||||
MipLevels = mipLevels,
|
|
||||||
Format = format,
|
|
||||||
Image = image,
|
|
||||||
Memory = memory,
|
|
||||||
View = view,
|
|
||||||
MipViews = mipViews,
|
|
||||||
RenderPass = renderPass,
|
|
||||||
Framebuffer = framebuffer,
|
|
||||||
};
|
|
||||||
var debugName = GuestImageDebugName(target, format);
|
|
||||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
|
||||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
|
||||||
for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++)
|
|
||||||
{
|
|
||||||
SetDebugName(
|
|
||||||
ObjectType.ImageView,
|
|
||||||
mipViews[mipLevel].Handle,
|
|
||||||
$"{debugName} mip{mipLevel}");
|
|
||||||
}
|
|
||||||
SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass");
|
|
||||||
SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer");
|
|
||||||
_guestImages.Add(target.Address, resource);
|
|
||||||
return resource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels)
|
private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels)
|
||||||
@@ -4469,7 +4833,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
if (!_deviceLost)
|
||||||
|
{
|
||||||
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
|
}
|
||||||
|
|
||||||
var completedWork = 0;
|
var completedWork = 0;
|
||||||
while (completedWork < MaxGuestWorkPerRender &&
|
while (completedWork < MaxGuestWorkPerRender &&
|
||||||
@@ -5327,6 +5694,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceVulkanResources() =>
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
private void RecordTranslatedGraphicsPass(
|
private void RecordTranslatedGraphicsPass(
|
||||||
TranslatedDrawResources resources,
|
TranslatedDrawResources resources,
|
||||||
RenderPass renderPass,
|
RenderPass renderPass,
|
||||||
@@ -5956,6 +6329,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_debugUtils is not null && _debugMessenger.Handle != 0)
|
||||||
|
{
|
||||||
|
_debugUtils.DestroyDebugUtilsMessenger(_instance, _debugMessenger, null);
|
||||||
|
}
|
||||||
_vulkanReady = false;
|
_vulkanReady = false;
|
||||||
_vk.DeviceWaitIdle(_device);
|
_vk.DeviceWaitIdle(_device);
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
@@ -6158,6 +6535,25 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryMarkDeviceLost(Exception exception)
|
||||||
|
{
|
||||||
|
if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deviceLost = true;
|
||||||
|
if (!_deviceLostLogged)
|
||||||
|
{
|
||||||
|
_deviceLostLogged = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceVulkanShader(string message)
|
private static void TraceVulkanShader(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"Silk.NET.Core": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0",
|
||||||
|
"Silk.NET.Vulkan": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
Reference in New Issue
Block a user