Compare commits

..

33 Commits

Author SHA1 Message Date
kostyaff c0fd6a80e8 Astro Bot shader type 4, pthread_cond_timedwait, and HLE/memory/cpu bug fixes (#40)
* [agc] Add shader type 4 (GS) and register defaults v13 support

Astro Bot (#11) crashes on boot due to two missing GPU features:

1. Shader type 4 (Geometry Shader) — SPI_SHADER_PGM_LO/HI register
   offsets 0x8A/0x8B were missing. Added constants and switch cases
   for shader type 4 in GetExpectedSpiShaderPgmLo/Hi. Also added
   type 4 to IsEsGeometryShaderType (2 or 4 or 6).

2. Register defaults version 13 — was not recognized as supported.
   Added RegisterDefaultsVersion13 constant and included it in
   IsSupportedRegisterDefaultsVersion.

* [kernel] Add POSIX pthread_cond_timedwait export

SILENT HILL (#4) and Poppy Playtime (#3) crash on boot due to
missing POSIX pthread_cond_timedwait (NID 27bAgiJmOh0).

The Sony wrapper scePthreadCondTimedwait (NID BmMjYxmew1w) was
already implemented, but the raw POSIX symbol was not exported.
Added [SysAbiExport] for pthread_cond_timedwait delegating to
existing PthreadCondWaitCore with timed: true.

* [memory] Fix FlushInstructionCache null process handle

PhysicalVirtualMemory.cs called FlushInstructionCache with null as the
process handle in two places (SetProtection and TryWriteExclusive).
On Windows, a null handle does not reliably resolve to the current
process — the correct call is GetCurrentProcess() (pseudo-handle -1).

Also corrected the P/Invoke signature:
- Changed return type from void to bool with [return: MarshalAs(Bool)]
- Added SetLastError = true
- Added GetCurrentProcess() P/Invoke import

This matches the pattern already used in DirectExecutionBackend.cs
which correctly passes GetCurrentProcess() to all FlushInstructionCache
calls.

* [hle] Distinguish NOT_FOUND from NOT_IMPLEMENTED and log duplicate NIDs

Three diagnostic improvements to the HLE dispatch path:

1. ModuleManager.RegisterFromAssembly — duplicate NID registration was
   silently skipped (dispatchTable first-wins, exportTable last-wins,
   causing metadata divergence). Now logs a warning with the NID and
   export name so conflicts are visible.

2. ModuleManager.TryDispatch — generation mismatch returned
   ORBIS_GEN2_ERROR_NOT_FOUND, conflating 'function does not exist'
   with 'function exists but not for this generation'. Now returns
   ORBIS_GEN2_ERROR_NOT_IMPLEMENTED for generation mismatch, matching
   the existing convention in CpuDispatcher. Also adds debug logging
   for both NOT_FOUND and NOT_IMPLEMENTED paths.

3. DirectExecutionBackend.Imports.cs — the import dispatch else-branch
   (the actual hot path that bypasses ModuleManager.TyDispatch via
   cached export) had the same conflation. Split into:
   - else if (export exists but generation mismatch) → NOT_IMPLEMENTED
   - else (no export at all) → NOT_FOUND
   This makes runtime diagnostics correctly distinguish missing exports
   from generation-unsupported exports.

* [cpu] Check VirtualProtect return values in all stub creation paths

9 VirtualProtect calls in DirectExecutionBackend.cs had unchecked
return values. If VirtualProtect silently fails, memory protection
remains incorrect — stubs allocated with PAGE_EXECUTE_READWRITE (0x40)
never get downgraded to PAGE_EXECUTE_READ (0x20), or guest thread
entry stubs never get upgraded to writable. This causes access
violations on next execution or silent data corruption.

Fixed all 9 sites with proper error handling:
- 6 stub creation methods (return 0 on failure + log error)
- 2 guest thread entry methods (set reason + return Exception)
- 1 guest entry method (set LastError + return MEMORY_FAULT)

Stub creation sites fixed:
- CreateImportDispatchStub (line ~1683)
- EnsureTlsHandler (void, log + return)
- CreateUnresolvedReturnStub (return 0)
- CreateGuestReturnStub (return 0)
- CreateExceptionHandlerTrampoline (return 0)
- CreateTlsStoreHelperStub (return 0)

Guest thread entry sites fixed:
- StartGuestThreadNativeCall (return Exception)
- StartGuestContinuationNativeCall (return Exception)
- RunGuestEntryPoint (return MEMORY_FAULT)

* [kernel] Remove unused duplicate _nextFileDescriptor field

KernelExports.cs declared _nextFileDescriptor but never used it.
The actual field used for file descriptor allocation lives in
KernelMemoryCompatExports.cs (lines 1314, 1337). This was a dead
duplicate causing CS0414 warning.

Build is now 0 errors, 0 warnings.

---------

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-10 21:48:50 +03:00
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
50 changed files with 3544 additions and 870 deletions
+2
View File
@@ -37,3 +37,5 @@ packages/
Thumbs.db
Desktop.ini
ehthumbs.db
.vs/
+7 -1
View File
@@ -1,4 +1,4 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@@ -7,10 +7,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
<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>
BIN
View File
Binary file not shown.
+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" />
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "disable"
"rollForward": "latestFeature"
}
}
+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);
}
+22 -2
View File
@@ -7,16 +7,19 @@ 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>
<PublishSingleFile>true</PublishSingleFile>
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -35,6 +38,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -43,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>
+330 -3
View File
@@ -4,9 +4,108 @@
"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",
@@ -59,6 +158,38 @@
"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",
@@ -73,6 +204,16 @@
"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"
},
@@ -89,6 +230,48 @@
"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, )",
@@ -133,9 +316,61 @@
"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": {
"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",
@@ -143,6 +378,52 @@
}
},
"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",
@@ -150,6 +431,52 @@
}
},
"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",
+2 -9
View File
@@ -415,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))
@@ -456,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,
@@ -358,12 +358,21 @@ public sealed partial class DirectExecutionBackend
}
orbisGen2Result = (OrbisGen2Result)returnValue;
}
else
{
dispatchResolved = false;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
}
else if (importStubEntry.Export is { } mismatchedExport)
{
dispatchResolved = true;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
}
else
{
dispatchResolved = false;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
}
}
finally
{
@@ -1679,10 +1679,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 65;
ptr2[num++] = 95;
ptr2[num++] = 195;
uint num2 = default(uint);
VirtualProtect(ptr, 192u, 32u, &num2);
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
return (nint)ptr;
uint num2 = default(uint);
if (!VirtualProtect(ptr, 192u, 32u, &num2))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
return 0;
}
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
return (nint)ptr;
}
catch
{
@@ -1763,7 +1767,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
tlsHandlerAddress[num++] = 195;
_tlsPatchStubOffset = (num + 15) & ~15;
uint num2 = default(uint);
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
return;
}
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
}
@@ -1784,7 +1792,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[i] = 144;
}
uint num = default(uint);
VirtualProtect(ptr, 4096u, 32u, &num);
if (!VirtualProtect(ptr, 4096u, 32u, &num))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
return 0;
}
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
return (nint)ptr;
}
@@ -1831,7 +1843,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
EmitByte(code, ref offset, 0xC3);
uint oldProtect = default;
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
return 0;
}
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr;
}
@@ -1923,7 +1939,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
uint oldProtect = default;
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
return 0;
}
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr;
}
@@ -2254,7 +2274,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr[num2++] = 144;
}
uint flNewProtect = default(uint);
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
return 0;
}
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
return num;
}
@@ -3591,7 +3615,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
@@ -3728,7 +3756,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
@@ -3988,7 +4020,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
uint num5 = default(uint);
VirtualProtect(ptr, stubSize, 64u, &num5);
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
{
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return false;
}
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
if (_hostRspSlotStorage != 0)
{
@@ -55,7 +55,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
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);
private static extern void* GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
@@ -451,7 +455,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
FlushInstructionCache(null, (void*)address, (nuint)size);
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
}
}
@@ -699,7 +703,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
}
}
+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>
+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];
+6 -2
View File
@@ -47,6 +47,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -105,6 +106,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
}
context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context);
+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
@@ -18,9 +18,9 @@ internal static class Gen5ShaderMetadataReader
out Gen5ShaderMetadata metadata)
{
metadata = default!;
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0 ||
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
{
return false;
}
@@ -28,8 +28,7 @@ internal static class Gen5ShaderMetadataReader
var resourceOffsets = new ulong[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!TryReadUInt64(
ctx,
if (!ctx.TryReadUInt64(
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
out resourceOffsets[resourceClass]))
{
@@ -138,17 +137,4 @@ internal static class Gen5ShaderMetadataReader
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
return true;
}
}
@@ -3,7 +3,6 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Numerics;
namespace SharpEmu.Libs.Agc;
@@ -1405,8 +1404,7 @@ internal static class Gen5ShaderScalarEvaluator
continue;
}
if (!TryReadUInt32(
ctx,
if (!ctx.TryReadUInt32(
address + (ulong)(index * sizeof(uint)),
out var value) &&
!TryReadUserDataScalarLoad(
@@ -1727,16 +1725,4 @@ internal static class Gen5ShaderScalarEvaluator
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
}
+5 -18
View File
@@ -294,7 +294,7 @@ internal static class Gen5ShaderTranslator
var instructionCount = 0;
for (uint pc = 0; instructionCount < MaxInstructions;)
{
if (!TryReadUInt32(ctx, address + pc, out var word))
if (!ctx.TryReadUInt32(address + pc, out var word))
{
error = $"read-failed pc=0x{pc:X}";
return false;
@@ -317,7 +317,7 @@ internal static class Gen5ShaderTranslator
words[0] = word;
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
{
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
if (!ctx.TryReadUInt32(address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
{
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
return false;
@@ -397,7 +397,7 @@ internal static class Gen5ShaderTranslator
case 0x34:
case 0x35:
encoding = Gen5ShaderEncoding.Vop3;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3Extra))
{
error = $"vop3-extra-read-failed pc=0x{pc:X}";
return false;
@@ -418,7 +418,7 @@ internal static class Gen5ShaderTranslator
return DecodeFlat(word, out name, out sizeDwords, out error);
case 0x38:
encoding = Gen5ShaderEncoding.Mubuf;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mubufExtra))
{
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
return false;
@@ -427,7 +427,7 @@ internal static class Gen5ShaderTranslator
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
case 0x3A:
encoding = Gen5ShaderEncoding.Mtbuf;
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mtbufExtra))
{
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
return false;
@@ -1741,19 +1741,6 @@ internal static class Gen5ShaderTranslator
counts[key] = count + 1;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private readonly record struct ShaderDecodeInfo(
int InstructionCount,
Dictionary<string, int> Counts,
+1 -14
View File
@@ -453,7 +453,7 @@ public static class AmprExports
var offset = 0UL;
while (offset < writeOffset)
{
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -883,19 +883,6 @@ public static class AmprExports
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!_traceAmpr)
+3 -10
View File
@@ -95,7 +95,7 @@ public static class AudioOut2Exports
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return TryWriteUInt64(ctx, outContextAddress, handle)
return ctx.TryWriteUInt64(outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -125,7 +125,7 @@ public static class AudioOut2Exports
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return TryWriteUInt64(ctx, outPortAddress, handle)
return ctx.TryWriteUInt64(outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -211,18 +211,11 @@ public static class AudioOut2Exports
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return TryWriteUInt64(ctx, outUserAddress, handle)
return ctx.TryWriteUInt64(outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
+160 -10
View File
@@ -2,7 +2,9 @@
// 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;
@@ -12,7 +14,70 @@ public static class AudioOutExports
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
private static int _nextPortHandle;
private sealed record PortState(int UserId, int Type, uint BufferLength, uint Frequency, int Format);
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",
@@ -33,13 +98,41 @@ public static class AudioOutExports
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)
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);
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);
}
@@ -51,11 +144,13 @@ public static class AudioOutExports
public static int AudioOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return SetReturn(
ctx,
Ports.TryRemove(handle, out _)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
if (!Ports.TryRemove(handle, out var port))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
port.Dispose();
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -65,8 +160,44 @@ public static class AudioOutExports
LibraryName = "libSceAudioOut")]
public static int AudioOutOutput(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
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(
@@ -89,4 +220,23 @@ public static class AudioOutExports
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);
}
+48 -113
View File
@@ -112,7 +112,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorAlignment);
}
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
@@ -130,7 +130,7 @@ public static class FiberExports
return SetReturn(ctx, error);
}
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -143,7 +143,7 @@ public static class FiberExports
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
}
@@ -240,7 +240,7 @@ public static class FiberExports
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -264,7 +264,7 @@ public static class FiberExports
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState);
@@ -284,7 +284,7 @@ public static class FiberExports
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
}
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -318,7 +318,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorPermission);
}
return TryWriteUInt64(ctx, outAddress, fiberAddress)
return ctx.TryWriteUInt64(outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
@@ -342,7 +342,7 @@ public static class FiberExports
return SetReturn(ctx, error);
}
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -352,12 +352,12 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
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) ||
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -384,7 +384,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorNull);
}
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -441,7 +441,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorPermission);
}
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
@@ -482,12 +482,12 @@ public static class FiberExports
}
if (optParam != 0 &&
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -497,25 +497,25 @@ public static class FiberExports
flags |= FiberFlagContextSizeCheck;
}
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
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) ||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
!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 (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@@ -616,9 +616,9 @@ public static class FiberExports
if (previousFiber != 0)
{
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorState);
}
@@ -631,12 +631,12 @@ public static class FiberExports
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
}
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return SetReturn(ctx, FiberErrorInvalid);
@@ -673,7 +673,7 @@ public static class FiberExports
var stackEnd = fields.ContextAddress + fields.ContextSize;
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
if (!TryWriteUInt64(ctx, entryRsp, 0))
if (!ctx.TryWriteUInt64(entryRsp, 0))
{
return false;
}
@@ -708,7 +708,7 @@ public static class FiberExports
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
@@ -779,11 +779,11 @@ public static class FiberExports
return FiberErrorInvalid;
}
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
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;
}
@@ -839,8 +839,8 @@ public static class FiberExports
return false;
}
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
magicStart != FiberSignature0 ||
magicEnd != FiberSignature1)
{
@@ -855,12 +855,12 @@ public static class FiberExports
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
{
fields = default;
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
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;
@@ -898,46 +898,6 @@ public static class FiberExports
return 0;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
@@ -965,31 +925,6 @@ public static class FiberExports
return true;
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
{
Span<byte> current = stackalloc byte[1];
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
@@ -3,7 +3,6 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
@@ -50,7 +49,7 @@ public static class KernelAprCompatExports
return completionResult;
}
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -141,7 +140,7 @@ public static class KernelAprCompatExports
return completionResult;
}
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -178,13 +177,6 @@ public static class KernelAprCompatExports
return tag is 0x0C or 0x10;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static void TraceApr(
CpuContext ctx,
string operation,
@@ -1,7 +1,6 @@
// 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;
@@ -55,7 +54,7 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -219,7 +218,7 @@ public static class KernelEventFlagCompatExports
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -234,7 +233,7 @@ public static class KernelEventFlagCompatExports
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, 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);
@@ -340,7 +339,7 @@ public static class KernelEventFlagCompatExports
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -453,77 +452,6 @@ public static class KernelEventFlagCompatExports
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
{
Span<byte> current = stackalloc byte[1];
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
@@ -614,7 +542,7 @@ public static class KernelEventFlagCompatExports
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadByte(ctx, address, out var value))
if (ctx.TryReadByte(address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
@@ -622,7 +550,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt32(ctx, address, out var value))
if (ctx.TryReadUInt32(address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
@@ -630,7 +558,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt64(ctx, address, out var value))
if (ctx.TryReadUInt64(address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
@@ -287,13 +287,13 @@ public static class KernelEventQueueCompatExports
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
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 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -336,7 +336,7 @@ public static class KernelEventQueueCompatExports
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -541,7 +541,7 @@ public static class KernelEventQueueCompatExports
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -654,29 +654,8 @@ public static class KernelEventQueueCompatExports
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
_ = 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}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelExports
{
private static int _nextFileDescriptor = 2;
private static readonly object _cxaGate = new();
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
private static readonly object _coredumpGate = new();
@@ -7,9 +7,7 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using System.Linq;
using System.Globalization;
namespace SharpEmu.Libs.Kernel;
@@ -2531,6 +2529,17 @@ public static class KernelMemoryCompatExports
if (fixedMapping && requestedAddress != 0)
{
mappedAddress = requestedAddress;
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
if (!reserved)
{
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
}
if (!reserved)
{
mappedAddress = 0;
}
}
else
{
@@ -2541,7 +2550,7 @@ public static class KernelMemoryCompatExports
Console.Error.WriteLine(
$"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}");
}
if (!reserved)
if (!reserved && !fixedMapping)
{
if (mappedAddress == 0)
{
@@ -2625,12 +2634,18 @@ public static class KernelMemoryCompatExports
if (fixedMapping && requestedAddress != 0)
{
mappedAddress = requestedAddress;
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
{
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
{
mappedAddress = 0;
}
}
}
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, OrbisPageSize, out mappedAddress))
{
mappedAddress = requestedAddress != 0 && fixedMapping
? requestedAddress
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
mappedAddress = AllocateMappedGuestAddress(ctx, length, 0x1000UL);
}
if (ShouldTraceDirectMemory())
@@ -3995,6 +4010,37 @@ public static class KernelMemoryCompatExports
out mappedAddress);
}
private static bool TryReserveExactGuestVirtualRange(
CpuContext ctx,
ulong desiredAddress,
ulong length,
int protection)
{
var executable = (protection & OrbisProtCpuExec) != 0;
return KernelVirtualRangeAllocator.TryReserve(
ctx,
desiredAddress,
length,
executable,
alignment: 0,
allowSearch: false,
allowAllocateAtAlternative: false,
"reserve fixed range",
out _);
}
private static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
{
if (address == 0 || length == 0 || ulong.MaxValue - address < length - 1)
{
return false;
}
Span<byte> probe = stackalloc byte[1];
return ctx.Memory.TryRead(address, probe) &&
ctx.Memory.TryRead(address + length - 1, probe);
}
private static bool IsMappedGuestRangeAvailable(
CpuContext ctx,
ulong address,
@@ -4479,7 +4525,7 @@ public static class KernelMemoryCompatExports
continue;
}
Span<byte> one = stackalloc byte[1];
var one = chunk.AsSpan(0, 1);
if (!TryReadCompat(ctx, current, one))
{
return false;
@@ -329,6 +329,12 @@ public static class KernelPthreadCompatExports
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "27bAgiJmOh0",
ExportName = "pthread_cond_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
[SysAbiExport(
Nid = "m5-2bsNfv7s",
@@ -1018,19 +1024,12 @@ public static class KernelPthreadCompatExports
}
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BitConverter.TryWriteBytes(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
{
@@ -22,6 +22,7 @@ public static class KernelPthreadExtendedCompatExports
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();
@@ -31,6 +32,7 @@ public static class KernelPthreadExtendedCompatExports
private static int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
private static long _nextSyntheticRwlockAttrHandleId = 1;
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
@@ -839,6 +841,37 @@ public static class KernelPthreadExtendedCompatExports
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;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "6ULAa0fq4jA",
ExportName = "scePthreadRwlockInit",
@@ -1016,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",
@@ -1045,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;
}
@@ -1,9 +1,7 @@
// 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.Kernel;
@@ -49,7 +47,7 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -68,7 +66,7 @@ public static class KernelSemaphoreCompatExports
Count = initialCount,
};
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -110,12 +108,12 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0)
{
if (!TryReadUInt32(ctx, timeoutAddress, out _))
if (!ctx.TryReadUInt32(timeoutAddress, out _))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = 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);
}
@@ -222,7 +220,7 @@ public static class KernelSemaphoreCompatExports
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -258,56 +256,6 @@ public static class KernelSemaphoreCompatExports
return value;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
{
return false;
}
var bytes = new byte[Math.Min(maxLength, 4096)];
for (var i = 0; i < bytes.Length; i++)
{
Span<byte> current = stackalloc byte[1];
if (!ctx.Memory.TryRead(address + (ulong)i, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, i);
return true;
}
bytes[i] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
+3 -10
View File
@@ -234,7 +234,7 @@ public static class PlayGoExports
if (outChunkIdList == 0)
{
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
return TryWriteUInt32(ctx, outEntries, availableEntries)
return ctx.TryWriteUInt32(outEntries, availableEntries)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -256,7 +256,7 @@ public static class PlayGoExports
}
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
return TryWriteUInt32(ctx, outEntries, entriesToWrite)
return ctx.TryWriteUInt32(outEntries, entriesToWrite)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -503,7 +503,7 @@ public static class PlayGoExports
}
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
return TryWriteUInt32(ctx, outEntries, 0)
return ctx.TryWriteUInt32(outEntries, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -748,13 +748,6 @@ public static class PlayGoExports
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(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)];
@@ -60,7 +60,7 @@ public static class SaveDataDialogExports
}
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 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.
@@ -182,19 +182,6 @@ public static class SaveDataDialogExports
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
value = 0;
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
+9 -29
View File
@@ -108,8 +108,8 @@ public static class SaveDataExports
var setNum = result.DirNamesNum == 0
? 0
: Math.Min(result.DirNamesNum, entries.Count);
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
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);
}
@@ -173,9 +173,9 @@ public static class SaveDataExports
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
!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))
{
@@ -263,7 +263,7 @@ public static class SaveDataExports
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
if (!TryWriteUInt32(ctx, resourceAddress, id))
if (!ctx.TryWriteUInt32(resourceAddress, id))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -280,8 +280,8 @@ public static class SaveDataExports
if (!TryReadInt32(ctx, address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
{
return false;
}
@@ -304,7 +304,7 @@ public static class SaveDataExports
{
result = default;
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
{
@@ -518,26 +518,6 @@ public static class SaveDataExports
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
+4
View File
@@ -15,6 +15,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Silk.NET.Windowing" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
+7 -20
View File
@@ -922,9 +922,9 @@ public static class VideoOutExports
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
{
attribute = default;
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
if (!ctx.TryReadUInt32(attributeAddress + 0x04, out var tilingMode) ||
!ctx.TryReadUInt32(attributeAddress + 0x0C, out var width) ||
!ctx.TryReadUInt32(attributeAddress + 0x10, out var height))
{
return false;
}
@@ -941,10 +941,10 @@ public static class VideoOutExports
return true;
}
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
if (!ctx.TryReadUInt32(attributeAddress + 0x00, out var pixelFormat32) ||
!ctx.TryReadUInt32(attributeAddress + 0x08, out var aspectRatio) ||
!ctx.TryReadUInt32(attributeAddress + 0x14, out var pitchInPixel) ||
!ctx.TryReadUInt32(attributeAddress + 0x18, out var option32))
{
return false;
}
@@ -1291,19 +1291,6 @@ public static class VideoOutExports
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
{
Span<byte> buffer = stackalloc byte[sizeof(short)];
@@ -4,4 +4,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>