Compare commits

..

111 Commits

Author SHA1 Message Date
Dawid 7337683c16 [fixes] stackalloc warnings, consolidate duplicated methods, minor adjustments in project settings (#39)
* [fixes] stackalloc warnings, consolidate duplicated methods

* [fix] remove unnecessary edit in .slnx file
2026-07-10 20:57:46 +03:00
ParantezTech b36ecc121c [readme] added GUI 2026-07-10 19:23:04 +03:00
Brando 42f0fa0e83 [gui] Add Avalonia desktop frontend (#35)
* [gui] Add Avalonia desktop frontend

Adds SharpEmu.GUI, a dark-themed desktop frontend that drives the
SharpEmu CLI as a child process:

- Game library with folder scanning for eboot.bin, search, and
  persisted settings (%APPDATA%/SharpEmu/gui-settings.json)
- Launch options mapped to CLI flags (log level, strict dynlib
  resolution, import trace limit)
- Live console with severity color-coding, bounded buffer, and
  crash-safe deferred auto-scroll
- EmulatorProcess launches the CLI via CreateProcessW with the same
  CET/CFG mitigation opt-outs the CLI applies to its own relaunched
  child (suppressed via SHARPEMU_DISABLE_MITIGATION_RELAUNCH so
  output is not lost to a detached console), inheritable pipes for
  stdout/stderr capture, a kill-on-close job object, and a fallback
  to an unmitigated launch on Windows builds that reject the policy
  bits

Also pins Tmds.DBus.Protocol 0.21.3 (transitive of Avalonia.Desktop)
to fix GHSA-xrw6-gwf8-vvr9.


* [gui] Integrate GUI into the SharpEmu executable

Per review feedback, the GUI is no longer a separate application.
SharpEmu.exe now opens the desktop frontend when started without
arguments and behaves exactly as the existing CLI when given any
argument:

- SharpEmu.GUI becomes a class library exposing GuiLauncher.Run(),
  hosted by SharpEmu.CLI
- The console window is hidden in GUI mode only when the process is
  its sole owner (double-click launch), never a terminal the user
  launched from
- In GUI mode the frontend spawns this same executable (with
  arguments) as the emulator child process, so the existing launch,
  piping, and mitigation machinery is unchanged


* [gui] Address review feedback: no console, single-file, param.json, portable settings

- Switch SharpEmu.exe to the GUI subsystem so no console window appears
  at startup. CLI mode attaches to the parent terminal console (or
  allocates one when started with arguments but no terminal) and
  rebinds missing std handles to CONOUT$; piped/redirected output is
  used as-is, so scripted and GUI-spawned runs are unaffected.
- Publish as a true single file: native libraries are embedded and
  self-extracted, with glfw kept as the only loose DLL next to the
  executable.
- The game library reads sce_sys/param.json and shows the game title
  with the title id beneath it, falling back to the folder name.
- GUI settings and the crash log now live next to the executable,
  matching the emulator convention, instead of %APPDATA%.


* [build] Add win-x64 sections to package lock files

Generated by dotnet publish -r win-x64 with locked restore enabled.


* [build] Regenerate lock files from project configuration

dotnet restore --force-evaluate; removes the win-x64 runtime sections
that a local RID-specific publish had written into projects that do
not declare a runtime identifier, which broke locked-mode restore in
CI. Verified with dotnet restore -p:RestoreLockedMode=true.


---------
2026-07-10 19:21:24 +03:00
Berk 41d61bde41 [memory] Host memory allocation issue fixed (#34) 2026-07-10 15:07:46 +03:00
Berk ac96fe5292 [sceAudio] added basic audio output support (#33) 2026-07-10 13:16:15 +03:00
Berk 9b9ca8f707 [loader] cut import overhead (#32) 2026-07-10 01:14:17 +03:00
Berk db20387502 [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 44c43c8ebd [kernel] Added KernelExceptionCompatExports and fstat, pthread_equal (#30) 2026-07-07 21:05:54 +03:00
Foued Attar 4b5b27e686 [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 5c823d0e8b Merge remote-tracking branch 'refs/remotes/origin/main' 2026-07-06 09:53:04 +03:00
ParantezTech 1679897089 update readme 2026-07-06 09:52:46 +03:00
Berk ce8f75411a [unity-fixes] support for Media/Modules/*.prx files (#27) 2026-07-05 22:10:37 +03:00
Berk f75da92fa5 [agc] new agc exports for agc improvements (sceAgcDcbDrawIndex, sceAgcDriverGetResourceRegistrationMaxNameLength, sceAgcDriverGetDefaultOwner, sceAgcDriverRegisterResource, sceAgcDriverUnknown_KRzWekV120(?)) (#26) 2026-07-05 22:10:15 +03:00
Berk 9eaaac7cee [ngs2 implements] Implement NGS2 exports (#25) 2026-07-05 22:09:50 +03:00
Berk d446779d34 [pad improvements] Added support for scePadSetVibrationMode (#24) 2026-07-05 22:09:36 +03:00
Berk 585d11b93b [sceAudio] added simple audio output implementation, needs to be improved (#23) 2026-07-05 22:09:25 +03:00
Berk 17f9535f0e [sceMsgDialog] implemented sceMsgDialogInitialize (#22) 2026-07-05 22:09:14 +03:00
Berk 8bfbb9c7fe Avplayer implements (#21) 2026-07-05 22:09:04 +03:00
Berk 2b8fe71a82 [savedata-improvements-2] added sceSaveDataCreateTransactionResource (#20) 2026-07-05 22:08:53 +03:00
ParantezTech 29b7f086c9 reuse 2026-07-04 19:14:44 +03:00
ParantezTech eb3d8d6eb1 [dotnet] configure local nuget package cache 2026-07-04 19:12:18 +03:00
Berk 787c75b9b3 [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 2a784aa405 [gpu-hotfix1] Cache improvements have been made (6x performance gain) (#18) 2026-07-04 15:19:32 +03:00
Berk 2649857cb5 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 c4b4aed2c5 [scePad] Just format code 2026-07-04 15:10:48 +03:00
Vlad Denisov 757f6ea4b5 [padExports]: add basic gamepad mappings (#16) 2026-07-04 14:40:43 +03:00
ParantezTech df3ee02f8d [readme] update screenshot image for dreaming sarah 2026-07-04 14:18:51 +03:00
ParantezTech dfe61ec643 [readme] update screenshot and add new image for dreaming sarah after shader decoder updates 2026-07-04 14:03:30 +03:00
Berk 52e17b5056 [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 c1f3abfd5d [ci] fix workflow dispatch and push triggers for main branch (#15) 2026-07-04 13:48:29 +03:00
Berk b5ee2c2cb7 [saveData] Add support for sceSaveDataMount3 (#14) 2026-07-03 13:25:38 +03:00
Berk 16c1b74636 [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
ParantezTech af35389bfd [libs] Add audio and system gesture exports 2026-06-28 23:47:00 +03:00
ParantezTech 4ae778ef39 [libs] Update system and video exports 2026-06-28 23:46:56 +03:00
ParantezTech a04e70c0b3 [libs] Update NP and pad exports 2026-06-28 23:46:56 +03:00
ParantezTech 02222c7919 [libs] Update network exports 2026-06-28 23:46:56 +03:00
ParantezTech 46b3a207c1 [libs] Update AMPR and PlayGo exports 2026-06-28 23:46:55 +03:00
ParantezTech 418d46beb5 [libs] Update AGC and fiber exports 2026-06-28 23:46:44 +03:00
ParantezTech 0e922a73ee [core] Improve native execution and guest threading 2026-06-28 23:45:26 +03:00
ParantezTech a5172fd2c0 [kernel] Guest-thread blocking for pthread_mutex_lock is currently disabled 2026-06-28 23:45:05 +03:00
ParantezTech 4ab614e68a [npManager] initial Network Platform implement (just set game title etc.) 2026-06-23 19:30:27 +03:00
ParantezTech e581fe41f4 [kernel] pthread improvements 2026-06-23 19:29:29 +03:00
ParantezTech 0f0ec9a020 [appContent] correct way to get metadata from game 2026-06-23 19:28:53 +03:00
ParantezTech 70048cb49f [agc] fix shader invalid argument 2026-06-23 19:27:57 +03:00
ParantezTech 4777ec4544 [sceShare] initial share exports 2026-06-23 19:27:24 +03:00
ParantezTech caee2ad692 [readme] alignment 2026-06-23 17:54:39 +03:00
ParantezTech 7c83f7f925 [readme] logo align 2026-06-23 17:52:45 +03:00
ParantezTech 0e37bc95e9 upload logo 2026-06-23 17:51:28 +03:00
ParantezTech 318425630a [readme] edit for new milestones 2026-06-23 16:06:58 +03:00
ParantezTech 992ef68ba8 [readme] update 2026-06-23 16:05:16 +03:00
ParantezTech 01ebab90c0 [playgo] In some games, chunk scenarios assertion rewrite 2026-06-23 15:50:43 +03:00
ParantezTech d134f9b9f6 [fiber] synchronization problems have been fixed for such a titles: Demon's Souls
[ampr] new exports
[memory] trampoline fixes
2026-06-23 15:48:45 +03:00
ParantezTech dad85b87c3 [video] Add game title to window 2026-06-21 23:19:16 +03:00
ParantezTech 464422f23e [video] Add Vulkan presentation 2026-06-21 23:19:05 +03:00
ParantezTech 5afa5e1ffa [hle] Add missing service exports 2026-06-21 23:18:55 +03:00
ParantezTech 5512da9e12 [kernel] Add thread and event primitives 2026-06-21 23:18:42 +03:00
ParantezTech 2a52ccfe6a [cpu] Add guest thread continuation 2026-06-21 23:18:29 +03:00
ParantezTech 08b315b5fc [kernel] Add guest memory support 2026-06-21 23:18:18 +03:00
ParantezTech 5d204afd10 [videoOut] add minimal Vulkan swapchain presenter 2026-06-14 12:12:09 +03:00
ParantezTech 7bd1e8dc29 change jpg to sharpemu videoOut test window 2026-06-07 16:15:43 +03:00
ParantezTech fad61e116e reuse 2026-06-07 15:56:07 +03:00
ParantezTech bc297fa175 [readme] update jpg 2026-06-07 15:48:11 +03:00
ParantezTech 79e767ed9f [readme] added rendering texture from Dreaming Sarah (this is just a presenter draw, not GPU swapchain) 2026-06-07 15:45:33 +03:00
ParantezTech 1ea1396979 [kernel/videoOut] extend memory managements and videoOut (this is not a swapchain) 2026-06-07 15:43:26 +03:00
ParantezTech a1dea499bc update readme 2026-05-10 20:37:49 +03:00
ParantezTech fee6af3136 update readme for new titles 2026-05-10 20:02:58 +03:00
ParantezTech bbf5ff7be8 more HLE's and fix cpu execution some titles 2026-05-10 19:51:57 +03:00
ParantezTech 0c859f04ad Added more HLE's 2026-04-27 12:40:50 +03:00
ParantezTech 994438963d [ci] temporary fix 2026-04-13 15:59:19 +03:00
ParantezTech 233af123af More new HLE's and fixes 2026-04-13 15:54:38 +03:00
ParantezTech 02eb9b30e9 correct patch 2026-03-28 18:40:54 +03:00
ParantezTech fa46819030 fix for actions (again) 2026-03-28 18:23:46 +03:00
ParantezTech 7a915e88dc fix dotnet version on Github Actions 2026-03-28 18:15:14 +03:00
ParantezTech 812879aa81 PlayGo, VideoOut minimum HLE implements and fix some direct runner 2026-03-28 18:02:37 +03:00
ParantezTech 71ba5cf1db More HLE implements: wcs* 2026-03-19 23:57:48 +03:00
ParantezTech 8f108ca01d Upgrade GitHub Actions to Node 24 2026-03-19 13:33:19 +03:00
ParantezTech cecf55bf2e fix: dereference of a possibly null reference. 2026-03-16 21:19:49 +03:00
ParantezTech 0ede7c70a9 [ci] update actions Node 20 deprecation 2026-03-16 21:18:58 +03:00
ParantezTech 8c39a55501 reuse 2026-03-16 20:49:40 +03:00
ParantezTech 0a4b36ffcb [dotnet/ci] constant version 2026-03-16 20:46:47 +03:00
ParantezTech 1a1d61b21c rework TLS allocations, more HLE's, increase import loop history 2026-03-16 20:22:58 +03:00
ParantezTech 3cf96f38de [github] bug_report fix 2026-03-16 20:14:58 +03:00
ParantezTech d8c44bb64f [github] added bug report issue template 2026-03-16 20:11:56 +03:00
ParantezTech a4b5171586 [github] added issue config & reuse 2026-03-16 20:01:08 +03:00
108 changed files with 45994 additions and 1194 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
+59
View File
@@ -0,0 +1,59 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Bug Report
description: Report a bug in the emulator
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
### Before creating an issue
- Do not create issues involving **software piracy**. Our rules specifically prohibit this.
Issues related to piracy will be closed and the author may be banned from the repository.
- The issue tracker is **not for asking questions**.
Question-type issues will be closed.
- Only open an issue if you are reporting an actual **bug in the emulator**.
- type: input
id: emulator_version
attributes:
label: Emulator Version / Commit
description: The emulator version or git commit where the bug occurred
placeholder: e.g. v0.0.2 / a1b2c3d
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: Describe the problem and how to reproduce it
placeholder: |
Steps to reproduce:
1. Launch emulator
2. Load game
3. Crash occurs
Expected behavior:
...
Actual behavior:
...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log File
description: |
Drag & drop your log file here, or paste the log contents.
Supported: .log / .txt / .zip
Large logs should be uploaded as a file.
render: shell
+4
View File
@@ -0,0 +1,4 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
blank_issues_enabled: false
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Game Compatibility Report
description: Report compatibility status of a game
labels: ["game-compatibility"]
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

+13 -4
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Build and Release
on:
@@ -6,11 +9,17 @@ on:
- "**"
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
workflow_dispatch:
permissions:
@@ -79,7 +88,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
dotnet-version: 10.0.103
cache: true
cache-dependency-path: |
Directory.Packages.props
@@ -106,7 +115,7 @@ jobs:
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
@@ -117,13 +126,13 @@ 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
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
+5
View File
@@ -26,11 +26,16 @@ arm64/
*.userosscache
*.sln.docstates
.packages
packages/
*.nupkg
.nuget/
.dotnet-home/
.cache/
.DS_Store
Thumbs.db
Desktop.ini
ehthumbs.db
.vs/
+12 -3
View File
@@ -1,13 +1,22 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.18" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<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" />
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
</ItemGroup>
</Project>
</Project>
BIN
View File
Binary file not shown.
+5 -1
View File
@@ -3,10 +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/**"
"_logs/**",
".github/images/**",
"assets/images/**"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
+1
View File
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Folder Name="/src/">
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
}
}
+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>
+102 -1
View File
@@ -3,6 +3,7 @@
using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
using SharpEmu.HLE;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
@@ -24,12 +25,32 @@ internal static partial class Program
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12;
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
[STAThread]
private static int Main(string[] args)
{
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild)
{
// No arguments: open the desktop frontend. Any argument selects
// the classic CLI behavior below.
return GuiLauncher.Run();
}
// The executable uses the GUI subsystem, so CLI mode has to connect
// itself to a console before the first write.
EnsureCliConsole();
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
{
return childExitCode;
@@ -101,6 +122,58 @@ internal static partial class Program
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
}
private static void EnsureCliConsole()
{
if (!OperatingSystem.IsWindows())
{
return;
}
// Standard handles already provided (pipes or file redirection, e.g.
// when the GUI or a script launches us): use them as-is.
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
{
return;
}
// Prefer the console of the parent process (interactive terminal);
// create one only when started with arguments but no terminal at all
// (e.g. a shortcut), so usage and errors remain visible.
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
{
_ = AllocConsole();
}
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
RebindStdHandleToConsole(STD_ERROR_HANDLE);
}
private static void RebindStdHandleToConsole(int stdHandle)
{
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
{
return;
}
var conOut = CreateFileW(
"CONOUT$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0,
OPEN_EXISTING,
0,
0);
if (IsHandleValid(conOut))
{
_ = SetStdHandle(stdHandle, conOut);
}
}
private static bool IsHandleValid(nint handle)
{
return handle != 0 && handle != -1;
}
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{
isMitigatedChild = false;
@@ -670,4 +743,32 @@ internal static partial class Program
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
[DllImport("kernel32.dll")]
private static extern nint GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AttachConsole(int processId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint GetStdHandle(int stdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetStdHandle(int stdHandle, nint handle);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateFileW(
string fileName,
uint desiredAccess,
uint shareMode,
nint securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
nint templateFile);
}
+26 -1
View File
@@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- GUI subsystem so starting without arguments opens the frontend with no
console window; CLI mode re-attaches to the parent terminal's console. -->
<OutputType>WinExe</OutputType>
<AssemblyName>SharpEmu</AssemblyName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
@@ -30,6 +33,15 @@ 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>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -38,4 +50,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
library is embedded into the single-file bundle. -->
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
<ItemGroup>
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
+451 -7
View File
@@ -4,9 +4,196 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
"requested": "[10.0.9, )",
"resolved": "10.0.9",
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
@@ -17,27 +204,284 @@
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.gui": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.3.18, )",
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
"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, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"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, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {},
"net10.0/osx-arm64": {},
"net10.0/win-x64": {}
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
+39 -13
View File
@@ -22,6 +22,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private const ulong StackSize = 0x0020_0000UL;
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
private const ulong TlsSize = 0x0001_0000UL;
private const ulong TlsPrefixSize = 0x0000_1000UL;
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
@@ -193,6 +194,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeGuestFrameChainSentinel(context))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeTls(context, tlsBase))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -337,11 +343,12 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
for (var i = 0; i < 32; i++)
{
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
var mappedBase = candidateBase - TlsPrefixSize;
try
{
_virtualMemory.Map(
candidateBase,
TlsSize,
mappedBase,
TlsSize + TlsPrefixSize,
fileOffset: 0,
fileData: ReadOnlySpan<byte>.Empty,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
@@ -358,9 +365,28 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private static bool InitializeTls(CpuContext context, ulong tlsBase)
{
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
}
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
{
var stackTop = context[CpuRegister.Rsp] + sizeof(ulong);
var sentinelFrame = AlignDown(stackTop - 0x20, 16);
var seedRsp = sentinelFrame - sizeof(ulong);
if (!context.TryWriteUInt64(sentinelFrame, 0) ||
!context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) ||
!context.TryWriteUInt64(seedRsp, 0))
{
return false;
}
context[CpuRegister.Rbp] = sentinelFrame;
context[CpuRegister.Rsp] = seedRsp;
return true;
}
private static bool InitializeProcessEntryFrame(
@@ -389,8 +415,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
const ulong entryParamsSize = 0x20;
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
@@ -398,6 +424,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return false;
}
var entryStackPointer = entryParamsAddress - sizeof(ulong);
if (!context.TryWriteUInt64(entryStackPointer, 0))
{
return false;
}
context[CpuRegister.Rsp] = entryStackPointer;
context[CpuRegister.Rdi] = entryParamsAddress;
context[CpuRegister.Rsi] = programExitHandlerAddress;
context[CpuRegister.Rdx] = 0;
@@ -423,13 +456,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return value & ~(alignment - 1);
}
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return context.Memory.TryWrite(address, buffer);
}
private static string BuildEntryFrameDiagnostic(
ulong entryPoint,
CpuContext context,
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
@@ -12,10 +13,23 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
private void RecordRecentImportTrace(string traceLine)
private void RecordRecentImportTrace(
long dispatchIndex,
string nid,
ulong returnRip,
ulong arg0,
ulong arg1,
ulong arg2)
{
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
dispatchIndex,
nid,
returnRip,
arg0,
arg1,
arg2);
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length)
{
@@ -34,10 +48,12 @@ public sealed partial class DirectExecutionBackend
for (int i = 0; i < _recentImportTraceCount; i++)
{
int num2 = (num + i) % _recentImportTrace.Length;
string text = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(text))
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Console.Error.WriteLine("[LOADER][INFO] " + text);
Console.Error.WriteLine(
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
}
@@ -97,12 +113,13 @@ public sealed partial class DirectExecutionBackend
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
{
if (_cpuContext == null || returnRip == 0)
var cpuContext = ActiveCpuContext;
if (cpuContext == null || returnRip == 0)
{
return;
}
Span<byte> destination = stackalloc byte[128];
if (!_cpuContext.Memory.TryRead(returnRip, destination))
if (!cpuContext.Memory.TryRead(returnRip, destination))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
return;
@@ -113,7 +130,7 @@ public sealed partial class DirectExecutionBackend
{
int num = BitConverter.ToInt32(destination.Slice(2, 4));
ulong num2 = returnRip + 6 + (ulong)num;
if (_cpuContext.TryReadUInt64(num2, out var value2))
if (cpuContext.TryReadUInt64(num2, out var value2))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
}
@@ -122,7 +139,7 @@ public sealed partial class DirectExecutionBackend
{
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
ulong num4 = returnRip + 7 + (ulong)num3;
if (_cpuContext.TryReadUInt64(num4, out var value3))
if (cpuContext.TryReadUInt64(num4, out var value3))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
}
@@ -134,12 +151,82 @@ public sealed partial class DirectExecutionBackend
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
ulong num6 = returnRip + (ulong)i;
ulong num7 = num6 + 6 + (ulong)num5;
if (_cpuContext.TryReadUInt64(num7, out var value4))
if (cpuContext.TryReadUInt64(num7, out var value4))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
}
}
}
Span<byte> targetBytes = stackalloc byte[32];
for (int i = 0; i + 5 <= destination.Length; i++)
{
if (destination[i] != 0xE8)
{
continue;
}
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != target)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
}
break;
}
if (cpuContext.Memory.TryRead(target, targetBytes))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
{
int slotRel32 = BitConverter.ToInt32(targetBytes.Slice(2, 4));
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
}
break;
}
}
}
}
}
}
private static bool IsUnresolvedSentinel(ulong value)
@@ -231,11 +318,24 @@ public sealed partial class DirectExecutionBackend
private unsafe static bool IsExecutableAddress(ulong address)
{
var pageAddress = address & ~0xFFFUL;
if (_knownExecutablePages.ContainsKey(pageAddress))
{
return true;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
}
return executable;
}
private static ulong AlignUp(ulong value, ulong alignment)
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
@@ -14,11 +15,19 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
private static int _lazyCommitTraceCount;
private unsafe void SetupExceptionHandler()
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -28,12 +37,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -61,7 +80,7 @@ public sealed partial class DirectExecutionBackend
{
if (_vectoredHandlerDepth > 0)
{
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
LogNestedVectoredException(exceptionInfo);
Console.Error.Flush();
return 0;
}
@@ -84,10 +103,18 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
}
if (exceptionCode == MSVC_CPP_EXCEPTION)
{
return 0;
}
switch (exceptionCode)
{
@@ -278,6 +305,37 @@ public sealed partial class DirectExecutionBackend
}
}
private static bool IsBenignHostDebugException(uint exceptionCode)
{
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
}
private unsafe static void LogNestedVectoredException(void* exceptionInfo)
{
int count = Interlocked.Increment(ref _nestedVehTraceCount);
if (count > 16 && count % 128 != 0)
{
return;
}
try
{
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
void* contextRecord = pointers->ContextRecord;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine(
$"[LOADER][TRACE] Nested VEH exception#{count}: code=0x{record->ExceptionCode:X8} ex=0x{(ulong)record->ExceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} type={accessType} target=0x{target:X16}; passing through.");
}
catch
{
Console.Error.WriteLine($"[LOADER][TRACE] Nested VEH exception#{count}; passing through.");
}
}
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
{
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
@@ -828,7 +886,7 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe static bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord)
private unsafe bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord, ulong rip, ulong rsp)
{
if (exceptionRecord->NumberParameters < 2)
{
@@ -845,6 +903,10 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!IsGuestOwnedLazyCommitAddress(faultAddress, out var owner))
{
return false;
}
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
@@ -852,7 +914,21 @@ public sealed partial class DirectExecutionBackend
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
bool committed = false;
ulong committedBase = 0;
@@ -860,14 +936,25 @@ public sealed partial class DirectExecutionBackend
if (mbi.State == 65536)
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
committedBase = windowBase;
committedSize = windowSize;
}
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
@@ -890,7 +977,10 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
}
@@ -899,14 +989,25 @@ public sealed partial class DirectExecutionBackend
return false;
}
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
committedBase = commitWindowBase;
committedSize = commitWindowSize;
}
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
@@ -935,9 +1036,46 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
{
baseAddress = 0;
length = 0;
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
{
return false;
}
ulong regionEnd = regionBase + regionSize;
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
if (windowBase < regionBase)
{
windowBase = regionBase;
}
if (windowBase >= regionEnd)
{
return false;
}
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
ulong windowSize = windowEnd - windowBase;
windowSize &= 0xFFFFFFFFFFFFF000uL;
if (windowSize == 0)
{
return false;
}
baseAddress = windowBase;
length = windowSize;
return true;
}
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
@@ -964,6 +1102,49 @@ public sealed partial class DirectExecutionBackend
}
return TryCommitRange(commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
{
const uint pageNoAccess = 0x01;
const uint pageReadOnly = 0x02;
const uint pageReadWrite = 0x04;
const uint pageWriteCopy = 0x08;
const uint pageExecute = 0x10;
const uint pageExecuteRead = 0x20;
const uint pageExecuteReadWrite = 0x40;
const uint pageExecuteWriteCopy = 0x80;
const uint pageGuard = 0x100;
const uint accessMask = 0xFF;
if ((protection & pageGuard) != 0)
{
return false;
}
uint access = protection & accessMask;
if (access == pageNoAccess)
{
return false;
}
return accessType switch
{
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
_ => false
};
}
}
private static bool ShouldTraceLazyCommit(int traceIndex)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
{
return true;
}
return traceIndex <= 16 || traceIndex % 256 == 0;
}
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
{
private readonly ICpuMemory _inner;
@@ -39,4 +39,15 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
return result;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_inner is IGuestMemoryAllocator allocator)
{
return allocator.TryAllocateGuestMemory(size, alignment, out address);
}
address = 0;
return false;
}
}
+13 -1
View File
@@ -21,7 +21,10 @@ public sealed class SelfImage
IReadOnlyList<ulong>? initializerFunctions = null,
ulong initFunctionEntryPoint = 0,
ulong imageBase = 0,
ulong procParamAddress = 0)
ulong procParamAddress = 0,
string? title = null,
string? titleId = null,
string? version = null)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
@@ -38,6 +41,9 @@ public sealed class SelfImage
InitFunctionEntryPoint = initFunctionEntryPoint;
_imageBase = imageBase;
ProcParamAddress = procParamAddress;
Title = title;
TitleId = titleId;
Version = version;
}
public bool IsSelf { get; }
@@ -63,4 +69,10 @@ public sealed class SelfImage
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
public ulong ProcParamAddress { get; }
public string? Title { get; }
public string? TitleId { get; }
public string? Version { get; }
}
+18 -10
View File
@@ -144,10 +144,9 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is empty.");
}
if (readParamJson)
{
TryLoadParamJson(fs, mountRoot);
}
var applicationInfo = readParamJson
? TryLoadParamJson(fs, mountRoot)
: default;
if (clearVirtualMemory)
{
@@ -270,15 +269,20 @@ public sealed class SelfLoader : ISelfLoader
initializerFunctions,
initFunctionEntryPoint,
imageBase,
procParamAddress);
procParamAddress,
applicationInfo.Title,
applicationInfo.TitleId,
applicationInfo.Version);
}
private static void TryLoadParamJson(IFileSystem? fs, string? mountRoot)
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
IFileSystem? fs,
string? mountRoot)
{
if (fs == null)
{
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
return;
return default;
}
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
@@ -298,12 +302,16 @@ public sealed class SelfLoader : ISelfLoader
if (foundPath == null)
{
Console.WriteLine("[LOADER] param.json not found (no root path / unknown layout).");
return;
return default;
}
var (title, titleId, ver) = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
var applicationInfo = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
Console.WriteLine($"[LOADER] Loading param.json at {foundPath}");
Console.WriteLine($"[LOADER] Title: {title ?? "(unknown)"}, TitleId: {titleId ?? "(unknown)"}, Version: {ver ?? "(unknown)"}");
Console.WriteLine(
$"[LOADER] Title: {applicationInfo.Title ?? "(unknown)"}, " +
$"TitleId: {applicationInfo.TitleId ?? "(unknown)"}, " +
$"Version: {applicationInfo.Version ?? "(unknown)"}");
return applicationInfo;
}
private static LoadContext ParseLayout(ReadOnlySpan<byte> imageData)
+607 -84
View File
@@ -7,15 +7,22 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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;
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
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;
@@ -29,6 +36,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02;
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);
@@ -40,6 +51,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
@@ -68,9 +82,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return false;
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -79,9 +94,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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;
}
@@ -95,7 +114,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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)
@@ -124,7 +145,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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)
@@ -178,12 +199,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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
@@ -192,9 +213,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
}
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -203,25 +225,135 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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;
}
public bool TryAllocateAtOrAbove(
ulong desiredAddress,
ulong size,
bool executable,
ulong alignment,
out ulong actualAddress)
{
actualAddress = 0;
if (size == 0)
{
return false;
}
var alignedSize = AlignUp(size, PageSize);
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
for (var attempt = 0; attempt < 0x10000; attempt++)
{
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
{
return false;
}
if (TryGetOverlappingRegionEnd(cursor, alignedSize, out var overlapEnd))
{
cursor = AlignUp(overlapEnd, effectiveAlignment);
continue;
}
try
{
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
if (actualAddress == cursor)
{
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
return true;
}
actualAddress = 0;
}
catch
{
}
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
}
return false;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
address = 0;
if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0)
{
return false;
}
lock (_guestAllocationGate)
{
if (_guestAllocationArenaBase == 0)
{
try
{
_guestAllocationArenaBase = AllocateAt(
GuestAllocationArenaAddress,
GuestAllocationArenaSize,
executable: false,
allowAlternative: true);
_guestAllocationOffset = GuestAllocationArenaStartOffset;
}
catch (Exception)
{
return false;
}
}
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
{
return false;
}
address = _guestAllocationArenaBase + alignedOffset;
_guestAllocationOffset = alignedOffset + size;
return true;
}
}
public void Clear()
{
lock (_gate)
lock (_guestAllocationGate)
{
foreach (var region in _regions)
_gate.EnterWriteLock();
try
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
foreach (var region in _regions)
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
}
_regions.Clear();
_pageProtections.Clear();
lock (_allocationSearchHintGate)
{
_allocationSearchHints.Clear();
}
}
_regions.Clear();
_pageProtections.Clear();
finally
{
_gate.ExitWriteLock();
}
_guestAllocationArenaBase = 0;
_guestAllocationOffset = 0;
}
}
@@ -238,7 +370,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart);
lock (_gate)
_gate.EnterWriteLock();
try
{
var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null)
@@ -269,7 +402,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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();
}
}
@@ -320,7 +457,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{
lock (_gate)
_gate.EnterReadLock();
try
{
var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++)
@@ -335,86 +473,240 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
}
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 (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
}
try
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);
}
}
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 (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
}
try
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);
}
}
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)
@@ -426,45 +718,146 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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)
{
overlapEnd = 0;
if (size == 0 || ulong.MaxValue - address < size - 1)
{
return false;
}
var end = address + size;
_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)
@@ -494,6 +887,100 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: false);
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: true);
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
{
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (_pageProtections.TryGetValue(pageAddress, out var flags))
{
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
{
return false;
}
}
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
{
return false;
}
}
return true;
}
private static bool IsReadableProtection(uint protection)
{
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
}
private static bool IsWritableProtection(uint protection)
{
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
}
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
}
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
{
if (size == 0 || !region.IsReservedOnly)
{
return true;
}
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region);
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;
}
if (info.State != MEM_RESERVE)
{
return false;
}
var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
pageAddress = rangeEnd;
}
return true;
}
private bool TryTemporarilyProtectForRead(
ulong address,
ulong size,
@@ -541,6 +1028,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
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)
@@ -558,4 +1068,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public bool IsReservedOnly { get; set; }
public uint Protection { get; set; }
}
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
+45 -4
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;
@@ -125,6 +126,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
public OrbisGen2Result Run(string ebootPath)
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
@@ -132,8 +135,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastBasicBlockTrace = null;
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(ebootPath);
var normalizedEbootPath = Path.GetFullPath(ebootPath);
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}");
@@ -350,6 +354,41 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName)))
{
return null;
}
var app0Root = Path.GetDirectoryName(normalizedEbootPath);
if (string.IsNullOrWhiteSpace(app0Root))
{
return null;
}
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
return new App0BindingScope(app0VariableName);
}
private sealed class App0BindingScope(string variableName) : IDisposable
{
private readonly string _variableName = variableName;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
Environment.SetEnvironmentVariable(_variableName, null);
_disposed = true;
}
}
private OrbisGen2Result? RunAllInitializers(
SelfImage mainImage,
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
@@ -504,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)
@@ -515,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);
@@ -523,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
+5 -1
View File
@@ -1,4 +1,4 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Iced" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
+100 -1
View File
@@ -8,17 +8,116 @@
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
"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, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"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, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.App"
RequestedThemeVariant="Dark">
<Application.Resources>
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" />
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
<SolidColorBrush x:Key="ElevatedBrush" Color="#1B2230" />
<SolidColorBrush x:Key="TextBrush" Color="#E8ECF4" />
<SolidColorBrush x:Key="MutedBrush" Color="#8B94A7" />
<SolidColorBrush x:Key="FaintBrush" Color="#5A6478" />
<SolidColorBrush x:Key="AccentBrush" Color="#7C5CFC" />
<SolidColorBrush x:Key="AccentHoverBrush" Color="#8F73FF" />
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
</Application.Resources>
<Application.Styles>
<FluentTheme />
<Style Selector="Window">
<Setter Property="FontFamily" Value="Inter, Segoe UI, sans-serif" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Border.card">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16" />
</Style>
<Style Selector="Border.pill">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Padding" Value="10,3" />
</Style>
<Style Selector="TextBlock.sectionTitle">
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="LetterSpacing" Value="1.5" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
</Style>
<Style Selector="TextBlock.fieldLabel">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerHoverBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.ghost">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ListBox.console">
<Setter Property="Background" Value="#0B0E14" />
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="ListBox.console ListBoxItem">
<Setter Property="Padding" Value="10,1" />
<Setter Property="MinHeight" Value="0" />
</Style>
<Style Selector="ListBox.library ListBoxItem">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Margin" Value="4,2" />
<Setter Property="Padding" Value="8,4" />
</Style>
</Application.Styles>
</Application>
+26
View File
@@ -0,0 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace SharpEmu.GUI;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
+645
View File
@@ -0,0 +1,645 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.GUI;
/// <summary>
/// Launches the SharpEmu CLI as a child process with the same CET/CFG mitigation
/// opt-outs the CLI would apply to its own relaunched child, while capturing
/// stdout/stderr through pipes. The CLI's internal relaunch is suppressed via
/// SHARPEMU_DISABLE_MITIGATION_RELAUNCH so output is not lost to a detached
/// console. A kill-on-close job object ties the emulator's lifetime to the GUI.
/// </summary>
internal sealed class EmulatorProcess : IDisposable
{
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
private const uint CREATE_NO_WINDOW = 0x08000000;
private const int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const uint INFINITE = 0xFFFFFFFF;
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private const int JobObjectExtendedLimitInformation = 9;
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private readonly object _sync = new();
private nint _processHandle;
private nint _jobHandle;
private Process? _fallbackProcess;
private bool _running;
private bool _disposed;
public event Action<string, bool>? OutputReceived;
public event Action<int>? Exited;
public bool IsRunning
{
get
{
lock (_sync)
{
return _running;
}
}
}
public void Start(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_running)
{
throw new InvalidOperationException("The emulator process is already running.");
}
if (OperatingSystem.IsWindows())
{
StartWindows(exePath, arguments, workingDirectory);
}
else
{
StartFallback(exePath, arguments, workingDirectory);
}
_running = true;
}
}
public void Stop()
{
lock (_sync)
{
if (!_running)
{
return;
}
if (_processHandle != 0)
{
_ = TerminateProcess(_processHandle, 1);
}
try
{
_fallbackProcess?.Kill(entireProcessTree: true);
}
catch (InvalidOperationException)
{
// Already exited.
}
}
}
public void Dispose()
{
lock (_sync)
{
if (_disposed)
{
return;
}
_disposed = true;
}
Stop();
}
private void StartWindows(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
// The CLI would otherwise relaunch itself into a mitigated child whose
// console output cannot flow through our pipes.
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH", "1");
var securityAttributes = new SECURITY_ATTRIBUTES
{
nLength = Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
bInheritHandle = 1,
};
if (!CreatePipe(out var stdoutRead, out var stdoutWrite, ref securityAttributes, 0) ||
!CreatePipe(out var stderrRead, out var stderrWrite, ref securityAttributes, 0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create output pipes.");
}
_ = SetHandleInformation(stdoutRead, HANDLE_FLAG_INHERIT, 0);
_ = SetHandleInformation(stderrRead, HANDLE_FLAG_INHERIT, 0);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
startupInfoEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startupInfoEx.StartupInfo.hStdOutput = stdoutWrite;
startupInfoEx.StartupInfo.hStdError = stderrWrite;
nint attributeList = 0;
nint mitigationPolicies = 0;
try
{
nuint attributeListSize = 0;
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to initialize the process attribute list.");
}
startupInfoEx.lpAttributeList = attributeList;
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
if (!UpdateProcThreadAttribute(
attributeList,
0,
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
mitigationPolicies,
(nuint)(sizeof(ulong) * 2),
0,
0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to apply the mitigation policy.");
}
var currentDirectory = workingDirectory ?? Environment.CurrentDirectory;
var created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out var processInfo);
if (!created)
{
// Some mitigation policy bits (e.g. XFG) are not supported on
// older Windows builds. Mirror the CLI's behavior and fall back
// to launching without the mitigation attribute list.
startupInfoEx.lpAttributeList = 0;
created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out processInfo);
}
if (!created)
{
var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
_processHandle = processInfo.hProcess;
_jobHandle = CreateJobObjectW(0, null);
if (_jobHandle != 0 &&
(!TryEnableKillOnJobClose(_jobHandle) || !AssignProcessToJobObject(_jobHandle, processInfo.hProcess)))
{
CloseHandle(_jobHandle);
_jobHandle = 0;
}
StartReaderThread(stdoutRead, isError: false);
StartReaderThread(stderrRead, isError: true);
StartExitWatcherThread();
}
catch
{
CloseHandle(stdoutRead);
CloseHandle(stderrRead);
throw;
}
finally
{
// The child owns duplicated pipe write ends; closing ours lets the
// readers observe EOF when the child exits.
CloseHandle(stdoutWrite);
CloseHandle(stderrWrite);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
Marshal.FreeHGlobal(attributeList);
}
if (mitigationPolicies != 0)
{
Marshal.FreeHGlobal(mitigationPolicies);
}
}
}
private void StartFallback(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
var startInfo = new ProcessStartInfo
{
FileName = exePath,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (var argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, e) =>
{
if (e.Data is not null)
{
OutputReceived?.Invoke(e.Data, false);
}
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data is not null)
{
OutputReceived?.Invoke(e.Data, true);
}
};
process.Exited += (_, _) =>
{
int exitCode;
try
{
exitCode = process.ExitCode;
}
catch (InvalidOperationException)
{
exitCode = -1;
}
OnExited(exitCode);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
_fallbackProcess = process;
}
private void StartReaderThread(nint readHandle, bool isError)
{
var thread = new Thread(() =>
{
using var stream = new FileStream(new SafeFileHandle(readHandle, ownsHandle: true), FileAccess.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
try
{
while (reader.ReadLine() is { } line)
{
OutputReceived?.Invoke(line, isError);
}
}
catch (IOException)
{
// Pipe broken on process teardown.
}
})
{
IsBackground = true,
Name = isError ? "SharpEmu stderr reader" : "SharpEmu stdout reader",
};
thread.Start();
}
private void StartExitWatcherThread()
{
var processHandle = _processHandle;
var thread = new Thread(() =>
{
_ = WaitForSingleObject(processHandle, INFINITE);
var exitCode = GetExitCodeProcess(processHandle, out var rawExitCode)
? unchecked((int)rawExitCode)
: -1;
OnExited(exitCode);
})
{
IsBackground = true,
Name = "SharpEmu exit watcher",
};
thread.Start();
}
private void OnExited(int exitCode)
{
lock (_sync)
{
if (!_running)
{
return;
}
_running = false;
if (_processHandle != 0)
{
CloseHandle(_processHandle);
_processHandle = 0;
}
if (_jobHandle != 0)
{
CloseHandle(_jobHandle);
_jobHandle = 0;
}
_fallbackProcess?.Dispose();
_fallbackProcess = null;
}
Exited?.Invoke(exitCode);
}
private static bool TryEnableKillOnJobClose(nint jobHandle)
{
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
var memory = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
return SetInformationJobObject(
jobHandle,
JobObjectExtendedLimitInformation,
memory,
unchecked((uint)size));
}
finally
{
Marshal.FreeHGlobal(memory);
}
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{
var builder = new StringBuilder();
builder.Append(QuoteArgument(processPath));
for (var i = 0; i < args.Count; i++)
{
builder.Append(' ');
builder.Append(QuoteArgument(args[i]));
}
return builder.ToString();
}
private static string QuoteArgument(string argument)
{
if (argument.Length == 0)
{
return "\"\"";
}
var needsQuotes = false;
foreach (var c in argument)
{
if (char.IsWhiteSpace(c) || c == '"')
{
needsQuotes = true;
break;
}
}
if (!needsQuotes)
{
return argument;
}
var builder = new StringBuilder(argument.Length + 2);
builder.Append('"');
var backslashCount = 0;
foreach (var c in argument)
{
if (c == '\\')
{
backslashCount++;
continue;
}
if (c == '"')
{
builder.Append('\\', (backslashCount * 2) + 1);
builder.Append('"');
backslashCount = 0;
continue;
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount);
backslashCount = 0;
}
builder.Append(c);
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount * 2);
}
builder.Append('"');
return builder.ToString();
}
[StructLayout(LayoutKind.Sequential)]
private struct SECURITY_ATTRIBUTES
{
public int nLength;
public nint lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public nint lpReserved;
public nint lpDesktop;
public nint lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public nint lpReserved2;
public nint hStdInput;
public nint hStdOutput;
public nint hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public nint lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public nint hProcess;
public nint hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public nuint MinimumWorkingSetSize;
public nuint MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public nint Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public nuint ProcessMemoryLimit;
public nuint JobMemoryLimit;
public nuint PeakProcessMemoryUsed;
public nuint PeakJobMemoryUsed;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreatePipe(
out nint hReadPipe,
out nint hWritePipe,
ref SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint hObject, uint dwMask, uint dwFlags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(
nint lpAttributeList,
int dwAttributeCount,
int dwFlags,
ref nuint lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute(
nint lpAttributeList,
uint dwFlags,
nint attribute,
nint lpValue,
nuint cbSize,
nint lpPreviousValue,
nint lpReturnSize);
[DllImport("kernel32.dll")]
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(
nint hJob,
int jobObjectInfoClass,
nint lpJobObjectInfo,
uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(
string applicationName,
StringBuilder commandLine,
nint processAttributes,
nint threadAttributes,
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
uint creationFlags,
nint environment,
string currentDirectory,
ref STARTUPINFOEX startupInfo,
out PROCESS_INFORMATION processInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateProcess(nint process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes)
{
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: $"{FormatSize(SizeBytes)} • {Path}";
private static string FormatSize(long bytes)
{
return bytes switch
{
>= 1L << 30 => $"{bytes / (double)(1L << 30):0.0} GiB",
>= 1L << 20 => $"{bytes / (double)(1L << 20):0.0} MiB",
>= 1L << 10 => $"{bytes / (double)(1L << 10):0.0} KiB",
_ => $"{bytes} B",
};
}
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
namespace SharpEmu.GUI;
/// <summary>
/// Entry point for the desktop frontend, hosted by the SharpEmu executable
/// when it is started without command-line arguments.
/// </summary>
public static class GuiLauncher
{
public static int Run()
{
try
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(Array.Empty<string>());
return 0;
}
catch (Exception ex)
{
WriteCrashLog(ex);
throw;
}
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
private static void WriteCrashLog(Exception ex)
{
try
{
File.AppendAllText(
Path.Combine(AppContext.BaseDirectory, "gui-crash.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}{Environment.NewLine}{Environment.NewLine}");
}
catch (Exception)
{
// Crash logging is best-effort.
}
}
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
public sealed class GuiSettings
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
};
public List<string> GameFolders { get; set; } = new();
public string LogLevel { get; set; } = "Info";
public int ImportTraceLimit { get; set; }
public bool StrictDynlibResolution { get; set; }
public string? EmulatorPath { get; set; }
// The emulator is portable and keeps its data next to the executable;
// the GUI follows the same convention.
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
public static GuiSettings Load()
{
try
{
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
}
}
catch (Exception)
{
// Corrupt or unreadable settings fall back to defaults.
}
return new GuiSettings();
}
public void Save()
{
try
{
var directory = Path.GetDirectoryName(SettingsPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(this, SerializerOptions));
}
catch (Exception)
{
// Settings persistence is best-effort.
}
}
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia.Media;
namespace SharpEmu.GUI;
public sealed record LogLine(string Text, IBrush Brush);
+157
View File
@@ -0,0 +1,157 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.MainWindow"
Title="SharpEmu"
Width="1280" Height="800"
MinWidth="980" MinHeight="620"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
<Grid RowDefinitions="44,*,32">
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
<Border Classes="pill">
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*">
<!-- Game library -->
<Border Grid.Column="0" Classes="card" Padding="0">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="pill">
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
</Grid>
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="4,4" Spacing="2">
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
</StackPanel>
</Grid>
</Border>
<!-- Right column -->
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
<!-- Selected game / launch -->
<Border Grid.Row="0" Classes="card">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel>
</Grid>
</Border>
<!-- Launch options -->
<Border Grid.Row="2" Classes="card">
<StackPanel Spacing="12">
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
<WrapPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="CPU engine" />
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<ComboBoxItem Content="Native" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log level" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<ComboBoxItem Content="Trace" />
<ComboBoxItem Content="Debug" />
<ComboBoxItem Content="Info" />
<ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Error" />
<ComboBoxItem Content="Critical" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
Value="0" FormatString="0" CornerRadius="8" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
<!-- Console -->
<Border Grid.Row="4" Classes="card" Padding="0">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,10,10">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</Grid>
</Grid>
<!-- Status bar -->
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
</Grid>
</Grid>
</Window>
+609
View File
@@ -0,0 +1,609 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Text.Json;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
namespace SharpEmu.GUI;
public partial class MainWindow : Window
{
private const int MaxConsoleLines = 4000;
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
private static readonly IBrush InfoLineBrush = new SolidColorBrush(Color.Parse("#6FA8FF"));
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
private readonly List<GameEntry> _allGames = new();
private readonly ObservableCollection<GameEntry> _visibleGames = new();
private readonly ObservableCollection<LogLine> _consoleLines = new();
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
private readonly DispatcherTimer _consoleFlushTimer;
private GuiSettings _settings = new();
private EmulatorProcess? _emulator;
private string? _emulatorExePath;
private bool _isRunning;
private int _autoScrollTicks;
public MainWindow()
{
InitializeComponent();
GameList.ItemsSource = _visibleGames;
ConsoleList.ItemsSource = _consoleLines;
_consoleFlushTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(80),
};
_consoleFlushTimer.Tick += (_, _) =>
{
FlushPendingConsoleLines();
MaybeAutoScroll();
};
_consoleFlushTimer.Start();
TitleBar.PointerPressed += OnTitleBarPointerPressed;
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
GameList.DoubleTapped += (_, _) => LaunchSelected();
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
LaunchButton.Click += (_, _) => LaunchSelected();
StopButton.Click += (_, _) => _emulator?.Stop();
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing();
}
private async Task OnOpenedAsync()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
if (version is not null)
{
VersionText.Text = $"v{version.ToString(3)}";
}
_settings = GuiSettings.Load();
ApplySettingsToControls();
LocateEmulator();
await RescanLibraryAsync();
}
private void OnWindowClosing()
{
ReadControlsIntoSettings();
_settings.Save();
_consoleFlushTimer.Stop();
_emulator?.Dispose();
}
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
BeginMoveDrag(e);
}
}
// ---- Settings <-> controls ----
private void ApplySettingsToControls()
{
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
{
"trace" => 0,
"debug" => 1,
"info" => 2,
"warning" or "warn" => 3,
"error" => 4,
"critical" or "fatal" => 5,
_ => 2,
};
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
}
private void ReadControlsIntoSettings()
{
_settings.LogLevel = SelectedLogLevel();
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
}
private string SelectedLogLevel()
{
return LogLevelBox.SelectedIndex switch
{
0 => "Trace",
1 => "Debug",
2 => "Info",
3 => "Warning",
4 => "Error",
5 => "Critical",
_ => "Info",
};
}
// ---- Emulator discovery ----
private void LocateEmulator()
{
var exeName = OperatingSystem.IsWindows() ? "SharpEmu.exe" : "SharpEmu";
var baseDirectory = AppContext.BaseDirectory;
var candidates = new List<string>();
if (!string.IsNullOrWhiteSpace(_settings.EmulatorPath))
{
candidates.Add(_settings.EmulatorPath);
}
// The GUI and the CLI are the same executable: with arguments it runs
// the emulator, so the preferred child process is this process itself.
if (Environment.ProcessPath is { } selfPath &&
Path.GetFileNameWithoutExtension(selfPath).Equals("SharpEmu", StringComparison.OrdinalIgnoreCase))
{
candidates.Add(selfPath);
}
candidates.Add(Path.Combine(baseDirectory, exeName));
candidates.Add(Path.Combine(baseDirectory, "win-x64", exeName));
candidates.Add(Path.Combine(baseDirectory, "..", exeName));
_emulatorExePath = candidates.FirstOrDefault(File.Exists) is { } found
? Path.GetFullPath(found)
: null;
EmulatorPathText.Text = _emulatorExePath is not null
? $"Emulator: {_emulatorExePath}"
: "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.";
}
// ---- Game library ----
private async Task AddFolderAsync()
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Choose a folder containing games",
AllowMultiple = false,
});
var path = folders.FirstOrDefault()?.TryGetLocalPath();
if (string.IsNullOrEmpty(path))
{
return;
}
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
{
_settings.GameFolders.Add(path);
_settings.Save();
}
await RescanLibraryAsync();
}
private async Task RescanLibraryAsync()
{
var folders = _settings.GameFolders.ToArray();
StatusBarRight.Text = "Scanning library…";
var games = await Task.Run(() => ScanFolders(folders));
_allGames.Clear();
_allGames.AddRange(games);
RefreshVisibleGames();
StatusBarRight.Text = folders.Length == 0
? "Add a game folder to populate the library."
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
}
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders)
{
var games = new List<GameEntry>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var enumeration = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
MaxRecursionDepth = 8,
};
foreach (var folder in folders)
{
if (!Directory.Exists(folder))
{
continue;
}
try
{
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
{
var fullPath = Path.GetFullPath(file);
if (!seen.Add(fullPath))
{
continue;
}
long size = 0;
try
{
size = new FileInfo(fullPath).Length;
}
catch (IOException)
{
}
var (title, titleId) = TryReadParamJson(fullPath);
games.Add(new GameEntry(title ?? GameNameFor(fullPath), titleId, fullPath, size));
}
}
catch (Exception)
{
// Skip folders that fail to enumerate.
}
}
games.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
return games;
}
/// <summary>
/// Reads the game title and title id from sce_sys/param.json next to the
/// executable, when present.
/// </summary>
private static (string? Title, string? TitleId) TryReadParamJson(string ebootPath)
{
try
{
var directory = Path.GetDirectoryName(ebootPath);
if (directory is null)
{
return (null, null);
}
var paramPath = Path.Combine(directory, "sce_sys", "param.json");
if (!File.Exists(paramPath))
{
return (null, null);
}
// ReadAllText handles a UTF-8 BOM, which JsonDocument rejects in
// raw bytes.
using var document = JsonDocument.Parse(File.ReadAllText(paramPath));
var root = document.RootElement;
string? titleId = null;
if (root.TryGetProperty("titleId", out var idElement) && idElement.ValueKind == JsonValueKind.String)
{
titleId = idElement.GetString();
}
string? title = null;
if (root.TryGetProperty("localizedParameters", out var localized) &&
localized.ValueKind == JsonValueKind.Object)
{
if (localized.TryGetProperty("defaultLanguage", out var language) &&
language.ValueKind == JsonValueKind.String &&
localized.TryGetProperty(language.GetString()!, out var defaultBlock) &&
defaultBlock.ValueKind == JsonValueKind.Object &&
defaultBlock.TryGetProperty("titleName", out var titleName) &&
titleName.ValueKind == JsonValueKind.String)
{
title = titleName.GetString();
}
else
{
foreach (var property in localized.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.Object &&
property.Value.TryGetProperty("titleName", out var anyTitleName) &&
anyTitleName.ValueKind == JsonValueKind.String)
{
title = anyTitleName.GetString();
break;
}
}
}
}
return (
string.IsNullOrWhiteSpace(title) ? null : title,
string.IsNullOrWhiteSpace(titleId) ? null : titleId);
}
catch (Exception)
{
return (null, null);
}
}
private static string GameNameFor(string ebootPath)
{
var directory = Path.GetDirectoryName(ebootPath);
var name = directory is not null ? Path.GetFileName(directory) : null;
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
}
private void RefreshVisibleGames()
{
var query = SearchBox.Text?.Trim() ?? string.Empty;
var selected = GameList.SelectedItem as GameEntry;
_visibleGames.Clear();
foreach (var game in _allGames)
{
if (query.Length == 0 ||
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase))
{
_visibleGames.Add(game);
}
}
GameCountText.Text = _visibleGames.Count.ToString();
if (selected is not null && _visibleGames.Contains(selected))
{
GameList.SelectedItem = selected;
}
UpdateSelectedGame();
}
private void UpdateSelectedGame()
{
if (GameList.SelectedItem is GameEntry game)
{
SelectedGameTitle.Text = game.Name;
SelectedGamePath.Text = game.Path;
}
else
{
SelectedGameTitle.Text = "No game selected";
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
}
UpdateRunButtons();
}
// ---- Launching ----
private async Task OpenFileAsync()
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open an executable to launch",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("PS executables") { Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
FilePickerFileTypes.All,
},
});
var path = files.FirstOrDefault()?.TryGetLocalPath();
if (!string.IsNullOrEmpty(path))
{
Launch(path, Path.GetFileName(path));
}
}
private void LaunchSelected()
{
if (GameList.SelectedItem is GameEntry game)
{
Launch(game.Path, game.Name);
}
}
private void Launch(string ebootPath, string displayName)
{
if (_isRunning)
{
return;
}
if (_emulatorExePath is null)
{
LocateEmulator();
if (_emulatorExePath is null)
{
AppendConsoleLine("SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).", ErrorLineBrush);
return;
}
}
ReadControlsIntoSettings();
_settings.Save();
var arguments = new List<string>
{
"--cpu-engine=native",
$"--log-level={_settings.LogLevel.ToLowerInvariant()}",
};
if (_settings.StrictDynlibResolution)
{
arguments.Add("--strict");
}
if (_settings.ImportTraceLimit > 0)
{
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
}
arguments.Add(ebootPath);
_consoleLines.Clear();
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
try
{
emulator.Start(_emulatorExePath, arguments, Path.GetDirectoryName(ebootPath));
}
catch (Exception ex)
{
emulator.Dispose();
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
return;
}
_emulator = emulator;
_isRunning = true;
StatusDot.Fill = SuccessLineBrush;
StatusText.Text = $"Running — {displayName}";
StatusBarRight.Text = $"Running {displayName}";
UpdateRunButtons();
}
private void OnEmulatorExited(int exitCode)
{
FlushPendingConsoleLines();
_isRunning = false;
_emulator?.Dispose();
_emulator = null;
var meaning = exitCode switch
{
0 => "OK",
1 => "invalid arguments",
2 => "eboot not found",
3 => "runtime exception",
4 => "emulation error",
_ => "unknown",
};
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
StatusBarRight.Text = "Idle";
UpdateRunButtons();
}
private void UpdateRunButtons()
{
LaunchButton.IsEnabled = !_isRunning && GameList.SelectedItem is GameEntry;
StopButton.IsEnabled = _isRunning;
OpenFileButton.IsEnabled = !_isRunning;
}
// ---- Console ----
private void FlushPendingConsoleLines()
{
if (_pendingLines.IsEmpty)
{
return;
}
var incoming = new List<LogLine>();
while (_pendingLines.TryDequeue(out var pending))
{
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
}
if (incoming.Count >= MaxConsoleLines)
{
// A burst larger than the cap: keep only the newest lines.
_consoleLines.Clear();
for (var i = incoming.Count - MaxConsoleLines; i < incoming.Count; i++)
{
_consoleLines.Add(incoming[i]);
}
}
else
{
var overflow = _consoleLines.Count + incoming.Count - MaxConsoleLines;
for (var i = 0; i < overflow; i++)
{
_consoleLines.RemoveAt(0);
}
foreach (var line in incoming)
{
_consoleLines.Add(line);
}
}
_autoScrollTicks = 3;
}
private void AppendConsoleLine(string text, IBrush brush)
{
_consoleLines.Add(new LogLine(text, brush));
_autoScrollTicks = 3;
MaybeAutoScroll();
}
private void MaybeAutoScroll()
{
// ScrollToEnd is applied over a few flush-timer ticks because the
// virtualizing panel re-estimates its extent after large batches, and
// a single scroll can land short of the true end. A synchronous
// ScrollIntoView during rapid adds is avoided entirely — it can crash
// the panel with "Invalid Arrange rectangle".
if (_autoScrollTicks <= 0 || AutoScrollCheck.IsChecked != true)
{
return;
}
_autoScrollTicks--;
(ConsoleList.Scroll as ScrollViewer)?.ScrollToEnd();
}
private static IBrush BrushForLine(string line)
{
if (line.Contains("[ERROR]", StringComparison.Ordinal) ||
line.Contains("[CRITICAL]", StringComparison.Ordinal))
{
return ErrorLineBrush;
}
if (line.Contains("[WARNING]", StringComparison.Ordinal))
{
return WarningLineBrush;
}
if (line.Contains("[INFO]", StringComparison.Ordinal))
{
return InfoLineBrush;
}
if (line.Contains("[DEBUG]", StringComparison.Ordinal) ||
line.Contains("[TRACE]", StringComparison.Ordinal))
{
return DimLineBrush;
}
return DefaultLineBrush;
}
private async Task CopyConsoleAsync()
{
if (_consoleLines.Count == 0 || Clipboard is null)
{
return;
}
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
await Clipboard.SetTextAsync(text);
}
}
+28
View File
@@ -0,0 +1,28 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<!-- Class library: the GUI is hosted by SharpEmu.CLI, which opens it when
the executable is started without arguments. -->
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Tmds.DBus.Protocol" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</ItemGroup>
</Project>
+186
View File
@@ -0,0 +1,186 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Avalonia": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Tmds.DBus.Protocol": {
"type": "Direct",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
}
}
}
}
+64 -9
View File
@@ -2,25 +2,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.HLE;
public sealed class CpuContext
public sealed class CpuContext(ICpuMemory memory, Generation generation)
{
private readonly ulong[] _registers = new ulong[16];
private readonly ulong[] _xmmRegisters = new ulong[32];
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
private bool _raxWritten;
public CpuContext(ICpuMemory memory, Generation generation)
{
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
TargetGeneration = generation;
}
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
public ICpuMemory Memory { get; }
public Generation TargetGeneration { get; }
public Generation TargetGeneration { get; } = generation;
public ulong Rip { get; set; }
@@ -130,6 +125,39 @@ public sealed class CpuContext
SetYmmUpper(registerIndex, highLow, highHigh);
}
public bool TryReadByte(ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
public bool TryReadUInt32(ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
public bool TryWriteUInt32(ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryReadUInt64(ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
@@ -150,6 +178,33 @@ public sealed class CpuContext
return Memory.TryWrite(address, buffer);
}
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
{
value = string.Empty;
if (address == 0 || capacity <= 0)
{
return false;
}
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
{
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
{
return false;
}
if (bytes[index] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
public bool PushUInt64(ulong value)
{
var rsp = this[CpuRegister.Rsp];
+456
View File
@@ -0,0 +1,456 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.HLE;
public readonly record struct GuestThreadStartRequest(
ulong ThreadHandle,
ulong EntryPoint,
ulong Argument,
ulong AttributeAddress,
string Name,
int Priority,
ulong AffinityMask);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
string Name,
string State,
long ImportCount,
string? LastImportNid,
ulong LastReturnRip,
string? BlockReason);
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
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);
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong stackAddress,
ulong stackSize,
string reason,
out string? error);
bool TryCallGuestContinuation(
CpuContext callerContext,
GuestCpuContinuation continuation,
string reason,
out string? error);
}
public readonly record struct GuestImportCallFrame(
bool IsValid,
ulong ReturnRip,
ulong ResumeRsp,
ulong ReturnSlotAddress);
public readonly record struct GuestCpuContinuation(
ulong Rip,
ulong Rsp,
ulong ReturnSlotAddress,
ulong Rflags,
ulong FsBase,
ulong GsBase,
ulong Rax,
ulong Rcx,
ulong Rdx,
ulong Rbx,
ulong Rbp,
ulong Rsi,
ulong Rdi,
ulong R8,
ulong R9,
ulong R12,
ulong R13,
ulong R14,
ulong R15);
public static class GuestThreadExecution
{
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingBlockContinuationValid;
[ThreadStatic]
private static GuestCpuContinuation _pendingBlockContinuation;
[ThreadStatic]
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static Func<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
[ThreadStatic]
private static bool _pendingEntryExit;
[ThreadStatic]
private static ulong _pendingEntryExitValue;
[ThreadStatic]
private static string? _pendingEntryExitReason;
[ThreadStatic]
private static bool _pendingContextTransfer;
[ThreadStatic]
private static GuestCpuContinuation _pendingContextTransferTarget;
[ThreadStatic]
private static bool _hasCurrentImportCallFrame;
[ThreadStatic]
private static ulong _currentImportReturnRip;
[ThreadStatic]
private static ulong _currentImportResumeRsp;
[ThreadStatic]
private static ulong _currentImportReturnSlotAddress;
public static IGuestThreadScheduler? Scheduler { get; set; }
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
public static ulong CurrentFiberAddress => _currentFiberAddress;
public static ulong EnterGuestThread(ulong threadHandle)
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
return previous;
}
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
}
public static ulong EnterFiber(ulong fiberAddress)
{
var previous = _currentFiberAddress;
_currentFiberAddress = fiberAddress;
return previous;
}
public static void RestoreFiber(ulong previousFiberAddress)
{
_currentFiberAddress = previousFiberAddress;
}
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey = null,
Func<int>? resumeHandler = null,
Func<bool>? wakeHandler = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
{
return false;
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockResumeHandler = resumeHandler;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
_pendingBlockContinuation = continuation;
_pendingBlockContinuationValid = true;
}
else
{
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
}
return true;
}
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out _,
out _,
out _,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out resumeHandler,
out wakeHandler,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
resumeHandler = null;
wakeHandler = null;
blockDeadlineTimestamp = 0;
return false;
}
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
{
return Stopwatch.GetTimestamp();
}
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
? long.MaxValue
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
var now = Stopwatch.GetTimestamp();
if (long.MaxValue - now <= ticks)
{
return long.MaxValue;
}
return now + Math.Max(1, ticks);
}
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
{
if (!TryGetCurrentImportCallFrame(out var frame) ||
frame.ReturnRip < 65536 ||
frame.ResumeRsp == 0 ||
frame.ReturnSlotAddress == 0)
{
continuation = default;
return false;
}
continuation = new GuestCpuContinuation(
frame.ReturnRip,
frame.ResumeRsp,
frame.ReturnSlotAddress,
context.Rflags,
context.FsBase,
context.GsBase,
0,
context[CpuRegister.Rcx],
context[CpuRegister.Rdx],
context[CpuRegister.Rbx],
context[CpuRegister.Rbp],
context[CpuRegister.Rsi],
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15]);
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
}
public static void RequestCurrentEntryExit(string reason, ulong value)
{
_pendingEntryExit = true;
_pendingEntryExitValue = value;
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
}
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
{
value = _pendingEntryExitValue;
reason = _pendingEntryExitReason ?? string.Empty;
if (!_pendingEntryExit)
{
return false;
}
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
return true;
}
public static void RequestCurrentContextTransfer(GuestCpuContinuation target)
{
_pendingContextTransferTarget = target;
_pendingContextTransfer = true;
}
public static bool TryConsumeCurrentContextTransfer(out GuestCpuContinuation target)
{
target = _pendingContextTransferTarget;
if (!_pendingContextTransfer)
{
return false;
}
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
return true;
}
public static GuestImportCallFrame EnterImportCallFrame(
ulong returnRip,
ulong resumeRsp,
ulong returnSlotAddress)
{
var previous = new GuestImportCallFrame(
_hasCurrentImportCallFrame,
_currentImportReturnRip,
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
_hasCurrentImportCallFrame = true;
_currentImportReturnRip = returnRip;
_currentImportResumeRsp = resumeRsp;
_currentImportReturnSlotAddress = returnSlotAddress;
return previous;
}
public static void RestoreImportCallFrame(GuestImportCallFrame previous)
{
_hasCurrentImportCallFrame = previous.IsValid;
_currentImportReturnRip = previous.ReturnRip;
_currentImportResumeRsp = previous.ResumeRsp;
_currentImportReturnSlotAddress = previous.ReturnSlotAddress;
}
public static bool TryGetCurrentImportCallFrame(out GuestImportCallFrame frame)
{
if (!_hasCurrentImportCallFrame)
{
frame = default;
return false;
}
frame = new GuestImportCallFrame(
true,
_currentImportReturnRip,
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
return true;
}
}
@@ -0,0 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public interface IGuestMemoryAllocator
{
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
}
+4
View File
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
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,140 @@
// 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 (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0 ||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
{
return false;
}
var resourceOffsets = new ulong[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!ctx.TryReadUInt64(
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;
}
}
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;
}
}
+920
View File
@@ -0,0 +1,920 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
public static class AmprExports
{
private const int CommandBufferHeaderSize = 0x28;
private const ulong CommandBufferSelfOffset = 0x00;
private const ulong CommandBufferDataOffset = 0x08;
private const ulong CommandBufferSizeOffset = 0x10;
private const ulong CommandBufferAux0Offset = 0x18;
private const ulong CommandBufferAux1Offset = 0x20;
private const ulong ReadFileRecordSize = 0x30;
private const ulong KernelEventQueueRecordSize = 0x30;
private const ulong WriteAddressRecordSize = 0x20;
private const uint ReadFileRecordType = 1;
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
{
public ulong Buffer;
public ulong Size;
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",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0: 0, aux1: 0, clear: true))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "ctor", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "a8uLzYY--tM",
ExportName = "sceAmprAprCommandBufferConstructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var aux0 = ctx[CpuRegister.Rsi];
var aux1 = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "apr_ctor", commandBuffer, aux0, aux1);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Qs1xtplKo0U",
ExportName = "sceAmprAprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
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;
}
TraceAmpr(ctx, "apr_dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GuchCTefuZw",
ExportName = "sceAmprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "N-FSPA4S3nI",
ExportName = "sceAmprCommandBufferSetBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferSetBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "set_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "baQO9ez2gL4",
ExportName = "sceAmprCommandBufferReset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferReset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
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;
}
TraceAmpr(ctx, "reset", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ULvXMDz56po",
ExportName = "sceAmprCommandBufferClearBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferClearBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var size, out _) ||
!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "clear_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = buffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mQ16-QdKv7k",
ExportName = "sceAmprAprCommandBufferReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferReadFile(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var fileId = unchecked((uint)ctx[CpuRegister.Rcx]);
var destination = ctx[CpuRegister.R8];
var size = ctx[CpuRegister.R9];
if (commandBuffer == 0 || (destination == 0 && size != 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var fileOffset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
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;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
return result;
}
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vWU-odnS+fU",
ExportName = "sceAmprMeasureCommandSizeReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeReadFile(CpuContext ctx)
{
TraceAmpr(ctx, "measure_read_file", 0, ReadFileRecordSize, 0);
ctx[CpuRegister.Rax] = ReadFileRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sSAUCCU1dv4",
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteKernelEventQueue0400(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_equeue", 0, KernelEventQueueRecordSize, 0);
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteAddressOnCompletion(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_address_complete", 0, WriteAddressRecordSize, 0);
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tZDDEo2tE5k",
ExportName = "sceAmprCommandBufferGetSize",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetSize(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out var size, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_size", commandBuffer, size, 0);
ctx[CpuRegister.Rax] = size;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GnxKOHEawhk",
ExportName = "sceAmprCommandBufferGetCurrentOffset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetCurrentOffset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferOffset(ctx, commandBuffer, out var offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_offset", commandBuffer, offset, 0);
ctx[CpuRegister.Rax] = offset;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H896Pt-yB4I",
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteKernelEventQueue0400(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendKernelEventQueueRecord(
ctx,
commandBuffer,
equeue,
ident,
completionToken,
userData))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteAddressOnCompletion(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
var value = ctx[CpuRegister.Rdx];
if (commandBuffer == 0 || address == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_address_complete", commandBuffer, address, value);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
{
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out _, out var state) || state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong writeOffset;
lock (state)
{
writeOffset = state.WriteOffset;
}
var offset = 0UL;
while (offset < writeOffset)
{
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
switch (recordType)
{
case ReadFileRecordType:
offset += ReadFileRecordSize;
break;
case KernelEventQueueRecordType:
if (!CompleteKernelEventQueueRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += KernelEventQueueRecordSize;
break;
case WriteAddressRecordType:
if (!CompleteWriteAddressRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += WriteAddressRecordSize;
break;
default:
TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
TraceAmpr(ctx, "complete", commandBuffer, buffer, writeOffset);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool InitializeCommandBuffer(
CpuContext ctx,
ulong commandBuffer,
ulong buffer,
ulong size,
ulong aux0,
ulong aux1,
bool clear)
{
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
if (clear)
{
header.Clear();
}
else
{
if (!ctx.Memory.TryRead(commandBuffer, header))
{
return false;
}
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
}
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)
{
return WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size, ulong writeOffset)
{
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
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)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = writeOffset;
}
}
private static bool TryGetCommandBufferState(
CpuContext ctx,
ulong commandBuffer,
out ulong buffer,
out ulong size,
out CommandBufferState? state)
{
if (_commandBuffers.TryGetValue(commandBuffer, out state))
{
lock (state)
{
buffer = state.Buffer;
size = state.Size;
}
return true;
}
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)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = 0;
}
return true;
}
buffer = 0;
size = 0;
state = null;
return false;
}
private static bool TryGetCommandBufferOffset(CpuContext ctx, ulong commandBuffer, out ulong offset)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
offset = 0;
return false;
}
lock (state)
{
offset = state.WriteOffset;
}
return true;
}
private static int TryReadFileToGuestMemory(
CpuContext ctx,
string hostPath,
ulong fileOffset,
ulong destination,
ulong size,
out ulong bytesRead)
{
bytesRead = 0;
if (size == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (fileOffset > long.MaxValue)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
const int ChunkSize = 1024 * 1024;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
try
{
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;
}
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);
int read;
lock (cachedFile.Gate)
{
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
if (read <= 0)
{
break;
}
if (!ctx.Memory.TryWrite(destination + bytesRead, buffer.AsSpan(0, read)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
bytesRead += (ulong)read;
}
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
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,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead)
{
Span<byte> record = stackalloc byte[(int)ReadFileRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], ReadFileRecordType);
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendKernelEventQueueRecord(
CpuContext ctx,
ulong commandBuffer,
ulong equeue,
ulong ident,
ulong completionToken,
ulong userData)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan<byte> record)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return false;
}
var recordSize = (ulong)record.Length;
lock (state)
{
if (state.Buffer == 0 ||
state.WriteOffset > state.Size ||
recordSize > state.Size - state.WriteOffset)
{
return false;
}
if (!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record))
{
return false;
}
state.WriteOffset += recordSize;
}
return true;
}
private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var filter = unchecked((short)BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
var equeue = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var ident = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
var userData = BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]);
var data = BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]);
var extra = BinaryPrimitives.ReadUInt64LittleEndian(record[0x28..]);
var queuedEvent = new KernelEventQueueCompatExports.KernelQueuedEvent(
ident,
filter,
0x20,
unchecked((uint)extra),
data,
userData);
_ = KernelEventQueueCompatExports.EnqueueEvent(equeue, queuedEvent);
TraceAmpr(ctx, "complete_equeue", equeue, ident, data);
return true;
}
private static bool CompleteWriteAddressRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var address = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var value = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
if (!ctx.TryWriteUInt64(address, value))
{
return false;
}
TraceAmpr(ctx, "complete_write_address", address, value, 0);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!_traceAmpr)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.{operation}: cmd=0x{commandBuffer:X16} arg0=0x{arg0:X16} arg1=0x{arg1:X16} ret=0x{returnRip:X16}");
}
private static void TraceAmprRead(
CpuContext ctx,
ulong commandBuffer,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead,
string? hostPath,
int result)
{
if (!_traceAmprReads)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.read_file: cmd=0x{commandBuffer:X16} id=0x{fileId:X8} dst=0x{destination:X16} size=0x{size:X16} offset=0x{fileOffset:X16} read=0x{bytesRead:X16} result=0x{result:X8} path='{hostPath ?? string.Empty}' ret=0x{returnRip:X16}");
}
}
@@ -0,0 +1,40 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
internal static class AmprFileRegistry
{
private static readonly ConcurrentDictionary<uint, string> _hostPathsById = new();
public static uint Register(string guestPath, string hostPath)
{
var id = ComputeFileId(guestPath);
_hostPathsById[id] = hostPath;
return id;
}
public static bool TryGetHostPath(uint id, out string hostPath)
{
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
const uint offsetBasis = 2166136261;
const uint prime = 16777619;
var hash = offsetBasis;
foreach (var b in bytes)
{
hash ^= b;
hash *= prime;
}
return hash;
}
}
@@ -2,11 +2,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Text.Json;
namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
private const int AppParamSkuFlagFull = 3;
[SysAbiExport(
Nid = "R9lA82OraNs",
ExportName = "sceAppContentInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentInitialize(CpuContext ctx)
{
var initParamAddress = ctx[CpuRegister.Rdi];
var bootParamAddress = ctx[CpuRegister.Rsi];
if (initParamAddress == 0 || bootParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> attrBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(attrBytes, 0);
if (!ctx.Memory.TryWrite(bootParamAddress + BootParamAttrOffset, attrBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "xnd8BJzAxmk",
ExportName = "sceAppContentGetAddcontInfoList",
@@ -14,7 +47,165 @@ public static class AppContentExports
LibraryName = "libSceAppContent")]
public static int AppContentGetAddcontInfoList(CpuContext ctx)
{
_ = ctx;
var hitCountAddress = ctx[CpuRegister.Rcx];
if (hitCountAddress != 0)
{
Span<byte> hitCountBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(hitCountBytes, 0);
if (!ctx.Memory.TryWrite(hitCountAddress, hitCountBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "99b82IKXpH4",
ExportName = "sceAppContentAppParamGetInt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentAppParamGetInt(CpuContext ctx)
{
var paramId = (uint)ctx[CpuRegister.Rdi];
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int value;
if (paramId == AppParamSkuFlag)
{
value = AppParamSkuFlagFull;
}
else if (!TryReadUserDefinedParam(paramId, out value))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAppContent($"app_param_get_int id={paramId} value={value}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentTemporaryDataMount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rsi];
if (mountPointAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Directory.CreateDirectory(ResolveTemp0Root());
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadUserDefinedParam(uint paramId, out int value)
{
value = 0;
if (paramId is < 1 or > 4)
{
return false;
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return true;
}
var paramJsonPath = Path.Combine(app0Root, "sce_sys", "param.json");
if (!File.Exists(paramJsonPath))
{
return true;
}
try
{
using var stream = File.OpenRead(paramJsonPath);
using var document = JsonDocument.Parse(stream);
var propertyName = $"userDefinedParam{paramId}";
if (document.RootElement.TryGetProperty(propertyName, out var element) &&
element.TryGetInt32(out var parsedValue))
{
value = parsedValue;
}
return true;
}
catch (IOException)
{
return true;
}
catch (UnauthorizedAccessException)
{
return true;
}
catch (JsonException)
{
return true;
}
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceAppContent(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
}
private static string ResolveTemp0Root()
{
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
if (!string.IsNullOrWhiteSpace(configuredRoot))
{
return Path.GetFullPath(configuredRoot);
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var appName = string.IsNullOrWhiteSpace(app0Root)
? "default"
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (string.IsNullOrWhiteSpace(appName))
{
appName = "default";
}
var invalidChars = Path.GetInvalidFileNameChars();
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0");
Environment.SetEnvironmentVariable(temp0VariableName, root);
return root;
}
}
+224
View File
@@ -0,0 +1,224 @@
// 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.Audio;
public static class AudioOut2Exports
{
private const int AudioOut2ContextParamSize = 0x80;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
[SysAbiExport(
Nid = "g2tViFIohHE",
ExportName = "sceAudioOut2Initialize",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2Initialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "t5YrizufpQc",
ExportName = "sceAudioOut2ContextResetParam",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextResetParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
param.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "pDmme7Bgm6E",
ExportName = "sceAudioOut2ContextQueryMemory",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "0x6o1VVAYSY",
ExportName = "sceAudioOut2ContextCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextCreate(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryAddress = ctx[CpuRegister.Rsi];
var memorySize = ctx[CpuRegister.Rdx];
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "on6ZH7Abo10",
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2PortCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortCreate(CpuContext ctx)
{
var type = unchecked((int)ctx[CpuRegister.Rdi]);
var paramAddress = ctx[CpuRegister.Rsi];
var outPortAddress = ctx[CpuRegister.Rdx];
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortGetState(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
Span<byte> state = stackalloc byte[0x20];
state.Clear();
var output = type == 2 ? 0x40 : 0x01;
var channels = type == 2 ? 1 : 2;
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
state[0x02] = unchecked((byte)channels);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "DImz2Ft9E2g",
ExportName = "sceAudioOut2GetSpeakerInfo",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cd+Rtw+D1x8",
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
ExportName = "sceAudioOut2UserCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserCreate(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+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,38 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.CommonDialog;
public static class CommonDialogExports
{
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
private static int _initialized;
[SysAbiExport(
Nid = "uoUpLGNkygk",
ExportName = "sceCommonDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceCommonDialog")]
public static int CommonDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadySystemInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[SysAbiExport(
Nid = "BQ3tey0JmQM",
ExportName = "sceCommonDialogIsUsed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceCommonDialog")]
public static int CommonDialogIsUsed(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -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;
}
}
+964
View File
@@ -0,0 +1,964 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Fiber;
public static class FiberExports
{
private const int MaxNameLength = 31;
private const int FiberInfoSize = 128;
private const int FiberContextMinimumSize = 512;
private const uint FiberSignature0 = 0xDEF1649C;
private const uint FiberSignature1 = 0xB37592A0;
private const uint FiberOptSignature = 0xBB40E64D;
private const ulong FiberStackSignature = 0x7149F2CA7149F2CAUL;
private const ulong FiberStackSizeCheck = 0xDEADBEEFDEADBEEFUL;
private const uint FiberStateRun = 1;
private const uint FiberStateIdle = 2;
private const uint FiberStateTerminated = 3;
private const uint FiberFlagContextSizeCheck = 0x10;
private const int FiberErrorNull = unchecked((int)0x80590001);
private const int FiberErrorAlignment = unchecked((int)0x80590002);
private const int FiberErrorRange = unchecked((int)0x80590003);
private const int FiberErrorInvalid = unchecked((int)0x80590004);
private const int FiberErrorPermission = unchecked((int)0x80590005);
private const int FiberErrorState = unchecked((int)0x80590006);
private const int FiberMagicStartOffset = 0;
private const int FiberStateOffset = 4;
private const int FiberEntryOffset = 8;
private const int FiberArgOnInitializeOffset = 16;
private const int FiberContextAddressOffset = 24;
private const int FiberContextSizeOffset = 32;
private const int FiberNameOffset = 40;
private const int FiberContextPointerOffset = 72;
private const int FiberFlagsOffset = 80;
private const int FiberContextStartOffset = 88;
private const int FiberContextEndOffset = 96;
private const int FiberMagicEndOffset = 104;
private static int _contextSizeCheck;
[ThreadStatic]
private static ulong _currentFiberAddress;
private static readonly object _fiberGate = new();
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
[SysAbiExport(
Nid = "hVYD7Ou2pCQ",
ExportName = "_sceFiberInitializeImpl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberInitialize(CpuContext ctx)
{
var optParam = ReadStackArg64(ctx, 0);
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags: 0);
}
[SysAbiExport(
Nid = "7+OJIpko9RY",
ExportName = "_sceFiberInitializeWithInternalOptionImpl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberInitializeWithInternalOption(CpuContext ctx)
{
var optParam = ReadStackArg64(ctx, 0);
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags);
}
[SysAbiExport(
Nid = "asjUJJ+aa8s",
ExportName = "sceFiberOptParamInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberOptParamInitialize(CpuContext ctx)
{
var optParam = ctx[CpuRegister.Rdi];
if (optParam == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
if ((optParam & 7) != 0)
{
return SetReturn(ctx, FiberErrorAlignment);
}
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
Nid = "JeNX5F-NzQU",
ExportName = "sceFiberFinalize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberFinalize(CpuContext ctx)
{
var fiber = ctx[CpuRegister.Rdi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (state != FiberStateIdle)
{
return SetReturn(ctx, FiberErrorState);
}
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "a0LLrZWac0M",
ExportName = "sceFiberRun",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberRun(CpuContext ctx)
{
return FiberRunCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
attachContextAddress: 0,
attachContextSize: 0,
reason: "sceFiberRun",
isSwitch: false);
}
[SysAbiExport(
Nid = "avfGJ94g36Q",
ExportName = "_sceFiberAttachContextAndRun",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberAttachContextAndRun(CpuContext ctx)
{
return FiberRunCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
attachContextAddress: ctx[CpuRegister.Rsi],
attachContextSize: ctx[CpuRegister.Rdx],
reason: "_sceFiberAttachContextAndRun",
isSwitch: false);
}
[SysAbiExport(
Nid = "PFT2S-tJ7Uk",
ExportName = "sceFiberSwitch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberSwitch(CpuContext ctx)
{
return FiberRunCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
attachContextAddress: 0,
attachContextSize: 0,
reason: "sceFiberSwitch",
isSwitch: true);
}
[SysAbiExport(
Nid = "ZqhZFuzKT6U",
ExportName = "_sceFiberAttachContextAndSwitch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberAttachContextAndSwitch(CpuContext ctx)
{
return FiberRunCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
attachContextAddress: ctx[CpuRegister.Rsi],
attachContextSize: ctx[CpuRegister.Rdx],
reason: "_sceFiberAttachContextAndSwitch",
isSwitch: true);
}
[SysAbiExport(
Nid = "B0ZX2hx9DMw",
ExportName = "sceFiberReturnToThread",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberReturnToThread(CpuContext ctx)
{
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return SetReturn(ctx, FiberErrorPermission);
}
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
{
return SetReturn(ctx, FiberErrorInvalid);
}
GuestCpuContinuation transferTarget;
ulong previousFiber;
lock (_fiberGate)
{
_continuations[fiberAddress] = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
argOnRunAddress);
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorPermission);
}
previousFiber = returnTarget.PreviousFiber;
if (previousFiber != 0)
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState);
}
transferTarget = previousContinuation.Context with { Rax = 0 };
}
else
{
if (!returnTarget.ThreadContinuation.HasValue ||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState);
}
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
}
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorInvalid);
}
}
_currentFiberAddress = previousFiber;
_ = GuestThreadExecution.EnterFiber(previousFiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "p+zLIOg27zU",
ExportName = "sceFiberGetSelf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberGetSelf(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
Nid = "uq2Y5BFz0PE",
ExportName = "sceFiberGetInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberGetInfo(CpuContext ctx)
{
var fiber = ctx[CpuRegister.Rdi];
var info = ctx[CpuRegister.Rsi];
if (info == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
if (!TryValidateFiber(ctx, fiber, out var error))
{
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
!TryWriteName(ctx, info + 40, fields.Name) ||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
{
return SetReturn(ctx, FiberErrorInvalid);
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "JzyT91ucGDc",
ExportName = "sceFiberRename",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberRename(CpuContext ctx)
{
var fiber = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return SetReturn(ctx, error);
}
if (nameAddress == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{
return SetReturn(ctx, FiberErrorInvalid);
}
return TryWriteName(ctx, fiber + FiberNameOffset, name)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
Nid = "Lcqty+QNWFc",
ExportName = "sceFiberStartContextSizeCheck",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberStartContextSizeCheck(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] != 0)
{
return SetReturn(ctx, FiberErrorInvalid);
}
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
Nid = "Kj4nXMpnM8Y",
ExportName = "sceFiberStopContextSizeCheck",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberStopContextSizeCheck(CpuContext ctx)
{
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
Nid = "0dy4JtMUcMQ",
ExportName = "_sceFiberGetThreadFramePointerAddress",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceFiber")]
public static int FiberGetThreadFramePointerAddress(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
if (ResolveCurrentFiberAddress(ctx) == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
private static int FiberInitializeCore(
CpuContext ctx,
ulong fiber,
ulong nameAddress,
ulong entry,
ulong argOnInitialize,
ulong contextAddress,
ulong contextSize,
ulong optParam,
uint flags)
{
if (fiber == 0 || nameAddress == 0 || entry == 0)
{
return SetReturn(ctx, FiberErrorNull);
}
if ((fiber & 7) != 0 ||
(contextAddress & 15) != 0 ||
(optParam & 7) != 0)
{
return SetReturn(ctx, FiberErrorAlignment);
}
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{
return SetReturn(ctx, FiberErrorRange);
}
if ((contextSize & 15) != 0 ||
(contextAddress == 0 && contextSize != 0) ||
(contextAddress != 0 && contextSize == 0))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (optParam != 0 &&
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (Volatile.Read(ref _contextSizeCheck) != 0)
{
flags |= FiberFlagContextSizeCheck;
}
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (contextAddress != 0)
{
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if ((flags & FiberFlagContextSizeCheck) != 0)
{
FillContextSizeCheck(ctx, contextAddress + sizeof(ulong), contextSize - sizeof(ulong));
}
}
if (contextAddress != 0 && contextSize != 0)
{
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
}
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
return SetReturn(ctx, 0);
}
private static int FiberRunCore(
CpuContext ctx,
ulong fiber,
ulong argOnRun,
ulong outArgumentAddress,
ulong attachContextAddress,
ulong attachContextSize,
string reason,
bool isSwitch)
{
if (!TryValidateFiber(ctx, fiber, out var error))
{
return SetReturn(ctx, error);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (attachContextAddress != 0 || attachContextSize != 0)
{
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
if (attachResult != 0)
{
return SetReturn(ctx, attachResult);
}
}
var previousFiber = ResolveCurrentFiberAddress(ctx);
if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0))
{
return SetReturn(ctx, FiberErrorPermission);
}
if (previousFiber == fiber)
{
return SetReturn(ctx, FiberErrorState);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return SetReturn(ctx, FiberErrorPermission);
}
GuestCpuContinuation transferTarget;
var resumed = false;
lock (_fiberGate)
{
if (!TryReadFiberFields(ctx, fiber, out fields))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorState);
}
FiberContinuation targetContinuation;
if (_continuations.TryGetValue(fiber, out var savedContinuation))
{
targetContinuation = savedContinuation;
resumed = true;
}
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return SetReturn(ctx, FiberErrorInvalid);
}
var callerContinuation = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
outArgumentAddress);
if (previousFiber != 0)
{
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorState);
}
_continuations[previousFiber] = callerContinuation;
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
}
else
{
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
}
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed)
{
_continuations.TryRemove(fiber, out _);
}
transferTarget = targetContinuation.Context with { Rax = 0 };
}
_currentFiberAddress = fiber;
_ = GuestThreadExecution.EnterFiber(fiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return SetReturn(ctx, 0);
}
private static bool TryCreateInitialContinuation(
CpuContext ctx,
FiberFields fields,
ulong argOnRun,
out FiberContinuation continuation)
{
continuation = default;
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
{
return false;
}
var stackEnd = fields.ContextAddress + fields.ContextSize;
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
if (!ctx.TryWriteUInt64(entryRsp, 0))
{
return false;
}
continuation = new FiberContinuation(
new GuestCpuContinuation(
fields.Entry,
entryRsp,
entryRsp,
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
ctx.FsBase,
ctx.GsBase,
0,
0,
0,
0,
0,
argOnRun,
fields.ArgOnInitialize,
0,
0,
0,
0,
0,
0),
0);
return true;
}
private static bool TryWriteResumeArgument(
CpuContext ctx,
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
ulong resumeRip,
ulong resumeRsp,
ulong returnSlotAddress) =>
new(
resumeRip,
resumeRsp,
returnSlotAddress,
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
ctx.FsBase,
ctx.GsBase,
0,
ctx[CpuRegister.Rcx],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rbx],
ctx[CpuRegister.Rbp],
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdi],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
ctx[CpuRegister.R12],
ctx[CpuRegister.R13],
ctx[CpuRegister.R14],
ctx[CpuRegister.R15]);
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
{
if (_currentFiberAddress != 0)
{
return _currentFiberAddress;
}
if (GuestThreadExecution.CurrentFiberAddress != 0)
{
return GuestThreadExecution.CurrentFiberAddress;
}
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
}
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
ResolveCurrentFiberAddress(ctx);
private static int AttachContext(
CpuContext ctx,
ulong fiber,
ulong contextAddress,
ulong contextSize,
ref FiberFields fields)
{
if ((contextAddress & 15) != 0)
{
return FiberErrorAlignment;
}
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{
return FiberErrorRange;
}
if ((contextSize & 15) != 0 ||
contextAddress == 0 ||
contextSize == 0 ||
fields.ContextAddress != 0)
{
return FiberErrorInvalid;
}
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
{
return FiberErrorInvalid;
}
fields = fields with
{
ContextAddress = contextAddress,
ContextSize = contextSize,
};
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
return 0;
}
private static bool TryFindFiberByStack(CpuContext ctx, out ulong fiber)
{
if (TryFindFiberByStackAddress(ctx[CpuRegister.Rsp], out fiber))
{
return true;
}
return TryFindFiberByStackAddress(ctx[CpuRegister.Rbp], out fiber);
}
private static bool TryFindFiberByStackAddress(ulong address, out ulong fiber)
{
if (address != 0)
{
foreach (var (candidate, range) in _stackRanges)
{
if (range.Contains(address))
{
fiber = candidate;
return true;
}
}
}
fiber = 0;
return false;
}
private static bool TryValidateFiber(CpuContext ctx, ulong fiber, out int error)
{
if (fiber == 0)
{
error = FiberErrorNull;
return false;
}
if ((fiber & 7) != 0)
{
error = FiberErrorAlignment;
return false;
}
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
magicStart != FiberSignature0 ||
magicEnd != FiberSignature1)
{
error = FiberErrorInvalid;
return false;
}
error = 0;
return true;
}
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
{
fields = default;
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
{
return false;
}
fields = new FiberFields(
state,
entry,
argOnInitialize,
contextAddress,
contextSize,
flags,
name);
return true;
}
private static void FillContextSizeCheck(CpuContext ctx, ulong address, ulong size)
{
Span<byte> value = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(value, FiberStackSizeCheck);
var end = address + size;
for (var current = address; current + sizeof(ulong) <= end; current += sizeof(ulong))
{
_ = ctx.Memory.TryWrite(current, value);
}
}
private static ulong ReadStackArg64(CpuContext ctx, int index)
{
if (ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong) + ((ulong)index * sizeof(ulong)), out var value))
{
return value;
}
return 0;
}
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
var bytes = Encoding.UTF8.GetBytes(name);
bytes.AsSpan(0, Math.Min(bytes.Length, MaxNameLength)).CopyTo(buffer);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = string.Empty;
return false;
}
var length = buffer.IndexOf((byte)0);
if (length < 0)
{
length = buffer.Length;
}
value = Encoding.UTF8.GetString(buffer[..length]);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceFiber(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
}
}
private readonly record struct FiberFields(
uint State,
ulong Entry,
ulong ArgOnInitialize,
ulong ContextAddress,
ulong ContextSize,
uint Flags,
string Name);
private readonly record struct FiberContinuation(
GuestCpuContinuation Context,
ulong ArgOnRunAddress);
private readonly record struct FiberReturnTarget(
ulong PreviousFiber,
FiberContinuation? ThreadContinuation);
private readonly record struct FiberStackRange(ulong Start, ulong Size)
{
public bool Contains(ulong address) =>
Size != 0 && address >= Start && address < Start + Size;
}
}
@@ -0,0 +1,24 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.GameUpdate;
public static class GameUpdateExports
{
private static int _initialized;
[SysAbiExport(
Nid = "YJtKLttI9fM",
ExportName = "sceGameUpdateInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceGameUpdate")]
public static int GameUpdateInitialize(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 1);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Json;
public static class JsonExports
{
[SysAbiExport(
Nid = "-hJRce8wn1U",
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int MemAllocatorConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("MemAllocator.ctor", thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "OcAgPxcq5Vk",
ExportName = "_ZN3sce4Json12MemAllocatorD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int MemAllocatorDestructor(CpuContext ctx)
{
TraceJson("MemAllocator.dtor", ctx[CpuRegister.Rdi], 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "cK6bYHf-Q5E",
ExportName = "_ZN3sce4Json11InitializerC1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer.ctor", thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RujUxbr3haM",
ExportName = "_ZN3sce4Json11InitializerD1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerDestructor(CpuContext ctx)
{
TraceJson("Initializer.dtor", ctx[CpuRegister.Rdi], 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Cxwy7wHq4J0",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_13InitParameterE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerInitialize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
var initParameterAddress = ctx[CpuRegister.Rsi];
if (thisAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
TraceJson("Initializer.initialize", thisAddress, initParameterAddress);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
}
}
@@ -0,0 +1,247 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
public static class KernelAprCompatExports
{
private static readonly ConcurrentDictionary<uint, AprSubmission> _submittedCommandBuffers = new();
private static int _nextSubmissionId;
private static int _aprWaitTraceCount;
private static readonly bool _traceApr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
[SysAbiExport(
Nid = "ASoW5WE-UPo",
ExportName = "sceKernelAprSubmitCommandBufferAndGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBufferAndGetResult(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var priority = ctx[CpuRegister.Rsi];
var resultAddress = ctx[CpuRegister.Rdx];
var outSubmissionId = ctx[CpuRegister.Rcx];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
if (submissionId == 0)
{
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
}
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "submit_get_result", submissionId, commandBuffer, priority, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "rqwFKI4PAiM",
ExportName = "sceKernelAprWaitCommandBuffer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
{
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
var waitArg1 = ctx[CpuRegister.Rsi];
var waitArg2 = ctx[CpuRegister.Rdx];
if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission))
{
TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eE4Szl8sil8",
ExportName = "sceKernelAprSubmitCommandBuffer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qvMUCyyaCSI",
ExportName = "sceKernelAprSubmitCommandBufferAndGetId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBufferAndGetId(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var outSubmissionId = ctx[CpuRegister.Rdx];
if (commandBuffer == 0 || outSubmissionId == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "submit_get_id", submissionId, commandBuffer, ctx[CpuRegister.Rsi], outSubmissionId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
{
Span<byte> result = stackalloc byte[sizeof(ulong)];
result.Clear();
return ctx.Memory.TryWrite(resultAddress, result);
}
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
{
if (waitArg2 == 0)
{
return submittedResultAddress;
}
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
{
return submittedResultAddress;
}
return waitArg2;
}
private static bool IsAmprCompletionToken(ulong value)
{
var tag = value >> 56;
return tag is 0x0C or 0x10;
}
private static void TraceApr(
CpuContext ctx,
string operation,
uint submissionId,
ulong commandBuffer,
ulong priority,
ulong aux)
{
if (!_traceApr)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}");
if (aux != 0 &&
ctx.TryReadUInt64(aux, out var result0) &&
ctx.TryReadUInt64(aux + sizeof(ulong), out var result1))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
}
}
private static void TraceAprWaitFailure(
CpuContext ctx,
string operation,
uint submissionId,
ulong commandBuffer,
ulong priority,
ulong resultAddress)
{
if (!_traceApr)
{
return;
}
var traceCount = Interlocked.Increment(ref _aprWaitTraceCount);
if (traceCount > 32 && (traceCount & 0x3FF) != 0)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " +
$"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " +
$"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}");
TraceReadableQword(ctx, operation, "rsi", priority);
TraceReadableQword(ctx, operation, "rdx", resultAddress);
TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]);
TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]);
}
private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address)
{
if (address == 0 || !ctx.TryReadUInt64(address, out var value))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}");
}
}
@@ -0,0 +1,566 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
namespace SharpEmu.Libs.Kernel;
public static class KernelEventFlagCompatExports
{
private const int MaxEventFlagNameLength = 31;
private const int HostWaitPumpMilliseconds = 1;
private const uint AttrThreadFifo = 0x01;
private const uint AttrThreadPriority = 0x02;
private const uint AttrSingle = 0x10;
private const uint AttrMulti = 0x20;
private const uint WaitAnd = 0x01;
private const uint WaitOr = 0x02;
private const uint ClearAll = 0x10;
private const uint ClearPattern = 0x20;
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1;
private sealed class EventFlagState
{
public required string Name { get; init; }
public required uint Attributes { get; init; }
public ulong Bits { get; set; }
public int WaitingThreads { get; set; }
public object Gate { get; } = new();
}
[SysAbiExport(
Nid = "BpFoboUJoZU",
ExportName = "sceKernelCreateEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCreateEventFlag(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
var attributes = unchecked((uint)ctx[CpuRegister.Rdx]);
var initialPattern = ctx[CpuRegister.Rcx];
var optionAddress = ctx[CpuRegister.R8];
if (outAddress == 0 ||
nameAddress == 0 ||
optionAddress != 0 ||
!IsValidAttributes(attributes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
_eventFlags[handle] = new EventFlagState
{
Name = name,
Attributes = attributes,
Bits = initialPattern,
};
if (!ctx.TryWriteUInt64(outAddress, handle))
{
_eventFlags.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "8mql9OcQnd4",
ExportName = "sceKernelDeleteEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
Monitor.PulseAll(state.Gate);
}
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "IOnSvHzqu6A",
ExportName = "sceKernelSetEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSetEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var pattern = ctx[CpuRegister.Rsi];
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
state.Bits |= pattern;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "7uhBFWRAS60",
ExportName = "sceKernelClearEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelClearEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
state.Bits &= pattern;
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "9lvj5DjHZiA",
ExportName = "sceKernelPollEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelPollEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var pattern = ctx[CpuRegister.Rsi];
var waitMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var resultAddress = ctx[CpuRegister.Rcx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (state.Gate)
{
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "JTvBflhYazQ",
ExportName = "sceKernelWaitEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelWaitEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var pattern = ctx[CpuRegister.Rsi];
var waitMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var resultAddress = ctx[CpuRegister.Rcx];
var timeoutAddress = ctx[CpuRegister.R8];
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
try
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return SetReturn(ctx, immediateWaitResult);
}
if (timeoutAddress != 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
() => (int)blockedWaitResult,
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
return true;
});
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true;
try
{
while (true)
{
Monitor.Exit(state.Gate);
try
{
scheduler.Pump(ctx, "sceKernelWaitEventFlag");
}
finally
{
Monitor.Enter(state.Gate);
}
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult);
}
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
}
}
finally
{
if (releaseWaiter)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
}
state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
Monitor.Exit(state.Gate);
}
}
[SysAbiExport(
Nid = "PZku4ZrXJqg",
ExportName = "sceKernelCancelEventFlag",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCancelEventFlag(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var setPattern = ctx[CpuRegister.Rsi];
var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
state.Bits = setPattern;
state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool IsValidAttributes(uint attributes)
{
var queueMode = attributes & 0x0F;
var threadMode = attributes & 0xF0;
return (queueMode is 0 or AttrThreadFifo or AttrThreadPriority) &&
(threadMode is 0 or AttrSingle or AttrMulti) &&
(attributes & ~0x33u) == 0;
}
private static bool IsValidWaitMode(uint waitMode)
{
var condition = waitMode & 0x0F;
var clearMode = waitMode & 0xF0;
return condition is WaitAnd or WaitOr &&
clearMode is 0 or ClearAll or ClearPattern &&
(waitMode & ~0x33u) == 0;
}
private static bool IsSatisfied(ulong bits, ulong pattern, uint waitMode) =>
(waitMode & 0x0F) == WaitAnd
? (bits & pattern) == pattern
: (bits & pattern) != 0;
private static void ApplyClearMode(EventFlagState state, ulong pattern, uint waitMode)
{
switch (waitMode & 0xF0)
{
case ClearAll:
state.Bits = 0;
break;
case ClearPattern:
state.Bits &= ~pattern;
break;
}
}
private static bool TryCompleteSatisfiedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true;
}
ApplyClearMode(state, pattern, waitMode);
return true;
}
private static bool TryPrepareBlockedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
lock (state.Gate)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
else
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true;
}
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceEventFlag(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
}
}
private static ulong GetCurrentReturnRip() =>
GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip
: 0UL;
private static string FormatFrameChain(CpuContext ctx)
{
Span<ulong> returns = stackalloc ulong[4];
var count = 0;
var frame = ctx[CpuRegister.Rbp];
for (var index = 0; index < returns.Length && frame != 0; index++)
{
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
{
break;
}
returns[count++] = returnAddress;
if (nextFrame <= frame)
{
break;
}
frame = nextFrame;
}
return count switch
{
0 => "none",
1 => $"0x{returns[0]:X16}",
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
};
}
private static string FormatGuestWaitObject(CpuContext ctx)
{
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var objectAddress = r12 != 0
? r12
: r13 >= 0xA8
? r13 - 0xA8
: 0;
var builder = new StringBuilder(256);
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
if (objectAddress == 0)
{
return builder.ToString();
}
builder.Append($" obj=0x{objectAddress:X16}");
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
if (r13 != 0)
{
AppendUInt64(builder, ctx, r13, "r13_0");
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
}
return builder.ToString();
}
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadByte(address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
}
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt32(address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
}
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt64(address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
}
}
@@ -0,0 +1,661 @@
// 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.Kernel;
public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17;
private static readonly object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
private static long _nextEventQueueHandle = 1;
public readonly record struct KernelQueuedEvent(
ulong Ident,
short Filter,
ushort Flags,
uint Fflags,
ulong Data,
ulong UserData);
private readonly record struct KernelEventRegistration(
ulong Ident,
short Filter,
ulong UserData);
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCreateEqueue(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventQueueHandle));
lock (_eventQueueGate)
{
_eventQueues.Add(handle);
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
}
if (!ctx.TryWriteUInt64(outAddress, handle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceEventQueue(ctx, "create", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jpFjmgAC5AE",
ExportName = "sceKernelDeleteEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteEqueue(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
lock (_eventQueueGate)
{
_eventQueues.Remove(handle);
_pendingEvents.Remove(handle);
_registeredEvents.Remove(handle);
}
TraceEventQueue(ctx, "delete", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WDszmSbWuDk",
ExportName = "sceKernelAddUserEventEdge",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4R6-OvI2cEA",
ExportName = "sceKernelAddUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LJDwdSNTnDg",
ExportName = "sceKernelDeleteUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "F6e0kwo4cnk",
ExportName = "sceKernelTriggerUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bBfz7kMF2Ho",
ExportName = "sceKernelAddAmprEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddAmprEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmpr,
ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "add_ampr", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
Nid = "vuae5JPNt9A",
ExportName = "sceKernelAddAmprSystemEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddAmprSystemEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmprSystem,
ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "add_ampr_system", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
Nid = "bMmid3pfyjo",
ExportName = "sceKernelDeleteAmprEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteAmprEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmpr);
TraceEventQueue(ctx, "delete_ampr", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
Nid = "Ij+ryuEClXQ",
ExportName = "sceKernelDeleteAmprSystemEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmprSystem);
TraceEventQueue(ctx, "delete_ampr_system", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
Nid = "QyrxcdBrb0M",
ExportName = "sceKernelGetKqueueFromEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetKqueueFromEqueue(CpuContext ctx)
{
ctx[CpuRegister.Rax] = ctx[CpuRegister.Rdi];
TraceEventQueue(ctx, "get_kqueue", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vz+pg2zdopI",
ExportName = "sceKernelGetEventUserData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventUserData(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData);
ctx[CpuRegister.Rax] = userData;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mJ7aghmgvfc",
ExportName = "sceKernelGetEventId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventId(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident);
ctx[CpuRegister.Rax] = ident;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "23CPPI1tyBY",
ExportName = "sceKernelGetEventFilter",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventFilter(CpuContext ctx)
{
Span<byte> filterBytes = stackalloc byte[sizeof(short)];
var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes)
? BinaryPrimitives.ReadInt16LittleEndian(filterBytes)
: (short)0;
ctx[CpuRegister.Rax] = unchecked((uint)filter);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kwGyyjohI50",
ExportName = "sceKernelGetEventData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventData(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data);
ctx[CpuRegister.Rax] = data;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fzyMKs9kim0",
ExportName = "sceKernelWaitEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelWaitEqueue(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var eventsAddress = ctx[CpuRegister.Rsi];
var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
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 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-deliver", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress == 0 &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
GetEventQueueWakeKey(handle),
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
() => HasPendingEvents(handle)))
{
TraceEventQueue(ctx, "wait-block", handle);
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 && !ctx.TryWriteUInt32(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;
}
public static bool IsValidEqueue(ulong handle)
{
lock (_eventQueueGate)
{
return _eventQueues.Contains(handle);
}
}
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
{
var queued = false;
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
queue.AddLast(queuedEvent);
queued = true;
Monitor.PulseAll(_eventQueueGate);
}
if (queued)
{
WakeEventQueue(handle);
}
return queued;
}
public static bool RegisterEvent(
ulong handle,
ulong ident,
short filter,
ulong userData)
{
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
{
return false;
}
if (!_registeredEvents.TryGetValue(handle, out var events))
{
events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
_registeredEvents[handle] = events;
}
events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData);
return true;
}
}
public static bool DeleteRegisteredEvent(
ulong handle,
ulong ident,
short filter)
{
lock (_eventQueueGate)
{
return _registeredEvents.TryGetValue(handle, out var events) &&
events.Remove((ident, filter));
}
}
public static int TriggerRegisteredEvents(
ulong ident,
short filter,
ulong data)
{
List<ulong>? wakeHandles = null;
var triggeredCount = 0;
lock (_eventQueueGate)
{
foreach (var (handle, registrations) in _registeredEvents)
{
if (!registrations.TryGetValue((ident, filter), out var registration))
{
continue;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
0,
1,
data,
registration.UserData));
(wakeHandles ??= new List<ulong>()).Add(handle);
triggeredCount++;
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
public static bool TriggerDisplayEvent(
ulong handle,
ulong ident,
short filter,
ulong eventHint,
ulong userData)
{
var triggered = false;
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var events))
{
events = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = events;
}
var count = 1UL;
var pendingNode = FindPendingEvent(events, ident, filter);
if (pendingNode is not null)
{
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
}
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
var eventData = timeBits | (count << 12) | (eventHint & 0xFFFF_FFFF_FFFF_0000UL);
var triggeredEvent = new KernelQueuedEvent(
ident,
filter,
0x20,
0,
eventData,
userData);
if (pendingNode is not null)
{
pendingNode.Value = triggeredEvent;
}
else
{
events.AddLast(triggeredEvent);
}
triggered = true;
}
if (triggered)
{
WakeEventQueue(handle);
}
return triggered;
}
private static int ResumeWaitEqueue(
CpuContext ctx,
ulong handle,
ulong eventsAddress,
int eventCapacity,
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return deliveredCount > 0
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
private static bool HasPendingEvents(ulong handle)
{
lock (_eventQueueGate)
{
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
}
}
private static void QueueOrUpdateEvent(
LinkedList<KernelQueuedEvent> queue,
KernelQueuedEvent queuedEvent)
{
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
if (pendingNode is null)
{
queue.AddLast(queuedEvent);
return;
}
pendingNode.Value = queuedEvent with
{
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
};
}
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
LinkedList<KernelQueuedEvent> queue,
ulong ident,
short filter)
{
for (var node = queue.First; node is not null; node = node.Next)
{
if (node.Value.Ident == ident && node.Value.Filter == filter)
{
return node;
}
}
return null;
}
private static string GetEventQueueWakeKey(ulong handle) =>
$"sceKernelWaitEqueue:{handle:X16}";
private static void WakeEventQueue(ulong handle)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
}
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
{
if (eventsAddress == 0 || eventCapacity <= 0)
{
return 0;
}
KernelQueuedEvent[] events;
lock (_eventQueueGate)
{
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
{
return 0;
}
var count = Math.Min(eventCapacity, queue.Count);
events = new KernelQueuedEvent[count];
for (var i = 0; i < count; i++)
{
events[i] = queue.First!.Value;
queue.RemoveFirst();
}
}
for (var i = 0; i < events.Length; i++)
{
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
{
return i;
}
}
return events.Length;
}
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
{
Span<byte> eventBytes = stackalloc byte[KernelEventSize];
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident);
BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter);
BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags);
BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags);
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data);
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData);
return ctx.Memory.TryWrite(address, eventBytes);
}
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
{
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
}
@@ -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;
}
}
+115 -91
View File
@@ -55,7 +55,10 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx)
{
_ = ctx;
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -68,7 +71,8 @@ public static class KernelExports
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] catchReturnFromMain(status={status})");
ctx[CpuRegister.Rax] = 0;
GuestThreadExecution.RequestCurrentEntryExit("catchReturnFromMain", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -200,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;
@@ -208,7 +222,29 @@ 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,
priority,
affinityMask);
if (!scheduler.TryStartThread(ctx, request, out var error))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] pthread_create: failed to schedule guest thread '{name}' entry=0x{entryAddress:X16}: {error}");
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
}
ctx[CpuRegister.Rax] = 0;
@@ -225,6 +261,42 @@ public static class KernelExports
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "3kg7rT0NQIs",
ExportName = "scePthreadExit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "FJrT5LuUBAU",
ExportName = "pthread_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePosix")]
public static int PosixPthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "onNY9Byn-W8",
ExportName = "scePthreadJoin",
@@ -234,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())
{
@@ -245,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;
}
@@ -264,22 +356,21 @@ public static class KernelExports
ExportName = "open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Open(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "1G3lF1Gg1k8",
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
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",
@@ -290,84 +381,17 @@ public static class KernelExports
{
ulong fmtPtr = ctx[CpuRegister.Rdi];
string fmt = ReadCString(ctx, fmtPtr, 4096);
ulong[] args =
string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1);
if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
{
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
};
int argIndex = 0;
var sb = new System.Text.StringBuilder(fmt.Length + 64);
for (int i = 0; i < fmt.Length; i++)
Console.Write($"[DEBUG][PRINF] {outStr}");
}
else
{
char ch = fmt[i];
if (ch != '%')
{
sb.Append(ch);
continue;
}
if (i + 1 < fmt.Length && fmt[i + 1] == '%')
{
sb.Append('%');
i++;
continue;
}
int j = i + 1;
while (j < fmt.Length && "-+ #0".IndexOf(fmt[j]) >= 0) j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
if (j < fmt.Length && fmt[j] == '.')
{
j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
}
if (j >= fmt.Length) break;
char spec = fmt[j];
i = j;
ulong a = argIndex < args.Length ? args[argIndex++] : 0;
switch (spec)
{
case 's':
sb.Append(a == 0 ? "(null)" : ReadCString(ctx, a, 4096));
break;
case 'd':
case 'i':
sb.Append(unchecked((int)a));
break;
case 'u':
sb.Append(unchecked((uint)a));
break;
case 'x':
sb.Append(unchecked((uint)a).ToString("x"));
break;
case 'p':
sb.Append("0x").Append(a.ToString("x16"));
break;
default:
sb.Append('%').Append(spec);
break;
}
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
}
string outStr = sb.ToString();
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
ctx[CpuRegister.Rax] = (ulong)outStr.Length;
ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,33 +3,80 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
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;
private const ulong SyntheticRwlockAttrHandleBase = 0x00006005_0000_0000;
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, ReaderWriterLockSlim> _rwlockStates = new();
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
private static readonly ConcurrentDictionary<int, TlsKeyState> _tlsKeys = new();
private static int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
private static long _nextSyntheticRwlockAttrHandleId = 1;
[ThreadStatic]
private static Dictionary<int, ulong>? _threadLocalSpecific;
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
{
@@ -40,6 +87,47 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
public Dictionary<ulong, int> ReaderCounts { get; } = new();
public int ReaderTotalCount { get; set; }
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
public int GetReaderCount(ulong threadId)
{
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
}
public void AddReader(ulong threadId)
{
ReaderCounts.TryGetValue(threadId, out var currentCount);
ReaderCounts[threadId] = currentCount + 1;
ReaderTotalCount++;
}
public bool RemoveReader(ulong threadId)
{
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
{
return false;
}
if (currentCount == 1)
{
ReaderCounts.Remove(threadId);
}
else
{
ReaderCounts[threadId] = currentCount - 1;
}
ReaderTotalCount = Math.Max(0, ReaderTotalCount - 1);
return true;
}
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -142,6 +230,35 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "rcrVFJsQWRY",
ExportName = "scePthreadGetaffinity",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetaffinity(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outMaskAddress = ctx[CpuRegister.Rsi];
if (thread == 0 || outMaskAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong affinityMask;
lock (_stateGate)
{
affinityMask = GetOrCreateThreadStateLocked(thread).AffinityMask;
}
if (!ctx.TryWriteUInt64(outMaskAddress, affinityMask))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1tKyG7RlMJo",
ExportName = "scePthreadGetprio",
@@ -194,6 +311,41 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Xs9hdiD7sAA",
ExportName = "pthread_setschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadSetschedparam(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var policy = unchecked((int)ctx[CpuRegister.Rsi]);
var schedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || schedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.Priority = schedPriority;
state.Attributes = state.Attributes with
{
SchedPolicy = policy,
SchedPriority = schedPriority,
};
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit",
@@ -207,15 +359,32 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var syntheticHandle = AllocateSyntheticHandle(SyntheticPthreadAttrHandleBase, ref _nextSyntheticPthreadAttrHandleId);
if (!ctx.TryWriteUInt64(attrAddress, syntheticHandle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
_attrStates[attrAddress] = PthreadAttrState.Default;
_attrStates[syntheticHandle] = PthreadAttrState.Default;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "wtkt-teR1so",
ExportName = "pthread_attr_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrInit(CpuContext ctx)
{
return PthreadAttrInit(ctx);
}
[SysAbiExport(
Nid = "62KCwEMmzcM",
ExportName = "scePthreadAttrDestroy",
@@ -229,15 +398,31 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
_attrStates.Remove(attrAddress);
if (resolvedAddress != attrAddress)
{
_attrStates.Remove(resolvedAddress);
}
}
_ = ctx.TryWriteUInt64(attrAddress, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "zHchY8ft5pk",
ExportName = "pthread_attr_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrDestroy(CpuContext ctx)
{
return PthreadAttrDestroy(ctx);
}
[SysAbiExport(
Nid = "x1X76arYMxU",
ExportName = "scePthreadAttrGet",
@@ -378,6 +563,37 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "-quPa4SEJUw",
ExportName = "scePthreadAttrGetstack",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrGetstack(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var outStackAddressPointer = ctx[CpuRegister.Rsi];
var outStackSizeAddress = ctx[CpuRegister.Rdx];
if (attrAddress == 0 || outStackAddressPointer == 0 || outStackSizeAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
PthreadAttrState state;
lock (_stateGate)
{
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteUInt64(outStackAddressPointer, state.StackAddress) ||
!ctx.TryWriteUInt64(outStackSizeAddress, state.StackSize))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "-fA+7ZlGDQs",
ExportName = "scePthreadAttrGetstacksize",
@@ -503,6 +719,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",
@@ -570,10 +815,57 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { StackSize = stackSize };
var state = GetOrCreateAttrStateLocked(resolvedAddress);
var updated = state with { StackSize = stackSize };
_attrStates[resolvedAddress] = updated;
if (resolvedAddress != attrAddress)
{
_attrStates[attrAddress] = updated;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "2Q0z6rnBrTE",
ExportName = "pthread_attr_setstacksize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrSetstacksize(CpuContext ctx)
{
return PthreadAttrSetstacksize(ctx);
}
[SysAbiExport(
Nid = "Bvn74vj6oLo",
ExportName = "scePthreadAttrSetstack",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadAttrSetstack(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
var stackAddress = ctx[CpuRegister.Rsi];
var stackSize = ctx[CpuRegister.Rdx];
if (attrAddress == 0 || stackAddress == 0 || stackSize == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(resolvedAddress);
var updated = state with { StackAddress = stackAddress, StackSize = stackSize };
_attrStates[resolvedAddress] = updated;
if (resolvedAddress != attrAddress)
{
_attrStates[attrAddress] = updated;
}
}
ctx[CpuRegister.Rax] = 0;
@@ -599,15 +891,14 @@ public static class KernelPthreadExtendedCompatExports
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
if (_rwlockStates.Remove(resolvedAddress, out var existing))
{
existing.Dispose();
}
var rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var rwlock = new PthreadRwlockState();
_rwlockStates[rwlockAddress] = rwlock;
_rwlockStates[syntheticHandle] = rwlock;
}
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -633,14 +924,10 @@ public static class KernelPthreadExtendedCompatExports
}
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
ReaderWriterLockSlim? state;
PthreadRwlockState? state;
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress, out state);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
_rwlockStates.TryGetValue(resolvedAddress, out state);
}
if (state is null)
@@ -648,8 +935,24 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
state.Dispose();
lock (state.SyncRoot)
{
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
}
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
}
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -702,29 +1005,38 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
try
{
if (rwlock.IsWriteLockHeld)
lock (rwlock.SyncRoot)
{
rwlock.ExitWriteLock();
}
else if (rwlock.IsReadLockHeld)
{
rwlock.ExitReadLock();
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
if (rwlock.WriterThreadId == currentThreadId)
{
rwlock.WriterThreadId = 0;
Monitor.PulseAll(rwlock.SyncRoot);
}
else if (rwlock.RemoveReader(currentThreadId))
{
if (rwlock.ReaderTotalCount == 0 || rwlock.WaitingWriters > 0)
{
Monitor.PulseAll(rwlock.SyncRoot);
}
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
}
}
catch (SynchronizationLockException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -737,6 +1049,47 @@ public static class KernelPthreadExtendedCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
[SysAbiExport(
Nid = "yOfGg-I1ZII",
ExportName = "scePthreadRwlockattrInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockattrInit(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockAttrHandleBase, ref _nextSyntheticRwlockAttrHandleId);
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, syntheticHandle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "i2ifZ3fS2fo",
ExportName = "scePthreadRwlockattrDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRwlockattrDestroy(CpuContext ctx)
{
var attrAddress = ctx[CpuRegister.Rdi];
if (attrAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mqULNdimTn0",
ExportName = "pthread_key_create",
@@ -752,15 +1105,13 @@ public static class KernelPthreadExtendedCompatExports
}
int key;
lock (_stateGate)
while (true)
{
while (_tlsKeys.ContainsKey(_nextTlsKey))
key = Interlocked.Increment(ref _nextTlsKey) - 1;
if (_tlsKeys.TryAdd(key, new TlsKeyState(destructor)))
{
_nextTlsKey++;
break;
}
key = _nextTlsKey++;
_tlsKeys[key] = new TlsKeyState(destructor);
}
if (!TryWriteInt32(ctx, outKeyAddress, key))
@@ -768,7 +1119,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((uint)key);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -787,15 +1138,16 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadKeyDelete(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
if (!_tlsKeys.TryRemove(key, out _))
{
if (!_tlsKeys.Remove(key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
foreach (var values in _threadLocalSpecific.Values)
{
values.TryRemove(key, out _);
}
_threadLocalSpecific?.Remove(key);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -816,16 +1168,16 @@ public static class KernelPthreadExtendedCompatExports
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi];
lock (_stateGate)
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
if (!_tlsKeys.ContainsKey(key))
{
if (!_tlsKeys.TryGetValue(key, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
_threadLocalSpecific ??= new Dictionary<int, ulong>();
_threadLocalSpecific[key] = value;
var values = _threadLocalSpecific.GetOrAdd(
currentThreadHandle,
static _ => new ConcurrentDictionary<int, ulong>());
values[key] = value;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -845,19 +1197,21 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadGetspecific(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
ulong value = 0;
if (!_tlsKeys.ContainsKey(key))
{
if (!_tlsKeys.TryGetValue(key, out _))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
ctx[CpuRegister.Rax] =
_threadLocalSpecific is not null && _threadLocalSpecific.TryGetValue(key, out var value)
? value
: 0UL;
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue))
{
value = storedValue;
}
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -880,21 +1234,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (rwlock.SyncRoot)
{
if (write)
{
rwlock.EnterWriteLock();
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
rwlock.WaitingWriters++;
try
{
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
{
Monitor.Wait(rwlock.SyncRoot);
}
}
finally
{
rwlock.WaitingWriters--;
}
rwlock.WriterThreadId = currentThreadId;
}
else
{
rwlock.EnterReadLock();
if (rwlock.WriterThreadId == currentThreadId)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (rwlock.WriterThreadId != 0 ||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
{
Monitor.Wait(rwlock.SyncRoot);
}
rwlock.AddReader(currentThreadId);
}
}
catch (LockRecursionException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -914,7 +1294,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (ctx.TryReadUInt64(rwlockAddress, out var pointedHandle) && pointedHandle != 0)
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
@@ -928,7 +1308,7 @@ public static class KernelPthreadExtendedCompatExports
return rwlockAddress;
}
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, out ReaderWriterLockSlim? rwlock)
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadRwlockState? rwlock)
{
resolvedAddress = 0;
rwlock = null;
@@ -946,7 +1326,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle))
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle))
{
return false;
}
@@ -973,7 +1353,7 @@ public static class KernelPthreadExtendedCompatExports
return false;
}
var createdRwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var createdRwlock = new PthreadRwlockState();
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockHandleBase, ref _nextSyntheticRwlockHandleId);
lock (_stateGate)
{
@@ -981,7 +1361,7 @@ public static class KernelPthreadExtendedCompatExports
_rwlockStates[syntheticHandle] = createdRwlock;
}
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
rwlock = createdRwlock;
return true;
@@ -1028,6 +1408,35 @@ public static class KernelPthreadExtendedCompatExports
return state;
}
private static ulong ResolvePthreadAttrHandle(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return 0;
}
lock (_stateGate)
{
if (_attrStates.ContainsKey(attrAddress))
{
return attrAddress;
}
}
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
if (_attrStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
}
}
return attrAddress;
}
private static bool TryWriteFixedUtf8CString(CpuContext ctx, ulong address, string value, int maxBytes)
{
if (maxBytes <= 0)
+17 -10
View File
@@ -1,8 +1,10 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -10,8 +12,7 @@ internal static class KernelPthreadState
{
private const int ThreadObjectSize = 0x1000;
private static readonly object Gate = new();
private static readonly Dictionary<ulong, ThreadIdentity> Threads = new();
private static readonly ConcurrentDictionary<ulong, ThreadIdentity> Threads = new();
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
private static long _nextUniqueThreadId = 1;
@@ -25,12 +26,24 @@ internal static class KernelPthreadState
internal static ulong GetCurrentThreadHandle()
{
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _))
{
return guestThreadHandle;
}
EnsureCurrentThreadRegistered();
return _currentThreadHandle;
}
internal static ulong GetCurrentThreadUniqueId()
{
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity))
{
return identity.UniqueId;
}
EnsureCurrentThreadRegistered();
return _currentThreadUniqueId;
}
@@ -43,10 +56,7 @@ internal static class KernelPthreadState
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity)
{
lock (Gate)
{
return Threads.TryGetValue(threadHandle, out identity);
}
return Threads.TryGetValue(threadHandle, out identity);
}
private static void EnsureCurrentThreadRegistered()
@@ -68,10 +78,7 @@ internal static class KernelPthreadState
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
var handle = unchecked((ulong)pointer.ToInt64());
lock (Gate)
{
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
}
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
return handle;
}
@@ -2,11 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
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;
@@ -25,12 +25,16 @@ public static class KernelRuntimeCompatExports
private const ulong TlsNewReplaceOffset = 0x300;
private const int MallocReplaceSize = 0x70;
private const int NewReplaceSize = 0x68;
private const int OrbisTimesecSize = sizeof(long) + sizeof(uint) + sizeof(uint);
private const ulong ModuleInfoHandleOffset = 0x108;
private const ulong ModuleInfoNameOffset = 0x10;
private const int ModuleInfoNameMaxBytes = 64;
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
private const ulong PrtAreaSize = 0x000000EC00000000UL;
private const int MapFlagFixed = 0x10;
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
private const int AioInitParamSize = 0x3C;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint PageExecuteReadWrite = 0x40;
@@ -51,6 +55,14 @@ public static class KernelRuntimeCompatExports
private static readonly object _prtApertureGate = new();
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
private static int _stackChkFailCount;
private static long _usleepTraceCount;
private static readonly bool _traceUsleep =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
private static readonly bool _traceGuestThreads =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
[ThreadStatic]
private static int _shortUsleepCount;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate();
@@ -63,33 +75,104 @@ public static class KernelRuntimeCompatExports
public static int KernelUsleep(CpuContext ctx)
{
var micros = ctx[CpuRegister.Rdi];
TraceUsleepSpin(ctx, micros);
if (micros == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var sleepMilliseconds = (int)Math.Min(micros / 1000UL, int.MaxValue);
if (sleepMilliseconds > 0)
{
Thread.Sleep(sleepMilliseconds);
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
var remainingMicros = micros % 1000UL;
if (remainingMicros > 0)
if (micros < 1000)
{
var targetTicks = (long)((double)remainingMicros * Stopwatch.Frequency / 1_000_000.0);
var spin = Stopwatch.StartNew();
while (spin.ElapsedTicks < targetTicks)
// 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.SpinWait(16);
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
}
else
{
_shortUsleepCount = 0;
var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue);
Thread.Sleep(sleepMilliseconds);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{
if (!_traceUsleep)
{
return;
}
var count = Interlocked.Increment(ref _usleepTraceCount);
if (count > 32 && count % 10000 != 0)
{
return;
}
var rbx = ctx[CpuRegister.Rbx];
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var lockAddress = rbx == 0 ? 0 : rbx + 0xF78;
var lockText = "unreadable";
if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue))
{
lockText = $"0x{lockValue:X16}";
}
var schedulerText = "unreadable";
if (r12 != 0 && ctx.TryReadUInt64(r12 + 8, out var schedulerAddress))
{
schedulerText = $"0x{schedulerAddress:X16}";
}
var waitValueText = "unreadable";
if (r13 != 0 && ctx.TryReadUInt64(r13, out var waitValue))
{
waitValueText = $"0x{waitValue:X16}";
}
var callerReturnText = "unreadable";
var rbp = ctx[CpuRegister.Rbp];
if (rbp != 0 && ctx.TryReadUInt64(rbp + 8, out var callerReturn))
{
callerReturnText = $"0x{callerReturn:X16}";
}
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip
: 0UL;
var thread = GuestThreadExecution.CurrentGuestThreadHandle;
var fiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
Console.Error.WriteLine(
$"[LOADER][TRACE] usleep#{count}: usec={micros} ret=0x{returnRip:X16} caller={callerReturnText} thread=0x{thread:X16} fiber=0x{fiber:X16} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r12=0x{r12:X16} scheduler@+8={schedulerText} r13=0x{r13:X16}:{waitValueText} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}");
if (count % 100000 == 0 &&
_traceGuestThreads &&
GuestThreadExecution.Scheduler is { } scheduler)
{
foreach (var snapshot in scheduler.SnapshotThreads())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] guest_thread.snapshot handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
}
[SysAbiExport(
Nid = "QBi7HCK03hw",
ExportName = "sceKernelClockGettime",
@@ -155,6 +238,37 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "n88vx3C5nW8",
ExportName = "gettimeofday",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixGettimeofday(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
var timezoneAddress = ctx[CpuRegister.Rsi];
var now = DateTimeOffset.UtcNow;
var seconds = now.ToUnixTimeSeconds();
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
if (timeAddress != 0 &&
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
{
return -1;
}
if (timezoneAddress != 0 &&
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
{
return -1;
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "-2IRUCO--PM",
ExportName = "sceKernelReadTsc",
@@ -257,6 +371,8 @@ public static class KernelRuntimeCompatExports
}
}
TraceProcParam(ctx, address);
ctx[CpuRegister.Rax] = address;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -269,6 +385,223 @@ public static class KernelRuntimeCompatExports
}
}
private static void TraceProcParam(CpuContext ctx, ulong address)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM"), "1", StringComparison.Ordinal))
{
return;
}
if (address == 0)
{
Console.Error.WriteLine("[LOADER][TRACE] proc_param: address=0");
return;
}
const int dumpSize = 0x200;
var buffer = GC.AllocateUninitializedArray<byte>(dumpSize);
if (!ctx.Memory.TryRead(address, buffer))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} unreadable");
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} size=0x{dumpSize:X}");
for (var offset = 0; offset < dumpSize; offset += 16)
{
var slice = buffer.AsSpan(offset, 16);
var hex = Convert.ToHexString(slice);
Console.Error.WriteLine($"[LOADER][TRACE] proc_param[{offset:X3}]: {hex}");
}
TraceProcParamPointers(ctx, address, buffer);
}
private static void TraceProcParamPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> buffer)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM_PTRS"), "1", StringComparison.Ordinal))
{
return;
}
if (buffer.Length < 0x50)
{
return;
}
for (var offset = 0x20; offset <= 0x48; offset += 8)
{
var ptr = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(offset, 8));
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr@{offset:X2}: 0x{ptr:X16}");
if (ptr == 0)
{
continue;
}
TraceProcParamPointerTarget(ctx, ptr);
}
}
private static void TraceProcParamPointerTarget(CpuContext ctx, ulong address)
{
const int maxAsciiBytes = 256;
const int maxWideChars = 128;
if (TryReadUtf8CString(ctx, address, maxAsciiBytes, out var asciiValue))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target ascii@0x{address:X16}: \"{asciiValue}\"");
return;
}
if (TryReadUtf16CString(ctx, address, maxWideChars, out var wideValue))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target wide@0x{address:X16}: \"{wideValue}\"");
return;
}
var preview = GC.AllocateUninitializedArray<byte>(64);
if (ctx.Memory.TryRead(address, preview))
{
var hex = Convert.ToHexString(preview);
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target hex@0x{address:X16}: {hex}");
TraceProcParamEmbeddedPointers(ctx, address, preview);
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target unreadable@0x{address:X16}");
}
}
private static bool TryReadUtf8CString(CpuContext ctx, ulong address, int maxBytes, out string value)
{
value = string.Empty;
var buffer = GC.AllocateUninitializedArray<byte>(maxBytes);
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var length = Array.IndexOf(buffer, (byte)0);
if (length < 0)
{
length = maxBytes;
}
if (length == 0)
{
return false;
}
var text = Encoding.UTF8.GetString(buffer, 0, length);
if (!IsMostlyPrintable(text))
{
return false;
}
value = text;
return true;
}
private static bool TryReadUtf16CString(CpuContext ctx, ulong address, int maxChars, out string value)
{
value = string.Empty;
var maxBytes = maxChars * 2;
var buffer = GC.AllocateUninitializedArray<byte>(maxBytes);
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var lengthBytes = -1;
for (var i = 0; i + 1 < buffer.Length; i += 2)
{
if (buffer[i] == 0 && buffer[i + 1] == 0)
{
lengthBytes = i;
break;
}
}
if (lengthBytes <= 0)
{
return false;
}
var text = Encoding.Unicode.GetString(buffer, 0, lengthBytes);
if (!IsMostlyPrintable(text))
{
return false;
}
value = text;
return true;
}
private static bool IsMostlyPrintable(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
var printable = 0;
for (var i = 0; i < text.Length; i++)
{
var ch = text[i];
if (ch == '\0')
{
continue;
}
if (!char.IsControl(ch) || ch == '\r' || ch == '\n' || ch == '\t')
{
printable++;
}
}
return printable >= Math.Max(4, text.Length * 3 / 4);
}
private static void TraceProcParamEmbeddedPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> data)
{
const int maxCandidates = 12;
var found = 0;
Span<byte> probe = stackalloc byte[2];
for (var offset = 0; offset + 8 <= data.Length; offset += 8)
{
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
if (candidate == 0)
{
continue;
}
if (!ctx.Memory.TryRead(candidate, probe))
{
continue;
}
if (TryReadUtf8CString(ctx, candidate, 256, out var ascii))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} ascii \"{ascii}\"");
if (++found >= maxCandidates)
{
return;
}
continue;
}
if (TryReadUtf16CString(ctx, candidate, 128, out var wide))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} wide \"{wide}\"");
if (++found >= maxCandidates)
{
return;
}
}
}
}
[SysAbiExport(
Nid = "9BcDykPmo1I",
ExportName = "__error",
@@ -277,19 +610,16 @@ public static class KernelRuntimeCompatExports
public static int ErrorAddress(CpuContext ctx)
{
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
if (address != 0)
{
Span<byte> zero = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryWrite(address, zero))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = address;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
internal static bool TrySetErrno(CpuContext ctx, int value)
{
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
return address != 0 && TryWriteInt32(ctx, address, value);
}
[SysAbiExport(
Nid = "bnZxYgAFeA0",
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
@@ -332,6 +662,17 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jh+8XiK4LeE",
ExportName = "sceKernelIsAddressSanitizerEnabled",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelIsAddressSanitizerEnabled(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ca7v6Cxulzs",
ExportName = "sceKernelSetGPO",
@@ -370,7 +711,7 @@ public static class KernelRuntimeCompatExports
{
var inOutAddressPointer = ctx[CpuRegister.Rdi];
var length = ctx[CpuRegister.Rsi];
var _flags = ctx[CpuRegister.Rdx];
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
var alignment = ctx[CpuRegister.Rcx];
if (inOutAddressPointer == 0 || length == 0)
{
@@ -382,7 +723,8 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var effectiveAlignment = alignment == 0 ? 0x10000UL : alignment;
var effectiveAlignment = alignment == 0 ? DefaultVirtualRangeAlignment : alignment;
var fixedMapping = (flags & MapFlagFixed) != 0;
ulong desiredAddress;
lock (_stateGate)
{
@@ -391,13 +733,16 @@ public static class KernelRuntimeCompatExports
: AlignUp(_nextReservedVirtualBase, effectiveAlignment);
}
if (!TryReserveVirtualRange(ctx, desiredAddress, length, out var mappedAddress))
if (!TryReserveVirtualRange(ctx, desiredAddress, length, effectiveAlignment, allowSearch: !fixedMapping, out var mappedAddress))
{
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}");
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))
{
@@ -409,6 +754,7 @@ public static class KernelRuntimeCompatExports
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
}
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -681,14 +1027,12 @@ public static class KernelRuntimeCompatExports
public static int StackCheckFail(CpuContext ctx)
{
var count = Interlocked.Increment(ref _stackChkFailCount);
if (count <= 8)
{
Console.Error.WriteLine(
$"[LOADER][WARNING] __stack_chk_fail recovery#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
Console.Error.WriteLine(
$"[LOADER][ERROR] __stack_chk_fail#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
var result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
GuestThreadExecution.RequestCurrentEntryExit("__stack_chk_fail", result);
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[SysAbiExport(
@@ -780,6 +1124,153 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nu4a0-arQis",
ExportName = "sceKernelAioInitializeParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> zero = stackalloc byte[AioInitParamSize];
zero.Clear();
if (!ctx.Memory.TryWrite(paramAddress, zero))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "-o5uEDpN+oY",
ExportName = "sceKernelConvertUtcToLocaltime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelConvertUtcToLocaltime(CpuContext ctx)
{
var utcSeconds = unchecked((long)ctx[CpuRegister.Rdi]);
var localTimeAddress = ctx[CpuRegister.Rsi];
var timesecAddress = ctx[CpuRegister.Rdx];
var dstSecondsAddress = ctx[CpuRegister.Rcx];
if (localTimeAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var utc = DateTimeOffset.FromUnixTimeSeconds(utcSeconds);
var local = TimeZoneInfo.ConvertTime(utc, TimeZoneInfo.Local);
var offset = local.Offset;
var localSeconds = utcSeconds + (long)offset.TotalSeconds;
var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(local.DateTime)
? (uint)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules()
.Where(rule => rule.DateStart <= local.Date && rule.DateEnd >= local.Date)
.Select(rule => rule.DaylightDelta.TotalSeconds)
.DefaultIfEmpty(0)
.Max())
: 0u;
var westSeconds = unchecked((uint)(int)offset.TotalSeconds);
if (!ctx.TryWriteUInt64(localTimeAddress, unchecked((ulong)localSeconds)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (timesecAddress != 0)
{
Span<byte> timesec = stackalloc byte[OrbisTimesecSize];
BinaryPrimitives.WriteInt64LittleEndian(timesec, utcSeconds);
BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long), sizeof(uint)), westSeconds);
BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long) + sizeof(uint), sizeof(uint)), dstSeconds);
if (!ctx.Memory.TryWrite(timesecAddress, timesec))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
if (dstSecondsAddress != 0 && !ctx.TryWriteUInt64(dstSecondsAddress, dstSeconds))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0NTHN1NKONI",
ExportName = "sceKernelConvertLocaltimeToUtc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelConvertLocaltimeToUtc(CpuContext ctx)
{
var localSeconds = unchecked((long)ctx[CpuRegister.Rdi]);
var utcTimeAddress = ctx[CpuRegister.Rdx];
var timezoneAddress = ctx[CpuRegister.Rcx];
var dstSecondsAddress = ctx[CpuRegister.R8];
if (timezoneAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var localDate = DateTimeOffset.FromUnixTimeSeconds(localSeconds).DateTime;
var offset = TimeZoneInfo.Local.GetUtcOffset(localDate);
var utcSeconds = localSeconds - (long)offset.TotalSeconds;
var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(localDate)
? (int)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules()
.Where(rule => rule.DateStart <= localDate.Date && rule.DateEnd >= localDate.Date)
.Select(rule => rule.DaylightDelta.TotalSeconds)
.DefaultIfEmpty(0)
.Max())
: 0;
var minutesWest = unchecked((int)-offset.TotalMinutes);
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (utcTimeAddress != 0 && !ctx.TryWriteUInt64(utcTimeAddress, unchecked((ulong)utcSeconds)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vYU8P9Td2Zo",
ExportName = "sceKernelAioInitializeImpl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeImpl(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
if (paramAddress == 0 || size < AioInitParamSize)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eR2bZFAAU0Q",
ExportName = "sceSysmoduleUnloadModule",
@@ -1044,30 +1535,40 @@ public static class KernelRuntimeCompatExports
ulong.TryParse(overrideHzText, out var overrideHz) &&
overrideHz >= minSane)
{
TraceKernelTscFrequency("env", overrideHz);
return overrideHz;
}
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
return cpuidHz;
}
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
{
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
return calibratedHz;
}
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
TraceKernelTscFrequency("cpuid", cpuidHz);
return cpuidHz;
}
var hostQpc = Stopwatch.Frequency > 0
? unchecked((ulong)Stopwatch.Frequency)
: DefaultKernelTscFrequency;
if (hostQpc >= minSane)
{
TraceKernelTscFrequency("qpc", hostQpc);
return hostQpc;
}
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
return DefaultKernelTscFrequency;
}
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
{
Console.Error.WriteLine($"[LOADER][INFO] Kernel TSC frequency: {frequencyHz} Hz ({source})");
}
private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
{
frequencyHz = 0;
@@ -1226,96 +1727,24 @@ public static class KernelRuntimeCompatExports
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
private static bool TryReserveVirtualRange(CpuContext ctx, ulong desiredAddress, ulong length, out ulong mappedAddress)
private static bool TryReserveVirtualRange(
CpuContext ctx,
ulong desiredAddress,
ulong length,
ulong alignment,
bool allowSearch,
out ulong mappedAddress)
{
mappedAddress = 0;
if (length == 0)
{
return false;
}
try
{
object memoryObject = ctx.Memory;
MethodInfo? allocateAt = null;
var allocateAtHasAllowAlternativeArg = false;
for (var depth = 0; depth < 4; depth++)
{
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (!string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
continue;
}
var parameters = candidate.GetParameters();
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
break;
}
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;
break;
}
}
if (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 (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, true }
: 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)
@@ -1328,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,266 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
public static class KernelSemaphoreCompatExports
{
private const int MaxSemaphoreNameLength = 128;
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
public required int InitialCount { get; init; }
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public object Gate { get; } = new();
}
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCreateSema(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
var attr = unchecked((uint)ctx[CpuRegister.Rdx]);
var initialCount = unchecked((int)ctx[CpuRegister.Rcx]);
var maxCount = unchecked((int)ctx[CpuRegister.R8]);
var optionAddress = ctx[CpuRegister.R9];
if (semaphoreAddress == 0 ||
nameAddress == 0 ||
attr > 2 ||
initialCount < 0 ||
maxCount <= 0 ||
initialCount > maxCount ||
optionAddress != 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
if (handle == 0)
{
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
};
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "Zxa0VhQVTsk",
ExportName = "sceKernelWaitSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelWaitSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
var timeoutAddress = ctx[CpuRegister.Rdx];
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out _))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelPollSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count < needCount)
{
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
semaphore.Count -= needCount;
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "4czppHBiriw",
ExportName = "sceKernelSignalSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSignalSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count > semaphore.MaxCount - signalCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "4DM06U2BNEY",
ExportName = "sceKernelCancelSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCancelSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (setCount > semaphore.MaxCount)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
[SysAbiExport(
Nid = "R1Jvn8bSCW8",
ExportName = "sceKernelDeleteSema",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteSema(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
}
}
}
@@ -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);
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
public static class Http2Exports
{
private const int Http2ErrorInvalidId = unchecked((int)0x80436004);
private const int Http2ErrorInvalidArgument = unchecked((int)0x80436016);
private static readonly ConcurrentDictionary<int, Http2Context> _contexts = new();
private static int _nextContextId;
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
[SysAbiExport(
Nid = "3JCe3lCbQ8A",
ExportName = "sceHttp2Init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2Init(CpuContext ctx)
{
var netId = unchecked((int)ctx[CpuRegister.Rdi]);
var sslId = unchecked((int)ctx[CpuRegister.Rsi]);
var poolSize = ctx[CpuRegister.Rdx];
var maxRequests = unchecked((int)ctx[CpuRegister.Rcx]);
if (poolSize == 0 || maxRequests <= 0)
{
return SetReturn(ctx, Http2ErrorInvalidArgument);
}
var id = Interlocked.Increment(ref _nextContextId);
_contexts[id] = new Http2Context(netId, sslId, poolSize, maxRequests);
TraceHttp2("init", id, unchecked((ulong)netId), unchecked((ulong)sslId), poolSize, unchecked((ulong)maxRequests));
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "YiBUtz-pGkc",
ExportName = "sceHttp2Term",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2Term(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _))
{
return SetReturn(ctx, Http2ErrorInvalidId);
}
TraceHttp2("term", id, 0, 0, 0, 0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP2"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] http2.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}");
}
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Network;
public static class HttpExports
{
private const int HttpErrorInvalidId = unchecked((int)0x80431100);
private const int HttpErrorInvalidValue = unchecked((int)0x804311FE);
private static readonly ConcurrentDictionary<int, HttpContext> Contexts = new();
private static readonly ConcurrentDictionary<int, HttpTemplate> Templates = new();
private static int _nextContextId;
private static int _nextTemplateId = 0x1000;
private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize);
private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig);
[SysAbiExport(
Nid = "A9cVMUtEp4Y",
ExportName = "sceHttpInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpInit(CpuContext ctx)
{
var netMemoryId = unchecked((int)ctx[CpuRegister.Rdi]);
var sslContextId = unchecked((int)ctx[CpuRegister.Rsi]);
var poolSize = ctx[CpuRegister.Rdx];
if (poolSize == 0)
{
return SetReturn(ctx, HttpErrorInvalidValue);
}
var id = Interlocked.Increment(ref _nextContextId);
Contexts[id] = new HttpContext(netMemoryId, sslContextId, poolSize);
TraceHttp("init", id, unchecked((ulong)netMemoryId), unchecked((ulong)sslContextId), poolSize, 0);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0gYjPTR-6cY",
ExportName = "sceHttpCreateTemplate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpCreateTemplate(CpuContext ctx)
{
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.ContainsKey(contextId))
{
return SetReturn(ctx, HttpErrorInvalidId);
}
var userAgentAddress = ctx[CpuRegister.Rsi];
var httpVersion = unchecked((int)ctx[CpuRegister.Rdx]);
var autoProxyConfig = ctx[CpuRegister.Rcx] != 0;
var id = Interlocked.Increment(ref _nextTemplateId);
Templates[id] = new HttpTemplate(contextId, userAgentAddress, httpVersion, autoProxyConfig);
TraceHttp("create_template", id, unchecked((ulong)contextId), userAgentAddress, unchecked((ulong)httpVersion), autoProxyConfig ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4I8vEpuEhZ8",
ExportName = "sceHttpDeleteTemplate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpDeleteTemplate(CpuContext ctx)
{
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
return Templates.TryRemove(templateId, out _)
? SetReturn(ctx, 0)
: SetReturn(ctx, HttpErrorInvalidId);
}
[SysAbiExport(
Nid = "Ik-KpLTlf7Q",
ExportName = "sceHttpTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpTerm(CpuContext ctx)
{
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.TryRemove(contextId, out _))
{
return SetReturn(ctx, HttpErrorInvalidId);
}
foreach (var pair in Templates)
{
if (pair.Value.ContextId == contextId)
{
Templates.TryRemove(pair.Key, out _);
}
}
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] http.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}");
}
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Network;
public static class NetCtlExports
{
private const int MaxCallbacks = 8;
private const int NatInfoSize = 16;
private const int NetCtlErrorNoSpace = unchecked((int)0x80412103);
private const int NetCtlErrorInvalidAddress = unchecked((int)0x80412107);
private const int NetCtlErrorNotConnected = unchecked((int)0x80412108);
private const int NetCtlInfoDevice = 1;
private const int NetCtlInfoEtherAddress = 2;
private const int NetCtlInfoMtu = 3;
private const int NetCtlInfoLink = 4;
private const int NetCtlInfoIpConfig = 11;
private const int NetCtlInfoDhcpHostname = 12;
private const int NetCtlInfoPppoeAuthName = 13;
private const int NetCtlInfoIpAddress = 14;
private const int NetCtlInfoNetmask = 15;
private const int NetCtlInfoDefaultRoute = 16;
private const int NetCtlInfoPrimaryDns = 17;
private const int NetCtlInfoSecondaryDns = 18;
private const int NetCtlInfoHttpProxyConfig = 19;
private const int NetCtlInfoHttpProxyServer = 20;
private const int NetCtlInfoHttpProxyPort = 21;
private const int NetCtlDeviceWired = 0;
private const int NetCtlLinkDisconnected = 0;
private const int NetCtlIpConfigStatic = 0;
private static readonly object CallbackGate = new();
private static readonly CallbackRegistration[] Callbacks = new CallbackRegistration[MaxCallbacks];
private readonly record struct CallbackRegistration(ulong Function, ulong Argument);
[SysAbiExport(
Nid = "gky0+oaNM4k",
ExportName = "sceNetCtlInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlInit(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JO4yuTuMoKI",
ExportName = "sceNetCtlGetNatInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetNatInfo(CpuContext ctx)
{
var natInfoAddress = ctx[CpuRegister.Rdi];
if (natInfoAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> natInfo = stackalloc byte[NatInfoSize];
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
if (size != NatInfoSize)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "iQw3iQPhvUQ",
ExportName = "sceNetCtlCheckCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlCheckCallback(CpuContext ctx)
{
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "uBPlr0lbuiI",
ExportName = "sceNetCtlGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetState(CpuContext ctx)
{
var stateAddress = ctx[CpuRegister.Rdi];
if (stateAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "UJ+Z7Q+4ck0",
ExportName = "sceNetCtlRegisterCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlRegisterCallback(CpuContext ctx)
{
var function = ctx[CpuRegister.Rdi];
var argument = ctx[CpuRegister.Rsi];
var callbackIdAddress = ctx[CpuRegister.Rdx];
if (function == 0 || callbackIdAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
lock (CallbackGate)
{
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
if (callbackId < 0)
{
return SetReturn(ctx, NetCtlErrorNoSpace);
}
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Callbacks[callbackId] = new CallbackRegistration(function, argument);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "obuxdTiwkF8",
ExportName = "sceNetCtlGetInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetInfo(CpuContext ctx)
{
var code = unchecked((int)ctx[CpuRegister.Rdi]);
var infoAddress = ctx[CpuRegister.Rsi];
if (infoAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
return code switch
{
NetCtlInfoDevice => WriteUInt32(ctx, infoAddress, NetCtlDeviceWired),
NetCtlInfoEtherAddress => WriteZeroBytes(ctx, infoAddress, 6),
NetCtlInfoMtu => WriteUInt32(ctx, infoAddress, 1500),
NetCtlInfoLink => WriteUInt32(ctx, infoAddress, NetCtlLinkDisconnected),
NetCtlInfoIpConfig => WriteUInt32(ctx, infoAddress, NetCtlIpConfigStatic),
NetCtlInfoDhcpHostname => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
NetCtlInfoPppoeAuthName => WriteAsciiZ(ctx, infoAddress, string.Empty, 128),
NetCtlInfoIpAddress => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
NetCtlInfoNetmask => WriteAsciiZ(ctx, infoAddress, "255.0.0.0", 16),
NetCtlInfoDefaultRoute => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
NetCtlInfoPrimaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
NetCtlInfoSecondaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
_ => SetReturn(ctx, NetCtlErrorNotConnected),
};
}
private static int WriteZeroBytes(CpuContext ctx, ulong address, int count)
{
Span<byte> bytes = stackalloc byte[count];
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
{
Span<byte> bytes = stackalloc byte[byteCount];
var copyCount = Math.Min(value.Length, byteCount - 1);
for (var i = 0; i < copyCount; i++)
{
bytes[i] = (byte)value[i];
}
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
}
+210
View File
@@ -0,0 +1,210 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
public static class NetExports
{
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static bool _initialized;
private sealed record NetPool(string Name, int Size, int Flags);
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
[SysAbiExport(
Nid = "Nlev7Lg8k3A",
ExportName = "sceNetInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetInit(CpuContext ctx)
{
_initialized = true;
TraceNet("init", 0, 0, 0, 0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "cTGkc6-TBlI",
ExportName = "sceNetTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetTerm(CpuContext ctx)
{
_initialized = false;
_pools.Clear();
_resolvers.Clear();
TraceNet("term", 0, 0, 0, 0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (size <= 0)
{
return SetReturn(ctx, NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextPoolId);
_pools[id] = new NetPool(name, size, flags);
TraceNet("pool.create", id, unchecked((ulong)size), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "K7RlrTkI-mw",
ExportName = "sceNetPoolDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetPoolDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_pools.TryRemove(id, out _))
{
return SetReturn(ctx, NetErrorBadFileDescriptor);
}
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var poolId = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0)
{
return SetReturn(ctx, NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextResolverId);
_resolvers[id] = new ResolverContext(name, poolId, flags, 0);
TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kJlYH5uMAWI",
ExportName = "sceNetResolverDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _)
? SetReturn(ctx, 0)
: SetReturn(ctx, NetErrorBadFileDescriptor);
}
[SysAbiExport(
Nid = "J5i3hiLJMPk",
ExportName = "sceNetResolverGetError",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverGetError(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return SetReturn(ctx, NetErrorInvalidArgument);
}
if (!_resolvers.TryGetValue(id, out var resolver))
{
return SetReturn(ctx, NetErrorBadFileDescriptor);
}
Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0)
{
return true;
}
Span<byte> one = stackalloc byte[1];
var bytes = new byte[maxLength];
var count = 0;
for (; count < maxLength; count++)
{
if (!ctx.Memory.TryRead(address + (ulong)count, one))
{
return false;
}
if (one[0] == 0)
{
break;
}
bytes[count] = one[0];
}
value = Encoding.UTF8.GetString(bytes, 0, count);
return true;
}
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] net.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16}");
}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
public static class SslExports
{
private const int SslErrorInvalidId = unchecked((int)0x8095F006);
private const int SslErrorOutOfSize = unchecked((int)0x8095F008);
private static readonly ConcurrentDictionary<int, SslContext> _contexts = new();
private static int _nextContextId;
private sealed record SslContext(ulong PoolSize);
[SysAbiExport(
Nid = "hdpVEUDFW3s",
ExportName = "sceSslInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSsl")]
public static int SslInit(CpuContext ctx)
{
var poolSize = ctx[CpuRegister.Rdi];
if (poolSize == 0)
{
return SetReturn(ctx, SslErrorOutOfSize);
}
var id = Interlocked.Increment(ref _nextContextId);
_contexts[id] = new SslContext(poolSize);
TraceSsl("init", id, poolSize);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0K1yQ6Lv-Yc",
ExportName = "sceSslTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSsl")]
public static int SslTerm(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _))
{
return SetReturn(ctx, SslErrorInvalidId);
}
TraceSsl("term", id, 0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "viRXSHZYd0c",
ExportName = "sceSslClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSsl")]
public static int SslClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
TraceSsl("close", id, 0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSsl(string operation, int id, ulong arg0)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SSL"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] ssl.{operation} id={id} arg0=0x{arg0:X16}");
}
}
+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}");
}
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Np;
public static class NpManagerExports
{
private const int NpTitleIdSize = 16;
private const int NpTitleSecretSize = 128;
[SysAbiExport(
Nid = "3Zl8BePTh9Y",
ExportName = "sceNpCheckCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpCheckCallback(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JELHf4xPufo",
ExportName = "sceNpCheckCallbackForLib",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpCheckCallbackForLib(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "VfRSmPmj8Q8",
ExportName = "sceNpRegisterStateCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpRegisterStateCallback(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qQJfO8HAiaY",
ExportName = "sceNpRegisterStateCallbackA",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpRegisterStateCallbackA(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0c7HbXRKUt4",
ExportName = "sceNpRegisterStateCallbackForToolkit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManagerForToolkit")]
public static int NpRegisterStateCallbackForToolkit(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eQH7nWPcAgc",
ExportName = "sceNpGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetState(CpuContext ctx)
{
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "Ec63y59l9tw",
ExportName = "sceNpSetNpTitleId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpSetNpTitleId(CpuContext ctx)
{
var titleIdAddress = ctx[CpuRegister.Rdi];
var titleSecretAddress = ctx[CpuRegister.Rsi];
if (titleIdAddress == 0 || titleSecretAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
Span<byte> titleSecret = stackalloc byte[NpTitleSecretSize];
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
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 string ReadTitleId(ReadOnlySpan<byte> bytes)
{
var length = 0;
while (length < 12 && length < bytes.Length && bytes[length] != 0)
{
length++;
}
return length == 0
? string.Empty
: System.Text.Encoding.ASCII.GetString(bytes[..length]);
}
private static void TraceNp(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
}
}
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
public static class NpSessionSignalingExports
{
[SysAbiExport(
Nid = "ysmw6J-P8Ak",
ExportName = "sceNpSessionSignalingInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpSessionSignaling")]
public static int NpSessionSignalingInitialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -0,0 +1,98 @@
// 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.Np;
public static class NpUniversalDataSystemExports
{
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
private static int _nextHandle = 1;
[SysAbiExport(
Nid = "sjaobBgqeB4",
ExportName = "sceNpUniversalDataSystemInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemInitialize(CpuContext ctx)
{
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
}
Span<byte> parameters = stackalloc byte[16];
return ctx.Memory.TryRead(parameterAddress, parameters)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "5zBnau1uIEo",
ExportName = "sceNpUniversalDataSystemCreateContext",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateContext(CpuContext ctx)
{
var contextAddress = ctx[CpuRegister.Rdi];
if (contextAddress == 0)
{
return SetReturn(ctx, 0);
}
Span<byte> context = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
return ctx.Memory.TryWrite(contextAddress, context)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "hT0IAEvN+M0",
ExportName = "sceNpUniversalDataSystemCreateHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
{
var handle = Interlocked.Increment(ref _nextHandle);
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
{
return SetReturn(ctx, 0);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "tpFJ8LIKvPw",
ExportName = "sceNpUniversalDataSystemRegisterContext",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
{
return SetReturn(ctx, 0);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
if (address == 0)
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
public static class NpWebApi2Exports
{
private const int NpWebApi2ErrorInvalidArgument = unchecked((int)0x80553402);
private static int _initialized;
[SysAbiExport(
Nid = "+o9816YQhqQ",
ExportName = "sceNpWebApi2Initialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2Initialize(CpuContext ctx)
{
var httpContextId = unchecked((int)ctx[CpuRegister.Rdi]);
var poolSize = ctx[CpuRegister.Rsi];
if (httpContextId <= 0 || poolSize == 0)
{
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
}
Interlocked.Exchange(ref _initialized, 1);
TraceNpWebApi2("init", httpContextId, poolSize);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2Terminate(CpuContext ctx)
{
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
Interlocked.Exchange(ref _initialized, 0);
TraceNpWebApi2("term", libraryContextId, 0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] npwebapi2.{operation} id={id} arg0=0x{arg0:X16} initialized={Volatile.Read(ref _initialized)}");
}
}
@@ -0,0 +1,24 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.NpGameIntent;
public static class NpGameIntentExports
{
private static int _initialized;
[SysAbiExport(
Nid = "m87BHxt-H60",
ExportName = "sceNpGameIntentInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpGameIntent")]
public static int NpGameIntentInitialize(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 1);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+263
View File
@@ -0,0 +1,263 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
private const int PrimaryUserId = 1;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private static bool _initialized;
[SysAbiExport(
Nid = "hv1luiJrqQM",
ExportName = "scePadInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadInit(CpuContext ctx)
{
_initialized = true;
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "xk0AcarP3V4",
ExportName = "scePadOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return SetReturn(ctx, OrbisPadErrorNotInitialized);
}
if (userId == -1)
{
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
}
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
{
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);
}
[SysAbiExport(
Nid = "clVvL4ZDntw",
ExportName = "scePadSetMotionSensorState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
? SetReturn(ctx, 0)
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
[SysAbiExport(
Nid = "gjP9-KQzoUk",
ExportName = "scePadGetControllerInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetControllerInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> information = stackalloc byte[ControllerInformationSize];
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
information[0x08] = 30;
information[0x09] = 30;
information[0x0A] = StandardPortType;
information[0x0B] = 1;
information[0x0C] = 1;
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
return ctx.Memory.TryWrite(informationAddress, information)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadReadState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "q1cHNfGycLI",
ExportName = "scePadRead",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadRead(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0 || count < 1 || count > 64)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 1)
: 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();
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();
var timestampMicroseconds =
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(
data[0x50..],
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data);
}
private static int SetReturn(CpuContext ctx, int result)
{
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;
}
}
+792
View File
@@ -0,0 +1,792 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text.RegularExpressions;
namespace SharpEmu.Libs.PlayGo;
public static class PlayGoExports
{
private const int OrbisPlayGoErrorInvalidArgument = unchecked((int)0x80B20004);
private const int OrbisPlayGoErrorNotInitialized = unchecked((int)0x80B20005);
private const int OrbisPlayGoErrorAlreadyInitialized = unchecked((int)0x80B20006);
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
private const uint PlayGoHandle = 1;
private const int PlayGoLocusNotDownloaded = 0;
private const int PlayGoLocusLocalSlow = 2;
private const int PlayGoLocusLocalFast = 3;
private const int PlayGoInstallSpeedSuspended = 0;
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+)""",
RegexOptions.CultureInvariant);
private static readonly Regex DefaultChunkPattern = new(
@"default_chunk\s*=\s*""(?<id>\d+)""",
RegexOptions.CultureInvariant);
private static readonly object _stateGate = new();
private static bool _initialized;
private static bool _opened;
private static PlayGoMetadata _metadata = PlayGoMetadata.Empty;
private static int _installSpeed = PlayGoInstallSpeedTrickle;
private static ulong _languageMask = ulong.MaxValue;
private static int _unknownChunkDiagnostics;
private static int _locusTraceDiagnostics;
[SysAbiExport(
Nid = "ts6GlZOKRrE",
ExportName = "scePlayGoInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoInitialize(CpuContext ctx)
{
var initParamsAddress = ctx[CpuRegister.Rdi];
if (initParamsAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (!ctx.TryReadUInt64(initParamsAddress + PlayGoInitBufAddrOffset, out var bufferAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Span<byte> bufferSizeBytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(initParamsAddress + PlayGoInitBufSizeOffset, bufferSizeBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var bufferSize = BinaryPrimitives.ReadUInt32LittleEndian(bufferSizeBytes);
if (bufferAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (bufferSize < PlayGoMinimumInitBufferSize)
{
return OrbisPlayGoErrorBadSize;
}
lock (_stateGate)
{
if (_initialized)
{
return OrbisPlayGoErrorAlreadyInitialized;
}
_metadata = LoadPlayGoMetadata();
_installSpeed = PlayGoInstallSpeedTrickle;
_languageMask = ulong.MaxValue;
_opened = false;
_initialized = true;
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "M1Gma1ocrGE",
ExportName = "scePlayGoOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoOpen(CpuContext ctx)
{
var outHandleAddress = ctx[CpuRegister.Rdi];
var paramAddress = ctx[CpuRegister.Rsi];
if (outHandleAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (paramAddress != 0)
{
return OrbisPlayGoErrorInvalidArgument;
}
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (!_metadata.Available)
{
return OrbisPlayGoErrorNotSupportPlayGo;
}
_opened = true;
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
}
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, PlayGoHandle);
if (!ctx.Memory.TryWrite(outHandleAddress, handleBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MPe0EeBGM-E",
ExportName = "scePlayGoTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoTerminate(CpuContext ctx)
{
_ = ctx;
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
_initialized = false;
_opened = false;
_metadata = PlayGoMetadata.Empty;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Uco1I0dlDi8",
ExportName = "scePlayGoClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoClose(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (handle != PlayGoHandle)
{
return OrbisPlayGoErrorBadHandle;
}
_opened = false;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "73fF1MFU8hA",
ExportName = "scePlayGoGetChunkId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetChunkId(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outChunkIdList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEntries = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outEntries == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (outChunkIdList != 0 && numberOfEntries == 0)
{
return OrbisPlayGoErrorBadSize;
}
ushort[] chunkIds;
lock (_stateGate)
{
chunkIds = _metadata.ChunkIds;
}
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 ctx.TryWriteUInt32(outEntries, availableEntries)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
if (entriesToWrite > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_chunk_id bad_size requested={numberOfEntries} available={availableEntries}");
return OrbisPlayGoErrorBadSize;
}
for (uint i = 0; i < entriesToWrite; i++)
{
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
return ctx.TryWriteUInt32(outEntries, entriesToWrite)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "v6EZ-YWRdMs",
ExportName = "scePlayGoGetEta",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetEta(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEta = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outEta == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_eta bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outEta:X16}");
return OrbisPlayGoErrorBadSize;
}
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
? chunkError
: TryWriteInt64(ctx, outEta, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "rvBSfTimejE",
ExportName = "scePlayGoGetInstallSpeed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetInstallSpeed(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outSpeed = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outSpeed == 0)
{
return OrbisPlayGoErrorBadPointer;
}
int speed;
lock (_stateGate)
{
speed = _installSpeed;
}
return TryWriteInt32(ctx, outSpeed, speed)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "3OMbYZBaa50",
ExportName = "scePlayGoGetLanguageMask",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetLanguageMask(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outLanguageMask = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outLanguageMask == 0)
{
return OrbisPlayGoErrorBadPointer;
}
ulong languageMask;
lock (_stateGate)
{
languageMask = _languageMask;
}
return ctx.TryWriteUInt64(outLanguageMask, languageMask)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "uWIYLFkkwqk",
ExportName = "scePlayGoGetLocus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetLocus(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outLoci = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outLoci == 0)
{
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;
}
var loci = new byte[numberOfEntries];
for (uint i = 0; i < numberOfEntries; i++)
{
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!IsKnownChunkId(chunkId))
{
if (Interlocked.Increment(ref _unknownChunkDiagnostics) <= 8)
{
ushort[] knownChunkIds;
lock (_stateGate)
{
knownChunkIds = _metadata.ChunkIds;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
$"known=[{string.Join(',', knownChunkIds)}]");
}
}
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;
}
[SysAbiExport(
Nid = "-RJWNMK3fC8",
ExportName = "scePlayGoGetProgress",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetProgress(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outProgress = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outProgress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
TracePlayGo($"get_progress bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outProgress:X16}");
return OrbisPlayGoErrorBadSize;
}
var chunkError = ValidateChunkIds(ctx, chunkIds, numberOfEntries);
if (chunkError != 0)
{
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);
return ctx.Memory.TryWrite(outProgress, progress)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "Nn7zKwnA5q0",
ExportName = "scePlayGoGetToDoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetToDoList(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outTodoList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEntries = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outTodoList == 0 || outEntries == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0)
{
TracePlayGo("get_todo bad_size entries=0");
return OrbisPlayGoErrorBadSize;
}
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
return ctx.TryWriteUInt32(outEntries, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "-Q1-u1a7p0g",
ExportName = "scePlayGoPrefetch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoPrefetch(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var minimumLocus = unchecked((int)ctx[CpuRegister.Rcx]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
if (minimumLocus is not PlayGoLocusNotDownloaded and not PlayGoLocusLocalSlow and not PlayGoLocusLocalFast)
{
return OrbisPlayGoErrorBadLocus;
}
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
? chunkError
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4AAcTU9R3XM",
ExportName = "scePlayGoSetInstallSpeed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetInstallSpeed(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var speed = unchecked((int)ctx[CpuRegister.Rsi]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (speed is not PlayGoInstallSpeedSuspended and not PlayGoInstallSpeedTrickle and not PlayGoInstallSpeedFull)
{
return OrbisPlayGoErrorInvalidArgument;
}
lock (_stateGate)
{
_installSpeed = speed;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LosLlHOpNqQ",
ExportName = "scePlayGoSetLanguageMask",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetLanguageMask(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var languageMask = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
lock (_stateGate)
{
_languageMask = languageMask;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gUPGiOQ1tmQ",
ExportName = "scePlayGoSetToDoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetToDoList(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var todoList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (todoList == 0)
{
return OrbisPlayGoErrorBadPointer;
}
return numberOfEntries == 0
? OrbisPlayGoErrorBadSize
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ValidateHandle(uint handle)
{
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (handle != PlayGoHandle || !_opened)
{
return OrbisPlayGoErrorBadHandle;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ValidateChunkIds(CpuContext ctx, ulong chunkIds, uint numberOfEntries)
{
for (uint i = 0; i < numberOfEntries; i++)
{
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_ = chunkId;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool IsKnownChunkId(ushort chunkId)
{
lock (_stateGate)
{
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
}
}
private static PlayGoMetadata LoadPlayGoMetadata()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return PlayGoMetadata.Empty;
}
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
var scenarioJson = Path.Combine(app0Root, "sce_sys", "playgo-scenario.json");
var chunkDefsXml = Path.Combine(app0Root, "playgo-chunkdefs.xml");
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
return PlayGoMetadata.Empty;
}
var chunkIds = LoadChunkIds(chunkDefsXml);
return new PlayGoMetadata(true, chunkIds);
}
private static ushort[] LoadChunkIds(string chunkDefsXml)
{
if (!File.Exists(chunkDefsXml))
{
return Array.Empty<ushort>();
}
try
{
var xml = File.ReadAllText(chunkDefsXml);
var chunkIds = new HashSet<ushort>();
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
AddChunkIds(xml, ChunkIdPattern, chunkIds);
var sorted = chunkIds.ToArray();
Array.Sort(sorted);
return sorted;
}
catch (IOException)
{
return Array.Empty<ushort>();
}
catch (UnauthorizedAccessException)
{
return Array.Empty<ushort>();
}
}
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
{
foreach (Match match in pattern.Matches(xml))
{
if (ushort.TryParse(match.Groups["id"].Value, out var chunkId))
{
chunkIds.Add(chunkId);
}
}
}
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> buffer = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
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>());
}
}
+85 -2
View File
@@ -8,6 +8,8 @@ namespace SharpEmu.Libs.Rtc;
public static class RtcExports
{
private const long DateTimeTicksPerMicrosecond = 10;
[SysAbiExport(
Nid = "ZPD1YOKI+Kw",
ExportName = "sceRtcGetCurrentClockLocalTime",
@@ -42,6 +44,29 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "18B2NS1y9UU",
ExportName = "sceRtcGetCurrentTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentTick(CpuContext ctx)
{
var tickAddress = ctx[CpuRegister.Rdi];
if (tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8w-H19ip48I",
ExportName = "sceRtcGetTick",
@@ -72,14 +97,57 @@ public static class RtcExports
rtcDateTime.Minute,
rtcDateTime.Second,
DateTimeKind.Utc);
tickValue = checked((ulong)((baseDateTime.Ticks / 10) + rtcDateTime.Microsecond));
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ueega6v3GUw",
ExportName = "sceRtcSetTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetTick(CpuContext ctx)
{
var dateTimeAddress = ctx[CpuRegister.Rdi];
var tickAddress = ctx[CpuRegister.Rsi];
if (dateTimeAddress == 0 || tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
DateTime dateTime;
try
{
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
}
catch (ArgumentOutOfRangeException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -108,6 +176,21 @@ public static class RtcExports
return true;
}
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
{
Span<byte> rtcDateTime = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
BinaryPrimitives.WriteUInt32LittleEndian(
rtcDateTime[12..16],
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
return ctx.Memory.TryWrite(address, rtcDateTime);
}
private readonly record struct RtcDateTime(
ushort Year,
ushort Month,
@@ -0,0 +1,200 @@
// 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 = ctx.TryReadUInt64(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 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,549 @@
// 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 (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!ctx.TryWriteUInt32(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) ||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
!ctx.TryReadUInt32(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 (!ctx.TryWriteUInt32(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) ||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
!ctx.TryReadUInt32(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) ||
!ctx.TryReadUInt32(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 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);
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Share;
public static class ShareExports
{
private const int MaxContentParamBytes = 4096;
private static int _initialized;
private static string _contentParam = string.Empty;
[SysAbiExport(
Nid = "nBDD66kiFW8",
ExportName = "sceShareInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceShareUtility")]
public static int ShareInitialize(CpuContext ctx)
{
var memorySize = ctx[CpuRegister.Rdi];
var priority = unchecked((int)ctx[CpuRegister.Rsi]);
var affinityMask = ctx[CpuRegister.Rdx];
if (memorySize == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Interlocked.Exchange(ref _initialized, 1);
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "7QZtURYnXG4",
ExportName = "sceShareSetContentParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceShareUtility")]
public static int ShareSetContentParam(CpuContext ctx)
{
var contentParamAddress = ctx[CpuRegister.Rdi];
if (contentParamAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_contentParam = contentParam;
if (Volatile.Read(ref _initialized) == 0)
{
TraceShare("set_content_param before initialize");
}
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
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 bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
Span<byte> bytes = stackalloc byte[maxLength];
Span<byte> one = stackalloc byte[1];
for (var index = 0; index < maxLength; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, one))
{
value = string.Empty;
return false;
}
if (one[0] == 0)
{
value = Encoding.UTF8.GetString(bytes[..index]);
return true;
}
bytes[index] = one[0];
}
value = string.Empty;
return false;
}
private static string FormatTraceString(string value)
{
var normalized = value.Replace("\r", "\\r", StringComparison.Ordinal).Replace("\n", "\\n", StringComparison.Ordinal);
return normalized.Length <= 120 ? normalized : string.Concat(normalized.AsSpan(0, 120), "...");
}
private static void TraceShare(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SHARE"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] share.{message}");
}
}
+11
View File
@@ -8,6 +8,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
</ItemGroup>
<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>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Some files were not shown because too many files have changed in this diff Show More