mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ddc09ea91 | |||
| c4326a1143 | |||
| 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]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Run(args);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SharpEmuLog.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Run(string[] args)
|
||||||
{
|
{
|
||||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
if (args.Length == 0 && !isMitigatedChild)
|
if (args.Length == 0 && !isMitigatedChild)
|
||||||
@@ -66,7 +78,7 @@ internal static partial class Program
|
|||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
if (!File.Exists(ebootPath))
|
if (!File.Exists(ebootPath))
|
||||||
{
|
{
|
||||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||||
|
|||||||
@@ -484,4 +484,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||||
|
|
||||||
private enum EntryFrameKind
|
private enum EntryFrameKind
|
||||||
{
|
{
|
||||||
ProcessEntry,
|
ProcessEntry,
|
||||||
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
Log.Debug("=== DispatchEntry START ===");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
Log.Debug("DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
Log.Error($"Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
|
|||||||
@@ -358,12 +358,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||||
}
|
}
|
||||||
else
|
else if (importStubEntry.Export is { } mismatchedExport)
|
||||||
{
|
{
|
||||||
dispatchResolved = false;
|
dispatchResolved = true;
|
||||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||||
}
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
||||||
|
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dispatchResolved = false;
|
||||||
|
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1679,10 +1679,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 65;
|
ptr2[num++] = 65;
|
||||||
ptr2[num++] = 95;
|
ptr2[num++] = 95;
|
||||||
ptr2[num++] = 195;
|
ptr2[num++] = 195;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
VirtualProtect(ptr, 192u, 32u, &num2);
|
if (!VirtualProtect(ptr, 192u, 32u, &num2))
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
{
|
||||||
return (nint)ptr;
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||||
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -1763,7 +1767,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
tlsHandlerAddress[num++] = 195;
|
tlsHandlerAddress[num++] = 195;
|
||||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
|
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||||
}
|
}
|
||||||
@@ -1784,7 +1792,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[i] = 144;
|
ptr2[i] = 144;
|
||||||
}
|
}
|
||||||
uint num = default(uint);
|
uint num = default(uint);
|
||||||
VirtualProtect(ptr, 4096u, 32u, &num);
|
if (!VirtualProtect(ptr, 4096u, 32u, &num))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -1831,7 +1843,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
EmitByte(code, ref offset, 0xC3);
|
EmitByte(code, ref offset, 0xC3);
|
||||||
|
|
||||||
uint oldProtect = default;
|
uint oldProtect = default;
|
||||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -1923,7 +1939,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = default;
|
uint oldProtect = default;
|
||||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -2254,7 +2274,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr[num2++] = 144;
|
ptr[num2++] = 144;
|
||||||
}
|
}
|
||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
|
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
@@ -3591,7 +3615,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
uint oldProtect = default(uint);
|
uint oldProtect = default(uint);
|
||||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||||
|
{
|
||||||
|
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||||
|
return GuestNativeCallExitReason.Exception;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
ActiveGuestThreadYieldRequested = false;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
@@ -3728,7 +3756,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
uint oldProtect = default(uint);
|
uint oldProtect = default(uint);
|
||||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||||
|
{
|
||||||
|
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
|
||||||
|
return GuestNativeCallExitReason.Exception;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
ActiveGuestThreadYieldRequested = false;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
@@ -3988,7 +4020,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uint num5 = default(uint);
|
uint num5 = default(uint);
|
||||||
VirtualProtect(ptr, stubSize, 64u, &num5);
|
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
|
||||||
|
{
|
||||||
|
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
if (_hostRspSlotStorage != 0)
|
if (_hostRspSlotStorage != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||||
|
|
||||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly object _allocationSearchHintGate = new();
|
private readonly object _allocationSearchHintGate = new();
|
||||||
@@ -55,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
private static extern void* GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -451,7 +458,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +706,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
if (IsExecutableProtection(oldProtect))
|
if (IsExecutableProtection(oldProtect))
|
||||||
{
|
{
|
||||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,7 +1055,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] {message}");
|
Log.Debug(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using SharpEmu.Libs.VideoOut;
|
|||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
using SharpEmu.Libs.SaveData;
|
using SharpEmu.Libs.SaveData;
|
||||||
|
using SharpEmu.Logging;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -20,6 +21,8 @@ namespace SharpEmu.Core.Runtime;
|
|||||||
|
|
||||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||||
{
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||||
|
|
||||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||||
|
|
||||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -128,7 +131,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
Log.Info($"Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
@@ -140,7 +143,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||||
@@ -163,7 +166,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -171,8 +174,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
Log.Info($"Dispatching, gen: {generation}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -182,8 +185,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
Log.Info($"DispatchEntry returned: {result}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
Log.Info($"Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -434,8 +437,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -446,8 +449,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Error(
|
||||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
$"Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,8 +471,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -508,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -577,7 +580,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
if (skippedModules.Length > 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulePaths.Length == 0)
|
if (modulePaths.Length == 0)
|
||||||
@@ -585,8 +588,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
@@ -621,18 +624,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failedModules++;
|
failedModules++;
|
||||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,8 +655,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,8 +688,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -698,8 +701,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Error(
|
||||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -708,8 +711,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -745,8 +748,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -855,8 +858,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
||||||
|
|
||||||
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" />
|
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||||
|
<GradientStop Offset="0" Color="#12151F" />
|
||||||
|
<GradientStop Offset="0.55" Color="#0D1017" />
|
||||||
|
<GradientStop Offset="1" Color="#0B0D14" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
|
||||||
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
||||||
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
||||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
||||||
@@ -24,6 +29,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
||||||
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
||||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||||
|
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
||||||
|
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
@@ -38,7 +45,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
<Setter Property="Padding" Value="16" />
|
<Setter Property="Padding" Value="16" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -61,6 +68,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Margin" Value="0,0,0,6" />
|
<Setter Property="Margin" Value="0,0,0,6" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button.accent">
|
<Style Selector="Button.accent">
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||||
<Setter Property="Foreground" Value="White" />
|
<Setter Property="Foreground" Value="White" />
|
||||||
@@ -98,6 +109,41 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ToggleButton.ghost">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
<Setter Property="Padding" Value="12,7" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="ContextMenu">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Padding" Value="6" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ContextMenu MenuItem">
|
||||||
|
<Setter Property="Padding" Value="10,7" />
|
||||||
|
<Setter Property="CornerRadius" Value="7" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ContextMenu Separator">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
|
||||||
|
<Setter Property="Height" Value="1" />
|
||||||
|
<Setter Property="Margin" Value="8,4" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ListBox.console">
|
<Style Selector="ListBox.console">
|
||||||
<Setter Property="Background" Value="#0B0E14" />
|
<Setter Property="Background" Value="#0B0E14" />
|
||||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
||||||
@@ -108,10 +154,44 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="MinHeight" Value="0" />
|
<Setter Property="MinHeight" Value="0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ListBox.library ListBoxItem">
|
<!-- Cover-art library grid -->
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||||
<Setter Property="Margin" Value="4,2" />
|
<Setter Property="Padding" Value="10" />
|
||||||
<Setter Property="Padding" Value="8,4" />
|
<Setter Property="Margin" Value="5" />
|
||||||
|
<Setter Property="CornerRadius" Value="14" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="RenderTransform" Value="translateY(0px)" />
|
||||||
|
<Setter Property="Transitions">
|
||||||
|
<Transitions>
|
||||||
|
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
||||||
|
</Transitions>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
|
||||||
|
<Setter Property="RenderTransform" Value="translateY(-3px)" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.coverShadow">
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.coverClip">
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="ClipToBounds" Value="True" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,147 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes)
|
public sealed class GameEntry : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
|
// Placeholder gradients for games without cover art, picked
|
||||||
|
// deterministically from the game name so a game keeps its color.
|
||||||
|
private static readonly (Color Start, Color End)[] PlaceholderPalette =
|
||||||
|
{
|
||||||
|
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
|
||||||
|
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
|
||||||
|
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
|
||||||
|
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
|
||||||
|
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
|
||||||
|
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
|
||||||
|
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
|
||||||
|
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private Bitmap? _cover;
|
||||||
|
private IBrush? _placeholderBrush;
|
||||||
|
private long _sizeBytes;
|
||||||
|
|
||||||
|
public GameEntry(
|
||||||
|
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
TitleId = titleId;
|
||||||
|
Path = path;
|
||||||
|
_sizeBytes = sizeBytes;
|
||||||
|
CoverPath = coverPath;
|
||||||
|
BackgroundPath = backgroundPath;
|
||||||
|
Initials = ComputeInitials(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string? TitleId { get; }
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total size of the game. Initially the eboot's own size from the scan;
|
||||||
|
/// replaced with the full install folder size once computed in the
|
||||||
|
/// background.
|
||||||
|
/// </summary>
|
||||||
|
public long SizeBytes
|
||||||
|
{
|
||||||
|
get => _sizeBytes;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_sizeBytes == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sizeBytes = value;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
||||||
|
public string? CoverPath { get; }
|
||||||
|
|
||||||
|
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
||||||
|
public string? BackgroundPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decoded key art used as the window backdrop while this game is
|
||||||
|
/// selected. Loaded on demand and cached; not exposed via binding.
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap? Background { get; set; }
|
||||||
|
|
||||||
|
public string Initials { get; }
|
||||||
|
|
||||||
|
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
||||||
|
// UI thread, while GameEntry itself is constructed on the scan thread.
|
||||||
|
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
|
||||||
|
|
||||||
|
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
|
||||||
|
public Bitmap? Cover
|
||||||
|
{
|
||||||
|
get => _cover;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_cover, value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cover = value;
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasCover => _cover is not null;
|
||||||
|
|
||||||
public string Detail => TitleId is not null
|
public string Detail => TitleId is not null
|
||||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||||
: $"{FormatSize(SizeBytes)} • {Path}";
|
: FormatSize(SizeBytes);
|
||||||
|
|
||||||
|
private static string ComputeInitials(string name)
|
||||||
|
{
|
||||||
|
var initials = name
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Where(word => char.IsLetterOrDigit(word[0]))
|
||||||
|
.Select(word => char.ToUpperInvariant(word[0]))
|
||||||
|
.Take(2)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return initials.Length > 0 ? new string(initials) : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IBrush BuildPlaceholderBrush(string name)
|
||||||
|
{
|
||||||
|
var hash = 0;
|
||||||
|
foreach (var ch in name)
|
||||||
|
{
|
||||||
|
hash = unchecked(hash * 31 + ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
|
||||||
|
return new LinearGradientBrush
|
||||||
|
{
|
||||||
|
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||||
|
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||||
|
GradientStops =
|
||||||
|
{
|
||||||
|
new GradientStop(start, 0),
|
||||||
|
new GradientStop(end, 1),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatSize(long bytes)
|
private static string FormatSize(long bytes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public List<string> GameFolders { get; set; } = new();
|
public List<string> GameFolders { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
||||||
|
public List<string> ExcludedGames { get; set; } = new();
|
||||||
|
|
||||||
public string LogLevel { get; set; } = "Info";
|
public string LogLevel { get; set; } = "Info";
|
||||||
|
|
||||||
public int ImportTraceLimit { get; set; }
|
public int ImportTraceLimit { get; set; }
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="SharpEmu.GUI.MainWindow"
|
x:Class="SharpEmu.GUI.MainWindow"
|
||||||
Title="SharpEmu"
|
Title="SharpEmu"
|
||||||
Width="1280" Height="800"
|
Width="1280" Height="820"
|
||||||
MinWidth="980" MinHeight="620"
|
MinWidth="980" MinHeight="640"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Background="{StaticResource BgBrush}"
|
Background="{StaticResource BgBrush}"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
@@ -18,10 +18,33 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<Grid RowDefinitions="44,*,32">
|
<Grid RowDefinitions="44,*,32">
|
||||||
|
|
||||||
|
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||||
|
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||||
|
<Panel Grid.Row="1" ClipToBounds="True">
|
||||||
|
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<Image.Transitions>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
|
||||||
|
</Transitions>
|
||||||
|
</Image.Transitions>
|
||||||
|
</Image>
|
||||||
|
<Border>
|
||||||
|
<Border.Background>
|
||||||
|
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
|
||||||
|
<GradientStop Offset="0" Color="#730D1017" />
|
||||||
|
<GradientStop Offset="0.55" Color="#BF0D1017" />
|
||||||
|
<GradientStop Offset="1" Color="#EB0D1017" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Border.Background>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- Title bar -->
|
<!-- Title bar -->
|
||||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||||
|
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||||
<Border Classes="pill">
|
<Border Classes="pill">
|
||||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
@@ -30,119 +53,204 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*">
|
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
||||||
|
|
||||||
<!-- Game library -->
|
<!-- Library toolbar -->
|
||||||
<Border Grid.Column="0" Classes="card" Padding="0">
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
|
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||||
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
|
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
<Border Grid.Column="1" Classes="pill">
|
</Border>
|
||||||
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||||
</Border>
|
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||||
|
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
|
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
|
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Cover-art grid -->
|
||||||
|
<Panel Grid.Row="1">
|
||||||
|
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||||
|
SelectionMode="Single" Padding="0">
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||||
|
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||||
|
Foreground="{StaticResource DangerHoverBrush}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</ContextMenu>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Width="128" Height="186" Spacing="7">
|
||||||
|
<Border Classes="coverShadow" Width="128" Height="128">
|
||||||
|
<Border Classes="coverClip">
|
||||||
|
<Panel>
|
||||||
|
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||||
|
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||||
|
Foreground="#E8ECF4" Opacity="0.85"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||||
|
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsVisible="False">
|
||||||
|
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||||
|
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||||
|
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||||
|
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||||
|
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Console (collapsible) -->
|
||||||
|
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||||
|
Margin="0,12,0,0" IsVisible="False">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||||
|
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||||
|
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||||
|
FontSize="12" Margin="0,0,12,0" />
|
||||||
|
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||||
|
Padding="10,4" Margin="0,0,8,0" />
|
||||||
|
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||||
|
Padding="10,4" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||||
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
|
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
||||||
|
|
||||||
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Margin="4,4" Spacing="2">
|
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||||
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
|
||||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
|
|
||||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" />
|
|
||||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
|
|
||||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Right column -->
|
<!-- Launch bar -->
|
||||||
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
|
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
<!-- Selected game / launch -->
|
<!-- Selected game cover thumbnail -->
|
||||||
<Border Grid.Row="0" Classes="card">
|
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
VerticalAlignment="Center">
|
||||||
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
|
<Panel x:Name="SelectedCoverPanel">
|
||||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
|
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
|
||||||
|
IsVisible="{Binding !HasCover, FallbackValue=False}">
|
||||||
|
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
|
||||||
|
Foreground="#E8ECF4" Opacity="0.85"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<Image Source="{Binding Cover}" Stretch="UniformToFill"
|
||||||
|
IsVisible="{Binding HasCover, FallbackValue=False}" />
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||||
|
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||||
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
|
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
||||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
|
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
||||||
|
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Launch options -->
|
<!-- Launch options (collapsible) -->
|
||||||
<Border Grid.Row="2" Classes="card">
|
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Margin="0,0,24,0">
|
||||||
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
|
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||||
<WrapPanel>
|
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||||
<StackPanel Margin="0,0,24,0">
|
<ComboBoxItem Content="Native" />
|
||||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
</ComboBox>
|
||||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
</StackPanel>
|
||||||
<ComboBoxItem Content="Native" />
|
<StackPanel Margin="0,0,24,0">
|
||||||
</ComboBox>
|
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||||
</StackPanel>
|
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||||
<StackPanel Margin="0,0,24,0">
|
<ComboBoxItem Content="Trace" />
|
||||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
<ComboBoxItem Content="Debug" />
|
||||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
<ComboBoxItem Content="Info" />
|
||||||
<ComboBoxItem Content="Trace" />
|
<ComboBoxItem Content="Warning" />
|
||||||
<ComboBoxItem Content="Debug" />
|
<ComboBoxItem Content="Error" />
|
||||||
<ComboBoxItem Content="Info" />
|
<ComboBoxItem Content="Critical" />
|
||||||
<ComboBoxItem Content="Warning" />
|
</ComboBox>
|
||||||
<ComboBoxItem Content="Error" />
|
</StackPanel>
|
||||||
<ComboBoxItem Content="Critical" />
|
<StackPanel Margin="0,0,24,0">
|
||||||
</ComboBox>
|
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||||
</StackPanel>
|
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||||
<StackPanel Margin="0,0,24,0">
|
Value="0" FormatString="0" CornerRadius="8" />
|
||||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
</StackPanel>
|
||||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
<StackPanel>
|
||||||
Value="0" FormatString="0" CornerRadius="8" />
|
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||||
</StackPanel>
|
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||||
<StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
</WrapPanel>
|
||||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
</StackPanel>
|
||||||
</StackPanel>
|
</Border>
|
||||||
</WrapPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Console -->
|
|
||||||
<Border Grid.Row="4" Classes="card" Padding="0">
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
|
|
||||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
|
||||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
|
||||||
FontSize="12" Margin="0,0,12,0" />
|
|
||||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
|
||||||
Padding="10,4" />
|
|
||||||
</Grid>
|
|
||||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
|
||||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,10,10">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
@@ -35,6 +41,16 @@ public partial class MainWindow : Window
|
|||||||
private string? _emulatorExePath;
|
private string? _emulatorExePath;
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private int _autoScrollTicks;
|
private int _autoScrollTicks;
|
||||||
|
private int _detailLoadGeneration;
|
||||||
|
private int _backdropGeneration;
|
||||||
|
|
||||||
|
// Controller navigation state.
|
||||||
|
private readonly DispatcherTimer _gamepadTimer;
|
||||||
|
private uint _previousPadButtons;
|
||||||
|
private long _navLeftNextAt;
|
||||||
|
private long _navRightNextAt;
|
||||||
|
private long _navUpNextAt;
|
||||||
|
private long _navDownNextAt;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -59,15 +75,142 @@ public partial class MainWindow : Window
|
|||||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
|
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||||
|
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
||||||
|
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
|
||||||
|
|
||||||
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
|
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||||
|
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
||||||
|
CtxCopyPath.Click += async (_, _) =>
|
||||||
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Path");
|
||||||
|
CtxCopyTitleId.Click += async (_, _) =>
|
||||||
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Title ID");
|
||||||
|
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||||
|
|
||||||
Opened += async (_, _) => await OnOpenedAsync();
|
Opened += async (_, _) => await OnOpenedAsync();
|
||||||
Closing += (_, _) => OnWindowClosing();
|
Closing += (_, _) => OnWindowClosing();
|
||||||
|
|
||||||
|
DualSenseReader.EnsureStarted();
|
||||||
|
_gamepadTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromMilliseconds(50),
|
||||||
|
};
|
||||||
|
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
||||||
|
_gamepadTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Controller navigation ----
|
||||||
|
|
||||||
|
private void PollGamepad()
|
||||||
|
{
|
||||||
|
if (!DualSenseReader.TryGetState(out var pad))
|
||||||
|
{
|
||||||
|
_previousPadButtons = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsActive)
|
||||||
|
{
|
||||||
|
// Ignore input while the launcher is in the background (e.g. the
|
||||||
|
// game window is focused and using the same controller).
|
||||||
|
_previousPadButtons = pad.Buttons;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Environment.TickCount64;
|
||||||
|
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
|
||||||
|
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
|
||||||
|
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
|
||||||
|
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
|
||||||
|
|
||||||
|
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
||||||
|
{
|
||||||
|
MoveSelection(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldNavigate(right, ref _navRightNextAt, now))
|
||||||
|
{
|
||||||
|
MoveSelection(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldNavigate(up, ref _navUpNextAt, now))
|
||||||
|
{
|
||||||
|
MoveSelection(-TilesPerRow());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldNavigate(down, ref _navDownNextAt, now))
|
||||||
|
{
|
||||||
|
MoveSelection(TilesPerRow());
|
||||||
|
}
|
||||||
|
|
||||||
|
var pressed = pad.Buttons & ~_previousPadButtons;
|
||||||
|
if ((pressed & 0x4000) != 0) // Cross
|
||||||
|
{
|
||||||
|
LaunchSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((pressed & 0x2000) != 0) // Circle
|
||||||
|
{
|
||||||
|
_emulator?.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_previousPadButtons = pad.Buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edge-triggered with hold-to-repeat: fires on press, then repeats
|
||||||
|
/// after 400ms at 130ms intervals while held.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ShouldNavigate(bool held, ref long nextAt, long now)
|
||||||
|
{
|
||||||
|
if (!held)
|
||||||
|
{
|
||||||
|
nextAt = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextAt == 0)
|
||||||
|
{
|
||||||
|
nextAt = now + 400;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now >= nextAt)
|
||||||
|
{
|
||||||
|
nextAt = now + 130;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveSelection(int delta)
|
||||||
|
{
|
||||||
|
if (_visibleGames.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = GameList.SelectedIndex < 0
|
||||||
|
? 0
|
||||||
|
: Math.Clamp(GameList.SelectedIndex + delta, 0, _visibleGames.Count - 1);
|
||||||
|
GameList.SelectedIndex = index;
|
||||||
|
GameList.ScrollIntoView(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int TilesPerRow()
|
||||||
|
{
|
||||||
|
// Tile footprint: 128 content + 20 item padding + 10 item margin.
|
||||||
|
const double TileOuterWidth = 158;
|
||||||
|
var width = GameList.Bounds.Width;
|
||||||
|
return width > TileOuterWidth ? (int)(width / TileOuterWidth) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnOpenedAsync()
|
private async Task OnOpenedAsync()
|
||||||
@@ -89,6 +232,7 @@ public partial class MainWindow : Window
|
|||||||
ReadControlsIntoSettings();
|
ReadControlsIntoSettings();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
_consoleFlushTimer.Stop();
|
_consoleFlushTimer.Stop();
|
||||||
|
_gamepadTimer.Stop();
|
||||||
_emulator?.Dispose();
|
_emulator?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +332,21 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_settings.GameFolders.Add(path);
|
_settings.GameFolders.Add(path);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding (or re-adding) a folder is an explicit signal to restore any
|
||||||
|
// games beneath it that were removed from the library earlier.
|
||||||
|
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
||||||
|
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
||||||
|
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,19 +356,118 @@ public partial class MainWindow : Window
|
|||||||
private async Task RescanLibraryAsync()
|
private async Task RescanLibraryAsync()
|
||||||
{
|
{
|
||||||
var folders = _settings.GameFolders.ToArray();
|
var folders = _settings.GameFolders.ToArray();
|
||||||
|
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
||||||
StatusBarRight.Text = "Scanning library…";
|
StatusBarRight.Text = "Scanning library…";
|
||||||
|
|
||||||
var games = await Task.Run(() => ScanFolders(folders));
|
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
||||||
|
|
||||||
_allGames.Clear();
|
_allGames.Clear();
|
||||||
_allGames.AddRange(games);
|
_allGames.AddRange(games);
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
|
LoadGameDetailsInBackground(games);
|
||||||
StatusBarRight.Text = folders.Length == 0
|
StatusBarRight.Text = folders.Length == 0
|
||||||
? "Add a game folder to populate the library."
|
? "Add a game folder to populate the library."
|
||||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders)
|
/// <summary>
|
||||||
|
/// Enriches games off the UI thread — decodes cover art and totals each
|
||||||
|
/// game's install folder size — posting results back as they become
|
||||||
|
/// ready. A newer scan invalidates older loads.
|
||||||
|
/// </summary>
|
||||||
|
private void LoadGameDetailsInBackground(IReadOnlyList<GameEntry> games)
|
||||||
|
{
|
||||||
|
var generation = ++_detailLoadGeneration;
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
// Covers first: they are cheap and the most visible, so the grid
|
||||||
|
// fills with art before the (potentially slow) size pass runs.
|
||||||
|
foreach (var game in games)
|
||||||
|
{
|
||||||
|
if (generation != _detailLoadGeneration)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.CoverPath is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(game.CoverPath);
|
||||||
|
var bitmap = Bitmap.DecodeToWidth(stream, 312);
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (generation == _detailLoadGeneration)
|
||||||
|
{
|
||||||
|
game.Cover = bitmap;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// A missing or undecodable image keeps the placeholder.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var game in games)
|
||||||
|
{
|
||||||
|
if (generation != _detailLoadGeneration)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = ComputeInstallSize(game.Path);
|
||||||
|
if (size > 0)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (generation == _detailLoadGeneration)
|
||||||
|
{
|
||||||
|
game.SizeBytes = size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Totals the size of the game's install folder (the directory holding
|
||||||
|
/// the eboot), which is far more accurate than the eboot alone.
|
||||||
|
/// </summary>
|
||||||
|
private static long ComputeInstallSize(string ebootPath)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
|
if (directory is null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long total = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var enumeration = new EnumerationOptions
|
||||||
|
{
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
RecurseSubdirectories = true,
|
||||||
|
};
|
||||||
|
foreach (var file in new DirectoryInfo(directory).EnumerateFiles("*", enumeration))
|
||||||
|
{
|
||||||
|
total += file.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Fall back to whatever was accumulated so far.
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
||||||
{
|
{
|
||||||
var games = new List<GameEntry>();
|
var games = new List<GameEntry>();
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -235,7 +490,7 @@ public partial class MainWindow : Window
|
|||||||
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
||||||
{
|
{
|
||||||
var fullPath = Path.GetFullPath(file);
|
var fullPath = Path.GetFullPath(file);
|
||||||
if (!seen.Add(fullPath))
|
if (!seen.Add(fullPath) || excludedPaths.Contains(fullPath))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -250,7 +505,9 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (title, titleId) = TryReadParamJson(fullPath);
|
var (title, titleId) = TryReadParamJson(fullPath);
|
||||||
games.Add(new GameEntry(title ?? GameNameFor(fullPath), titleId, fullPath, size));
|
games.Add(new GameEntry(
|
||||||
|
title ?? GameNameFor(fullPath), titleId, fullPath, size,
|
||||||
|
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -332,6 +589,56 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the cover art shipped with the game: sce_sys/icon0.png next to
|
||||||
|
/// the executable (falling back to pic0.png).
|
||||||
|
/// </summary>
|
||||||
|
private static string? FindCoverFor(string ebootPath)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
|
if (directory is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceSys = Path.Combine(directory, "sce_sys");
|
||||||
|
foreach (var candidate in new[] { "icon0.png", "pic0.png" })
|
||||||
|
{
|
||||||
|
var coverPath = Path.Combine(sceSys, candidate);
|
||||||
|
if (File.Exists(coverPath))
|
||||||
|
{
|
||||||
|
return coverPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the key art shipped with the game (sce_sys/pic0.png, falling
|
||||||
|
/// back to pic1.png), used as the window backdrop when selected.
|
||||||
|
/// </summary>
|
||||||
|
private static string? FindBackgroundFor(string ebootPath)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
|
if (directory is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceSys = Path.Combine(directory, "sce_sys");
|
||||||
|
foreach (var candidate in new[] { "pic0.png", "pic1.png" })
|
||||||
|
{
|
||||||
|
var backgroundPath = Path.Combine(sceSys, candidate);
|
||||||
|
if (File.Exists(backgroundPath))
|
||||||
|
{
|
||||||
|
return backgroundPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GameNameFor(string ebootPath)
|
private static string GameNameFor(string ebootPath)
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
@@ -339,26 +646,125 @@ public partial class MainWindow : Window
|
|||||||
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
|
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Game context menu ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects the tile under the pointer before its context menu opens, and
|
||||||
|
/// suppresses the menu on empty grid space.
|
||||||
|
/// </summary>
|
||||||
|
private void OnGameContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||||
|
{
|
||||||
|
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>(includeSelf: true);
|
||||||
|
if (item?.DataContext is not GameEntry game)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameList.SelectedItem = game;
|
||||||
|
CtxLaunch.IsEnabled = !_isRunning;
|
||||||
|
CtxCopyTitleId.IsEnabled = game.TitleId is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSelectedGameFolder()
|
||||||
|
{
|
||||||
|
if (GameList.SelectedItem is not GameEntry game)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "explorer.exe",
|
||||||
|
Arguments = $"/select,\"{game.Path}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (Path.GetDirectoryName(game.Path) is { } directory)
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = OperatingSystem.IsMacOS() ? "open" : "xdg-open",
|
||||||
|
Arguments = $"\"{directory}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusBarRight.Text = $"Could not open folder: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyToClipboardAsync(string? text, string what)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text) || Clipboard is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clipboard.SetTextAsync(text);
|
||||||
|
StatusBarRight.Text = $"{what} copied to clipboard.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSelectedFromLibrary()
|
||||||
|
{
|
||||||
|
if (GameList.SelectedItem is not GameEntry game)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_settings.ExcludedGames.Add(game.Path);
|
||||||
|
_settings.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
GameList.SelectedItem = null;
|
||||||
|
RefreshVisibleGames();
|
||||||
|
StatusBarRight.Text = $"Removed “{game.Name}” from the library. Re-add its folder to restore it.";
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshVisibleGames()
|
private void RefreshVisibleGames()
|
||||||
{
|
{
|
||||||
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||||
var selected = GameList.SelectedItem as GameEntry;
|
var selectedPath = (GameList.SelectedItem as GameEntry)?.Path;
|
||||||
|
|
||||||
_visibleGames.Clear();
|
_visibleGames.Clear();
|
||||||
foreach (var game in _allGames)
|
foreach (var game in _allGames)
|
||||||
{
|
{
|
||||||
if (query.Length == 0 ||
|
if (query.Length == 0 ||
|
||||||
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase))
|
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(game.TitleId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
{
|
{
|
||||||
_visibleGames.Add(game);
|
_visibleGames.Add(game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GameCountText.Text = _visibleGames.Count.ToString();
|
GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
|
||||||
if (selected is not null && _visibleGames.Contains(selected))
|
|
||||||
|
if (selectedPath is not null &&
|
||||||
|
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
is { } reselected)
|
||||||
{
|
{
|
||||||
GameList.SelectedItem = selected;
|
GameList.SelectedItem = reselected;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmptyState.IsVisible = _visibleGames.Count == 0;
|
||||||
|
if (_visibleGames.Count == 0)
|
||||||
|
{
|
||||||
|
var hasFilter = query.Length > 0;
|
||||||
|
EmptyStateTitle.Text = hasFilter ? "No games match your search" : "Your library is empty";
|
||||||
|
EmptyStateHint.Text = hasFilter
|
||||||
|
? $"Nothing in the library matches “{query}”."
|
||||||
|
: "Add a folder containing your games to get started.";
|
||||||
|
EmptyAddFolderButton.IsVisible = !hasFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSelectedGame();
|
UpdateSelectedGame();
|
||||||
@@ -370,16 +776,59 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
SelectedGameTitle.Text = game.Name;
|
SelectedGameTitle.Text = game.Name;
|
||||||
SelectedGamePath.Text = game.Path;
|
SelectedGamePath.Text = game.Path;
|
||||||
|
SelectedCoverPanel.DataContext = game;
|
||||||
|
_ = UpdateBackdropAsync(game);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedGameTitle.Text = "No game selected";
|
SelectedGameTitle.Text = "No game selected";
|
||||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
||||||
|
SelectedCoverPanel.DataContext = null;
|
||||||
|
_ = UpdateBackdropAsync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fades the window backdrop to the selected game's key art. The image
|
||||||
|
/// decodes off the UI thread and is cached on the entry; a newer
|
||||||
|
/// selection cancels the fade-in of an older one.
|
||||||
|
/// </summary>
|
||||||
|
private async Task UpdateBackdropAsync(GameEntry? game)
|
||||||
|
{
|
||||||
|
var generation = ++_backdropGeneration;
|
||||||
|
BackdropImage.Opacity = 0;
|
||||||
|
|
||||||
|
if (game?.BackgroundPath is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.Background is null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = game.BackgroundPath;
|
||||||
|
game.Background = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(path);
|
||||||
|
return Bitmap.DecodeToWidth(stream, 1600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return; // undecodable key art: keep the plain background
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generation == _backdropGeneration)
|
||||||
|
{
|
||||||
|
BackdropImage.Source = game.Background;
|
||||||
|
BackdropImage.Opacity = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Launching ----
|
// ---- Launching ----
|
||||||
|
|
||||||
private async Task OpenFileAsync()
|
private async Task OpenFileAsync()
|
||||||
@@ -448,6 +897,7 @@ public partial class MainWindow : Window
|
|||||||
arguments.Add(ebootPath);
|
arguments.Add(ebootPath);
|
||||||
|
|
||||||
_consoleLines.Clear();
|
_consoleLines.Clear();
|
||||||
|
ConsoleToggle.IsChecked = true;
|
||||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
|
|||||||
@@ -21,8 +21,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
|
|
||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- The DualSense HID reader is shared with the emulator's pad HLE. It is
|
||||||
|
dependency-free, so it is compiled in directly rather than pulling a
|
||||||
|
reference to all of SharpEmu.Libs into the launcher. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
||||||
|
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -138,6 +138,51 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryReadUInt16(ulong address, out ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||||
|
if (!Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWriteUInt16(ulong address, ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||||
|
return Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReadInt32(ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
|
||||||
|
{
|
||||||
|
if (checkNil && address == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryReadUInt32(ulong address, out uint value)
|
public bool TryReadUInt32(ulong address, out uint value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
@@ -158,6 +203,13 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return Memory.TryWrite(address, buffer);
|
return Memory.TryWrite(address, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryWriteInt64(ulong address, long value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||||
|
return Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryReadUInt64(ulong address, out ulong value)
|
public bool TryReadUInt64(ulong address, out ulong value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
@@ -224,4 +276,23 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int SetReturn(int result, Type? cast = null)
|
||||||
|
{
|
||||||
|
var value = cast switch
|
||||||
|
{
|
||||||
|
null => (ulong)result,
|
||||||
|
_ when cast == typeof(long) => (ulong)(long)result,
|
||||||
|
_ => throw new NotSupportedException(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this[CpuRegister.Rax] = unchecked(value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SetReturn(OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
var handler = CreateHandler(type, method, instances);
|
var handler = CreateHandler(type, method, instances);
|
||||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
return false;
|
return false;
|
||||||
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if ((export.Target & context.TargetGeneration) == 0)
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
{
|
{
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context.ClearRaxWriteFlag();
|
context.ClearRaxWriteFlag();
|
||||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ public static class AgcExports
|
|||||||
private const uint SpiShaderPgmHiEs = 0xC9;
|
private const uint SpiShaderPgmHiEs = 0xC9;
|
||||||
private const uint SpiShaderPgmLoLs = 0x148;
|
private const uint SpiShaderPgmLoLs = 0x148;
|
||||||
private const uint SpiShaderPgmHiLs = 0x149;
|
private const uint SpiShaderPgmHiLs = 0x149;
|
||||||
|
private const uint SpiShaderPgmLoGs = 0x8A;
|
||||||
|
private const uint SpiShaderPgmHiGs = 0x8B;
|
||||||
private const uint SpiPsInputEna = 0x1B3;
|
private const uint SpiPsInputEna = 0x1B3;
|
||||||
private const uint SpiPsInputAddr = 0x1B4;
|
private const uint SpiPsInputAddr = 0x1B4;
|
||||||
private const uint ComputePgmLo = 0x20C;
|
private const uint ComputePgmLo = 0x20C;
|
||||||
@@ -115,6 +117,7 @@ public static class AgcExports
|
|||||||
private const uint RegisterDefaultsVersion7 = 7;
|
private const uint RegisterDefaultsVersion7 = 7;
|
||||||
private const uint RegisterDefaultsVersion8 = 8;
|
private const uint RegisterDefaultsVersion8 = 8;
|
||||||
private const uint RegisterDefaultsVersion10 = 10;
|
private const uint RegisterDefaultsVersion10 = 10;
|
||||||
|
private const uint RegisterDefaultsVersion13 = 13;
|
||||||
private const int RegisterDefaultsSize = 0x40;
|
private const int RegisterDefaultsSize = 0x40;
|
||||||
private const int RegisterDefaultBlockSize = 16 * 8;
|
private const int RegisterDefaultBlockSize = 16 * 8;
|
||||||
|
|
||||||
@@ -139,6 +142,7 @@ public static class AgcExports
|
|||||||
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
||||||
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
||||||
private static readonly object _submitTraceGate = new();
|
private static readonly object _submitTraceGate = new();
|
||||||
|
private static readonly object _textureHashTraceGate = new();
|
||||||
private static readonly HashSet<uint> _tracedDcbSizes = new();
|
private static readonly HashSet<uint> _tracedDcbSizes = new();
|
||||||
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
|
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
|
||||||
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
|
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
|
||||||
@@ -146,6 +150,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
|
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
|
||||||
private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
|
private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
|
||||||
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
||||||
|
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
|
||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
@@ -165,6 +170,10 @@ public static class AgcExports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
private static readonly bool _traceTextureHashes = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_HASHES"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private static long _dcbWriteDataTraceCount;
|
private static long _dcbWriteDataTraceCount;
|
||||||
private static long _dcbWaitRegMemTraceCount;
|
private static long _dcbWaitRegMemTraceCount;
|
||||||
private static long _createShaderTraceCount;
|
private static long _createShaderTraceCount;
|
||||||
@@ -404,11 +413,11 @@ public static class AgcExports
|
|||||||
var version = (uint)ctx[CpuRegister.Rsi];
|
var version = (uint)ctx[CpuRegister.Rsi];
|
||||||
if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version))
|
if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -439,19 +448,19 @@ public static class AgcExports
|
|||||||
var codeAddress = ctx[CpuRegister.Rdx];
|
var codeAddress = ctx[CpuRegister.Rdx];
|
||||||
if (headerAddress == 0 || codeAddress == 0)
|
if (headerAddress == 0 || codeAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(headerAddress, out var fileHeader) ||
|
if (!ctx.TryReadUInt32(headerAddress, out var fileHeader) ||
|
||||||
!ctx.TryReadUInt32(headerAddress + sizeof(uint), out var version))
|
!ctx.TryReadUInt32(headerAddress + sizeof(uint), out var version))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileHeader != ShaderFileHeader || version != ShaderVersion)
|
if (fileHeader != ShaderFileHeader || version != ShaderVersion)
|
||||||
{
|
{
|
||||||
TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}");
|
TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) ||
|
if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) ||
|
||||||
@@ -462,12 +471,12 @@ public static class AgcExports
|
|||||||
!RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) ||
|
!RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) ||
|
||||||
!ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress))
|
!ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(headerAddress + ShaderUserDataOffset, out var userDataAddress))
|
if (!ctx.TryReadUInt64(headerAddress + ShaderUserDataOffset, out var userDataAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userDataAddress != 0 &&
|
if (userDataAddress != 0 &&
|
||||||
@@ -477,18 +486,18 @@ public static class AgcExports
|
|||||||
!RelocatePointerField(ctx, userDataAddress + 0x18) ||
|
!RelocatePointerField(ctx, userDataAddress + 0x18) ||
|
||||||
!RelocatePointerField(ctx, userDataAddress + 0x20)))
|
!RelocatePointerField(ctx, userDataAddress + 0x20)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress))
|
if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destinationAddress != 0 &&
|
if (destinationAddress != 0 &&
|
||||||
!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_submitTraceGate)
|
lock (_submitTraceGate)
|
||||||
@@ -564,14 +573,14 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0)
|
if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadByte(geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
|
if (!ctx.TryReadByte(geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
|
||||||
!ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
!ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
||||||
specialsAddress == 0)
|
specialsAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) ||
|
if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) ||
|
||||||
@@ -581,7 +590,7 @@ public static class AgcExports
|
|||||||
!ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) ||
|
!ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) ||
|
||||||
!ctx.TryWriteUInt32(ucRegistersAddress + 20, primitiveType))
|
!ctx.TryWriteUInt32(ucRegistersAddress + 20, primitiveType))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}");
|
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}");
|
||||||
@@ -602,13 +611,13 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (registersAddress == 0 || geometryShaderAddress == 0)
|
if (registersAddress == 0 || geometryShaderAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) ||
|
if (!ctx.TryReadUInt64(geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) ||
|
||||||
!ctx.TryReadUInt32(geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount))
|
!ctx.TryReadUInt32(geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong inputSemanticsAddress = 0;
|
ulong inputSemanticsAddress = 0;
|
||||||
@@ -616,7 +625,7 @@ public static class AgcExports
|
|||||||
(!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) ||
|
(!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) ||
|
||||||
!ctx.TryReadUInt32(pixelShaderAddress + ShaderNumInputSemanticsOffset, out _)))
|
!ctx.TryReadUInt32(pixelShaderAddress + ShaderNumInputSemanticsOffset, out _)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint i = 0; i < 32; i++)
|
for (uint i = 0; i < 32; i++)
|
||||||
@@ -638,7 +647,7 @@ public static class AgcExports
|
|||||||
if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) ||
|
if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) ||
|
||||||
!ctx.TryWriteUInt32(destination + sizeof(uint), value))
|
!ctx.TryWriteUInt32(destination + sizeof(uint), value))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +668,7 @@ public static class AgcExports
|
|||||||
var type = (int)ctx[CpuRegister.Rdx];
|
var type = (int)ctx[CpuRegister.Rdx];
|
||||||
if (outputAddress == 0 || commandAddress == 0)
|
if (outputAddress == 0 || commandAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var payloadAddress = commandAddress + 8;
|
var payloadAddress = commandAddress + 8;
|
||||||
@@ -667,7 +676,7 @@ public static class AgcExports
|
|||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(commandAddress, out var header))
|
if (!ctx.TryReadUInt32(commandAddress, out var header))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u
|
payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u
|
||||||
@@ -677,7 +686,7 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (!ctx.TryWriteUInt64(outputAddress, payloadAddress))
|
if (!ctx.TryWriteUInt64(outputAddress, payloadAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTraceHotPath(ref _packetPayloadTraceCount))
|
if (ShouldTraceHotPath(ref _packetPayloadTraceCount))
|
||||||
@@ -702,20 +711,20 @@ public static class AgcExports
|
|||||||
var dwordCount = (uint)ctx[CpuRegister.Rsi];
|
var dwordCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
if (commandBufferAddress == 0 || dwordCount < 2 || dwordCount > 0x4001)
|
if (commandBufferAddress == 0 || dwordCount < 2 || dwordCount > 0x4001)
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) ||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) ||
|
||||||
!ctx.TryWriteUInt32(commandAddress, Pm4(dwordCount, ItNop, RZero)))
|
!ctx.TryWriteUInt32(commandAddress, Pm4(dwordCount, ItNop, RZero)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint index = 1; index < dwordCount; index++)
|
for (uint index = 1; index < dwordCount; index++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteUInt32(commandAddress + ((ulong)index * sizeof(uint)), 0))
|
if (!ctx.TryWriteUInt32(commandAddress + ((ulong)index * sizeof(uint)), 0))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1735,12 +1744,12 @@ public static class AgcExports
|
|||||||
op != ItNop ||
|
op != ItNop ||
|
||||||
register != RDmaData)
|
register != RDmaData)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandAddress + 16, destinationAddress)
|
return ctx.TryWriteUInt64(commandAddress + 16, destinationAddress)
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1754,7 +1763,7 @@ public static class AgcExports
|
|||||||
var address = ctx[CpuRegister.Rsi];
|
var address = ctx[CpuRegister.Rsi];
|
||||||
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldOffset = op == ItWaitRegMem
|
var fieldOffset = op == ItWaitRegMem
|
||||||
@@ -1764,12 +1773,12 @@ public static class AgcExports
|
|||||||
: 0;
|
: 0;
|
||||||
if (fieldOffset == 0)
|
if (fieldOffset == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
|
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1785,12 +1794,12 @@ public static class AgcExports
|
|||||||
op != ItNop ||
|
op != ItNop ||
|
||||||
register != RReleaseMem)
|
register != RReleaseMem)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandAddress + 12, address)
|
return ctx.TryWriteUInt64(commandAddress + 12, address)
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1931,11 +1940,11 @@ public static class AgcExports
|
|||||||
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||||
userData))
|
userData))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}");
|
TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1952,11 +1961,11 @@ public static class AgcExports
|
|||||||
eventId,
|
eventId,
|
||||||
KernelEventQueueCompatExports.KernelEventFilterGraphics))
|
KernelEventQueueCompatExports.KernelEventFilterGraphics))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
|
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1971,7 +1980,7 @@ public static class AgcExports
|
|||||||
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
||||||
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
|
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tracePackets = false;
|
var tracePackets = false;
|
||||||
@@ -2011,7 +2020,7 @@ public static class AgcExports
|
|||||||
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
|
||||||
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
|
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tracePackets = false;
|
var tracePackets = false;
|
||||||
@@ -2057,16 +2066,16 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(outAddress, 256))
|
if (!ctx.TryWriteUInt32(outAddress, 256))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private const uint DefaultAgcOwner = 1;
|
private const uint DefaultAgcOwner = 1;
|
||||||
@@ -2081,16 +2090,16 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (ownerAddress == 0)
|
if (ownerAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
|
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -2110,7 +2119,7 @@ public static class AgcExports
|
|||||||
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
||||||
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -2125,7 +2134,7 @@ public static class AgcExports
|
|||||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -3744,7 +3753,6 @@ public static class AgcExports
|
|||||||
var sourceWidth = descriptor.TileMode == 0
|
var sourceWidth = descriptor.TileMode == 0
|
||||||
? GetLinearTexturePitch(
|
? GetLinearTexturePitch(
|
||||||
Math.Max(descriptor.Width, descriptor.Pitch),
|
Math.Max(descriptor.Width, descriptor.Pitch),
|
||||||
descriptor.Height,
|
|
||||||
descriptor.Format)
|
descriptor.Format)
|
||||||
: descriptor.Width;
|
: descriptor.Width;
|
||||||
var sourceByteCount = GetTextureByteCount(
|
var sourceByteCount = GetTextureByteCount(
|
||||||
@@ -3761,7 +3769,7 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (!isStorage &&
|
if (!isStorage &&
|
||||||
descriptor.Address != 0 &&
|
descriptor.Address != 0 &&
|
||||||
VulkanVideoPresenter.IsGuestImageAvailable(
|
VulkanVideoPresenter.IsGpuGuestImageAvailable(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Format,
|
descriptor.Format,
|
||||||
descriptor.NumberType))
|
descriptor.NumberType))
|
||||||
@@ -3822,6 +3830,8 @@ public static class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TraceTextureHash(descriptor, source);
|
||||||
|
|
||||||
var nonZero = 0;
|
var nonZero = 0;
|
||||||
for (var i = 0; i < source.Length; i++)
|
for (var i = 0; i < source.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -3881,6 +3891,34 @@ public static class AgcExports
|
|||||||
MipLevel: 0);
|
MipLevel: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceTextureHash(TextureDescriptor descriptor, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
if (!_traceTextureHashes ||
|
||||||
|
descriptor.Address == 0 ||
|
||||||
|
descriptor.Width > 256 ||
|
||||||
|
descriptor.Height > 256)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = ComputeFingerprint(source);
|
||||||
|
var key = (descriptor.Address, descriptor.Width, descriptor.Height);
|
||||||
|
lock (_textureHashTraceGate)
|
||||||
|
{
|
||||||
|
if (_tracedTextureHashes.TryGetValue(key, out var previousHash) &&
|
||||||
|
previousHash == hash)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tracedTextureHashes[key] = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] agc.texture_hash addr=0x{descriptor.Address:X16} " +
|
||||||
|
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||||
descriptor.Count >= 4
|
descriptor.Count >= 4
|
||||||
? new VulkanGuestSampler(
|
? new VulkanGuestSampler(
|
||||||
@@ -4524,23 +4562,17 @@ public static class AgcExports
|
|||||||
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format)
|
private static uint GetLinearTexturePitch(uint pitch, uint format)
|
||||||
{
|
{
|
||||||
var bytesPerTexel = GetTextureBytesPerTexel(format);
|
var bytesPerTexel = GetTextureBytesPerTexel(format);
|
||||||
if (bytesPerTexel == 0 || height == 0)
|
if (bytesPerTexel == 0)
|
||||||
{
|
{
|
||||||
return pitch;
|
return pitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel);
|
// GFX10 ADDR_SW_LINEAR aligns each row to 256 bytes.
|
||||||
var alignedPitch = AlignUp(pitch, pitchAlignment);
|
var pitchAlignment = Math.Max(1UL, 256UL / bytesPerTexel);
|
||||||
var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel);
|
return checked((uint)AlignUp(pitch, pitchAlignment));
|
||||||
while ((alignedPitch * height) % sliceAlignment != 0)
|
|
||||||
{
|
|
||||||
alignedPitch += pitchAlignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checked((uint)alignedPitch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment) =>
|
private static ulong AlignUp(ulong value, ulong alignment) =>
|
||||||
@@ -4830,9 +4862,13 @@ public static class AgcExports
|
|||||||
var type = (fields[3] >> 28) & 0xFu;
|
var type = (fields[3] >> 28) & 0xFu;
|
||||||
var baseLevel = (fields[3] >> 12) & 0xFu;
|
var baseLevel = (fields[3] >> 12) & 0xFu;
|
||||||
var lastLevel = (fields[3] >> 16) & 0xFu;
|
var lastLevel = (fields[3] >> 16) & 0xFu;
|
||||||
var pitch = fields.Count >= 5
|
// The 128-bit RDNA2 2D resource derives pitch[12:0] from width;
|
||||||
? ((fields[4] >> 13) & 0x3FFFu) + 1
|
// the optional extension word only supplies pitch[13].
|
||||||
: width;
|
var pitch = width;
|
||||||
|
if (fields.Count >= 5)
|
||||||
|
{
|
||||||
|
pitch = ((((width - 1) & 0x1FFFu) | (((fields[4] >> 13) & 1u) << 13)) + 1);
|
||||||
|
}
|
||||||
var dstSelect = fields[3] & 0xFFFu;
|
var dstSelect = fields[3] & 0xFFFu;
|
||||||
if (address == 0 || width == 0 || height == 0)
|
if (address == 0 || width == 0 || height == 0)
|
||||||
{
|
{
|
||||||
@@ -5081,6 +5117,7 @@ public static class AgcExports
|
|||||||
0 => ComputePgmLo,
|
0 => ComputePgmLo,
|
||||||
1 => SpiShaderPgmLoPs,
|
1 => SpiShaderPgmLoPs,
|
||||||
2 or 6 => SpiShaderPgmLoEs,
|
2 or 6 => SpiShaderPgmLoEs,
|
||||||
|
4 => SpiShaderPgmLoGs,
|
||||||
7 => SpiShaderPgmLoLs,
|
7 => SpiShaderPgmLoLs,
|
||||||
_ => 0u,
|
_ => 0u,
|
||||||
};
|
};
|
||||||
@@ -5089,6 +5126,7 @@ public static class AgcExports
|
|||||||
0 => ComputePgmHi,
|
0 => ComputePgmHi,
|
||||||
1 => SpiShaderPgmHiPs,
|
1 => SpiShaderPgmHiPs,
|
||||||
2 or 6 => SpiShaderPgmHiEs,
|
2 or 6 => SpiShaderPgmHiEs,
|
||||||
|
4 => SpiShaderPgmHiGs,
|
||||||
7 => SpiShaderPgmHiLs,
|
7 => SpiShaderPgmHiLs,
|
||||||
_ => 0u,
|
_ => 0u,
|
||||||
};
|
};
|
||||||
@@ -5105,7 +5143,7 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
||||||
shaderType is 2 or 6;
|
shaderType is 2 or 4 or 6;
|
||||||
|
|
||||||
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
||||||
{
|
{
|
||||||
@@ -5113,13 +5151,13 @@ public static class AgcExports
|
|||||||
var registersAddress = ctx[CpuRegister.Rsi];
|
var registersAddress = ctx[CpuRegister.Rsi];
|
||||||
if (commandAddress == 0 || registersAddress == 0)
|
if (commandAddress == 0 || registersAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) ||
|
if (!ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) ||
|
||||||
!ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32)))
|
!ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}");
|
TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}");
|
||||||
@@ -5133,13 +5171,13 @@ public static class AgcExports
|
|||||||
var registerCount = (uint)ctx[CpuRegister.Rsi];
|
var registerCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
if (commandAddress == 0)
|
if (commandAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(commandAddress + 4, out var currentCount) ||
|
if (!ctx.TryReadUInt32(commandAddress + 4, out var currentCount) ||
|
||||||
!ctx.TryWriteUInt32(commandAddress + 4, currentCount + registerCount))
|
!ctx.TryWriteUInt32(commandAddress + 4, currentCount + registerCount))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}");
|
TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}");
|
||||||
@@ -5252,7 +5290,8 @@ public static class AgcExports
|
|||||||
return version is
|
return version is
|
||||||
RegisterDefaultsVersion7 or
|
RegisterDefaultsVersion7 or
|
||||||
RegisterDefaultsVersion8 or
|
RegisterDefaultsVersion8 or
|
||||||
RegisterDefaultsVersion10;
|
RegisterDefaultsVersion10 or
|
||||||
|
RegisterDefaultsVersion13;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetRegisterDefaultsAllocation(
|
private static bool TryGetRegisterDefaultsAllocation(
|
||||||
@@ -5382,12 +5421,6 @@ public static class AgcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint Pm4(uint lengthDwords, uint op, uint register) =>
|
private static uint Pm4(uint lengthDwords, uint op, uint register) =>
|
||||||
0xC0000000u |
|
0xC0000000u |
|
||||||
((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) |
|
((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) |
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
@@ -36,9 +35,9 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||||
directResourceCount > MaxMetadataEntries)
|
directResourceCount > MaxMetadataEntries)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -47,8 +46,7 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
var resourceCounts = new ushort[ResourceClassCount];
|
var resourceCounts = new ushort[ResourceClassCount];
|
||||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt16(
|
if (!ctx.TryReadUInt16(
|
||||||
ctx,
|
|
||||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||||
out resourceCounts[resourceClass]) ||
|
out resourceCounts[resourceClass]) ||
|
||||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||||
@@ -67,7 +65,7 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint type = 0; type < directResourceCount; type++)
|
for (uint type = 0; type < directResourceCount; type++)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -95,8 +93,7 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint slot = 0; slot < count; slot++)
|
for (uint slot = 0; slot < count; slot++)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt16(
|
if (!ctx.TryReadUInt16(
|
||||||
ctx,
|
|
||||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||||
out var sharp))
|
out var sharp))
|
||||||
{
|
{
|
||||||
@@ -124,17 +121,4 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
resources);
|
resources);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
|
||||||
if (!ctx.Memory.TryRead(address, bytes))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public static class AppContentExports
|
|||||||
var valueAddress = ctx[CpuRegister.Rsi];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
int value;
|
int value;
|
||||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
|||||||
}
|
}
|
||||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -168,12 +168,6 @@ public static class AppContentExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceAppContent(string message)
|
private static void TraceAppContent(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ public static class AudioOut2Exports
|
|||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
if (paramAddress == 0)
|
if (paramAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
@@ -48,8 +47,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(paramAddress, param)
|
return ctx.Memory.TryWrite(paramAddress, param)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -63,7 +62,7 @@ public static class AudioOut2Exports
|
|||||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||||
@@ -74,8 +73,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -91,13 +90,13 @@ public static class AudioOut2Exports
|
|||||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -105,7 +104,7 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2ContextDestroy",
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JK2wamZPzwM",
|
Nid = "JK2wamZPzwM",
|
||||||
@@ -120,14 +119,14 @@ public static class AudioOut2Exports
|
|||||||
var contextAddress = ctx[CpuRegister.Rcx];
|
var contextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -141,7 +140,7 @@ public static class AudioOut2Exports
|
|||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle == 0 || stateAddress == 0)
|
if (handle == 0 || stateAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = (int)((handle >> 16) & 0xFF);
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
@@ -154,8 +153,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(stateAddress, state)
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -168,7 +167,7 @@ public static class AudioOut2Exports
|
|||||||
var infoAddress = ctx[CpuRegister.Rdi];
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (infoAddress == 0)
|
if (infoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[0x40];
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
@@ -178,8 +177,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info)
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -187,14 +186,14 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2PortDestroy",
|
ExportName = "sceAudioOut2PortDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IaZXJ9M79uo",
|
Nid = "IaZXJ9M79uo",
|
||||||
ExportName = "sceAudioOut2UserDestroy",
|
ExportName = "sceAudioOut2UserDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xywYcRB7nbQ",
|
Nid = "xywYcRB7nbQ",
|
||||||
@@ -207,18 +206,12 @@ public static class AudioOut2Exports
|
|||||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using SharpEmu.HLE;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ public static class AudioOutExports
|
|||||||
ExportName = "sceAudioOutInit",
|
ExportName = "sceAudioOutInit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut")]
|
LibraryName = "libSceAudioOut")]
|
||||||
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0);
|
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ekNvsT22rsY",
|
Nid = "ekNvsT22rsY",
|
||||||
@@ -101,7 +100,7 @@ public static class AudioOutExports
|
|||||||
if (bufferLength == 0 || frequency == 0 ||
|
if (bufferLength == 0 || frequency == 0 ||
|
||||||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
WinMmAudioPort? backend = null;
|
WinMmAudioPort? backend = null;
|
||||||
@@ -133,7 +132,7 @@ public static class AudioOutExports
|
|||||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||||
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||||
$"{bufferLength} frames, backend={backendName}");
|
$"{bufferLength} frames, backend={backendName}");
|
||||||
return SetReturn(ctx, handle);
|
return ctx.SetReturn(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -146,11 +145,11 @@ public static class AudioOutExports
|
|||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!Ports.TryRemove(handle, out var port))
|
if (!Ports.TryRemove(handle, out var port))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
port.Dispose();
|
port.Dispose();
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -164,12 +163,12 @@ public static class AudioOutExports
|
|||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!Ports.TryGetValue(handle, out var port))
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceAddress == 0)
|
if (sourceAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||||
@@ -178,7 +177,7 @@ public static class AudioOutExports
|
|||||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||||
if (!ctx.Memory.TryRead(sourceAddress, source))
|
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port.Backend is null ||
|
if (port.Backend is null ||
|
||||||
@@ -192,7 +191,7 @@ public static class AudioOutExports
|
|||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -208,19 +207,12 @@ public static class AudioOutExports
|
|||||||
public static int AudioOutSetVolume(CpuContext ctx)
|
public static int AudioOutSetVolume(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return SetReturn(
|
return ctx.SetReturn(
|
||||||
ctx,
|
|
||||||
Ports.ContainsKey(handle)
|
Ports.ContainsKey(handle)
|
||||||
? 0
|
? 0
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
private static bool TryGetFormat(
|
||||||
int rawFormat,
|
int rawFormat,
|
||||||
out int channels,
|
out int channels,
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ public static class AvPlayerExports
|
|||||||
var dataAddress = ctx[CpuRegister.Rsi];
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return SetReturn(
|
return ctx.SetReturn(
|
||||||
ctx,
|
|
||||||
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
||||||
? 0
|
? 0
|
||||||
: InvalidParameters);
|
: InvalidParameters);
|
||||||
@@ -63,15 +62,7 @@ public static class AvPlayerExports
|
|||||||
{
|
{
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return SetReturn(
|
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||||
ctx,
|
|
||||||
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,17 +104,17 @@ public static class FiberExports
|
|||||||
var optParam = ctx[CpuRegister.Rdi];
|
var optParam = ctx[CpuRegister.Rdi];
|
||||||
if (optParam == 0)
|
if (optParam == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((optParam & 7) != 0)
|
if ((optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorAlignment);
|
return ctx.SetReturn(FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
|||||||
var fiber = ctx[CpuRegister.Rdi];
|
var fiber = ctx[CpuRegister.Rdi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != FiberStateIdle)
|
if (state != FiberStateIdle)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
|||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnArgument = ctx[CpuRegister.Rdi];
|
var returnArgument = ctx[CpuRegister.Rdi];
|
||||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
|||||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousFiber = returnTarget.PreviousFiber;
|
previousFiber = returnTarget.PreviousFiber;
|
||||||
@@ -267,7 +267,7 @@ public static class FiberExports
|
|||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||||
@@ -278,7 +278,7 @@ public static class FiberExports
|
|||||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||||
@@ -286,7 +286,7 @@ public static class FiberExports
|
|||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
|||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -334,22 +334,22 @@ public static class FiberExports
|
|||||||
var info = ctx[CpuRegister.Rsi];
|
var info = ctx[CpuRegister.Rsi];
|
||||||
if (info == 0)
|
if (info == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||||
@@ -359,10 +359,10 @@ public static class FiberExports
|
|||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
|||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (ctx[CpuRegister.Rdi] != 0)
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorState);
|
: ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
|||||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorState);
|
: ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberInitializeCore(
|
private static int FiberInitializeCore(
|
||||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fiber & 7) != 0 ||
|
if ((fiber & 7) != 0 ||
|
||||||
(contextAddress & 15) != 0 ||
|
(contextAddress & 15) != 0 ||
|
||||||
(optParam & 7) != 0)
|
(optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorAlignment);
|
return ctx.SetReturn(FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorRange);
|
return ctx.SetReturn(FiberErrorRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((contextSize & 15) != 0 ||
|
if ((contextSize & 15) != 0 ||
|
||||||
(contextAddress == 0 && contextSize != 0) ||
|
(contextAddress == 0 && contextSize != 0) ||
|
||||||
(contextAddress != 0 && contextSize == 0))
|
(contextAddress != 0 && contextSize == 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
if (optParam != 0 &&
|
||||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||||
@@ -510,14 +510,14 @@ public static class FiberExports
|
|||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
if (contextAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||||
@@ -532,7 +532,7 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberRunCore(
|
private static int FiberRunCore(
|
||||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
|||||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||||
if (attachResult != 0)
|
if (attachResult != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, attachResult);
|
return ctx.SetReturn(attachResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
|||||||
if ((isSwitch && previousFiber == 0) ||
|
if ((isSwitch && previousFiber == 0) ||
|
||||||
(!isSwitch && previousFiber != 0))
|
(!isSwitch && previousFiber != 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
if (previousFiber == fiber)
|
if (previousFiber == fiber)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
if (fields.State != FiberStateIdle)
|
if (fields.State != FiberStateIdle)
|
||||||
{
|
{
|
||||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
FiberContinuation targetContinuation;
|
FiberContinuation targetContinuation;
|
||||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
var callerContinuation = new FiberContinuation(
|
var callerContinuation = new FiberContinuation(
|
||||||
@@ -620,7 +620,7 @@ public static class FiberExports
|
|||||||
previousState != FiberStateRun ||
|
previousState != FiberStateRun ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations[previousFiber] = callerContinuation;
|
_continuations[previousFiber] = callerContinuation;
|
||||||
@@ -639,7 +639,7 @@ public static class FiberExports
|
|||||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
}
|
}
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed)
|
if (resumed)
|
||||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
|||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateInitialContinuation(
|
private static bool TryCreateInitialContinuation(
|
||||||
@@ -925,12 +925,6 @@ public static class FiberExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceFiber(string message)
|
private static void TraceFiber(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
optionAddress != 0 ||
|
optionAddress != 0 ||
|
||||||
!IsValidAttributes(attributes))
|
!IsValidAttributes(attributes))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
@@ -75,11 +75,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
{
|
{
|
||||||
_eventFlags.TryRemove(handle, out _);
|
_eventFlags.TryRemove(handle, out _);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -92,7 +92,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var handle = ctx[CpuRegister.Rdi];
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
if (!_eventFlags.TryRemove(handle, out var state))
|
if (!_eventFlags.TryRemove(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -101,7 +101,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -116,7 +116,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var returnRip = GetCurrentReturnRip();
|
var returnRip = GetCurrentReturnRip();
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -127,7 +127,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -141,7 +141,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var pattern = ctx[CpuRegister.Rsi];
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -150,7 +150,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -167,29 +167,29 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,18 +209,18 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Enter(state.Gate);
|
Monitor.Enter(state.Gate);
|
||||||
@@ -228,7 +228,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
{
|
{
|
||||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, immediateWaitResult);
|
return ctx.SetReturn(immediateWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
@@ -236,7 +236,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
@@ -271,7 +271,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is null)
|
if (scheduler is null)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -296,7 +296,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
releaseWaiter = false;
|
releaseWaiter = false;
|
||||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
return SetReturn(ctx, pumpedWaitResult);
|
return ctx.SetReturn(pumpedWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||||
@@ -313,7 +313,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -333,7 +333,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -341,7 +341,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (waiterCountAddress != 0 &&
|
if (waiterCountAddress != 0 &&
|
||||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Bits = setPattern;
|
state.Bits = setPattern;
|
||||||
@@ -350,7 +350,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidAttributes(uint attributes)
|
private static bool IsValidAttributes(uint attributes)
|
||||||
@@ -452,13 +452,6 @@ public static class KernelEventFlagCompatExports
|
|||||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
var value = (int)result;
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceEventFlag(string message)
|
private static void TraceEventFlag(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelExports
|
public static class KernelExports
|
||||||
{
|
{
|
||||||
private static int _nextFileDescriptor = 2;
|
|
||||||
private static readonly object _cxaGate = new();
|
private static readonly object _cxaGate = new();
|
||||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||||
private static readonly object _coredumpGate = new();
|
private static readonly object _coredumpGate = new();
|
||||||
@@ -53,14 +52,7 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx)
|
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||||
{
|
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XKRegsFpEpk",
|
Nid = "XKRegsFpEpk",
|
||||||
@@ -173,11 +165,7 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int UnderscoreExit(CpuContext ctx)
|
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||||
{
|
|
||||||
_ = ctx;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -438,4 +426,13 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
||||||
|
{
|
||||||
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2903,7 +2903,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectionOut != 0 && !TryWriteInt32(ctx, protectionOut, region.Protection))
|
if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -2942,7 +2942,7 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
||||||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
!ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -3800,7 +3800,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var addr = argumentSource.NextGpArg();
|
var addr = argumentSource.NextGpArg();
|
||||||
if (addr != 0)
|
if (addr != 0)
|
||||||
{
|
{
|
||||||
_ = TryWriteInt32(ctx, addr, sb.Length);
|
_ = ctx.TryWriteInt32(addr, sb.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -4964,7 +4964,7 @@ public static class KernelMemoryCompatExports
|
|||||||
processedCount++;
|
processedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount))
|
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -5801,13 +5801,6 @@ public static class KernelMemoryCompatExports
|
|||||||
return (protect & expected) != 0;
|
return (protect & expected) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BitConverter.TryWriteBytes(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
|
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
|
||||||
{
|
{
|
||||||
if (fd is 0 or 1 or 2)
|
if (fd is 0 or 1 or 2)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -329,6 +327,12 @@ public static class KernelPthreadCompatExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "27bAgiJmOh0",
|
||||||
|
ExportName = "pthread_cond_timedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "m5-2bsNfv7s",
|
Nid = "m5-2bsNfv7s",
|
||||||
@@ -383,42 +387,42 @@ public static class KernelPthreadCompatExports
|
|||||||
var initRoutine = ctx[CpuRegister.Rsi];
|
var initRoutine = ctx[CpuRegister.Rsi];
|
||||||
if (onceAddress == 0 || initRoutine == 0)
|
if (onceAddress == 0 || initRoutine == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
|
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onceValue == PthreadOnceDone)
|
if (onceValue == PthreadOnceDone)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
var gate = GetPthreadOnceGate(onceAddress);
|
var gate = GetPthreadOnceGate(onceAddress);
|
||||||
var shouldCall = false;
|
var shouldCall = false;
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (onceValue == PthreadOnceInProgress)
|
while (onceValue == PthreadOnceInProgress)
|
||||||
{
|
{
|
||||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onceValue != PthreadOnceDone)
|
if (onceValue != PthreadOnceDone)
|
||||||
{
|
{
|
||||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
|
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldCall = true;
|
shouldCall = true;
|
||||||
@@ -434,21 +438,21 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
||||||
{
|
{
|
||||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
@@ -456,7 +460,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
@@ -1315,32 +1319,6 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryRead(address, bytes))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||||
{
|
{
|
||||||
var createdState = new PthreadMutexState
|
var createdState = new PthreadMutexState
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -279,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -326,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -496,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -739,7 +737,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -762,7 +760,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1112,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1451,24 +1449,4 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
payload[^1] = 0;
|
payload[^1] = 0;
|
||||||
return ctx.Memory.TryWrite(address, payload);
|
return ctx.Memory.TryWrite(address, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryRead(address, bytes))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
@@ -259,8 +256,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (timezoneAddress != 0 &&
|
if (timezoneAddress != 0 &&
|
||||||
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
|
||||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -617,7 +614,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||||
{
|
{
|
||||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||||
return address != 0 && TryWriteInt32(ctx, address, value);
|
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -827,7 +824,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -857,7 +854,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1058,7 +1055,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
public static int KernelStopUnloadModule(CpuContext ctx)
|
public static int KernelStopUnloadModule(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var resultAddress = ctx[CpuRegister.R9];
|
var resultAddress = ctx[CpuRegister.R9];
|
||||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1076,7 +1073,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
{
|
{
|
||||||
var modulePathAddress = ctx[CpuRegister.Rdi];
|
var modulePathAddress = ctx[CpuRegister.Rdi];
|
||||||
var resultAddress = ctx[CpuRegister.R9];
|
var resultAddress = ctx[CpuRegister.R9];
|
||||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1233,8 +1230,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
: 0;
|
: 0;
|
||||||
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
||||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1244,7 +1241,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
|
if (dstSecondsAddress != 0 && !ctx.TryWriteInt32(dstSecondsAddress, dstSeconds))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1304,7 +1301,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
_loadedSysmodules.Add(moduleId);
|
_loadedSysmodules.Add(moduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1394,7 +1391,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1431,7 +1428,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
||||||
for (var i = 0; i < writableCount; i++)
|
for (var i = 0; i < writableCount; i++)
|
||||||
{
|
{
|
||||||
if (!TryWriteInt32(ctx, handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
if (!ctx.TryWriteInt32(handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1472,26 +1469,42 @@ public static class KernelRuntimeCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
const int pageSize = 4096;
|
||||||
Span<byte> one = stackalloc byte[1];
|
const int inlineChunkSize = 64;
|
||||||
for (var i = 0; i < maxLength; i++)
|
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
||||||
|
var length = 0;
|
||||||
|
|
||||||
|
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
||||||
{
|
{
|
||||||
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
var current = address + (ulong)offset;
|
||||||
|
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||||
|
var chunkSize = Math.Min(
|
||||||
|
buffer.Length - length,
|
||||||
|
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
||||||
|
|
||||||
|
if (chunkSize <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (one[0] == 0)
|
var chunk = buffer.Slice(length, chunkSize);
|
||||||
|
if (!ctx.Memory.TryRead(current, chunk))
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nulIndex = chunk.IndexOf((byte)0);
|
||||||
|
if (nulIndex >= 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.Add(one[0]);
|
length += chunkSize;
|
||||||
|
offset += chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||||
@@ -1504,13 +1517,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
return unchecked(ctx.FsBase + offset);
|
return unchecked(ctx.FsBase + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static nint AllocateStackChkGuardObject()
|
private static nint AllocateStackChkGuardObject()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
initialCount > maxCount ||
|
initialCount > maxCount ||
|
||||||
optionAddress != 0)
|
optionAddress != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
@@ -69,11 +69,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -89,12 +89,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
@@ -103,30 +103,30 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
semaphore.WaitingThreads++;
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +142,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
@@ -155,12 +155,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,24 +176,24 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signalCount <= 0)
|
if (signalCount <= 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,25 +210,25 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setCount > semaphore.MaxCount)
|
if (setCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
semaphore.WaitingThreads = 0;
|
semaphore.WaitingThreads = 0;
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,18 +242,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
var value = (int)result;
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Network;
|
namespace SharpEmu.Libs.Network;
|
||||||
@@ -32,7 +30,7 @@ public static class Http2Exports
|
|||||||
|
|
||||||
if (poolSize == 0 || maxRequests <= 0)
|
if (poolSize == 0 || maxRequests <= 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, Http2ErrorInvalidArgument);
|
return ctx.SetReturn(Http2ErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = Interlocked.Increment(ref _nextContextId);
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
@@ -53,17 +51,11 @@ public static class Http2Exports
|
|||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!_contexts.TryRemove(id, out _))
|
if (!_contexts.TryRemove(id, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, Http2ErrorInvalidId);
|
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceHttp2("term", id, 0, 0, 0, 0);
|
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Network;
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ public static class HttpExports
|
|||||||
var poolSize = ctx[CpuRegister.Rdx];
|
var poolSize = ctx[CpuRegister.Rdx];
|
||||||
if (poolSize == 0)
|
if (poolSize == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, HttpErrorInvalidValue);
|
return ctx.SetReturn(HttpErrorInvalidValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = Interlocked.Increment(ref _nextContextId);
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
@@ -53,7 +52,7 @@ public static class HttpExports
|
|||||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!Contexts.ContainsKey(contextId))
|
if (!Contexts.ContainsKey(contextId))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, HttpErrorInvalidId);
|
return ctx.SetReturn(HttpErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userAgentAddress = ctx[CpuRegister.Rsi];
|
var userAgentAddress = ctx[CpuRegister.Rsi];
|
||||||
@@ -75,8 +74,8 @@ public static class HttpExports
|
|||||||
{
|
{
|
||||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return Templates.TryRemove(templateId, out _)
|
return Templates.TryRemove(templateId, out _)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, HttpErrorInvalidId);
|
: ctx.SetReturn(HttpErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -89,7 +88,7 @@ public static class HttpExports
|
|||||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!Contexts.TryRemove(contextId, out _))
|
if (!Contexts.TryRemove(contextId, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, HttpErrorInvalidId);
|
return ctx.SetReturn(HttpErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var pair in Templates)
|
foreach (var pair in Templates)
|
||||||
@@ -100,13 +99,7 @@ public static class HttpExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||||
|
|||||||
@@ -57,27 +57,27 @@ public static class NetCtlExports
|
|||||||
var natInfoAddress = ctx[CpuRegister.Rdi];
|
var natInfoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (natInfoAddress == 0)
|
if (natInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||||
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
||||||
if (size != NatInfoSize)
|
if (size != NatInfoSize)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
|
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
|
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
||||||
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -87,7 +87,7 @@ public static class NetCtlExports
|
|||||||
LibraryName = "libSceNetCtl")]
|
LibraryName = "libSceNetCtl")]
|
||||||
public static int NetCtlCheckCallback(CpuContext ctx)
|
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -100,14 +100,14 @@ public static class NetCtlExports
|
|||||||
var stateAddress = ctx[CpuRegister.Rdi];
|
var stateAddress = ctx[CpuRegister.Rdi];
|
||||||
if (stateAddress == 0)
|
if (stateAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -122,7 +122,7 @@ public static class NetCtlExports
|
|||||||
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
||||||
if (function == 0 || callbackIdAddress == 0)
|
if (function == 0 || callbackIdAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (CallbackGate)
|
lock (CallbackGate)
|
||||||
@@ -130,20 +130,20 @@ public static class NetCtlExports
|
|||||||
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||||
if (callbackId < 0)
|
if (callbackId < 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetCtlErrorNoSpace);
|
return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
||||||
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -157,7 +157,7 @@ public static class NetCtlExports
|
|||||||
var infoAddress = ctx[CpuRegister.Rsi];
|
var infoAddress = ctx[CpuRegister.Rsi];
|
||||||
if (infoAddress == 0)
|
if (infoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
return code switch
|
return code switch
|
||||||
@@ -177,7 +177,7 @@ public static class NetCtlExports
|
|||||||
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
||||||
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||||
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
|
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
|
||||||
_ => SetReturn(ctx, NetCtlErrorNotConnected),
|
_ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +185,8 @@ public static class NetCtlExports
|
|||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[count];
|
Span<byte> bytes = stackalloc byte[count];
|
||||||
return ctx.Memory.TryWrite(address, bytes)
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
|
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
@@ -194,8 +194,8 @@ public static class NetCtlExports
|
|||||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
return ctx.Memory.TryWrite(address, bytes)
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
|
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||||
@@ -203,8 +203,8 @@ public static class NetCtlExports
|
|||||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||||
return ctx.Memory.TryWrite(address, bytes)
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
|
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
|
||||||
@@ -217,13 +217,7 @@ public static class NetCtlExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(address, bytes)
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Network;
|
namespace SharpEmu.Libs.Network;
|
||||||
@@ -35,7 +33,7 @@ public static class NetExports
|
|||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
TraceNet("init", 0, 0, 0, 0);
|
TraceNet("init", 0, 0, 0, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -49,7 +47,7 @@ public static class NetExports
|
|||||||
_pools.Clear();
|
_pools.Clear();
|
||||||
_resolvers.Clear();
|
_resolvers.Clear();
|
||||||
TraceNet("term", 0, 0, 0, 0);
|
TraceNet("term", 0, 0, 0, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -65,7 +63,7 @@ public static class NetExports
|
|||||||
|
|
||||||
if (size <= 0)
|
if (size <= 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
@@ -90,11 +88,11 @@ public static class NetExports
|
|||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!_pools.TryRemove(id, out _))
|
if (!_pools.TryRemove(id, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -109,7 +107,7 @@ public static class NetExports
|
|||||||
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
if (flags != 0)
|
if (flags != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
@@ -131,8 +129,8 @@ public static class NetExports
|
|||||||
{
|
{
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return _resolvers.TryRemove(id, out _)
|
return _resolvers.TryRemove(id, out _)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, NetErrorBadFileDescriptor);
|
: ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -146,25 +144,19 @@ public static class NetExports
|
|||||||
var statusAddress = ctx[CpuRegister.Rsi];
|
var statusAddress = ctx[CpuRegister.Rsi];
|
||||||
if (statusAddress == 0)
|
if (statusAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_resolvers.TryGetValue(id, out var resolver))
|
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> status = stackalloc byte[sizeof(int)];
|
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||||
return ctx.Memory.TryWrite(statusAddress, status)
|
return ctx.Memory.TryWrite(statusAddress, status)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Network;
|
namespace SharpEmu.Libs.Network;
|
||||||
@@ -28,7 +26,7 @@ public static class SslExports
|
|||||||
var poolSize = ctx[CpuRegister.Rdi];
|
var poolSize = ctx[CpuRegister.Rdi];
|
||||||
if (poolSize == 0)
|
if (poolSize == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, SslErrorOutOfSize);
|
return ctx.SetReturn(SslErrorOutOfSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = Interlocked.Increment(ref _nextContextId);
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
@@ -49,11 +47,11 @@ public static class SslExports
|
|||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
if (!_contexts.TryRemove(id, out _))
|
if (!_contexts.TryRemove(id, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, SslErrorInvalidId);
|
return ctx.SetReturn(SslErrorInvalidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSsl("term", id, 0);
|
TraceSsl("term", id, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -65,13 +63,7 @@ public static class SslExports
|
|||||||
{
|
{
|
||||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
TraceSsl("close", id, 0);
|
TraceSsl("close", id, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceSsl(string operation, int id, ulong arg0)
|
private static void TraceSsl(string operation, int id, ulong arg0)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Ngs2;
|
namespace SharpEmu.Libs.Ngs2;
|
||||||
|
|
||||||
@@ -39,13 +38,13 @@ public static class Ngs2Exports
|
|||||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -53,7 +52,7 @@ public static class Ngs2Exports
|
|||||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -68,7 +67,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.Remove(handle))
|
if (!Systems.Remove(handle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rackHandles = Racks
|
var rackHandles = Racks
|
||||||
@@ -81,7 +80,7 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -98,19 +97,19 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -118,7 +117,7 @@ public static class Ngs2Exports
|
|||||||
Racks[handle] = new RackState(systemHandle, rackId);
|
Racks[handle] = new RackState(systemHandle, rackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -133,13 +132,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(handle))
|
if (!Racks.ContainsKey(handle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveRackLocked(handle);
|
RemoveRackLocked(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -156,7 +155,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(rackHandle))
|
if (!Racks.ContainsKey(rackHandle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing = Voices.FirstOrDefault(
|
var existing = Voices.FirstOrDefault(
|
||||||
@@ -164,20 +163,20 @@ public static class Ngs2Exports
|
|||||||
if (existing.Key != 0)
|
if (existing.Key != 0)
|
||||||
{
|
{
|
||||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -185,7 +184,7 @@ public static class Ngs2Exports
|
|||||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -197,9 +196,10 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return SetReturn(
|
return ctx.SetReturn(
|
||||||
ctx,
|
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||||
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
? 0
|
||||||
|
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,13 +224,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint i = 0; i < bufferInfoCount; i++)
|
for (uint i = 0; i < bufferInfoCount; i++)
|
||||||
@@ -239,14 +239,14 @@ public static class Ngs2Exports
|
|||||||
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferAddress != 0 && bufferSize != 0)
|
if (bufferAddress != 0 && bufferSize != 0)
|
||||||
{
|
{
|
||||||
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ public static class Ngs2Exports
|
|||||||
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||||
@@ -313,10 +313,4 @@ public static class Ngs2Exports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ public static class NpEntitlementAccessExports
|
|||||||
clear.Clear();
|
clear.Clear();
|
||||||
if (!ctx.Memory.TryWrite(bootParam, clear))
|
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -48,20 +48,14 @@ public static class NpEntitlementAccessExports
|
|||||||
emptyList.Clear();
|
emptyList.Clear();
|
||||||
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceNpEntitlementAccess(
|
TraceNpEntitlementAccess(
|
||||||
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
||||||
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceNpEntitlementAccess(string message)
|
private static void TraceNpEntitlementAccess(string message)
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ public static class NpManagerExports
|
|||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
if (stateAddress == 0)
|
if (stateAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
|
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
||||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -97,7 +97,7 @@ public static class NpManagerExports
|
|||||||
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
||||||
if (titleIdAddress == 0 || titleSecretAddress == 0)
|
if (titleIdAddress == 0 || titleSecretAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
|
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
|
||||||
@@ -105,17 +105,11 @@ public static class NpManagerExports
|
|||||||
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
||||||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
|
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Np;
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
@@ -22,13 +21,13 @@ public static class NpUniversalDataSystemExports
|
|||||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||||
if (parameterAddress == 0)
|
if (parameterAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
|
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> parameters = stackalloc byte[16];
|
Span<byte> parameters = stackalloc byte[16];
|
||||||
return ctx.Memory.TryRead(parameterAddress, parameters)
|
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -41,14 +40,14 @@ public static class NpUniversalDataSystemExports
|
|||||||
var contextAddress = ctx[CpuRegister.Rdi];
|
var contextAddress = ctx[CpuRegister.Rdi];
|
||||||
if (contextAddress == 0)
|
if (contextAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> context = stackalloc byte[sizeof(int)];
|
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||||
return ctx.Memory.TryWrite(contextAddress, context)
|
return ctx.Memory.TryWrite(contextAddress, context)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0, typeof(long))
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -59,13 +58,13 @@ public static class NpUniversalDataSystemExports
|
|||||||
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = Interlocked.Increment(ref _nextHandle);
|
var handle = Interlocked.Increment(ref _nextHandle);
|
||||||
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
|
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
|
||||||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
|
ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -75,24 +74,6 @@ public static class NpUniversalDataSystemExports
|
|||||||
LibraryName = "libSceNpUniversalDataSystem")]
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
if (address == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Np;
|
namespace SharpEmu.Libs.Np;
|
||||||
@@ -25,12 +23,12 @@ public static class NpWebApi2Exports
|
|||||||
|
|
||||||
if (httpContextId <= 0 || poolSize == 0)
|
if (httpContextId <= 0 || poolSize == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
|
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Exchange(ref _initialized, 1);
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -43,13 +41,7 @@ public static class NpWebApi2Exports
|
|||||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
Interlocked.Exchange(ref _initialized, 0);
|
Interlocked.Exchange(ref _initialized, 0);
|
||||||
TraceNpWebApi2("term", libraryContextId, 0);
|
TraceNpWebApi2("term", libraryContextId, 0);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||||
|
|||||||
@@ -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)
|
public static int PadInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
return SetReturn(ctx, 0);
|
DualSenseReader.EnsureStarted();
|
||||||
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -46,21 +47,24 @@ public static class PadExports
|
|||||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == -1)
|
if (userId == -1)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options");
|
DualSenseReader.EnsureStarted();
|
||||||
return SetReturn(ctx, PrimaryPadHandle);
|
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||||
|
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||||
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
|
||||||
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -72,8 +76,8 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return handle == PrimaryPadHandle
|
return handle == PrimaryPadHandle
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -87,12 +91,12 @@ public static class PadExports
|
|||||||
var informationAddress = ctx[CpuRegister.Rsi];
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (handle != PrimaryPadHandle)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (informationAddress == 0)
|
if (informationAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
||||||
@@ -107,8 +111,8 @@ public static class PadExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(informationAddress, information)
|
return ctx.Memory.TryWrite(informationAddress, information)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -122,17 +126,17 @@ public static class PadExports
|
|||||||
var dataAddress = ctx[CpuRegister.Rsi];
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle != PrimaryPadHandle)
|
if (handle != PrimaryPadHandle)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataAddress == 0)
|
if (dataAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WriteNeutralPadData(ctx, dataAddress)
|
return WriteNeutralPadData(ctx, dataAddress)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -147,17 +151,17 @@ public static class PadExports
|
|||||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
if (handle != PrimaryPadHandle)
|
if (handle != PrimaryPadHandle)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataAddress == 0 || count < 1 || count > 64)
|
if (dataAddress == 0 || count < 1 || count > 64)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WriteNeutralPadData(ctx, dataAddress)
|
return WriteNeutralPadData(ctx, dataAddress)
|
||||||
? SetReturn(ctx, 1)
|
? ctx.SetReturn(1)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -167,7 +171,84 @@ public static class PadExports
|
|||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadSetVibrationMode(CpuContext ctx)
|
public static int PadSetVibrationMode(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "yFVnOdGxvZY",
|
||||||
|
ExportName = "scePadSetVibration",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadSetVibration(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
|
||||||
|
Span<byte> parameter = stackalloc byte[2];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RR4novUEENY",
|
||||||
|
ExportName = "scePadSetLightBar",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadSetLightBar(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
|
||||||
|
Span<byte> color = stackalloc byte[4];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, color))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DscD1i9HX1w",
|
||||||
|
ExportName = "scePadResetLightBar",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadResetLightBar(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
DualSenseReader.ResetLightbar();
|
||||||
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||||
@@ -176,17 +257,33 @@ public static class PadExports
|
|||||||
data.Clear();
|
data.Clear();
|
||||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
|
||||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||||
|
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||||
|
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||||
|
|
||||||
|
if (DualSenseReader.TryGetState(out var pad))
|
||||||
|
{
|
||||||
|
buttons |= pad.Buttons;
|
||||||
|
// The controller stick wins whenever it is deflected past a
|
||||||
|
// small deadzone; otherwise any keyboard value stays.
|
||||||
|
leftX = MergeAxis(pad.LeftX, leftX);
|
||||||
|
leftY = MergeAxis(pad.LeftY, leftY);
|
||||||
|
rightX = MergeAxis(pad.RightX, rightX);
|
||||||
|
rightY = MergeAxis(pad.RightY, rightY);
|
||||||
|
l2 = Math.Max(l2, pad.L2);
|
||||||
|
r2 = Math.Max(r2, pad.R2);
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||||
data[0x04] = leftX;
|
data[0x04] = leftX;
|
||||||
data[0x05] = leftY;
|
data[0x05] = leftY;
|
||||||
data[0x06] = rightX;
|
data[0x06] = rightX;
|
||||||
data[0x07] = rightY;
|
data[0x07] = rightY;
|
||||||
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
data[0x08] = l2;
|
||||||
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
data[0x09] = r2;
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||||
data[0x4C] = 1;
|
data[0x4C] = 1;
|
||||||
var timestampTicks = Stopwatch.GetTimestamp();
|
var timestampTicks = Stopwatch.GetTimestamp();
|
||||||
@@ -201,12 +298,6 @@ public static class PadExports
|
|||||||
return ctx.Memory.TryWrite(dataAddress, data);
|
return ctx.Memory.TryWrite(dataAddress, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern short GetAsyncKeyState(int vKey);
|
private static extern short GetAsyncKeyState(int vKey);
|
||||||
|
|
||||||
@@ -260,4 +351,10 @@ public static class PadExports
|
|||||||
if (positive && !negative) return 255;
|
if (positive && !negative) return 255;
|
||||||
return 128;
|
return 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte MergeAxis(byte controller, byte keyboard)
|
||||||
|
{
|
||||||
|
const int Deadzone = 10;
|
||||||
|
return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ public static class PlayGoExports
|
|||||||
for (uint i = 0; i < entriesToWrite; i++)
|
for (uint i = 0; i < entriesToWrite; i++)
|
||||||
{
|
{
|
||||||
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||||
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
|
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ public static class PlayGoExports
|
|||||||
|
|
||||||
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
||||||
? chunkError
|
? chunkError
|
||||||
: TryWriteInt64(ctx, outEta, 0)
|
: ctx.TryWriteInt64(outEta, 0)
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ public static class PlayGoExports
|
|||||||
speed = _installSpeed;
|
speed = _installSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteInt32(ctx, outSpeed, speed)
|
return ctx.TryWriteInt32(outSpeed, speed)
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ public static class PlayGoExports
|
|||||||
var loci = new byte[numberOfEntries];
|
var loci = new byte[numberOfEntries];
|
||||||
for (uint i = 0; i < numberOfEntries; i++)
|
for (uint i = 0; i < numberOfEntries; i++)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -648,7 +648,7 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
for (uint i = 0; i < numberOfEntries; i++)
|
for (uint i = 0; i < numberOfEntries; i++)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -728,40 +728,6 @@ public static class PlayGoExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
|
||||||
if (!ctx.Memory.TryRead(address, buffer))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
|
||||||
return ctx.Memory.TryWrite(address, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
|
|
||||||
return ctx.Memory.TryWrite(address, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
|
||||||
return ctx.Memory.TryWrite(address, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TracePlayGo(string message)
|
private static void TracePlayGo(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.SaveData;
|
namespace SharpEmu.Libs.SaveData;
|
||||||
|
|
||||||
@@ -35,10 +34,10 @@ public static class SaveDataDialogExports
|
|||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorAlreadyInitialized);
|
return ctx.SetReturn(ErrorAlreadyInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -51,12 +50,12 @@ public static class SaveDataDialogExports
|
|||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
if (paramAddress == 0)
|
if (paramAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorArgNull);
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_status is not (StatusInitialized or StatusFinished))
|
if (_status is not (StatusInitialized or StatusFinished))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorNotInitialized);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||||
@@ -66,7 +65,7 @@ public static class SaveDataDialogExports
|
|||||||
// guest polling sees a finished dialog instead of spinning forever.
|
// guest polling sees a finished dialog instead of spinning forever.
|
||||||
Interlocked.Exchange(ref _status, StatusFinished);
|
Interlocked.Exchange(ref _status, StatusFinished);
|
||||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
||||||
return SetReturn(ctx, ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -74,21 +73,21 @@ public static class SaveDataDialogExports
|
|||||||
ExportName = "sceSaveDataDialogGetStatus",
|
ExportName = "sceSaveDataDialogGetStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "KK3Bdg1RWK0",
|
Nid = "KK3Bdg1RWK0",
|
||||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "en7gNVnh878",
|
Nid = "en7gNVnh878",
|
||||||
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
|
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(1);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yEiJ-qqr6Cg",
|
Nid = "yEiJ-qqr6Cg",
|
||||||
@@ -100,12 +99,12 @@ public static class SaveDataDialogExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rdi];
|
var resultAddress = ctx[CpuRegister.Rdi];
|
||||||
if (resultAddress == 0)
|
if (resultAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorArgNull);
|
return ctx.SetReturn(ErrorArgNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Volatile.Read(ref _status) != StatusFinished)
|
if (Volatile.Read(ref _status) != StatusFinished)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorNotFinished);
|
return ctx.SetReturn(ErrorNotFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> result = stackalloc byte[ResultSize];
|
Span<byte> result = stackalloc byte[ResultSize];
|
||||||
@@ -117,10 +116,10 @@ public static class SaveDataDialogExports
|
|||||||
|
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,10 +131,10 @@ public static class SaveDataDialogExports
|
|||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorNotRunning);
|
return ctx.SetReturn(ErrorNotRunning);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -147,12 +146,12 @@ public static class SaveDataDialogExports
|
|||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, ErrorNotInitialized);
|
return ctx.SetReturn(ErrorNotInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastMode = 0;
|
_lastMode = 0;
|
||||||
_lastUserData = 0;
|
_lastUserData = 0;
|
||||||
return SetReturn(ctx, ErrorOk);
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -160,14 +159,14 @@ public static class SaveDataDialogExports
|
|||||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hay1CfTmLyA",
|
Nid = "hay1CfTmLyA",
|
||||||
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||||
|
|
||||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
{
|
{
|
||||||
@@ -182,12 +181,6 @@ public static class SaveDataDialogExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceSaveDataDialog(string message)
|
private static void TraceSaveDataDialog(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -50,15 +50,15 @@ public static class SaveDataExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,18 +73,18 @@ public static class SaveDataExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rsi];
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
if (condAddress == 0 || resultAddress == 0)
|
if (condAddress == 0 || resultAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -96,7 +96,7 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||||
@@ -111,18 +111,18 @@ public static class SaveDataExports
|
|||||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setNum == 0)
|
if (setNum == 0)
|
||||||
{
|
{
|
||||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.DirNamesAddress == 0)
|
if (result.DirNamesAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < setNum; i++)
|
for (var i = 0; i < setNum; i++)
|
||||||
@@ -138,20 +138,20 @@ public static class SaveDataExports
|
|||||||
(result.InfosAddress != 0 &&
|
(result.InfosAddress != 0 &&
|
||||||
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +166,10 @@ public static class SaveDataExports
|
|||||||
var resultAddress = ctx[CpuRegister.Rsi];
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
if (mountAddress == 0 || resultAddress == 0)
|
if (mountAddress == 0 || resultAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadInt32(ctx, mountAddress, out var userId) ||
|
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
||||||
@@ -179,12 +179,12 @@ public static class SaveDataExports
|
|||||||
dirNameAddress == 0 ||
|
dirNameAddress == 0 ||
|
||||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -199,12 +199,12 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
if (!existed && !create && !createIfMissing)
|
if (!existed && !create && !createIfMissing)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existed && create)
|
if (existed && create)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorExists);
|
return ctx.SetReturn(OrbisSaveDataErrorExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existed)
|
if (!existed)
|
||||||
@@ -221,26 +221,26 @@ public static class SaveDataExports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
||||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
||||||
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
||||||
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,26 +258,26 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
if (resourceAddress == 0)
|
if (resourceAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(resourceAddress, id))
|
if (!ctx.TryWriteUInt32(resourceAddress, id))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
if (!TryReadInt32(ctx, address, out var userId) ||
|
if (!ctx.TryReadInt32(address, out var userId) ||
|
||||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
||||||
@@ -505,25 +505,6 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryRead(address, bytes))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceSaveData(string message)
|
private static void TraceSaveData(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Share;
|
namespace SharpEmu.Libs.Share;
|
||||||
@@ -26,13 +25,13 @@ public static class ShareExports
|
|||||||
var affinityMask = ctx[CpuRegister.Rdx];
|
var affinityMask = ctx[CpuRegister.Rdx];
|
||||||
if (memorySize == 0)
|
if (memorySize == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Exchange(ref _initialized, 1);
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
|
||||||
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
|
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -45,12 +44,12 @@ public static class ShareExports
|
|||||||
var contentParamAddress = ctx[CpuRegister.Rdi];
|
var contentParamAddress = ctx[CpuRegister.Rdi];
|
||||||
if (contentParamAddress == 0)
|
if (contentParamAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
|
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
_contentParam = contentParam;
|
_contentParam = contentParam;
|
||||||
@@ -60,13 +59,7 @@ public static class ShareExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
|
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
||||||
return (int)result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public static class SystemServiceExports
|
|||||||
var valueAddress = ctx[CpuRegister.Rsi];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = parameterId switch
|
var value = parameterId switch
|
||||||
@@ -37,8 +37,8 @@ public static class SystemServiceExports
|
|||||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||||
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -51,7 +51,7 @@ public static class SystemServiceExports
|
|||||||
var statusAddress = ctx[CpuRegister.Rdi];
|
var statusAddress = ctx[CpuRegister.Rdi];
|
||||||
if (statusAddress == 0)
|
if (statusAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||||
@@ -60,8 +60,8 @@ public static class SystemServiceExports
|
|||||||
status[0x06] = 1;
|
status[0x06] = 1;
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(statusAddress, status)
|
return ctx.Memory.TryWrite(statusAddress, status)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -74,7 +74,7 @@ public static class SystemServiceExports
|
|||||||
var infoAddress = ctx[CpuRegister.Rdi];
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (infoAddress == 0)
|
if (infoAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
|
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
|
||||||
@@ -82,8 +82,8 @@ public static class SystemServiceExports
|
|||||||
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
|
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info)
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -94,12 +94,6 @@ public static class SystemServiceExports
|
|||||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||||
{
|
{
|
||||||
VulkanVideoPresenter.HideSplashScreen();
|
VulkanVideoPresenter.HideSplashScreen();
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.UserService;
|
namespace SharpEmu.Libs.UserService;
|
||||||
|
|
||||||
@@ -40,12 +39,12 @@ public static class UserServiceExports
|
|||||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdAddress == 0)
|
if (userIdAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -58,7 +57,7 @@ public static class UserServiceExports
|
|||||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdListAddress == 0)
|
if (userIdListAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||||
@@ -67,8 +66,8 @@ public static class UserServiceExports
|
|||||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -81,20 +80,20 @@ public static class UserServiceExports
|
|||||||
var eventAddress = ctx[CpuRegister.Rdi];
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
if (eventAddress == 0)
|
if (eventAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||||
return ctx.Memory.TryWrite(eventAddress, payload)
|
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -109,25 +108,25 @@ public static class UserServiceExports
|
|||||||
var capacity = ctx[CpuRegister.Rdx];
|
var capacity = ctx[CpuRegister.Rdx];
|
||||||
if (userId != PrimaryUserId)
|
if (userId != PrimaryUserId)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||||
if (capacity <= (ulong)nameBytes.Length)
|
if (capacity <= (ulong)nameBytes.Length)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||||
nameBytes.CopyTo(output);
|
nameBytes.CopyTo(output);
|
||||||
return ctx.Memory.TryWrite(nameAddress, output)
|
return ctx.Memory.TryWrite(nameAddress, output)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -141,29 +140,16 @@ public static class UserServiceExports
|
|||||||
var valueAddress = ctx[CpuRegister.Rsi];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (parameterId != 1000)
|
if (parameterId != 1000)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteInt32(ctx, valueAddress, 0)
|
return ctx.TryWriteInt32(valueAddress, 0)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
|
||||||
{
|
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
||||||
return ctx.Memory.TryWrite(address, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Queue<object> _pendingGuestWork = new();
|
private static readonly Queue<object> _pendingGuestWork = new();
|
||||||
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
||||||
|
private static readonly Dictionary<ulong, uint> _gpuGuestImages = new();
|
||||||
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
|
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
|
||||||
_tracedGuestImageSubmissions = [];
|
_tracedGuestImageSubmissions = [];
|
||||||
private static Thread? _thread;
|
private static Thread? _thread;
|
||||||
@@ -600,7 +601,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsGuestImageAvailable(
|
internal static bool IsGpuGuestImageAvailable(
|
||||||
ulong address,
|
ulong address,
|
||||||
uint format,
|
uint format,
|
||||||
uint numberType)
|
uint numberType)
|
||||||
@@ -613,7 +614,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
return _gpuGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||||
availableFormat == guestFormat;
|
availableFormat == guestFormat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -971,6 +972,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public VulkanGuestSampler SamplerState;
|
public VulkanGuestSampler SamplerState;
|
||||||
public Sampler Sampler;
|
public Sampler Sampler;
|
||||||
public GuestImageResource? GuestImage;
|
public GuestImageResource? GuestImage;
|
||||||
|
public ulong CpuContentFingerprint;
|
||||||
|
public bool UpdatesCpuContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class GlobalBufferResource
|
private sealed class GlobalBufferResource
|
||||||
@@ -1009,6 +1012,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public Framebuffer Framebuffer;
|
public Framebuffer Framebuffer;
|
||||||
public bool Initialized;
|
public bool Initialized;
|
||||||
public bool InitialUploadPending;
|
public bool InitialUploadPending;
|
||||||
|
public bool IsCpuBacked;
|
||||||
|
public ulong CpuContentFingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record PendingGuestSubmission(
|
private sealed record PendingGuestSubmission(
|
||||||
@@ -1248,7 +1253,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||||
byte* validationLayerName = null;
|
byte* validationLayerName = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var applicationInfo = new ApplicationInfo
|
var applicationInfo = new ApplicationInfo
|
||||||
@@ -1308,7 +1313,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
||||||
{
|
{
|
||||||
_debugUtils = debugUtils;
|
_debugUtils = debugUtils;
|
||||||
@@ -1378,7 +1383,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
||||||
"vkCreateDebugUtilsMessengerEXT");
|
"vkCreateDebugUtilsMessengerEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe uint DebugCallback(
|
private static unsafe uint DebugCallback(
|
||||||
DebugUtilsMessageSeverityFlagsEXT severity,
|
DebugUtilsMessageSeverityFlagsEXT severity,
|
||||||
DebugUtilsMessageTypeFlagsEXT type,
|
DebugUtilsMessageTypeFlagsEXT type,
|
||||||
@@ -2838,6 +2843,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
$"tile={texture.TileMode} format={vkFormat}");
|
$"tile={texture.TileMode} format={vkFormat}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
|
||||||
|
{
|
||||||
|
return refresh;
|
||||||
|
}
|
||||||
|
|
||||||
return new TextureResource
|
return new TextureResource
|
||||||
{
|
{
|
||||||
Address = texture.Address,
|
Address = texture.Address,
|
||||||
@@ -2855,6 +2865,71 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return CreateTextureResource(texture);
|
return CreateTextureResource(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryCreateCpuTextureRefreshResource(
|
||||||
|
VulkanGuestDrawTexture texture,
|
||||||
|
GuestImageResource guestImage,
|
||||||
|
ImageView view,
|
||||||
|
out TextureResource resource)
|
||||||
|
{
|
||||||
|
resource = default!;
|
||||||
|
if (!guestImage.IsCpuBacked ||
|
||||||
|
guestImage.Width != texture.Width ||
|
||||||
|
guestImage.Height != texture.Height ||
|
||||||
|
guestImage.MipLevels != 1 ||
|
||||||
|
texture.RgbaPixels.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowLength = texture.TileMode == 0
|
||||||
|
? Math.Max(texture.Pitch, texture.Width)
|
||||||
|
: texture.Width;
|
||||||
|
var expectedSize = GetTextureByteCount(texture.Format, rowLength, texture.Height);
|
||||||
|
if (expectedSize == 0 || expectedSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixels = texture.RgbaPixels.Length == (int)expectedSize
|
||||||
|
? texture.RgbaPixels
|
||||||
|
: CreateFallbackTexturePixels(texture.Format, rowLength, texture.Height, expectedSize);
|
||||||
|
var fingerprint = ComputeTextureContentFingerprint(pixels);
|
||||||
|
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
||||||
|
guestImage.CpuContentFingerprint == fingerprint)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadPixels = texture.Format == 13
|
||||||
|
? ExpandRgb32Pixels(pixels)
|
||||||
|
: pixels;
|
||||||
|
var debugName = TextureDebugName(texture, guestImage.Format);
|
||||||
|
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||||
|
uploadPixels,
|
||||||
|
$"{debugName} refresh staging");
|
||||||
|
TraceVulkanShader(
|
||||||
|
$"vk.texture_refresh addr=0x{texture.Address:X16} " +
|
||||||
|
$"size={texture.Width}x{texture.Height} bytes={uploadPixels.Length}");
|
||||||
|
resource = new TextureResource
|
||||||
|
{
|
||||||
|
Address = texture.Address,
|
||||||
|
StagingBuffer = stagingBuffer,
|
||||||
|
StagingMemory = stagingMemory,
|
||||||
|
Image = guestImage.Image,
|
||||||
|
View = view,
|
||||||
|
Width = guestImage.Width,
|
||||||
|
Height = guestImage.Height,
|
||||||
|
RowLength = rowLength,
|
||||||
|
DstSelect = texture.DstSelect,
|
||||||
|
NeedsUpload = true,
|
||||||
|
SamplerState = texture.Sampler,
|
||||||
|
GuestImage = guestImage,
|
||||||
|
CpuContentFingerprint = fingerprint,
|
||||||
|
UpdatesCpuContent = true,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsCompatibleGuestImageAlias(
|
private static bool IsCompatibleGuestImageAlias(
|
||||||
VulkanGuestDrawTexture texture,
|
VulkanGuestDrawTexture texture,
|
||||||
GuestImageResource guestImage)
|
GuestImageResource guestImage)
|
||||||
@@ -3096,24 +3171,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var uploadPixels = texture.Format == 13
|
var uploadPixels = texture.Format == 13
|
||||||
? ExpandRgb32Pixels(pixels)
|
? ExpandRgb32Pixels(pixels)
|
||||||
: pixels;
|
: pixels;
|
||||||
var uploadSize = (ulong)uploadPixels.Length;
|
var contentFingerprint = ComputeTextureContentFingerprint(pixels);
|
||||||
|
|
||||||
var stagingBuffer = CreateBuffer(
|
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||||
uploadSize,
|
uploadPixels,
|
||||||
BufferUsageFlags.TransferSrcBit,
|
$"{TextureDebugName(texture, vkFormat)} staging");
|
||||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
|
|
||||||
out var stagingMemory);
|
|
||||||
void* mapped;
|
|
||||||
Check(_vk.MapMemory(_device, stagingMemory, 0, uploadSize, 0, &mapped), "vkMapMemory(texture)");
|
|
||||||
fixed (byte* source = uploadPixels)
|
|
||||||
{
|
|
||||||
System.Buffer.MemoryCopy(
|
|
||||||
source,
|
|
||||||
mapped,
|
|
||||||
uploadPixels.Length,
|
|
||||||
uploadPixels.Length);
|
|
||||||
}
|
|
||||||
_vk.UnmapMemory(_device, stagingMemory);
|
|
||||||
|
|
||||||
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
||||||
var imageInfo = new ImageCreateInfo
|
var imageInfo = new ImageCreateInfo
|
||||||
@@ -3163,7 +3225,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
||||||
var debugName = TextureDebugName(texture, vkFormat);
|
var debugName = TextureDebugName(texture, vkFormat);
|
||||||
SetDebugName(ObjectType.Buffer, stagingBuffer.Handle, $"{debugName} staging");
|
|
||||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||||
var resource = new TextureResource
|
var resource = new TextureResource
|
||||||
@@ -3181,6 +3242,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
NeedsUpload = true,
|
NeedsUpload = true,
|
||||||
OwnsStorage = true,
|
OwnsStorage = true,
|
||||||
SamplerState = texture.Sampler,
|
SamplerState = texture.Sampler,
|
||||||
|
CpuContentFingerprint = contentFingerprint,
|
||||||
|
UpdatesCpuContent = texture.Address != 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (texture.Address != 0 &&
|
if (texture.Address != 0 &&
|
||||||
@@ -3197,6 +3260,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Memory = imageMemory,
|
Memory = imageMemory,
|
||||||
View = view,
|
View = view,
|
||||||
InitialUploadPending = true,
|
InitialUploadPending = true,
|
||||||
|
IsCpuBacked = true,
|
||||||
|
CpuContentFingerprint = contentFingerprint,
|
||||||
};
|
};
|
||||||
_guestImages.Add(texture.Address, guestImage);
|
_guestImages.Add(texture.Address, guestImage);
|
||||||
resource.OwnsStorage = false;
|
resource.OwnsStorage = false;
|
||||||
@@ -3209,6 +3274,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (guestFormat != 0)
|
if (guestFormat != 0)
|
||||||
{
|
{
|
||||||
_availableGuestImages[texture.Address] = guestFormat;
|
_availableGuestImages[texture.Address] = guestFormat;
|
||||||
|
_gpuGuestImages.Remove(texture.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3216,6 +3282,41 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(
|
||||||
|
byte[] pixels,
|
||||||
|
string debugName)
|
||||||
|
{
|
||||||
|
var size = (ulong)pixels.Length;
|
||||||
|
var buffer = CreateBuffer(
|
||||||
|
size,
|
||||||
|
BufferUsageFlags.TransferSrcBit,
|
||||||
|
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
|
||||||
|
out var memory);
|
||||||
|
void* mapped;
|
||||||
|
Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(texture)");
|
||||||
|
fixed (byte* source = pixels)
|
||||||
|
{
|
||||||
|
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
||||||
|
}
|
||||||
|
_vk.UnmapMemory(_device, memory);
|
||||||
|
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
|
||||||
|
return (buffer, memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeTextureContentFingerprint(ReadOnlySpan<byte> pixels)
|
||||||
|
{
|
||||||
|
const ulong offsetBasis = 14695981039346656037;
|
||||||
|
const ulong prime = 1099511628211;
|
||||||
|
var hash = offsetBasis;
|
||||||
|
foreach (var value in pixels)
|
||||||
|
{
|
||||||
|
hash ^= value;
|
||||||
|
hash *= prime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
private void DumpTextureUpload(
|
private void DumpTextureUpload(
|
||||||
VulkanGuestDrawTexture texture,
|
VulkanGuestDrawTexture texture,
|
||||||
byte[] pixels,
|
byte[] pixels,
|
||||||
@@ -4267,12 +4368,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
||||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
||||||
|
|
||||||
|
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
|
||||||
var toColorAttachment = new ImageMemoryBarrier
|
var toColorAttachment = new ImageMemoryBarrier
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
SrcAccessMask = target.Initialized ? AccessFlags.ShaderReadBit : 0,
|
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
|
||||||
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||||
OldLayout = target.Initialized
|
OldLayout = targetHasPriorContents
|
||||||
? ImageLayout.ShaderReadOnlyOptimal
|
? ImageLayout.ShaderReadOnlyOptimal
|
||||||
: ImageLayout.Undefined,
|
: ImageLayout.Undefined,
|
||||||
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
@@ -4283,8 +4385,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
target.Initialized
|
targetHasPriorContents
|
||||||
? PipelineStageFlags.FragmentShaderBit
|
? PipelineStageFlags.AllCommandsBit
|
||||||
: PipelineStageFlags.TopOfPipeBit,
|
: PipelineStageFlags.TopOfPipeBit,
|
||||||
PipelineStageFlags.ColorAttachmentOutputBit,
|
PipelineStageFlags.ColorAttachmentOutputBit,
|
||||||
0,
|
0,
|
||||||
@@ -4337,12 +4439,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
MarkSampledImagesInitialized(resources);
|
MarkSampledImagesInitialized(resources);
|
||||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||||
|
|
||||||
var guestTextureFormat = GetGuestTextureFormat(target.Format);
|
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
|
||||||
|
work.Target.Format,
|
||||||
|
work.Target.NumberType);
|
||||||
if (work.PublishTarget && guestTextureFormat != 0)
|
if (work.PublishTarget && guestTextureFormat != 0)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
_availableGuestImages[target.Address] = guestTextureFormat;
|
||||||
|
_gpuGuestImages[target.Address] = guestTextureFormat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
||||||
@@ -4384,6 +4489,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Remove(work.Target.Address);
|
_availableGuestImages.Remove(work.Target.Address);
|
||||||
|
_gpuGuestImages.Remove(work.Target.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4423,6 +4529,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
existing.Format == format)
|
existing.Format == format)
|
||||||
{
|
{
|
||||||
|
existing.IsCpuBacked = false;
|
||||||
|
existing.CpuContentFingerprint = 0;
|
||||||
if (existing.RenderPass.Handle == 0)
|
if (existing.RenderPass.Handle == 0)
|
||||||
{
|
{
|
||||||
var attachmentView = existing.MipViews.Length > 0
|
var attachmentView = existing.MipViews.Length > 0
|
||||||
@@ -4448,6 +4556,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Remove(target.Address);
|
_availableGuestImages.Remove(target.Address);
|
||||||
|
_gpuGuestImages.Remove(target.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5386,11 +5495,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasPriorContents = texture.GuestImage is { } guestImage &&
|
||||||
|
(guestImage.Initialized || guestImage.InitialUploadPending);
|
||||||
var toTransfer = new ImageMemoryBarrier
|
var toTransfer = new ImageMemoryBarrier
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
|
SrcAccessMask = hasPriorContents
|
||||||
|
? AccessFlags.ShaderReadBit
|
||||||
|
: 0,
|
||||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||||
OldLayout = ImageLayout.Undefined,
|
OldLayout = hasPriorContents
|
||||||
|
? ImageLayout.ShaderReadOnlyOptimal
|
||||||
|
: ImageLayout.Undefined,
|
||||||
NewLayout = ImageLayout.TransferDstOptimal,
|
NewLayout = ImageLayout.TransferDstOptimal,
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
@@ -5399,7 +5515,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineStageFlags.TopOfPipeBit,
|
hasPriorContents
|
||||||
|
? PipelineStageFlags.AllCommandsBit
|
||||||
|
: PipelineStageFlags.TopOfPipeBit,
|
||||||
PipelineStageFlags.TransferBit,
|
PipelineStageFlags.TransferBit,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -5569,6 +5687,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (format != 0)
|
if (format != 0)
|
||||||
{
|
{
|
||||||
_availableGuestImages[texture.Address] = format;
|
_availableGuestImages[texture.Address] = format;
|
||||||
|
_gpuGuestImages[texture.Address] = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traceContents &&
|
if (traceContents &&
|
||||||
@@ -5608,6 +5727,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
guestImage.Initialized = true;
|
guestImage.Initialized = true;
|
||||||
guestImage.InitialUploadPending = false;
|
guestImage.InitialUploadPending = false;
|
||||||
|
if (texture.UpdatesCpuContent)
|
||||||
|
{
|
||||||
|
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6379,6 +6502,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_availableGuestImages.Clear();
|
_availableGuestImages.Clear();
|
||||||
|
_gpuGuestImages.Clear();
|
||||||
}
|
}
|
||||||
DestroySwapchainResources();
|
DestroySwapchainResources();
|
||||||
if (_device.Handle != 0)
|
if (_device.Handle != 0)
|
||||||
|
|||||||
@@ -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);
|
new(StringComparer.Ordinal);
|
||||||
private static readonly object ConfigurationSync = new();
|
private static readonly object ConfigurationSync = new();
|
||||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||||
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
||||||
useColors: ResolveColorEnabledFromEnvironment(),
|
|
||||||
includeTimestamp: false);
|
|
||||||
|
|
||||||
public static LogLevel MinimumLevel
|
public static LogLevel MinimumLevel
|
||||||
{
|
{
|
||||||
@@ -37,11 +35,26 @@ public static class SharpEmuLog
|
|||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
lock (ConfigurationSync)
|
lock (ConfigurationSync)
|
||||||
{
|
{
|
||||||
|
if (ReferenceEquals(_sink, value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sink is IDisposable disposable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_sink = value;
|
_sink = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||||
{
|
{
|
||||||
if (minimumLevel.HasValue)
|
if (minimumLevel.HasValue)
|
||||||
@@ -55,6 +68,22 @@ public static class SharpEmuLog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the active sink if it implements <see cref="IDisposable"/>.
|
||||||
|
/// Call at shutdown to flush file buffers. Logging after this call
|
||||||
|
/// continues to work for non-disposable sinks (e.g. <see cref="ConsoleLogSink"/>).
|
||||||
|
/// </summary>
|
||||||
|
public static void Shutdown()
|
||||||
|
{
|
||||||
|
lock (ConfigurationSync)
|
||||||
|
{
|
||||||
|
if (_sink is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static SharpEmuLogger For(string category)
|
public static SharpEmuLogger For(string category)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||||
@@ -146,6 +175,30 @@ public static class SharpEmuLog
|
|||||||
return !IsTrueLike(raw);
|
return !IsTrueLike(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ISharpEmuLogSink ResolveSinkFromEnvironment()
|
||||||
|
{
|
||||||
|
var consoleSink = new ConsoleLogSink(
|
||||||
|
useColors: ResolveColorEnabledFromEnvironment(),
|
||||||
|
includeTimestamp: false);
|
||||||
|
|
||||||
|
var logFilePath = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FILE");
|
||||||
|
if (!string.IsNullOrWhiteSpace(logFilePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
|
||||||
|
return new CompositeLogSink(consoleSink, fileSink);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Bootstrapping — the logging system is not yet active, so stderr is the only channel.
|
||||||
|
Console.Error.WriteLine($"[SHARPEMU_LOG] Failed to open log file '{logFilePath}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consoleSink;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsTrueLike(string? text)
|
private static bool IsTrueLike(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
Reference in New Issue
Block a user