mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 347e33f3c9 | |||
| 48a694e509 | |||
| 165927882b | |||
| d9d1aeaef9 | |||
| 57e737b5d7 | |||
| a78b02f22b | |||
| 70ec2928ea | |||
| c618c116ba | |||
| 29021b5a71 | |||
| c0fd6a80e8 |
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 120 KiB |
@@ -36,6 +36,18 @@ internal static partial class Program
|
||||
|
||||
[STAThread]
|
||||
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);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
@@ -66,7 +78,7 @@ internal static partial class Program
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
|
||||
if (!File.Exists(ebootPath))
|
||||
{
|
||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||
|
||||
@@ -484,4 +484,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
|
||||
@@ -358,12 +358,21 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
else if (importStubEntry.Export is { } mismatchedExport)
|
||||
{
|
||||
dispatchResolved = true;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
||||
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1679,10 +1679,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[num++] = 65;
|
||||
ptr2[num++] = 95;
|
||||
ptr2[num++] = 195;
|
||||
uint num2 = default(uint);
|
||||
VirtualProtect(ptr, 192u, 32u, &num2);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||
return (nint)ptr;
|
||||
uint num2 = default(uint);
|
||||
if (!VirtualProtect(ptr, 192u, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1763,7 +1767,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
tlsHandlerAddress[num++] = 195;
|
||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||
uint num2 = default(uint);
|
||||
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
|
||||
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
return;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
}
|
||||
@@ -1784,7 +1792,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[i] = 144;
|
||||
}
|
||||
uint num = default(uint);
|
||||
VirtualProtect(ptr, 4096u, 32u, &num);
|
||||
if (!VirtualProtect(ptr, 4096u, 32u, &num))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1831,7 +1843,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1923,7 +1939,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -2254,7 +2274,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr[num2++] = 144;
|
||||
}
|
||||
uint flNewProtect = default(uint);
|
||||
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
|
||||
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
||||
return num;
|
||||
}
|
||||
@@ -3591,7 +3615,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
@@ -3728,7 +3756,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
@@ -3988,7 +4020,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return false;
|
||||
}
|
||||
uint num5 = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &num5);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
|
||||
{
|
||||
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
if (_hostRspSlotStorage != 0)
|
||||
{
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
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 object _guestAllocationGate = 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);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
private static extern void* GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
{
|
||||
@@ -451,7 +458,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +706,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,7 +1055,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] {message}");
|
||||
Log.Debug(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -11,6 +11,7 @@ using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -20,6 +21,8 @@ namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -128,7 +131,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
@@ -140,7 +143,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
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 activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -163,7 +166,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -171,8 +174,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -182,8 +185,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -434,8 +437,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -446,8 +449,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
_cpuExecutionOptions);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -468,8 +471,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -508,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -577,7 +580,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
.ToArray();
|
||||
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)
|
||||
@@ -585,8 +588,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
@@ -621,18 +624,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
loadedModules++;
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
@@ -652,8 +655,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,8 +688,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -698,8 +701,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -708,8 +711,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -745,8 +748,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -855,8 +858,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
image.EntryPoint,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
Log.Info(
|
||||
$"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)
|
||||
|
||||
@@ -11,7 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Application.Resources>
|
||||
<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="CardBrush" Color="#141924" />
|
||||
<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="DangerHoverBrush" Color="#F2555A" />
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
||||
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
@@ -38,7 +45,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
</Style>
|
||||
|
||||
@@ -61,6 +68,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.accent">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
@@ -98,6 +109,41 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</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">
|
||||
<Setter Property="Background" Value="#0B0E14" />
|
||||
<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" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.library ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Margin" Value="4,2" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<!-- Cover-art library grid -->
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<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>
|
||||
</Application.Styles>
|
||||
|
||||
|
||||
@@ -1,13 +1,147 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
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
|
||||
? $"{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)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ public sealed class GuiSettings
|
||||
|
||||
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 int ImportTraceLimit { get; set; }
|
||||
|
||||
@@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.MainWindow"
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="800"
|
||||
MinWidth="980" MinHeight="620"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
@@ -18,10 +18,33 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<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 -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Classes="pill">
|
||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
@@ -30,119 +53,204 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Grid>
|
||||
|
||||
<!-- 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 -->
|
||||
<Border Grid.Column="0" Classes="card" Padding="0">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
|
||||
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill">
|
||||
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<!-- Library toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||
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>
|
||||
|
||||
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
|
||||
|
||||
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="4,4" Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
|
||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" />
|
||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
|
||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Right column -->
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
|
||||
<!-- Launch bar -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected game / launch -->
|
||||
<Border Grid.Row="0" Classes="card">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
|
||||
<!-- Selected game cover thumbnail -->
|
||||
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
||||
VerticalAlignment="Center">
|
||||
<Panel x:Name="SelectedCoverPanel">
|
||||
<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."
|
||||
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
</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="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch options -->
|
||||
<Border Grid.Row="2" Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
|
||||
<WrapPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Console -->
|
||||
<Border Grid.Row="4" Classes="card" Padding="0">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
Padding="10,4" />
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,10,10">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
<!-- Launch options (collapsible) -->
|
||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
|
||||
@@ -5,11 +5,17 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Diagnostics;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using SharpEmu.Libs.Pad;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
@@ -35,6 +41,16 @@ public partial class MainWindow : Window
|
||||
private string? _emulatorExePath;
|
||||
private bool _isRunning;
|
||||
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()
|
||||
{
|
||||
@@ -59,15 +75,142 @@ public partial class MainWindow : Window
|
||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||
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();
|
||||
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()
|
||||
@@ -89,6 +232,7 @@ public partial class MainWindow : Window
|
||||
ReadControlsIntoSettings();
|
||||
_settings.Save();
|
||||
_consoleFlushTimer.Stop();
|
||||
_gamepadTimer.Stop();
|
||||
_emulator?.Dispose();
|
||||
}
|
||||
|
||||
@@ -188,9 +332,21 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_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();
|
||||
}
|
||||
|
||||
@@ -200,19 +356,118 @@ public partial class MainWindow : Window
|
||||
private async Task RescanLibraryAsync()
|
||||
{
|
||||
var folders = _settings.GameFolders.ToArray();
|
||||
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
||||
StatusBarRight.Text = "Scanning library…";
|
||||
|
||||
var games = await Task.Run(() => ScanFolders(folders));
|
||||
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
||||
|
||||
_allGames.Clear();
|
||||
_allGames.AddRange(games);
|
||||
RefreshVisibleGames();
|
||||
LoadGameDetailsInBackground(games);
|
||||
StatusBarRight.Text = folders.Length == 0
|
||||
? "Add a game folder to populate the library."
|
||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||
}
|
||||
|
||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders)
|
||||
/// <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 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))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(file);
|
||||
if (!seen.Add(fullPath))
|
||||
if (!seen.Add(fullPath) || excludedPaths.Contains(fullPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -250,7 +505,9 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ebootPath);
|
||||
@@ -339,26 +646,125 @@ public partial class MainWindow : Window
|
||||
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()
|
||||
{
|
||||
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||
var selected = GameList.SelectedItem as GameEntry;
|
||||
var selectedPath = (GameList.SelectedItem as GameEntry)?.Path;
|
||||
|
||||
_visibleGames.Clear();
|
||||
foreach (var game in _allGames)
|
||||
{
|
||||
if (query.Length == 0 ||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
GameCountText.Text = _visibleGames.Count.ToString();
|
||||
if (selected is not null && _visibleGames.Contains(selected))
|
||||
GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
|
||||
|
||||
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();
|
||||
@@ -370,16 +776,59 @@ public partial class MainWindow : Window
|
||||
{
|
||||
SelectedGameTitle.Text = game.Name;
|
||||
SelectedGamePath.Text = game.Path;
|
||||
SelectedCoverPanel.DataContext = game;
|
||||
_ = UpdateBackdropAsync(game);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedGameTitle.Text = "No game selected";
|
||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
||||
SelectedCoverPanel.DataContext = null;
|
||||
_ = UpdateBackdropAsync(null);
|
||||
}
|
||||
|
||||
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 ----
|
||||
|
||||
private async Task OpenFileAsync()
|
||||
@@ -448,6 +897,7 @@ public partial class MainWindow : Window
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
_consoleLines.Clear();
|
||||
ConsoleToggle.IsChecked = true;
|
||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
|
||||
@@ -21,8 +21,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</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>
|
||||
|
||||
@@ -138,6 +138,51 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
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)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
@@ -158,6 +203,13 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -105,6 +106,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context.ClearRaxWriteFlag();
|
||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ public static class AgcExports
|
||||
private const uint SpiShaderPgmHiEs = 0xC9;
|
||||
private const uint SpiShaderPgmLoLs = 0x148;
|
||||
private const uint SpiShaderPgmHiLs = 0x149;
|
||||
private const uint SpiShaderPgmLoGs = 0x8A;
|
||||
private const uint SpiShaderPgmHiGs = 0x8B;
|
||||
private const uint SpiPsInputEna = 0x1B3;
|
||||
private const uint SpiPsInputAddr = 0x1B4;
|
||||
private const uint ComputePgmLo = 0x20C;
|
||||
@@ -115,6 +117,7 @@ public static class AgcExports
|
||||
private const uint RegisterDefaultsVersion7 = 7;
|
||||
private const uint RegisterDefaultsVersion8 = 8;
|
||||
private const uint RegisterDefaultsVersion10 = 10;
|
||||
private const uint RegisterDefaultsVersion13 = 13;
|
||||
private const int RegisterDefaultsSize = 0x40;
|
||||
private const int RegisterDefaultBlockSize = 16 * 8;
|
||||
|
||||
@@ -139,6 +142,7 @@ public static class AgcExports
|
||||
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
||||
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
||||
private static readonly object _submitTraceGate = new();
|
||||
private static readonly object _textureHashTraceGate = 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)> _tracedShaderDecodePairs = new();
|
||||
@@ -146,6 +150,7 @@ public static class AgcExports
|
||||
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<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 Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
@@ -165,6 +170,10 @@ public static class AgcExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
||||
"1",
|
||||
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 _dcbWaitRegMemTraceCount;
|
||||
private static long _createShaderTraceCount;
|
||||
@@ -404,11 +413,11 @@ public static class AgcExports
|
||||
var version = (uint)ctx[CpuRegister.Rsi];
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -439,19 +448,19 @@ public static class AgcExports
|
||||
var codeAddress = ctx[CpuRegister.Rdx];
|
||||
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) ||
|
||||
!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)
|
||||
{
|
||||
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) ||
|
||||
@@ -462,12 +471,12 @@ public static class AgcExports
|
||||
!RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) ||
|
||||
!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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userDataAddress != 0 &&
|
||||
@@ -477,18 +486,18 @@ public static class AgcExports
|
||||
!RelocatePointerField(ctx, userDataAddress + 0x18) ||
|
||||
!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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (destinationAddress != 0 &&
|
||||
!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (_submitTraceGate)
|
||||
@@ -564,14 +573,14 @@ public static class AgcExports
|
||||
|
||||
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) ||
|
||||
!ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
||||
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) ||
|
||||
@@ -581,7 +590,7 @@ public static class AgcExports
|
||||
!ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) ||
|
||||
!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}");
|
||||
@@ -602,13 +611,13 @@ public static class AgcExports
|
||||
|
||||
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) ||
|
||||
!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;
|
||||
@@ -616,7 +625,7 @@ public static class AgcExports
|
||||
(!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) ||
|
||||
!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++)
|
||||
@@ -638,7 +647,7 @@ public static class AgcExports
|
||||
if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) ||
|
||||
!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];
|
||||
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;
|
||||
@@ -667,7 +676,7 @@ public static class AgcExports
|
||||
{
|
||||
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
|
||||
@@ -677,7 +686,7 @@ public static class AgcExports
|
||||
|
||||
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))
|
||||
@@ -702,20 +711,20 @@ public static class AgcExports
|
||||
var dwordCount = (uint)ctx[CpuRegister.Rsi];
|
||||
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) ||
|
||||
!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++)
|
||||
{
|
||||
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 ||
|
||||
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)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1754,7 +1763,7 @@ public static class AgcExports
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
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
|
||||
@@ -1764,12 +1773,12 @@ public static class AgcExports
|
||||
: 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)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1785,12 +1794,12 @@ public static class AgcExports
|
||||
op != ItNop ||
|
||||
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)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1931,11 +1940,11 @@ public static class AgcExports
|
||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1952,11 +1961,11 @@ public static class AgcExports
|
||||
eventId,
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1971,7 +1980,7 @@ public static class AgcExports
|
||||
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
||||
!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;
|
||||
@@ -2011,7 +2020,7 @@ public static class AgcExports
|
||||
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
||||
!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;
|
||||
@@ -2057,16 +2066,16 @@ public static class AgcExports
|
||||
|
||||
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))
|
||||
{
|
||||
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");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private const uint DefaultAgcOwner = 1;
|
||||
@@ -2081,16 +2090,16 @@ public static class AgcExports
|
||||
|
||||
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))
|
||||
{
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -2110,7 +2119,7 @@ public static class AgcExports
|
||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -2125,7 +2134,7 @@ public static class AgcExports
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -3744,7 +3753,6 @@ public static class AgcExports
|
||||
var sourceWidth = descriptor.TileMode == 0
|
||||
? GetLinearTexturePitch(
|
||||
Math.Max(descriptor.Width, descriptor.Pitch),
|
||||
descriptor.Height,
|
||||
descriptor.Format)
|
||||
: descriptor.Width;
|
||||
var sourceByteCount = GetTextureByteCount(
|
||||
@@ -3761,7 +3769,7 @@ public static class AgcExports
|
||||
|
||||
if (!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
VulkanVideoPresenter.IsGuestImageAvailable(
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
@@ -3822,6 +3830,8 @@ public static class AgcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
TraceTextureHash(descriptor, source);
|
||||
|
||||
var nonZero = 0;
|
||||
for (var i = 0; i < source.Length; i++)
|
||||
{
|
||||
@@ -3881,6 +3891,34 @@ public static class AgcExports
|
||||
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) =>
|
||||
descriptor.Count >= 4
|
||||
? new VulkanGuestSampler(
|
||||
@@ -4524,23 +4562,17 @@ public static class AgcExports
|
||||
: 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);
|
||||
if (bytesPerTexel == 0 || height == 0)
|
||||
if (bytesPerTexel == 0)
|
||||
{
|
||||
return pitch;
|
||||
}
|
||||
|
||||
var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel);
|
||||
var alignedPitch = AlignUp(pitch, pitchAlignment);
|
||||
var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel);
|
||||
while ((alignedPitch * height) % sliceAlignment != 0)
|
||||
{
|
||||
alignedPitch += pitchAlignment;
|
||||
}
|
||||
|
||||
return checked((uint)alignedPitch);
|
||||
// GFX10 ADDR_SW_LINEAR aligns each row to 256 bytes.
|
||||
var pitchAlignment = Math.Max(1UL, 256UL / bytesPerTexel);
|
||||
return checked((uint)AlignUp(pitch, pitchAlignment));
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment) =>
|
||||
@@ -4830,9 +4862,13 @@ public static class AgcExports
|
||||
var type = (fields[3] >> 28) & 0xFu;
|
||||
var baseLevel = (fields[3] >> 12) & 0xFu;
|
||||
var lastLevel = (fields[3] >> 16) & 0xFu;
|
||||
var pitch = fields.Count >= 5
|
||||
? ((fields[4] >> 13) & 0x3FFFu) + 1
|
||||
: width;
|
||||
// The 128-bit RDNA2 2D resource derives pitch[12:0] from width;
|
||||
// the optional extension word only supplies pitch[13].
|
||||
var pitch = width;
|
||||
if (fields.Count >= 5)
|
||||
{
|
||||
pitch = ((((width - 1) & 0x1FFFu) | (((fields[4] >> 13) & 1u) << 13)) + 1);
|
||||
}
|
||||
var dstSelect = fields[3] & 0xFFFu;
|
||||
if (address == 0 || width == 0 || height == 0)
|
||||
{
|
||||
@@ -5081,6 +5117,7 @@ public static class AgcExports
|
||||
0 => ComputePgmLo,
|
||||
1 => SpiShaderPgmLoPs,
|
||||
2 or 6 => SpiShaderPgmLoEs,
|
||||
4 => SpiShaderPgmLoGs,
|
||||
7 => SpiShaderPgmLoLs,
|
||||
_ => 0u,
|
||||
};
|
||||
@@ -5089,6 +5126,7 @@ public static class AgcExports
|
||||
0 => ComputePgmHi,
|
||||
1 => SpiShaderPgmHiPs,
|
||||
2 or 6 => SpiShaderPgmHiEs,
|
||||
4 => SpiShaderPgmHiGs,
|
||||
7 => SpiShaderPgmHiLs,
|
||||
_ => 0u,
|
||||
};
|
||||
@@ -5105,7 +5143,7 @@ public static class AgcExports
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -5113,13 +5151,13 @@ public static class AgcExports
|
||||
var registersAddress = ctx[CpuRegister.Rsi];
|
||||
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)) ||
|
||||
!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}");
|
||||
@@ -5133,13 +5171,13 @@ public static class AgcExports
|
||||
var registerCount = (uint)ctx[CpuRegister.Rsi];
|
||||
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) ||
|
||||
!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}");
|
||||
@@ -5252,7 +5290,8 @@ public static class AgcExports
|
||||
return version is
|
||||
RegisterDefaultsVersion7 or
|
||||
RegisterDefaultsVersion8 or
|
||||
RegisterDefaultsVersion10;
|
||||
RegisterDefaultsVersion10 or
|
||||
RegisterDefaultsVersion13;
|
||||
}
|
||||
|
||||
private static bool TryGetRegisterDefaultsAllocation(
|
||||
@@ -5382,12 +5421,6 @@ public static class AgcExports
|
||||
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) =>
|
||||
0xC0000000u |
|
||||
((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) |
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
@@ -36,9 +35,9 @@ internal static class Gen5ShaderMetadataReader
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
directResourceCount > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
@@ -47,8 +46,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt16(
|
||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||
out resourceCounts[resourceClass]) ||
|
||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||
@@ -67,7 +65,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -95,8 +93,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
for (uint slot = 0; slot < count; slot++)
|
||||
{
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt16(
|
||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||
out var sharp))
|
||||
{
|
||||
@@ -124,17 +121,4 @@ internal static class Gen5ShaderMetadataReader
|
||||
resources);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public static class AppContentExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
||||
}
|
||||
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)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
@@ -37,7 +36,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
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];
|
||||
@@ -48,8 +47,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -63,7 +62,7 @@ public static class AudioOut2Exports
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
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];
|
||||
@@ -74,8 +73,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -91,13 +90,13 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
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);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -105,7 +104,7 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JK2wamZPzwM",
|
||||
@@ -120,14 +119,14 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,7 +140,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
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);
|
||||
@@ -154,8 +153,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -168,7 +167,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
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];
|
||||
@@ -178,8 +177,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -187,14 +186,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IaZXJ9M79uo",
|
||||
ExportName = "sceAudioOut2UserDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -207,18 +206,12 @@ public static class AudioOut2Exports
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
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);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
@@ -84,7 +83,7 @@ public static class AudioOutExports
|
||||
ExportName = "sceAudioOutInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ekNvsT22rsY",
|
||||
@@ -101,7 +100,7 @@ public static class AudioOutExports
|
||||
if (bufferLength == 0 || frequency == 0 ||
|
||||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
WinMmAudioPort? backend = null;
|
||||
@@ -133,7 +132,7 @@ public static class AudioOutExports
|
||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||
$"{bufferLength} frames, backend={backendName}");
|
||||
return SetReturn(ctx, handle);
|
||||
return ctx.SetReturn(handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -146,11 +145,11 @@ public static class AudioOutExports
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
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();
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -164,12 +163,12 @@ public static class AudioOutExports
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
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)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||
@@ -178,7 +177,7 @@ public static class AudioOutExports
|
||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||
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 ||
|
||||
@@ -192,7 +191,7 @@ public static class AudioOutExports
|
||||
port.PaceSilence();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -208,19 +207,12 @@ public static class AudioOutExports
|
||||
public static int AudioOutSetVolume(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return SetReturn(
|
||||
ctx,
|
||||
return ctx.SetReturn(
|
||||
Ports.ContainsKey(handle)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool TryGetFormat(
|
||||
int rawFormat,
|
||||
out int channels,
|
||||
|
||||
@@ -46,8 +46,7 @@ public static class AvPlayerExports
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
return ctx.SetReturn(
|
||||
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
||||
? 0
|
||||
: InvalidParameters);
|
||||
@@ -63,15 +62,7 @@ public static class AvPlayerExports
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,17 +104,17 @@ public static class FiberExports
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (state != FiberStateIdle)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
@@ -267,7 +267,7 @@ public static class FiberExports
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
@@ -278,7 +278,7 @@ public static class FiberExports
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
@@ -286,7 +286,7 @@ public static class FiberExports
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -334,22 +334,22 @@ public static class FiberExports
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||
@@ -359,10 +359,10 @@ public static class FiberExports
|
||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorRange);
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!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))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||
@@ -510,14 +510,14 @@ public static class FiberExports
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
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}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return SetReturn(ctx, attachResult);
|
||||
return ctx.SetReturn(attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
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;
|
||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
@@ -620,7 +620,7 @@ public static class FiberExports
|
||||
previousState != FiberStateRun ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
@@ -639,7 +639,7 @@ public static class FiberExports
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
@@ -925,12 +925,6 @@ public static class FiberExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -51,17 +51,17 @@ public static class KernelEventFlagCompatExports
|
||||
optionAddress != 0 ||
|
||||
!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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -75,11 +75,11 @@ public static class KernelEventFlagCompatExports
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
{
|
||||
_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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -92,7 +92,7 @@ public static class KernelEventFlagCompatExports
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
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)
|
||||
@@ -101,7 +101,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -116,7 +116,7 @@ public static class KernelEventFlagCompatExports
|
||||
var returnRip = GetCurrentReturnRip();
|
||||
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)
|
||||
@@ -127,7 +127,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,7 +141,7 @@ public static class KernelEventFlagCompatExports
|
||||
var pattern = ctx[CpuRegister.Rsi];
|
||||
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)
|
||||
@@ -150,7 +150,7 @@ public static class KernelEventFlagCompatExports
|
||||
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(
|
||||
@@ -167,29 +167,29 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,18 +209,18 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.Enter(state.Gate);
|
||||
@@ -228,7 +228,7 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
return SetReturn(ctx, immediateWaitResult);
|
||||
return ctx.SetReturn(immediateWaitResult);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
@@ -236,7 +236,7 @@ public static class KernelEventFlagCompatExports
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
@@ -271,7 +271,7 @@ public static class KernelEventFlagCompatExports
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
@@ -296,7 +296,7 @@ public static class KernelEventFlagCompatExports
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
return ctx.SetReturn(pumpedWaitResult);
|
||||
}
|
||||
|
||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||
@@ -313,7 +313,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -333,7 +333,7 @@ public static class KernelEventFlagCompatExports
|
||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||
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)
|
||||
@@ -341,7 +341,7 @@ public static class KernelEventFlagCompatExports
|
||||
if (waiterCountAddress != 0 &&
|
||||
!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;
|
||||
@@ -350,7 +350,7 @@ public static class KernelEventFlagCompatExports
|
||||
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)
|
||||
@@ -452,13 +452,6 @@ public static class KernelEventFlagCompatExports
|
||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void TraceEventFlag(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelExports
|
||||
{
|
||||
private static int _nextFileDescriptor = 2;
|
||||
private static readonly object _cxaGate = new();
|
||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||
private static readonly object _coredumpGate = new();
|
||||
|
||||
@@ -2903,7 +2903,7 @@ public static class KernelMemoryCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -2942,7 +2942,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||
!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;
|
||||
}
|
||||
@@ -3800,7 +3800,7 @@ public static class KernelMemoryCompatExports
|
||||
var addr = argumentSource.NextGpArg();
|
||||
if (addr != 0)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, addr, sb.Length);
|
||||
_ = ctx.TryWriteInt32(addr, sb.Length);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4964,7 +4964,7 @@ public static class KernelMemoryCompatExports
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount))
|
||||
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -5801,13 +5801,6 @@ public static class KernelMemoryCompatExports
|
||||
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)
|
||||
{
|
||||
if (fd is 0 or 1 or 2)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -329,6 +327,12 @@ public static class KernelPthreadCompatExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||
[SysAbiExport(
|
||||
Nid = "27bAgiJmOh0",
|
||||
ExportName = "pthread_cond_timedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "m5-2bsNfv7s",
|
||||
@@ -383,42 +387,42 @@ public static class KernelPthreadCompatExports
|
||||
var initRoutine = ctx[CpuRegister.Rsi];
|
||||
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)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var gate = GetPthreadOnceGate(onceAddress);
|
||||
var shouldCall = false;
|
||||
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)
|
||||
{
|
||||
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 (!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;
|
||||
@@ -434,21 +438,21 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
@@ -456,7 +460,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
{
|
||||
var createdState = new PthreadMutexState
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -279,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -326,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -496,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -739,7 +737,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -762,7 +760,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1451,24 +1449,4 @@ public static class KernelPthreadExtendedCompatExports
|
||||
payload[^1] = 0;
|
||||
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.Libs.Fiber;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
@@ -259,8 +256,8 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
if (timezoneAddress != 0 &&
|
||||
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
||||
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -617,7 +614,7 @@ public static class KernelRuntimeCompatExports
|
||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||
{
|
||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||
return address != 0 && TryWriteInt32(ctx, address, value);
|
||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -827,7 +824,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -857,7 +854,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1058,7 +1055,7 @@ public static class KernelRuntimeCompatExports
|
||||
public static int KernelStopUnloadModule(CpuContext ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -1076,7 +1073,7 @@ public static class KernelRuntimeCompatExports
|
||||
{
|
||||
var modulePathAddress = ctx[CpuRegister.Rdi];
|
||||
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;
|
||||
}
|
||||
@@ -1233,8 +1230,8 @@ public static class KernelRuntimeCompatExports
|
||||
: 0;
|
||||
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||
|
||||
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1244,7 +1241,7 @@ public static class KernelRuntimeCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -1304,7 +1301,7 @@ public static class KernelRuntimeCompatExports
|
||||
_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;
|
||||
}
|
||||
@@ -1394,7 +1391,7 @@ public static class KernelRuntimeCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -1431,7 +1428,7 @@ public static class KernelRuntimeCompatExports
|
||||
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
||||
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;
|
||||
}
|
||||
@@ -1504,13 +1501,6 @@ public static class KernelRuntimeCompatExports
|
||||
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()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -44,12 +44,12 @@ public static class KernelSemaphoreCompatExports
|
||||
initialCount > maxCount ||
|
||||
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))
|
||||
{
|
||||
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));
|
||||
@@ -69,11 +69,11 @@ public static class KernelSemaphoreCompatExports
|
||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||
{
|
||||
_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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -89,12 +89,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
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)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
@@ -103,30 +103,30 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
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 (!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);
|
||||
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"))
|
||||
{
|
||||
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++;
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -155,12 +155,12 @@ public static class KernelSemaphoreCompatExports
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,24 +176,24 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
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)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
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;
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.WaitingThreads = 0;
|
||||
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]);
|
||||
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}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -32,7 +30,7 @@ public static class Http2Exports
|
||||
|
||||
if (poolSize == 0 || maxRequests <= 0)
|
||||
{
|
||||
return SetReturn(ctx, Http2ErrorInvalidArgument);
|
||||
return ctx.SetReturn(Http2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -53,17 +51,11 @@ public static class Http2Exports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, Http2ErrorInvalidId);
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
|
||||
@@ -33,7 +32,7 @@ public static class HttpExports
|
||||
var poolSize = ctx[CpuRegister.Rdx];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidValue);
|
||||
return ctx.SetReturn(HttpErrorInvalidValue);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -53,7 +52,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.ContainsKey(contextId))
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
var userAgentAddress = ctx[CpuRegister.Rsi];
|
||||
@@ -75,8 +74,8 @@ public static class HttpExports
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return Templates.TryRemove(templateId, out _)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, HttpErrorInvalidId);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -89,7 +88,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.TryRemove(contextId, out _))
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
foreach (var pair in Templates)
|
||||
@@ -100,13 +99,7 @@ public static class HttpExports
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -57,27 +57,27 @@ public static class NetCtlExports
|
||||
var natInfoAddress = ctx[CpuRegister.Rdi];
|
||||
if (natInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||
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)]);
|
||||
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[8..], 3);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
||||
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -87,7 +87,7 @@ public static class NetCtlExports
|
||||
LibraryName = "libSceNetCtl")]
|
||||
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -100,14 +100,14 @@ public static class NetCtlExports
|
||||
var stateAddress = ctx[CpuRegister.Rdi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -122,7 +122,7 @@ public static class NetCtlExports
|
||||
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
||||
if (function == 0 || callbackIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
lock (CallbackGate)
|
||||
@@ -130,20 +130,20 @@ public static class NetCtlExports
|
||||
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||
if (callbackId < 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorNoSpace);
|
||||
return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
||||
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -157,7 +157,7 @@ public static class NetCtlExports
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
return code switch
|
||||
@@ -177,7 +177,7 @@ public static class NetCtlExports
|
||||
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
||||
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||
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];
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -35,7 +33,7 @@ public static class NetExports
|
||||
{
|
||||
_initialized = true;
|
||||
TraceNet("init", 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -49,7 +47,7 @@ public static class NetExports
|
||||
_pools.Clear();
|
||||
_resolvers.Clear();
|
||||
TraceNet("term", 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -65,7 +63,7 @@ public static class NetExports
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -90,11 +88,11 @@ public static class NetExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_pools.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -109,7 +107,7 @@ public static class NetExports
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (flags != 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -131,8 +129,8 @@ public static class NetExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return _resolvers.TryRemove(id, out _)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -146,25 +144,19 @@ public static class NetExports
|
||||
var statusAddress = ctx[CpuRegister.Rsi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||
{
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -28,7 +26,7 @@ public static class SslExports
|
||||
var poolSize = ctx[CpuRegister.Rdi];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, SslErrorOutOfSize);
|
||||
return ctx.SetReturn(SslErrorOutOfSize);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -49,11 +47,11 @@ public static class SslExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, SslErrorInvalidId);
|
||||
return ctx.SetReturn(SslErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceSsl("term", id, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -65,13 +63,7 @@ public static class SslExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
TraceSsl("close", id, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceSsl(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Ngs2;
|
||||
|
||||
@@ -39,13 +38,13 @@ public static class Ngs2Exports
|
||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
@@ -53,7 +52,7 @@ public static class Ngs2Exports
|
||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -68,7 +67,7 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.Remove(handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
|
||||
var rackHandles = Racks
|
||||
@@ -81,7 +80,7 @@ public static class Ngs2Exports
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -98,19 +97,19 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var 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)
|
||||
@@ -118,7 +117,7 @@ public static class Ngs2Exports
|
||||
Racks[handle] = new RackState(systemHandle, rackId);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -133,13 +132,13 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Racks.ContainsKey(handle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
RemoveRackLocked(handle);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -156,7 +155,7 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Racks.ContainsKey(rackHandle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
var existing = Voices.FirstOrDefault(
|
||||
@@ -164,20 +163,20 @@ public static class Ngs2Exports
|
||||
if (existing.Key != 0)
|
||||
{
|
||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var 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)
|
||||
@@ -185,7 +184,7 @@ public static class Ngs2Exports
|
||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -197,9 +196,10 @@ public static class Ngs2Exports
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
return ctx.SetReturn(
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||
? 0
|
||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,13 +224,13 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
@@ -239,14 +239,14 @@ public static class Ngs2Exports
|
||||
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||
!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 (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}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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"),
|
||||
"1",
|
||||
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();
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -48,20 +48,14 @@ public static class NpEntitlementAccessExports
|
||||
emptyList.Clear();
|
||||
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(
|
||||
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
||||
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static void TraceNpEntitlementAccess(string message)
|
||||
|
||||
@@ -76,14 +76,14 @@ public static class NpManagerExports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
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)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -97,7 +97,7 @@ public static class NpManagerExports
|
||||
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
||||
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];
|
||||
@@ -105,17 +105,11 @@ public static class NpManagerExports
|
||||
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
||||
!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)}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
@@ -22,13 +21,13 @@ public static class NpUniversalDataSystemExports
|
||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
|
||||
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> parameters = stackalloc byte[16];
|
||||
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -41,14 +40,14 @@ public static class NpUniversalDataSystemExports
|
||||
var contextAddress = ctx[CpuRegister.Rdi];
|
||||
if (contextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||
return ctx.Memory.TryWrite(contextAddress, context)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -59,13 +58,13 @@ public static class NpUniversalDataSystemExports
|
||||
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
||||
{
|
||||
var handle = Interlocked.Increment(ref _nextHandle);
|
||||
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
|
||||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
|
||||
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
|
||||
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(
|
||||
@@ -75,24 +74,6 @@ public static class NpUniversalDataSystemExports
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
if (address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
@@ -25,12 +23,12 @@ public static class NpWebApi2Exports
|
||||
|
||||
if (httpContextId <= 0 || poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -43,13 +41,7 @@ public static class NpWebApi2Exports
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
TraceNpWebApi2("term", libraryContextId, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ public static class PadExports
|
||||
public static int PadInit(CpuContext ctx)
|
||||
{
|
||||
_initialized = true;
|
||||
return SetReturn(ctx, 0);
|
||||
DualSenseReader.EnsureStarted();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -46,21 +47,24 @@ public static class PadExports
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
||||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId == -1)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
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");
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
DualSenseReader.EnsureStarted();
|
||||
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(
|
||||
@@ -72,8 +76,8 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return handle == PrimaryPadHandle
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -87,12 +91,12 @@ public static class PadExports
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
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];
|
||||
@@ -107,8 +111,8 @@ public static class PadExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||
|
||||
return ctx.Memory.TryWrite(informationAddress, information)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -122,17 +126,17 @@ public static class PadExports
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
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)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -147,17 +151,17 @@ public static class PadExports
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
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)
|
||||
? SetReturn(ctx, 1)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(1)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -167,7 +171,84 @@ public static class PadExports
|
||||
LibraryName = "libScePad")]
|
||||
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)
|
||||
@@ -176,17 +257,33 @@ public static class PadExports
|
||||
data.Clear();
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||
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[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
data[0x08] = l2;
|
||||
data[0x09] = r2;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
@@ -201,12 +298,6 @@ public static class PadExports
|
||||
return ctx.Memory.TryWrite(dataAddress, data);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
@@ -260,4 +351,10 @@ public static class PadExports
|
||||
if (positive && !negative) return 255;
|
||||
return 128;
|
||||
}
|
||||
|
||||
private static byte MergeAxis(byte controller, byte keyboard)
|
||||
{
|
||||
const int Deadzone = 10;
|
||||
return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ public static class PlayGoExports
|
||||
for (uint i = 0; i < entriesToWrite; 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;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ public static class PlayGoExports
|
||||
|
||||
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
||||
? chunkError
|
||||
: TryWriteInt64(ctx, outEta, 0)
|
||||
: ctx.TryWriteInt64(outEta, 0)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public static class PlayGoExports
|
||||
speed = _installSpeed;
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, outSpeed, speed)
|
||||
return ctx.TryWriteInt32(outSpeed, speed)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -399,7 +399,7 @@ public static class PlayGoExports
|
||||
var loci = new byte[numberOfEntries];
|
||||
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;
|
||||
}
|
||||
@@ -648,7 +648,7 @@ public static class PlayGoExports
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
@@ -35,10 +34,10 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
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(
|
||||
@@ -51,12 +50,12 @@ public static class SaveDataDialogExports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (_status is not (StatusInitialized or StatusFinished))
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_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.
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -74,21 +73,21 @@ public static class SaveDataDialogExports
|
||||
ExportName = "sceSaveDataDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
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(
|
||||
Nid = "KK3Bdg1RWK0",
|
||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
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(
|
||||
Nid = "en7gNVnh878",
|
||||
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(1);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yEiJ-qqr6Cg",
|
||||
@@ -100,12 +99,12 @@ public static class SaveDataDialogExports
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotFinished);
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
@@ -117,10 +116,10 @@ public static class SaveDataDialogExports
|
||||
|
||||
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(
|
||||
@@ -132,10 +131,10 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
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(
|
||||
@@ -147,12 +146,12 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = 0;
|
||||
_lastUserData = 0;
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -160,14 +159,14 @@ public static class SaveDataDialogExports
|
||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hay1CfTmLyA",
|
||||
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
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)
|
||||
{
|
||||
@@ -182,12 +181,6 @@ public static class SaveDataDialogExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveDataDialog(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -50,15 +50,15 @@ public static class SaveDataExports
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,18 +73,18 @@ public static class SaveDataExports
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (condAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||
!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)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -96,7 +96,7 @@ public static class SaveDataExports
|
||||
}
|
||||
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);
|
||||
@@ -111,18 +111,18 @@ public static class SaveDataExports
|
||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (setNum == 0)
|
||||
{
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (result.DirNamesAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
for (var i = 0; i < setNum; i++)
|
||||
@@ -138,20 +138,20 @@ public static class SaveDataExports
|
||||
(result.InfosAddress != 0 &&
|
||||
!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}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ public static class SaveDataExports
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
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 + 0x10, out var blocks) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
||||
@@ -179,12 +179,12 @@ public static class SaveDataExports
|
||||
dirNameAddress == 0 ||
|
||||
!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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -199,12 +199,12 @@ public static class SaveDataExports
|
||||
|
||||
if (!existed && !create && !createIfMissing)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
|
||||
}
|
||||
|
||||
if (existed && create)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorExists);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorExists);
|
||||
}
|
||||
|
||||
if (!existed)
|
||||
@@ -221,26 +221,26 @@ public static class SaveDataExports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
||||
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(
|
||||
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
||||
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
||||
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,26 +258,26 @@ public static class SaveDataExports
|
||||
|
||||
if (resourceAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||
|
||||
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(
|
||||
$"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)
|
||||
{
|
||||
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 + 0x10, out var dirNameAddress) ||
|
||||
!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)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Share;
|
||||
@@ -26,13 +25,13 @@ public static class ShareExports
|
||||
var affinityMask = ctx[CpuRegister.Rdx];
|
||||
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);
|
||||
|
||||
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(
|
||||
@@ -45,12 +44,12 @@ public static class ShareExports
|
||||
var contentParamAddress = ctx[CpuRegister.Rdi];
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_contentParam = contentParam;
|
||||
@@ -60,13 +59,7 @@ public static class ShareExports
|
||||
}
|
||||
|
||||
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
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];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
var value = parameterId switch
|
||||
@@ -37,8 +37,8 @@ public static class SystemServiceExports
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -51,7 +51,7 @@ public static class SystemServiceExports
|
||||
var statusAddress = ctx[CpuRegister.Rdi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||
@@ -60,8 +60,8 @@ public static class SystemServiceExports
|
||||
status[0x06] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -74,7 +74,7 @@ public static class SystemServiceExports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
|
||||
@@ -82,8 +82,8 @@ public static class SystemServiceExports
|
||||
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -94,12 +94,6 @@ public static class SystemServiceExports
|
||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||
{
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.UserService;
|
||||
|
||||
@@ -40,12 +39,12 @@ public static class UserServiceExports
|
||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -58,7 +57,7 @@ public static class UserServiceExports
|
||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdListAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||
@@ -67,8 +66,8 @@ public static class UserServiceExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -81,20 +80,20 @@ public static class UserServiceExports
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
if (eventAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
|
||||
}
|
||||
|
||||
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -109,25 +108,25 @@ public static class UserServiceExports
|
||||
var capacity = ctx[CpuRegister.Rdx];
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||
if (capacity <= (ulong)nameBytes.Length)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
|
||||
}
|
||||
|
||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||
nameBytes.CopyTo(output);
|
||||
return ctx.Memory.TryWrite(nameAddress, output)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,29 +140,16 @@ public static class UserServiceExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (parameterId != 1000)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, valueAddress, 0)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (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;
|
||||
return ctx.TryWriteInt32(valueAddress, 0)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Queue<object> _pendingGuestWork = 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)>
|
||||
_tracedGuestImageSubmissions = [];
|
||||
private static Thread? _thread;
|
||||
@@ -600,7 +601,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsGuestImageAvailable(
|
||||
internal static bool IsGpuGuestImageAvailable(
|
||||
ulong address,
|
||||
uint format,
|
||||
uint numberType)
|
||||
@@ -613,7 +614,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||
return _gpuGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||
availableFormat == guestFormat;
|
||||
}
|
||||
}
|
||||
@@ -971,6 +972,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public VulkanGuestSampler SamplerState;
|
||||
public Sampler Sampler;
|
||||
public GuestImageResource? GuestImage;
|
||||
public ulong CpuContentFingerprint;
|
||||
public bool UpdatesCpuContent;
|
||||
}
|
||||
|
||||
private sealed class GlobalBufferResource
|
||||
@@ -1009,6 +1012,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public Framebuffer Framebuffer;
|
||||
public bool Initialized;
|
||||
public bool InitialUploadPending;
|
||||
public bool IsCpuBacked;
|
||||
public ulong CpuContentFingerprint;
|
||||
}
|
||||
|
||||
private sealed record PendingGuestSubmission(
|
||||
@@ -1248,7 +1253,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||
byte* validationLayerName = null;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var applicationInfo = new ApplicationInfo
|
||||
@@ -1308,7 +1313,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
{
|
||||
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||
}
|
||||
|
||||
|
||||
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
||||
{
|
||||
_debugUtils = debugUtils;
|
||||
@@ -1378,7 +1383,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
||||
"vkCreateDebugUtilsMessengerEXT");
|
||||
}
|
||||
|
||||
|
||||
private static unsafe uint DebugCallback(
|
||||
DebugUtilsMessageSeverityFlagsEXT severity,
|
||||
DebugUtilsMessageTypeFlagsEXT type,
|
||||
@@ -2838,6 +2843,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"tile={texture.TileMode} format={vkFormat}");
|
||||
}
|
||||
|
||||
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
|
||||
{
|
||||
return refresh;
|
||||
}
|
||||
|
||||
return new TextureResource
|
||||
{
|
||||
Address = texture.Address,
|
||||
@@ -2855,6 +2865,71 @@ internal static unsafe class VulkanVideoPresenter
|
||||
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(
|
||||
VulkanGuestDrawTexture texture,
|
||||
GuestImageResource guestImage)
|
||||
@@ -3096,24 +3171,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var uploadPixels = texture.Format == 13
|
||||
? ExpandRgb32Pixels(pixels)
|
||||
: pixels;
|
||||
var uploadSize = (ulong)uploadPixels.Length;
|
||||
var contentFingerprint = ComputeTextureContentFingerprint(pixels);
|
||||
|
||||
var stagingBuffer = CreateBuffer(
|
||||
uploadSize,
|
||||
BufferUsageFlags.TransferSrcBit,
|
||||
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 (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
uploadPixels,
|
||||
$"{TextureDebugName(texture, vkFormat)} staging");
|
||||
|
||||
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
||||
var imageInfo = new ImageCreateInfo
|
||||
@@ -3163,7 +3225,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
||||
var debugName = TextureDebugName(texture, vkFormat);
|
||||
SetDebugName(ObjectType.Buffer, stagingBuffer.Handle, $"{debugName} staging");
|
||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||
var resource = new TextureResource
|
||||
@@ -3181,6 +3242,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
NeedsUpload = true,
|
||||
OwnsStorage = true,
|
||||
SamplerState = texture.Sampler,
|
||||
CpuContentFingerprint = contentFingerprint,
|
||||
UpdatesCpuContent = texture.Address != 0,
|
||||
};
|
||||
|
||||
if (texture.Address != 0 &&
|
||||
@@ -3197,6 +3260,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Memory = imageMemory,
|
||||
View = view,
|
||||
InitialUploadPending = true,
|
||||
IsCpuBacked = true,
|
||||
CpuContentFingerprint = contentFingerprint,
|
||||
};
|
||||
_guestImages.Add(texture.Address, guestImage);
|
||||
resource.OwnsStorage = false;
|
||||
@@ -3209,6 +3274,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (guestFormat != 0)
|
||||
{
|
||||
_availableGuestImages[texture.Address] = guestFormat;
|
||||
_gpuGuestImages.Remove(texture.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3216,6 +3282,41 @@ internal static unsafe class VulkanVideoPresenter
|
||||
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(
|
||||
VulkanGuestDrawTexture texture,
|
||||
byte[] pixels,
|
||||
@@ -4267,12 +4368,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
||||
|
||||
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
|
||||
var toColorAttachment = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = target.Initialized ? AccessFlags.ShaderReadBit : 0,
|
||||
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
|
||||
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||
OldLayout = target.Initialized
|
||||
OldLayout = targetHasPriorContents
|
||||
? ImageLayout.ShaderReadOnlyOptimal
|
||||
: ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
@@ -4283,8 +4385,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
_commandBuffer,
|
||||
target.Initialized
|
||||
? PipelineStageFlags.FragmentShaderBit
|
||||
targetHasPriorContents
|
||||
? PipelineStageFlags.AllCommandsBit
|
||||
: PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.ColorAttachmentOutputBit,
|
||||
0,
|
||||
@@ -4337,12 +4439,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
MarkSampledImagesInitialized(resources);
|
||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||
|
||||
var guestTextureFormat = GetGuestTextureFormat(target.Format);
|
||||
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
|
||||
work.Target.Format,
|
||||
work.Target.NumberType);
|
||||
if (work.PublishTarget && guestTextureFormat != 0)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
||||
_gpuGuestImages[target.Address] = guestTextureFormat;
|
||||
}
|
||||
}
|
||||
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
||||
@@ -4384,6 +4489,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Remove(work.Target.Address);
|
||||
_gpuGuestImages.Remove(work.Target.Address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4423,6 +4529,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
existing.MipLevels == mipLevels &&
|
||||
existing.Format == format)
|
||||
{
|
||||
existing.IsCpuBacked = false;
|
||||
existing.CpuContentFingerprint = 0;
|
||||
if (existing.RenderPass.Handle == 0)
|
||||
{
|
||||
var attachmentView = existing.MipViews.Length > 0
|
||||
@@ -4448,6 +4556,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Remove(target.Address);
|
||||
_gpuGuestImages.Remove(target.Address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5386,11 +5495,18 @@ internal static unsafe class VulkanVideoPresenter
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasPriorContents = texture.GuestImage is { } guestImage &&
|
||||
(guestImage.Initialized || guestImage.InitialUploadPending);
|
||||
var toTransfer = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = hasPriorContents
|
||||
? AccessFlags.ShaderReadBit
|
||||
: 0,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = ImageLayout.Undefined,
|
||||
OldLayout = hasPriorContents
|
||||
? ImageLayout.ShaderReadOnlyOptimal
|
||||
: ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
@@ -5399,7 +5515,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
_commandBuffer,
|
||||
PipelineStageFlags.TopOfPipeBit,
|
||||
hasPriorContents
|
||||
? PipelineStageFlags.AllCommandsBit
|
||||
: PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
@@ -5569,6 +5687,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (format != 0)
|
||||
{
|
||||
_availableGuestImages[texture.Address] = format;
|
||||
_gpuGuestImages[texture.Address] = format;
|
||||
}
|
||||
|
||||
if (traceContents &&
|
||||
@@ -5608,6 +5727,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
guestImage.Initialized = true;
|
||||
guestImage.InitialUploadPending = false;
|
||||
if (texture.UpdatesCpuContent)
|
||||
{
|
||||
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6379,6 +6502,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Clear();
|
||||
_gpuGuestImages.Clear();
|
||||
}
|
||||
DestroySwapchainResources();
|
||||
if (_device.Handle != 0)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -12,9 +12,7 @@ public static class SharpEmuLog
|
||||
new(StringComparer.Ordinal);
|
||||
private static readonly object ConfigurationSync = new();
|
||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
||||
useColors: ResolveColorEnabledFromEnvironment(),
|
||||
includeTimestamp: false);
|
||||
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
||||
|
||||
public static LogLevel MinimumLevel
|
||||
{
|
||||
@@ -37,11 +35,26 @@ public static class SharpEmuLog
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
if (ReferenceEquals(_sink, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sink is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_sink = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||
@@ -146,6 +175,30 @@ public static class SharpEmuLog
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
Reference in New Issue
Block a user