mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1bba8d99d | |||
| fe1a592496 | |||
| 9d4dad2c24 | |||
| 38d94d8e54 | |||
| 61a1abdb13 | |||
| 88452218cb | |||
| 427a511e48 | |||
| e103795768 | |||
| 24c6e44800 | |||
| 3a30d6419f | |||
| 2d8a795f23 | |||
| adb2acdb39 | |||
| 5e5bead02c | |||
| e98f02966a | |||
| 9ab1e2f21a | |||
| 340eb9e0ab | |||
| 03e3d25e85 | |||
| 2bc466b10e | |||
| 45d0be474b | |||
| 5bb91eff9b | |||
| f4df0ed4bd | |||
| 23691a2bdc | |||
| 5c5ed8c064 | |||
| a4748ce266 | |||
| 5617646c8a |
+1
-1
@@ -14,7 +14,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
[*.{md,json,yml,props,csproj}]
|
||||
[*.{md,json,yml,xml,props,csproj}]
|
||||
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
@@ -126,7 +126,7 @@ jobs:
|
||||
needs:
|
||||
- init
|
||||
- build
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -26,6 +26,7 @@ arm64/
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.packages
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
@@ -10,7 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Iced" Version="1.21.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.Windowing" Version="2.23.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
+3
-1
@@ -3,12 +3,14 @@ version = 1
|
||||
[[annotations]]
|
||||
path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<configuration>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value=".packages" />
|
||||
</config>
|
||||
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
|
||||
<packageSourceMapping>
|
||||
<packageSource key="Nuget">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -16,7 +16,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> -->
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
@@ -30,6 +30,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.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.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -103,6 +104,16 @@
|
||||
"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": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
@@ -338,10 +338,15 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
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();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||
{
|
||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||
}
|
||||
else if (importStubEntry.Export is { } cachedExport &&
|
||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||
{
|
||||
@@ -628,25 +633,56 @@ public sealed partial class DirectExecutionBackend
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||
"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)
|
||||
{
|
||||
var resultValue = unchecked((int)result);
|
||||
if (resultValue > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedFileProbeMiss =
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||
IsExpectedFileProbeNotFoundNid(nid);
|
||||
var expectedTimedWaitTimeout =
|
||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||
unchecked((int)result) == 60;
|
||||
var expectedEqueueTimeout =
|
||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
var expectedMutexTrylockBusy =
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
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;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + (int)result;
|
||||
if (!ShouldLogExpectedImportResults())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + resultValue;
|
||||
int count;
|
||||
lock (_importResultLogSampleGate)
|
||||
{
|
||||
@@ -658,6 +694,12 @@ public sealed partial class DirectExecutionBackend
|
||||
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) =>
|
||||
nid is
|
||||
"eV9wAD2riIA" or // sceKernelStat
|
||||
@@ -1180,8 +1222,11 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
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;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1194,6 +1239,62 @@ public sealed partial class DirectExecutionBackend
|
||||
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()
|
||||
{
|
||||
var cpuContext = ActiveCpuContext;
|
||||
|
||||
@@ -11,7 +11,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly object _guestAllocationGate = new();
|
||||
private readonly object _allocationSearchHintGate = 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 bool _disposed;
|
||||
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 GuestAllocationArenaStartOffset = PageSize;
|
||||
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 uint MEM_COMMIT = 0x1000;
|
||||
@@ -35,6 +38,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
@@ -81,7 +85,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -96,7 +100,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -110,7 +114,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var reservedOnly = false;
|
||||
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||
var preferReserveOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
void* result = null;
|
||||
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)");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (result == null)
|
||||
@@ -193,12 +199,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
lazyPrimeState = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes: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
|
||||
{
|
||||
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
|
||||
@@ -210,7 +216,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -227,7 +233,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
: (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;
|
||||
}
|
||||
@@ -247,7 +253,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var alignedSize = AlignUp(size, PageSize);
|
||||
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++)
|
||||
{
|
||||
@@ -267,6 +274,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||
if (actualAddress == cursor)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -334,6 +342,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -390,7 +402,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
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
|
||||
{
|
||||
@@ -473,30 +485,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
@@ -533,30 +549,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
@@ -589,13 +609,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
@@ -637,13 +658,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
@@ -699,15 +721,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (virtualAddress >= region.VirtualAddress &&
|
||||
virtualAddress < region.VirtualAddress + region.Size)
|
||||
{
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -720,14 +736,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return FindRegion(virtualAddress, size) is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -737,14 +746,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var low = 0;
|
||||
var high = _regions.Count - 1;
|
||||
MemoryRegion? candidate = null;
|
||||
while (low <= high)
|
||||
{
|
||||
if (TryResolveRegionOffset(address, size, region, out _))
|
||||
var middle = low + ((high - low) >> 1);
|
||||
var region = _regions[middle];
|
||||
if (region.VirtualAddress <= address)
|
||||
{
|
||||
return region;
|
||||
candidate = region;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return candidate is not null &&
|
||||
TryResolveRegionOffset(address, size, candidate, out _)
|
||||
? candidate
|
||||
: null;
|
||||
}
|
||||
|
||||
private void InsertRegionSorted(MemoryRegion region)
|
||||
{
|
||||
var low = 0;
|
||||
var high = _regions.Count;
|
||||
while (low < high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (_regions[middle].VirtualAddress < region.VirtualAddress)
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
|
||||
_regions.Insert(low, region);
|
||||
}
|
||||
|
||||
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
|
||||
@@ -762,6 +805,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
var regionEnd = region.VirtualAddress + region.Size;
|
||||
if (region.VirtualAddress >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < regionEnd && region.VirtualAddress < end)
|
||||
{
|
||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||
@@ -776,6 +829,37 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
{
|
||||
offset = 0;
|
||||
@@ -944,6 +1028,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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()
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -543,6 +543,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
Path.Combine(ebootDirectory, "sce_module"),
|
||||
Path.Combine(ebootDirectory, "sce_modules"),
|
||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||
}
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(Directory.Exists)
|
||||
@@ -554,7 +555,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
}
|
||||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(Directory.EnumerateFiles)
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(path =>
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
@@ -562,7 +565,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var modulePaths = allModulePaths
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.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.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -88,6 +89,16 @@
|
||||
"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": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
@@ -155,6 +155,16 @@ public static class AgcExports
|
||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||
byte[]> _computeSpirvCache = new();
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceAgcShader =
|
||||
_traceAgc ||
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _dcbWriteDataTraceCount;
|
||||
private static long _dcbWaitRegMemTraceCount;
|
||||
private static long _createShaderTraceCount;
|
||||
@@ -1194,6 +1204,51 @@ public static class AgcExports
|
||||
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(
|
||||
Nid = "Yw0jKSqop+E",
|
||||
ExportName = "sceAgcDcbDrawIndexAuto",
|
||||
@@ -1996,6 +2051,88 @@ public static class AgcExports
|
||||
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(
|
||||
Nid = "h9z6+0hEydk",
|
||||
ExportName = "sceAgcSuspendPoint",
|
||||
@@ -3472,7 +3609,7 @@ public static class AgcExports
|
||||
',',
|
||||
draw.GlobalMemoryBindings.Select((binding, index) =>
|
||||
$"{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
|
||||
? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" +
|
||||
Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Data.Length, 32)))
|
||||
@@ -3493,6 +3630,29 @@ public static class AgcExports
|
||||
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" +
|
||||
$"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}"
|
||||
: "full";
|
||||
var rasterRegisters = new (string Name, uint Offset)[]
|
||||
{
|
||||
("screen_tl", PaScScreenScissorTl),
|
||||
("screen_br", PaScScreenScissorBr),
|
||||
("window_off", PaScWindowOffset),
|
||||
("window_tl", PaScWindowScissorTl),
|
||||
("window_br", PaScWindowScissorBr),
|
||||
("generic_tl", PaScGenericScissorTl),
|
||||
("generic_br", PaScGenericScissorBr),
|
||||
("vport_tl", PaScVportScissor0Tl),
|
||||
("vport_br", PaScVportScissor0Br),
|
||||
("mode", PaScModeCntl0),
|
||||
("xscale", PaClVportXScale),
|
||||
("xoffset", PaClVportXOffset),
|
||||
("yscale", PaClVportYScale),
|
||||
("yoffset", PaClVportYOffset),
|
||||
};
|
||||
var raster = string.Join(
|
||||
',',
|
||||
rasterRegisters.Select(entry =>
|
||||
state.CxRegisters.TryGetValue(entry.Offset, out var value)
|
||||
? $"{entry.Name}=0x{value:X8}"
|
||||
: $"{entry.Name}=missing"));
|
||||
var blend = draw.RenderState.Blend;
|
||||
TraceAgcShader(
|
||||
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
|
||||
@@ -3500,6 +3660,7 @@ public static class AgcExports
|
||||
$"primitive=0x{draw.PrimitiveType:X} " +
|
||||
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
|
||||
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
|
||||
$"raster=[{raster}] " +
|
||||
$"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " +
|
||||
$"targets=[{targets}] textures=[{textures}] " +
|
||||
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
|
||||
@@ -3583,7 +3744,7 @@ public static class AgcExports
|
||||
descriptor.Width > 8192 ||
|
||||
descriptor.Height > 8192)
|
||||
{
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3601,7 +3762,32 @@ public static class AgcExports
|
||||
sourceByteCount > MaxPresentedTextureBytes ||
|
||||
sourceByteCount > int.MaxValue)
|
||||
{
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
VulkanVideoPresenter.IsGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
{
|
||||
texture = new VulkanGuestDrawTexture(
|
||||
descriptor.Address,
|
||||
descriptor.Width,
|
||||
descriptor.Height,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType,
|
||||
[],
|
||||
IsFallback: false,
|
||||
IsStorage: false,
|
||||
MipLevels: descriptor.MipLevels,
|
||||
MipLevel: mipLevel,
|
||||
Pitch: sourceWidth,
|
||||
TileMode: descriptor.TileMode,
|
||||
DstSelect: descriptor.DstSelect,
|
||||
Sampler: ToVulkanSampler(samplerDescriptor));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3639,7 +3825,7 @@ public static class AgcExports
|
||||
var source = new byte[(int)sourceByteCount];
|
||||
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
||||
{
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage);
|
||||
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3682,18 +3868,25 @@ public static class AgcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(bool isStorage = false) =>
|
||||
new(
|
||||
private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
|
||||
bool isStorage,
|
||||
uint format,
|
||||
uint numberType)
|
||||
{
|
||||
var fallbackFormat = format == 0 ? 10u : format;
|
||||
var fallbackNumberType = numberType;
|
||||
return new(
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
56,
|
||||
NumberType: 0,
|
||||
fallbackFormat,
|
||||
fallbackNumberType,
|
||||
[0, 0, 0, 255],
|
||||
IsFallback: true,
|
||||
IsStorage: isStorage,
|
||||
MipLevels: 1,
|
||||
MipLevel: 0);
|
||||
}
|
||||
|
||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||
descriptor.Count >= 4
|
||||
@@ -5381,7 +5574,7 @@ public static class AgcExports
|
||||
|
||||
private static void TraceAgc(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
|
||||
if (!_traceAgc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -5391,8 +5584,7 @@ public static class AgcExports
|
||||
|
||||
private static void TraceAgcShader(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
||||
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal))
|
||||
if (!_traceAgcShader)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,7 +812,11 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left),
|
||||
_ => registers[destination.Value] | (1u << ((int)left & 31)),
|
||||
};
|
||||
scalarConditionCode = registers[destination.Value] != 0;
|
||||
if (instruction.Opcode != "SBitset1B32")
|
||||
{
|
||||
scalarConditionCode = registers[destination.Value] != 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -838,13 +842,15 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
case "SSubU32":
|
||||
result = left - right;
|
||||
scalarConditionCode = left >= right;
|
||||
scalarConditionCode = right > left;
|
||||
break;
|
||||
case "SAddI32":
|
||||
result = unchecked((uint)((int)left + (int)right));
|
||||
scalarConditionCode = SignedAddOverflow(left, right, result);
|
||||
break;
|
||||
case "SSubI32":
|
||||
result = unchecked((uint)((int)left - (int)right));
|
||||
scalarConditionCode = SignedSubOverflow(left, right, result);
|
||||
break;
|
||||
case "SAddcU32":
|
||||
{
|
||||
@@ -855,23 +861,27 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
case "SSubbU32":
|
||||
{
|
||||
var borrow = scalarConditionCode ? 0UL : 1UL;
|
||||
var borrow = scalarConditionCode ? 1UL : 0UL;
|
||||
var subtrahend = (ulong)right + borrow;
|
||||
result = unchecked(left - (uint)subtrahend);
|
||||
scalarConditionCode = left >= subtrahend;
|
||||
scalarConditionCode = subtrahend > left;
|
||||
break;
|
||||
}
|
||||
case "SMinI32":
|
||||
result = unchecked((uint)Math.Min((int)left, (int)right));
|
||||
scalarConditionCode = (int)left < (int)right;
|
||||
break;
|
||||
case "SMinU32":
|
||||
result = Math.Min(left, right);
|
||||
scalarConditionCode = left < right;
|
||||
break;
|
||||
case "SMaxI32":
|
||||
result = unchecked((uint)Math.Max((int)left, (int)right));
|
||||
scalarConditionCode = (int)left > (int)right;
|
||||
break;
|
||||
case "SMaxU32":
|
||||
result = Math.Max(left, right);
|
||||
scalarConditionCode = left > right;
|
||||
break;
|
||||
case "SCselectB32":
|
||||
result = scalarConditionCode ? left : right;
|
||||
@@ -935,6 +945,7 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
var offset = (int)right & 31;
|
||||
var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset);
|
||||
result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
}
|
||||
case "SBfeI32":
|
||||
@@ -944,23 +955,41 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
result = width == 0
|
||||
? 0
|
||||
: unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width)));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
}
|
||||
case "SAbsdiffI32":
|
||||
result = unchecked((uint)Math.Abs((long)(int)left - (int)right));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
case "SLshl1AddU32":
|
||||
result = (left << 1) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 1) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl2AddU32":
|
||||
result = (left << 2) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 2) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl3AddU32":
|
||||
result = (left << 3) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 3) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl4AddU32":
|
||||
result = (left << 4) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 4) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SPackLlB32B16":
|
||||
result = (left & 0xFFFFu) | (right << 16);
|
||||
break;
|
||||
@@ -1002,7 +1031,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SNandSaveexecB64" or
|
||||
"SNorSaveexecB64" or
|
||||
"SXnorSaveexecB64" or
|
||||
"SAndn1SaveexecB64"))
|
||||
"SAndn1SaveexecB64" or
|
||||
"SOrn1SaveexecB64"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1030,11 +1060,12 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SAndSaveexecB64" => oldExec & source,
|
||||
"SOrSaveexecB64" => oldExec | source,
|
||||
"SXorSaveexecB64" => oldExec ^ source,
|
||||
"SAndn1SaveexecB64" => ~oldExec & source,
|
||||
"SAndn2SaveexecB64" => oldExec & ~source,
|
||||
"SOrn2SaveexecB64" => oldExec | ~source,
|
||||
"SNandSaveexecB64" => ~(oldExec & source),
|
||||
"SNorSaveexecB64" => ~(oldExec | source),
|
||||
"SAndn1SaveexecB64" => ~source & oldExec,
|
||||
"SAndn2SaveexecB64" => source & ~oldExec,
|
||||
"SOrn1SaveexecB64" => ~source | oldExec,
|
||||
"SOrn2SaveexecB64" => source | ~oldExec,
|
||||
"SNandSaveexecB64" => ~(source & oldExec),
|
||||
"SNorSaveexecB64" => ~(source | oldExec),
|
||||
_ => ~(oldExec ^ source),
|
||||
};
|
||||
|
||||
@@ -1104,6 +1135,12 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
|
||||
private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask;
|
||||
|
||||
private static bool SignedAddOverflow(uint left, uint right, uint result) =>
|
||||
((left ^ result) & (right ^ result) & 0x80000000u) != 0;
|
||||
|
||||
private static bool SignedSubOverflow(uint left, uint right, uint result) =>
|
||||
((left ^ right) & (left ^ result) & 0x80000000u) != 0;
|
||||
|
||||
private static bool TryExecuteScalarCompare(
|
||||
Gen5ShaderInstruction instruction,
|
||||
uint[] registers,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
@@ -13,6 +14,14 @@ internal static class Gen5ShaderTranslator
|
||||
private const int MaxInstructions = 4096;
|
||||
private const int MinimumUserDataDwords = 16;
|
||||
private const int MaximumUserDataDwords = 256;
|
||||
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
|
||||
|
||||
private sealed class ShaderDecodeCache
|
||||
{
|
||||
public object Gate { get; } = new();
|
||||
public Dictionary<ulong, Gen5ShaderProgram> Programs { get; } = new();
|
||||
public Dictionary<ulong, Gen5ShaderMetadata?> Metadata { get; } = new();
|
||||
}
|
||||
|
||||
private static readonly uint[] FullscreenBarycentricEs =
|
||||
[
|
||||
@@ -117,16 +126,51 @@ internal static class Gen5ShaderTranslator
|
||||
uint userDataScalarRegisterBase = 0)
|
||||
{
|
||||
state = default!;
|
||||
if (!TryDecodeProgram(ctx, shaderAddress, out var program, out error))
|
||||
error = string.Empty;
|
||||
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
|
||||
Gen5ShaderProgram? program;
|
||||
lock (cache.Gate)
|
||||
{
|
||||
return false;
|
||||
cache.Programs.TryGetValue(shaderAddress, out program);
|
||||
}
|
||||
|
||||
if (program is null)
|
||||
{
|
||||
if (!TryDecodeProgram(ctx, shaderAddress, out program, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (cache.Gate)
|
||||
{
|
||||
cache.Programs.TryAdd(shaderAddress, program);
|
||||
}
|
||||
}
|
||||
|
||||
Gen5ShaderMetadata? metadata = null;
|
||||
if (shaderHeaderAddress != 0 &&
|
||||
Gen5ShaderMetadataReader.TryRead(ctx, shaderHeaderAddress, out var decodedMetadata))
|
||||
if (shaderHeaderAddress != 0)
|
||||
{
|
||||
metadata = decodedMetadata;
|
||||
var metadataCached = false;
|
||||
lock (cache.Gate)
|
||||
{
|
||||
metadataCached = cache.Metadata.TryGetValue(shaderHeaderAddress, out metadata);
|
||||
}
|
||||
|
||||
if (!metadataCached)
|
||||
{
|
||||
if (Gen5ShaderMetadataReader.TryRead(
|
||||
ctx,
|
||||
shaderHeaderAddress,
|
||||
out var decodedMetadata))
|
||||
{
|
||||
metadata = decodedMetadata;
|
||||
}
|
||||
|
||||
lock (cache.Gate)
|
||||
{
|
||||
cache.Metadata.TryAdd(shaderHeaderAddress, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var userData = new uint[GetUserDataDwordCount(metadata)];
|
||||
@@ -452,6 +496,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x2A => "SNorSaveexecB64",
|
||||
0x2B => "SXnorSaveexecB64",
|
||||
0x37 => "SAndn1SaveexecB64",
|
||||
0x38 => "SOrn1SaveexecB64",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
||||
@@ -649,6 +649,20 @@ internal static partial class Gen5SpirvTranslator
|
||||
width);
|
||||
break;
|
||||
}
|
||||
case "VBfiB32":
|
||||
{
|
||||
var mask = GetRawSource(instruction, 0);
|
||||
var insert = GetRawSource(instruction, 1);
|
||||
var source = GetRawSource(instruction, 2);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
BitwiseAnd(mask, insert),
|
||||
BitwiseAnd(
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, mask),
|
||||
source));
|
||||
break;
|
||||
}
|
||||
case "VCvtPkrtzF16F32":
|
||||
{
|
||||
var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0));
|
||||
@@ -961,6 +975,16 @@ internal static partial class Gen5SpirvTranslator
|
||||
StoreS(destination, result);
|
||||
Store(_scc, IsNotZero(result));
|
||||
return true;
|
||||
case "SBitset1B32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.BitFieldInsert,
|
||||
_uintType,
|
||||
LoadS(destination),
|
||||
UInt(1),
|
||||
BitwiseAnd(left, UInt(31)),
|
||||
UInt(1));
|
||||
StoreS(destination, result);
|
||||
return true;
|
||||
default:
|
||||
{
|
||||
if (instruction.Sources.Count < 2)
|
||||
@@ -987,13 +1011,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
right);
|
||||
Store(_scc, _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
right,
|
||||
left));
|
||||
break;
|
||||
case "SAddI32":
|
||||
result = IAdd(left, right);
|
||||
Store(_scc, SignedAddOverflow(left, right, result));
|
||||
break;
|
||||
case "SSubI32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1001,6 +1026,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, SignedSubOverflow(left, right, result));
|
||||
break;
|
||||
case "SAddcU32":
|
||||
{
|
||||
@@ -1037,8 +1063,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
Load(_boolType, _scc),
|
||||
UInt(0),
|
||||
UInt(1));
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var partial = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
@@ -1049,23 +1075,31 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
partial,
|
||||
borrow);
|
||||
var firstNoBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
var firstBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right);
|
||||
var secondNoBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
right,
|
||||
left);
|
||||
var secondBorrow = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
partial,
|
||||
borrow);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
borrow,
|
||||
UInt(1)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
right,
|
||||
left));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
firstNoBorrow,
|
||||
secondNoBorrow));
|
||||
firstBorrow,
|
||||
secondBorrow));
|
||||
break;
|
||||
}
|
||||
case "SMulI32":
|
||||
@@ -1077,6 +1111,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
break;
|
||||
case "SAndB32":
|
||||
result = BitwiseAnd(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SOrB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1084,6 +1119,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SXorB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1091,22 +1127,64 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SAndn2B32":
|
||||
result = BitwiseAnd(
|
||||
left,
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SOrn2B32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
left,
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SNandB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
BitwiseAnd(left, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SNorB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
left,
|
||||
right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SXnorB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseXor,
|
||||
_uintType,
|
||||
left,
|
||||
right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SLshlB32":
|
||||
result = ShiftLeftLogical(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SLshrB32":
|
||||
result = ShiftRightLogical(
|
||||
left,
|
||||
BitwiseAnd(right, UInt(31)));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SAshrI32":
|
||||
result = ShiftRightArithmetic(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SBfmB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1149,6 +1227,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
offset,
|
||||
width);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
}
|
||||
case "SCselectB32":
|
||||
@@ -1161,9 +1240,47 @@ internal static partial class Gen5SpirvTranslator
|
||||
break;
|
||||
case "SMinU32":
|
||||
result = Ext(38, _uintType, left, right);
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
break;
|
||||
case "SMinI32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
Ext(39, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.SLessThan,
|
||||
_boolType,
|
||||
Bitcast(_intType, left),
|
||||
Bitcast(_intType, right)));
|
||||
break;
|
||||
case "SMaxU32":
|
||||
result = Ext(41, _uintType, left, right);
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
break;
|
||||
case "SMaxI32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
Ext(42, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.SGreaterThan,
|
||||
_boolType,
|
||||
Bitcast(_intType, left),
|
||||
Bitcast(_intType, right)));
|
||||
break;
|
||||
case "SLshl1AddU32":
|
||||
case "SLshl2AddU32":
|
||||
@@ -1308,17 +1425,55 @@ internal static partial class Gen5SpirvTranslator
|
||||
"SXorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseXor, _ulongType, oldExec, left),
|
||||
"SAndn2SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd, _ulongType, oldExec, notLeft),
|
||||
"SAndn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
left,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
oldExec),
|
||||
left),
|
||||
oldExec)),
|
||||
"SAndn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
notLeft,
|
||||
oldExec),
|
||||
"SOrn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
notLeft,
|
||||
oldExec),
|
||||
"SOrn2SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr, _ulongType, oldExec, notLeft),
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
left,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
oldExec)),
|
||||
"SNandSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
"SNorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
"SXnorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseXor,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
_ => 0u,
|
||||
};
|
||||
if (newExec == 0)
|
||||
@@ -1524,6 +1679,22 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
StoreS64(destination, value);
|
||||
if (instruction.Opcode is
|
||||
"SNotB64" or
|
||||
"SAndB64" or
|
||||
"SOrB64" or
|
||||
"SXorB64" or
|
||||
"SAndn1B64" or
|
||||
"SAndn2B64" or
|
||||
"SOrn1B64" or
|
||||
"SOrn2B64" or
|
||||
"SNandB64" or
|
||||
"SNorB64" or
|
||||
"SXnorB64")
|
||||
{
|
||||
Store(_scc, IsNotZero64(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2110,6 +2281,53 @@ internal static partial class Gen5SpirvTranslator
|
||||
value,
|
||||
_module.Constant64(_ulongType, 0));
|
||||
|
||||
private uint SignBit(uint value) =>
|
||||
ShiftRightLogical(value, UInt(31));
|
||||
|
||||
private uint SignedAddOverflow(uint left, uint right, uint result)
|
||||
{
|
||||
var leftSign = SignBit(left);
|
||||
var rightSign = SignBit(right);
|
||||
var resultSign = SignBit(result);
|
||||
var sameSourceSign = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
rightSign);
|
||||
var resultSignChanged = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
resultSign);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
sameSourceSign,
|
||||
resultSignChanged);
|
||||
}
|
||||
|
||||
private uint SignedSubOverflow(uint left, uint right, uint result)
|
||||
{
|
||||
var leftSign = SignBit(left);
|
||||
var rightSign = SignBit(right);
|
||||
var resultSign = SignBit(result);
|
||||
var differentSourceSign = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
rightSign);
|
||||
var resultSignChanged = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
resultSign);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
differentSourceSign,
|
||||
resultSignChanged);
|
||||
}
|
||||
|
||||
private static bool TryDecodeInlineConstant(uint encoded, out uint value)
|
||||
{
|
||||
if (encoded == 125)
|
||||
|
||||
@@ -467,9 +467,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
||||
var isStorage =
|
||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
||||
var isDepth =
|
||||
binding.Opcode.Contains("SampleC", StringComparison.Ordinal) ||
|
||||
binding.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
||||
var (format, componentKind) =
|
||||
DecodeImageFormat(binding.ResourceDescriptor);
|
||||
var componentType = componentKind switch
|
||||
@@ -494,7 +491,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
var imageType = _module.TypeImage(
|
||||
componentType,
|
||||
SpirvImageDim.Dim2D,
|
||||
depth: isDepth,
|
||||
depth: false,
|
||||
arrayed: false,
|
||||
multisampled: false,
|
||||
sampled: isStorage ? 2u : 1u,
|
||||
@@ -1661,18 +1658,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||
var imageOperands =
|
||||
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
||||
var reference = hasCompare
|
||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||
: 0u;
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
coordinates,
|
||||
};
|
||||
if (hasCompare)
|
||||
{
|
||||
operands.Add(
|
||||
Bitcast(
|
||||
_floatType,
|
||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||
}
|
||||
|
||||
if (imageOperands != 0)
|
||||
{
|
||||
@@ -1689,29 +1682,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
hasCompare
|
||||
? explicitLod
|
||||
? SpirvOp.ImageSampleDrefExplicitLod
|
||||
: SpirvOp.ImageSampleDrefImplicitLod
|
||||
: explicitLod
|
||||
? SpirvOp.ImageSampleExplicitLod
|
||||
: SpirvOp.ImageSampleImplicitLod,
|
||||
hasCompare ? resource.ComponentType : resource.VectorType,
|
||||
explicitLod
|
||||
? SpirvOp.ImageSampleExplicitLod
|
||||
: SpirvOp.ImageSampleImplicitLod,
|
||||
resource.VectorType,
|
||||
[.. operands]);
|
||||
if (hasCompare)
|
||||
{
|
||||
var scalar = sampled;
|
||||
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));
|
||||
sampled = EmitManualDepthCompare(resource, sampled, reference);
|
||||
}
|
||||
}
|
||||
else if (instruction.Opcode.StartsWith(
|
||||
@@ -1725,6 +1703,9 @@ internal static partial class Gen5SpirvTranslator
|
||||
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
||||
var coordinates = BuildFloatCoordinates(image, start);
|
||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||
var reference = hasCompare
|
||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||
: 0u;
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
@@ -1732,10 +1713,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
};
|
||||
if (hasCompare)
|
||||
{
|
||||
operands.Add(
|
||||
Bitcast(
|
||||
_floatType,
|
||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||
operands.Add(UInt(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1756,11 +1734,28 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
hasCompare
|
||||
? SpirvOp.ImageDrefGather
|
||||
: SpirvOp.ImageGather,
|
||||
SpirvOp.ImageGather,
|
||||
resource.VectorType,
|
||||
[.. 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;
|
||||
}
|
||||
else
|
||||
@@ -1794,6 +1789,67 @@ internal static partial class Gen5SpirvTranslator
|
||||
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)
|
||||
{
|
||||
var x = Bitcast(
|
||||
|
||||
@@ -24,6 +24,7 @@ public static class AmprExports
|
||||
private const uint KernelEventQueueRecordType = 2;
|
||||
private const uint WriteAddressRecordType = 3;
|
||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly bool _traceAmpr =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _traceAmprReads =
|
||||
@@ -37,6 +38,23 @@ public static class AmprExports
|
||||
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(
|
||||
Nid = "8aI7R7WaOlc",
|
||||
ExportName = "sceAmprCommandBufferConstructor",
|
||||
@@ -249,7 +267,7 @@ public static class AmprExports
|
||||
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);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
@@ -634,24 +652,43 @@ public static class AmprExports
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(
|
||||
hostPath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
ChunkSize,
|
||||
FileOptions.SequentialScan);
|
||||
if (fileOffset >= (ulong)stream.Length)
|
||||
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
||||
{
|
||||
return openResult;
|
||||
}
|
||||
|
||||
long fileLength;
|
||||
lock (cachedFile.Gate)
|
||||
{
|
||||
fileLength = cachedFile.Stream.Length;
|
||||
}
|
||||
|
||||
if (fileOffset >= (ulong)fileLength)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
stream.Position = unchecked((long)fileOffset);
|
||||
|
||||
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 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)
|
||||
{
|
||||
break;
|
||||
@@ -681,6 +718,44 @@ public static class AmprExports
|
||||
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(
|
||||
CpuContext ctx,
|
||||
ulong commandBuffer,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Collections.Concurrent;
|
||||
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 record PortState(int UserId, int Type, uint BufferLength, uint Frequency, int Format);
|
||||
|
||||
[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)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = Interlocked.Increment(ref _nextPortHandle);
|
||||
Ports[handle] = new PortState(userId, type, bufferLength, frequency, format);
|
||||
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]);
|
||||
return SetReturn(
|
||||
ctx,
|
||||
Ports.TryRemove(handle, out _)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QOQtbeDqsT4",
|
||||
ExportName = "sceAudioOutOutput",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutOutput(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -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 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);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
@@ -300,6 +316,41 @@ public static class KernelEventQueueCompatExports
|
||||
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);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -330,6 +381,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
queue.AddLast(queuedEvent);
|
||||
queued = true;
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
|
||||
if (queued)
|
||||
@@ -494,7 +546,9 @@ public static class KernelEventQueueCompatExports
|
||||
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)
|
||||
@@ -612,4 +666,17 @@ public static class KernelEventQueueCompatExports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
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")]
|
||||
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(
|
||||
Nid = "hcuQgD53UxM",
|
||||
ExportName = "printf",
|
||||
|
||||
@@ -5,9 +5,9 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
@@ -103,10 +103,12 @@ public static class KernelMemoryCompatExports
|
||||
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
||||
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = 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<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||
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 ulong _nextPhysicalAddress;
|
||||
private static ulong _nextVirtualAddress;
|
||||
@@ -231,6 +233,25 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "8zTFvBIAIN8",
|
||||
ExportName = "memset",
|
||||
@@ -1322,6 +1343,7 @@ public static class KernelMemoryCompatExports
|
||||
if (IsMutatingOpen(flags))
|
||||
{
|
||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||
InvalidateAprFileSizeCache(hostPath);
|
||||
}
|
||||
|
||||
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
||||
@@ -1530,6 +1552,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
File.Delete(hostPath);
|
||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||
InvalidateAprFileSizeCache(hostPath);
|
||||
AddNegativeStatCacheForGuestPath(guestPath);
|
||||
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2435,6 +2458,36 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "L-Q3LEjIbgA",
|
||||
ExportName = "sceKernelMapDirectMemory",
|
||||
@@ -2546,8 +2599,11 @@ public static class KernelMemoryCompatExports
|
||||
var length = ctx[CpuRegister.Rsi];
|
||||
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var flags = ctx[CpuRegister.Rcx];
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
||||
if (ShouldTraceDirectMemory())
|
||||
{
|
||||
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)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
@@ -2577,8 +2633,11 @@ public static class KernelMemoryCompatExports
|
||||
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
||||
if (ShouldTraceDirectMemory())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
||||
}
|
||||
|
||||
if (mappedAddress == 0)
|
||||
{
|
||||
@@ -2614,6 +2673,16 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "2SKEx6bSq-4",
|
||||
ExportName = "sceKernelBatchMap",
|
||||
@@ -2667,6 +2736,44 @@ public static class KernelMemoryCompatExports
|
||||
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(
|
||||
Nid = "rVjRvHJ0X6c",
|
||||
ExportName = "sceKernelVirtualQuery",
|
||||
@@ -2724,11 +2831,16 @@ public static class KernelMemoryCompatExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
|
||||
payload[32] = unchecked((byte)stateFlags);
|
||||
|
||||
var name = region.IsDirect
|
||||
? "direct"
|
||||
: region.IsFlexible
|
||||
? "flexible"
|
||||
: string.Empty;
|
||||
string name;
|
||||
lock (_memoryGate)
|
||||
{
|
||||
name = _mappedRegionNames.GetValueOrDefault(region.Address)
|
||||
?? (region.IsDirect
|
||||
? "direct"
|
||||
: region.IsFlexible
|
||||
? "flexible"
|
||||
: string.Empty);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
var nameBytes = Encoding.ASCII.GetBytes(name);
|
||||
@@ -3870,118 +3982,17 @@ public static class KernelMemoryCompatExports
|
||||
ulong alignment,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = null;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
||||
return KernelVirtualRangeAllocator.TryReserve(
|
||||
ctx,
|
||||
desiredAddress,
|
||||
length,
|
||||
executable,
|
||||
alignment,
|
||||
allowSearch: true,
|
||||
allowAllocateAtAlternative: false,
|
||||
"reserve range",
|
||||
out mappedAddress);
|
||||
}
|
||||
|
||||
private static bool IsMappedGuestRangeAvailable(
|
||||
@@ -4134,6 +4145,20 @@ public static class KernelMemoryCompatExports
|
||||
var app0Root = ResolveApp0Root();
|
||||
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) ||
|
||||
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -5796,22 +5821,40 @@ public static class KernelMemoryCompatExports
|
||||
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
||||
{
|
||||
size = 0;
|
||||
|
||||
string cachePath;
|
||||
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)
|
||||
{
|
||||
var length = fileInfo.Length;
|
||||
size = length < 0 ? 0UL : unchecked((ulong)length);
|
||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!new DirectoryInfo(hostPath).Exists)
|
||||
if (!new DirectoryInfo(cachePath).Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size = 65536;
|
||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||
return true;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (count <= 0)
|
||||
|
||||
@@ -95,6 +95,13 @@ public static class KernelPthreadCompatExports
|
||||
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(
|
||||
Nid = "T72hz6ffq08",
|
||||
ExportName = "scePthreadYield",
|
||||
|
||||
@@ -717,6 +717,35 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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(
|
||||
Nid = "DzES9hQF4f4",
|
||||
ExportName = "scePthreadAttrSetschedparam",
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Security.Cryptography;
|
||||
@@ -87,11 +86,11 @@ public static class KernelRuntimeCompatExports
|
||||
|
||||
if (micros < 1000)
|
||||
{
|
||||
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||
if ((++_shortUsleepCount & 31) == 0)
|
||||
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
|
||||
// PS microsecond waits into Windows millisecond sleeps on hot paths.
|
||||
if ((++_shortUsleepCount & 255) == 0)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -739,8 +738,11 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
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 (ShouldTraceVirtualMemory())
|
||||
{
|
||||
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))
|
||||
{
|
||||
@@ -752,6 +754,7 @@ public static class KernelRuntimeCompatExports
|
||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1732,117 +1735,16 @@ public static class KernelRuntimeCompatExports
|
||||
bool allowSearch,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = null;
|
||||
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;
|
||||
}
|
||||
return KernelVirtualRangeAllocator.TryReserve(
|
||||
ctx,
|
||||
desiredAddress,
|
||||
length,
|
||||
executable: false,
|
||||
alignment,
|
||||
allowSearch,
|
||||
allowAllocateAtAlternative: allowSearch,
|
||||
"reserve_virtual_range",
|
||||
out mappedAddress);
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
@@ -1855,4 +1757,9 @@ public static class KernelRuntimeCompatExports
|
||||
var mask = alignment - 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
@@ -58,6 +59,7 @@ public static class PadExports
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options");
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
}
|
||||
|
||||
@@ -158,14 +160,33 @@ public static class PadExports
|
||||
: 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)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
data.Clear();
|
||||
data[0x04] = 128;
|
||||
data[0x05] = 128;
|
||||
data[0x06] = 128;
|
||||
data[0x07] = 128;
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||
data[0x04] = leftX;
|
||||
data[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
@@ -185,4 +206,58 @@ public static class PadExports
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
|
||||
private static bool IsKeyDown(int vk) =>
|
||||
(GetAsyncKeyState(vk) & 0x8000) != 0;
|
||||
|
||||
private static bool IsEmulatorWindowFocused()
|
||||
{
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
||||
return processId == (uint)Environment.ProcessId;
|
||||
}
|
||||
|
||||
private static uint ReadKeyboardButtons()
|
||||
{
|
||||
uint buttons = 0;
|
||||
// D-pad
|
||||
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
|
||||
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
|
||||
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
|
||||
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
|
||||
// Face buttons
|
||||
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
|
||||
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
|
||||
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
|
||||
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
|
||||
// Shoulder buttons
|
||||
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
|
||||
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
|
||||
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
|
||||
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
|
||||
// Options (Start)
|
||||
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static byte ReadAnalogStick(bool negative, bool positive)
|
||||
{
|
||||
if (negative && !positive) return 0;
|
||||
if (positive && !negative) return 255;
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
cond = default;
|
||||
@@ -400,7 +430,7 @@ public static class SaveDataExports
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
var root = string.IsNullOrWhiteSpace(configured)
|
||||
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
|
||||
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
|
||||
: configured;
|
||||
return Path.GetFullPath(root);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="Silk.NET.Windowing" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
|
||||
@@ -48,6 +49,13 @@ public static class VideoOutExports
|
||||
private static int _frameDumpCount;
|
||||
private static long _nextFrameDumpIndex;
|
||||
private static string _windowTitle = "SharpEmu VideoOut";
|
||||
private static readonly bool _logFrameRate = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||
private static long _submittedFrameCount;
|
||||
private static long _presentedFrameCount;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
|
||||
{
|
||||
@@ -812,9 +820,46 @@ public static class VideoOutExports
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
ReportFrameRate(presented: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
internal static void ReportPresentedFrame() =>
|
||||
ReportFrameRate(presented: true);
|
||||
|
||||
private static void ReportFrameRate(bool presented)
|
||||
{
|
||||
if (!_logFrameRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (presented)
|
||||
{
|
||||
Interlocked.Increment(ref _presentedFrameCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _submittedFrameCount);
|
||||
}
|
||||
|
||||
var started = Volatile.Read(ref _frameRateWindowStart);
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var elapsedTicks = now - started;
|
||||
if (elapsedTicks < Stopwatch.Frequency ||
|
||||
Interlocked.CompareExchange(ref _frameRateWindowStart, now, started) != started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||
$"presented_fps={presentedCount / elapsedSeconds:F1}");
|
||||
}
|
||||
|
||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||
{
|
||||
lock (_stateGate)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,16 @@
|
||||
"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": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
Reference in New Issue
Block a user