Compare commits

..

55 Commits

Author SHA1 Message Date
Berk fbf3377f65 [sceAudio] added basic audio output support (#33) 2026-07-10 13:16:15 +03:00
Berk a63a7de35a [loader] cut import overhead (#32) 2026-07-10 01:14:17 +03:00
Berk fe1a592496 [shader-decoder-part1-hotfix] New format support, maintenance8 support, robustness improvements, and bug fixes. This commit includes updates to the AGC exports, Gen5 SPIR-V translator, and Vulkan video presenter to enhance compatibility and performance. (#31) 2026-07-07 21:06:04 +03:00
Berk 9d4dad2c24 [kernel] Added KernelExceptionCompatExports and fstat, pthread_equal (#30) 2026-07-07 21:05:54 +03:00
Foued Attar 38d94d8e54 [vulkan] enable required PhysicalDeviceFeatures for translated shaders (#29)
- Enable VertexPipelineStoresAndAtomics/FragmentStoresAndAtomics:
  fixes vkCreateGraphicsPipelines() rejecting guestBuffers storage
  descriptor as NonWritable in vertex/fragment stages.
- Enable ShaderInt64: fixes vkCreateShaderModule() rejecting SPIR-V
  using 64-bit integer capability.
- Query GetPhysicalDeviceFeatures first and only enable what the GPU
  actually reports as supported, with a warning fallback otherwise.

Also adds optional Vulkan Validation Layers (SHARPEMU_VK_VALIDATION=1)
to surface these VUID errors during development instead of silent
VK_ERROR_DEVICE_LOST.
2026-07-06 16:56:03 +03:00
ParantezTech 61a1abdb13 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-07-06 09:53:04 +03:00
ParantezTech 88452218cb update readme 2026-07-06 09:52:46 +03:00
Berk 427a511e48 [unity-fixes] support for Media/Modules/*.prx files (#27) 2026-07-05 22:10:37 +03:00
Berk e103795768 [agc] new agc exports for agc improvements (sceAgcDcbDrawIndex, sceAgcDriverGetResourceRegistrationMaxNameLength, sceAgcDriverGetDefaultOwner, sceAgcDriverRegisterResource, sceAgcDriverUnknown_KRzWekV120(?)) (#26) 2026-07-05 22:10:15 +03:00
Berk 24c6e44800 [ngs2 implements] Implement NGS2 exports (#25) 2026-07-05 22:09:50 +03:00
Berk 3a30d6419f [pad improvements] Added support for scePadSetVibrationMode (#24) 2026-07-05 22:09:36 +03:00
Berk 2d8a795f23 [sceAudio] added simple audio output implementation, needs to be improved (#23) 2026-07-05 22:09:25 +03:00
Berk adb2acdb39 [sceMsgDialog] implemented sceMsgDialogInitialize (#22) 2026-07-05 22:09:14 +03:00
Berk 5e5bead02c Avplayer implements (#21) 2026-07-05 22:09:04 +03:00
Berk e98f02966a [savedata-improvements-2] added sceSaveDataCreateTransactionResource (#20) 2026-07-05 22:08:53 +03:00
ParantezTech 9ab1e2f21a reuse 2026-07-04 19:14:44 +03:00
ParantezTech 340eb9e0ab [dotnet] configure local nuget package cache 2026-07-04 19:12:18 +03:00
Berk 03e3d25e85 [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix) (#19)
* [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix)

* [reuse] I forgot to add comma

* [readme] update logo
2026-07-04 17:05:09 +03:00
Berk 2bc466b10e [gpu-hotfix1] Cache improvements have been made (6x performance gain) (#18) 2026-07-04 15:19:32 +03:00
Berk 45d0be474b Savedata fix 1 (#17)
* [scePad] Just format code

* [sceSaveData] user folder sometimes appeared in the previous folder has been fixed.
2026-07-04 15:18:21 +03:00
ParantezTech 5bb91eff9b [scePad] Just format code 2026-07-04 15:10:48 +03:00
Vlad Denisov f4df0ed4bd [padExports]: add basic gamepad mappings (#16) 2026-07-04 14:40:43 +03:00
ParantezTech 23691a2bdc [readme] update screenshot image for dreaming sarah 2026-07-04 14:18:51 +03:00
ParantezTech 5c5ed8c064 [readme] update screenshot and add new image for dreaming sarah after shader decoder updates 2026-07-04 14:03:30 +03:00
Berk a4748ce266 [shader-decoder-part1] Implemented a shader decoder (Part 1) (#12)
* [shader-decoder-part1] Implemented a shader decoder for Gen5 shaders, including IR generation, metadata reading, scalar evaluation, and SPIR-V translation. Updated related exports and video output components to support the new shader decoding functionality.

* [shader decoder] correct RDNA2 operands, fixing synchronization problems

* [shader-decoder] RDNA2 decoder improvements

* [shader-decoder] fix RDNA2 shift masking and sprite draws

* [shader-decoder] improve RDNA2 shader decoder to support more instructions and fix some issues with the previous implementation.
2026-07-04 13:51:08 +03:00
Berk 5617646c8a [ci] fix workflow dispatch and push triggers for main branch (#15) 2026-07-04 13:48:29 +03:00
Berk a4f3d3cd7f [saveData] Add support for sceSaveDataMount3 (#14) 2026-07-03 13:25:38 +03:00
Berk 92526ecacf [core] Update native execution and kernel exports, phtread improvement (#13) 2026-07-03 13:19:24 +03:00
ParantezTech 0d89b2488b [dotnet] remove shaderc package because it is no longer needed 2026-07-02 17:15:51 +03:00
ParantezTech c3019b78d6 [ampr] more improvements to the Ampr library, generally for performance 2026-07-01 13:50:52 +03:00
ParantezTech dd5e524879 [pthread] pthread improvements and fixes 2026-07-01 13:50:24 +03:00
ParantezTech df9d0e6aaf [NpEntitlementAccess] added sceNpEntitlementAccessGetAddcontEntitlementInfoList export 2026-07-01 13:49:49 +03:00
ParantezTech 253d0fae3f [saveData] minimal save data dialog exports 2026-07-01 13:49:20 +03:00
ParantezTech 78d719ef9e [core] more leaf for speedup 2026-07-01 13:48:45 +03:00
ParantezTech be3806cdf6 [packages] added Silk.NET.Shaderc for debugging GLSL 2026-07-01 13:45:13 +03:00
ParantezTech dc47deee93 [agc] new imports for shader translation 2026-06-29 18:29:05 +03:00
ParantezTech 1c838cf8d6 [core] more leaf imports 2026-06-29 18:13:21 +03:00
ParantezTech 2f269566a5 [readme] update Demon's Souls text 2026-06-29 18:10:46 +03:00
ParantezTech 9d7afef6f6 [readme] update Demon's Souls new boot info 2026-06-29 14:40:27 +03:00
ParantezTech daeff21516 [readme] update screenshots 2026-06-29 14:34:36 +03:00
ParantezTech 82619deb34 [ci] ignore image files from actions 2026-06-29 14:32:46 +03:00
ParantezTech 697ad7be80 [libs] Add NP entitlement validation 2026-06-29 14:31:37 +03:00
ParantezTech 73c987e49f [libs] Improve video output handling 2026-06-29 14:31:26 +03:00
ParantezTech cd79275117 [libs] Enhance PlayGo streaming service 2026-06-29 14:31:18 +03:00
ParantezTech b14ecae504 [core] Refine native CPU execution imports 2026-06-29 14:31:10 +03:00
ParantezTech 873f473b65 [video] hide splash 2026-06-29 13:32:17 +03:00
ParantezTech 9a1a3789ef [video] cap presenter ticks 2026-06-29 13:32:03 +03:00
ParantezTech 0f3d4032cb [core] add leaf imports 2026-06-29 13:30:50 +03:00
ParantezTech 0c4a757695 [core] log leaf import args 2026-06-29 13:30:32 +03:00
ParantezTech 5fcc8eaa37 [saveData] add dir search 2026-06-29 13:30:08 +03:00
ParantezTech 21105e0346 [ampr] batch buffer IO 2026-06-29 13:28:37 +03:00
ParantezTech b424df5a64 [kernel] speed up printf 2026-06-29 13:28:30 +03:00
ParantezTech 7f85971e39 [kernel] carry pthread scheduling 2026-06-29 13:27:59 +03:00
ParantezTech e28259a99d [core] speed up guest memory 2026-06-29 13:26:04 +03:00
ParantezTech 3d2a4b74ac [readme] add screenshot running Demon's Souls 2026-06-29 00:17:13 +03:00
55 changed files with 23308 additions and 656 deletions
+1 -1
View File
@@ -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: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

+7 -1
View File
@@ -9,11 +9,17 @@ on:
- "**"
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
workflow_dispatch:
permissions:
@@ -120,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
+1
View File
@@ -26,6 +26,7 @@ arm64/
*.userosscache
*.sln.docstates
.packages
packages/
*.nupkg
.nuget/
+2 -2
View File
@@ -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>
BIN
View File
Binary file not shown.
+3 -1
View File
@@ -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

+18
View File
@@ -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>
+6 -1
View File
@@ -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>
+11
View File
@@ -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)
{
@@ -482,6 +487,8 @@ public sealed partial class DirectExecutionBackend
catch (Exception ex)
{
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
return 18446744071562199298uL;
}
@@ -527,15 +534,14 @@ public sealed partial class DirectExecutionBackend
if (dispatchIndex % 100000 == 0)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid}) " +
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
$"ret=0x{returnRip:X16}");
}
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
returnRip,
(ulong)argPackPtr + 104uL,
ActiveGuestReturnSlotAddress);
int returnValue;
try
if (IsNoBlockLeafImport(importStubEntry.Nid))
{
cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext);
@@ -544,9 +550,25 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
}
finally
else
{
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
returnRip,
(ulong)argPackPtr + 104uL,
ActiveGuestReturnSlotAddress);
try
{
cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
}
finally
{
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
}
}
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
@@ -591,20 +613,76 @@ public sealed partial class DirectExecutionBackend
return true;
}
private static bool IsNoBlockLeafImport(string nid) =>
nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
"baQO9ez2gL4" or // sceAmprCommandBufferReset
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"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;
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout)
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;
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)
{
@@ -616,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
@@ -635,6 +719,31 @@ public sealed partial class DirectExecutionBackend
"7H0iTOciTLo" or
"2Z+PpY6CaJg" or
"8aI7R7WaOlc" or
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
"LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
"JrtiDtKeS38" or // sceAgcAcbResetQueue
"cFazmnXpJOE" or // sceAgcAcbEventWrite
"KT-hTp-Ch14" or // sceAgcAcbAcquireMem
"htn36gPnBk4" or // sceAgcAcbWaitRegMem
"eZ4+17OQz4Q" or // sceAgcAcbWriteData
"j3EtxFkSIhQ" or // sceAgcAcbDispatchIndirect
"gSRnr79F8tQ" or // sceAgcDriverSubmitAcb
"i1jyy49AjXU" or // sceAgcDcbWriteData
"VmW0Tdpy420" or // sceAgcDcbWaitRegMem
"WmAc2MEj6Io" or // sceAgcDcbDmaData
"rUuVjyR+Rd4" or // sceAgcDcbGetLodStatsGetSize
"vuSXe69VILM" or // sceAgcDcbGetLodStats
"RmaJwLtc8rY" or // sceAgcDcbSetBaseIndirectArgs
"CtB+A9-VxO0" or // sceAgcDcbDispatchIndirect
"+kSrjIVxKFE" or // sceAgcDcbPushMarker
"H7uZqCoNuWk" or // sceAgcDcbPopMarker
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
"a8uLzYY--tM" or
"Qs1xtplKo0U" or
"GuchCTefuZw" or
@@ -653,7 +762,18 @@ public sealed partial class DirectExecutionBackend
"rqwFKI4PAiM" or
"eE4Szl8sil8" or
"qvMUCyyaCSI" or
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
"ZP4e7rlzOUk" or // sceSaveDataMount3
"ERKzksauAJA" or // sceSaveDataDialogGetStatus
"KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus
"en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay
"jO8DM8oyego" or // sceNpEntitlementAccessInitialize
"TFyU+KFBv54" or // sceNpEntitlementAccessGetAddcontEntitlementInfoList
"27bAgiJmOh0" or // pthread_cond_timedwait
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen
@@ -1102,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;
}
@@ -1116,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;
@@ -347,10 +347,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public string Name { get; init; } = string.Empty;
public int Priority { get; init; }
public ulong AffinityMask { get; init; }
public CpuContext Context { get; init; } = null!;
public GuestThreadRunState State { get; set; }
public ulong ExitValue { get; set; }
public string? BlockReason { get; set; }
public bool HasBlockedContinuation { get; set; }
@@ -388,14 +394,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private Action? _work;
private volatile bool _stopping;
public GuestContinuationRunner(ulong guestThreadHandle)
public GuestContinuationRunner(ulong guestThreadHandle, ThreadPriority priority)
{
_guestThreadHandle = guestThreadHandle;
_thread = new Thread(ThreadMain)
{
IsBackground = true,
Name = $"GuestContinuation-{guestThreadHandle:X}",
Priority = ThreadPriority.BelowNormal,
Priority = priority,
};
_thread.Start();
}
@@ -1262,7 +1268,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
libraryName.IndexOf("Kernel", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool PreferLleForLibcExport(string exportName)
private bool PreferLleForLibcExport(string exportName)
{
if (string.IsNullOrWhiteSpace(exportName))
{
@@ -1283,6 +1289,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return true;
}
if (IsLibcAllocatorExport(exportName))
{
return CanUseLleLibcAllocatorFamily();
}
if (string.Equals(value, "0", StringComparison.Ordinal))
{
return true;
@@ -1294,6 +1304,51 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return IsSafeLleLibcExport(exportName);
}
private bool CanUseLleLibcAllocatorFamily()
{
return HasUsableLleLibcExport("gQX+4GDQjpM", "malloc") &&
HasUsableLleLibcExport("tIhsqj0qsFE", "free") &&
HasUsableLleLibcExport("2X5agFjKxMc", "calloc") &&
HasUsableLleLibcExport("Y7aJ1uydPMo", "realloc") &&
HasUsableLleLibcExport("Ujf3KzMvRmI", "memalign") &&
HasUsableLleLibcExport("2Btkg8k24Zg", "aligned_alloc") &&
HasUsableLleLibcExport("cVSk9y8URbc", "posix_memalign");
}
private bool HasUsableLleLibcExport(string nid, string exportName)
{
if (TryResolveRuntimeSymbolAddress(nid, out var address) && IsDirectImportTargetUsable(address))
{
return true;
}
foreach (var candidate in EnumerateRuntimeSymbolCandidates(exportName))
{
if (TryResolveRuntimeSymbolAddress(candidate, out address) && IsDirectImportTargetUsable(address))
{
return true;
}
}
return false;
}
private static bool IsLibcAllocatorExport(string exportName)
{
return exportName switch
{
"malloc" or
"free" or
"calloc" or
"realloc" or
"memalign" or
"aligned_alloc" or
"posix_memalign" or
"malloc_usable_size" => true,
_ => false,
};
}
private static bool IsSafeLleLibcExport(string exportName)
{
return exportName switch
@@ -2393,13 +2448,78 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Interlocked.Increment(ref _readyGuestThreadCount);
}
Console.Error.WriteLine(
$"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16}");
$"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} " +
$"entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16} priority={thread.Priority} " +
$"host_priority={MapGuestThreadPriority(thread.Priority)} affinity=0x{thread.AffinityMask:X}");
Pump(creatorContext, "pthread_create");
return true;
}
public bool SupportsGuestContextTransfer => true;
public bool TryJoinThread(
CpuContext callerContext,
ulong threadHandle,
out ulong returnValue,
out string? error)
{
returnValue = 0;
error = null;
if (threadHandle == 0)
{
error = "thread handle is zero";
return false;
}
if (threadHandle == GuestThreadExecution.CurrentGuestThreadHandle)
{
error = "thread cannot join itself";
return false;
}
while (!ActiveForcedGuestExit)
{
Thread? hostThread;
lock (_guestThreadGate)
{
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
{
error = $"unknown guest thread 0x{threadHandle:X16}";
return false;
}
if (thread.State == GuestThreadRunState.Exited)
{
returnValue = thread.ExitValue;
return true;
}
if (thread.State == GuestThreadRunState.Faulted)
{
error =
$"guest thread 0x{threadHandle:X16} faulted: " +
(thread.BlockReason ?? "unknown error");
return false;
}
hostThread = thread.HostThread;
}
if (hostThread is not null &&
!ReferenceEquals(hostThread, Thread.CurrentThread))
{
hostThread.Join(1);
}
else
{
Thread.Sleep(1);
}
}
error = "guest execution stopped while joining thread";
return false;
}
public void Pump(CpuContext callerContext, string reason)
{
_ = callerContext;
@@ -2448,7 +2568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
IsBackground = true,
Name = $"SharpEmu-{thread.Name}",
Priority = ThreadPriority.BelowNormal,
Priority = MapGuestThreadPriority(thread.Priority),
};
lock (_guestThreadGate)
{
@@ -2851,7 +2971,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
{
runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(currentGuestThreadHandle);
runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(
currentGuestThreadHandle,
MapGuestThreadPriority(guestThread.Priority));
}
else
{
@@ -2990,6 +3112,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
EntryPoint = request.EntryPoint,
Argument = request.Argument,
Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name,
Priority = request.Priority,
AffinityMask = request.AffinityMask,
Context = context,
State = GuestThreadRunState.Ready,
};
@@ -3129,11 +3253,77 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
}
private static ThreadPriority MapGuestThreadPriority(int priority)
{
if (priority <= 478)
{
return ThreadPriority.Highest;
}
if (priority >= 733)
{
return ThreadPriority.Lowest;
}
return ThreadPriority.Normal;
}
private void ApplyGuestThreadAffinity(ulong guestAffinityMask)
{
var hostAffinityMask = MapGuestThreadAffinity(guestAffinityMask);
if (hostAffinityMask == 0)
{
return;
}
if (SetThreadAffinityMask(GetCurrentThread(), (nuint)hostAffinityMask) == 0 && _logGuestThreads)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Failed to set guest thread affinity guest=0x{guestAffinityMask:X} " +
$"host=0x{hostAffinityMask:X} error={Marshal.GetLastWin32Error()}");
}
}
private static ulong MapGuestThreadAffinity(ulong guestAffinityMask)
{
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue)
{
return 0;
}
var processorCount = Math.Min(Environment.ProcessorCount, 64);
if (processorCount == 0)
{
return 0;
}
ulong hostAffinityMask = 0;
for (var guestCpu = 0; guestCpu < 64; guestCpu++)
{
if ((guestAffinityMask & (1UL << guestCpu)) == 0)
{
continue;
}
var hostCpu = processorCount < 8
? guestCpu % processorCount
: processorCount >= 16
? guestCpu * 2
: guestCpu;
if (hostCpu < processorCount)
{
hostAffinityMask |= 1UL << hostCpu;
}
}
return hostAffinityMask;
}
private void RunGuestThread(GuestThreadState thread, string reason)
{
var previousLastError = LastError;
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle);
var previousGuestThreadState = _activeGuestThreadState;
ApplyGuestThreadAffinity(thread.AffinityMask);
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
_activeGuestThreadState = thread;
try
@@ -3178,11 +3368,23 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
switch (exitReason)
{
case GuestNativeCallExitReason.Returned:
thread.ExitValue = thread.Context[CpuRegister.Rax];
thread.State = GuestThreadRunState.Exited;
break;
case GuestNativeCallExitReason.Blocked:
thread.State = GuestThreadRunState.Blocked;
thread.BlockReason = blockReason;
if (thread.HasBlockedContinuation &&
thread.BlockWakeHandler is not null &&
thread.BlockWakeHandler())
{
thread.State = GuestThreadRunState.Ready;
thread.BlockReason = null;
thread.BlockWakeHandler = null;
thread.BlockDeadlineTimestamp = 0;
_readyGuestThreads.Enqueue(thread);
Interlocked.Increment(ref _readyGuestThreadCount);
}
break;
default:
thread.State = GuestThreadRunState.Faulted;
@@ -4144,6 +4346,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("kernel32.dll")]
private static extern nint GetCurrentThread();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
+382 -111
View File
@@ -9,9 +9,11 @@ namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{
private readonly object _gate = new();
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);
@@ -78,9 +82,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false;
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -89,9 +94,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection
});
}
finally
{
_gate.ExitWriteLock();
}
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;
}
@@ -105,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)
@@ -134,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)
@@ -188,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
@@ -202,9 +213,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -213,11 +225,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection
});
}
finally
{
_gate.ExitWriteLock();
}
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;
}
@@ -237,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++)
{
@@ -257,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;
}
@@ -315,7 +333,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
lock (_guestAllocationGate)
{
lock (_gate)
_gate.EnterWriteLock();
try
{
foreach (var region in _regions)
{
@@ -323,6 +342,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
_regions.Clear();
_pageProtections.Clear();
lock (_allocationSearchHintGate)
{
_allocationSearchHints.Clear();
}
}
finally
{
_gate.ExitWriteLock();
}
_guestAllocationArenaBase = 0;
@@ -343,7 +370,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart);
lock (_gate)
_gate.EnterWriteLock();
try
{
var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null)
@@ -374,7 +402,11 @@ 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
{
_gate.ExitWriteLock();
}
}
@@ -425,7 +457,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{
lock (_gate)
_gate.EnterReadLock();
try
{
var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++)
@@ -440,116 +473,240 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
return snapshot;
}
finally
{
_gate.ExitReadLock();
}
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
lock (_gate)
var requiresExclusiveAccess = false;
_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 (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
}
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* destPtr = destination)
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
}
finally
{
RestorePageProtections(touchedPages);
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
}
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false;
}
_gate.EnterWriteLock();
try
{
return TryReadExclusive(virtualAddress, destination);
}
finally
{
_gate.ExitWriteLock();
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
lock (_gate)
var requiresExclusiveAccess = false;
_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 (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
}
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* srcPtr = source)
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
}
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false;
}
_gate.EnterWriteLock();
try
{
return TryWriteExclusive(virtualAddress, source);
}
finally
{
_gate.ExitWriteLock();
}
}
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
{
var srcPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
}
finally
{
RestorePageProtections(touchedPages);
}
return true;
}
return false;
}
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
}
return true;
}
return false;
}
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
@@ -561,45 +718,76 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public void* GetPointer(ulong virtualAddress)
{
lock (_gate)
_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
{
_gate.ExitReadLock();
}
}
public bool IsAccessible(ulong virtualAddress, ulong size)
{
lock (_gate)
_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
{
_gate.ExitReadLock();
}
}
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)
@@ -611,21 +799,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
var end = address + size;
lock (_gate)
_gate.EnterReadLock();
try
{
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);
}
}
}
finally
{
_gate.ExitReadLock();
}
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;
@@ -707,15 +941,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
var pageAddress = startPage;
while (pageAddress < endPage)
{
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
return false;
}
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
? ulong.MaxValue
: info.BaseAddress + info.RegionSize;
var rangeEnd = Math.Min(endPage, queriedEnd);
if (rangeEnd <= pageAddress)
{
return false;
}
if (info.State == MEM_COMMIT)
{
pageAddress = rangeEnd;
continue;
}
@@ -724,10 +969,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false;
}
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
pageAddress = rangeEnd;
}
return true;
@@ -780,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)
+6 -2
View File
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
@@ -136,6 +137,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
@@ -541,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)
@@ -552,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);
@@ -560,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
+11
View File
@@ -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, )",
+9 -1
View File
@@ -10,7 +10,9 @@ public readonly record struct GuestThreadStartRequest(
ulong EntryPoint,
ulong Argument,
ulong AttributeAddress,
string Name);
string Name,
int Priority,
ulong AffinityMask);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
@@ -27,6 +29,12 @@ public interface IGuestThreadScheduler
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
bool TryJoinThread(
CpuContext callerContext,
ulong threadHandle,
out ulong returnValue,
out string? error);
void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal enum Gen5ShaderEncoding
{
Sop1,
Sop2,
Sopc,
Sopp,
Sopk,
Smrd,
Smem,
Mubuf,
Mtbuf,
Vop1,
Vop2,
Vopc,
Vop3,
Vintrp,
Ds,
Flat,
Vop3p,
Mimg,
Exp,
}
internal enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
EncodedConstant,
LiteralConstant,
}
internal enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
Sampler,
ConstantBuffer,
}
internal enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
internal enum Gen5SpirvStage
{
Vertex,
Pixel,
Compute,
}
internal sealed record Gen5SpirvShader(
byte[] Spirv,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
uint AttributeCount,
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
internal readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
internal sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary<uint, uint> DirectResources,
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
internal readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
uint? ThreadGroupSizeRegister)
{
public bool TryGetExpression(uint scalarRegister, out string expression)
{
if (WorkGroupXRegister == scalarRegister)
{
expression = "gl_WorkGroupID.x";
return true;
}
if (WorkGroupYRegister == scalarRegister)
{
expression = "gl_WorkGroupID.y";
return true;
}
if (WorkGroupZRegister == scalarRegister)
{
expression = "gl_WorkGroupID.z";
return true;
}
if (ThreadGroupSizeRegister == scalarRegister)
{
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
return true;
}
expression = string.Empty;
return false;
}
public void ClearStaticValues(Span<uint> scalarRegisters)
{
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
}
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
{
if (scalarRegister is { } register && register < scalarRegisters.Length)
{
scalarRegisters[(int)register] = 0;
}
}
}
internal sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList<uint> UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
public static Gen5Operand Vector(uint index) =>
new(Gen5OperandKind.VectorRegister, index);
public static Gen5Operand Source(uint encoded, uint? literal = null)
{
if (encoded >= 256)
{
return Vector(encoded - 256);
}
if (encoded is 249 or 255 && literal.HasValue)
{
return new(Gen5OperandKind.LiteralConstant, literal.Value);
}
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
{
return Scalar(encoded);
}
return new(Gen5OperandKind.EncodedConstant, encoded);
}
public override string ToString() => Kind switch
{
Gen5OperandKind.ScalarRegister => $"s{Value}",
Gen5OperandKind.VectorRegister => $"v{Value}",
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
_ => $"src[{Value}]",
};
}
internal abstract record Gen5InstructionControl;
internal sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList<uint> AddressRegisters,
uint VectorData,
uint ScalarResource,
uint ScalarSampler,
uint Dimension,
bool IsArray,
bool Glc,
bool Slc) : Gen5InstructionControl
{
public uint GetAddressRegister(int component) =>
component < AddressRegisters.Count
? AddressRegisters[component]
: VectorAddress + (uint)component;
}
internal sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarAddress,
int OffsetBytes,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarResource,
int OffsetBytes,
bool IndexEnabled,
bool OffsetEnabled,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
internal sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
internal sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
internal sealed record Gen5SdwaControl(
uint DestinationSelect,
uint Source0Select,
uint Source1Select,
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp) : Gen5InstructionControl;
internal sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
uint AbsoluteMask,
uint NegateMask,
uint BankMask,
uint RowMask) : Gen5InstructionControl;
internal sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
internal sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
internal sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
IReadOnlyList<uint> ResourceDescriptor,
IReadOnlyList<uint> SamplerDescriptor,
uint? MipLevel);
internal sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList<uint> InstructionPcs,
byte[] Data);
internal sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data);
internal sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters,
IReadOnlyList<uint> ScalarRegisters,
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
internal sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
IReadOnlyList<uint> Words,
IReadOnlyList<Gen5Operand> Sources,
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
internal sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList<Gen5ShaderInstruction> Instructions)
{
public IEnumerable<Gen5ImageControl> ImageResources =>
Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ImageControl>();
}
@@ -0,0 +1,154 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Agc;
internal static class Gen5ShaderMetadataReader
{
private const ulong ShaderUserDataOffset = 0x08;
private const int ResourceClassCount = 4;
private const int MaxMetadataEntries = 4096;
public static bool TryRead(
CpuContext ctx,
ulong shaderHeaderAddress,
out Gen5ShaderMetadata metadata)
{
metadata = default!;
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0 ||
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
{
return false;
}
var resourceOffsets = new ulong[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!TryReadUInt64(
ctx,
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
out resourceOffsets[resourceClass]))
{
return false;
}
}
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
directResourceCount > MaxMetadataEntries)
{
return false;
}
var resourceCounts = new ushort[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!TryReadUInt16(
ctx,
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
out resourceCounts[resourceClass]) ||
resourceCounts[resourceClass] > MaxMetadataEntries)
{
return false;
}
}
var directResources = new Dictionary<uint, uint>();
if (directResourceCount != 0)
{
if (directResourceOffsetsAddress == 0)
{
return false;
}
for (uint type = 0; type < directResourceCount; type++)
{
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
{
return false;
}
if (offset != ushort.MaxValue)
{
directResources[type] = offset;
}
}
}
var resources = new List<Gen5ShaderResourceMapping>();
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
var count = resourceCounts[resourceClass];
if (count == 0)
{
continue;
}
if (resourceOffsets[resourceClass] == 0)
{
return false;
}
for (uint slot = 0; slot < count; slot++)
{
if (!TryReadUInt16(
ctx,
resourceOffsets[resourceClass] + slot * sizeof(ushort),
out var sharp))
{
return false;
}
var offset = (uint)(sharp & 0x7FFF);
if (offset == 0x7FFF)
{
continue;
}
resources.Add(new Gen5ShaderResourceMapping(
(Gen5ShaderResourceKind)resourceClass,
slot,
offset,
(sharp & 0x8000) != 0));
}
}
metadata = new Gen5ShaderMetadata(
extendedUserDataSize,
shaderResourceTableSize,
directResources,
resources);
return true;
}
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
return true;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal static class SpirvFixedShaders
{
public static byte[] CreateFullscreenVertex(uint attributeCount)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var boolType = module.TypeBool();
var uintType = module.TypeInt(32, signed: false);
var floatType = module.TypeFloat(32);
var vec4Type = module.TypeVector(floatType, 4);
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
module.AddName(vertexIndex, "vertexIndex");
module.AddDecoration(
vertexIndex,
SpirvDecoration.BuiltIn,
(uint)SpirvBuiltIn.VertexIndex);
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(position, "position");
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
var attributes = new uint[attributeCount];
for (uint index = 0; index < attributeCount; index++)
{
attributes[index] =
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(attributes[index], $"attr{index}");
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
var one = module.Constant(uintType, 1);
var two = module.Constant(uintType, 2);
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
var zero = module.ConstantFloat(floatType, 0f);
var oneFloat = module.ConstantFloat(floatType, 1f);
var twoFloat = module.ConstantFloat(floatType, 2f);
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
var positionValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
xPosition,
yPosition,
zero,
oneFloat);
module.AddStatement(SpirvOp.Store, position, positionValue);
var attributeValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
x,
y,
zero,
oneFloat);
foreach (var attribute in attributes)
{
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
var interfaces = new uint[2 + attributes.Length];
interfaces[0] = vertexIndex;
interfaces[1] = position;
attributes.CopyTo(interfaces, 2);
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
_ = boolType;
return module.Build();
}
public static byte[] CreateCopyFragment()
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var floatType = module.TypeFloat(32);
var vec2Type = module.TypeVector(floatType, 2);
var vec4Type = module.TypeVector(floatType, 4);
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var imageType = module.TypeImage(
floatType,
SpirvImageDim.Dim2D,
depth: false,
arrayed: false,
multisampled: false,
sampled: 1,
SpirvImageFormat.Unknown);
var sampledImageType = module.TypeSampledImage(imageType);
var sampledImagePointer =
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
module.AddName(attribute, "attr0");
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
var texture = module.AddGlobalVariable(
sampledImagePointer,
SpirvStorageClass.UniformConstant);
module.AddName(texture, "tex0");
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(output, "outColor");
module.AddDecoration(output, SpirvDecoration.Location, 0);
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
var coordinates = module.AddInstruction(
SpirvOp.VectorShuffle,
vec2Type,
attributeValue,
attributeValue,
0,
1);
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
var lod = module.ConstantFloat(floatType, 0f);
var color = module.AddInstruction(
SpirvOp.ImageSampleExplicitLod,
vec4Type,
sampledImage,
coordinates,
2,
lod);
module.AddStatement(SpirvOp.Store, output, color);
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(
SpirvExecutionModel.Fragment,
main,
"main",
[attribute, texture, output]);
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
return module.Build();
}
}
+885
View File
@@ -0,0 +1,885 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.Agc;
internal enum SpirvOp : ushort
{
Nop = 0,
Name = 5,
Extension = 10,
ExtInstImport = 11,
ExtInst = 12,
MemoryModel = 14,
EntryPoint = 15,
ExecutionMode = 16,
Capability = 17,
TypeVoid = 19,
TypeBool = 20,
TypeInt = 21,
TypeFloat = 22,
TypeVector = 23,
TypeImage = 25,
TypeSampler = 26,
TypeSampledImage = 27,
TypeArray = 28,
TypeRuntimeArray = 29,
TypeStruct = 30,
TypePointer = 32,
TypeFunction = 33,
ConstantTrue = 41,
ConstantFalse = 42,
Constant = 43,
ConstantComposite = 44,
ConstantNull = 46,
Function = 54,
FunctionParameter = 55,
FunctionEnd = 56,
FunctionCall = 57,
Variable = 59,
Load = 61,
Store = 62,
AccessChain = 65,
ArrayLength = 68,
Decorate = 71,
VectorExtractDynamic = 77,
VectorInsertDynamic = 78,
VectorShuffle = 79,
CompositeConstruct = 80,
CompositeExtract = 81,
CompositeInsert = 82,
CopyObject = 83,
SampledImage = 86,
ImageSampleImplicitLod = 87,
ImageSampleExplicitLod = 88,
ImageSampleDrefImplicitLod = 89,
ImageSampleDrefExplicitLod = 90,
ImageFetch = 95,
ImageGather = 96,
ImageDrefGather = 97,
ImageRead = 98,
ImageWrite = 99,
Image = 100,
ImageQuerySizeLod = 103,
ImageQuerySize = 104,
ImageQueryLod = 105,
ImageQueryLevels = 106,
ImageQuerySamples = 107,
ConvertFToU = 109,
ConvertFToS = 110,
ConvertSToF = 111,
ConvertUToF = 112,
UConvert = 113,
SConvert = 114,
FConvert = 115,
Bitcast = 124,
SNegate = 126,
FNegate = 127,
IAdd = 128,
FAdd = 129,
ISub = 130,
FSub = 131,
IMul = 132,
FMul = 133,
UDiv = 134,
SDiv = 135,
FDiv = 136,
UMod = 137,
SRem = 138,
SMod = 139,
FRem = 140,
FMod = 141,
IAddCarry = 149,
ISubBorrow = 150,
UMulExtended = 151,
SMulExtended = 152,
Any = 154,
All = 155,
IsNan = 156,
IsInf = 157,
LogicalEqual = 164,
LogicalNotEqual = 165,
LogicalOr = 166,
LogicalAnd = 167,
LogicalNot = 168,
Select = 169,
IEqual = 170,
INotEqual = 171,
UGreaterThan = 172,
SGreaterThan = 173,
UGreaterThanEqual = 174,
SGreaterThanEqual = 175,
ULessThan = 176,
SLessThan = 177,
ULessThanEqual = 178,
SLessThanEqual = 179,
FOrdEqual = 180,
FUnordEqual = 181,
FOrdNotEqual = 182,
FUnordNotEqual = 183,
FOrdLessThan = 184,
FUnordLessThan = 185,
FOrdGreaterThan = 186,
FUnordGreaterThan = 187,
FOrdLessThanEqual = 188,
FUnordLessThanEqual = 189,
FOrdGreaterThanEqual = 190,
FUnordGreaterThanEqual = 191,
ShiftRightLogical = 194,
ShiftRightArithmetic = 195,
ShiftLeftLogical = 196,
BitwiseOr = 197,
BitwiseXor = 198,
BitwiseAnd = 199,
Not = 200,
BitFieldInsert = 201,
BitFieldSExtract = 202,
BitFieldUExtract = 203,
BitReverse = 204,
BitCount = 205,
ControlBarrier = 224,
MemoryBarrier = 225,
AtomicIAdd = 234,
Phi = 245,
LoopMerge = 246,
SelectionMerge = 247,
Label = 248,
Branch = 249,
BranchConditional = 250,
Switch = 251,
Kill = 252,
Return = 253,
ReturnValue = 254,
Unreachable = 255,
GroupNonUniformElect = 333,
GroupNonUniformAll = 334,
GroupNonUniformAny = 335,
GroupNonUniformAllEqual = 336,
GroupNonUniformBroadcast = 337,
GroupNonUniformBroadcastFirst = 338,
GroupNonUniformBallot = 339,
GroupNonUniformShuffle = 345,
GroupNonUniformShuffleXor = 346,
GroupNonUniformShuffleUp = 347,
GroupNonUniformShuffleDown = 348,
}
internal enum SpirvCapability : uint
{
Shader = 1,
Float16 = 9,
Float64 = 10,
Int64 = 11,
Int16 = 22,
ImageGatherExtended = 25,
StorageImageExtendedFormats = 49,
ImageQuery = 50,
StorageImageReadWithoutFormat = 55,
StorageImageWriteWithoutFormat = 56,
GroupNonUniform = 61,
GroupNonUniformVote = 62,
GroupNonUniformBallot = 64,
GroupNonUniformShuffle = 65,
RuntimeDescriptorArray = 5302,
}
internal enum SpirvStorageClass : uint
{
UniformConstant = 0,
Input = 1,
Uniform = 2,
Output = 3,
Workgroup = 4,
Private = 6,
Function = 7,
PushConstant = 9,
Image = 11,
StorageBuffer = 12,
}
internal enum SpirvExecutionModel : uint
{
Vertex = 0,
Fragment = 4,
GLCompute = 5,
}
internal enum SpirvExecutionMode : uint
{
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
}
internal enum SpirvDecoration : uint
{
Block = 2,
ArrayStride = 6,
BuiltIn = 11,
NoPerspective = 13,
Flat = 14,
Location = 30,
Binding = 33,
DescriptorSet = 34,
Offset = 35,
}
internal enum SpirvBuiltIn : uint
{
Position = 0,
VertexIndex = 42,
InstanceIndex = 43,
FragCoord = 15,
FrontFacing = 17,
WorkgroupId = 26,
LocalInvocationId = 27,
GlobalInvocationId = 28,
LocalInvocationIndex = 29,
SubgroupLocalInvocationId = 41,
}
internal enum SpirvImageDim : uint
{
Dim1D = 0,
Dim2D = 1,
Dim3D = 2,
Cube = 3,
Buffer = 5,
}
internal enum SpirvImageFormat : uint
{
Unknown = 0,
Rgba32f = 1,
Rgba16f = 2,
R32f = 3,
Rgba8 = 4,
Rgba8Snorm = 5,
Rg32f = 6,
Rg16f = 7,
R11fG11fB10f = 8,
R16f = 9,
Rgba16 = 10,
Rgb10A2 = 11,
Rg16 = 12,
Rg8 = 13,
R16 = 14,
R8 = 15,
Rgba16Snorm = 16,
Rg16Snorm = 17,
Rg8Snorm = 18,
R16Snorm = 19,
R8Snorm = 20,
Rgba32i = 21,
Rgba16i = 22,
Rgba8i = 23,
R32i = 24,
Rg32i = 25,
Rg16i = 26,
Rg8i = 27,
R16i = 28,
R8i = 29,
Rgba32ui = 30,
Rgba16ui = 31,
Rgba8ui = 32,
R32ui = 33,
Rgb10A2ui = 34,
Rg32ui = 35,
Rg16ui = 36,
Rg8ui = 37,
R16ui = 38,
R8ui = 39,
}
internal sealed class SpirvModuleBuilder
{
private const uint Magic = 0x07230203;
private const uint Version15 = 0x00010500;
private const uint Generator = 0x53504500; // "SPE"
private readonly List<uint> _capabilities = [];
private readonly List<uint> _extensions = [];
private readonly List<uint> _imports = [];
private readonly List<uint> _memoryModel = [];
private readonly List<uint> _entryPoints = [];
private readonly List<uint> _executionModes = [];
private readonly List<uint> _debug = [];
private readonly List<uint> _annotations = [];
private readonly List<uint> _typesConstantsGlobals = [];
private readonly List<uint> _functions = [];
private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = [];
private readonly Dictionary<uint, uint> _floatTypes = [];
private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = [];
private readonly Dictionary<
(
uint SampledType,
SpirvImageDim Dimension,
bool Depth,
bool Arrayed,
bool Multisampled,
uint Sampled,
SpirvImageFormat Format
),
uint> _imageTypes = [];
private readonly Dictionary<uint, uint> _sampledImageTypes = [];
private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = [];
private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = [];
private readonly Dictionary<uint, uint> _runtimeArrayTypes = [];
private readonly Dictionary<string, uint> _functionTypes = [];
private readonly Dictionary<(uint Type, ulong Value), uint> _constants = [];
private readonly HashSet<SpirvCapability> _declaredCapabilities = [];
private readonly Dictionary<string, uint> _extInstImports = [];
private uint _nextId = 1;
private uint? _voidType;
private uint? _boolType;
public uint AllocateId() => _nextId++;
public void AddCapability(SpirvCapability capability)
{
if (_declaredCapabilities.Add(capability))
{
Emit(_capabilities, SpirvOp.Capability, (uint)capability);
}
}
public void AddExtension(string extension) =>
EmitWithString(_extensions, SpirvOp.Extension, [], extension);
public uint ImportExtInst(string name)
{
if (_extInstImports.TryGetValue(name, out var existing))
{
return existing;
}
var id = AllocateId();
EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name);
_extInstImports.Add(name, id);
return id;
}
public void SetLogicalGlsl450MemoryModel() =>
Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1);
public void AddEntryPoint(
SpirvExecutionModel model,
uint function,
string name,
IReadOnlyList<uint> interfaces)
{
var prefix = new uint[2 + interfaces.Count];
prefix[0] = (uint)model;
prefix[1] = function;
for (var index = 0; index < interfaces.Count; index++)
{
prefix[index + 2] = interfaces[index];
}
EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2);
}
public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = function;
values[1] = (uint)mode;
operands.CopyTo(values, 2);
Emit(_executionModes, SpirvOp.ExecutionMode, values);
}
public void AddName(uint target, string name) =>
EmitWithString(_debug, SpirvOp.Name, [target], name);
public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = target;
values[1] = (uint)decoration;
operands.CopyTo(values, 2);
Emit(_annotations, SpirvOp.Decorate, values);
}
public void AddMemberDecoration(
uint target,
uint member,
SpirvDecoration decoration,
params uint[] operands)
{
var values = new uint[3 + operands.Length];
values[0] = target;
values[1] = member;
values[2] = (uint)decoration;
operands.CopyTo(values, 3);
EmitRaw(_annotations, 72, values);
}
public uint TypeVoid()
{
if (_voidType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id);
_voidType = id;
return id;
}
public uint TypeBool()
{
if (_boolType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id);
_boolType = id;
return id;
}
public uint TypeInt(uint width, bool signed)
{
var key = (width, signed);
if (_integerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u);
_integerTypes.Add(key, id);
return id;
}
public uint TypeFloat(uint width)
{
if (_floatTypes.TryGetValue(width, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width);
_floatTypes.Add(width, id);
return id;
}
public uint TypeVector(uint componentType, uint count)
{
var key = (componentType, count);
if (_vectorTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count);
_vectorTypes.Add(key, id);
return id;
}
public uint TypeImage(
uint sampledType,
SpirvImageDim dimension,
bool depth,
bool arrayed,
bool multisampled,
uint sampled,
SpirvImageFormat format)
{
var key = (
sampledType,
dimension,
depth,
arrayed,
multisampled,
sampled,
format);
if (_imageTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.TypeImage,
id,
sampledType,
(uint)dimension,
depth ? 1u : 0u,
arrayed ? 1u : 0u,
multisampled ? 1u : 0u,
sampled,
(uint)format);
_imageTypes.Add(key, id);
return id;
}
public uint TypeSampledImage(uint imageType)
{
if (_sampledImageTypes.TryGetValue(imageType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType);
_sampledImageTypes.Add(imageType, id);
return id;
}
public uint TypeArray(uint elementType, uint count)
{
var key = (elementType, count);
if (_arrayTypes.TryGetValue(key, out var existing))
{
return existing;
}
var length = Constant(TypeInt(32, false), count);
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length);
_arrayTypes.Add(key, id);
return id;
}
public uint TypeRuntimeArray(uint elementType)
{
if (_runtimeArrayTypes.TryGetValue(elementType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType);
_runtimeArrayTypes.Add(elementType, id);
return id;
}
public uint TypeStruct(params uint[] memberTypes)
{
var id = AllocateId();
var operands = new uint[memberTypes.Length + 1];
operands[0] = id;
memberTypes.CopyTo(operands, 1);
Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands);
return id;
}
public uint TypePointer(SpirvStorageClass storageClass, uint type)
{
var key = (storageClass, type);
if (_pointerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type);
_pointerTypes.Add(key, id);
return id;
}
public uint TypeFunction(uint returnType, params uint[] parameterTypes)
{
var key = returnType + ":" + string.Join(',', parameterTypes);
if (_functionTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
var operands = new uint[parameterTypes.Length + 2];
operands[0] = id;
operands[1] = returnType;
parameterTypes.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands);
_functionTypes.Add(key, id);
return id;
}
public uint ConstantBool(bool value)
{
var type = TypeBool();
var key = (type, value ? 1UL : 0UL);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse,
type,
id);
_constants.Add(key, id);
return id;
}
public uint Constant(uint type, uint value)
{
var key = (type, (ulong)value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value);
_constants.Add(key, id);
return id;
}
public uint Constant64(uint type, ulong value)
{
var key = (type, value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.Constant,
type,
id,
(uint)value,
(uint)(value >> 32));
_constants.Add(key, id);
return id;
}
public uint ConstantFloat(uint type, float value) =>
Constant(type, BitConverter.SingleToUInt32Bits(value));
public uint ConstantComposite(uint type, params uint[] constituents)
{
var id = AllocateId();
var operands = new uint[constituents.Length + 2];
operands[0] = type;
operands[1] = id;
constituents.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands);
return id;
}
public uint ConstantNull(uint type)
{
var key = (type, ulong.MaxValue);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id);
_constants.Add(key, id);
return id;
}
public uint AddGlobalVariable(
uint pointerType,
SpirvStorageClass storageClass,
uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_typesConstantsGlobals,
SpirvOp.Variable,
pointerType,
id,
(uint)storageClass,
initializer.Value);
}
else
{
Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass);
}
return id;
}
public uint BeginFunction(uint returnType, uint functionType)
{
var id = AllocateId();
Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType);
return id;
}
public uint AddFunctionParameter(uint type) =>
EmitResult(_functions, SpirvOp.FunctionParameter, type);
public uint AddLabel(uint? id = null)
{
var result = id ?? AllocateId();
Emit(_functions, SpirvOp.Label, result);
return result;
}
public uint AddFunctionVariable(uint pointerType, uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function,
initializer.Value);
}
else
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function);
}
return id;
}
public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) =>
EmitResult(_functions, opcode, resultType, operands);
public void AddStatement(SpirvOp opcode, params uint[] operands) =>
Emit(_functions, opcode, operands);
public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd);
public byte[] Build()
{
if (_memoryModel.Count == 0)
{
SetLogicalGlsl450MemoryModel();
}
var wordCount =
5 +
_capabilities.Count +
_extensions.Count +
_imports.Count +
_memoryModel.Count +
_entryPoints.Count +
_executionModes.Count +
_debug.Count +
_annotations.Count +
_typesConstantsGlobals.Count +
_functions.Count;
var words = new uint[wordCount];
var offset = 0;
WriteWord(Magic);
WriteWord(Version15);
WriteWord(Generator);
WriteWord(_nextId);
WriteWord(0);
WriteSection(_capabilities);
WriteSection(_extensions);
WriteSection(_imports);
WriteSection(_memoryModel);
WriteSection(_entryPoints);
WriteSection(_executionModes);
WriteSection(_debug);
WriteSection(_annotations);
WriteSection(_typesConstantsGlobals);
WriteSection(_functions);
var bytes = new byte[wordCount * sizeof(uint)];
Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length);
return bytes;
void WriteWord(uint value)
{
words[offset++] = value;
}
void WriteSection(List<uint> section)
{
foreach (var value in section)
{
WriteWord(value);
}
}
}
private uint EmitResult(
List<uint> section,
SpirvOp opcode,
uint resultType,
params uint[] operands)
{
var result = AllocateId();
var values = new uint[operands.Length + 2];
values[0] = resultType;
values[1] = result;
operands.CopyTo(values, 2);
Emit(section, opcode, values);
return result;
}
private static void Emit(List<uint> section, SpirvOp opcode, params uint[] operands) =>
EmitRaw(section, (ushort)opcode, operands);
private static void EmitRaw(List<uint> section, ushort opcode, params uint[] operands)
{
section.Add(((uint)(operands.Length + 1) << 16) | opcode);
section.AddRange(operands);
}
private static void EmitWithString(
List<uint> section,
SpirvOp opcode,
IReadOnlyList<uint> prefix,
string value,
int stringBeforeTailCount = -1)
{
var encoded = EncodeString(value);
if (stringBeforeTailCount < 0)
{
var operands = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < prefix.Count; index++)
{
operands[index] = prefix[index];
}
encoded.CopyTo(operands, prefix.Count);
Emit(section, opcode, operands);
return;
}
var result = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < stringBeforeTailCount; index++)
{
result[index] = prefix[index];
}
encoded.CopyTo(result, stringBeforeTailCount);
for (var index = stringBeforeTailCount; index < prefix.Count; index++)
{
result[index + encoded.Length] = prefix[index];
}
Emit(section, opcode, result);
}
private static uint[] EncodeString(string value)
{
var byteCount = Encoding.UTF8.GetByteCount(value) + 1;
var words = new uint[(byteCount + 3) / 4];
Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan()));
return words;
}
}
+155 -36
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
@@ -23,8 +24,12 @@ 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 =
_traceAmpr ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
private sealed class CommandBufferState
{
@@ -33,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",
@@ -77,12 +99,7 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var buffer = 0UL;
var size = 0UL;
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -106,8 +123,9 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
auxiliaryPointers.Clear();
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -181,8 +199,15 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
if (
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -242,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;
@@ -476,20 +501,34 @@ public static class AmprExports
ulong aux1,
bool clear)
{
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
if (clear)
{
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
header.Clear();
if (!ctx.Memory.TryWrite(commandBuffer, header))
}
else
{
if (!ctx.Memory.TryRead(commandBuffer, header))
{
return false;
}
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
}
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
if (!ctx.Memory.TryWrite(commandBuffer, header))
{
return false;
}
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
return true;
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
@@ -504,6 +543,26 @@ public static class AmprExports
return false;
}
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
}
private static void UpdateCommandBufferState(
ulong commandBuffer,
ulong buffer,
ulong size,
ulong writeOffset)
{
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
@@ -511,15 +570,6 @@ public static class AmprExports
state.Size = size;
state.WriteOffset = writeOffset;
}
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
}
private static bool TryGetCommandBufferState(
@@ -540,9 +590,11 @@ public static class AmprExports
return true;
}
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
{
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
@@ -595,23 +647,48 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
const int ChunkSize = 64 * 1024;
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
const int ChunkSize = 1024 * 1024;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
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;
@@ -633,10 +710,52 @@ public static class AmprExports
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
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,
@@ -801,7 +920,7 @@ public static class AmprExports
string? hostPath,
int result)
{
if (!_traceAmpr)
if (!_traceAmprReads)
{
return;
}
+242
View File
@@ -0,0 +1,242 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AudioOutExports
{
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
private static int _nextPortHandle;
private sealed class PortState : IDisposable
{
private readonly object _paceGate = new();
private long _nextSilentOutput;
public PortState(
int userId,
int type,
uint bufferLength,
uint frequency,
int format,
int channels,
int bytesPerSample,
bool isFloat,
WinMmAudioPort? backend)
{
UserId = userId;
Type = type;
BufferLength = bufferLength;
Frequency = frequency;
Format = format;
Channels = channels;
BytesPerSample = bytesPerSample;
IsFloat = isFloat;
Backend = backend;
}
public int UserId { get; }
public int Type { get; }
public uint BufferLength { get; }
public uint Frequency { get; }
public int Format { get; }
public int Channels { get; }
public int BytesPerSample { get; }
public bool IsFloat { get; }
public WinMmAudioPort? Backend { get; }
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
public void PaceSilence()
{
long delay;
lock (_paceGate)
{
var now = Stopwatch.GetTimestamp();
if (_nextSilentOutput < now)
{
_nextSilentOutput = now;
}
delay = _nextSilentOutput - now;
_nextSilentOutput += checked(
(long)Math.Ceiling(
Stopwatch.Frequency * (double)BufferLength / Frequency));
}
if (delay > 0)
{
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
}
}
public void Dispose() => Backend?.Dispose();
}
[SysAbiExport(
Nid = "JfEPXVxhFqA",
ExportName = "sceAudioOutInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "ekNvsT22rsY",
ExportName = "sceAudioOutOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
var format = unchecked((int)ctx[CpuRegister.R9]);
if (bufferLength == 0 || frequency == 0 ||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
WinMmAudioPort? backend = null;
string backendName;
try
{
backend = new WinMmAudioPort(frequency);
backendName = "winmm";
}
catch (Exception exception)
{
backendName = "silent";
Console.Error.WriteLine(
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
}
var handle = Interlocked.Increment(ref _nextPortHandle);
Ports[handle] = new PortState(
userId,
type,
bufferLength,
frequency,
format,
channels,
bytesPerSample,
isFloat,
backend);
Console.Error.WriteLine(
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
$"{bufferLength} frames, backend={backendName}");
return SetReturn(ctx, handle);
}
[SysAbiExport(
Nid = "s1--uE9mBFw",
ExportName = "sceAudioOutClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Ports.TryRemove(handle, out var port))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
port.Dispose();
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "QOQtbeDqsT4",
ExportName = "sceAudioOutOutput",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutOutput(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var sourceAddress = ctx[CpuRegister.Rsi];
if (!Ports.TryGetValue(handle, out var port))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (sourceAddress == 0)
{
return SetReturn(ctx, 0);
}
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
try
{
var source = buffer.AsSpan(0, port.BufferByteLength);
if (!ctx.Memory.TryRead(sourceAddress, source))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (port.Backend is null ||
!port.Backend.Submit(
source,
port.BufferLength,
port.Channels,
port.BytesPerSample,
port.IsFloat))
{
port.PaceSilence();
}
return SetReturn(ctx, 0);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
[SysAbiExport(
Nid = "b+uAV89IlxE",
ExportName = "sceAudioOutSetVolume",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutSetVolume(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return SetReturn(
ctx,
Ports.ContainsKey(handle)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static bool TryGetFormat(
int rawFormat,
out int channels,
out int bytesPerSample,
out bool isFloat)
{
var format = rawFormat & 0xFF;
channels = format switch
{
0 or 3 => 1,
1 or 4 => 2,
2 or 5 or 6 or 7 => 8,
_ => 0,
};
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
isFloat = bytesPerSample == 4;
return channels != 0;
}
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Audio;
internal sealed class WinMmAudioPort : IDisposable
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WinMmAudioPort(uint sampleRate)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
}
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(
ReadOnlySpan<byte> source,
uint frames,
int channels,
int bytesPerSample,
bool isFloat)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
var outputLength = checked((int)frames * 4);
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)frames),
channels,
bytesPerSample,
isFloat);
return QueueBuffer(output.AsSpan(0, outputLength));
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static extern uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
private static extern uint WaveOutReset(IntPtr device);
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
private static extern uint WaveOutClose(IntPtr device);
}
@@ -0,0 +1,77 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
namespace SharpEmu.Libs.AvPlayer;
public static class AvPlayerExports
{
private const int InvalidParameters = unchecked((int)0x806A0001);
private static readonly object StateGate = new();
private static readonly HashSet<ulong> Players = new();
[SysAbiExport(
Nid = "aS66RI0gGgo",
ExportName = "sceAvPlayerInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerInit(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0 ||
!KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
lock (StateGate)
{
Players.Add(handle);
}
ctx[CpuRegister.Rax] = handle;
return unchecked((int)handle);
}
[SysAbiExport(
Nid = "HD1YKVU26-M",
ExportName = "sceAvPlayerPostInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerPostInit(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var dataAddress = ctx[CpuRegister.Rsi];
lock (StateGate)
{
return SetReturn(
ctx,
handle != 0 && dataAddress != 0 && Players.Contains(handle)
? 0
: InvalidParameters);
}
}
[SysAbiExport(
Nid = "NkJwDzKmIlw",
ExportName = "sceAvPlayerClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerClose(CpuContext ctx)
{
lock (StateGate)
{
return SetReturn(
ctx,
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
}
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,27 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
public static class MsgDialogExports
{
private const int AlreadyInitialized = unchecked((int)0x80B80002);
private static int _initialized;
[SysAbiExport(
Nid = "lDqxaY1UbEo",
ExportName = "sceMsgDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadyInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -276,6 +276,22 @@ public static class KernelEventQueueCompatExports
var outCountAddress = ctx[CpuRegister.Rcx];
var 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;
}
}
+52 -6
View File
@@ -204,6 +204,16 @@ public static class KernelExports
var nameAddress = ctx[CpuRegister.R8];
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
ctx,
attrAddress,
out var priority,
out var affinityMask);
KernelPthreadExtendedCompatExports.RegisterThreadStart(
threadHandle,
name,
priority,
affinityMask);
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -212,13 +222,22 @@ public static class KernelExports
if (ShouldTracePthread())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}");
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} " +
$"entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} " +
$"name='{name}' priority={priority} affinity=0x{affinityMask:X} -> thread=0x{threadHandle:X16}");
}
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is not null && entryAddress != 0)
{
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name);
var request = new GuestThreadStartRequest(
threadHandle,
entryAddress,
argument,
attrAddress,
name,
priority,
affinityMask);
if (!scheduler.TryStartThread(ctx, request, out var error))
{
Console.Error.WriteLine(
@@ -287,10 +306,6 @@ public static class KernelExports
{
var threadId = ctx[CpuRegister.Rdi];
var returnValueAddress = ctx[CpuRegister.Rsi];
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (ShouldTracePthread())
{
@@ -298,6 +313,30 @@ public static class KernelExports
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
}
var returnValue = 0UL;
if (GuestThreadExecution.Scheduler is { } scheduler &&
!scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}");
var result = string.Equals(
error,
"thread cannot join itself",
StringComparison.Ordinal)
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
if (returnValueAddress != 0 &&
!ctx.TryWriteUInt64(returnValueAddress, returnValue))
{
ctx[CpuRegister.Rax] =
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -326,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;
@@ -99,12 +99,16 @@ public static class KernelMemoryCompatExports
private static readonly object _tlsGate = new();
private static readonly object _ioTraceGate = new();
private static readonly object _statCacheGate = new();
private static readonly object _guestMountGate = new();
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;
@@ -153,6 +157,32 @@ public static class KernelMemoryCompatExports
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
public static void RegisterGuestPathMount(string guestMountPoint, string hostRoot)
{
ArgumentException.ThrowIfNullOrWhiteSpace(guestMountPoint);
ArgumentException.ThrowIfNullOrWhiteSpace(hostRoot);
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
if (normalizedMountPoint is null || normalizedMountPoint == "/")
{
throw new ArgumentException("Guest mount point must name a directory.", nameof(guestMountPoint));
}
var normalizedHostRoot = Path.GetFullPath(hostRoot);
Directory.CreateDirectory(normalizedHostRoot);
lock (_guestMountGate)
{
_guestMounts[normalizedMountPoint] = normalizedHostRoot;
}
lock (_statCacheGate)
{
_negativeStatCache.RemoveWhere(path =>
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
}
}
internal static bool TryAllocateHleData(
CpuContext ctx,
ulong length,
@@ -203,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",
@@ -602,9 +651,7 @@ public static class KernelMemoryCompatExports
}
else
{
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
rendered = FormatString(ctx, format, ref vaCursor);
}
Console.Write(rendered);
@@ -1296,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}");
@@ -1504,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;
@@ -2409,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",
@@ -2520,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;
@@ -2551,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)
{
@@ -2588,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",
@@ -2641,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",
@@ -2698,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);
@@ -2908,9 +3046,8 @@ public static class KernelMemoryCompatExports
var format = Encoding.UTF8.GetString(formatBytes);
var result = FormatString(ctx, format);
var outputBytes = Encoding.UTF8.GetBytes(result);
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
return WriteSnprintfOutput(ctx, destination, bufferSize, result);
}
private static int VsnprintfCore(CpuContext ctx)
@@ -2931,12 +3068,9 @@ public static class KernelMemoryCompatExports
return WriteSnprintfOutput(ctx, destination, bufferSize, formatBytes);
}
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
var rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
var rendered = FormatString(ctx, format, ref vaCursor);
var outputBytes = Encoding.UTF8.GetBytes(rendered);
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
return WriteSnprintfOutput(ctx, destination, bufferSize, rendered);
}
private static int SwprintfCore(CpuContext ctx)
@@ -2977,9 +3111,7 @@ public static class KernelMemoryCompatExports
else
{
TraceWidePrintfVaList(ctx, "vswprintf", format, vaListAddress, vaCursor);
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
rendered = FormatString(ctx, format, ref vaCursor);
}
TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered);
@@ -2994,10 +3126,8 @@ public static class KernelMemoryCompatExports
return false;
}
if (!TryReadUInt32Compat(ctx, vaListAddress + 0, out var gpOffset) ||
!TryReadUInt32Compat(ctx, vaListAddress + 4, out var fpOffset) ||
!TryReadUInt64Compat(ctx, vaListAddress + 8, out var overflowArgArea) ||
!TryReadUInt64Compat(ctx, vaListAddress + 16, out var regSaveArea))
Span<byte> vaList = stackalloc byte[24];
if (!TryReadCompat(ctx, vaListAddress, vaList))
{
return false;
}
@@ -3005,10 +3135,10 @@ public static class KernelMemoryCompatExports
cursor = new SysVAmd64VaListCursor(
ctx,
vaListAddress,
gpOffset,
fpOffset,
overflowArgArea,
regSaveArea);
BinaryPrimitives.ReadUInt32LittleEndian(vaList),
BinaryPrimitives.ReadUInt32LittleEndian(vaList[4..]),
BinaryPrimitives.ReadUInt64LittleEndian(vaList[8..]),
BinaryPrimitives.ReadUInt64LittleEndian(vaList[16..]));
return true;
}
@@ -3038,6 +3168,21 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int WriteSnprintfOutput(
CpuContext ctx,
ulong destination,
ulong bufferSize,
string output)
{
if (bufferSize == 0 || destination == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Encoding.UTF8.GetByteCount(output));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
return WriteSnprintfOutput(ctx, destination, bufferSize, Encoding.UTF8.GetBytes(output));
}
private static int WriteSwprintfOutput(
CpuContext ctx,
ulong destination,
@@ -3329,30 +3474,8 @@ public static class KernelMemoryCompatExports
internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex)
{
var gpIndex = Math.Max(0, firstGpArgIndex);
ulong GetGpArg(int index)
{
return index switch
{
0 => ctx[CpuRegister.Rdi],
1 => ctx[CpuRegister.Rsi],
2 => ctx[CpuRegister.Rdx],
3 => ctx[CpuRegister.Rcx],
4 => ctx[CpuRegister.R8],
5 => ctx[CpuRegister.R9],
_ => ReadStackArg(ctx, (ulong)(index - 6) * 8)
};
}
ulong NextGpArg() => GetGpArg(gpIndex++);
double NextFloatArg()
{
var rawBits = NextGpArg();
return BitConverter.Int64BitsToDouble(unchecked((long)rawBits));
}
return FormatString(ctx, format, NextGpArg, NextFloatArg);
var argumentSource = new RegisterPrintfArgumentSource(ctx, Math.Max(0, firstGpArgIndex));
return FormatString(ctx, format, ref argumentSource);
}
private static string FormatString(CpuContext ctx, string format)
@@ -3360,13 +3483,13 @@ public static class KernelMemoryCompatExports
return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3);
}
private static string FormatString(
private static string FormatString<TArgumentSource>(
CpuContext ctx,
string format,
Func<ulong> nextGpArg,
Func<double> nextFloatArg)
ref TArgumentSource argumentSource)
where TArgumentSource : struct, IPrintfArgumentSource
{
var sb = new StringBuilder();
var sb = new StringBuilder(format.Length + 32);
for (var i = 0; i < format.Length; i++)
{
@@ -3405,7 +3528,7 @@ public static class KernelMemoryCompatExports
var width = 0;
if (i < format.Length && format[i] == '*')
{
width = unchecked((int)nextGpArg());
width = unchecked((int)argumentSource.NextGpArg());
i++;
if (width < 0)
{
@@ -3428,7 +3551,7 @@ public static class KernelMemoryCompatExports
i++;
if (i < format.Length && format[i] == '*')
{
precision = unchecked((int)nextGpArg());
precision = unchecked((int)argumentSource.NextGpArg());
i++;
}
else if (i < format.Length && char.IsDigit(format[i]))
@@ -3482,14 +3605,14 @@ public static class KernelMemoryCompatExports
{
long value = lengthMod switch
{
"hh" => unchecked((sbyte)nextGpArg()),
"h" => unchecked((short)nextGpArg()),
"l" => unchecked((long)nextGpArg()),
"ll" => unchecked((long)nextGpArg()),
"j" => unchecked((long)nextGpArg()),
"z" => unchecked((long)nextGpArg()),
"t" => unchecked((long)nextGpArg()),
_ => unchecked((int)nextGpArg())
"hh" => unchecked((sbyte)argumentSource.NextGpArg()),
"h" => unchecked((short)argumentSource.NextGpArg()),
"l" => unchecked((long)argumentSource.NextGpArg()),
"ll" => unchecked((long)argumentSource.NextGpArg()),
"j" => unchecked((long)argumentSource.NextGpArg()),
"z" => unchecked((long)argumentSource.NextGpArg()),
"t" => unchecked((long)argumentSource.NextGpArg()),
_ => unchecked((int)argumentSource.NextGpArg())
};
var formatted = value.ToString();
@@ -3506,14 +3629,14 @@ public static class KernelMemoryCompatExports
{
ulong value = lengthMod switch
{
"hh" => (byte)nextGpArg(),
"h" => (ushort)nextGpArg(),
"l" => nextGpArg(),
"ll" => nextGpArg(),
"j" => nextGpArg(),
"z" => nextGpArg(),
"t" => nextGpArg(),
_ => (uint)nextGpArg()
"hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)argumentSource.NextGpArg(),
"l" => argumentSource.NextGpArg(),
"ll" => argumentSource.NextGpArg(),
"j" => argumentSource.NextGpArg(),
"z" => argumentSource.NextGpArg(),
"t" => argumentSource.NextGpArg(),
_ => (uint)argumentSource.NextGpArg()
};
var formatted = value.ToString();
@@ -3526,14 +3649,14 @@ public static class KernelMemoryCompatExports
{
ulong value = lengthMod switch
{
"hh" => (byte)nextGpArg(),
"h" => (ushort)nextGpArg(),
"l" => nextGpArg(),
"ll" => nextGpArg(),
"j" => nextGpArg(),
"z" => nextGpArg(),
"t" => nextGpArg(),
_ => (uint)nextGpArg()
"hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)argumentSource.NextGpArg(),
"l" => argumentSource.NextGpArg(),
"ll" => argumentSource.NextGpArg(),
"j" => argumentSource.NextGpArg(),
"z" => argumentSource.NextGpArg(),
"t" => argumentSource.NextGpArg(),
_ => (uint)argumentSource.NextGpArg()
};
var formatted = specifier == 'x'
@@ -3551,14 +3674,14 @@ public static class KernelMemoryCompatExports
{
ulong value = lengthMod switch
{
"hh" => (byte)nextGpArg(),
"h" => (ushort)nextGpArg(),
"l" => nextGpArg(),
"ll" => nextGpArg(),
"j" => nextGpArg(),
"z" => nextGpArg(),
"t" => nextGpArg(),
_ => (uint)nextGpArg()
"hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)argumentSource.NextGpArg(),
"l" => argumentSource.NextGpArg(),
"ll" => argumentSource.NextGpArg(),
"j" => argumentSource.NextGpArg(),
"z" => argumentSource.NextGpArg(),
"t" => argumentSource.NextGpArg(),
_ => (uint)argumentSource.NextGpArg()
};
var formatted = Convert.ToString((long)value, 8);
@@ -3571,7 +3694,7 @@ public static class KernelMemoryCompatExports
case 'p':
{
var value = nextGpArg();
var value = argumentSource.NextGpArg();
var formatted = value == 0
? "(nil)"
: $"0x{value:X}";
@@ -3581,7 +3704,7 @@ public static class KernelMemoryCompatExports
case 's':
{
var strAddr = nextGpArg();
var strAddr = argumentSource.NextGpArg();
TracePrintfStringArgument(ctx, lengthMod, strAddr);
if (strAddr == 0)
{
@@ -3620,14 +3743,14 @@ public static class KernelMemoryCompatExports
string renderedChar;
if (lengthMod == "l")
{
var scalar = unchecked((ushort)nextGpArg());
var scalar = unchecked((ushort)argumentSource.NextGpArg());
renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText)
? wideCharText
: "?";
}
else
{
renderedChar = ((char)(byte)nextGpArg()).ToString();
renderedChar = ((char)(byte)argumentSource.NextGpArg()).ToString();
}
sb.Append(PadString(renderedChar, width, leftAlign, false));
@@ -3641,7 +3764,7 @@ public static class KernelMemoryCompatExports
case 'g':
case 'G':
{
var value = nextFloatArg();
var value = argumentSource.NextFloatArg();
var formatStr = precision >= 0
? $"{{0:{specifier}{precision}}}"
@@ -3659,7 +3782,7 @@ public static class KernelMemoryCompatExports
case 'n':
{
var addr = nextGpArg();
var addr = argumentSource.NextGpArg();
if (addr != 0)
{
_ = TryWriteInt32(ctx, addr, sb.Length);
@@ -3699,7 +3822,46 @@ public static class KernelMemoryCompatExports
return leftAlign ? str + padding : padding + str;
}
private struct SysVAmd64VaListCursor
private interface IPrintfArgumentSource
{
ulong NextGpArg();
double NextFloatArg();
}
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
{
private readonly CpuContext _ctx;
private int _gpIndex;
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
{
_ctx = ctx;
_gpIndex = gpIndex;
}
public ulong NextGpArg()
{
var index = _gpIndex++;
return index switch
{
0 => _ctx[CpuRegister.Rdi],
1 => _ctx[CpuRegister.Rsi],
2 => _ctx[CpuRegister.Rdx],
3 => _ctx[CpuRegister.Rcx],
4 => _ctx[CpuRegister.R8],
5 => _ctx[CpuRegister.R9],
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
};
}
public double NextFloatArg()
{
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
}
}
private struct SysVAmd64VaListCursor : IPrintfArgumentSource
{
private const uint GpSaveAreaLimit = 48;
private const uint FpSaveAreaLimit = 176;
@@ -3820,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(
@@ -4011,6 +4072,11 @@ public static class KernelMemoryCompatExports
return guestPath;
}
if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath))
{
return mountedPath;
}
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
@@ -4079,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))
{
@@ -4109,6 +4189,51 @@ public static class KernelMemoryCompatExports
return guestPath;
}
private static bool TryResolveRegisteredGuestMount(string guestPath, out string hostPath)
{
hostPath = string.Empty;
var normalizedGuestPath = NormalizeGuestStatCachePath(guestPath);
if (normalizedGuestPath is null)
{
return false;
}
string? matchedMountPoint = null;
string? matchedHostRoot = null;
lock (_guestMountGate)
{
foreach (var (mountPoint, hostRoot) in _guestMounts)
{
if ((string.Equals(normalizedGuestPath, mountPoint, StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith(mountPoint + "/", StringComparison.OrdinalIgnoreCase)) &&
(matchedMountPoint is null || mountPoint.Length > matchedMountPoint.Length))
{
matchedMountPoint = mountPoint;
matchedHostRoot = hostRoot;
}
}
}
if (matchedMountPoint is null || matchedHostRoot is null)
{
return false;
}
var relativePath = normalizedGuestPath[matchedMountPoint.Length..].TrimStart('/');
var candidate = Path.GetFullPath(Path.Combine(
matchedHostRoot,
NormalizeMountRelativePath(relativePath)));
var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar;
if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) &&
!candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
{
return false;
}
hostPath = candidate;
return true;
}
private static string? ResolveApp0Root()
{
var cached = Volatile.Read(ref _cachedApp0Root);
@@ -4297,9 +4422,32 @@ public static class KernelMemoryCompatExports
}
const int maxChunkSize = 4096;
const int inlineChunkSize = 256;
Span<byte> inlineChunk = stackalloc byte[inlineChunkSize];
var firstPageRemaining = maxChunkSize - (int)(address & (maxChunkSize - 1));
var firstReadLength = Math.Min(limit, Math.Min(inlineChunkSize, firstPageRemaining));
var firstSpan = inlineChunk[..firstReadLength];
ulong offset = 0;
if (TryReadCompat(ctx, address, firstSpan))
{
var nulIndex = firstSpan.IndexOf((byte)0);
if (nulIndex >= 0)
{
bytes = firstSpan[..nulIndex].ToArray();
return true;
}
offset = unchecked((ulong)firstReadLength);
}
var chunk = GC.AllocateUninitializedArray<byte>(Math.Min(maxChunkSize, limit));
var writer = new ArrayBufferWriter<byte>(Math.Min(limit, 256));
ulong offset = 0;
if (offset != 0)
{
firstSpan.CopyTo(writer.GetSpan(firstReadLength));
writer.Advance(firstReadLength);
}
while (offset < (ulong)limit)
{
var current = address + offset;
@@ -5265,6 +5413,37 @@ public static class KernelMemoryCompatExports
}
}
internal static bool TryReadTrackedLibcHeap(
ulong address,
Span<byte> destination)
{
if (destination.IsEmpty)
{
return true;
}
var length = (ulong)destination.Length;
lock (_libcAllocGate)
{
foreach (var (allocationAddress, allocation) in _libcAllocations)
{
var allocationSize = (ulong)allocation.Size;
var offset = address >= allocationAddress
? address - allocationAddress
: ulong.MaxValue;
if (offset > allocationSize ||
length > allocationSize - offset)
{
continue;
}
return TryReadHostMemory(address, destination);
}
}
return false;
}
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
{
address = 0;
@@ -5642,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
@@ -5956,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)
@@ -11,7 +11,6 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadCompatExports
{
private const int MutexTypeDefault = 0;
private const int MutexTypeErrorCheck = 1;
private const int MutexTypeRecursive = 2;
private const int MutexTypeNormal = 3;
@@ -44,7 +43,7 @@ public static class KernelPthreadCompatExports
public SemaphoreSlim Semaphore { get; } = new(1, 1);
public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeDefault;
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
}
@@ -96,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",
@@ -541,7 +547,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.Type is MutexTypeDefault or MutexTypeNormal or MutexTypeAdaptiveNp)
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
{
if (tryOnly)
{
@@ -676,7 +682,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var initialState = new PthreadMutexAttrState(MutexTypeDefault, 0);
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
if (!WriteMutexAttrObject(ctx, handle, initialState))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -735,7 +741,7 @@ public static class KernelPthreadCompatExports
{
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
}
updatedState = state with { Type = NormalizeMutexType(type) };
@@ -764,7 +770,7 @@ public static class KernelPthreadCompatExports
{
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
}
updatedState = state with { Protocol = protocol };
@@ -847,7 +853,7 @@ public static class KernelPthreadCompatExports
return false;
}
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state);
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeErrorCheck, out resolvedAddress, out state);
}
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
@@ -891,7 +897,7 @@ public static class KernelPthreadCompatExports
{
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
? state
: new PthreadMutexAttrState(MutexTypeDefault, 0);
: new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
}
}
@@ -1293,12 +1299,12 @@ public static class KernelPthreadCompatExports
{
return type switch
{
0 => MutexTypeDefault,
0 => MutexTypeErrorCheck,
1 => MutexTypeErrorCheck,
2 => MutexTypeRecursive,
3 => MutexTypeNormal,
4 => MutexTypeAdaptiveNp,
_ => MutexTypeDefault,
_ => MutexTypeErrorCheck,
};
}
@@ -13,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadExtendedCompatExports
{
private const int DefaultThreadPriority = 700;
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
private const ulong DefaultThreadAffinityMask = 0x7FUL;
private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL;
private const int DefaultInheritSched = 0;
private const int DefaultSchedPolicy = 0;
private const int DefaultSchedPriority = 0;
private const int DefaultInheritSched = 4;
private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = DefaultThreadPriority;
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
@@ -34,6 +34,48 @@ public static class KernelPthreadExtendedCompatExports
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
internal static void GetThreadStartScheduling(
CpuContext ctx,
ulong attrAddress,
out int priority,
out ulong affinityMask)
{
if (attrAddress == 0)
{
priority = DefaultThreadPriority;
affinityMask = DefaultThreadAffinityMask;
return;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
priority = attributes.SchedPriority;
affinityMask = attributes.AffinityMask;
}
}
internal static void RegisterThreadStart(
ulong thread,
string name,
int priority,
ulong affinityMask)
{
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.Name = name;
state.Priority = priority;
state.AffinityMask = affinityMask;
state.Attributes = state.Attributes with
{
SchedPriority = priority,
AffinityMask = affinityMask,
};
}
}
private sealed class ThreadState
{
public string Name { get; set; } = string.Empty;
@@ -675,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);
}
+322
View File
@@ -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;
}
}
@@ -0,0 +1,76 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
public static class NpEntitlementAccessExports
{
private const int BootParamClearSize = 0x20;
private const int EmptyAddcontInfoListSize = 0x10;
[SysAbiExport(
Nid = "jO8DM8oyego",
ExportName = "sceNpEntitlementAccessInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessInitialize(CpuContext ctx)
{
var initParam = ctx[CpuRegister.Rdi];
var bootParam = ctx[CpuRegister.Rsi];
if (bootParam != 0)
{
Span<byte> clear = stackalloc byte[BootParamClearSize];
clear.Clear();
if (!ctx.Memory.TryWrite(bootParam, clear))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "TFyU+KFBv54",
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessGetAddcontEntitlementInfoList(CpuContext ctx)
{
var listAddress = ctx[CpuRegister.Rsi];
if (listAddress != 0)
{
Span<byte> emptyList = stackalloc byte[EmptyAddcontInfoListSize];
emptyList.Clear();
if (!ctx.Memory.TryWrite(listAddress, emptyList))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess(
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceNpEntitlementAccess(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] np.entitlement.{message}");
}
}
+79 -4
View File
@@ -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;
}
}
+43
View File
@@ -29,6 +29,7 @@ public static class PlayGoExports
private const int PlayGoInstallSpeedTrickle = 1;
private const int PlayGoInstallSpeedFull = 2;
private const uint MaxPlayGoQueryEntries = 0x4000;
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
private static readonly Regex ChunkIdPattern = new(
@"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""",
@@ -45,6 +46,7 @@ public static class PlayGoExports
private static int _installSpeed = PlayGoInstallSpeedTrickle;
private static ulong _languageMask = ulong.MaxValue;
private static int _unknownChunkDiagnostics;
private static int _locusTraceDiagnostics;
[SysAbiExport(
Nid = "ts6GlZOKRrE",
@@ -94,6 +96,7 @@ public static class PlayGoExports
_languageMask = ulong.MaxValue;
_opened = false;
_initialized = true;
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -131,6 +134,7 @@ public static class PlayGoExports
}
_opened = true;
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
}
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
@@ -229,6 +233,7 @@ public static class PlayGoExports
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
if (outChunkIdList == 0)
{
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
return TryWriteUInt32(ctx, outEntries, availableEntries)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -237,6 +242,7 @@ public static class PlayGoExports
var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
if (entriesToWrite > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_chunk_id bad_size requested={numberOfEntries} available={availableEntries}");
return OrbisPlayGoErrorBadSize;
}
@@ -249,6 +255,7 @@ public static class PlayGoExports
}
}
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
return TryWriteUInt32(ctx, outEntries, entriesToWrite)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -279,6 +286,7 @@ public static class PlayGoExports
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_eta bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outEta:X16}");
return OrbisPlayGoErrorBadSize;
}
@@ -376,8 +384,15 @@ public static class PlayGoExports
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == PlayGoAllEntriesSentinel)
{
TracePlayGo($"get_locus sentinel entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_locus bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
return OrbisPlayGoErrorBadSize;
}
@@ -408,6 +423,7 @@ public static class PlayGoExports
loci[i] = PlayGoLocusLocalFast;
}
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
return ctx.Memory.TryWrite(outLoci, loci)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -438,6 +454,7 @@ public static class PlayGoExports
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_progress bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outProgress:X16}");
return OrbisPlayGoErrorBadSize;
}
@@ -447,6 +464,7 @@ public static class PlayGoExports
return chunkError;
}
TracePlayGo($"get_progress entries={numberOfEntries}");
Span<byte> progress = stackalloc byte[sizeof(ulong) * 2];
BinaryPrimitives.WriteUInt64LittleEndian(progress, 0);
BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0);
@@ -480,9 +498,11 @@ public static class PlayGoExports
if (numberOfEntries == 0)
{
TracePlayGo("get_todo bad_size entries=0");
return OrbisPlayGoErrorBadSize;
}
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
return TryWriteUInt32(ctx, outEntries, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -749,6 +769,29 @@ public static class PlayGoExports
return ctx.Memory.TryWrite(address, buffer);
}
private static void TracePlayGo(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] playgo.{message}");
}
}
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
{
return;
}
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
if (entries != 1 || count <= 32 || count % 1000 == 0)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
}
}
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
{
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
@@ -0,0 +1,213 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.SaveData;
public static class SaveDataDialogExports
{
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusRunning = 2;
private const int StatusFinished = 3;
private const int ErrorOk = 0;
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
private const int ErrorAlreadyInitialized = unchecked((int)0x80B80004);
private const int ErrorNotFinished = unchecked((int)0x80B80005);
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
private const int ErrorArgNull = unchecked((int)0x80B8000D);
private const int ResultSize = 0x48;
private static int _status;
private static int _lastMode;
private static ulong _lastUserData;
[SysAbiExport(
Nid = "s9e3+YpRnzw",
ExportName = "sceSaveDataDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogInitialize(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
{
return SetReturn(ctx, ErrorAlreadyInitialized);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "4tPhsP6FpDI",
ExportName = "sceSaveDataDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogOpen(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return SetReturn(ctx, ErrorArgNull);
}
if (_status is not (StatusInitialized or StatusFinished))
{
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 0;
// There is no host save dialog yet. Complete immediately with OK so
// guest polling sees a finished dialog instead of spinning forever.
Interlocked.Exchange(ref _status, StatusFinished);
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "ERKzksauAJA",
ExportName = "sceSaveDataDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "KK3Bdg1RWK0",
ExportName = "sceSaveDataDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "en7gNVnh878",
ExportName = "sceSaveDataDialogIsReadyToDisplay",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
[SysAbiExport(
Nid = "yEiJ-qqr6Cg",
ExportName = "sceSaveDataDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetResult(CpuContext ctx)
{
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, ErrorArgNull);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return SetReturn(ctx, ErrorNotFinished);
}
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "fH46Lag88XY",
ExportName = "sceSaveDataDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogClose(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return SetReturn(ctx, ErrorNotRunning);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "YuH2FA7azqQ",
ExportName = "sceSaveDataDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogTerminate(CpuContext ctx)
{
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = 0;
_lastUserData = 0;
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "V-uEeFKARJU",
ExportName = "sceSaveDataDialogProgressBarInc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
[SysAbiExport(
Nid = "hay1CfTmLyA",
ExportName = "sceSaveDataDialogProgressBarSetValue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
value = 0;
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
value = 0;
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveDataDialog(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] save_data_dialog.{message}");
}
}
@@ -0,0 +1,569 @@
// 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.Text;
namespace SharpEmu.Libs.SaveData;
public static class SaveDataExports
{
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
private const int SaveDataTitleIdSize = 10;
private const int SaveDataDirNameSize = 32;
private const int SaveDataParamSize = 0x530;
private const int SaveDataSearchInfoSize = 0x30;
private const ulong ResultHitNumOffset = 0x00;
private const ulong ResultDirNamesOffset = 0x08;
private const ulong ResultDirNamesNumOffset = 0x10;
private const ulong ResultSetNumOffset = 0x14;
private const ulong ResultParamsOffset = 0x18;
private const ulong ResultInfosOffset = 0x20;
private const uint SortKeyFreeBlocks = 5;
private const uint SortOrderDescent = 1;
private const uint MountModeCreate = 1u << 2;
private const uint MountModeCreate2 = 1u << 5;
private const int MountResultSize = 0x40;
private static readonly object _stateGate = new();
private static string? _titleId;
public static void ConfigureApplicationInfo(string? titleId)
{
lock (_stateGate)
{
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
}
}
[SysAbiExport(
Nid = "TywrFKCoLGY",
ExportName = "sceSaveDataInitialize3",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataInitialize3(CpuContext ctx)
{
try
{
Directory.CreateDirectory(ResolveSaveDataRoot());
return SetReturn(ctx, 0);
}
catch (IOException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
[SysAbiExport(
Nid = "dyIhnXq-0SM",
ExportName = "sceSaveDataDirNameSearch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataDirNameSearch(CpuContext ctx)
{
var condAddress = ctx[CpuRegister.Rdi];
var resultAddress = ctx[CpuRegister.Rsi];
if (condAddress == 0 || resultAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
!TryReadSearchResult(ctx, resultAddress, out var result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
{
string titleId;
if (cond.TitleIdAddress == 0)
{
titleId = ResolveConfiguredTitleId();
}
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
var entries = Directory.Exists(root)
? EnumerateSaveDirectories(root, cond.Pattern)
: [];
entries = SortEntries(entries, cond.SortKey, cond.SortOrder);
var setNum = result.DirNamesNum == 0
? 0
: Math.Min(result.DirNamesNum, entries.Count);
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (setNum == 0)
{
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
return SetReturn(ctx, 0);
}
if (result.DirNamesAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
for (var i = 0; i < setNum; i++)
{
var entry = entries[i];
if (!TryWriteFixedAscii(
ctx,
result.DirNamesAddress + ((ulong)i * SaveDataDirNameSize),
SaveDataDirNameSize,
entry.Name) ||
(result.ParamsAddress != 0 &&
!TryWriteParam(ctx, result.ParamsAddress + ((ulong)i * SaveDataParamSize), entry)) ||
(result.InfosAddress != 0 &&
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
return SetReturn(ctx, 0);
}
catch (IOException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
[SysAbiExport(
Nid = "ZP4e7rlzOUk",
ExportName = "sceSaveDataMount3",
Target = Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataMount3(CpuContext ctx)
{
var mountAddress = ctx[CpuRegister.Rdi];
var resultAddress = ctx[CpuRegister.Rsi];
if (mountAddress == 0 || resultAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadInt32(ctx, mountAddress, out var userId) ||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
dirNameAddress == 0 ||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
{
var titleId = ResolveConfiguredTitleId();
var savePath = Path.Combine(
ResolveTitleSaveRoot(userId, titleId),
SanitizePathSegment(dirName));
var existed = Directory.Exists(savePath);
var create = (mountMode & MountModeCreate) != 0;
var createIfMissing = (mountMode & MountModeCreate2) != 0;
if (!existed && !create && !createIfMissing)
{
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
}
if (existed && create)
{
return SetReturn(ctx, OrbisSaveDataErrorExists);
}
if (!existed)
{
Directory.CreateDirectory(savePath);
}
const string mountPoint = "/savedata0";
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath);
Span<byte> result = stackalloc byte[MountResultSize];
result.Clear();
WriteAscii(result[..16], mountPoint);
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSaveData(
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
return SetReturn(ctx, 0);
}
catch (IOException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (ArgumentException)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
}
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;
if (!TryReadInt32(ctx, address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
{
return false;
}
string pattern;
if (dirNameAddress == 0)
{
pattern = string.Empty;
}
else if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out pattern))
{
return false;
}
cond = new SearchCond(userId, titleIdAddress, pattern, sortKey, sortOrder);
return true;
}
private static bool TryReadSearchResult(CpuContext ctx, ulong address, out SearchResult result)
{
result = default;
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
{
return false;
}
result = new SearchResult(dirNamesAddress, dirNamesNum, paramsAddress, infosAddress);
return true;
}
private static List<SaveEntry> EnumerateSaveDirectories(string root, string pattern)
{
var entries = new List<SaveEntry>();
foreach (var directory in Directory.EnumerateDirectories(root))
{
var name = Path.GetFileName(directory);
if (string.IsNullOrWhiteSpace(name) ||
name.StartsWith("sce_", StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(pattern) && !MatchPattern(name, pattern)))
{
continue;
}
var info = new DirectoryInfo(directory);
entries.Add(new SaveEntry(name, directory, info.LastWriteTimeUtc));
}
return entries;
}
private static List<SaveEntry> SortEntries(List<SaveEntry> entries, uint sortKey, uint sortOrder)
{
IOrderedEnumerable<SaveEntry> sorted = sortKey switch
{
3 => entries.OrderBy(entry => entry.LastWriteUtc),
_ => entries.OrderBy(entry => entry.Name, StringComparer.OrdinalIgnoreCase),
};
var list = sorted.ToList();
if (sortOrder == SortOrderDescent)
{
list.Reverse();
}
return list;
}
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
{
var param = new byte[SaveDataParamSize];
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
BinaryPrimitives.WriteInt64LittleEndian(
param.AsSpan(0x508, sizeof(long)),
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
return ctx.Memory.TryWrite(address, param);
}
private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry)
{
var size = GetDirectorySize(entry.Path);
var usedBlocks = checked((ulong)((size + 32767) / 32768));
var blocks = Math.Max(96UL, usedBlocks);
Span<byte> info = stackalloc byte[SaveDataSearchInfoSize];
info.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks);
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], blocks - usedBlocks);
return ctx.Memory.TryWrite(address, info);
}
private static long GetDirectorySize(string root)
{
long total = 0;
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
total += new FileInfo(file).Length;
}
return total;
}
private static bool MatchPattern(string value, string pattern) =>
MatchPattern(value.AsSpan(), pattern.AsSpan());
private static bool MatchPattern(ReadOnlySpan<char> value, ReadOnlySpan<char> pattern)
{
if (pattern.IsEmpty)
{
return value.IsEmpty;
}
if (pattern[0] == '%')
{
for (var i = 0; i <= value.Length; i++)
{
if (MatchPattern(value[i..], pattern[1..]))
{
return true;
}
}
return false;
}
if (value.IsEmpty)
{
return false;
}
if (pattern[0] == '_' ||
char.ToUpperInvariant(pattern[0]) == char.ToUpperInvariant(value[0]))
{
return MatchPattern(value[1..], pattern[1..]);
}
return false;
}
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
private static string ResolveSaveDataRoot()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
var root = string.IsNullOrWhiteSpace(configured)
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
: configured;
return Path.GetFullPath(root);
}
private static string ResolveConfiguredTitleId()
{
lock (_stateGate)
{
if (!string.IsNullOrWhiteSpace(_titleId))
{
return _titleId;
}
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var app0Name = string.IsNullOrWhiteSpace(app0Root)
? null
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (!string.IsNullOrWhiteSpace(app0Name))
{
var candidate = app0Name.Split('-', StringSplitOptions.RemoveEmptyEntries)[0];
if (!string.IsNullOrWhiteSpace(candidate))
{
return SanitizePathSegment(candidate);
}
}
return "default";
}
private static string SanitizePathSegment(string value)
{
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
}
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
{
value = string.Empty;
Span<byte> buffer = stackalloc byte[length];
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var stringLength = buffer.IndexOf((byte)0);
if (stringLength < 0)
{
stringLength = buffer.Length;
}
value = Encoding.ASCII.GetString(buffer[..stringLength]);
return true;
}
private static bool TryWriteFixedAscii(CpuContext ctx, ulong address, int length, string value)
{
Span<byte> buffer = stackalloc byte[length];
buffer.Clear();
WriteAscii(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static void WriteAscii(Span<byte> destination, string value)
{
var count = Math.Min(value.Length, Math.Max(0, destination.Length - 1));
for (var i = 0; i < count; i++)
{
var ch = value[i];
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
}
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveData(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] savedata.{message}");
}
}
private readonly record struct SearchCond(
int UserId,
ulong TitleIdAddress,
string Pattern,
uint SortKey,
uint SortOrder);
private readonly record struct SearchResult(
ulong DirNamesAddress,
uint DirNamesNum,
ulong ParamsAddress,
ulong InfosAddress);
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
}
+1
View File
@@ -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>
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
namespace SharpEmu.Libs.SystemService;
@@ -85,6 +86,17 @@ public static class SystemServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "Vo5V8KAwCmk",
ExportName = "sceSystemServiceHideSplashScreen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceHideSplashScreen(CpuContext ctx)
{
VulkanVideoPresenter.HideSplashScreen();
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
+82 -3
View File
@@ -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)
{
@@ -360,7 +368,24 @@ public static class VideoOutExports
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
}
[SysAbiExport(
Nid = "zgXifHT9ErY",
ExportName = "sceVideoOutIsFlipPending",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutIsFlipPending(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!TryGetPort(handle, out _))
{
return OrbisVideoOutErrorInvalidHandle;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -423,7 +448,7 @@ public static class VideoOutExports
}
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: false);
internal static void SubmitHostRgbaFrame(ReadOnlySpan<byte> rgbaFrame, uint width, uint height)
{
@@ -731,7 +756,13 @@ public static class VideoOutExports
return groupIndex < 0 ? groupIndex : setIndex;
}
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
private static int SubmitFlip(
CpuContext ctx,
int handle,
int bufferIndex,
int flipMode,
long flipArg,
bool submitGpuImage)
{
if (!TryGetPort(handle, out var port))
{
@@ -759,6 +790,17 @@ public static class VideoOutExports
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
}
if (submitGpuImage &&
bufferIndex >= 0 &&
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
_ = VulkanVideoPresenter.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
displayBuffer.PitchInPixel);
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
@@ -778,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
+10
View File
@@ -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, )",