Compare commits

...

12 Commits

Author SHA1 Message Date
Mike Saito 9ddc09ea91 core: page-aware TryReadUtf8Z and unify exit/_exit handling (#57)
Read guest C strings in page-bounded chunks without heap allocations.
Return false when the buffer fills without a null terminator. Route exit
and _exit through RequestProcessExit.
2026-07-11 15:42:43 +03:00
Brando c4326a1143 Update README.md for Discord (#55)
* Added Discord link
2026-07-11 14:21:06 +03:00
ParantezTech 347e33f3c9 [GUI] update icon to .ico format 2026-07-11 14:03:55 +03:00
Brando 48a694e509 GUI: redesign library as a cover-art grid with game management (#48)
* GUI: redesign library as a cover-art grid with game management

Replace the sidebar game list with a full-width grid of cover tiles.
Cover art is loaded automatically from each game's sce_sys/icon0.png
(pic0.png fallback) and decoded off the UI thread; games without art
get a deterministic gradient placeholder with the title's initials.

- New layout: search/scan toolbar, tile grid with hover and selection
  states, bottom launch bar with cover thumbnail, collapsible launch
  options and console panels (console auto-opens on launch)
- Right-click context menu on tiles: launch, open game folder, copy
  path/title ID, remove from library
- Removed games persist in an ExcludedGames settings list; re-adding
  a folder restores any removed games beneath it
- Search now also matches title IDs
- Fix: placeholder brushes were constructed on the scan thread, which
  throws in Avalonia and was silently swallowed, yielding empty scans

* GUI: show full install folder size instead of eboot.bin size

The library previously displayed the size of eboot.bin alone, which
wildly understates a game's real footprint. The install folder is now
totaled recursively in the existing background pass (after cover art,
which is cheaper and more visible), and each tile updates live once
its size is ready.

* GUI: selection backdrop, smaller tiles, controller navigation

Address review feedback on the library redesign:

- Selecting a game fades its key art (sce_sys/pic0.png, pic1.png
  fallback) in as the window backdrop, dimmed by a gradient scrim;
  decoded off the UI thread and cached per entry
- Cover tiles reduced from 156px to 128px
- The library can be driven with a DualSense: d-pad/left stick moves
  the selection (hold-to-repeat, row-aware), Cross launches, Circle
  stops; input is ignored while the launcher window is unfocused.
  Reuses the pad HID reader by compile-linking its dependency-free
  sources instead of referencing all of SharpEmu.Libs
2026-07-11 13:17:10 +03:00
Brando 165927882b Pad: native DualSense support via raw HID (#52)
* Pad: native DualSense support via raw HID

Read a real DualSense (or DualSense Edge) controller directly over
Win32 HID and feed its state into scePadRead/scePadReadState, replacing
the keyboard-only input path. No new dependencies.

- Device discovery by Sony VID/PID through setupapi/hid.dll, with
  hot-plug: disconnects fall back to keyboard and reconnect
  automatically
- USB input report 0x01 and Bluetooth extended report 0x31 (activated
  via the feature report 0x05 handshake) are both parsed
- Full mapping to SCE_PAD_BUTTON conventions: face buttons, d-pad hat,
  L1/R1/L2/R2 digital bits, analog triggers, L3/R3, Options, touchpad
  click, both sticks
- Controller and keyboard input merge: buttons OR together, controller
  sticks win past a small deadzone, triggers take the max

* Pad: rumble and lightbar output for DualSense

Wire scePadSetVibration, scePadSetLightBar and scePadResetLightBar to
real DualSense output reports. The output payload follows the same
layout as the Linux hid-playstation driver: both rumble motors,
lightbar RGB and the player LED indicator.

- USB uses output report 0x02; Bluetooth uses the 0x31 wrapper with a
  sequence tag and CRC32 (0xA2-seeded) trailer, transport detected
  from the first input report
- Output goes through a dedicated device handle so writes never
  contend with the blocking input read loop
- On connect the controller gets a default state (blue lightbar,
  player 1 LED); rumble state resets on disconnect

Verified on hardware over USB: lightbar color cycling and both motors.
Bluetooth output is implemented per spec but not yet hardware-tested.
2026-07-11 11:54:38 +03:00
kostyaff d9d1aeaef9 [logging] Migrate CpuDispatcher, SharpEmuRuntime, PhysicalVirtualMemory to SharpEmuLog (#51)
Replaces 34 Console.Error.WriteLine call sites across 3 Core files
with structured SharpEmuLog calls (Debug/Info/Warning/Error/Critical).

CpuDispatcher.cs (10 sites):
- DispatchEntry/DispatchModuleInitializer START and entry-point logs -> Debug
- FATAL EXCEPTION catch blocks -> Critical (with exception object)
- Native backend FAILED -> Error

SharpEmuRuntime.cs (23 sites):
- Loading/Entry/Dispatching/DispatchEntry returned -> Info
- Module load/registered/preload summary -> Info
- Initializer dispatch failed/module start failed -> Error
- Imported data unresolved -> Warning, write-failed -> Error
- Import stub conflict -> Warning
- Trace-level rebind logs -> Debug

PhysicalVirtualMemory.cs (1 site):
- TraceVmem helper -> Log.Debug (SHARPEMU_LOG_VMEM env gate preserved)

Build: 0 errors, 0 warnings

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-11 11:54:15 +03:00
kostyaff 57e737b5d7 [logging] Add FileLogSink, CompositeLogSink, and SHARPEMU_LOG_FILE env support (#50)
- FileLogSink: thread-safe file writer with AutoFlush, FileShare.Read for
  concurrent read access (tail -f), automatic parent directory creation,
  full date-time timestamps, IDisposable for graceful shutdown
- CompositeLogSink: fan-out to multiple sinks with per-sink exception
  isolation (one broken sink cannot silence the others), IDisposable
  propagates to children
- SharpEmuLog: ResolveSinkFromEnvironment() reads SHARPEMU_LOG_FILE and
  creates CompositeLogSink(console + file) when set; Sink setter now
  disposes the previous IDisposable sink to prevent file handle leaks;
  Shutdown() flushes and disposes the active sink
- Program.cs: Main wrapped in try/finally to guarantee SharpEmuLog.Shutdown()
  runs on all exit paths (GUI, mitigated child, normal, exception)

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-11 11:53:59 +03:00
ParantezTech a78b02f22b [dotnet] revert package for Microsoft.NET.ILLink.Tasks 2026-07-11 05:26:24 +03:00
ParantezTech 70ec2928ea [revert] Revert VulkanVideoPresenter.cs to previous version, added new screenshot, and updated packages.lock.json 2026-07-11 05:20:13 +03:00
Berk c618c116ba [shader-decoder] Fix address calculation for SW linear textures (#45) 2026-07-11 05:12:41 +03:00
Dawid 29021b5a71 [fixes] move repeating methods into CpuContext (#41) 2026-07-10 23:46:50 +03:00
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
54 changed files with 2774 additions and 1035 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 120 KiB

BIN
View File
Binary file not shown.
+13 -1
View File
@@ -36,6 +36,18 @@ internal static partial class Program
[STAThread] [STAThread]
private static int Main(string[] args) private static int Main(string[] args)
{
try
{
return Run(args);
}
finally
{
SharpEmuLog.Shutdown();
}
}
private static int Run(string[] args)
{ {
args = NormalizeInternalArguments(args, out var isMitigatedChild); args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild) if (args.Length == 0 && !isMitigatedChild)
@@ -66,7 +78,7 @@ internal static partial class Program
ebootPath = Path.GetFullPath(ebootPath); ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}"); Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
if (!File.Exists(ebootPath)) if (!File.Exists(ebootPath))
{ {
Log.Error($"EBOOT file was not found: {ebootPath}"); Log.Error($"EBOOT file was not found: {ebootPath}");
+1 -1
View File
@@ -484,4 +484,4 @@
} }
} }
} }
} }
+11 -10
View File
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory; using SharpEmu.Core.Memory;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu; namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{ {
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
private enum EntryFrameKind private enum EntryFrameKind
{ {
ProcessEntry, ProcessEntry,
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string processImageName = "eboot.bin", string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default) CpuExecutionOptions executionOptions = default)
{ {
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ==="); Log.Debug("=== DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}"); Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
try try
{ {
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}"); Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw; throw;
} }
} }
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string moduleName = "module", string moduleName = "module",
CpuExecutionOptions executionOptions = default) CpuExecutionOptions executionOptions = default)
{ {
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ==="); Log.Debug("=== DispatchModuleInitializer START ===");
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}"); Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
try try
{ {
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}"); Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw; throw;
} }
} }
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
CpuExecutionOptions executionOptions = default, CpuExecutionOptions executionOptions = default,
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry) EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
{ {
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING..."); Log.Debug("DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint; LastEntryPoint = entryPoint;
LastTrapInfo = null; LastTrapInfo = null;
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
LastMilestoneLog, LastMilestoneLog,
Environment.NewLine, Environment.NewLine,
$"CpuEngine native-only failed: {backendError}"); $"CpuEngine native-only failed: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}"); Log.Error($"Native backend FAILED: {backendError}");
return FailEarly( return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable); CpuExitReason.NativeBackendUnavailable);
@@ -358,12 +358,21 @@ public sealed partial class DirectExecutionBackend
} }
orbisGen2Result = (OrbisGen2Result)returnValue; orbisGen2Result = (OrbisGen2Result)returnValue;
} }
else else if (importStubEntry.Export is { } mismatchedExport)
{ {
dispatchResolved = false; dispatchResolved = true;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result); 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 finally
{ {
@@ -1679,10 +1679,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 65; ptr2[num++] = 65;
ptr2[num++] = 95; ptr2[num++] = 95;
ptr2[num++] = 195; ptr2[num++] = 195;
uint num2 = default(uint); uint num2 = default(uint);
VirtualProtect(ptr, 192u, 32u, &num2); if (!VirtualProtect(ptr, 192u, 32u, &num2))
FlushInstructionCache(GetCurrentProcess(), ptr, 192u); {
return (nint)ptr; 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 catch
{ {
@@ -1763,7 +1767,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
tlsHandlerAddress[num++] = 195; tlsHandlerAddress[num++] = 195;
_tlsPatchStubOffset = (num + 15) & ~15; _tlsPatchStubOffset = (num + 15) & ~15;
uint num2 = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}"); 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; ptr2[i] = 144;
} }
uint num = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
return (nint)ptr; return (nint)ptr;
} }
@@ -1831,7 +1843,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
EmitByte(code, ref offset, 0xC3); EmitByte(code, ref offset, 0xC3);
uint oldProtect = default; 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); FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr; return (nint)ptr;
} }
@@ -1923,7 +1939,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int)); *(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
uint oldProtect = default; 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); FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr; return (nint)ptr;
} }
@@ -2254,7 +2274,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr[num2++] = 144; ptr[num2++] = 144;
} }
uint flNewProtect = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
return num; return num;
} }
@@ -3591,7 +3615,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception; return GuestNativeCallExitReason.Exception;
} }
uint oldProtect = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false; ActiveGuestThreadYieldRequested = false;
@@ -3728,7 +3756,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception; return GuestNativeCallExitReason.Exception;
} }
uint oldProtect = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false; ActiveGuestThreadYieldRequested = false;
@@ -3988,7 +4020,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false; return false;
} }
uint num5 = default(uint); 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); FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
if (_hostRspSlotStorage != 0) if (_hostRspSlotStorage != 0)
{ {
@@ -4,11 +4,14 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Memory; namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{ {
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
private readonly object _guestAllocationGate = new(); private readonly object _guestAllocationGate = new();
private readonly object _allocationSearchHintGate = new(); private readonly object _allocationSearchHintGate = new();
@@ -55,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength); private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")] [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) public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{ {
@@ -451,7 +458,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
if ((flags & ProgramHeaderFlags.Execute) != 0) if ((flags & ProgramHeaderFlags.Execute) != 0)
{ {
FlushInstructionCache(null, (void*)address, (nuint)size); FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
} }
} }
@@ -699,7 +706,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _); VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect)) if (IsExecutableProtection(oldProtect))
{ {
FlushInstructionCache(null, destPtr, (nuint)source.Length); FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
} }
} }
@@ -1048,7 +1055,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return; return;
} }
Console.Error.WriteLine($"[VMEM] {message}"); Log.Debug(message);
} }
public void Dispose() public void Dispose()
+38 -35
View File
@@ -11,6 +11,7 @@ using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent; using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData; using SharpEmu.Libs.SaveData;
using SharpEmu.Logging;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -20,6 +21,8 @@ namespace SharpEmu.Core.Runtime;
public sealed class SharpEmuRuntime : ISharpEmuRuntime public sealed class SharpEmuRuntime : ISharpEmuRuntime
{ {
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
private readonly record struct LoadedModuleImage(string Path, SelfImage Image); private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
@@ -128,7 +131,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{ {
var normalizedEbootPath = Path.GetFullPath(ebootPath); var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath); using var app0Binding = BindApp0Root(normalizedEbootPath);
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}"); Log.Info($"Loading: {ebootPath}");
LastExecutionDiagnostics = null; LastExecutionDiagnostics = null;
LastExecutionTrace = null; LastExecutionTrace = null;
LastSessionSummary = null; LastSessionSummary = null;
@@ -140,7 +143,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
SaveDataExports.ConfigureApplicationInfo(image.TitleId); SaveDataExports.ConfigureApplicationInfo(image.TitleId);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}"); Log.Info($"Entry: 0x{image.EntryPoint:X16}");
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4; var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs); var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal); var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
@@ -163,7 +166,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName); processImageName);
if (initializerResult is { } failedInitializerResult) if (initializerResult is { } failedInitializerResult)
{ {
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}"); Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace; LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog; LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary); LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -171,8 +174,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return failedInitializerResult; return failedInitializerResult;
} }
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}"); Log.Info($"Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}"); Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry( var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint, image.EntryPoint,
@@ -182,8 +185,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName, processImageName,
_cpuExecutionOptions); _cpuExecutionOptions);
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}"); Log.Info($"DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}"); Log.Info($"Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace; LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog; LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary); LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -434,8 +437,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
moduleName = $"module#{i}"; moduleName = $"module#{i}";
} }
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}"); $"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
var result = _cpuDispatcher.DispatchModuleInitializer( var result = _cpuDispatcher.DispatchModuleInitializer(
initEntryPoint, initEntryPoint,
@@ -446,8 +449,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
_cpuExecutionOptions); _cpuExecutionOptions);
if (result != OrbisGen2Result.ORBIS_GEN2_OK) if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{ {
Console.Error.WriteLine( Log.Error(
$"[RUNTIME] Module start failed: {moduleName} -> {result}"); $"Module start failed: {moduleName} -> {result}");
return result; return result;
} }
} }
@@ -468,8 +471,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null; return null;
} }
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}"); $"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
var result = RunInitializerList( var result = RunInitializerList(
$"{label}:preinit", $"{label}:preinit",
@@ -508,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
continue; continue;
} }
Console.Error.WriteLine( Log.Debug(
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}"); $" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
var result = _cpuDispatcher.DispatchEntry( var result = _cpuDispatcher.DispatchEntry(
initializerAddress, initializerAddress,
@@ -577,7 +580,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
.ToArray(); .ToArray();
if (skippedModules.Length > 0) if (skippedModules.Length > 0)
{ {
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}"); Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
} }
if (modulePaths.Length == 0) if (modulePaths.Length == 0)
@@ -585,8 +588,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages; return loadedImages;
} }
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}"); Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)..."); Log.Info($"Loading {modulePaths.Length} module(s)...");
var loadedModules = 0; var loadedModules = 0;
var failedModules = 0; var failedModules = 0;
var mergedImportCount = 0; var mergedImportCount = 0;
@@ -621,18 +624,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage)); loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
loadedModules++; loadedModules++;
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}"); $"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
} }
catch (Exception ex) catch (Exception ex)
{ {
failedModules++; failedModules++;
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})"); Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
} }
} }
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}"); $"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
return loadedImages; return loadedImages;
} }
@@ -652,8 +655,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (rebound != 0 || unresolved != 0) if (rebound != 0 || unresolved != 0)
{ {
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}"); $"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
} }
} }
@@ -685,8 +688,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{ {
if (logRebind) if (logRebind)
{ {
Console.Error.WriteLine( Log.Warning(
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}"); $"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
} }
unresolved++; unresolved++;
@@ -698,8 +701,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{ {
if (logRebind) if (logRebind)
{ {
Console.Error.WriteLine( Log.Error(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}"); $"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
} }
unresolved++; unresolved++;
@@ -708,8 +711,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (logRebind) if (logRebind)
{ {
Console.Error.WriteLine( Log.Debug(
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}"); $"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
} }
rebound++; rebound++;
@@ -745,8 +748,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{ {
if (!string.Equals(existingNid, nid, StringComparison.Ordinal)) if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{ {
Console.Error.WriteLine( Log.Warning(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})"); $"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
} }
continue; continue;
@@ -855,8 +858,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
image.EntryPoint, image.EntryPoint,
isMain, isMain,
isSystemModule); isSystemModule);
Console.Error.WriteLine( Log.Info(
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}"); $"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
} }
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size) private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
+86 -6
View File
@@ -11,7 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Application.Resources> <Application.Resources>
<Color x:Key="SystemAccentColor">#7C5CFC</Color> <Color x:Key="SystemAccentColor">#7C5CFC</Color>
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" /> <LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0" Color="#12151F" />
<GradientStop Offset="0.55" Color="#0D1017" />
<GradientStop Offset="1" Color="#0B0D14" />
</LinearGradientBrush>
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" /> <SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
<SolidColorBrush x:Key="CardBrush" Color="#141924" /> <SolidColorBrush x:Key="CardBrush" Color="#141924" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" /> <SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
@@ -24,6 +29,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" /> <SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" /> <SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" /> <SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>
@@ -38,7 +45,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Background" Value="{StaticResource CardBrush}" /> <Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="16" /> <Setter Property="Padding" Value="16" />
</Style> </Style>
@@ -61,6 +68,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Margin" Value="0,0,0,6" /> <Setter Property="Margin" Value="0,0,0,6" />
</Style> </Style>
<Style Selector="TextBox">
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.accent"> <Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" /> <Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" /> <Setter Property="Foreground" Value="White" />
@@ -98,6 +109,41 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Foreground" Value="{StaticResource TextBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style> </Style>
<Style Selector="ToggleButton.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="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ContextMenu">
<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="6" />
</Style>
<Style Selector="ContextMenu MenuItem">
<Setter Property="Padding" Value="10,7" />
<Setter Property="CornerRadius" Value="7" />
</Style>
<Style Selector="ContextMenu Separator">
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="8,4" />
</Style>
<Style Selector="ListBox.console"> <Style Selector="ListBox.console">
<Setter Property="Background" Value="#0B0E14" /> <Setter Property="Background" Value="#0B0E14" />
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" /> <Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
@@ -108,10 +154,44 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="MinHeight" Value="0" /> <Setter Property="MinHeight" Value="0" />
</Style> </Style>
<Style Selector="ListBox.library ListBoxItem"> <!-- Cover-art library grid -->
<Setter Property="CornerRadius" Value="8" /> <Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="Margin" Value="4,2" /> <Setter Property="Padding" Value="10" />
<Setter Property="Padding" Value="8,4" /> <Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="RenderTransform" Value="translateY(0px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
</Transitions>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
<Setter Property="RenderTransform" Value="translateY(-3px)" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
</Style>
<Style Selector="Border.coverShadow">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
</Style>
<Style Selector="Border.coverClip">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
</Style> </Style>
</Application.Styles> </Application.Styles>
+136 -2
View File
@@ -1,13 +1,147 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes) public sealed class GameEntry : INotifyPropertyChanged
{ {
// Placeholder gradients for games without cover art, picked
// deterministically from the game name so a game keeps its color.
private static readonly (Color Start, Color End)[] PlaceholderPalette =
{
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
};
private Bitmap? _cover;
private IBrush? _placeholderBrush;
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
BackgroundPath = backgroundPath;
Initials = ComputeInitials(name);
}
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public string? TitleId { get; }
public string Path { get; }
/// <summary>
/// Total size of the game. Initially the eboot's own size from the scan;
/// replaced with the full install folder size once computed in the
/// background.
/// </summary>
public long SizeBytes
{
get => _sizeBytes;
set
{
if (_sizeBytes == value)
{
return;
}
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
}
}
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
public string? CoverPath { get; }
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
public string? BackgroundPath { get; }
/// <summary>
/// Decoded key art used as the window backdrop while this game is
/// selected. Loaded on demand and cached; not exposed via binding.
/// </summary>
public Bitmap? Background { get; set; }
public string Initials { get; }
// Built lazily: brushes are AvaloniaObjects that must be created on the
// UI thread, while GameEntry itself is constructed on the scan thread.
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
public Bitmap? Cover
{
get => _cover;
set
{
if (ReferenceEquals(_cover, value))
{
return;
}
_cover = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
}
}
public bool HasCover => _cover is not null;
public string Detail => TitleId is not null public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}" ? $"{TitleId} • {FormatSize(SizeBytes)}"
: $"{FormatSize(SizeBytes)} • {Path}"; : FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
var initials = name
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(word => char.IsLetterOrDigit(word[0]))
.Select(word => char.ToUpperInvariant(word[0]))
.Take(2)
.ToArray();
return initials.Length > 0 ? new string(initials) : "?";
}
private static IBrush BuildPlaceholderBrush(string name)
{
var hash = 0;
foreach (var ch in name)
{
hash = unchecked(hash * 31 + ch);
}
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
return new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop(start, 0),
new GradientStop(end, 1),
},
};
}
private static string FormatSize(long bytes) private static string FormatSize(long bytes)
{ {
+3
View File
@@ -14,6 +14,9 @@ public sealed class GuiSettings
public List<string> GameFolders { get; set; } = new(); public List<string> GameFolders { get; set; } = new();
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
public List<string> ExcludedGames { get; set; } = new();
public string LogLevel { get; set; } = "Info"; public string LogLevel { get; set; } = "Info";
public int ImportTraceLimit { get; set; } public int ImportTraceLimit { get; set; }
+205 -97
View File
@@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.MainWindow" x:Class="SharpEmu.GUI.MainWindow"
Title="SharpEmu" Title="SharpEmu"
Width="1280" Height="800" Width="1280" Height="820"
MinWidth="980" MinHeight="620" MinWidth="980" MinHeight="640"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}" Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
@@ -18,10 +18,33 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Grid RowDefinitions="44,*,32"> <Grid RowDefinitions="44,*,32">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
<Panel Grid.Row="1" ClipToBounds="True">
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Image.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
</Transitions>
</Image.Transitions>
</Image>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#730D1017" />
<GradientStop Offset="0.55" Color="#BF0D1017" />
<GradientStop Offset="1" Color="#EB0D1017" />
</LinearGradientBrush>
</Border.Background>
</Border>
</Panel>
<!-- Title bar --> <!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}"> <Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" /> <Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" /> <TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
<Border Classes="pill"> <Border Classes="pill">
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" /> <TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
@@ -30,119 +53,204 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid> </Grid>
<!-- Main content --> <!-- Main content -->
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*"> <Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
<!-- Game library --> <!-- Library toolbar -->
<Border Grid.Column="0" Classes="card" Padding="0"> <Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
<Grid RowDefinitions="Auto,Auto,*,Auto"> <TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10"> <Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" /> <TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
<Border Grid.Column="1" Classes="pill"> </Border>
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" /> <TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
</Border> VerticalAlignment="Center" Margin="0,0,12,0" />
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content=" Add folder"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
VerticalAlignment="Center" />
</Grid>
<!-- Cover-art grid -->
<Panel Grid.Row="1">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="186" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<StackPanel Spacing="2">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
</Panel>
<!-- Console (collapsible) -->
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
<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> </Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" /> BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Margin="4,4" Spacing="2"> <TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </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> </Grid>
</Border> </Border>
<!-- Right column --> <!-- Launch bar -->
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*"> <Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Selected game / launch --> <!-- Selected game cover thumbnail -->
<Border Grid.Row="0" Classes="card"> <Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
<Grid ColumnDefinitions="*,Auto"> VerticalAlignment="Center">
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center"> <Panel x:Name="SelectedCoverPanel">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" /> <Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
IsVisible="{Binding !HasCover, FallbackValue=False}">
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill"
IsVisible="{Binding HasCover, FallbackValue=False}" />
</Panel>
</Border>
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly." <TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" /> FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="7">
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" /> <Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" /> <Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" /> <Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border>
<!-- Launch options --> <!-- Launch options (collapsible) -->
<Border Grid.Row="2" Classes="card"> <WrapPanel x:Name="OptionsPanel" IsVisible="False">
<StackPanel Spacing="12"> <StackPanel Margin="0,0,24,0">
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" /> <TextBlock Classes="fieldLabel" Text="CPU engine" />
<WrapPanel> <ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<StackPanel Margin="0,0,24,0"> <ComboBoxItem Content="Native" />
<TextBlock Classes="fieldLabel" Text="CPU engine" /> </ComboBox>
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8"> </StackPanel>
<ComboBoxItem Content="Native" /> <StackPanel Margin="0,0,24,0">
</ComboBox> <TextBlock Classes="fieldLabel" Text="Log level" />
</StackPanel> <ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<StackPanel Margin="0,0,24,0"> <ComboBoxItem Content="Trace" />
<TextBlock Classes="fieldLabel" Text="Log level" /> <ComboBoxItem Content="Debug" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8"> <ComboBoxItem Content="Info" />
<ComboBoxItem Content="Trace" /> <ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Debug" /> <ComboBoxItem Content="Error" />
<ComboBoxItem Content="Info" /> <ComboBoxItem Content="Critical" />
<ComboBoxItem Content="Warning" /> </ComboBox>
<ComboBoxItem Content="Error" /> </StackPanel>
<ComboBoxItem Content="Critical" /> <StackPanel Margin="0,0,24,0">
</ComboBox> <TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
</StackPanel> <NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
<StackPanel Margin="0,0,24,0"> Value="0" FormatString="0" CornerRadius="8" />
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" /> </StackPanel>
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16" <StackPanel>
Value="0" FormatString="0" CornerRadius="8" /> <TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
</StackPanel> <ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
<StackPanel> </StackPanel>
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" /> </WrapPanel>
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" /> </StackPanel>
</StackPanel> </Border>
</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> </Grid>
<!-- Status bar --> <!-- Status bar -->
+459 -9
View File
@@ -5,11 +5,17 @@ using System.Collections.Concurrent;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
using SharpEmu.Libs.Pad;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
@@ -35,6 +41,16 @@ public partial class MainWindow : Window
private string? _emulatorExePath; private string? _emulatorExePath;
private bool _isRunning; private bool _isRunning;
private int _autoScrollTicks; private int _autoScrollTicks;
private int _detailLoadGeneration;
private int _backdropGeneration;
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
private uint _previousPadButtons;
private long _navLeftNextAt;
private long _navRightNextAt;
private long _navUpNextAt;
private long _navDownNextAt;
public MainWindow() public MainWindow()
{ {
@@ -59,15 +75,142 @@ public partial class MainWindow : Window
GameList.DoubleTapped += (_, _) => LaunchSelected(); GameList.DoubleTapped += (_, _) => LaunchSelected();
SearchBox.TextChanged += (_, _) => RefreshVisibleGames(); SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
AddFolderButton.Click += async (_, _) => await AddFolderAsync(); AddFolderButton.Click += async (_, _) => await AddFolderAsync();
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
RescanButton.Click += async (_, _) => await RescanLibraryAsync(); RescanButton.Click += async (_, _) => await RescanLibraryAsync();
OpenFileButton.Click += async (_, _) => await OpenFileAsync(); OpenFileButton.Click += async (_, _) => await OpenFileAsync();
LaunchButton.Click += (_, _) => LaunchSelected(); LaunchButton.Click += (_, _) => LaunchSelected();
StopButton.Click += (_, _) => _emulator?.Stop(); StopButton.Click += (_, _) => _emulator?.Stop();
ClearLogButton.Click += (_, _) => _consoleLines.Clear(); ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync(); CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
CtxLaunch.Click += (_, _) => LaunchSelected();
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
CtxCopyPath.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Path");
CtxCopyTitleId.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Title ID");
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync(); Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing(); Closing += (_, _) => OnWindowClosing();
DualSenseReader.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
};
_gamepadTimer.Tick += (_, _) => PollGamepad();
_gamepadTimer.Start();
}
// ---- Controller navigation ----
private void PollGamepad()
{
if (!DualSenseReader.TryGetState(out var pad))
{
_previousPadButtons = 0;
return;
}
if (!IsActive)
{
// Ignore input while the launcher is in the background (e.g. the
// game window is focused and using the same controller).
_previousPadButtons = pad.Buttons;
return;
}
var now = Environment.TickCount64;
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
MoveSelection(-1);
}
if (ShouldNavigate(right, ref _navRightNextAt, now))
{
MoveSelection(1);
}
if (ShouldNavigate(up, ref _navUpNextAt, now))
{
MoveSelection(-TilesPerRow());
}
if (ShouldNavigate(down, ref _navDownNextAt, now))
{
MoveSelection(TilesPerRow());
}
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & 0x4000) != 0) // Cross
{
LaunchSelected();
}
if ((pressed & 0x2000) != 0) // Circle
{
_emulator?.Stop();
}
_previousPadButtons = pad.Buttons;
}
/// <summary>
/// Edge-triggered with hold-to-repeat: fires on press, then repeats
/// after 400ms at 130ms intervals while held.
/// </summary>
private static bool ShouldNavigate(bool held, ref long nextAt, long now)
{
if (!held)
{
nextAt = 0;
return false;
}
if (nextAt == 0)
{
nextAt = now + 400;
return true;
}
if (now >= nextAt)
{
nextAt = now + 130;
return true;
}
return false;
}
private void MoveSelection(int delta)
{
if (_visibleGames.Count == 0)
{
return;
}
var index = GameList.SelectedIndex < 0
? 0
: Math.Clamp(GameList.SelectedIndex + delta, 0, _visibleGames.Count - 1);
GameList.SelectedIndex = index;
GameList.ScrollIntoView(index);
}
private int TilesPerRow()
{
// Tile footprint: 128 content + 20 item padding + 10 item margin.
const double TileOuterWidth = 158;
var width = GameList.Bounds.Width;
return width > TileOuterWidth ? (int)(width / TileOuterWidth) : 1;
} }
private async Task OnOpenedAsync() private async Task OnOpenedAsync()
@@ -89,6 +232,7 @@ public partial class MainWindow : Window
ReadControlsIntoSettings(); ReadControlsIntoSettings();
_settings.Save(); _settings.Save();
_consoleFlushTimer.Stop(); _consoleFlushTimer.Stop();
_gamepadTimer.Stop();
_emulator?.Dispose(); _emulator?.Dispose();
} }
@@ -188,9 +332,21 @@ public partial class MainWindow : Window
return; return;
} }
var changed = false;
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase)) if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
{ {
_settings.GameFolders.Add(path); _settings.GameFolders.Add(path);
changed = true;
}
// Adding (or re-adding) a folder is an explicit signal to restore any
// games beneath it that were removed from the library earlier.
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
if (changed)
{
_settings.Save(); _settings.Save();
} }
@@ -200,19 +356,118 @@ public partial class MainWindow : Window
private async Task RescanLibraryAsync() private async Task RescanLibraryAsync()
{ {
var folders = _settings.GameFolders.ToArray(); var folders = _settings.GameFolders.ToArray();
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
StatusBarRight.Text = "Scanning library…"; StatusBarRight.Text = "Scanning library…";
var games = await Task.Run(() => ScanFolders(folders)); var games = await Task.Run(() => ScanFolders(folders, excluded));
_allGames.Clear(); _allGames.Clear();
_allGames.AddRange(games); _allGames.AddRange(games);
RefreshVisibleGames(); RefreshVisibleGames();
LoadGameDetailsInBackground(games);
StatusBarRight.Text = folders.Length == 0 StatusBarRight.Text = folders.Length == 0
? "Add a game folder to populate the library." ? "Add a game folder to populate the library."
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s)."; : $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
} }
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders) /// <summary>
/// Enriches games off the UI thread — decodes cover art and totals each
/// game's install folder size — posting results back as they become
/// ready. A newer scan invalidates older loads.
/// </summary>
private void LoadGameDetailsInBackground(IReadOnlyList<GameEntry> games)
{
var generation = ++_detailLoadGeneration;
_ = Task.Run(() =>
{
// Covers first: they are cheap and the most visible, so the grid
// fills with art before the (potentially slow) size pass runs.
foreach (var game in games)
{
if (generation != _detailLoadGeneration)
{
return;
}
if (game.CoverPath is null)
{
continue;
}
try
{
using var stream = File.OpenRead(game.CoverPath);
var bitmap = Bitmap.DecodeToWidth(stream, 312);
Dispatcher.UIThread.Post(() =>
{
if (generation == _detailLoadGeneration)
{
game.Cover = bitmap;
}
});
}
catch (Exception)
{
// A missing or undecodable image keeps the placeholder.
}
}
foreach (var game in games)
{
if (generation != _detailLoadGeneration)
{
return;
}
var size = ComputeInstallSize(game.Path);
if (size > 0)
{
Dispatcher.UIThread.Post(() =>
{
if (generation == _detailLoadGeneration)
{
game.SizeBytes = size;
}
});
}
}
});
}
/// <summary>
/// Totals the size of the game's install folder (the directory holding
/// the eboot), which is far more accurate than the eboot alone.
/// </summary>
private static long ComputeInstallSize(string ebootPath)
{
var directory = Path.GetDirectoryName(ebootPath);
if (directory is null)
{
return 0;
}
long total = 0;
try
{
var enumeration = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
};
foreach (var file in new DirectoryInfo(directory).EnumerateFiles("*", enumeration))
{
total += file.Length;
}
}
catch (Exception)
{
// Fall back to whatever was accumulated so far.
}
return total;
}
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
{ {
var games = new List<GameEntry>(); var games = new List<GameEntry>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -235,7 +490,7 @@ public partial class MainWindow : Window
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration)) foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
{ {
var fullPath = Path.GetFullPath(file); var fullPath = Path.GetFullPath(file);
if (!seen.Add(fullPath)) if (!seen.Add(fullPath) || excludedPaths.Contains(fullPath))
{ {
continue; continue;
} }
@@ -250,7 +505,9 @@ public partial class MainWindow : Window
} }
var (title, titleId) = TryReadParamJson(fullPath); var (title, titleId) = TryReadParamJson(fullPath);
games.Add(new GameEntry(title ?? GameNameFor(fullPath), titleId, fullPath, size)); games.Add(new GameEntry(
title ?? GameNameFor(fullPath), titleId, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
} }
} }
catch (Exception) catch (Exception)
@@ -332,6 +589,56 @@ public partial class MainWindow : Window
} }
} }
/// <summary>
/// Finds the cover art shipped with the game: sce_sys/icon0.png next to
/// the executable (falling back to pic0.png).
/// </summary>
private static string? FindCoverFor(string ebootPath)
{
var directory = Path.GetDirectoryName(ebootPath);
if (directory is null)
{
return null;
}
var sceSys = Path.Combine(directory, "sce_sys");
foreach (var candidate in new[] { "icon0.png", "pic0.png" })
{
var coverPath = Path.Combine(sceSys, candidate);
if (File.Exists(coverPath))
{
return coverPath;
}
}
return null;
}
/// <summary>
/// Finds the key art shipped with the game (sce_sys/pic0.png, falling
/// back to pic1.png), used as the window backdrop when selected.
/// </summary>
private static string? FindBackgroundFor(string ebootPath)
{
var directory = Path.GetDirectoryName(ebootPath);
if (directory is null)
{
return null;
}
var sceSys = Path.Combine(directory, "sce_sys");
foreach (var candidate in new[] { "pic0.png", "pic1.png" })
{
var backgroundPath = Path.Combine(sceSys, candidate);
if (File.Exists(backgroundPath))
{
return backgroundPath;
}
}
return null;
}
private static string GameNameFor(string ebootPath) private static string GameNameFor(string ebootPath)
{ {
var directory = Path.GetDirectoryName(ebootPath); var directory = Path.GetDirectoryName(ebootPath);
@@ -339,26 +646,125 @@ public partial class MainWindow : Window
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name; return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
} }
// ---- Game context menu ----
/// <summary>
/// Selects the tile under the pointer before its context menu opens, and
/// suppresses the menu on empty grid space.
/// </summary>
private void OnGameContextRequested(object? sender, ContextRequestedEventArgs e)
{
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>(includeSelf: true);
if (item?.DataContext is not GameEntry game)
{
e.Handled = true;
return;
}
GameList.SelectedItem = game;
CtxLaunch.IsEnabled = !_isRunning;
CtxCopyTitleId.IsEnabled = game.TitleId is not null;
}
private void OpenSelectedGameFolder()
{
if (GameList.SelectedItem is not GameEntry game)
{
return;
}
try
{
if (OperatingSystem.IsWindows())
{
Process.Start(new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{game.Path}\"",
UseShellExecute = false,
});
}
else if (Path.GetDirectoryName(game.Path) is { } directory)
{
Process.Start(new ProcessStartInfo
{
FileName = OperatingSystem.IsMacOS() ? "open" : "xdg-open",
Arguments = $"\"{directory}\"",
UseShellExecute = false,
});
}
}
catch (Exception ex)
{
StatusBarRight.Text = $"Could not open folder: {ex.Message}";
}
}
private async Task CopyToClipboardAsync(string? text, string what)
{
if (string.IsNullOrEmpty(text) || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(text);
StatusBarRight.Text = $"{what} copied to clipboard.";
}
private void RemoveSelectedFromLibrary()
{
if (GameList.SelectedItem is not GameEntry game)
{
return;
}
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
{
_settings.ExcludedGames.Add(game.Path);
_settings.Save();
}
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
GameList.SelectedItem = null;
RefreshVisibleGames();
StatusBarRight.Text = $"Removed “{game.Name}” from the library. Re-add its folder to restore it.";
}
private void RefreshVisibleGames() private void RefreshVisibleGames()
{ {
var query = SearchBox.Text?.Trim() ?? string.Empty; var query = SearchBox.Text?.Trim() ?? string.Empty;
var selected = GameList.SelectedItem as GameEntry; var selectedPath = (GameList.SelectedItem as GameEntry)?.Path;
_visibleGames.Clear(); _visibleGames.Clear();
foreach (var game in _allGames) foreach (var game in _allGames)
{ {
if (query.Length == 0 || if (query.Length == 0 ||
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) || game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase)) game.Path.Contains(query, StringComparison.OrdinalIgnoreCase) ||
(game.TitleId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false))
{ {
_visibleGames.Add(game); _visibleGames.Add(game);
} }
} }
GameCountText.Text = _visibleGames.Count.ToString(); GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
if (selected is not null && _visibleGames.Contains(selected))
if (selectedPath is not null &&
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
is { } reselected)
{ {
GameList.SelectedItem = selected; GameList.SelectedItem = reselected;
}
EmptyState.IsVisible = _visibleGames.Count == 0;
if (_visibleGames.Count == 0)
{
var hasFilter = query.Length > 0;
EmptyStateTitle.Text = hasFilter ? "No games match your search" : "Your library is empty";
EmptyStateHint.Text = hasFilter
? $"Nothing in the library matches “{query}”."
: "Add a folder containing your games to get started.";
EmptyAddFolderButton.IsVisible = !hasFilter;
} }
UpdateSelectedGame(); UpdateSelectedGame();
@@ -370,16 +776,59 @@ public partial class MainWindow : Window
{ {
SelectedGameTitle.Text = game.Name; SelectedGameTitle.Text = game.Name;
SelectedGamePath.Text = game.Path; SelectedGamePath.Text = game.Path;
SelectedCoverPanel.DataContext = game;
_ = UpdateBackdropAsync(game);
} }
else else
{ {
SelectedGameTitle.Text = "No game selected"; SelectedGameTitle.Text = "No game selected";
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly."; SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
SelectedCoverPanel.DataContext = null;
_ = UpdateBackdropAsync(null);
} }
UpdateRunButtons(); UpdateRunButtons();
} }
/// <summary>
/// Fades the window backdrop to the selected game's key art. The image
/// decodes off the UI thread and is cached on the entry; a newer
/// selection cancels the fade-in of an older one.
/// </summary>
private async Task UpdateBackdropAsync(GameEntry? game)
{
var generation = ++_backdropGeneration;
BackdropImage.Opacity = 0;
if (game?.BackgroundPath is null)
{
return;
}
if (game.Background is null)
{
try
{
var path = game.BackgroundPath;
game.Background = await Task.Run(() =>
{
using var stream = File.OpenRead(path);
return Bitmap.DecodeToWidth(stream, 1600);
});
}
catch (Exception)
{
return; // undecodable key art: keep the plain background
}
}
if (generation == _backdropGeneration)
{
BackdropImage.Source = game.Background;
BackdropImage.Opacity = 1.0;
}
}
// ---- Launching ---- // ---- Launching ----
private async Task OpenFileAsync() private async Task OpenFileAsync()
@@ -448,6 +897,7 @@ public partial class MainWindow : Window
arguments.Add(ebootPath); arguments.Add(ebootPath);
_consoleLines.Clear(); _consoleLines.Clear();
ConsoleToggle.IsChecked = true;
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush); AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
var emulator = new EmulatorProcess(); var emulator = new EmulatorProcess();
+8 -1
View File
@@ -21,8 +21,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" /> <AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</ItemGroup> </ItemGroup>
<!-- The DualSense HID reader is shared with the emulator's pad HLE. It is
dependency-free, so it is compiled in directly rather than pulling a
reference to all of SharpEmu.Libs into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
</ItemGroup>
</Project> </Project>
+71
View File
@@ -138,6 +138,51 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
return true; return true;
} }
public bool TryReadUInt16(ulong address, out ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
if (!Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
return true;
}
public bool TryWriteUInt16(ulong address, ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryReadInt32(ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
{
if (checkNil && address == 0)
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return Memory.TryWrite(address, bytes);
}
public bool TryReadUInt32(ulong address, out uint value) public bool TryReadUInt32(ulong address, out uint value)
{ {
Span<byte> buffer = stackalloc byte[sizeof(uint)]; Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -158,6 +203,13 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
return Memory.TryWrite(address, buffer); return Memory.TryWrite(address, buffer);
} }
public bool TryWriteInt64(ulong address, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryReadUInt64(ulong address, out ulong value) public bool TryReadUInt64(ulong address, out ulong value)
{ {
Span<byte> buffer = stackalloc byte[sizeof(ulong)]; Span<byte> buffer = stackalloc byte[sizeof(ulong)];
@@ -224,4 +276,23 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
this[CpuRegister.Rsp] = rsp + sizeof(ulong); this[CpuRegister.Rsp] = rsp + sizeof(ulong);
return true; return true;
} }
public int SetReturn(int result, Type? cast = null)
{
var value = cast switch
{
null => (ulong)result,
_ when cast == typeof(long) => (ulong)(long)result,
_ => throw new NotSupportedException(),
};
this[CpuRegister.Rax] = unchecked(value);
return result;
}
public int SetReturn(OrbisGen2Result result)
{
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
} }
+6 -2
View File
@@ -47,6 +47,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances); var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler)) if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{ {
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue; continue;
} }
@@ -105,6 +106,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export)) 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); context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false; return false;
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0) if ((export.Target & context.TargetGeneration) == 0)
{ {
context[CpuRegister.Rax] = unchecked((ulong)(int)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}).");
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false; return false;
} }
context.ClearRaxWriteFlag(); context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context); int ret = ((SysAbiFunction)function).Invoke(context);
+108 -75
View File
@@ -58,6 +58,8 @@ public static class AgcExports
private const uint SpiShaderPgmHiEs = 0xC9; private const uint SpiShaderPgmHiEs = 0xC9;
private const uint SpiShaderPgmLoLs = 0x148; private const uint SpiShaderPgmLoLs = 0x148;
private const uint SpiShaderPgmHiLs = 0x149; private const uint SpiShaderPgmHiLs = 0x149;
private const uint SpiShaderPgmLoGs = 0x8A;
private const uint SpiShaderPgmHiGs = 0x8B;
private const uint SpiPsInputEna = 0x1B3; private const uint SpiPsInputEna = 0x1B3;
private const uint SpiPsInputAddr = 0x1B4; private const uint SpiPsInputAddr = 0x1B4;
private const uint ComputePgmLo = 0x20C; private const uint ComputePgmLo = 0x20C;
@@ -115,6 +117,7 @@ public static class AgcExports
private const uint RegisterDefaultsVersion7 = 7; private const uint RegisterDefaultsVersion7 = 7;
private const uint RegisterDefaultsVersion8 = 8; private const uint RegisterDefaultsVersion8 = 8;
private const uint RegisterDefaultsVersion10 = 10; private const uint RegisterDefaultsVersion10 = 10;
private const uint RegisterDefaultsVersion13 = 13;
private const int RegisterDefaultsSize = 0x40; private const int RegisterDefaultsSize = 0x40;
private const int RegisterDefaultBlockSize = 16 * 8; private const int RegisterDefaultBlockSize = 16 * 8;
@@ -139,6 +142,7 @@ public static class AgcExports
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28; private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
private const uint CbSetShRegisterRangeMarker = 0x6875000D; private const uint CbSetShRegisterRangeMarker = 0x6875000D;
private static readonly object _submitTraceGate = new(); private static readonly object _submitTraceGate = new();
private static readonly object _textureHashTraceGate = new();
private static readonly HashSet<uint> _tracedDcbSizes = new(); private static readonly HashSet<uint> _tracedDcbSizes = new();
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new(); private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new(); private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
@@ -146,6 +150,7 @@ public static class AgcExports
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new(); private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
private static readonly HashSet<ulong> _tracedComputeShaders = new(); private static readonly HashSet<ulong> _tracedComputeShaders = new();
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new(); private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new(); private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
private static readonly Dictionary< private static readonly Dictionary<
@@ -165,6 +170,10 @@ public static class AgcExports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
"1", "1",
StringComparison.Ordinal); StringComparison.Ordinal);
private static readonly bool _traceTextureHashes = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_HASHES"),
"1",
StringComparison.Ordinal);
private static long _dcbWriteDataTraceCount; private static long _dcbWriteDataTraceCount;
private static long _dcbWaitRegMemTraceCount; private static long _dcbWaitRegMemTraceCount;
private static long _createShaderTraceCount; private static long _createShaderTraceCount;
@@ -404,11 +413,11 @@ public static class AgcExports
var version = (uint)ctx[CpuRegister.Rsi]; var version = (uint)ctx[CpuRegister.Rsi];
if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version)) if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}"); TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -439,19 +448,19 @@ public static class AgcExports
var codeAddress = ctx[CpuRegister.Rdx]; var codeAddress = ctx[CpuRegister.Rdx];
if (headerAddress == 0 || codeAddress == 0) if (headerAddress == 0 || codeAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadUInt32(headerAddress, out var fileHeader) || if (!ctx.TryReadUInt32(headerAddress, out var fileHeader) ||
!ctx.TryReadUInt32(headerAddress + sizeof(uint), out var version)) !ctx.TryReadUInt32(headerAddress + sizeof(uint), out var version))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (fileHeader != ShaderFileHeader || version != ShaderVersion) if (fileHeader != ShaderFileHeader || version != ShaderVersion)
{ {
TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}"); TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) || if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) ||
@@ -462,12 +471,12 @@ public static class AgcExports
!RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) || !RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) ||
!ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress)) !ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (!ctx.TryReadUInt64(headerAddress + ShaderUserDataOffset, out var userDataAddress)) if (!ctx.TryReadUInt64(headerAddress + ShaderUserDataOffset, out var userDataAddress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (userDataAddress != 0 && if (userDataAddress != 0 &&
@@ -477,18 +486,18 @@ public static class AgcExports
!RelocatePointerField(ctx, userDataAddress + 0x18) || !RelocatePointerField(ctx, userDataAddress + 0x18) ||
!RelocatePointerField(ctx, userDataAddress + 0x20))) !RelocatePointerField(ctx, userDataAddress + 0x20)))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress)) if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (destinationAddress != 0 && if (destinationAddress != 0 &&
!ctx.TryWriteUInt64(destinationAddress, headerAddress)) !ctx.TryWriteUInt64(destinationAddress, headerAddress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
lock (_submitTraceGate) lock (_submitTraceGate)
@@ -564,14 +573,14 @@ public static class AgcExports
if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0) if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadByte(geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) || if (!ctx.TryReadByte(geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
!ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) || !ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
specialsAddress == 0) specialsAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) || if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) ||
@@ -581,7 +590,7 @@ public static class AgcExports
!ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) || !ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) ||
!ctx.TryWriteUInt32(ucRegistersAddress + 20, primitiveType)) !ctx.TryWriteUInt32(ucRegistersAddress + 20, primitiveType))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}"); TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}");
@@ -602,13 +611,13 @@ public static class AgcExports
if (registersAddress == 0 || geometryShaderAddress == 0) if (registersAddress == 0 || geometryShaderAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadUInt64(geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) || if (!ctx.TryReadUInt64(geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) ||
!ctx.TryReadUInt32(geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount)) !ctx.TryReadUInt32(geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
ulong inputSemanticsAddress = 0; ulong inputSemanticsAddress = 0;
@@ -616,7 +625,7 @@ public static class AgcExports
(!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) || (!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) ||
!ctx.TryReadUInt32(pixelShaderAddress + ShaderNumInputSemanticsOffset, out _))) !ctx.TryReadUInt32(pixelShaderAddress + ShaderNumInputSemanticsOffset, out _)))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
for (uint i = 0; i < 32; i++) for (uint i = 0; i < 32; i++)
@@ -638,7 +647,7 @@ public static class AgcExports
if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) || if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) ||
!ctx.TryWriteUInt32(destination + sizeof(uint), value)) !ctx.TryWriteUInt32(destination + sizeof(uint), value))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
@@ -659,7 +668,7 @@ public static class AgcExports
var type = (int)ctx[CpuRegister.Rdx]; var type = (int)ctx[CpuRegister.Rdx];
if (outputAddress == 0 || commandAddress == 0) if (outputAddress == 0 || commandAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var payloadAddress = commandAddress + 8; var payloadAddress = commandAddress + 8;
@@ -667,7 +676,7 @@ public static class AgcExports
{ {
if (!ctx.TryReadUInt32(commandAddress, out var header)) if (!ctx.TryReadUInt32(commandAddress, out var header))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u
@@ -677,7 +686,7 @@ public static class AgcExports
if (!ctx.TryWriteUInt64(outputAddress, payloadAddress)) if (!ctx.TryWriteUInt64(outputAddress, payloadAddress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (ShouldTraceHotPath(ref _packetPayloadTraceCount)) if (ShouldTraceHotPath(ref _packetPayloadTraceCount))
@@ -702,20 +711,20 @@ public static class AgcExports
var dwordCount = (uint)ctx[CpuRegister.Rsi]; var dwordCount = (uint)ctx[CpuRegister.Rsi];
if (commandBufferAddress == 0 || dwordCount < 2 || dwordCount > 0x4001) if (commandBufferAddress == 0 || dwordCount < 2 || dwordCount > 0x4001)
{ {
return ReturnPointer(ctx, 0); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) || if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) ||
!ctx.TryWriteUInt32(commandAddress, Pm4(dwordCount, ItNop, RZero))) !ctx.TryWriteUInt32(commandAddress, Pm4(dwordCount, ItNop, RZero)))
{ {
return ReturnPointer(ctx, 0); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
for (uint index = 1; index < dwordCount; index++) for (uint index = 1; index < dwordCount; index++)
{ {
if (!ctx.TryWriteUInt32(commandAddress + ((ulong)index * sizeof(uint)), 0)) if (!ctx.TryWriteUInt32(commandAddress + ((ulong)index * sizeof(uint)), 0))
{ {
return ReturnPointer(ctx, 0); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
@@ -1735,12 +1744,12 @@ public static class AgcExports
op != ItNop || op != ItNop ||
register != RDmaData) register != RDmaData)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
return ctx.TryWriteUInt64(commandAddress + 16, destinationAddress) return ctx.TryWriteUInt64(commandAddress + 16, destinationAddress)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -1754,7 +1763,7 @@ public static class AgcExports
var address = ctx[CpuRegister.Rsi]; var address = ctx[CpuRegister.Rsi];
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register)) if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var fieldOffset = op == ItWaitRegMem var fieldOffset = op == ItWaitRegMem
@@ -1764,12 +1773,12 @@ public static class AgcExports
: 0; : 0;
if (fieldOffset == 0) if (fieldOffset == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address) return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -1785,12 +1794,12 @@ public static class AgcExports
op != ItNop || op != ItNop ||
register != RReleaseMem) register != RReleaseMem)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
return ctx.TryWriteUInt64(commandAddress + 12, address) return ctx.TryWriteUInt64(commandAddress + 12, address)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -1931,11 +1940,11 @@ public static class AgcExports
KernelEventQueueCompatExports.KernelEventFilterGraphics, KernelEventQueueCompatExports.KernelEventFilterGraphics,
userData)) userData))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}"); TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -1952,11 +1961,11 @@ public static class AgcExports
eventId, eventId,
KernelEventQueueCompatExports.KernelEventFilterGraphics)) KernelEventQueueCompatExports.KernelEventFilterGraphics))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}"); TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -1971,7 +1980,7 @@ public static class AgcExports
!ctx.TryReadUInt64(packetAddress, out var commandAddress) || !ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount)) !ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var tracePackets = false; var tracePackets = false;
@@ -2011,7 +2020,7 @@ public static class AgcExports
!ctx.TryReadUInt64(packetAddress, out var commandAddress) || !ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount)) !ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var tracePackets = false; var tracePackets = false;
@@ -2057,16 +2066,16 @@ public static class AgcExports
if (outAddress == 0) if (outAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryWriteUInt32(outAddress, 256)) if (!ctx.TryWriteUInt32(outAddress, 256))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256"); TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
private const uint DefaultAgcOwner = 1; private const uint DefaultAgcOwner = 1;
@@ -2081,16 +2090,16 @@ public static class AgcExports
if (ownerAddress == 0) if (ownerAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner)) if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}"); TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -2110,7 +2119,7 @@ public static class AgcExports
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " + $"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
$"name=0x{nameAddress:X16} type={type} flags={flags}"); $"name=0x{nameAddress:X16} type={type} flags={flags}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -2125,7 +2134,7 @@ public static class AgcExports
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " + $"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}"); $"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -3744,7 +3753,6 @@ public static class AgcExports
var sourceWidth = descriptor.TileMode == 0 var sourceWidth = descriptor.TileMode == 0
? GetLinearTexturePitch( ? GetLinearTexturePitch(
Math.Max(descriptor.Width, descriptor.Pitch), Math.Max(descriptor.Width, descriptor.Pitch),
descriptor.Height,
descriptor.Format) descriptor.Format)
: descriptor.Width; : descriptor.Width;
var sourceByteCount = GetTextureByteCount( var sourceByteCount = GetTextureByteCount(
@@ -3761,7 +3769,7 @@ public static class AgcExports
if (!isStorage && if (!isStorage &&
descriptor.Address != 0 && descriptor.Address != 0 &&
VulkanVideoPresenter.IsGuestImageAvailable( VulkanVideoPresenter.IsGpuGuestImageAvailable(
descriptor.Address, descriptor.Address,
descriptor.Format, descriptor.Format,
descriptor.NumberType)) descriptor.NumberType))
@@ -3822,6 +3830,8 @@ public static class AgcExports
return true; return true;
} }
TraceTextureHash(descriptor, source);
var nonZero = 0; var nonZero = 0;
for (var i = 0; i < source.Length; i++) for (var i = 0; i < source.Length; i++)
{ {
@@ -3881,6 +3891,34 @@ public static class AgcExports
MipLevel: 0); MipLevel: 0);
} }
private static void TraceTextureHash(TextureDescriptor descriptor, ReadOnlySpan<byte> source)
{
if (!_traceTextureHashes ||
descriptor.Address == 0 ||
descriptor.Width > 256 ||
descriptor.Height > 256)
{
return;
}
var hash = ComputeFingerprint(source);
var key = (descriptor.Address, descriptor.Width, descriptor.Height);
lock (_textureHashTraceGate)
{
if (_tracedTextureHashes.TryGetValue(key, out var previousHash) &&
previousHash == hash)
{
return;
}
_tracedTextureHashes[key] = hash;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.texture_hash addr=0x{descriptor.Address:X16} " +
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
}
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) => private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
descriptor.Count >= 4 descriptor.Count >= 4
? new VulkanGuestSampler( ? new VulkanGuestSampler(
@@ -4524,23 +4562,17 @@ public static class AgcExports
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
} }
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format) private static uint GetLinearTexturePitch(uint pitch, uint format)
{ {
var bytesPerTexel = GetTextureBytesPerTexel(format); var bytesPerTexel = GetTextureBytesPerTexel(format);
if (bytesPerTexel == 0 || height == 0) if (bytesPerTexel == 0)
{ {
return pitch; return pitch;
} }
var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel); // GFX10 ADDR_SW_LINEAR aligns each row to 256 bytes.
var alignedPitch = AlignUp(pitch, pitchAlignment); var pitchAlignment = Math.Max(1UL, 256UL / bytesPerTexel);
var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel); return checked((uint)AlignUp(pitch, pitchAlignment));
while ((alignedPitch * height) % sliceAlignment != 0)
{
alignedPitch += pitchAlignment;
}
return checked((uint)alignedPitch);
} }
private static ulong AlignUp(ulong value, ulong alignment) => private static ulong AlignUp(ulong value, ulong alignment) =>
@@ -4830,9 +4862,13 @@ public static class AgcExports
var type = (fields[3] >> 28) & 0xFu; var type = (fields[3] >> 28) & 0xFu;
var baseLevel = (fields[3] >> 12) & 0xFu; var baseLevel = (fields[3] >> 12) & 0xFu;
var lastLevel = (fields[3] >> 16) & 0xFu; var lastLevel = (fields[3] >> 16) & 0xFu;
var pitch = fields.Count >= 5 // The 128-bit RDNA2 2D resource derives pitch[12:0] from width;
? ((fields[4] >> 13) & 0x3FFFu) + 1 // the optional extension word only supplies pitch[13].
: width; var pitch = width;
if (fields.Count >= 5)
{
pitch = ((((width - 1) & 0x1FFFu) | (((fields[4] >> 13) & 1u) << 13)) + 1);
}
var dstSelect = fields[3] & 0xFFFu; var dstSelect = fields[3] & 0xFFFu;
if (address == 0 || width == 0 || height == 0) if (address == 0 || width == 0 || height == 0)
{ {
@@ -5081,6 +5117,7 @@ public static class AgcExports
0 => ComputePgmLo, 0 => ComputePgmLo,
1 => SpiShaderPgmLoPs, 1 => SpiShaderPgmLoPs,
2 or 6 => SpiShaderPgmLoEs, 2 or 6 => SpiShaderPgmLoEs,
4 => SpiShaderPgmLoGs,
7 => SpiShaderPgmLoLs, 7 => SpiShaderPgmLoLs,
_ => 0u, _ => 0u,
}; };
@@ -5089,6 +5126,7 @@ public static class AgcExports
0 => ComputePgmHi, 0 => ComputePgmHi,
1 => SpiShaderPgmHiPs, 1 => SpiShaderPgmHiPs,
2 or 6 => SpiShaderPgmHiEs, 2 or 6 => SpiShaderPgmHiEs,
4 => SpiShaderPgmHiGs,
7 => SpiShaderPgmHiLs, 7 => SpiShaderPgmHiLs,
_ => 0u, _ => 0u,
}; };
@@ -5105,7 +5143,7 @@ public static class AgcExports
} }
private static bool IsEsGeometryShaderType(byte shaderType) => private static bool IsEsGeometryShaderType(byte shaderType) =>
shaderType is 2 or 6; shaderType is 2 or 4 or 6;
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace) private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
{ {
@@ -5113,13 +5151,13 @@ public static class AgcExports
var registersAddress = ctx[CpuRegister.Rsi]; var registersAddress = ctx[CpuRegister.Rsi];
if (commandAddress == 0 || registersAddress == 0) if (commandAddress == 0 || registersAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) || if (!ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) ||
!ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32))) !ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32)))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}"); TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}");
@@ -5133,13 +5171,13 @@ public static class AgcExports
var registerCount = (uint)ctx[CpuRegister.Rsi]; var registerCount = (uint)ctx[CpuRegister.Rsi];
if (commandAddress == 0) if (commandAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadUInt32(commandAddress + 4, out var currentCount) || if (!ctx.TryReadUInt32(commandAddress + 4, out var currentCount) ||
!ctx.TryWriteUInt32(commandAddress + 4, currentCount + registerCount)) !ctx.TryWriteUInt32(commandAddress + 4, currentCount + registerCount))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}"); TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}");
@@ -5252,7 +5290,8 @@ public static class AgcExports
return version is return version is
RegisterDefaultsVersion7 or RegisterDefaultsVersion7 or
RegisterDefaultsVersion8 or RegisterDefaultsVersion8 or
RegisterDefaultsVersion10; RegisterDefaultsVersion10 or
RegisterDefaultsVersion13;
} }
private static bool TryGetRegisterDefaultsAllocation( private static bool TryGetRegisterDefaultsAllocation(
@@ -5382,12 +5421,6 @@ public static class AgcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static uint Pm4(uint lengthDwords, uint op, uint register) => private static uint Pm4(uint lengthDwords, uint op, uint register) =>
0xC0000000u | 0xC0000000u |
((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) | ((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) |
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Agc; namespace SharpEmu.Libs.Agc;
@@ -36,9 +35,9 @@ internal static class Gen5ShaderMetadataReader
} }
} }
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) || if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) || !ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) || !ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
directResourceCount > MaxMetadataEntries) directResourceCount > MaxMetadataEntries)
{ {
return false; return false;
@@ -47,8 +46,7 @@ internal static class Gen5ShaderMetadataReader
var resourceCounts = new ushort[ResourceClassCount]; var resourceCounts = new ushort[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++) for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{ {
if (!TryReadUInt16( if (!ctx.TryReadUInt16(
ctx,
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)), userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
out resourceCounts[resourceClass]) || out resourceCounts[resourceClass]) ||
resourceCounts[resourceClass] > MaxMetadataEntries) resourceCounts[resourceClass] > MaxMetadataEntries)
@@ -67,7 +65,7 @@ internal static class Gen5ShaderMetadataReader
for (uint type = 0; type < directResourceCount; type++) for (uint type = 0; type < directResourceCount; type++)
{ {
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset)) if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
{ {
return false; return false;
} }
@@ -95,8 +93,7 @@ internal static class Gen5ShaderMetadataReader
for (uint slot = 0; slot < count; slot++) for (uint slot = 0; slot < count; slot++)
{ {
if (!TryReadUInt16( if (!ctx.TryReadUInt16(
ctx,
resourceOffsets[resourceClass] + slot * sizeof(ushort), resourceOffsets[resourceClass] + slot * sizeof(ushort),
out var sharp)) out var sharp))
{ {
@@ -124,17 +121,4 @@ internal static class Gen5ShaderMetadataReader
resources); resources);
return true; return true;
} }
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
return true;
}
} }
@@ -73,7 +73,7 @@ public static class AppContentExports
var valueAddress = ctx[CpuRegister.Rsi]; var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0) if (valueAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
int value; int value;
@@ -83,18 +83,18 @@ public static class AppContentExports
} }
else if (!TryReadUserDefinedParam(paramId, out value)) else if (!TryReadUserDefinedParam(paramId, out value))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> valueBytes = stackalloc byte[sizeof(int)]; Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value); BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
if (!ctx.Memory.TryWrite(valueAddress, valueBytes)) if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceAppContent($"app_param_get_int id={paramId} value={value}"); TraceAppContent($"app_param_get_int id={paramId} value={value}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -168,12 +168,6 @@ public static class AppContentExports
} }
} }
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceAppContent(string message) private static void TraceAppContent(string message)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal)) if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
+24 -31
View File
@@ -3,7 +3,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Audio; namespace SharpEmu.Libs.Audio;
@@ -37,7 +36,7 @@ public static class AudioOut2Exports
var paramAddress = ctx[CpuRegister.Rdi]; var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0) if (paramAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize]; Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
@@ -48,8 +47,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400); BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param) return ctx.Memory.TryWrite(paramAddress, param)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -63,7 +62,7 @@ public static class AudioOut2Exports
var memoryInfoAddress = ctx[CpuRegister.Rsi]; var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0) if (paramAddress == 0 || memoryInfoAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> memoryInfo = stackalloc byte[0x20]; Span<byte> memoryInfo = stackalloc byte[0x20];
@@ -74,8 +73,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo) return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -91,13 +90,13 @@ public static class AudioOut2Exports
var outContextAddress = ctx[CpuRegister.Rcx]; var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0) if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle); var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle) return ctx.TryWriteUInt64(outContextAddress, handle)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -105,7 +104,7 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextDestroy", ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0); public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport( [SysAbiExport(
Nid = "JK2wamZPzwM", Nid = "JK2wamZPzwM",
@@ -120,14 +119,14 @@ public static class AudioOut2Exports
var contextAddress = ctx[CpuRegister.Rcx]; var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0) if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF; var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId; var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle) return ctx.TryWriteUInt64(outPortAddress, handle)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -141,7 +140,7 @@ public static class AudioOut2Exports
var stateAddress = ctx[CpuRegister.Rsi]; var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0) if (handle == 0 || stateAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var type = (int)((handle >> 16) & 0xFF); var type = (int)((handle >> 16) & 0xFF);
@@ -154,8 +153,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1); BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state) return ctx.Memory.TryWrite(stateAddress, state)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -168,7 +167,7 @@ public static class AudioOut2Exports
var infoAddress = ctx[CpuRegister.Rdi]; var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0) if (infoAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> info = stackalloc byte[0x40]; Span<byte> info = stackalloc byte[0x40];
@@ -178,8 +177,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000); BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info) return ctx.Memory.TryWrite(infoAddress, info)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -187,14 +186,14 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2PortDestroy", ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0); public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport( [SysAbiExport(
Nid = "IaZXJ9M79uo", Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy", ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5, Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")] LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0); public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport( [SysAbiExport(
Nid = "xywYcRB7nbQ", Nid = "xywYcRB7nbQ",
@@ -207,18 +206,12 @@ public static class AudioOut2Exports
var outUserAddress = ctx[CpuRegister.Rsi]; var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0) if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle); var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle) return ctx.TryWriteUInt64(outUserAddress, handle)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
} }
+10 -18
View File
@@ -5,7 +5,6 @@ using SharpEmu.HLE;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Threading;
namespace SharpEmu.Libs.Audio; namespace SharpEmu.Libs.Audio;
@@ -84,7 +83,7 @@ public static class AudioOutExports
ExportName = "sceAudioOutInit", ExportName = "sceAudioOutInit",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")] LibraryName = "libSceAudioOut")]
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0); public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport( [SysAbiExport(
Nid = "ekNvsT22rsY", Nid = "ekNvsT22rsY",
@@ -101,7 +100,7 @@ public static class AudioOutExports
if (bufferLength == 0 || frequency == 0 || if (bufferLength == 0 || frequency == 0 ||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat)) !TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
WinMmAudioPort? backend = null; WinMmAudioPort? backend = null;
@@ -133,7 +132,7 @@ public static class AudioOutExports
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " + $"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " + $"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
$"{bufferLength} frames, backend={backendName}"); $"{bufferLength} frames, backend={backendName}");
return SetReturn(ctx, handle); return ctx.SetReturn(handle);
} }
[SysAbiExport( [SysAbiExport(
@@ -146,11 +145,11 @@ public static class AudioOutExports
var handle = unchecked((int)ctx[CpuRegister.Rdi]); var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Ports.TryRemove(handle, out var port)) if (!Ports.TryRemove(handle, out var port))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
port.Dispose(); port.Dispose();
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -164,12 +163,12 @@ public static class AudioOutExports
var sourceAddress = ctx[CpuRegister.Rsi]; var sourceAddress = ctx[CpuRegister.Rsi];
if (!Ports.TryGetValue(handle, out var port)) if (!Ports.TryGetValue(handle, out var port))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (sourceAddress == 0) if (sourceAddress == 0)
{ {
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength); var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
@@ -178,7 +177,7 @@ public static class AudioOutExports
var source = buffer.AsSpan(0, port.BufferByteLength); var source = buffer.AsSpan(0, port.BufferByteLength);
if (!ctx.Memory.TryRead(sourceAddress, source)) if (!ctx.Memory.TryRead(sourceAddress, source))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (port.Backend is null || if (port.Backend is null ||
@@ -192,7 +191,7 @@ public static class AudioOutExports
port.PaceSilence(); port.PaceSilence();
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
finally finally
{ {
@@ -208,19 +207,12 @@ public static class AudioOutExports
public static int AudioOutSetVolume(CpuContext ctx) public static int AudioOutSetVolume(CpuContext ctx)
{ {
var handle = unchecked((int)ctx[CpuRegister.Rdi]); var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return SetReturn( return ctx.SetReturn(
ctx,
Ports.ContainsKey(handle) Ports.ContainsKey(handle)
? 0 ? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static bool TryGetFormat( private static bool TryGetFormat(
int rawFormat, int rawFormat,
out int channels, out int channels,
+2 -11
View File
@@ -46,8 +46,7 @@ public static class AvPlayerExports
var dataAddress = ctx[CpuRegister.Rsi]; var dataAddress = ctx[CpuRegister.Rsi];
lock (StateGate) lock (StateGate)
{ {
return SetReturn( return ctx.SetReturn(
ctx,
handle != 0 && dataAddress != 0 && Players.Contains(handle) handle != 0 && dataAddress != 0 && Players.Contains(handle)
? 0 ? 0
: InvalidParameters); : InvalidParameters);
@@ -63,15 +62,7 @@ public static class AvPlayerExports
{ {
lock (StateGate) lock (StateGate)
{ {
return SetReturn( return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
ctx,
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
} }
} }
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
} }
+62 -68
View File
@@ -104,17 +104,17 @@ public static class FiberExports
var optParam = ctx[CpuRegister.Rdi]; var optParam = ctx[CpuRegister.Rdi];
if (optParam == 0) if (optParam == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
if ((optParam & 7) != 0) if ((optParam & 7) != 0)
{ {
return SetReturn(ctx, FiberErrorAlignment); return ctx.SetReturn(FiberErrorAlignment);
} }
return ctx.TryWriteUInt32(optParam, FiberOptSignature) return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorInvalid); : ctx.SetReturn(FiberErrorInvalid);
} }
[SysAbiExport( [SysAbiExport(
@@ -127,24 +127,24 @@ public static class FiberExports
var fiber = ctx[CpuRegister.Rdi]; var fiber = ctx[CpuRegister.Rdi];
if (!TryValidateFiber(ctx, fiber, out var error)) if (!TryValidateFiber(ctx, fiber, out var error))
{ {
return SetReturn(ctx, error); return ctx.SetReturn(error);
} }
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state)) if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (state != FiberStateIdle) if (state != FiberStateIdle)
{ {
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
_continuations.TryRemove(fiber, out _); _continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _); _returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _); _stackRanges.TryRemove(fiber, out _);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated); _ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -229,20 +229,20 @@ public static class FiberExports
var fiberAddress = ResolveCurrentFiberAddress(ctx); var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0) if (fiberAddress == 0)
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } || if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)) !GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
var returnArgument = ctx[CpuRegister.Rdi]; var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi]; var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0)) if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
GuestCpuContinuation transferTarget; GuestCpuContinuation transferTarget;
@@ -256,7 +256,7 @@ public static class FiberExports
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget)) if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
{ {
_continuations.TryRemove(fiberAddress, out _); _continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
previousFiber = returnTarget.PreviousFiber; previousFiber = returnTarget.PreviousFiber;
@@ -267,7 +267,7 @@ public static class FiberExports
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun)) !ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
{ {
_continuations.TryRemove(fiberAddress, out _); _continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
transferTarget = previousContinuation.Context with { Rax = 0 }; transferTarget = previousContinuation.Context with { Rax = 0 };
@@ -278,7 +278,7 @@ public static class FiberExports
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument)) !TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
{ {
_continuations.TryRemove(fiberAddress, out _); _continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 }; transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
@@ -286,7 +286,7 @@ public static class FiberExports
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle)) if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
} }
@@ -296,7 +296,7 @@ public static class FiberExports
TraceFiber( TraceFiber(
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " + $"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}"); $"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -309,18 +309,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi]; var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0) if (outAddress == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
var fiberAddress = ResolveCurrentFiberAddress(ctx); var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0) if (fiberAddress == 0)
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
return ctx.TryWriteUInt64(outAddress, fiberAddress) return ctx.TryWriteUInt64(outAddress, fiberAddress)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorInvalid); : ctx.SetReturn(FiberErrorInvalid);
} }
[SysAbiExport( [SysAbiExport(
@@ -334,22 +334,22 @@ public static class FiberExports
var info = ctx[CpuRegister.Rsi]; var info = ctx[CpuRegister.Rsi];
if (info == 0) if (info == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
if (!TryValidateFiber(ctx, fiber, out var error)) if (!TryValidateFiber(ctx, fiber, out var error))
{ {
return SetReturn(ctx, error); return ctx.SetReturn(error);
} }
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize) if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (!TryReadFiberFields(ctx, fiber, out var fields)) if (!TryReadFiberFields(ctx, fiber, out var fields))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) || if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
@@ -359,10 +359,10 @@ public static class FiberExports
!TryWriteName(ctx, info + 40, fields.Name) || !TryWriteName(ctx, info + 40, fields.Name) ||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue)) !ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -376,22 +376,22 @@ public static class FiberExports
var nameAddress = ctx[CpuRegister.Rsi]; var nameAddress = ctx[CpuRegister.Rsi];
if (!TryValidateFiber(ctx, fiber, out var error)) if (!TryValidateFiber(ctx, fiber, out var error))
{ {
return SetReturn(ctx, error); return ctx.SetReturn(error);
} }
if (nameAddress == 0) if (nameAddress == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name)) if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
return TryWriteName(ctx, fiber + FiberNameOffset, name) return TryWriteName(ctx, fiber + FiberNameOffset, name)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorInvalid); : ctx.SetReturn(FiberErrorInvalid);
} }
[SysAbiExport( [SysAbiExport(
@@ -403,12 +403,12 @@ public static class FiberExports
{ {
if (ctx[CpuRegister.Rdi] != 0) if (ctx[CpuRegister.Rdi] != 0)
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0 return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorState); : ctx.SetReturn(FiberErrorState);
} }
[SysAbiExport( [SysAbiExport(
@@ -419,8 +419,8 @@ public static class FiberExports
public static int FiberStopContextSizeCheck(CpuContext ctx) public static int FiberStopContextSizeCheck(CpuContext ctx)
{ {
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1 return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorState); : ctx.SetReturn(FiberErrorState);
} }
[SysAbiExport( [SysAbiExport(
@@ -433,17 +433,17 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi]; var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0) if (outAddress == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
if (ResolveCurrentFiberAddress(ctx) == 0) if (ResolveCurrentFiberAddress(ctx) == 0)
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp]) return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, FiberErrorInvalid); : ctx.SetReturn(FiberErrorInvalid);
} }
private static int FiberInitializeCore( private static int FiberInitializeCore(
@@ -459,37 +459,37 @@ public static class FiberExports
{ {
if (fiber == 0 || nameAddress == 0 || entry == 0) if (fiber == 0 || nameAddress == 0 || entry == 0)
{ {
return SetReturn(ctx, FiberErrorNull); return ctx.SetReturn(FiberErrorNull);
} }
if ((fiber & 7) != 0 || if ((fiber & 7) != 0 ||
(contextAddress & 15) != 0 || (contextAddress & 15) != 0 ||
(optParam & 7) != 0) (optParam & 7) != 0)
{ {
return SetReturn(ctx, FiberErrorAlignment); return ctx.SetReturn(FiberErrorAlignment);
} }
if (contextSize != 0 && contextSize < FiberContextMinimumSize) if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{ {
return SetReturn(ctx, FiberErrorRange); return ctx.SetReturn(FiberErrorRange);
} }
if ((contextSize & 15) != 0 || if ((contextSize & 15) != 0 ||
(contextAddress == 0 && contextSize != 0) || (contextAddress == 0 && contextSize != 0) ||
(contextAddress != 0 && contextSize == 0)) (contextAddress != 0 && contextSize == 0))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (optParam != 0 && if (optParam != 0 &&
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature)) (!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name)) if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (Volatile.Read(ref _contextSizeCheck) != 0) if (Volatile.Read(ref _contextSizeCheck) != 0)
@@ -510,14 +510,14 @@ public static class FiberExports
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) || !ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1)) !ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (contextAddress != 0) if (contextAddress != 0)
{ {
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature)) if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if ((flags & FiberFlagContextSizeCheck) != 0) if ((flags & FiberFlagContextSizeCheck) != 0)
@@ -532,7 +532,7 @@ public static class FiberExports
} }
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'"); TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
private static int FiberRunCore( private static int FiberRunCore(
@@ -547,12 +547,12 @@ public static class FiberExports
{ {
if (!TryValidateFiber(ctx, fiber, out var error)) if (!TryValidateFiber(ctx, fiber, out var error))
{ {
return SetReturn(ctx, error); return ctx.SetReturn(error);
} }
if (!TryReadFiberFields(ctx, fiber, out var fields)) if (!TryReadFiberFields(ctx, fiber, out var fields))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (attachContextAddress != 0 || attachContextSize != 0) if (attachContextAddress != 0 || attachContextSize != 0)
@@ -560,7 +560,7 @@ public static class FiberExports
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields); var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
if (attachResult != 0) if (attachResult != 0)
{ {
return SetReturn(ctx, attachResult); return ctx.SetReturn(attachResult);
} }
} }
@@ -568,16 +568,16 @@ public static class FiberExports
if ((isSwitch && previousFiber == 0) || if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0)) (!isSwitch && previousFiber != 0))
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
if (previousFiber == fiber) if (previousFiber == fiber)
{ {
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } || if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)) !GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{ {
return SetReturn(ctx, FiberErrorPermission); return ctx.SetReturn(FiberErrorPermission);
} }
GuestCpuContinuation transferTarget; GuestCpuContinuation transferTarget;
@@ -586,12 +586,12 @@ public static class FiberExports
{ {
if (!TryReadFiberFields(ctx, fiber, out fields)) if (!TryReadFiberFields(ctx, fiber, out fields))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (fields.State != FiberStateIdle) if (fields.State != FiberStateIdle)
{ {
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}"); TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
FiberContinuation targetContinuation; FiberContinuation targetContinuation;
@@ -602,12 +602,12 @@ public static class FiberExports
} }
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation)) else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun)) if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{ {
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
var callerContinuation = new FiberContinuation( var callerContinuation = new FiberContinuation(
@@ -620,7 +620,7 @@ public static class FiberExports
previousState != FiberStateRun || previousState != FiberStateRun ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle)) !ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
{ {
return SetReturn(ctx, FiberErrorState); return ctx.SetReturn(FiberErrorState);
} }
_continuations[previousFiber] = callerContinuation; _continuations[previousFiber] = callerContinuation;
@@ -639,7 +639,7 @@ public static class FiberExports
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun); _ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
} }
_returnTargets.TryRemove(fiber, out _); _returnTargets.TryRemove(fiber, out _);
return SetReturn(ctx, FiberErrorInvalid); return ctx.SetReturn(FiberErrorInvalid);
} }
if (resumed) if (resumed)
@@ -656,7 +656,7 @@ public static class FiberExports
TraceFiber( TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " + $"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}"); $"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
private static bool TryCreateInitialContinuation( private static bool TryCreateInitialContinuation(
@@ -925,12 +925,6 @@ public static class FiberExports
return true; return true;
} }
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceFiber(string message) private static void TraceFiber(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
@@ -51,17 +51,17 @@ public static class KernelEventFlagCompatExports
optionAddress != 0 || optionAddress != 0 ||
!IsValidAttributes(attributes)) !IsValidAttributes(attributes))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name)) if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength) if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle)); var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
@@ -75,11 +75,11 @@ public static class KernelEventFlagCompatExports
if (!ctx.TryWriteUInt64(outAddress, handle)) if (!ctx.TryWriteUInt64(outAddress, handle))
{ {
_eventFlags.TryRemove(handle, out _); _eventFlags.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}"); TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -92,7 +92,7 @@ public static class KernelEventFlagCompatExports
var handle = ctx[CpuRegister.Rdi]; var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state)) if (!_eventFlags.TryRemove(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
lock (state.Gate) lock (state.Gate)
@@ -101,7 +101,7 @@ public static class KernelEventFlagCompatExports
} }
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'"); TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -116,7 +116,7 @@ public static class KernelEventFlagCompatExports
var returnRip = GetCurrentReturnRip(); var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state)) if (!_eventFlags.TryGetValue(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
lock (state.Gate) lock (state.Gate)
@@ -127,7 +127,7 @@ public static class KernelEventFlagCompatExports
} }
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -141,7 +141,7 @@ public static class KernelEventFlagCompatExports
var pattern = ctx[CpuRegister.Rsi]; var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state)) if (!_eventFlags.TryGetValue(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
lock (state.Gate) lock (state.Gate)
@@ -150,7 +150,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}"); TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
} }
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -167,29 +167,29 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state)) if (!_eventFlags.TryGetValue(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (pattern == 0 || !IsValidWaitMode(waitMode)) if (pattern == 0 || !IsValidWaitMode(waitMode))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
lock (state.Gate) lock (state.Gate)
{ {
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits)) if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (!IsSatisfied(state.Bits, pattern, waitMode)) if (!IsSatisfied(state.Bits, pattern, waitMode))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
} }
ApplyClearMode(state, pattern, waitMode); ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}"); TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -209,18 +209,18 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state)) if (!_eventFlags.TryGetValue(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (pattern == 0 || !IsValidWaitMode(waitMode)) if (pattern == 0 || !IsValidWaitMode(waitMode))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
uint timeoutUsec = 0; uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec)) if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
Monitor.Enter(state.Gate); Monitor.Enter(state.Gate);
@@ -228,7 +228,7 @@ public static class KernelEventFlagCompatExports
{ {
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult)) if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{ {
return SetReturn(ctx, immediateWaitResult); return ctx.SetReturn(immediateWaitResult);
} }
if (timeoutAddress != 0) if (timeoutAddress != 0)
@@ -236,7 +236,7 @@ public static class KernelEventFlagCompatExports
_ = ctx.TryWriteUInt32(timeoutAddress, 0); _ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits); _ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}"); 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); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
} }
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle; var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
@@ -271,7 +271,7 @@ public static class KernelEventFlagCompatExports
var scheduler = GuestThreadExecution.Scheduler; var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null) if (scheduler is null)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
} }
state.WaitingThreads++; state.WaitingThreads++;
@@ -296,7 +296,7 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false; releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}"); TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult); return ctx.SetReturn(pumpedWaitResult);
} }
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds); Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
@@ -313,7 +313,7 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads++; state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
finally finally
{ {
@@ -333,7 +333,7 @@ public static class KernelEventFlagCompatExports
var waiterCountAddress = ctx[CpuRegister.Rdx]; var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state)) if (!_eventFlags.TryGetValue(handle, out var state))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
lock (state.Gate) lock (state.Gate)
@@ -341,7 +341,7 @@ public static class KernelEventFlagCompatExports
if (waiterCountAddress != 0 && if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads))) !ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
state.Bits = setPattern; state.Bits = setPattern;
@@ -350,7 +350,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}"); TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
} }
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
private static bool IsValidAttributes(uint attributes) private static bool IsValidAttributes(uint attributes)
@@ -452,13 +452,6 @@ public static class KernelEventFlagCompatExports
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) => private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits); address == 0 || ctx.TryWriteUInt64(address, bits);
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceEventFlag(string message) private static void TraceEventFlag(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
+11 -14
View File
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelExports public static class KernelExports
{ {
private static int _nextFileDescriptor = 2;
private static readonly object _cxaGate = new(); private static readonly object _cxaGate = new();
private static readonly List<CxaDestructorEntry> _cxaDestructors = new(); private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
private static readonly object _coredumpGate = new(); private static readonly object _coredumpGate = new();
@@ -53,14 +52,7 @@ public static class KernelExports
ExportName = "exit", ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int Exit(CpuContext ctx) public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "XKRegsFpEpk", Nid = "XKRegsFpEpk",
@@ -173,11 +165,7 @@ public static class KernelExports
ExportName = "_exit", ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx) public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "Ac86z8q7T8A", Nid = "Ac86z8q7T8A",
@@ -438,4 +426,13 @@ public static class KernelExports
{ {
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
} }
private static int RequestProcessExit(CpuContext ctx, string syscallName)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
} }
@@ -2903,7 +2903,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
if (protectionOut != 0 && !TryWriteInt32(ctx, protectionOut, region.Protection)) if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -2942,7 +2942,7 @@ public static class KernelMemoryCompatExports
if (!ctx.TryWriteUInt64(infoAddress, block.Start) || if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) || !ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType)) !ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -3800,7 +3800,7 @@ public static class KernelMemoryCompatExports
var addr = argumentSource.NextGpArg(); var addr = argumentSource.NextGpArg();
if (addr != 0) if (addr != 0)
{ {
_ = TryWriteInt32(ctx, addr, sb.Length); _ = ctx.TryWriteInt32(addr, sb.Length);
} }
} }
break; break;
@@ -4964,7 +4964,7 @@ public static class KernelMemoryCompatExports
processedCount++; processedCount++;
} }
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount)) if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -5801,13 +5801,6 @@ public static class KernelMemoryCompatExports
return (protect & expected) != 0; return (protect & expected) != 0;
} }
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BitConverter.TryWriteBytes(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress) private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
{ {
if (fd is 0 or 1 or 2) if (fd is 0 or 1 or 2)
@@ -3,8 +3,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
@@ -329,6 +327,12 @@ public static class KernelPthreadCompatExports
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false); 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( [SysAbiExport(
Nid = "m5-2bsNfv7s", Nid = "m5-2bsNfv7s",
@@ -383,42 +387,42 @@ public static class KernelPthreadCompatExports
var initRoutine = ctx[CpuRegister.Rsi]; var initRoutine = ctx[CpuRegister.Rsi];
if (onceAddress == 0 || initRoutine == 0) if (onceAddress == 0 || initRoutine == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!TryReadInt32(ctx, onceAddress, out var onceValue)) if (!ctx.TryReadInt32(onceAddress, out var onceValue))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (onceValue == PthreadOnceDone) if (onceValue == PthreadOnceDone)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
var gate = GetPthreadOnceGate(onceAddress); var gate = GetPthreadOnceGate(onceAddress);
var shouldCall = false; var shouldCall = false;
lock (gate) lock (gate)
{ {
if (!TryReadInt32(ctx, onceAddress, out onceValue)) if (!ctx.TryReadInt32(onceAddress, out onceValue))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
while (onceValue == PthreadOnceInProgress) while (onceValue == PthreadOnceInProgress)
{ {
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1)); Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
if (!TryReadInt32(ctx, onceAddress, out onceValue)) if (!ctx.TryReadInt32(onceAddress, out onceValue))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
if (onceValue != PthreadOnceDone) if (onceValue != PthreadOnceDone)
{ {
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress)) if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
shouldCall = true; shouldCall = true;
@@ -434,21 +438,21 @@ public static class KernelPthreadCompatExports
{ {
lock (gate) lock (gate)
{ {
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized); _ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate); Monitor.PulseAll(gate);
} }
TracePthreadOnce(onceAddress, initRoutine, "failed", error); TracePthreadOnce(onceAddress, initRoutine, "failed", error);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
} }
lock (gate) lock (gate)
{ {
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone)) if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
{ {
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized); _ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate); Monitor.PulseAll(gate);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
Monitor.PulseAll(gate); Monitor.PulseAll(gate);
@@ -456,7 +460,7 @@ public static class KernelPthreadCompatExports
} }
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null); TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress) private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
@@ -1315,32 +1319,6 @@ public static class KernelPthreadCompatExports
} }
} }
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state) private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{ {
var createdState = new PthreadMutexState var createdState = new PthreadMutexState
@@ -2,10 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text; using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
@@ -279,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
priority = GetOrCreateThreadStateLocked(thread).Priority; priority = GetOrCreateThreadStateLocked(thread).Priority;
} }
if (!TryWriteInt32(ctx, outPriorityAddress, priority)) if (!ctx.TryWriteInt32(outPriorityAddress, priority))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -326,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority)) if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -496,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress); state = GetOrCreateAttrStateLocked(attrAddress);
} }
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState)) if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -739,7 +737,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress); state = GetOrCreateAttrStateLocked(attrAddress);
} }
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority)) if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -762,7 +760,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority)) if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1114,7 +1112,7 @@ public static class KernelPthreadExtendedCompatExports
} }
} }
if (!TryWriteInt32(ctx, outKeyAddress, key)) if (!ctx.TryWriteInt32(outKeyAddress, key))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1451,24 +1449,4 @@ public static class KernelPthreadExtendedCompatExports
payload[^1] = 0; payload[^1] = 0;
return ctx.Memory.TryWrite(address, payload); return ctx.Memory.TryWrite(address, payload);
} }
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
} }
@@ -4,14 +4,11 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Fiber; using SharpEmu.Libs.Fiber;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
@@ -259,8 +256,8 @@ public static class KernelRuntimeCompatExports
} }
if (timezoneAddress != 0 && if (timezoneAddress != 0 &&
(!TryWriteInt32(ctx, timezoneAddress, 0) || (!ctx.TryWriteInt32(timezoneAddress, 0) ||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0))) !ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
{ {
return -1; return -1;
} }
@@ -617,7 +614,7 @@ public static class KernelRuntimeCompatExports
internal static bool TrySetErrno(CpuContext ctx, int value) internal static bool TrySetErrno(CpuContext ctx, int value)
{ {
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset); var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
return address != 0 && TryWriteInt32(ctx, address, value); return address != 0 && ctx.TryWriteInt32(address, value);
} }
[SysAbiExport( [SysAbiExport(
@@ -827,7 +824,7 @@ public static class KernelRuntimeCompatExports
} }
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress); var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle)) if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -857,7 +854,7 @@ public static class KernelRuntimeCompatExports
} }
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress); var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle)) if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1058,7 +1055,7 @@ public static class KernelRuntimeCompatExports
public static int KernelStopUnloadModule(CpuContext ctx) public static int KernelStopUnloadModule(CpuContext ctx)
{ {
var resultAddress = ctx[CpuRegister.R9]; var resultAddress = ctx[CpuRegister.R9];
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0)) if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1076,7 +1073,7 @@ public static class KernelRuntimeCompatExports
{ {
var modulePathAddress = ctx[CpuRegister.Rdi]; var modulePathAddress = ctx[CpuRegister.Rdi];
var resultAddress = ctx[CpuRegister.R9]; var resultAddress = ctx[CpuRegister.R9];
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0)) if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1233,8 +1230,8 @@ public static class KernelRuntimeCompatExports
: 0; : 0;
var minutesWest = unchecked((int)-offset.TotalMinutes); var minutesWest = unchecked((int)-offset.TotalMinutes);
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) || if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60)) !ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1244,7 +1241,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds)) if (dstSecondsAddress != 0 && !ctx.TryWriteInt32(dstSecondsAddress, dstSeconds))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1304,7 +1301,7 @@ public static class KernelRuntimeCompatExports
_loadedSysmodules.Add(moduleId); _loadedSysmodules.Add(moduleId);
} }
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0)) if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1394,7 +1391,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, module.Handle)) if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, module.Handle))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1431,7 +1428,7 @@ public static class KernelRuntimeCompatExports
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length); var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
for (var i = 0; i < writableCount; i++) for (var i = 0; i < writableCount; i++)
{ {
if (!TryWriteInt32(ctx, handlesAddress + (ulong)(i * sizeof(int)), handles[i])) if (!ctx.TryWriteInt32(handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1472,26 +1469,42 @@ public static class KernelRuntimeCompatExports
return false; return false;
} }
var bytes = new List<byte>(Math.Min(maxLength, 64)); const int pageSize = 4096;
Span<byte> one = stackalloc byte[1]; const int inlineChunkSize = 64;
for (var i = 0; i < maxLength; i++) Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
var length = 0;
for (var offset = 0; offset < maxLength && length < buffer.Length;)
{ {
if (!ctx.Memory.TryRead(address + (ulong)i, one)) var current = address + (ulong)offset;
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
var chunkSize = Math.Min(
buffer.Length - length,
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
if (chunkSize <= 0)
{ {
return false; return false;
} }
if (one[0] == 0) var chunk = buffer.Slice(length, chunkSize);
if (!ctx.Memory.TryRead(current, chunk))
{ {
value = Encoding.UTF8.GetString(bytes.ToArray()); return false;
}
var nulIndex = chunk.IndexOf((byte)0);
if (nulIndex >= 0)
{
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
return true; return true;
} }
bytes.Add(one[0]); length += chunkSize;
offset += chunkSize;
} }
value = Encoding.UTF8.GetString(bytes.ToArray()); return false;
return true;
} }
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset) private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
@@ -1504,13 +1517,6 @@ public static class KernelRuntimeCompatExports
return unchecked(ctx.FsBase + offset); return unchecked(ctx.FsBase + offset);
} }
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static nint AllocateStackChkGuardObject() private static nint AllocateStackChkGuardObject()
{ {
try try
@@ -44,12 +44,12 @@ public static class KernelSemaphoreCompatExports
initialCount > maxCount || initialCount > maxCount ||
optionAddress != 0) optionAddress != 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name)) if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle)); var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
@@ -69,11 +69,11 @@ public static class KernelSemaphoreCompatExports
if (!ctx.TryWriteUInt32(semaphoreAddress, handle)) if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{ {
_semaphores.TryRemove(handle, out _); _semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}"); TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -89,12 +89,12 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore)) if (!_semaphores.TryGetValue(handle, out var semaphore))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (needCount < 1 || needCount > semaphore.MaxCount) if (needCount < 1 || needCount > semaphore.MaxCount)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
lock (semaphore.Gate) lock (semaphore.Gate)
@@ -103,30 +103,30 @@ public static class KernelSemaphoreCompatExports
{ {
semaphore.Count -= needCount; semaphore.Count -= needCount;
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
if (timeoutAddress != 0) if (timeoutAddress != 0)
{ {
if (!ctx.TryReadUInt32(timeoutAddress, out _)) if (!ctx.TryReadUInt32(timeoutAddress, out _))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
_ = ctx.TryWriteUInt32(timeoutAddress, 0); _ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
} }
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema")) if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
{ {
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
} }
semaphore.WaitingThreads++; semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -142,12 +142,12 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore)) if (!_semaphores.TryGetValue(handle, out var semaphore))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (needCount < 1 || needCount > semaphore.MaxCount) if (needCount < 1 || needCount > semaphore.MaxCount)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
lock (semaphore.Gate) lock (semaphore.Gate)
@@ -155,12 +155,12 @@ public static class KernelSemaphoreCompatExports
if (semaphore.Count < needCount) if (semaphore.Count < needCount)
{ {
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
} }
semaphore.Count -= needCount; semaphore.Count -= needCount;
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -176,24 +176,24 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore)) if (!_semaphores.TryGetValue(handle, out var semaphore))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (signalCount <= 0) if (signalCount <= 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
lock (semaphore.Gate) lock (semaphore.Gate)
{ {
if (semaphore.Count > semaphore.MaxCount - signalCount) if (semaphore.Count > semaphore.MaxCount - signalCount)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
semaphore.Count += signalCount; semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -210,25 +210,25 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore)) if (!_semaphores.TryGetValue(handle, out var semaphore))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
if (setCount > semaphore.MaxCount) if (setCount > semaphore.MaxCount)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
lock (semaphore.Gate) lock (semaphore.Gate)
{ {
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads))) if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount; semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0; semaphore.WaitingThreads = 0;
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}"); TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -242,18 +242,11 @@ public static class KernelSemaphoreCompatExports
var handle = unchecked((uint)ctx[CpuRegister.Rdi]); var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore)) if (!_semaphores.TryRemove(handle, out var semaphore))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
} }
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'"); TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
} }
private static void TraceSemaphore(string message) private static void TraceSemaphore(string message)
+3 -11
View File
@@ -1,9 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Network; namespace SharpEmu.Libs.Network;
@@ -32,7 +30,7 @@ public static class Http2Exports
if (poolSize == 0 || maxRequests <= 0) if (poolSize == 0 || maxRequests <= 0)
{ {
return SetReturn(ctx, Http2ErrorInvalidArgument); return ctx.SetReturn(Http2ErrorInvalidArgument);
} }
var id = Interlocked.Increment(ref _nextContextId); var id = Interlocked.Increment(ref _nextContextId);
@@ -53,17 +51,11 @@ public static class Http2Exports
var id = unchecked((int)ctx[CpuRegister.Rdi]); var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _)) if (!_contexts.TryRemove(id, out _))
{ {
return SetReturn(ctx, Http2ErrorInvalidId); return ctx.SetReturn(Http2ErrorInvalidId);
} }
TraceHttp2("term", id, 0, 0, 0, 0); TraceHttp2("term", id, 0, 0, 0, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3) private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
+6 -13
View File
@@ -3,7 +3,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Network; namespace SharpEmu.Libs.Network;
@@ -33,7 +32,7 @@ public static class HttpExports
var poolSize = ctx[CpuRegister.Rdx]; var poolSize = ctx[CpuRegister.Rdx];
if (poolSize == 0) if (poolSize == 0)
{ {
return SetReturn(ctx, HttpErrorInvalidValue); return ctx.SetReturn(HttpErrorInvalidValue);
} }
var id = Interlocked.Increment(ref _nextContextId); var id = Interlocked.Increment(ref _nextContextId);
@@ -53,7 +52,7 @@ public static class HttpExports
var contextId = unchecked((int)ctx[CpuRegister.Rdi]); var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.ContainsKey(contextId)) if (!Contexts.ContainsKey(contextId))
{ {
return SetReturn(ctx, HttpErrorInvalidId); return ctx.SetReturn(HttpErrorInvalidId);
} }
var userAgentAddress = ctx[CpuRegister.Rsi]; var userAgentAddress = ctx[CpuRegister.Rsi];
@@ -75,8 +74,8 @@ public static class HttpExports
{ {
var templateId = unchecked((int)ctx[CpuRegister.Rdi]); var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
return Templates.TryRemove(templateId, out _) return Templates.TryRemove(templateId, out _)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, HttpErrorInvalidId); : ctx.SetReturn(HttpErrorInvalidId);
} }
[SysAbiExport( [SysAbiExport(
@@ -89,7 +88,7 @@ public static class HttpExports
var contextId = unchecked((int)ctx[CpuRegister.Rdi]); var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.TryRemove(contextId, out _)) if (!Contexts.TryRemove(contextId, out _))
{ {
return SetReturn(ctx, HttpErrorInvalidId); return ctx.SetReturn(HttpErrorInvalidId);
} }
foreach (var pair in Templates) foreach (var pair in Templates)
@@ -100,13 +99,7 @@ public static class HttpExports
} }
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3) private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
+23 -29
View File
@@ -57,27 +57,27 @@ public static class NetCtlExports
var natInfoAddress = ctx[CpuRegister.Rdi]; var natInfoAddress = ctx[CpuRegister.Rdi];
if (natInfoAddress == 0) if (natInfoAddress == 0)
{ {
return SetReturn(ctx, NetCtlErrorInvalidAddress); return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
} }
Span<byte> natInfo = stackalloc byte[NatInfoSize]; Span<byte> natInfo = stackalloc byte[NatInfoSize];
if (!ctx.Memory.TryRead(natInfoAddress, natInfo)) if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]); var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
if (size != NatInfoSize) if (size != NatInfoSize)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, typeof(long));
} }
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1); BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3); BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001); BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
return ctx.Memory.TryWrite(natInfoAddress, natInfo) return ctx.Memory.TryWrite(natInfoAddress, natInfo)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -87,7 +87,7 @@ public static class NetCtlExports
LibraryName = "libSceNetCtl")] LibraryName = "libSceNetCtl")]
public static int NetCtlCheckCallback(CpuContext ctx) public static int NetCtlCheckCallback(CpuContext ctx)
{ {
return SetReturn(ctx, 0); return ctx.SetReturn(0, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -100,14 +100,14 @@ public static class NetCtlExports
var stateAddress = ctx[CpuRegister.Rdi]; var stateAddress = ctx[CpuRegister.Rdi];
if (stateAddress == 0) if (stateAddress == 0)
{ {
return SetReturn(ctx, NetCtlErrorInvalidAddress); return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
} }
Span<byte> stateBytes = stackalloc byte[sizeof(int)]; Span<byte> stateBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0); BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
return ctx.Memory.TryWrite(stateAddress, stateBytes) return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -122,7 +122,7 @@ public static class NetCtlExports
var callbackIdAddress = ctx[CpuRegister.Rdx]; var callbackIdAddress = ctx[CpuRegister.Rdx];
if (function == 0 || callbackIdAddress == 0) if (function == 0 || callbackIdAddress == 0)
{ {
return SetReturn(ctx, NetCtlErrorInvalidAddress); return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
} }
lock (CallbackGate) lock (CallbackGate)
@@ -130,20 +130,20 @@ public static class NetCtlExports
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0); var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
if (callbackId < 0) if (callbackId < 0)
{ {
return SetReturn(ctx, NetCtlErrorNoSpace); return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
} }
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)]; Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId)); BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes)) if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
Callbacks[callbackId] = new CallbackRegistration(function, argument); Callbacks[callbackId] = new CallbackRegistration(function, argument);
} }
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -157,7 +157,7 @@ public static class NetCtlExports
var infoAddress = ctx[CpuRegister.Rsi]; var infoAddress = ctx[CpuRegister.Rsi];
if (infoAddress == 0) if (infoAddress == 0)
{ {
return SetReturn(ctx, NetCtlErrorInvalidAddress); return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
} }
return code switch return code switch
@@ -177,7 +177,7 @@ public static class NetCtlExports
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0), NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256), NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0), NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
_ => SetReturn(ctx, NetCtlErrorNotConnected), _ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
}; };
} }
@@ -185,8 +185,8 @@ public static class NetCtlExports
{ {
Span<byte> bytes = stackalloc byte[count]; Span<byte> bytes = stackalloc byte[count];
return ctx.Memory.TryWrite(address, bytes) return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
private static int WriteUInt32(CpuContext ctx, ulong address, uint value) private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
@@ -194,8 +194,8 @@ public static class NetCtlExports
Span<byte> bytes = stackalloc byte[sizeof(uint)]; Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes) return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value) private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
@@ -203,8 +203,8 @@ public static class NetCtlExports
Span<byte> bytes = stackalloc byte[sizeof(ushort)]; Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value); BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes) return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount) private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
@@ -217,13 +217,7 @@ public static class NetCtlExports
} }
return ctx.Memory.TryWrite(address, bytes) return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
} }
} }
+12 -20
View File
@@ -1,11 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text; using System.Text;
using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Network; namespace SharpEmu.Libs.Network;
@@ -35,7 +33,7 @@ public static class NetExports
{ {
_initialized = true; _initialized = true;
TraceNet("init", 0, 0, 0, 0); TraceNet("init", 0, 0, 0, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -49,7 +47,7 @@ public static class NetExports
_pools.Clear(); _pools.Clear();
_resolvers.Clear(); _resolvers.Clear();
TraceNet("term", 0, 0, 0, 0); TraceNet("term", 0, 0, 0, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -65,7 +63,7 @@ public static class NetExports
if (size <= 0) if (size <= 0)
{ {
return SetReturn(ctx, NetErrorInvalidArgument); return ctx.SetReturn(NetErrorInvalidArgument);
} }
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value) var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
@@ -90,11 +88,11 @@ public static class NetExports
var id = unchecked((int)ctx[CpuRegister.Rdi]); var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_pools.TryRemove(id, out _)) if (!_pools.TryRemove(id, out _))
{ {
return SetReturn(ctx, NetErrorBadFileDescriptor); return ctx.SetReturn(NetErrorBadFileDescriptor);
} }
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL); TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -109,7 +107,7 @@ public static class NetExports
var flags = unchecked((int)ctx[CpuRegister.Rdx]); var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0) if (flags != 0)
{ {
return SetReturn(ctx, NetErrorInvalidArgument); return ctx.SetReturn(NetErrorInvalidArgument);
} }
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value) var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
@@ -131,8 +129,8 @@ public static class NetExports
{ {
var id = unchecked((int)ctx[CpuRegister.Rdi]); var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _) return _resolvers.TryRemove(id, out _)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, NetErrorBadFileDescriptor); : ctx.SetReturn(NetErrorBadFileDescriptor);
} }
[SysAbiExport( [SysAbiExport(
@@ -146,25 +144,19 @@ public static class NetExports
var statusAddress = ctx[CpuRegister.Rsi]; var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0) if (statusAddress == 0)
{ {
return SetReturn(ctx, NetErrorInvalidArgument); return ctx.SetReturn(NetErrorInvalidArgument);
} }
if (!_resolvers.TryGetValue(id, out var resolver)) if (!_resolvers.TryGetValue(id, out var resolver))
{ {
return SetReturn(ctx, NetErrorBadFileDescriptor); return ctx.SetReturn(NetErrorBadFileDescriptor);
} }
Span<byte> status = stackalloc byte[sizeof(int)]; Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError); BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status) return ctx.Memory.TryWrite(statusAddress, status)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value) private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
+4 -12
View File
@@ -1,9 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Network; namespace SharpEmu.Libs.Network;
@@ -28,7 +26,7 @@ public static class SslExports
var poolSize = ctx[CpuRegister.Rdi]; var poolSize = ctx[CpuRegister.Rdi];
if (poolSize == 0) if (poolSize == 0)
{ {
return SetReturn(ctx, SslErrorOutOfSize); return ctx.SetReturn(SslErrorOutOfSize);
} }
var id = Interlocked.Increment(ref _nextContextId); var id = Interlocked.Increment(ref _nextContextId);
@@ -49,11 +47,11 @@ public static class SslExports
var id = unchecked((int)ctx[CpuRegister.Rdi]); var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _)) if (!_contexts.TryRemove(id, out _))
{ {
return SetReturn(ctx, SslErrorInvalidId); return ctx.SetReturn(SslErrorInvalidId);
} }
TraceSsl("term", id, 0); TraceSsl("term", id, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -65,13 +63,7 @@ public static class SslExports
{ {
var id = unchecked((int)ctx[CpuRegister.Rdi]); var id = unchecked((int)ctx[CpuRegister.Rdi]);
TraceSsl("close", id, 0); TraceSsl("close", id, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
private static void TraceSsl(string operation, int id, ulong arg0) private static void TraceSsl(string operation, int id, ulong arg0)
+26 -32
View File
@@ -4,7 +4,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Ngs2; namespace SharpEmu.Libs.Ngs2;
@@ -39,13 +38,13 @@ public static class Ngs2Exports
var outHandleAddress = ctx[CpuRegister.Rdx]; var outHandleAddress = ctx[CpuRegister.Rdx];
if (outHandleAddress == 0) if (outHandleAddress == 0)
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
} }
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) || if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle)) !ctx.TryWriteUInt64(outHandleAddress, handle))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
lock (StateGate) lock (StateGate)
@@ -53,7 +52,7 @@ public static class Ngs2Exports
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid))); Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -68,7 +67,7 @@ public static class Ngs2Exports
{ {
if (!Systems.Remove(handle)) if (!Systems.Remove(handle))
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
} }
var rackHandles = Racks var rackHandles = Racks
@@ -81,7 +80,7 @@ public static class Ngs2Exports
} }
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -98,19 +97,19 @@ public static class Ngs2Exports
{ {
if (!Systems.ContainsKey(systemHandle)) if (!Systems.ContainsKey(systemHandle))
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
} }
} }
if (outHandleAddress == 0) if (outHandleAddress == 0)
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
} }
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) || if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle)) !ctx.TryWriteUInt64(outHandleAddress, handle))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
lock (StateGate) lock (StateGate)
@@ -118,7 +117,7 @@ public static class Ngs2Exports
Racks[handle] = new RackState(systemHandle, rackId); Racks[handle] = new RackState(systemHandle, rackId);
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -133,13 +132,13 @@ public static class Ngs2Exports
{ {
if (!Racks.ContainsKey(handle)) if (!Racks.ContainsKey(handle))
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle); return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
} }
RemoveRackLocked(handle); RemoveRackLocked(handle);
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -156,7 +155,7 @@ public static class Ngs2Exports
{ {
if (!Racks.ContainsKey(rackHandle)) if (!Racks.ContainsKey(rackHandle))
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle); return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
} }
var existing = Voices.FirstOrDefault( var existing = Voices.FirstOrDefault(
@@ -164,20 +163,20 @@ public static class Ngs2Exports
if (existing.Key != 0) if (existing.Key != 0)
{ {
return ctx.TryWriteUInt64(outHandleAddress, existing.Key) return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
if (outHandleAddress == 0) if (outHandleAddress == 0)
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
} }
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) || if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
!ctx.TryWriteUInt64(outHandleAddress, handle)) !ctx.TryWriteUInt64(outHandleAddress, handle))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
lock (StateGate) lock (StateGate)
@@ -185,7 +184,7 @@ public static class Ngs2Exports
Voices[handle] = new VoiceState(rackHandle, voiceIndex); Voices[handle] = new VoiceState(rackHandle, voiceIndex);
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -197,9 +196,10 @@ public static class Ngs2Exports
{ {
lock (StateGate) lock (StateGate)
{ {
return SetReturn( return ctx.SetReturn(
ctx, Voices.ContainsKey(ctx[CpuRegister.Rdi])
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle); ? 0
: OrbisNgs2ErrorInvalidVoiceHandle);
} }
} }
@@ -224,13 +224,13 @@ public static class Ngs2Exports
{ {
if (!Systems.ContainsKey(systemHandle)) if (!Systems.ContainsKey(systemHandle))
{ {
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
} }
} }
if (bufferInfoCount != 0 && bufferInfoAddress == 0) if (bufferInfoCount != 0 && bufferInfoAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
for (uint i = 0; i < bufferInfoCount; i++) for (uint i = 0; i < bufferInfoCount; i++)
@@ -239,14 +239,14 @@ public static class Ngs2Exports
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) || if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize)) !ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (bufferAddress != 0 && bufferSize != 0) if (bufferAddress != 0 && bufferSize != 0)
{ {
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize)) if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
} }
@@ -258,7 +258,7 @@ public static class Ngs2Exports
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}"); $"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
} }
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle) private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
@@ -313,10 +313,4 @@ public static class Ngs2Exports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"), Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
"1", "1",
StringComparison.Ordinal); StringComparison.Ordinal);
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
} }
@@ -26,12 +26,12 @@ public static class NpEntitlementAccessExports
clear.Clear(); clear.Clear();
if (!ctx.Memory.TryWrite(bootParam, clear)) if (!ctx.Memory.TryWrite(bootParam, clear))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}"); TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -48,20 +48,14 @@ public static class NpEntitlementAccessExports
emptyList.Clear(); emptyList.Clear();
if (!ctx.Memory.TryWrite(listAddress, emptyList)) if (!ctx.Memory.TryWrite(listAddress, emptyList))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
TraceNpEntitlementAccess( TraceNpEntitlementAccess(
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " + $"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty"); $"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
} }
private static void TraceNpEntitlementAccess(string message) private static void TraceNpEntitlementAccess(string message)
+6 -12
View File
@@ -76,14 +76,14 @@ public static class NpManagerExports
var stateAddress = ctx[CpuRegister.Rsi]; var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0) if (stateAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> stateBytes = stackalloc byte[sizeof(uint)]; Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1); BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
return ctx.Memory.TryWrite(stateAddress, stateBytes) return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -97,7 +97,7 @@ public static class NpManagerExports
var titleSecretAddress = ctx[CpuRegister.Rsi]; var titleSecretAddress = ctx[CpuRegister.Rsi];
if (titleIdAddress == 0 || titleSecretAddress == 0) if (titleIdAddress == 0 || titleSecretAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> titleId = stackalloc byte[NpTitleIdSize]; Span<byte> titleId = stackalloc byte[NpTitleIdSize];
@@ -105,17 +105,11 @@ public static class NpManagerExports
if (!ctx.Memory.TryRead(titleIdAddress, titleId) || if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret)) !ctx.Memory.TryRead(titleSecretAddress, titleSecret))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'"); TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
} }
private static string ReadTitleId(ReadOnlySpan<byte> bytes) private static string ReadTitleId(ReadOnlySpan<byte> bytes)
@@ -3,7 +3,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Np; namespace SharpEmu.Libs.Np;
@@ -22,13 +21,13 @@ public static class NpUniversalDataSystemExports
var parameterAddress = ctx[CpuRegister.Rdi]; var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0) if (parameterAddress == 0)
{ {
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument); return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
} }
Span<byte> parameters = stackalloc byte[16]; Span<byte> parameters = stackalloc byte[16];
return ctx.Memory.TryRead(parameterAddress, parameters) return ctx.Memory.TryRead(parameterAddress, parameters)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -41,14 +40,14 @@ public static class NpUniversalDataSystemExports
var contextAddress = ctx[CpuRegister.Rdi]; var contextAddress = ctx[CpuRegister.Rdi];
if (contextAddress == 0) if (contextAddress == 0)
{ {
return SetReturn(ctx, 0); return ctx.SetReturn(0, typeof(long));
} }
Span<byte> context = stackalloc byte[sizeof(int)]; Span<byte> context = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(context, 1); BinaryPrimitives.WriteInt32LittleEndian(context, 1);
return ctx.Memory.TryWrite(contextAddress, context) return ctx.Memory.TryWrite(contextAddress, context)
? SetReturn(ctx, 0) ? ctx.SetReturn(0, typeof(long))
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -59,13 +58,13 @@ public static class NpUniversalDataSystemExports
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx) public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
{ {
var handle = Interlocked.Increment(ref _nextHandle); var handle = Interlocked.Increment(ref _nextHandle);
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) || if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle)) ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
{ {
return SetReturn(ctx, 0); return ctx.SetReturn(0, typeof(long));
} }
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
} }
[SysAbiExport( [SysAbiExport(
@@ -75,24 +74,6 @@ public static class NpUniversalDataSystemExports
LibraryName = "libSceNpUniversalDataSystem")] LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx) public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
{ {
return SetReturn(ctx, 0); return ctx.SetReturn(0, typeof(long));
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
if (address == 0)
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
} }
} }
+3 -11
View File
@@ -1,8 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Np; namespace SharpEmu.Libs.Np;
@@ -25,12 +23,12 @@ public static class NpWebApi2Exports
if (httpContextId <= 0 || poolSize == 0) if (httpContextId <= 0 || poolSize == 0)
{ {
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument); return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
} }
Interlocked.Exchange(ref _initialized, 1); Interlocked.Exchange(ref _initialized, 1);
TraceNpWebApi2("init", httpContextId, poolSize); TraceNpWebApi2("init", httpContextId, poolSize);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -43,13 +41,7 @@ public static class NpWebApi2Exports
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]); var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
Interlocked.Exchange(ref _initialized, 0); Interlocked.Exchange(ref _initialized, 0);
TraceNpWebApi2("term", libraryContextId, 0); TraceNpWebApi2("term", libraryContextId, 0);
return SetReturn(ctx, 0); return ctx.SetReturn(0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
private static void TraceNpWebApi2(string operation, int id, ulong arg0) private static void TraceNpWebApi2(string operation, int id, ulong arg0)
+470
View File
@@ -0,0 +1,470 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad;
/// <summary>
/// Snapshot of the DualSense state, already translated to ORBIS pad
/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered;
/// triggers 0..255).
/// </summary>
internal readonly record struct DualSenseState(
bool Connected,
uint Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte L2,
byte R2);
/// <summary>
/// Reads a DualSense controller over raw HID on a background thread.
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
/// activated by requesting feature report 0x05), with hot-plug retry.
/// </summary>
internal static class DualSenseReader
{
private const ushort SonyVendorId = 0x054C;
private const ushort DualSenseProductId = 0x0CE6;
private const ushort DualSenseEdgeProductId = 0x0DF2;
// SCE_PAD_BUTTON bit values.
private const uint ButtonL3 = 0x0002;
private const uint ButtonR3 = 0x0004;
private const uint ButtonOptions = 0x0008;
private const uint ButtonUp = 0x0010;
private const uint ButtonRight = 0x0020;
private const uint ButtonDown = 0x0040;
private const uint ButtonLeft = 0x0080;
private const uint ButtonL2 = 0x0100;
private const uint ButtonR2 = 0x0200;
private const uint ButtonL1 = 0x0400;
private const uint ButtonR1 = 0x0800;
private const uint ButtonTriangle = 0x1000;
private const uint ButtonCircle = 0x2000;
private const uint ButtonCross = 0x4000;
private const uint ButtonSquare = 0x8000;
private const uint ButtonTouchPad = 0x100000;
private static readonly object Gate = new();
private static DualSenseState _state;
private static bool _started;
// Output (rumble/lightbar) state, all guarded by Gate.
private static string? _devicePath;
private static bool _bluetooth;
private static bool _outputReady;
private static bool _lightbarSetupPending;
private static byte _outputSequence;
private static FileStream? _outputStream;
private static byte _motorLeft;
private static byte _motorRight;
private static byte _lightbarRed;
private static byte _lightbarGreen;
private static byte _lightbarBlue = 64; // PS-style blue default
private static byte _playerLeds = 0x04; // center LED = player 1
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (Gate)
{
if (_started)
{
return;
}
_started = true;
var thread = new Thread(ReadLoop)
{
IsBackground = true,
Name = "DualSenseReader",
};
thread.Start();
}
}
internal static bool TryGetState(out DualSenseState state)
{
lock (Gate)
{
state = _state;
}
return state.Connected;
}
private static void SetState(in DualSenseState state)
{
lock (Gate)
{
_state = state;
}
}
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
internal static void SetRumble(byte largeMotor, byte smallMotor)
{
lock (Gate)
{
if (_motorLeft == largeMotor && _motorRight == smallMotor)
{
return;
}
_motorLeft = largeMotor;
_motorRight = smallMotor;
SendOutputLocked();
}
}
internal static void SetLightbar(byte red, byte green, byte blue)
{
lock (Gate)
{
if (_lightbarRed == red && _lightbarGreen == green && _lightbarBlue == blue)
{
return;
}
_lightbarRed = red;
_lightbarGreen = green;
_lightbarBlue = blue;
SendOutputLocked();
}
}
internal static void ResetLightbar() => SetLightbar(0, 0, 64);
private static void OnDeviceIdentified(string path, bool bluetooth)
{
lock (Gate)
{
_devicePath = path;
_bluetooth = bluetooth;
_outputReady = true;
_lightbarSetupPending = true;
// Announce ourselves on the hardware: default lightbar + player 1 LED.
SendOutputLocked();
}
}
private static void OnDeviceLost()
{
lock (Gate)
{
_devicePath = null;
_outputReady = false;
_motorLeft = 0;
_motorRight = 0;
_outputStream?.Dispose();
_outputStream = null;
}
}
private static void SendOutputLocked()
{
if (!_outputReady || _devicePath is null)
{
return; // flushed by OnDeviceIdentified once connected
}
try
{
if (_outputStream is null)
{
var handle = HidNative.CreateFile(
_devicePath,
HidNative.GenericRead | HidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
return; // read-only device access: outputs unavailable
}
_outputStream = new FileStream(handle, FileAccess.Write, bufferSize: 1);
}
var report = BuildOutputReportLocked();
_outputStream.Write(report, 0, report.Length);
_outputStream.Flush();
}
catch (Exception)
{
_outputStream?.Dispose();
_outputStream = null;
}
}
private static byte[] BuildOutputReportLocked()
{
// Common 47-byte output payload (offsets per the DualSense output
// report layout, same as Linux hid-playstation).
Span<byte> common = stackalloc byte[47];
common[0] = 0x03; // valid_flag0: compatible vibration + haptics select
common[1] = 0x04 | 0x10; // valid_flag1: lightbar + player indicator
common[2] = _motorRight; // right (weak) motor
common[3] = _motorLeft; // left (strong) motor
if (_lightbarSetupPending)
{
common[38] |= 0x02; // valid_flag2: lightbar setup control enable
common[41] = 0x01; // lightbar_setup: light on
_lightbarSetupPending = false;
}
common[43] = _playerLeds;
common[44] = _lightbarRed;
common[45] = _lightbarGreen;
common[46] = _lightbarBlue;
if (!_bluetooth)
{
var usbReport = new byte[48];
usbReport[0] = 0x02;
common.CopyTo(usbReport.AsSpan(1));
return usbReport;
}
// Bluetooth: 0x31 wrapper with sequence tag and CRC32 over a 0xA2
// seed byte plus the first 74 report bytes.
var btReport = new byte[78];
btReport[0] = 0x31;
btReport[1] = (byte)((_outputSequence & 0x0F) << 4);
_outputSequence = (byte)((_outputSequence + 1) & 0x0F);
btReport[2] = 0x10;
common.CopyTo(btReport.AsSpan(3));
var crc = Crc32(0xA2, btReport.AsSpan(0, 74));
btReport[74] = (byte)crc;
btReport[75] = (byte)(crc >> 8);
btReport[76] = (byte)(crc >> 16);
btReport[77] = (byte)(crc >> 24);
return btReport;
}
private static uint Crc32(byte seed, ReadOnlySpan<byte> data)
{
var crc = Crc32Update(0xFFFFFFFFu, seed);
foreach (var value in data)
{
crc = Crc32Update(crc, value);
}
return ~crc;
}
private static uint Crc32Update(uint crc, byte value)
{
crc ^= value;
for (var bit = 0; bit < 8; bit++)
{
crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1));
}
return crc;
}
private static void ReadLoop()
{
var announcedConnect = false;
while (true)
{
SafeFileHandle? handle = null;
try
{
handle = OpenDualSense(out var devicePath);
if (handle is null || devicePath is null)
{
SetState(default);
announcedConnect = false;
Thread.Sleep(1000);
continue;
}
// Bluetooth quirk: the DualSense sends a simplified report
// until feature report 0x05 is requested, which switches it
// to the full 0x31 input report. Harmless over USB.
var feature = new byte[41];
feature[0] = 0x05;
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
if (!announcedConnect)
{
Console.Error.WriteLine("[LOADER][INFO] DualSense controller connected.");
announcedConnect = true;
}
using var stream = new FileStream(handle, FileAccess.Read, bufferSize: 1);
handle = null; // stream owns it now
var buffer = new byte[256];
var transportKnown = false;
while (true)
{
var read = stream.Read(buffer, 0, buffer.Length);
if (read <= 0)
{
break;
}
if (TryParseReport(buffer.AsSpan(0, read), out var state))
{
if (!transportKnown)
{
// The first parsed report tells us the transport,
// which the output (rumble/lightbar) path needs.
transportKnown = true;
OnDeviceIdentified(devicePath, bluetooth: buffer[0] == 0x31);
}
SetState(state);
}
}
}
catch (Exception)
{
// Unplugged or read error: fall through and retry.
}
finally
{
handle?.Dispose();
}
if (announcedConnect)
{
Console.Error.WriteLine("[LOADER][INFO] DualSense controller disconnected.");
announcedConnect = false;
}
OnDeviceLost();
SetState(default);
Thread.Sleep(1000);
}
}
private static SafeFileHandle? OpenDualSense(out string? devicePath)
{
devicePath = null;
foreach (var path in HidNative.EnumerateHidDevicePaths())
{
// Open without access rights just to query VID/PID.
using var probe = HidNative.CreateFile(
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
if (probe.IsInvalid)
{
continue;
}
var attributes = new HidNative.HiddAttributes { Size = 12 };
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
attributes.VendorId != SonyVendorId ||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
{
continue;
}
// Read+write so feature reports work; fall back to read-only.
var handle = HidNative.CreateFile(
path,
HidNative.GenericRead | HidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
handle = HidNative.CreateFile(
path,
HidNative.GenericRead,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
}
if (!handle.IsInvalid)
{
devicePath = path;
return handle;
}
handle.Dispose();
}
return null;
}
private static bool TryParseReport(ReadOnlySpan<byte> report, out DualSenseState state)
{
// USB: report id 0x01, payload starts at [1].
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
int offset;
if (report.Length >= 11 && report[0] == 0x01)
{
offset = 1;
}
else if (report.Length >= 12 && report[0] == 0x31)
{
offset = 2;
}
else
{
state = default;
return false;
}
var leftX = report[offset + 0];
var leftY = report[offset + 1];
var rightX = report[offset + 2];
var rightY = report[offset + 3];
var l2 = report[offset + 4];
var r2 = report[offset + 5];
var buttons0 = report[offset + 7];
var buttons1 = report[offset + 8];
var buttons2 = report[offset + 9];
uint buttons = 0;
buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0;
buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0;
buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0;
buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0;
buttons |= HatToButtons(buttons0 & 0x0F);
buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0;
buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0;
state = new DualSenseState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
L2: l2,
R2: r2);
return true;
}
private static uint HatToButtons(int hat) => hat switch
{
0 => ButtonUp,
1 => ButtonUp | ButtonRight,
2 => ButtonRight,
3 => ButtonRight | ButtonDown,
4 => ButtonDown,
5 => ButtonDown | ButtonLeft,
6 => ButtonLeft,
7 => ButtonLeft | ButtonUp,
_ => 0,
};
}
+136
View File
@@ -0,0 +1,136 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad;
/// <summary>
/// Minimal Win32 HID interop used to talk to a DualSense controller
/// directly, without any external input library.
/// </summary>
internal static partial class HidNative
{
internal const int DigcfPresent = 0x02;
internal const int DigcfDeviceInterface = 0x10;
internal const uint GenericRead = 0x80000000;
internal const uint GenericWrite = 0x40000000;
internal const uint FileShareRead = 0x1;
internal const uint FileShareWrite = 0x2;
internal const uint OpenExisting = 3;
[StructLayout(LayoutKind.Sequential)]
internal struct SpDeviceInterfaceData
{
public int CbSize;
public Guid InterfaceClassGuid;
public int Flags;
public nint Reserved;
}
[StructLayout(LayoutKind.Sequential)]
internal struct HiddAttributes
{
public int Size;
public ushort VendorId;
public ushort ProductId;
public ushort VersionNumber;
}
[DllImport("hid.dll")]
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
[DllImport("hid.dll")]
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
[DllImport("hid.dll")]
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiEnumDeviceInterfaces(
nint deviceInfoSet,
nint deviceInfoData,
ref Guid interfaceClassGuid,
int memberIndex,
ref SpDeviceInterfaceData deviceInterfaceData);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
internal static extern bool SetupDiGetDeviceInterfaceDetail(
nint deviceInfoSet,
ref SpDeviceInterfaceData deviceInterfaceData,
nint deviceInterfaceDetailData,
int deviceInterfaceDetailDataSize,
out int requiredSize,
nint deviceInfoData);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern SafeFileHandle CreateFile(
string fileName,
uint desiredAccess,
uint shareMode,
nint securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
nint templateFile);
/// <summary>
/// Enumerates the device paths of all present HID interfaces.
/// </summary>
internal static List<string> EnumerateHidDevicePaths()
{
var paths = new List<string>();
HidD_GetHidGuid(out var hidGuid);
var deviceInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, 0, DigcfPresent | DigcfDeviceInterface);
if (deviceInfoSet == -1 || deviceInfoSet == 0)
{
return paths;
}
try
{
var interfaceData = new SpDeviceInterfaceData
{
CbSize = Marshal.SizeOf<SpDeviceInterfaceData>(),
};
for (var index = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, 0, ref hidGuid, index, ref interfaceData); index++)
{
SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, 0, 0, out var requiredSize, 0);
if (requiredSize <= 0)
{
continue;
}
var detailBuffer = Marshal.AllocHGlobal(requiredSize);
try
{
// SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize is 8 on x64
// (DWORD + aligned WCHAR[1]); the path string follows it.
Marshal.WriteInt32(detailBuffer, 8);
if (SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, detailBuffer, requiredSize, out _, 0) &&
Marshal.PtrToStringUni(detailBuffer + 4) is { Length: > 0 } path)
{
paths.Add(path);
}
}
finally
{
Marshal.FreeHGlobal(detailBuffer);
}
}
}
finally
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
}
return paths;
}
}
+127 -30
View File
@@ -30,7 +30,8 @@ public static class PadExports
public static int PadInit(CpuContext ctx) public static int PadInit(CpuContext ctx)
{ {
_initialized = true; _initialized = true;
return SetReturn(ctx, 0); DualSenseReader.EnsureStarted();
return ctx.SetReturn(0);
} }
[SysAbiExport( [SysAbiExport(
@@ -46,21 +47,24 @@ public static class PadExports
var parameterAddress = ctx[CpuRegister.Rcx]; var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized) if (!_initialized)
{ {
return SetReturn(ctx, OrbisPadErrorNotInitialized); return ctx.SetReturn(OrbisPadErrorNotInitialized);
} }
if (userId == -1) if (userId == -1)
{ {
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle); return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
} }
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0) if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
{ {
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected); return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
} }
Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options"); DualSenseReader.EnsureStarted();
return SetReturn(ctx, PrimaryPadHandle); Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
return ctx.SetReturn(PrimaryPadHandle);
} }
[SysAbiExport( [SysAbiExport(
@@ -72,8 +76,8 @@ public static class PadExports
{ {
var handle = unchecked((int)ctx[CpuRegister.Rdi]); var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle return handle == PrimaryPadHandle
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, OrbisPadErrorInvalidHandle); : ctx.SetReturn(OrbisPadErrorInvalidHandle);
} }
[SysAbiExport( [SysAbiExport(
@@ -87,12 +91,12 @@ public static class PadExports
var informationAddress = ctx[CpuRegister.Rsi]; var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle) if (handle != PrimaryPadHandle)
{ {
return SetReturn(ctx, OrbisPadErrorInvalidHandle); return ctx.SetReturn(OrbisPadErrorInvalidHandle);
} }
if (informationAddress == 0) if (informationAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Span<byte> information = stackalloc byte[ControllerInformationSize]; Span<byte> information = stackalloc byte[ControllerInformationSize];
@@ -107,8 +111,8 @@ public static class PadExports
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0); BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
return ctx.Memory.TryWrite(informationAddress, information) return ctx.Memory.TryWrite(informationAddress, information)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -122,17 +126,17 @@ public static class PadExports
var dataAddress = ctx[CpuRegister.Rsi]; var dataAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle) if (handle != PrimaryPadHandle)
{ {
return SetReturn(ctx, OrbisPadErrorInvalidHandle); return ctx.SetReturn(OrbisPadErrorInvalidHandle);
} }
if (dataAddress == 0) if (dataAddress == 0)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
return WriteNeutralPadData(ctx, dataAddress) return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -147,17 +151,17 @@ public static class PadExports
var count = unchecked((int)ctx[CpuRegister.Rdx]); var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (handle != PrimaryPadHandle) if (handle != PrimaryPadHandle)
{ {
return SetReturn(ctx, OrbisPadErrorInvalidHandle); return ctx.SetReturn(OrbisPadErrorInvalidHandle);
} }
if (dataAddress == 0 || count < 1 || count > 64) if (dataAddress == 0 || count < 1 || count > 64)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
return WriteNeutralPadData(ctx, dataAddress) return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 1) ? ctx.SetReturn(1)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -167,7 +171,84 @@ public static class PadExports
LibraryName = "libScePad")] LibraryName = "libScePad")]
public static int PadSetVibrationMode(CpuContext ctx) public static int PadSetVibrationMode(CpuContext ctx)
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "yFVnOdGxvZY",
ExportName = "scePadSetVibration",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetVibration(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (parameterAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
Span<byte> parameter = stackalloc byte[2];
if (!ctx.Memory.TryRead(parameterAddress, parameter))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "RR4novUEENY",
ExportName = "scePadSetLightBar",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (parameterAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
Span<byte> color = stackalloc byte[4];
if (!ctx.Memory.TryRead(parameterAddress, color))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "DscD1i9HX1w",
ExportName = "scePadResetLightBar",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
DualSenseReader.ResetLightbar();
return ctx.SetReturn(0);
} }
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress) private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
@@ -176,17 +257,33 @@ public static class PadExports
data.Clear(); data.Clear();
var acceptsKeyboardInput = IsEmulatorWindowFocused(); var acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0; var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128; var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128; var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128; var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128; var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
if (DualSenseReader.TryGetState(out var pad))
{
buttons |= pad.Buttons;
// The controller stick wins whenever it is deflected past a
// small deadzone; otherwise any keyboard value stays.
leftX = MergeAxis(pad.LeftX, leftX);
leftY = MergeAxis(pad.LeftY, leftY);
rightX = MergeAxis(pad.RightX, rightX);
rightY = MergeAxis(pad.RightY, rightY);
l2 = Math.Max(l2, pad.L2);
r2 = Math.Max(r2, pad.R2);
}
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX; data[0x04] = leftX;
data[0x05] = leftY; data[0x05] = leftY;
data[0x06] = rightX; data[0x06] = rightX;
data[0x07] = rightY; data[0x07] = rightY;
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0; data[0x08] = l2;
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0; data[0x09] = r2;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f); BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1; data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp(); var timestampTicks = Stopwatch.GetTimestamp();
@@ -201,12 +298,6 @@ public static class PadExports
return ctx.Memory.TryWrite(dataAddress, data); return ctx.Memory.TryWrite(dataAddress, data);
} }
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey); private static extern short GetAsyncKeyState(int vKey);
@@ -260,4 +351,10 @@ public static class PadExports
if (positive && !negative) return 255; if (positive && !negative) return 255;
return 128; return 128;
} }
private static byte MergeAxis(byte controller, byte keyboard)
{
const int Deadzone = 10;
return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
}
} }
+5 -39
View File
@@ -249,7 +249,7 @@ public static class PlayGoExports
for (uint i = 0; i < entriesToWrite; i++) for (uint i = 0; i < entriesToWrite; i++)
{ {
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i]; var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId)) if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -292,7 +292,7 @@ public static class PlayGoExports
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0 return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
? chunkError ? chunkError
: TryWriteInt64(ctx, outEta, 0) : ctx.TryWriteInt64(outEta, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -324,7 +324,7 @@ public static class PlayGoExports
speed = _installSpeed; speed = _installSpeed;
} }
return TryWriteInt32(ctx, outSpeed, speed) return ctx.TryWriteInt32(outSpeed, speed)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -399,7 +399,7 @@ public static class PlayGoExports
var loci = new byte[numberOfEntries]; var loci = new byte[numberOfEntries];
for (uint i = 0; i < numberOfEntries; i++) for (uint i = 0; i < numberOfEntries; i++)
{ {
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId)) if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -648,7 +648,7 @@ public static class PlayGoExports
{ {
for (uint i = 0; i < numberOfEntries; i++) for (uint i = 0; i < numberOfEntries; i++)
{ {
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId)) if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -728,40 +728,6 @@ public static class PlayGoExports
} }
} }
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> buffer = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static void TracePlayGo(string message) private static void TracePlayGo(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
@@ -3,7 +3,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.SaveData; namespace SharpEmu.Libs.SaveData;
@@ -35,10 +34,10 @@ public static class SaveDataDialogExports
{ {
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone) if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
{ {
return SetReturn(ctx, ErrorAlreadyInitialized); return ctx.SetReturn(ErrorAlreadyInitialized);
} }
return SetReturn(ctx, ErrorOk); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -51,12 +50,12 @@ public static class SaveDataDialogExports
var paramAddress = ctx[CpuRegister.Rdi]; var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0) if (paramAddress == 0)
{ {
return SetReturn(ctx, ErrorArgNull); return ctx.SetReturn(ErrorArgNull);
} }
if (_status is not (StatusInitialized or StatusFinished)) if (_status is not (StatusInitialized or StatusFinished))
{ {
return SetReturn(ctx, ErrorNotInitialized); return ctx.SetReturn(ErrorNotInitialized);
} }
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0; _lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
@@ -66,7 +65,7 @@ public static class SaveDataDialogExports
// guest polling sees a finished dialog instead of spinning forever. // guest polling sees a finished dialog instead of spinning forever.
Interlocked.Exchange(ref _status, StatusFinished); Interlocked.Exchange(ref _status, StatusFinished);
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished"); TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
return SetReturn(ctx, ErrorOk); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -74,21 +73,21 @@ public static class SaveDataDialogExports
ExportName = "sceSaveDataDialogGetStatus", ExportName = "sceSaveDataDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status)); public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
[SysAbiExport( [SysAbiExport(
Nid = "KK3Bdg1RWK0", Nid = "KK3Bdg1RWK0",
ExportName = "sceSaveDataDialogUpdateStatus", ExportName = "sceSaveDataDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status)); public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
[SysAbiExport( [SysAbiExport(
Nid = "en7gNVnh878", Nid = "en7gNVnh878",
ExportName = "sceSaveDataDialogIsReadyToDisplay", ExportName = "sceSaveDataDialogIsReadyToDisplay",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1); public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(1);
[SysAbiExport( [SysAbiExport(
Nid = "yEiJ-qqr6Cg", Nid = "yEiJ-qqr6Cg",
@@ -100,12 +99,12 @@ public static class SaveDataDialogExports
var resultAddress = ctx[CpuRegister.Rdi]; var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0) if (resultAddress == 0)
{ {
return SetReturn(ctx, ErrorArgNull); return ctx.SetReturn(ErrorArgNull);
} }
if (Volatile.Read(ref _status) != StatusFinished) if (Volatile.Read(ref _status) != StatusFinished)
{ {
return SetReturn(ctx, ErrorNotFinished); return ctx.SetReturn(ErrorNotFinished);
} }
Span<byte> result = stackalloc byte[ResultSize]; Span<byte> result = stackalloc byte[ResultSize];
@@ -117,10 +116,10 @@ public static class SaveDataDialogExports
if (!ctx.Memory.TryWrite(resultAddress, result)) if (!ctx.Memory.TryWrite(resultAddress, result))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
return SetReturn(ctx, ErrorOk); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -132,10 +131,10 @@ public static class SaveDataDialogExports
{ {
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning) if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{ {
return SetReturn(ctx, ErrorNotRunning); return ctx.SetReturn(ErrorNotRunning);
} }
return SetReturn(ctx, ErrorOk); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -147,12 +146,12 @@ public static class SaveDataDialogExports
{ {
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone) if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{ {
return SetReturn(ctx, ErrorNotInitialized); return ctx.SetReturn(ErrorNotInitialized);
} }
_lastMode = 0; _lastMode = 0;
_lastUserData = 0; _lastUserData = 0;
return SetReturn(ctx, ErrorOk); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -160,14 +159,14 @@ public static class SaveDataDialogExports
ExportName = "sceSaveDataDialogProgressBarInc", ExportName = "sceSaveDataDialogProgressBarInc",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk); public static int SaveDataDialogProgressBarInc(CpuContext ctx) => ctx.SetReturn(ErrorOk);
[SysAbiExport( [SysAbiExport(
Nid = "hay1CfTmLyA", Nid = "hay1CfTmLyA",
ExportName = "sceSaveDataDialogProgressBarSetValue", ExportName = "sceSaveDataDialogProgressBarSetValue",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk); public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => ctx.SetReturn(ErrorOk);
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value) private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{ {
@@ -182,12 +181,6 @@ public static class SaveDataDialogExports
return true; return true;
} }
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveDataDialog(string message) private static void TraceSaveDataDialog(string message)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal)) if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
+29 -48
View File
@@ -50,15 +50,15 @@ public static class SaveDataExports
try try
{ {
Directory.CreateDirectory(ResolveSaveDataRoot()); Directory.CreateDirectory(ResolveSaveDataRoot());
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
catch (IOException) catch (IOException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
} }
@@ -73,18 +73,18 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi]; var resultAddress = ctx[CpuRegister.Rsi];
if (condAddress == 0 || resultAddress == 0) if (condAddress == 0 || resultAddress == 0)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
if (!TryReadSearchCond(ctx, condAddress, out var cond) || if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
!TryReadSearchResult(ctx, resultAddress, out var result)) !TryReadSearchResult(ctx, resultAddress, out var result))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent) if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
try try
@@ -96,7 +96,7 @@ public static class SaveDataExports
} }
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId)) else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
var root = ResolveTitleSaveRoot(cond.UserId, titleId); var root = ResolveTitleSaveRoot(cond.UserId, titleId);
@@ -111,18 +111,18 @@ public static class SaveDataExports
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) || if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum))) !ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (setNum == 0) if (setNum == 0)
{ {
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'"); TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
if (result.DirNamesAddress == 0) if (result.DirNamesAddress == 0)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
for (var i = 0; i < setNum; i++) for (var i = 0; i < setNum; i++)
@@ -138,20 +138,20 @@ public static class SaveDataExports
(result.InfosAddress != 0 && (result.InfosAddress != 0 &&
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry))) !TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
} }
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'"); TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
catch (IOException) catch (IOException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
} }
@@ -166,10 +166,10 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi]; var resultAddress = ctx[CpuRegister.Rsi];
if (mountAddress == 0 || resultAddress == 0) if (mountAddress == 0 || resultAddress == 0)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
if (!TryReadInt32(ctx, mountAddress, out var userId) || if (!ctx.TryReadInt32(mountAddress, out var userId) ||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) || !ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) || !ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) || !ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
@@ -179,12 +179,12 @@ public static class SaveDataExports
dirNameAddress == 0 || dirNameAddress == 0 ||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName)) !TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (userId < 0 || string.IsNullOrWhiteSpace(dirName)) if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
try try
@@ -199,12 +199,12 @@ public static class SaveDataExports
if (!existed && !create && !createIfMissing) if (!existed && !create && !createIfMissing)
{ {
return SetReturn(ctx, OrbisSaveDataErrorNotFound); return ctx.SetReturn(OrbisSaveDataErrorNotFound);
} }
if (existed && create) if (existed && create)
{ {
return SetReturn(ctx, OrbisSaveDataErrorExists); return ctx.SetReturn(OrbisSaveDataErrorExists);
} }
if (!existed) if (!existed)
@@ -221,26 +221,26 @@ public static class SaveDataExports
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u); BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
if (!ctx.Memory.TryWrite(resultAddress, result)) if (!ctx.Memory.TryWrite(resultAddress, result))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceSaveData( TraceSaveData(
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " + $"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " + $"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
$"mount_point={mountPoint} created={!existed} root='{savePath}'"); $"mount_point={mountPoint} created={!existed} root='{savePath}'");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
catch (IOException) catch (IOException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorInternal); return ctx.SetReturn(OrbisSaveDataErrorInternal);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
} }
@@ -258,26 +258,26 @@ public static class SaveDataExports
if (resourceAddress == 0) if (resourceAddress == 0)
{ {
return SetReturn(ctx, OrbisSaveDataErrorParameter); return ctx.SetReturn(OrbisSaveDataErrorParameter);
} }
var id = (uint)Interlocked.Increment(ref _nextTransactionResource); var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
if (!ctx.TryWriteUInt32(resourceAddress, id)) if (!ctx.TryWriteUInt32(resourceAddress, id))
{ {
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceSaveData( TraceSaveData(
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}"); $"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
return SetReturn(ctx, 0); return ctx.SetReturn(0);
} }
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond) private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
{ {
cond = default; cond = default;
if (!TryReadInt32(ctx, address, out var userId) || if (!ctx.TryReadInt32(address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) || !ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) || !ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) || !ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
@@ -505,25 +505,6 @@ public static class SaveDataExports
} }
} }
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveData(string message) private static void TraceSaveData(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
+5 -12
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Text; using System.Text;
using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
namespace SharpEmu.Libs.Share; namespace SharpEmu.Libs.Share;
@@ -26,13 +25,13 @@ public static class ShareExports
var affinityMask = ctx[CpuRegister.Rdx]; var affinityMask = ctx[CpuRegister.Rdx];
if (memorySize == 0) if (memorySize == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
Interlocked.Exchange(ref _initialized, 1); Interlocked.Exchange(ref _initialized, 1);
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}"); TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
[SysAbiExport( [SysAbiExport(
@@ -45,12 +44,12 @@ public static class ShareExports
var contentParamAddress = ctx[CpuRegister.Rdi]; var contentParamAddress = ctx[CpuRegister.Rdi];
if (contentParamAddress == 0) if (contentParamAddress == 0)
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam)) if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
{ {
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
_contentParam = contentParam; _contentParam = contentParam;
@@ -60,13 +59,7 @@ public static class ShareExports
} }
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'"); TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
} }
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value) private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
@@ -24,7 +24,7 @@ public static class SystemServiceExports
var valueAddress = ctx[CpuRegister.Rsi]; var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0) if (valueAddress == 0)
{ {
return SetReturn(ctx, OrbisSystemServiceErrorParameter); return ctx.SetReturn(OrbisSystemServiceErrorParameter);
} }
var value = parameterId switch var value = parameterId switch
@@ -37,8 +37,8 @@ public static class SystemServiceExports
Span<byte> valueBytes = stackalloc byte[sizeof(int)]; Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value); BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
return ctx.Memory.TryWrite(valueAddress, valueBytes) return ctx.Memory.TryWrite(valueAddress, valueBytes)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -51,7 +51,7 @@ public static class SystemServiceExports
var statusAddress = ctx[CpuRegister.Rdi]; var statusAddress = ctx[CpuRegister.Rdi];
if (statusAddress == 0) if (statusAddress == 0)
{ {
return SetReturn(ctx, OrbisSystemServiceErrorParameter); return ctx.SetReturn(OrbisSystemServiceErrorParameter);
} }
Span<byte> status = stackalloc byte[SystemServiceStatusSize]; Span<byte> status = stackalloc byte[SystemServiceStatusSize];
@@ -60,8 +60,8 @@ public static class SystemServiceExports
status[0x06] = 1; status[0x06] = 1;
return ctx.Memory.TryWrite(statusAddress, status) return ctx.Memory.TryWrite(statusAddress, status)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -74,7 +74,7 @@ public static class SystemServiceExports
var infoAddress = ctx[CpuRegister.Rdi]; var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0) if (infoAddress == 0)
{ {
return SetReturn(ctx, OrbisSystemServiceErrorParameter); return ctx.SetReturn(OrbisSystemServiceErrorParameter);
} }
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize]; Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
@@ -82,8 +82,8 @@ public static class SystemServiceExports
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f); BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
return ctx.Memory.TryWrite(infoAddress, info) return ctx.Memory.TryWrite(infoAddress, info)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -94,12 +94,6 @@ public static class SystemServiceExports
public static int SystemServiceHideSplashScreen(CpuContext ctx) public static int SystemServiceHideSplashScreen(CpuContext ctx)
{ {
VulkanVideoPresenter.HideSplashScreen(); VulkanVideoPresenter.HideSplashScreen();
return SetReturn(ctx, 0); return ctx.SetReturn(0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
} }
@@ -4,7 +4,6 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService; namespace SharpEmu.Libs.UserService;
@@ -40,12 +39,12 @@ public static class UserServiceExports
var userIdAddress = ctx[CpuRegister.Rdi]; var userIdAddress = ctx[CpuRegister.Rdi];
if (userIdAddress == 0) if (userIdAddress == 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
} }
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId) return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -58,7 +57,7 @@ public static class UserServiceExports
var userIdListAddress = ctx[CpuRegister.Rdi]; var userIdListAddress = ctx[CpuRegister.Rdi];
if (userIdListAddress == 0) if (userIdListAddress == 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
} }
Span<byte> userIds = stackalloc byte[sizeof(int) * 4]; Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
@@ -67,8 +66,8 @@ public static class UserServiceExports
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId); BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId); BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
return ctx.Memory.TryWrite(userIdListAddress, userIds) return ctx.Memory.TryWrite(userIdListAddress, userIds)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -81,20 +80,20 @@ public static class UserServiceExports
var eventAddress = ctx[CpuRegister.Rdi]; var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0) if (eventAddress == 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
} }
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0) if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorNoEvent); return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
} }
Span<byte> payload = stackalloc byte[sizeof(int) * 2]; Span<byte> payload = stackalloc byte[sizeof(int) * 2];
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0); BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId); BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
return ctx.Memory.TryWrite(eventAddress, payload) return ctx.Memory.TryWrite(eventAddress, payload)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -109,25 +108,25 @@ public static class UserServiceExports
var capacity = ctx[CpuRegister.Rdx]; var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId) if (userId != PrimaryUserId)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter); return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
} }
if (nameAddress == 0) if (nameAddress == 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
} }
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName); var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
if (capacity <= (ulong)nameBytes.Length) if (capacity <= (ulong)nameBytes.Length)
{ {
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort); return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
} }
Span<byte> output = stackalloc byte[nameBytes.Length + 1]; Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output); nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output) return ctx.Memory.TryWrite(nameAddress, output)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport( [SysAbiExport(
@@ -141,29 +140,16 @@ public static class UserServiceExports
var valueAddress = ctx[CpuRegister.Rsi]; var valueAddress = ctx[CpuRegister.Rsi];
if (parameterId != 1000) if (parameterId != 1000)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter); return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
} }
if (valueAddress == 0) if (valueAddress == 0)
{ {
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument); return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
} }
return TryWriteInt32(ctx, valueAddress, 0) return ctx.TryWriteInt32(valueAddress, 0)
? SetReturn(ctx, 0) ? ctx.SetReturn(0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
} }
} }
@@ -168,6 +168,7 @@ internal static unsafe class VulkanVideoPresenter
private static readonly object _gate = new(); private static readonly object _gate = new();
private static readonly Queue<object> _pendingGuestWork = new(); private static readonly Queue<object> _pendingGuestWork = new();
private static readonly Dictionary<ulong, uint> _availableGuestImages = new(); private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
private static readonly Dictionary<ulong, uint> _gpuGuestImages = new();
private static readonly HashSet<(ulong Address, uint Width, uint Height)> private static readonly HashSet<(ulong Address, uint Width, uint Height)>
_tracedGuestImageSubmissions = []; _tracedGuestImageSubmissions = [];
private static Thread? _thread; private static Thread? _thread;
@@ -600,7 +601,7 @@ internal static unsafe class VulkanVideoPresenter
return true; return true;
} }
internal static bool IsGuestImageAvailable( internal static bool IsGpuGuestImageAvailable(
ulong address, ulong address,
uint format, uint format,
uint numberType) uint numberType)
@@ -613,7 +614,7 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate) lock (_gate)
{ {
return _availableGuestImages.TryGetValue(address, out var availableFormat) && return _gpuGuestImages.TryGetValue(address, out var availableFormat) &&
availableFormat == guestFormat; availableFormat == guestFormat;
} }
} }
@@ -971,6 +972,8 @@ internal static unsafe class VulkanVideoPresenter
public VulkanGuestSampler SamplerState; public VulkanGuestSampler SamplerState;
public Sampler Sampler; public Sampler Sampler;
public GuestImageResource? GuestImage; public GuestImageResource? GuestImage;
public ulong CpuContentFingerprint;
public bool UpdatesCpuContent;
} }
private sealed class GlobalBufferResource private sealed class GlobalBufferResource
@@ -1009,6 +1012,8 @@ internal static unsafe class VulkanVideoPresenter
public Framebuffer Framebuffer; public Framebuffer Framebuffer;
public bool Initialized; public bool Initialized;
public bool InitialUploadPending; public bool InitialUploadPending;
public bool IsCpuBacked;
public ulong CpuContentFingerprint;
} }
private sealed record PendingGuestSubmission( private sealed record PendingGuestSubmission(
@@ -1248,7 +1253,7 @@ internal static unsafe class VulkanVideoPresenter
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu"); var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1"; var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
byte* validationLayerName = null; byte* validationLayerName = null;
try try
{ {
var applicationInfo = new ApplicationInfo var applicationInfo = new ApplicationInfo
@@ -1308,7 +1313,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
throw new InvalidOperationException("VK_KHR_surface is unavailable."); throw new InvalidOperationException("VK_KHR_surface is unavailable.");
} }
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils)) if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
{ {
_debugUtils = debugUtils; _debugUtils = debugUtils;
@@ -1378,7 +1383,7 @@ internal static unsafe class VulkanVideoPresenter
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger), Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
"vkCreateDebugUtilsMessengerEXT"); "vkCreateDebugUtilsMessengerEXT");
} }
private static unsafe uint DebugCallback( private static unsafe uint DebugCallback(
DebugUtilsMessageSeverityFlagsEXT severity, DebugUtilsMessageSeverityFlagsEXT severity,
DebugUtilsMessageTypeFlagsEXT type, DebugUtilsMessageTypeFlagsEXT type,
@@ -2838,6 +2843,11 @@ internal static unsafe class VulkanVideoPresenter
$"tile={texture.TileMode} format={vkFormat}"); $"tile={texture.TileMode} format={vkFormat}");
} }
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
{
return refresh;
}
return new TextureResource return new TextureResource
{ {
Address = texture.Address, Address = texture.Address,
@@ -2855,6 +2865,71 @@ internal static unsafe class VulkanVideoPresenter
return CreateTextureResource(texture); return CreateTextureResource(texture);
} }
private bool TryCreateCpuTextureRefreshResource(
VulkanGuestDrawTexture texture,
GuestImageResource guestImage,
ImageView view,
out TextureResource resource)
{
resource = default!;
if (!guestImage.IsCpuBacked ||
guestImage.Width != texture.Width ||
guestImage.Height != texture.Height ||
guestImage.MipLevels != 1 ||
texture.RgbaPixels.Length == 0)
{
return false;
}
var rowLength = texture.TileMode == 0
? Math.Max(texture.Pitch, texture.Width)
: texture.Width;
var expectedSize = GetTextureByteCount(texture.Format, rowLength, texture.Height);
if (expectedSize == 0 || expectedSize > int.MaxValue)
{
return false;
}
var pixels = texture.RgbaPixels.Length == (int)expectedSize
? texture.RgbaPixels
: CreateFallbackTexturePixels(texture.Format, rowLength, texture.Height, expectedSize);
var fingerprint = ComputeTextureContentFingerprint(pixels);
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
guestImage.CpuContentFingerprint == fingerprint)
{
return false;
}
var uploadPixels = texture.Format == 13
? ExpandRgb32Pixels(pixels)
: pixels;
var debugName = TextureDebugName(texture, guestImage.Format);
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
uploadPixels,
$"{debugName} refresh staging");
TraceVulkanShader(
$"vk.texture_refresh addr=0x{texture.Address:X16} " +
$"size={texture.Width}x{texture.Height} bytes={uploadPixels.Length}");
resource = new TextureResource
{
Address = texture.Address,
StagingBuffer = stagingBuffer,
StagingMemory = stagingMemory,
Image = guestImage.Image,
View = view,
Width = guestImage.Width,
Height = guestImage.Height,
RowLength = rowLength,
DstSelect = texture.DstSelect,
NeedsUpload = true,
SamplerState = texture.Sampler,
GuestImage = guestImage,
CpuContentFingerprint = fingerprint,
UpdatesCpuContent = true,
};
return true;
}
private static bool IsCompatibleGuestImageAlias( private static bool IsCompatibleGuestImageAlias(
VulkanGuestDrawTexture texture, VulkanGuestDrawTexture texture,
GuestImageResource guestImage) GuestImageResource guestImage)
@@ -3096,24 +3171,11 @@ internal static unsafe class VulkanVideoPresenter
var uploadPixels = texture.Format == 13 var uploadPixels = texture.Format == 13
? ExpandRgb32Pixels(pixels) ? ExpandRgb32Pixels(pixels)
: pixels; : pixels;
var uploadSize = (ulong)uploadPixels.Length; var contentFingerprint = ComputeTextureContentFingerprint(pixels);
var stagingBuffer = CreateBuffer( var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
uploadSize, uploadPixels,
BufferUsageFlags.TransferSrcBit, $"{TextureDebugName(texture, vkFormat)} staging");
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out var stagingMemory);
void* mapped;
Check(_vk.MapMemory(_device, stagingMemory, 0, uploadSize, 0, &mapped), "vkMapMemory(texture)");
fixed (byte* source = uploadPixels)
{
System.Buffer.MemoryCopy(
source,
mapped,
uploadPixels.Length,
uploadPixels.Length);
}
_vk.UnmapMemory(_device, stagingMemory);
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat); var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
var imageInfo = new ImageCreateInfo var imageInfo = new ImageCreateInfo
@@ -3163,7 +3225,6 @@ internal static unsafe class VulkanVideoPresenter
}; };
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)"); Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
var debugName = TextureDebugName(texture, vkFormat); var debugName = TextureDebugName(texture, vkFormat);
SetDebugName(ObjectType.Buffer, stagingBuffer.Handle, $"{debugName} staging");
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image"); SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view"); SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
var resource = new TextureResource var resource = new TextureResource
@@ -3181,6 +3242,8 @@ internal static unsafe class VulkanVideoPresenter
NeedsUpload = true, NeedsUpload = true,
OwnsStorage = true, OwnsStorage = true,
SamplerState = texture.Sampler, SamplerState = texture.Sampler,
CpuContentFingerprint = contentFingerprint,
UpdatesCpuContent = texture.Address != 0,
}; };
if (texture.Address != 0 && if (texture.Address != 0 &&
@@ -3197,6 +3260,8 @@ internal static unsafe class VulkanVideoPresenter
Memory = imageMemory, Memory = imageMemory,
View = view, View = view,
InitialUploadPending = true, InitialUploadPending = true,
IsCpuBacked = true,
CpuContentFingerprint = contentFingerprint,
}; };
_guestImages.Add(texture.Address, guestImage); _guestImages.Add(texture.Address, guestImage);
resource.OwnsStorage = false; resource.OwnsStorage = false;
@@ -3209,6 +3274,7 @@ internal static unsafe class VulkanVideoPresenter
if (guestFormat != 0) if (guestFormat != 0)
{ {
_availableGuestImages[texture.Address] = guestFormat; _availableGuestImages[texture.Address] = guestFormat;
_gpuGuestImages.Remove(texture.Address);
} }
} }
} }
@@ -3216,6 +3282,41 @@ internal static unsafe class VulkanVideoPresenter
return resource; return resource;
} }
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(
byte[] pixels,
string debugName)
{
var size = (ulong)pixels.Length;
var buffer = CreateBuffer(
size,
BufferUsageFlags.TransferSrcBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out var memory);
void* mapped;
Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(texture)");
fixed (byte* source = pixels)
{
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
}
_vk.UnmapMemory(_device, memory);
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
return (buffer, memory);
}
private static ulong ComputeTextureContentFingerprint(ReadOnlySpan<byte> pixels)
{
const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211;
var hash = offsetBasis;
foreach (var value in pixels)
{
hash ^= value;
hash *= prime;
}
return hash;
}
private void DumpTextureUpload( private void DumpTextureUpload(
VulkanGuestDrawTexture texture, VulkanGuestDrawTexture texture,
byte[] pixels, byte[] pixels,
@@ -4267,12 +4368,13 @@ internal static unsafe class VulkanVideoPresenter
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit); RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit); RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
var toColorAttachment = new ImageMemoryBarrier var toColorAttachment = new ImageMemoryBarrier
{ {
SType = StructureType.ImageMemoryBarrier, SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = target.Initialized ? AccessFlags.ShaderReadBit : 0, SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
DstAccessMask = AccessFlags.ColorAttachmentWriteBit, DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
OldLayout = target.Initialized OldLayout = targetHasPriorContents
? ImageLayout.ShaderReadOnlyOptimal ? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined, : ImageLayout.Undefined,
NewLayout = ImageLayout.ColorAttachmentOptimal, NewLayout = ImageLayout.ColorAttachmentOptimal,
@@ -4283,8 +4385,8 @@ internal static unsafe class VulkanVideoPresenter
}; };
_vk.CmdPipelineBarrier( _vk.CmdPipelineBarrier(
_commandBuffer, _commandBuffer,
target.Initialized targetHasPriorContents
? PipelineStageFlags.FragmentShaderBit ? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit, : PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.ColorAttachmentOutputBit, PipelineStageFlags.ColorAttachmentOutputBit,
0, 0,
@@ -4337,12 +4439,15 @@ internal static unsafe class VulkanVideoPresenter
MarkSampledImagesInitialized(resources); MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false); MarkStorageImagesInitialized(resources, traceContents: false);
var guestTextureFormat = GetGuestTextureFormat(target.Format); var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
work.Target.Format,
work.Target.NumberType);
if (work.PublishTarget && guestTextureFormat != 0) if (work.PublishTarget && guestTextureFormat != 0)
{ {
lock (_gate) lock (_gate)
{ {
_availableGuestImages[target.Address] = guestTextureFormat; _availableGuestImages[target.Address] = guestTextureFormat;
_gpuGuestImages[target.Address] = guestTextureFormat;
} }
} }
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address)) if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
@@ -4384,6 +4489,7 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate) lock (_gate)
{ {
_availableGuestImages.Remove(work.Target.Address); _availableGuestImages.Remove(work.Target.Address);
_gpuGuestImages.Remove(work.Target.Address);
} }
} }
@@ -4423,6 +4529,8 @@ internal static unsafe class VulkanVideoPresenter
existing.MipLevels == mipLevels && existing.MipLevels == mipLevels &&
existing.Format == format) existing.Format == format)
{ {
existing.IsCpuBacked = false;
existing.CpuContentFingerprint = 0;
if (existing.RenderPass.Handle == 0) if (existing.RenderPass.Handle == 0)
{ {
var attachmentView = existing.MipViews.Length > 0 var attachmentView = existing.MipViews.Length > 0
@@ -4448,6 +4556,7 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate) lock (_gate)
{ {
_availableGuestImages.Remove(target.Address); _availableGuestImages.Remove(target.Address);
_gpuGuestImages.Remove(target.Address);
} }
} }
@@ -5386,11 +5495,18 @@ internal static unsafe class VulkanVideoPresenter
continue; continue;
} }
var hasPriorContents = texture.GuestImage is { } guestImage &&
(guestImage.Initialized || guestImage.InitialUploadPending);
var toTransfer = new ImageMemoryBarrier var toTransfer = new ImageMemoryBarrier
{ {
SType = StructureType.ImageMemoryBarrier, SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = hasPriorContents
? AccessFlags.ShaderReadBit
: 0,
DstAccessMask = AccessFlags.TransferWriteBit, DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = ImageLayout.Undefined, OldLayout = hasPriorContents
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal, NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored, DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
@@ -5399,7 +5515,9 @@ internal static unsafe class VulkanVideoPresenter
}; };
_vk.CmdPipelineBarrier( _vk.CmdPipelineBarrier(
_commandBuffer, _commandBuffer,
PipelineStageFlags.TopOfPipeBit, hasPriorContents
? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit,
0, 0,
0, 0,
@@ -5569,6 +5687,7 @@ internal static unsafe class VulkanVideoPresenter
if (format != 0) if (format != 0)
{ {
_availableGuestImages[texture.Address] = format; _availableGuestImages[texture.Address] = format;
_gpuGuestImages[texture.Address] = format;
} }
if (traceContents && if (traceContents &&
@@ -5608,6 +5727,10 @@ internal static unsafe class VulkanVideoPresenter
guestImage.Initialized = true; guestImage.Initialized = true;
guestImage.InitialUploadPending = false; guestImage.InitialUploadPending = false;
if (texture.UpdatesCpuContent)
{
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
}
} }
} }
} }
@@ -6379,6 +6502,7 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate) lock (_gate)
{ {
_availableGuestImages.Clear(); _availableGuestImages.Clear();
_gpuGuestImages.Clear();
} }
DestroySwapchainResources(); DestroySwapchainResources();
if (_device.Handle != 0) if (_device.Handle != 0)
+68
View File
@@ -0,0 +1,68 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Logging;
/// <summary>
/// Dispatches each <see cref="LogEntry"/> to every child sink.
/// Exceptions thrown by a child are swallowed so one failing sink
/// (e.g. a closed file) cannot silence the others.
/// </summary>
public sealed class CompositeLogSink : ISharpEmuLogSink, IDisposable
{
private readonly ISharpEmuLogSink[] _sinks;
private bool _disposed;
public CompositeLogSink(params ISharpEmuLogSink[] sinks)
{
ArgumentNullException.ThrowIfNull(sinks);
foreach (var sink in sinks)
{
ArgumentNullException.ThrowIfNull(sink, nameof(sinks));
}
_sinks = sinks;
}
public IReadOnlyList<ISharpEmuLogSink> Sinks => _sinks;
public void Write(in LogEntry entry)
{
foreach (var sink in _sinks)
{
try
{
sink.Write(in entry);
}
catch
{
// A broken sink must not prevent the remaining sinks from logging.
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
foreach (var sink in _sinks)
{
if (sink is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch
{
}
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO;
using System.Text;
namespace SharpEmu.Logging;
/// <summary>
/// Writes log entries to a file. Thread-safe via an internal lock.
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
/// </summary>
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
{
private readonly object _sync = new();
private readonly StreamWriter _writer;
private bool _disposed;
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var fileStream = new FileStream(
path,
append ? FileMode.Append : FileMode.Create,
FileAccess.Write,
FileShare.Read,
bufferSize: 4096,
FileOptions.SequentialScan);
_writer = new StreamWriter(fileStream, Encoding.UTF8)
{
AutoFlush = true
};
IncludeTimestamp = includeTimestamp;
}
public bool IncludeTimestamp { get; set; }
public void Write(in LogEntry entry)
{
lock (_sync)
{
if (_disposed)
{
return;
}
if (IncludeTimestamp)
{
_writer.Write('[');
_writer.Write(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"));
_writer.Write(']');
}
_writer.Write('[');
_writer.Write(ToLevelLabel(entry.Level));
_writer.Write(']');
_writer.Write('[');
_writer.Write(entry.Category);
_writer.Write(']');
_writer.Write(' ');
_writer.Write(entry.SourceFileName);
if (entry.SourceLine > 0)
{
_writer.Write(':');
_writer.Write(entry.SourceLine);
}
_writer.Write(' ');
_writer.WriteLine(entry.Message);
if (entry.Exception is not null)
{
_writer.WriteLine(entry.Exception);
}
}
}
public void Dispose()
{
lock (_sync)
{
if (_disposed)
{
return;
}
_disposed = true;
_writer.Flush();
_writer.Dispose();
}
}
private static string ToLevelLabel(LogLevel level) => level switch
{
LogLevel.Trace => "TRACE",
LogLevel.Debug => "DEBUG",
LogLevel.Info => "INFO",
LogLevel.Warning => "WARNING",
LogLevel.Error => "ERROR",
LogLevel.Critical => "CRITICAL",
_ => "LOG",
};
}
+57 -4
View File
@@ -12,9 +12,7 @@ public static class SharpEmuLog
new(StringComparer.Ordinal); new(StringComparer.Ordinal);
private static readonly object ConfigurationSync = new(); private static readonly object ConfigurationSync = new();
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment(); private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
private static ISharpEmuLogSink _sink = new ConsoleLogSink( private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
useColors: ResolveColorEnabledFromEnvironment(),
includeTimestamp: false);
public static LogLevel MinimumLevel public static LogLevel MinimumLevel
{ {
@@ -37,11 +35,26 @@ public static class SharpEmuLog
ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(value);
lock (ConfigurationSync) lock (ConfigurationSync)
{ {
if (ReferenceEquals(_sink, value))
{
return;
}
if (_sink is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch
{
}
}
_sink = value; _sink = value;
} }
} }
} }
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null) public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
{ {
if (minimumLevel.HasValue) if (minimumLevel.HasValue)
@@ -55,6 +68,22 @@ public static class SharpEmuLog
} }
} }
/// <summary>
/// Disposes the active sink if it implements <see cref="IDisposable"/>.
/// Call at shutdown to flush file buffers. Logging after this call
/// continues to work for non-disposable sinks (e.g. <see cref="ConsoleLogSink"/>).
/// </summary>
public static void Shutdown()
{
lock (ConfigurationSync)
{
if (_sink is IDisposable disposable)
{
disposable.Dispose();
}
}
}
public static SharpEmuLogger For(string category) public static SharpEmuLogger For(string category)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(category); ArgumentException.ThrowIfNullOrWhiteSpace(category);
@@ -146,6 +175,30 @@ public static class SharpEmuLog
return !IsTrueLike(raw); return !IsTrueLike(raw);
} }
private static ISharpEmuLogSink ResolveSinkFromEnvironment()
{
var consoleSink = new ConsoleLogSink(
useColors: ResolveColorEnabledFromEnvironment(),
includeTimestamp: false);
var logFilePath = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FILE");
if (!string.IsNullOrWhiteSpace(logFilePath))
{
try
{
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
return new CompositeLogSink(consoleSink, fileSink);
}
catch (Exception ex)
{
// Bootstrapping — the logging system is not yet active, so stderr is the only channel.
Console.Error.WriteLine($"[SHARPEMU_LOG] Failed to open log file '{logFilePath}': {ex.Message}");
}
}
return consoleSink;
}
private static bool IsTrueLike(string? text) private static bool IsTrueLike(string? text)
{ {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))