mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
[AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)
* [Core] Add POSIX native execution and PS5 SELF support Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases. * [HLE] Expand PS5 service and media compatibility Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT. * [AGC/Vulkan] Extend Gen5 shader and presentation support Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders. * [Core] Align static TLS reservation across hosts * [Pad] Align primary user ID with UserService * [Gpu] Preserve runtime scalar buffers across renderer seam * [AGC] Restore omitted command helper exports * [Vulkan] Reuse primary views for promoted MRT targets * [Vulkan] Preserve scratch storage bindings in compute dispatches
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Bink 2 bridge
|
||||||
|
|
||||||
|
Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
|
||||||
|
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||||
|
decoder cannot observe or replace those frames.
|
||||||
|
|
||||||
|
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||||
|
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||||
|
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||||
|
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||||
|
|
||||||
|
Without an adapter, Bink movies are skipped by default: their open call returns
|
||||||
|
not-found so games that mark cinematics as optional progress to their next
|
||||||
|
state instead of waiting on an empty Bink GPU texture.
|
||||||
|
|
||||||
|
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||||
|
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||||
|
only; it does not decode the movie or alter its game logic. Set
|
||||||
|
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||||
|
|
||||||
|
## Supplying the adapter
|
||||||
|
|
||||||
|
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||||
|
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||||
|
The adapter deliberately contains only a three-function C ABI so the managed
|
||||||
|
emulator never depends on RAD's private binary ABI.
|
||||||
|
|
||||||
|
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||||
|
executable, or point to it explicitly:
|
||||||
|
|
||||||
|
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||||
|
./SharpEmu /path/to/eboot.bin
|
||||||
|
|
||||||
|
The expected exports are sharpemu_bink2_open_utf8,
|
||||||
|
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||||
|
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||||
|
frame. The managed side validates dimensions and retains ownership of the
|
||||||
|
destination buffer.
|
||||||
|
|
||||||
|
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||||
|
and retains the existing guest rendering path.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*
|
||||||
|
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||||
|
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "bink.h"
|
||||||
|
|
||||||
|
typedef struct sharpemu_bink2_info {
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t frames_per_second_numerator;
|
||||||
|
uint32_t frames_per_second_denominator;
|
||||||
|
} sharpemu_bink2_info;
|
||||||
|
|
||||||
|
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||||
|
HBINK bink;
|
||||||
|
if (!path || !movie || !info) return 0;
|
||||||
|
|
||||||
|
bink = BinkOpen(path, 0);
|
||||||
|
if (!bink) return 0;
|
||||||
|
|
||||||
|
*movie = bink;
|
||||||
|
info->width = bink->Width;
|
||||||
|
info->height = bink->Height;
|
||||||
|
info->frames_per_second_numerator = bink->FrameRate;
|
||||||
|
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||||
|
uint32_t stride, uint32_t destination_bytes) {
|
||||||
|
uint64_t needed;
|
||||||
|
if (!movie || !destination || stride < movie->Width * 4) return 0;
|
||||||
|
needed = (uint64_t)stride * movie->Height;
|
||||||
|
if (needed > destination_bytes) return 0;
|
||||||
|
|
||||||
|
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||||
|
if (BinkWait(movie)) return 0;
|
||||||
|
BinkDoFrame(movie);
|
||||||
|
BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
|
||||||
|
BinkNextFrame(movie);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sharpemu_bink2_close(HBINK movie) {
|
||||||
|
if (movie) BinkClose(movie);
|
||||||
|
}
|
||||||
@@ -45,6 +45,11 @@ internal static partial class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// Avoid blocking full collections while guest and render threads are
|
||||||
|
// running, and establish the GC mode before the runtime reserves the
|
||||||
|
// fixed guest address-space window.
|
||||||
|
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Run(args);
|
return Run(args);
|
||||||
@@ -84,6 +89,7 @@ internal static partial class Program
|
|||||||
{
|
{
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
|
ConfigureMoltenVkDefaults();
|
||||||
PreloadMacVulkanLoader();
|
PreloadMacVulkanLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +157,26 @@ internal static partial class Program
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies MoltenVK performance defaults before the Vulkan loader is
|
||||||
|
/// loaded. Existing user-provided values always take precedence.
|
||||||
|
/// </summary>
|
||||||
|
private static void ConfigureMoltenVkDefaults()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION", "1", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0);
|
||||||
|
_ = MacSetEnv("MVK_CONFIG_RESUME_LOST_DEVICE", "1", 0);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Failed to set MoltenVK defaults: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
||||||
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
||||||
@@ -1319,4 +1345,7 @@ internal static partial class Program
|
|||||||
uint creationDisposition,
|
uint creationDisposition,
|
||||||
uint flagsAndAttributes,
|
uint flagsAndAttributes,
|
||||||
nint templateFile);
|
nint templateFile);
|
||||||
|
|
||||||
|
[DllImport("libSystem", EntryPoint = "setenv")]
|
||||||
|
private static extern int MacSetEnv(string name, string value, int overwrite);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<!-- Server GC reserves one large heap range per logical processor. On
|
||||||
|
high-core-count hosts those ranges can overlap fixed PS5 image bases
|
||||||
|
before the guest address space is established. Workstation GC avoids
|
||||||
|
that collision and keeps the emulator's required mappings available. -->
|
||||||
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ 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.HLE.Host;
|
|
||||||
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,
|
||||||
@@ -31,9 +27,9 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||||
private const ulong TlsSize = 0x0001_0000UL;
|
private const ulong TlsSize = 0x0001_0000UL;
|
||||||
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
|
||||||
// amd64 variant II); libc.prx alone reaches beyond -0x1700, so give the
|
// amd64 variant II). Keep every host in sync with GuestTlsTemplate's
|
||||||
// prefix a full 64KB on POSIX. Windows keeps its historical 4KB prefix.
|
// startup reservation; PS5 modules routinely reach beyond one host page.
|
||||||
private static readonly ulong TlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
|
private const ulong TlsPrefixSize = GuestTlsTemplate.StartupStaticTlsReservation;
|
||||||
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
|
||||||
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
|
||||||
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
|
||||||
@@ -49,19 +45,16 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
];
|
];
|
||||||
private readonly IVirtualMemory _virtualMemory;
|
private readonly IVirtualMemory _virtualMemory;
|
||||||
private readonly IModuleManager _moduleManager;
|
private readonly IModuleManager _moduleManager;
|
||||||
private readonly IHostPlatform? _hostPlatform;
|
|
||||||
private INativeCpuBackend? _nativeCpuBackend;
|
private INativeCpuBackend? _nativeCpuBackend;
|
||||||
|
|
||||||
public CpuDispatcher(
|
public CpuDispatcher(
|
||||||
IVirtualMemory virtualMemory,
|
IVirtualMemory virtualMemory,
|
||||||
IModuleManager moduleManager,
|
IModuleManager moduleManager,
|
||||||
INativeCpuBackend? nativeCpuBackend = null,
|
INativeCpuBackend? nativeCpuBackend = null)
|
||||||
IHostPlatform? hostPlatform = null)
|
|
||||||
{
|
{
|
||||||
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
|
||||||
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
|
||||||
_nativeCpuBackend = nativeCpuBackend;
|
_nativeCpuBackend = nativeCpuBackend;
|
||||||
_hostPlatform = hostPlatform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong? LastEntryPoint { get; private set; }
|
public ulong? LastEntryPoint { get; private set; }
|
||||||
@@ -94,8 +87,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchEntry START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -103,7 +96,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +110,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -132,7 +126,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +141,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Log.Debug("DispatchEntryCore STARTING...");
|
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -277,7 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
entryFrameDiagnostic,
|
entryFrameDiagnostic,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
"CpuEngine: native-only");
|
"CpuEngine: native-only");
|
||||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
|
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||||
if (_nativeCpuBackend.TryExecute(
|
if (_nativeCpuBackend.TryExecute(
|
||||||
context,
|
context,
|
||||||
entryPoint,
|
entryPoint,
|
||||||
@@ -318,7 +313,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Log.Error($"Native backend FAILED: {backendError}");
|
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
@@ -377,11 +372,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
|
|
||||||
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
||||||
{
|
{
|
||||||
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
|
if (!context.TryWriteUInt64(tlsBase - 0xF0, 0) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
!context.TryWriteUInt64(tlsBase + 0x00, tlsBase) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
!context.TryWriteUInt64(tlsBase + 0x10, tlsBase) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
|
!context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBA00UL) ||
|
||||||
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
!context.TryWriteUInt64(tlsBase + 0x60, tlsBase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed the static TLS block below the thread pointer with the main
|
||||||
|
// module's initialized thread-locals (variant II layout).
|
||||||
|
SharpEmu.HLE.GuestTlsTemplate.SeedThreadBlock(context, tlsBase);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
||||||
@@ -407,35 +410,54 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
ulong programExitHandlerAddress)
|
ulong programExitHandlerAddress)
|
||||||
{
|
{
|
||||||
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
|
||||||
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
|
var arguments = new List<string>(3) { imageName };
|
||||||
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
|
var configuredArguments = Environment.GetEnvironmentVariable("SHARPEMU_GUEST_ARGS");
|
||||||
? stackalloc byte[encodedNameLength + 1]
|
if (!string.IsNullOrWhiteSpace(configuredArguments))
|
||||||
: new byte[encodedNameLength + 1];
|
{
|
||||||
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
|
// The PS5 entry-parameter ABI exposes three inline argv pointers.
|
||||||
|
// Two compatibility arguments are therefore safe without changing
|
||||||
|
// the fixed 0x20-byte structure expected by existing titles.
|
||||||
|
var compatibilityArguments = configuredArguments.Split(
|
||||||
|
(char[]?)null,
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
arguments.AddRange(compatibilityArguments.Take(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
var cursor = context[CpuRegister.Rsp];
|
||||||
|
var argumentAddresses = new ulong[arguments.Count];
|
||||||
|
for (var index = arguments.Count - 1; index >= 0; index--)
|
||||||
|
{
|
||||||
|
var encoded = Encoding.UTF8.GetBytes(arguments[index] + '\0');
|
||||||
|
cursor = AlignDown(cursor - (ulong)encoded.Length, 16);
|
||||||
|
if (!context.Memory.TryWrite(cursor, encoded))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
argv0Buffer[encodedNameLength] = 0;
|
argumentAddresses[index] = cursor;
|
||||||
var cursor = context[CpuRegister.Rsp];
|
|
||||||
|
|
||||||
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
|
|
||||||
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ulong entryParamsSize = 0x20;
|
const ulong entryParamsSize = 0x20;
|
||||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
var entryParamsAddress = AlignDown(cursor - entryParamsSize, 16);
|
||||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, (uint)arguments.Count) ||
|
||||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argumentAddresses[0]) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
!context.TryWriteUInt64(
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
entryParamsAddress + 0x10,
|
||||||
|
argumentAddresses.Length > 1 ? argumentAddresses[1] : 0) ||
|
||||||
|
!context.TryWriteUInt64(
|
||||||
|
entryParamsAddress + 0x18,
|
||||||
|
argumentAddresses.Length > 2 ? argumentAddresses[2] : 0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arguments.Count > 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[DISPATCHER] Guest arguments: {string.Join(' ', arguments.Skip(1))}");
|
||||||
|
}
|
||||||
|
|
||||||
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
||||||
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
||||||
{
|
{
|
||||||
@@ -468,6 +490,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
return value & ~(alignment - 1);
|
return value & ~(alignment - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return context.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildEntryFrameDiagnostic(
|
private static string BuildEntryFrameDiagnostic(
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -17,6 +17,55 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||||
|
|
||||||
|
private static readonly bool _perfHleHistogram =
|
||||||
|
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE"), "1", System.StringComparison.Ordinal);
|
||||||
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, long> _perfHleCounts = new();
|
||||||
|
private static long _perfHleTotal;
|
||||||
|
private static long _perfHleDispatchTicks;
|
||||||
|
|
||||||
|
private static void RecordPerfHleDispatchTime(long ticks)
|
||||||
|
{
|
||||||
|
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
|
||||||
|
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
|
||||||
|
if (calls > 0 && calls % 500000 == 0)
|
||||||
|
{
|
||||||
|
var avgUs = (double)total / System.Diagnostics.Stopwatch.Frequency * 1_000_000.0 / calls;
|
||||||
|
System.Console.Error.WriteLine($"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us total_managed_s={(double)total / System.Diagnostics.Stopwatch.Frequency:F2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly bool _perfHleNoDict =
|
||||||
|
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE_NODICT"), "1", System.StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static void RecordPerfHleCall(string name)
|
||||||
|
{
|
||||||
|
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
|
||||||
|
if (!_perfHleNoDict)
|
||||||
|
{
|
||||||
|
_perfHleCounts.AddOrUpdate(name, 1, static (_, v) => v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total % 500000 == 0 && !_perfHleNoDict)
|
||||||
|
{
|
||||||
|
// Snapshot via foreach (a safe moving enumerator) before sorting.
|
||||||
|
// LINQ over a ConcurrentDictionary uses ICollection.CopyTo, which
|
||||||
|
// throws ArgumentException if another thread adds a key between the
|
||||||
|
// Count read and the copy — that exception was being swallowed into
|
||||||
|
// a CPU_TRAP return and crashing the guest.
|
||||||
|
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, long>>(_perfHleCounts.Count + 16);
|
||||||
|
foreach (var kvp in _perfHleCounts)
|
||||||
|
{
|
||||||
|
snapshot.Add(kvp);
|
||||||
|
}
|
||||||
|
|
||||||
|
var top = snapshot
|
||||||
|
.OrderByDescending(kvp => kvp.Value)
|
||||||
|
.Take(20)
|
||||||
|
.Select(kvp => $"{kvp.Key}={kvp.Value}");
|
||||||
|
System.Console.Error.WriteLine($"[PERF][HLE] total={total} top: {string.Join(", ", top)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RecordRecentImportTrace(
|
private void RecordRecentImportTrace(
|
||||||
long dispatchIndex,
|
long dispatchIndex,
|
||||||
string nid,
|
string nid,
|
||||||
@@ -25,101 +74,40 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong arg1,
|
ulong arg1,
|
||||||
ulong arg2)
|
ulong arg2)
|
||||||
{
|
{
|
||||||
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
var trace = _recentImportTrace;
|
||||||
|
trace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||||
dispatchIndex,
|
dispatchIndex,
|
||||||
nid,
|
nid,
|
||||||
returnRip,
|
returnRip,
|
||||||
arg0,
|
arg0,
|
||||||
arg1,
|
arg1,
|
||||||
arg2);
|
arg2,
|
||||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
Environment.CurrentManagedThreadId);
|
||||||
|
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % trace.Length;
|
||||||
|
if (_recentImportTraceCount < trace.Length)
|
||||||
{
|
{
|
||||||
_recentImportTraceCount++;
|
_recentImportTraceCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordDeferredBootstrapTrace(
|
|
||||||
long dispatchIndex,
|
|
||||||
ulong op,
|
|
||||||
ulong symbolPointer,
|
|
||||||
ulong outputPointer,
|
|
||||||
ulong returnRip)
|
|
||||||
{
|
|
||||||
lock (_deferredBootstrapTraceGate)
|
|
||||||
{
|
|
||||||
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
|
||||||
dispatchIndex,
|
|
||||||
op,
|
|
||||||
symbolPointer,
|
|
||||||
outputPointer,
|
|
||||||
returnRip);
|
|
||||||
_deferredBootstrapTraceWriteIndex =
|
|
||||||
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
|
||||||
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
|
||||||
{
|
|
||||||
_deferredBootstrapTraceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrainDeferredBootstrapTraces()
|
|
||||||
{
|
|
||||||
if (!_logBootstrap)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeferredBootstrapTraceEntry[] pending;
|
|
||||||
lock (_deferredBootstrapTraceGate)
|
|
||||||
{
|
|
||||||
if (_deferredBootstrapTraceCount == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
|
||||||
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
|
||||||
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
|
||||||
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
|
||||||
{
|
|
||||||
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
|
||||||
}
|
|
||||||
|
|
||||||
_deferredBootstrapTraceCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in pending)
|
|
||||||
{
|
|
||||||
var symbolText = "<unreadable>";
|
|
||||||
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
|
||||||
{
|
|
||||||
symbolText = sym;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
|
||||||
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
|
||||||
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DumpRecentImportTrace()
|
private void DumpRecentImportTrace()
|
||||||
{
|
{
|
||||||
if (_recentImportTraceCount == 0)
|
var trace = _recentImportTrace;
|
||||||
|
if (trace is null || _recentImportTraceCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
Log.Info($" Recent import calls for managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ({_recentImportTraceCount}):");
|
||||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + trace.Length) % trace.Length;
|
||||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
{
|
{
|
||||||
int num2 = (num + i) % _recentImportTrace.Length;
|
int num2 = (num + i) % trace.Length;
|
||||||
var entry = _recentImportTrace[num2];
|
var entry = trace[num2];
|
||||||
if (!string.IsNullOrEmpty(entry.Nid))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Log.Info(
|
Log.Info(
|
||||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
$" #{entry.DispatchIndex} managed={entry.ManagedThreadId} guest=0x{entry.GuestThreadHandle:X16} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,9 +123,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num2 = 0;
|
int num2 = 0;
|
||||||
List<ulong> list = new List<ulong>(16);
|
List<ulong> list = new List<ulong>(16);
|
||||||
ulong num3 = scanStart;
|
ulong num3 = scanStart;
|
||||||
var hostMemory = ResolveDiagnosticsHostMemory();
|
MEMORY_BASIC_INFORMATION64 lpBuffer;
|
||||||
HostRegionInfo lpBuffer;
|
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
|
|
||||||
{
|
{
|
||||||
ulong baseAddress = lpBuffer.BaseAddress;
|
ulong baseAddress = lpBuffer.BaseAddress;
|
||||||
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
ulong num4 = baseAddress + lpBuffer.RegionSize;
|
||||||
@@ -147,7 +134,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
ulong value = Math.Max(num3, baseAddress);
|
ulong value = Math.Max(num3, baseAddress);
|
||||||
ulong num5 = Math.Min(num4, scanEnd);
|
ulong num5 = Math.Min(num4, scanEnd);
|
||||||
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
|
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
|
||||||
{
|
{
|
||||||
ulong num6 = AlignUp(value, 8uL);
|
ulong num6 = AlignUp(value, 8uL);
|
||||||
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
|
||||||
@@ -186,6 +173,50 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const int preludeSize = 192;
|
||||||
|
Span<byte> prelude = stackalloc byte[preludeSize];
|
||||||
|
if (returnRip >= preludeSize && cpuContext.Memory.TryRead(returnRip - preludeSize, prelude))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} pre-return bytes @0x{returnRip - preludeSize:X16}: " +
|
||||||
|
BitConverter.ToString(prelude.ToArray()).Replace("-", " "));
|
||||||
|
|
||||||
|
List<DecodedInst>? bestCallChain = null;
|
||||||
|
var preludeAddress = returnRip - preludeSize;
|
||||||
|
for (var startOffset = 0; startOffset < preludeSize; startOffset++)
|
||||||
|
{
|
||||||
|
var cursor = preludeAddress + (ulong)startOffset;
|
||||||
|
var candidate = new List<DecodedInst>();
|
||||||
|
while (cursor < returnRip && candidate.Count < 96 &&
|
||||||
|
IcedDecoder.TryReadGuestBytes(cpuContext.Memory, cursor, 15, out var instructionBytes) &&
|
||||||
|
IcedDecoder.TryDecode(cursor, instructionBytes, out var instruction) &&
|
||||||
|
instruction.Length > 0 &&
|
||||||
|
cursor + (ulong)instruction.Length <= returnRip)
|
||||||
|
{
|
||||||
|
candidate.Add(instruction);
|
||||||
|
cursor += (ulong)instruction.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor == returnRip &&
|
||||||
|
candidate.Count > 0 &&
|
||||||
|
string.Equals(candidate[^1].Mnemonic, "Call", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
(bestCallChain is null || candidate.Count > bestCallChain.Count))
|
||||||
|
{
|
||||||
|
bestCallChain = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestCallChain is not null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} pre-return disassembly:");
|
||||||
|
foreach (var instruction in bestCallChain.TakeLast(32))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] 0x{instruction.Rip:X16}: {instruction.Text} " +
|
||||||
|
$"bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Span<byte> destination = stackalloc byte[128];
|
Span<byte> destination = stackalloc byte[128];
|
||||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||||
{
|
{
|
||||||
@@ -237,15 +268,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong callRip = returnRip + (ulong)i;
|
ulong callRip = returnRip + (ulong)i;
|
||||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||||
var importEntries = _importEntries;
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
|
||||||
{
|
{
|
||||||
if (importEntries[importIndex].Address != target)
|
if (_importEntries[importIndex].Address != target)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
@@ -272,14 +302,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (importEntries[importIndex].Address != slotTarget)
|
if (_importEntries[importIndex].Address != slotTarget)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Log.Debug(
|
||||||
@@ -303,6 +333,28 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ParseOptionalHexAddress(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = value.Trim();
|
||||||
|
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
text = text[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ulong.TryParse(
|
||||||
|
text,
|
||||||
|
System.Globalization.NumberStyles.HexNumber,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var address)
|
||||||
|
? address
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsPlausibleReturnAddress(ulong address)
|
private static bool IsPlausibleReturnAddress(ulong address)
|
||||||
{
|
{
|
||||||
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
|
||||||
@@ -352,7 +404,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -361,7 +413,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
|
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -393,12 +445,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
|
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||||
if (executable)
|
if (executable)
|
||||||
{
|
{
|
||||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
@@ -417,14 +469,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return (value + num) & ~num;
|
return (value + num) & ~num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostics helpers are static (reachable from static handler paths), so
|
|
||||||
// they use the platform injected into the backend active on this thread and
|
|
||||||
// fall back to the process-wide singleton only when no run is bound.
|
|
||||||
private static IHostMemory ResolveDiagnosticsHostMemory()
|
|
||||||
{
|
|
||||||
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsReadableProtection(uint protect)
|
private static bool IsReadableProtection(uint protect)
|
||||||
{
|
{
|
||||||
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
if ((protect & 0x100) != 0 || (protect & 1) != 0)
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ using System.Reflection;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
using SharpEmu.Core.Cpu.Native.Windows;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -19,6 +17,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||||
private static int _lazyCommitTraceCount;
|
private static int _lazyCommitTraceCount;
|
||||||
|
private static int _guestAllocatorHoleRecoveries;
|
||||||
|
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
@@ -30,12 +30,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||||
if (_rawExceptionHandlerStub == 0)
|
if (_rawExceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||||
}
|
}
|
||||||
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -45,22 +45,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||||
if (_exceptionHandlerStub == 0)
|
if (_exceptionHandlerStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||||
}
|
}
|
||||||
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||||
if (_unhandledFilterStub == 0)
|
if (_unhandledFilterStub == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
}
|
}
|
||||||
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -68,8 +68,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
||||||
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
|
||||||
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
||||||
@@ -108,19 +108,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
|
||||||
// the managed import gateway means the VEH->managed reentry happened from
|
|
||||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
|
||||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
|
||||||
{
|
{
|
||||||
TraceThreadMode(
|
return -1;
|
||||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
}
|
||||||
|
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (exceptionCode == 3221225477u &&
|
||||||
|
TryRecoverGuestAllocatorHole(exceptionRecord, contextRecord, rip))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -135,10 +139,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case WindowsFaultCodes.AccessViolation:
|
case 3221225477u:
|
||||||
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.FastFail:
|
case 3221226505u:
|
||||||
{
|
{
|
||||||
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
||||||
@@ -148,27 +152,56 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
ulong rax = ReadCtxU64(contextRecord, 120);
|
||||||
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
ulong rbx = ReadCtxU64(contextRecord, 144);
|
||||||
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
ulong rcx = ReadCtxU64(contextRecord, 128);
|
||||||
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
ulong rdx = ReadCtxU64(contextRecord, 136);
|
||||||
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
ulong rsi = ReadCtxU64(contextRecord, 168);
|
||||||
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
ulong rdi = ReadCtxU64(contextRecord, 176);
|
||||||
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
ulong rbp = ReadCtxU64(contextRecord, 160);
|
||||||
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
ulong r8 = ReadCtxU64(contextRecord, 184);
|
||||||
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
ulong r9 = ReadCtxU64(contextRecord, 192);
|
||||||
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
ulong r10 = ReadCtxU64(contextRecord, 200);
|
||||||
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
ulong r11 = ReadCtxU64(contextRecord, 208);
|
||||||
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
ulong r12 = ReadCtxU64(contextRecord, 216);
|
||||||
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
ulong r13 = ReadCtxU64(contextRecord, 224);
|
||||||
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
ulong r14 = ReadCtxU64(contextRecord, 232);
|
||||||
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
ulong r15 = ReadCtxU64(contextRecord, 240);
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Host thread: managed={Environment.CurrentManagedThreadId} " +
|
||||||
|
$"name='{Thread.CurrentThread.Name ?? "<unnamed>"}'");
|
||||||
|
if (_activeGuestThreadState is { } activeGuestThread)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Guest thread: handle=0x{activeGuestThread.ThreadHandle:X16} " +
|
||||||
|
$"name='{activeGuestThread.Name}' state={activeGuestThread.State} " +
|
||||||
|
$"last_import={activeGuestThread.LastImportNid ?? "<none>"} " +
|
||||||
|
$"last_ret=0x{activeGuestThread.LastReturnRip:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Last import registers: " +
|
||||||
|
$"rax=0x{Volatile.Read(ref activeGuestThread.LastImportRax):X16} " +
|
||||||
|
$"result_valid={Volatile.Read(ref activeGuestThread.LastImportResultValid) != 0} " +
|
||||||
|
$"rdi=0x{activeGuestThread.LastImportRdi:X16} " +
|
||||||
|
$"rsi=0x{activeGuestThread.LastImportRsi:X16} " +
|
||||||
|
$"rdx=0x{activeGuestThread.LastImportRdx:X16} " +
|
||||||
|
$"rcx=0x{activeGuestThread.LastImportRcx:X16} " +
|
||||||
|
$"r8=0x{activeGuestThread.LastImportR8:X16} " +
|
||||||
|
$"r9=0x{activeGuestThread.LastImportR9:X16}");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Last import stack args: " +
|
||||||
|
$"0=0x{activeGuestThread.LastImportStack0:X16} " +
|
||||||
|
$"1=0x{activeGuestThread.LastImportStack1:X16} " +
|
||||||
|
$"2=0x{activeGuestThread.LastImportStack2:X16} " +
|
||||||
|
$"3=0x{activeGuestThread.LastImportStack3:X16} " +
|
||||||
|
$"4=0x{activeGuestThread.LastImportStack4:X16} " +
|
||||||
|
$"5=0x{activeGuestThread.LastImportStack5:X16}");
|
||||||
|
}
|
||||||
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
||||||
@@ -193,7 +226,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong accessType = 0;
|
ulong accessType = 0;
|
||||||
ulong target = 0;
|
ulong target = 0;
|
||||||
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
|
||||||
{
|
{
|
||||||
accessType = *exceptionRecord->ExceptionInformation;
|
accessType = *exceptionRecord->ExceptionInformation;
|
||||||
target = exceptionRecord->ExceptionInformation[1];
|
target = exceptionRecord->ExceptionInformation[1];
|
||||||
@@ -206,9 +239,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
};
|
};
|
||||||
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
||||||
if (_hostMemory.Query(target, out var mbi))
|
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -225,13 +258,46 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_FAULT_STACK_WINDOW"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Full fault stack window (RSP-0x300..RSP+0x100):");
|
||||||
|
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
|
||||||
|
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
|
||||||
|
{
|
||||||
|
if (!TryReadHostQword(stackAddr, out var value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = unchecked((long)(stackAddr - rsp));
|
||||||
|
var relativeText = relative >= 0
|
||||||
|
? $"+0x{relative:X}"
|
||||||
|
: $"-0x{-relative:X}";
|
||||||
|
var symbolText = TryFormatNearestRuntimeSymbol(value, out var stackSymbol)
|
||||||
|
? $" [{stackSymbol}]"
|
||||||
|
: string.Empty;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] [rsp{relativeText}] " +
|
||||||
|
$"@0x{stackAddr:X16} = 0x{value:X16}{symbolText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpPointerWindow("fault-register-rbx", rbx, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-rsi", rsi, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-rdi", rdi, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-r13", r13, 0x60);
|
||||||
|
DumpPointerWindow("fault-register-r14", r14, 0x60);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
||||||
ulong frame = rbp;
|
ulong frame = rbp;
|
||||||
for (int i = 0; i < 12; i++)
|
for (int i = 0; i < 12; i++)
|
||||||
{
|
{
|
||||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
if (frame < 0x10000)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -256,7 +322,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
case WindowsFaultCodes.AccessViolation:
|
case 3221225477u:
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
||||||
@@ -290,22 +356,57 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
||||||
}
|
}
|
||||||
|
for (var stackIndex = 0; stackIndex < 16; stackIndex++)
|
||||||
|
{
|
||||||
|
byte[] stackSlot = new byte[8];
|
||||||
|
if (!TryReadHostBytes(rsp + (ulong)(stackIndex * 8), stackSlot))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var candidate = BitConverter.ToUInt64(stackSlot);
|
||||||
|
if (candidate < _entryPoint || candidate >= _entryPoint + 0x10000000 || candidate < 24)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] callSiteWindow = new byte[48];
|
||||||
|
if (TryReadHostBytes(candidate - 24, callSiteWindow))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
|
||||||
|
BitConverter.ToString(callSiteWindow).Replace("-", " "));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
||||||
}
|
}
|
||||||
DumpRecentImportTrace();
|
DumpRecentImportTrace();
|
||||||
DumpGuestDisasmDiagnostics(rip, rbp);
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
|
DumpGuestRegisterWindowDiagnostics(
|
||||||
|
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp,
|
||||||
|
r8, r9, r10, r11, r12, r13, r14, r15);
|
||||||
DumpGuestReferenceDiagnostics();
|
DumpGuestReferenceDiagnostics();
|
||||||
DumpGuestPointerWindowDiagnostics();
|
DumpGuestPointerWindowDiagnostics();
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.Breakpoint:
|
case 2147483651u:
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
||||||
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
||||||
break;
|
break;
|
||||||
case WindowsFaultCodes.IllegalInstruction:
|
case 1073741845u:
|
||||||
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Abort (SIGABRT)");
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
|
break;
|
||||||
|
case 3221225501u:
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
||||||
|
byte[] illegalCode = new byte[16];
|
||||||
|
if (TryReadHostBytes(rip, illegalCode))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(illegalCode).Replace("-", " "));
|
||||||
|
}
|
||||||
|
DumpRecentImportTrace();
|
||||||
|
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +420,108 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryRecoverAuxiliaryThreadExecuteFault(
|
||||||
|
EXCEPTION_RECORD* exceptionRecord,
|
||||||
|
void* contextRecord,
|
||||||
|
ulong rip)
|
||||||
|
{
|
||||||
|
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||||
|
rip >= 0x0000000800000000UL ||
|
||||||
|
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostExit = ActiveEntryReturnSentinelRip;
|
||||||
|
if (hostExit < 0x10000)
|
||||||
|
{
|
||||||
|
hostExit = unchecked((ulong)_guestReturnStub);
|
||||||
|
}
|
||||||
|
if (hostExit < 0x10000)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Could not recover auxiliary TBB execute fault: target=0x{rip:X16} " +
|
||||||
|
$"active_exit=0x{ActiveEntryReturnSentinelRip:X16} guest_return_stub=0x{unchecked((ulong)_guestReturnStub):X16}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||||
|
WriteCtxU64(contextRecord, 120, 0);
|
||||||
|
WriteCtxU64(contextRecord, 248, hostExit);
|
||||||
|
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||||
|
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||||
|
{
|
||||||
|
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] opcode = new byte[2];
|
||||||
|
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Interlocked.Increment(ref _ignoredGuestInt41Count);
|
||||||
|
WriteCtxU64(contextRecord, 248, rip + 2);
|
||||||
|
if (count <= 16 || count % 65536 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static bool TryRecoverGuestAllocatorHole(
|
||||||
|
EXCEPTION_RECORD* exceptionRecord,
|
||||||
|
void* contextRecord,
|
||||||
|
ulong rip)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal) ||
|
||||||
|
exceptionRecord->NumberParameters < 2 ||
|
||||||
|
exceptionRecord->ExceptionInformation[0] != 0 ||
|
||||||
|
exceptionRecord->ExceptionInformation[1] != 8 ||
|
||||||
|
ReadCtxU64(contextRecord, CTX_RDI) != 0 ||
|
||||||
|
rip < 0x10000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demon's Souls occasionally leaves an empty payload in a locked pool
|
||||||
|
// tree node. The allocator dereferences payload+8 before reaching its
|
||||||
|
// existing empty-pool fallback. Match the instruction stream instead of
|
||||||
|
// a title-specific absolute address, then resume at that fallback so the
|
||||||
|
// lock is released and the allocator can try its next backing pool.
|
||||||
|
const ulong allocatorHoleSignature = 0x634CFF568D08778BUL;
|
||||||
|
if (*(ulong*)rip != allocatorHoleSignature || *((byte*)rip + 8) != 0xF2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulong emptyPoolFallbackDelta = 0x8E;
|
||||||
|
WriteCtxU64(contextRecord, CTX_RIP, rip + emptyPoolFallbackDelta);
|
||||||
|
var recovery = Interlocked.Increment(ref _guestAllocatorHoleRecoveries);
|
||||||
|
if (recovery <= 16 || (recovery & (recovery - 1)) == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Guest allocator empty-node adapter recovery #{recovery}: " +
|
||||||
|
$"rip=0x{rip:X16} -> 0x{rip + emptyPoolFallbackDelta:X16}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsBenignHostDebugException(uint exceptionCode)
|
private static bool IsBenignHostDebugException(uint exceptionCode)
|
||||||
{
|
{
|
||||||
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
||||||
@@ -337,8 +540,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
void* contextRecord = pointers->ContextRecord;
|
void* contextRecord = pointers->ContextRecord;
|
||||||
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
||||||
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
||||||
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -398,7 +601,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp)
|
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp, ulong rsp)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -410,12 +613,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimized guest code frequently omits frame pointers. The return
|
||||||
|
// address at RSP is then more useful than an RBP walk and identifies the
|
||||||
|
// exact call site that supplied the faulting arguments.
|
||||||
|
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
|
||||||
|
{
|
||||||
|
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ulong frame = rbp;
|
ulong frame = rbp;
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
if (frame < 140733193388032L || frame > 140737488355327L)
|
if (frame < 0x10000)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -484,7 +695,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong address = scanBase;
|
ulong address = scanBase;
|
||||||
while (address < scanEnd)
|
while (address < scanEnd)
|
||||||
{
|
{
|
||||||
if (!_hostMemory.Query(address, out var mbi))
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -496,9 +707,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Committed &&
|
if (mbi.State == MEM_COMMIT &&
|
||||||
IsReadableProtection(mbi.RawProtection) &&
|
IsReadableProtection(mbi.Protect) &&
|
||||||
IsExecutableProtection(mbi.RawProtection))
|
IsExecutableProtection(mbi.Protect))
|
||||||
{
|
{
|
||||||
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
||||||
}
|
}
|
||||||
@@ -559,6 +770,55 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DumpGuestRegisterWindowDiagnostics(
|
||||||
|
ulong rax,
|
||||||
|
ulong rbx,
|
||||||
|
ulong rcx,
|
||||||
|
ulong rdx,
|
||||||
|
ulong rsi,
|
||||||
|
ulong rdi,
|
||||||
|
ulong rbp,
|
||||||
|
ulong rsp,
|
||||||
|
ulong r8,
|
||||||
|
ulong r9,
|
||||||
|
ulong r10,
|
||||||
|
ulong r11,
|
||||||
|
ulong r12,
|
||||||
|
ulong r13,
|
||||||
|
ulong r14,
|
||||||
|
ulong r15)
|
||||||
|
{
|
||||||
|
if (!string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_REGISTER_WINDOWS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A register can be the only surviving reference to the object or
|
||||||
|
// argument array that caused a native guest fault. Capture a compact
|
||||||
|
// window while the process is alive so the post-mortem log can
|
||||||
|
// distinguish an absent object from a partially initialized one.
|
||||||
|
var registers = new (string Name, ulong Value)[]
|
||||||
|
{
|
||||||
|
("rax", rax), ("rbx", rbx), ("rcx", rcx), ("rdx", rdx),
|
||||||
|
("rsi", rsi), ("rdi", rdi), ("rbp", rbp), ("rsp", rsp),
|
||||||
|
("r8", r8), ("r9", r9), ("r10", r10), ("r11", r11),
|
||||||
|
("r12", r12), ("r13", r13), ("r14", r14), ("r15", r15),
|
||||||
|
};
|
||||||
|
var seen = new HashSet<ulong>();
|
||||||
|
foreach (var (name, value) in registers)
|
||||||
|
{
|
||||||
|
if (value < 0x10000 || !seen.Add(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpPointerWindow($"register-{name}", value, 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ScanExecutableRegionForTargetReferences(
|
private void ScanExecutableRegionForTargetReferences(
|
||||||
ulong regionBase,
|
ulong regionBase,
|
||||||
ulong regionEnd,
|
ulong regionEnd,
|
||||||
@@ -803,13 +1063,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_hostMemory.Query(address, out var mbi))
|
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
||||||
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -848,7 +1108,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool TryReadHostBytes(ulong address, byte[] buffer)
|
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
|
||||||
{
|
{
|
||||||
if (address < 65536)
|
if (address < 65536)
|
||||||
{
|
{
|
||||||
@@ -861,9 +1121,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong end = address + (ulong)buffer.Length;
|
ulong end = address + (ulong)buffer.Length;
|
||||||
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
|
||||||
{
|
{
|
||||||
if (!_hostMemory.Query(page, out var mbi) ||
|
if (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||||
mbi.State != HostRegionState.Committed ||
|
mbi.State != MEM_COMMIT ||
|
||||||
!IsReadableProtection(mbi.RawProtection))
|
!IsReadableProtection(mbi.Protect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -976,25 +1236,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!_hostMemory.Query(faultAddress, out var mbi))
|
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||||
{
|
{
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1003,10 +1263,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
ulong committedSize = 0;
|
ulong committedSize = 0;
|
||||||
|
|
||||||
if (mbi.State == HostRegionState.Free)
|
if (mbi.State == 65536)
|
||||||
{
|
{
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = windowBase;
|
committedBase = windowBase;
|
||||||
@@ -1015,7 +1275,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = largeBase;
|
||||||
@@ -1026,13 +1286,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1045,7 +1305,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1053,13 +1313,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbi.State != HostRegionState.Reserved)
|
if (mbi.State != 8192)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = commitWindowBase;
|
committedBase = commitWindowBase;
|
||||||
@@ -1068,7 +1328,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = largeCommitBase;
|
||||||
@@ -1079,19 +1339,19 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!committed)
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = region64kBase;
|
committedBase = region64kBase;
|
||||||
committedSize = 65536uL;
|
committedSize = 65536uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
committedSize = 8192uL;
|
committedSize = 8192uL;
|
||||||
}
|
}
|
||||||
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = pageBase;
|
committedBase = pageBase;
|
||||||
@@ -1104,7 +1364,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
if (traceLazyCommit)
|
if (traceLazyCommit)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
@@ -1145,33 +1405,31 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||||
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
|
||||||
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
||||||
{
|
{
|
||||||
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
if (!TryReserveRange(reserveAddress, reserveSize))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
return TryCommitRange(commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,6 @@ using System.Collections.Generic;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.HLE.Host.Posix;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -37,6 +35,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
private bool _nativeWorkersDisposed;
|
private bool _nativeWorkersDisposed;
|
||||||
private int _nativeWorkerCreationFailedLogged;
|
private int _nativeWorkerCreationFailedLogged;
|
||||||
|
|
||||||
|
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern nint CreateThread(
|
||||||
|
nint lpThreadAttributes,
|
||||||
|
nuint dwStackSize,
|
||||||
|
nint lpStartAddress,
|
||||||
|
nint lpParameter,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
out uint lpThreadId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||||
// thread; falls back to the historical inline calli (guest frames above this
|
// thread; falls back to the historical inline calli (guest frames above this
|
||||||
// thread's managed frames) when workers are disabled or unavailable.
|
// thread's managed frames) when workers are disabled or unavailable.
|
||||||
@@ -49,7 +61,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var worker = RentNativeGuestExecutor();
|
var worker = RentNativeGuestExecutor();
|
||||||
if (worker is null)
|
if (worker is null)
|
||||||
{
|
{
|
||||||
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return CallNativeEntry(entryStub);
|
return CallNativeEntry(entryStub);
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -80,7 +92,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||||
{
|
{
|
||||||
if (NativeGuestWorkersDisabled)
|
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||||
|
// kernel32!CreateThread. POSIX hosts use the established inline entry
|
||||||
|
// path until the worker loop has a pthread/eventfd implementation.
|
||||||
|
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -234,7 +249,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||||
{
|
{
|
||||||
if (!EnsureHostRuntimeExports(backend._hostSymbols))
|
if (!EnsureKernel32Exports())
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -247,27 +262,32 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
|
private static bool EnsureKernel32Exports()
|
||||||
{
|
{
|
||||||
if (_exitThreadAddress != 0)
|
if (_exitThreadAddress != 0)
|
||||||
{
|
{
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||||
}
|
}
|
||||||
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
|
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||||
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
|
if (kernel32 == 0)
|
||||||
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||||
|
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||||
|
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Initialize()
|
private bool Initialize()
|
||||||
{
|
{
|
||||||
_selfHandle = GCHandle.Alloc(this);
|
_selfHandle = GCHandle.Alloc(this);
|
||||||
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
|
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||||
if (_controlBlock == null)
|
if (_controlBlock == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
|
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||||
if (_loopStub == null)
|
if (_loopStub == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -375,15 +395,17 @@ public sealed partial class DirectExecutionBackend
|
|||||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = 0;
|
uint oldProtect = 0;
|
||||||
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
|
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
|
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||||
_threadHandle = _backend._hostThreading.CreateNativeThread(
|
_threadHandle = CreateThread(
|
||||||
(nint)_loopStub,
|
|
||||||
0,
|
0,
|
||||||
WorkerStackReservation,
|
WorkerStackReservation,
|
||||||
|
(nint)_loopStub,
|
||||||
|
0,
|
||||||
|
StackSizeParamIsAReservation,
|
||||||
out _nativeThreadId);
|
out _nativeThreadId);
|
||||||
if (_threadHandle == 0)
|
if (_threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -511,7 +533,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||||
_prevState = _activeGuestThreadState;
|
_prevState = _activeGuestThreadState;
|
||||||
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
|
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||||
_entered = true;
|
_entered = true;
|
||||||
_activeExecutionBackend = backend;
|
_activeExecutionBackend = backend;
|
||||||
@@ -523,11 +545,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
_activeGuestThreadState = _runState;
|
_activeGuestThreadState = _runState;
|
||||||
backend.BindTlsBase(_runContext!);
|
backend.BindTlsBase(_runContext!);
|
||||||
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||||
if (_runState is { } state)
|
if (_runState is { } state)
|
||||||
{
|
{
|
||||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||||
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
|
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||||
}
|
}
|
||||||
if (_runAffinityMask != 0)
|
if (_runAffinityMask != 0)
|
||||||
{
|
{
|
||||||
@@ -557,7 +579,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||||
}
|
}
|
||||||
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||||
_activeExecutionBackend = _prevBackend;
|
_activeExecutionBackend = _prevBackend;
|
||||||
_activeCpuContext = _prevContext;
|
_activeCpuContext = _prevContext;
|
||||||
@@ -594,8 +616,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var exited = _threadHandle == 0;
|
var exited = _threadHandle == 0;
|
||||||
if (_threadHandle != 0)
|
if (_threadHandle != 0)
|
||||||
{
|
{
|
||||||
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
|
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||||
_backend._hostThreading.CloseThreadHandle(_threadHandle);
|
CloseHandle(_threadHandle);
|
||||||
_threadHandle = 0;
|
_threadHandle = 0;
|
||||||
}
|
}
|
||||||
if (!exited)
|
if (!exited)
|
||||||
@@ -609,12 +631,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (_loopStub != null)
|
if (_loopStub != null)
|
||||||
{
|
{
|
||||||
_backend._hostMemory.Free((ulong)_loopStub);
|
VirtualFree(_loopStub, 0u, 32768u);
|
||||||
_loopStub = null;
|
_loopStub = null;
|
||||||
}
|
}
|
||||||
if (_controlBlock != null)
|
if (_controlBlock != null)
|
||||||
{
|
{
|
||||||
_backend._hostMemory.Free((ulong)_controlBlock);
|
VirtualFree(_controlBlock, 0u, 32768u);
|
||||||
_controlBlock = null;
|
_controlBlock = null;
|
||||||
}
|
}
|
||||||
if (_selfHandle.IsAllocated)
|
if (_selfHandle.IsAllocated)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Native.Windows;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
// runtime keeps turning its own faults into managed exceptions.
|
// runtime keeps turning its own faults into managed exceptions.
|
||||||
|
|
||||||
private const int PosixSigIll = 4;
|
private const int PosixSigIll = 4;
|
||||||
|
private const int PosixSigTrap = 5;
|
||||||
|
private const int PosixSigAbort = 6;
|
||||||
private const int PosixSigSegv = 11;
|
private const int PosixSigSegv = 11;
|
||||||
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
|
||||||
|
|
||||||
@@ -62,6 +64,9 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
private static bool _posixSignalWarmup;
|
private static bool _posixSignalWarmup;
|
||||||
private static readonly nint[] _posixPreviousActions = new nint[32];
|
private static readonly nint[] _posixPreviousActions = new nint[32];
|
||||||
private static int _posixSignalTraceCount;
|
private static int _posixSignalTraceCount;
|
||||||
|
private static long _perfSignalCount;
|
||||||
|
private static readonly bool _perfSignalCounter =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PERF_MEM"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static int _posixSignalHandlerDepth;
|
private static int _posixSignalHandlerDepth;
|
||||||
@@ -88,10 +93,13 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
WarmUpPosixSignalPath();
|
WarmUpPosixSignalPath();
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
|
||||||
|
|
||||||
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
if (!InstallPosixSignalHandler(PosixSigSegv) ||
|
||||||
!InstallPosixSignalHandler(PosixSigBus) ||
|
!InstallPosixSignalHandler(PosixSigBus) ||
|
||||||
!InstallPosixSignalHandler(PosixSigIll))
|
!InstallPosixSignalHandler(PosixSigIll) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigTrap) ||
|
||||||
|
!InstallPosixSignalHandler(PosixSigAbort))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
|
||||||
}
|
}
|
||||||
@@ -132,8 +140,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
// action and sigaction(0, ...) fails with EINVAL).
|
// action and sigaction(0, ...) fails with EINVAL).
|
||||||
EXCEPTION_RECORD record = default;
|
EXCEPTION_RECORD record = default;
|
||||||
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
|
||||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
byte* contextRecord = stackalloc byte[Win64ContextSize];
|
||||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
new Span<byte>(contextRecord, Win64ContextSize).Clear();
|
||||||
EXCEPTION_POINTERS pointers;
|
EXCEPTION_POINTERS pointers;
|
||||||
pointers.ExceptionRecord = &record;
|
pointers.ExceptionRecord = &record;
|
||||||
pointers.ContextRecord = contextRecord;
|
pointers.ContextRecord = contextRecord;
|
||||||
@@ -190,8 +198,27 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
_posixSignalHandlerDepth++;
|
_posixSignalHandlerDepth++;
|
||||||
|
if (_perfSignalCounter)
|
||||||
|
{
|
||||||
|
var n = Interlocked.Increment(ref _perfSignalCount);
|
||||||
|
if (n % 100000 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[PERF][MEM] posix_faults={n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Guest-image write tracking runs first: it only needs the fault
|
||||||
|
// address (safe for host and guest threads alike) and must resume
|
||||||
|
// the faulting write immediately after restoring write access.
|
||||||
|
if (signal != PosixSigIll &&
|
||||||
|
siginfo != 0 &&
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||||
|
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
if (TryHandlePosixFault(signal, siginfo, ucontext))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -217,8 +244,8 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
|
byte* contextRecord = stackalloc byte[Win64ContextSize];
|
||||||
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
|
new Span<byte>(contextRecord, Win64ContextSize).Clear();
|
||||||
int[] offsets = PosixRegisterOffsets;
|
int[] offsets = PosixRegisterOffsets;
|
||||||
for (int i = 0; i < offsets.Length; i++)
|
for (int i = 0; i < offsets.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -231,6 +258,14 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
record.ExceptionCode = 3221225501u;
|
record.ExceptionCode = 3221225501u;
|
||||||
}
|
}
|
||||||
|
else if (signal == PosixSigTrap)
|
||||||
|
{
|
||||||
|
record.ExceptionCode = 2147483651u;
|
||||||
|
}
|
||||||
|
else if (signal == PosixSigAbort)
|
||||||
|
{
|
||||||
|
record.ExceptionCode = 1073741845u;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,425 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// POSIX replacements for the kernel32 helpers the native backend embeds in
|
||||||
|
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
|
||||||
|
/// convention the emitted call sites were written for (first argument in
|
||||||
|
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
|
||||||
|
/// emission code stays identical across platforms.
|
||||||
|
/// </summary>
|
||||||
|
internal static unsafe class PosixHostStubs
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static bool _initialized;
|
||||||
|
private static nint _tlsGetValueStub;
|
||||||
|
private static nint _queryPerformanceCounterStub;
|
||||||
|
private static nint _switchToThreadStub;
|
||||||
|
private static nint _sleepStub;
|
||||||
|
|
||||||
|
public static nint TlsGetValueStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _tlsGetValueStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint QueryPerformanceCounterStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SwitchToThreadStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _switchToThreadStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint SleepStubAddress
|
||||||
|
{
|
||||||
|
get { EnsureInitialized(); return _sleepStub; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint CreateWorkerEvent()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_create(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var semaphore = Marshal.AllocHGlobal(64);
|
||||||
|
if (sem_init(semaphore, 0, 0) != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(semaphore);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return semaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SignalWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
_ = dispatch_semaphore_signal(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sem_post(handle) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
|
||||||
|
return dispatch_semaphore_wait(handle, deadline) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutMilliseconds < 0)
|
||||||
|
{
|
||||||
|
while (sem_wait(handle) != 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
||||||
|
while (sem_trywait(handle) != 0)
|
||||||
|
{
|
||||||
|
if (Environment.TickCount64 >= deadlineTicks)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DestroyWorkerEvent(nint handle)
|
||||||
|
{
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
dispatch_release(handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = sem_destroy(handle);
|
||||||
|
Marshal.FreeHGlobal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
|
||||||
|
public static uint TlsAlloc()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
nuint key;
|
||||||
|
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint key32;
|
||||||
|
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsFree(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_key_delete_mac((nuint)key) == 0
|
||||||
|
: pthread_key_delete_linux(key) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TlsSetValue(uint key, nint value)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_setspecific_mac((nuint)key, value) == 0
|
||||||
|
: pthread_setspecific_linux(key, value) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nint TlsGetValue(uint key)
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsMacOS()
|
||||||
|
? pthread_getspecific_mac((nuint)key)
|
||||||
|
: pthread_getspecific_linux(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
|
||||||
|
public static uint GetCurrentThreadId()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
ulong tid;
|
||||||
|
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unchecked((uint)gettid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
|
||||||
|
/// thunk that accepts up to four integer arguments in the Win64 ABI the
|
||||||
|
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
|
||||||
|
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
|
||||||
|
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
|
||||||
|
/// the stack 16-byte aligned for the call, and forwards the rax result.
|
||||||
|
/// </summary>
|
||||||
|
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
|
||||||
|
{
|
||||||
|
var page = (byte*)HostMemory.Alloc(
|
||||||
|
null,
|
||||||
|
4096,
|
||||||
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
|
||||||
|
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
|
||||||
|
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
|
||||||
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
||||||
|
}
|
||||||
|
|
||||||
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
||||||
|
return (nint)page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildStubs();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildStubs()
|
||||||
|
{
|
||||||
|
var page = (byte*)HostMemory.Alloc(
|
||||||
|
null,
|
||||||
|
4096,
|
||||||
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
|
||||||
|
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
|
||||||
|
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
|
||||||
|
_sleepStub = EmitSleep(page, ref offset);
|
||||||
|
|
||||||
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
||||||
|
}
|
||||||
|
|
||||||
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitTlsGetValue(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
// On macOS x86-64 pthread keys index the gs-based thread specific
|
||||||
|
// data array directly, so TlsGetValue(index in ecx) collapses to a
|
||||||
|
// single load that clobbers nothing but RAX.
|
||||||
|
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||||
|
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: call pthread_getspecific, preserving the registers that are
|
||||||
|
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
||||||
|
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
|
||||||
|
// consumers only need a monotonically increasing counter, which rdtsc
|
||||||
|
// provides without leaving Win64-safe registers.
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
|
||||||
|
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
|
||||||
|
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
|
||||||
|
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSwitchToThread(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
var schedYield = ResolveLibcExport("sched_yield");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint EmitSleep(byte* page, ref int offset)
|
||||||
|
{
|
||||||
|
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||||
|
var usleep = ResolveLibcExport("usleep");
|
||||||
|
var start = (nint)(page + offset);
|
||||||
|
Emit(page, ref offset, 0x56); // push rsi
|
||||||
|
Emit(page, ref offset, 0x57); // push rdi
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||||
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||||
|
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||||
|
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||||
|
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||||
|
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||||
|
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||||
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||||
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||||
|
Emit(page, ref offset, 0x5F); // pop rdi
|
||||||
|
Emit(page, ref offset, 0x5E); // pop rsi
|
||||||
|
Emit(page, ref offset, 0xC3); // ret
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint ResolveLibcExport(string name)
|
||||||
|
{
|
||||||
|
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
|
||||||
|
return NativeLibrary.GetExport(libc, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Emit(byte* page, ref int offset, params byte[] bytes)
|
||||||
|
{
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
page[offset++] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
|
||||||
|
{
|
||||||
|
Emit(page, ref offset, 0x48, 0xB8);
|
||||||
|
*(long*)(page + offset) = value;
|
||||||
|
offset += sizeof(long);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
||||||
|
private static extern int pthread_key_create_linux(uint* key, nint destructor);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
||||||
|
private static extern int pthread_key_delete_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_mac(nuint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
||||||
|
private static extern int pthread_setspecific_linux(uint key, nint value);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_mac(nuint key);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
||||||
|
private static extern nint pthread_getspecific_linux(uint key);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int gettid();
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_create(long value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_signal(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern void dispatch_release(nint handle);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_init(nint semaphore, int shared, uint value);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_post(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_wait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_trywait(nint semaphore);
|
||||||
|
|
||||||
|
[DllImport("libc")]
|
||||||
|
private static extern int sem_destroy(nint semaphore);
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -12,15 +11,17 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
private readonly List<nint> _allocatedStubs = new();
|
private readonly List<nint> _allocatedStubs = new();
|
||||||
private readonly Dictionary<string, nint> _importHandlers = new();
|
private readonly Dictionary<string, nint> _importHandlers = new();
|
||||||
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
private readonly Dictionary<ulong, nint> _stubAddresses = new();
|
||||||
private readonly IHostMemory _hostMemory;
|
|
||||||
private byte* _pltMemory;
|
private byte* _pltMemory;
|
||||||
private int _pltOffset;
|
private int _pltOffset;
|
||||||
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
|
||||||
|
|
||||||
public StubManager(IHostMemory? hostMemory = null)
|
public StubManager()
|
||||||
{
|
{
|
||||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
_pltMemory = (byte*)VirtualAlloc(
|
||||||
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
|
null,
|
||||||
|
(nuint)PltMemorySize,
|
||||||
|
AllocationType.Reserve | AllocationType.Commit,
|
||||||
|
MemoryProtection.ExecuteReadWrite);
|
||||||
|
|
||||||
if (_pltMemory == null)
|
if (_pltMemory == null)
|
||||||
{
|
{
|
||||||
@@ -184,7 +185,7 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_pltMemory != null)
|
if (_pltMemory != null)
|
||||||
{
|
{
|
||||||
_hostMemory.Free((ulong)_pltMemory);
|
VirtualFree(_pltMemory, 0, FreeType.Release);
|
||||||
_pltMemory = null;
|
_pltMemory = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,5 +194,29 @@ public sealed unsafe class StubManager : IDisposable
|
|||||||
_stubAddresses.Clear();
|
_stubAddresses.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect) =>
|
||||||
|
HostMemory.Alloc(lpAddress, dwSize, (uint)flAllocationType, (uint)flProtect);
|
||||||
|
|
||||||
|
private static bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType) =>
|
||||||
|
HostMemory.Free(lpAddress, dwSize, (uint)dwFreeType);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum AllocationType : uint
|
||||||
|
{
|
||||||
|
Commit = 0x1000,
|
||||||
|
Reserve = 0x2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum MemoryProtection : uint
|
||||||
|
{
|
||||||
|
ExecuteReadWrite = 0x40,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum FreeType : uint
|
||||||
|
{
|
||||||
|
Release = 0x8000,
|
||||||
|
}
|
||||||
|
|
||||||
public delegate void ImportHandler(CpuContext context);
|
public delegate void ImportHandler(CpuContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public enum ProgramHeaderType : uint
|
|||||||
|
|
||||||
Tls = 7,
|
Tls = 7,
|
||||||
|
|
||||||
|
GnuEhFrame = 0x6474E550,
|
||||||
|
|
||||||
SceRela = 0x60000000,
|
SceRela = 0x60000000,
|
||||||
|
|
||||||
SceProcParam = 0x61000001,
|
SceProcParam = 0x61000001,
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ public sealed class SelfImage
|
|||||||
ulong procParamAddress = 0,
|
ulong procParamAddress = 0,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
string? titleId = null,
|
string? titleId = null,
|
||||||
string? version = null)
|
string? version = null,
|
||||||
|
uint tlsModuleId = 0,
|
||||||
|
ulong tlsMemorySize = 0,
|
||||||
|
ulong tlsStaticOffset = 0)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||||
@@ -44,6 +47,9 @@ public sealed class SelfImage
|
|||||||
Title = title;
|
Title = title;
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
Version = version;
|
Version = version;
|
||||||
|
TlsModuleId = tlsModuleId;
|
||||||
|
TlsMemorySize = tlsMemorySize;
|
||||||
|
TlsStaticOffset = tlsStaticOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSelf { get; }
|
public bool IsSelf { get; }
|
||||||
@@ -75,4 +81,11 @@ public sealed class SelfImage
|
|||||||
public string? TitleId { get; }
|
public string? TitleId { get; }
|
||||||
|
|
||||||
public string? Version { get; }
|
public string? Version { get; }
|
||||||
|
|
||||||
|
public uint TlsModuleId { get; }
|
||||||
|
|
||||||
|
public ulong TlsMemorySize { get; }
|
||||||
|
|
||||||
|
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
|
||||||
|
public ulong TlsStaticOffset { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -10,16 +11,12 @@ using SharpEmu.Core;
|
|||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Loader;
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
public sealed class SelfLoader : ISelfLoader
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
private const uint SelfMagic = 0x4F153D1D;
|
||||||
private const uint ElfMagic = 0x7F454C46;
|
|
||||||
private const uint Ps4SelfMagic = 0x4F153D1D;
|
|
||||||
private const uint Ps5SelfMagic = 0x5414F5EE;
|
|
||||||
private const ulong SelfSegmentFlag = 0x800;
|
private const ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
|
||||||
@@ -58,11 +55,22 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private const long DtSceSymTab = 0x61000039;
|
private const long DtSceSymTab = 0x61000039;
|
||||||
private const long DtSceSymTabSize = 0x6100003F;
|
private const long DtSceSymTabSize = 0x6100003F;
|
||||||
|
|
||||||
|
private const uint RelocationTypeNone = 0;
|
||||||
private const uint RelocationTypeAbsolute64 = 1;
|
private const uint RelocationTypeAbsolute64 = 1;
|
||||||
|
private const uint RelocationTypePc32 = 2;
|
||||||
|
private const uint RelocationTypePlt32 = 4;
|
||||||
private const uint RelocationTypeGlobalData = 6;
|
private const uint RelocationTypeGlobalData = 6;
|
||||||
private const uint RelocationTypeJumpSlot = 7;
|
private const uint RelocationTypeJumpSlot = 7;
|
||||||
private const uint RelocationTypeRelative = 8;
|
private const uint RelocationTypeRelative = 8;
|
||||||
private const uint RelocationTypeTlsModuleId = 16;
|
private const uint RelocationTypeUnsigned32 = 10;
|
||||||
|
private const uint RelocationTypeSigned32 = 11;
|
||||||
|
private const uint RelocationTypeTlsModuleId = 16; // R_X86_64_DTPMOD64
|
||||||
|
private const uint RelocationTypeTlsDtpOff64 = 17; // R_X86_64_DTPOFF64
|
||||||
|
private const uint RelocationTypeTlsTpOff64 = 18; // R_X86_64_TPOFF64
|
||||||
|
private const uint RelocationTypePc64 = 24;
|
||||||
|
private const uint RelocationTypeSize32 = 32;
|
||||||
|
private const uint RelocationTypeSize64 = 33;
|
||||||
|
private const uint RelocationTypeRelative64 = 38;
|
||||||
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
|
||||||
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
|
||||||
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
|
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
|
||||||
@@ -88,6 +96,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
|
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
|
||||||
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
|
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
|
||||||
|
|
||||||
|
static SelfLoader()
|
||||||
|
{
|
||||||
|
RunRelocationSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
|
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
|
||||||
{
|
{
|
||||||
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
|
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
|
||||||
@@ -156,16 +169,22 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
virtualMemory.Clear();
|
virtualMemory.Clear();
|
||||||
_nextTlsModuleId = 1;
|
_nextTlsModuleId = 1;
|
||||||
|
GuestTlsTemplate.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
|
||||||
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
|
||||||
|
|
||||||
var loadContext = ParseLayout(imageData);
|
var loadContext = ParseLayout(imageData);
|
||||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||||
ValidateElfHeader(elfHeader);
|
ValidateElfHeader(elfHeader);
|
||||||
|
|
||||||
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
|
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
|
||||||
|
var hasTlsSegment = TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var processTlsHeader, out _) &&
|
||||||
|
processTlsHeader.MemorySize != 0;
|
||||||
|
var tlsModuleId = hasTlsSegment
|
||||||
|
? (_nextTlsModuleId == 0 ? 1u : _nextTlsModuleId)
|
||||||
|
: 0u;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} " +
|
||||||
|
$"assigned={tlsModuleId} has_pt_tls={hasTlsSegment}");
|
||||||
|
|
||||||
var totalImageSize = CalculateTotalImageSize(programHeaders);
|
var totalImageSize = CalculateTotalImageSize(programHeaders);
|
||||||
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
|
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
|
||||||
@@ -198,6 +217,13 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
|
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
|
||||||
|
// Register every module before relocations so DTPMOD/DTPOFF/TPOFF use
|
||||||
|
// the module's real PT_TLS identity and Variant II static offset.
|
||||||
|
var tlsInfo = RegisterModuleTlsTemplate(
|
||||||
|
programHeaders,
|
||||||
|
virtualMemory,
|
||||||
|
imageBase,
|
||||||
|
tlsModuleId);
|
||||||
var importStubs = ResolveAndPatchImportStubs(
|
var importStubs = ResolveAndPatchImportStubs(
|
||||||
imageData,
|
imageData,
|
||||||
loadContext,
|
loadContext,
|
||||||
@@ -255,11 +281,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
|
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
if (tlsModuleId != 0 && _nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
|
||||||
{
|
{
|
||||||
_nextTlsModuleId++;
|
_nextTlsModuleId++;
|
||||||
}
|
}
|
||||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||||
|
|
||||||
return new SelfImage(
|
return new SelfImage(
|
||||||
loadContext.IsSelf,
|
loadContext.IsSelf,
|
||||||
@@ -276,7 +302,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
procParamAddress,
|
procParamAddress,
|
||||||
applicationInfo.Title,
|
applicationInfo.Title,
|
||||||
applicationInfo.TitleId,
|
applicationInfo.TitleId,
|
||||||
applicationInfo.Version);
|
applicationInfo.Version,
|
||||||
|
tlsModuleId,
|
||||||
|
tlsInfo.MemorySize,
|
||||||
|
tlsInfo.StaticOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
||||||
@@ -325,8 +354,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
throw new InvalidDataException("Input image is too small to contain an ELF header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
|
||||||
if (magic is Ps4SelfMagic or Ps5SelfMagic)
|
|
||||||
{
|
{
|
||||||
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
|
||||||
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
|
||||||
@@ -348,10 +376,19 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (magic != ElfMagic)
|
// Not a recognized (fake-signed) SELF. Only a bare, decrypted ELF is
|
||||||
|
// acceptable here; anything else — most commonly a still-encrypted
|
||||||
|
// retail eboot — must be reported clearly rather than failing later
|
||||||
|
// with an opaque "not a valid ELF header" message.
|
||||||
|
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
|
||||||
|
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
|
||||||
|
if (leadingWord != ElfMagicBigEndian)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException(
|
throw new InvalidDataException(
|
||||||
$"Unsupported executable signature 0x{magic:X8}");
|
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
|
||||||
|
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
|
||||||
|
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
|
||||||
|
$"fake-signed (fSELF) image.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
|
||||||
@@ -414,15 +451,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var virtualAddress = header.VirtualAddress + imageBase;
|
var virtualAddress = header.VirtualAddress + imageBase;
|
||||||
|
|
||||||
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||||
if (header.Alignment > 1)
|
if (header.Alignment > 1)
|
||||||
{
|
{
|
||||||
var vaddrMod = virtualAddress % header.Alignment;
|
var vaddrMod = virtualAddress % header.Alignment;
|
||||||
var offsetMod = header.Offset % header.Alignment;
|
var offsetMod = header.Offset % header.Alignment;
|
||||||
if (vaddrMod != offsetMod)
|
if (vaddrMod != offsetMod)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Segment {index} ELF alignment mismatch! " +
|
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||||
}
|
}
|
||||||
@@ -465,6 +502,43 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ModuleTlsInfo RegisterModuleTlsTemplate(
|
||||||
|
IReadOnlyList<ProgramHeader> programHeaders,
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
ulong imageBase,
|
||||||
|
uint tlsModuleId)
|
||||||
|
{
|
||||||
|
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var tlsHeader, out _) ||
|
||||||
|
tlsHeader.MemorySize == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tdata (initialized) bytes come from the mapped segment; tbss is the
|
||||||
|
// implicitly-zero remainder up to MemorySize.
|
||||||
|
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
|
||||||
|
var initImage = fileSize > 0 ? new byte[fileSize] : [];
|
||||||
|
if (fileSize > 0 &&
|
||||||
|
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
|
||||||
|
initImage = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var staticOffset = GuestTlsTemplate.RegisterModule(
|
||||||
|
tlsModuleId,
|
||||||
|
initImage,
|
||||||
|
tlsHeader.MemorySize,
|
||||||
|
tlsHeader.Alignment,
|
||||||
|
tlsHeader.VirtualAddress);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TLS] Module {tlsModuleId} TLS template: memsz=0x{tlsHeader.MemorySize:X} " +
|
||||||
|
$"filesz=0x{tlsHeader.FileSize:X} align=0x{tlsHeader.Alignment:X} " +
|
||||||
|
$"static_offset=0x{staticOffset:X} total_static=0x{GuestTlsTemplate.StaticTlsSize:X}");
|
||||||
|
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
|
||||||
ReadOnlySpan<byte> imageData,
|
ReadOnlySpan<byte> imageData,
|
||||||
LoadContext loadContext,
|
LoadContext loadContext,
|
||||||
@@ -544,12 +618,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
uint maxSymbolIndex = 0;
|
uint maxSymbolIndex = 0;
|
||||||
foreach (var relocation in relocations)
|
foreach (var relocation in relocations)
|
||||||
{
|
{
|
||||||
|
if (relocation.Type == RelocationTypeNone)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relocation.Type is RelocationTypeRelative or RelocationTypeTlsModuleId)
|
if (relocation.Type is RelocationTypeNone or RelocationTypeRelative or RelocationTypeRelative64 or RelocationTypeTlsModuleId)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -633,13 +712,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
importedRelocations = BuildImportedRelocations(descriptors);
|
importedRelocations = BuildImportedRelocations(descriptors);
|
||||||
|
|
||||||
var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids);
|
var stubImportNids = orderedImportNids
|
||||||
|
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
|
||||||
|
.ToArray();
|
||||||
|
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
|
||||||
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
|
||||||
|
|
||||||
int printCount = Math.Min(10, orderedImportNids.Count);
|
int printCount = Math.Min(10, stubImportNids.Length);
|
||||||
for (int i = 0; i < printCount; i++)
|
for (int i = 0; i < printCount; i++)
|
||||||
{
|
{
|
||||||
var nid = orderedImportNids[i];
|
var nid = stubImportNids[i];
|
||||||
var addr = stubsByAddress.First(x => x.Value == nid).Key;
|
var addr = stubsByAddress.First(x => x.Value == nid).Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,45 +745,65 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
foreach (var descriptor in descriptors)
|
foreach (var descriptor in descriptors)
|
||||||
{
|
{
|
||||||
ulong targetValue;
|
ulong symbolValue;
|
||||||
if (descriptor.ImportNid is null)
|
if (descriptor.ImportNid is null)
|
||||||
{
|
{
|
||||||
targetValue = AddSigned(descriptor.SymbolValue, descriptor.Addend);
|
symbolValue = descriptor.SymbolValue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
if (addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
|
||||||
|
{
|
||||||
|
symbolValue = stubAddress;
|
||||||
|
}
|
||||||
|
else if (descriptor.IsWeak)
|
||||||
|
{
|
||||||
|
// ELF unresolved weak definitions use S=0. They must not
|
||||||
|
// receive a trap import stub, which would turn a permitted
|
||||||
|
// null test into a call to an unresolved-import handler.
|
||||||
|
symbolValue = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
targetValue = AddSigned(stubAddress, descriptor.Addend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetValue < 0x1000)
|
var targetValue = ComputeRelocationValue(descriptor, symbolValue);
|
||||||
|
|
||||||
|
if (targetValue < 0x1000 && descriptor.ValueKind is
|
||||||
|
RelocationValueKind.TlsOffset or
|
||||||
|
RelocationValueKind.PcRelative or
|
||||||
|
RelocationValueKind.SymbolSize)
|
||||||
|
{
|
||||||
|
// A TLS offset (TPOFF64/DTPOFF64) is a signed displacement, not a
|
||||||
|
// mapped address, so a small or negative value here is expected.
|
||||||
|
}
|
||||||
|
else if (targetValue < 0x1000 && !descriptor.IsWeak)
|
||||||
{
|
{
|
||||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt64(virtualMemory, descriptor.TargetAddress, targetValue))
|
if (!TryWriteRelocationValue(virtualMemory, descriptor, targetValue, out var writeError))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}.");
|
throw new InvalidDataException(
|
||||||
|
$"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}: {writeError}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,29 +896,36 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
// Surface unsupported relocation types loudly instead of
|
||||||
|
// skipping silently: TLS-relative (17/18), COPY (5), and
|
||||||
|
// IRELATIVE/ifunc (37) leave their targets unrelocated, which
|
||||||
|
// manifests later as reads of zero or calls into address 0.
|
||||||
|
ReportUnsupportedRelocation(relocation.Type, relocation.Offset, imageBase);
|
||||||
|
if (relocation.Type is 5 or 37)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
throw new NotSupportedException(
|
||||||
|
$"Relocation type {relocation.Type} requires deferred runtime-linker processing.");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress))
|
var relocationWriteSize = GetRelocationWriteSize(relocation.Type);
|
||||||
|
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, relocationWriteSize, out var targetAddress))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relocation.Type == RelocationTypeRelative)
|
if (relocation.Type is RelocationTypeRelative or RelocationTypeRelative64)
|
||||||
{
|
{
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
@@ -830,7 +939,12 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (relocation.Type == RelocationTypeTlsModuleId)
|
if (relocation.Type == RelocationTypeTlsModuleId)
|
||||||
{
|
{
|
||||||
var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId;
|
if (tlsModuleId == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"R_X86_64_DTPMOD64 at 0x{targetAddress:X16} references a module without PT_TLS.");
|
||||||
|
}
|
||||||
|
var dtpmodValue = tlsModuleId;
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
0,
|
0,
|
||||||
@@ -851,50 +965,100 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend;
|
if (relocation.Type is RelocationTypeTlsDtpOff64 or RelocationTypeTlsTpOff64)
|
||||||
var symbolBind = GetSymbolBind(symbol.Info);
|
|
||||||
if (symbolBind == SymbolBindLocal)
|
|
||||||
{
|
{
|
||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
// Variant II static TLS: the module block sits at
|
||||||
if (symbolAddress == 0)
|
// [tp - blockSize, tp). DTPOFF64 is the module-relative offset
|
||||||
|
// (st_value + addend); TPOFF64 is that offset expressed relative
|
||||||
|
// to the thread pointer, i.e. minus the aligned block size.
|
||||||
|
var tlsSymbolOffset = AddSigned(symbol.Value, relocation.Addend);
|
||||||
|
if (!GuestTlsTemplate.TryGetStaticOffset(tlsModuleId, out var moduleStaticOffset))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"[LOADER][TLS] Missing PT_TLS registration for module {tlsModuleId}; " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"cannot apply relocation {relocation.Type} at 0x{targetAddress:X16}.");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
var tlsValue = relocation.Type == RelocationTypeTlsTpOff64
|
||||||
|
? unchecked(tlsSymbolOffset - moduleStaticOffset)
|
||||||
|
: tlsSymbolOffset;
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
0,
|
||||||
null,
|
null,
|
||||||
symbolAddress,
|
tlsValue,
|
||||||
RelocationValueKind.Pointer,
|
RelocationValueKind.TlsOffset,
|
||||||
IsDataImport: false));
|
IsDataImport: false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var symbolBind = GetSymbolBind(symbol.Info);
|
||||||
|
if (symbolIndex == 0)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: false));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbolBind == SymbolBindLocal)
|
||||||
|
{
|
||||||
|
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||||
|
? 0
|
||||||
|
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
|
if (symbolAddress == 0)
|
||||||
|
{
|
||||||
|
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: false));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (symbol.Value != 0)
|
if (symbol.Value != 0)
|
||||||
{
|
{
|
||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
|
||||||
|
? 0
|
||||||
|
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
|
||||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
symbol,
|
||||||
null,
|
|
||||||
symbolAddress,
|
symbolAddress,
|
||||||
RelocationValueKind.Pointer,
|
importNid: null,
|
||||||
IsDataImport: false));
|
isWeak: false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,16 +1066,26 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
|
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
|
||||||
{
|
{
|
||||||
|
if (symbolBind == SymbolBindWeak)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: true));
|
||||||
|
}
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -919,6 +1093,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var nid = ExtractNid(symbolName);
|
var nid = ExtractNid(symbolName);
|
||||||
if (string.IsNullOrWhiteSpace(nid))
|
if (string.IsNullOrWhiteSpace(nid))
|
||||||
{
|
{
|
||||||
|
if (symbolBind == SymbolBindWeak)
|
||||||
|
{
|
||||||
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: null,
|
||||||
|
isWeak: true));
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,14 +1111,72 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
orderedImportNids.Add(nid);
|
orderedImportNids.Add(nid);
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(CreateSymbolRelocationDescriptor(
|
||||||
|
relocation,
|
||||||
|
targetAddress,
|
||||||
|
symbol,
|
||||||
|
symbolAddress: 0,
|
||||||
|
importNid: nid,
|
||||||
|
isWeak: symbolBind == SymbolBindWeak));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RelocationDescriptor CreateSymbolRelocationDescriptor(
|
||||||
|
ElfRelocation relocation,
|
||||||
|
ulong targetAddress,
|
||||||
|
ElfSymbol symbol,
|
||||||
|
ulong symbolAddress,
|
||||||
|
string? importNid,
|
||||||
|
bool isWeak)
|
||||||
|
{
|
||||||
|
var valueKind = relocation.Type switch
|
||||||
|
{
|
||||||
|
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypePc64 => RelocationValueKind.PcRelative,
|
||||||
|
RelocationTypeSize32 or RelocationTypeSize64 => RelocationValueKind.SymbolSize,
|
||||||
|
_ => RelocationValueKind.Pointer,
|
||||||
|
};
|
||||||
|
var writeKind = relocation.Type switch
|
||||||
|
{
|
||||||
|
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypeSigned32 => RelocationWriteKind.Int32,
|
||||||
|
RelocationTypeUnsigned32 or RelocationTypeSize32 => RelocationWriteKind.UInt32,
|
||||||
|
_ => RelocationWriteKind.UInt64,
|
||||||
|
};
|
||||||
|
var symbolValue = valueKind == RelocationValueKind.SymbolSize
|
||||||
|
? symbol.Size
|
||||||
|
: symbolAddress;
|
||||||
|
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot
|
||||||
|
? 0
|
||||||
|
: relocation.Addend;
|
||||||
|
return new RelocationDescriptor(
|
||||||
targetAddress,
|
targetAddress,
|
||||||
addend,
|
addend,
|
||||||
nid,
|
importNid,
|
||||||
0,
|
symbolValue,
|
||||||
RelocationValueKind.Pointer,
|
valueKind,
|
||||||
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject));
|
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject,
|
||||||
|
writeKind,
|
||||||
|
isWeak);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldCreateImportStub(
|
||||||
|
string nid,
|
||||||
|
IReadOnlyList<RelocationDescriptor> descriptors,
|
||||||
|
IModuleManager? moduleManager)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < descriptors.Count; i++)
|
||||||
|
{
|
||||||
|
var descriptor = descriptors[i];
|
||||||
|
if (!string.Equals(descriptor.ImportNid, nid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterRuntimeSymbolsAndHooks(
|
private static void RegisterRuntimeSymbolsAndHooks(
|
||||||
@@ -959,8 +1201,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,8 +1276,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1418,8 +1660,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||||
if (mapSize > int.MaxValue)
|
if (mapSize > int.MaxValue)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||||
@@ -1691,11 +1933,51 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
private static bool IsSupportedRelocationType(uint relocationType)
|
private static bool IsSupportedRelocationType(uint relocationType)
|
||||||
{
|
{
|
||||||
return relocationType is
|
return relocationType is
|
||||||
|
RelocationTypeNone or
|
||||||
RelocationTypeAbsolute64 or
|
RelocationTypeAbsolute64 or
|
||||||
|
RelocationTypePc32 or
|
||||||
|
RelocationTypePlt32 or
|
||||||
RelocationTypeGlobalData or
|
RelocationTypeGlobalData or
|
||||||
RelocationTypeJumpSlot or
|
RelocationTypeJumpSlot or
|
||||||
RelocationTypeRelative or
|
RelocationTypeRelative or
|
||||||
RelocationTypeTlsModuleId;
|
RelocationTypeUnsigned32 or
|
||||||
|
RelocationTypeSigned32 or
|
||||||
|
RelocationTypeTlsModuleId or
|
||||||
|
RelocationTypeTlsDtpOff64 or
|
||||||
|
RelocationTypeTlsTpOff64 or
|
||||||
|
RelocationTypePc64 or
|
||||||
|
RelocationTypeSize32 or
|
||||||
|
RelocationTypeSize64 or
|
||||||
|
RelocationTypeRelative64;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<uint> _reportedUnsupportedRelocationTypes = new();
|
||||||
|
|
||||||
|
private static void ReportUnsupportedRelocation(uint relocationType, ulong offset, ulong imageBase)
|
||||||
|
{
|
||||||
|
// Report each distinct unsupported type once to keep the log useful.
|
||||||
|
lock (_reportedUnsupportedRelocationTypes)
|
||||||
|
{
|
||||||
|
if (!_reportedUnsupportedRelocationTypes.Add(relocationType))
|
||||||
|
{
|
||||||
|
if (IsFocusRelocationOffset(offset, imageBase))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocationType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = relocationType switch
|
||||||
|
{
|
||||||
|
5 => "R_X86_64_COPY",
|
||||||
|
37 => "R_X86_64_IRELATIVE (ifunc)",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] Unsupported relocation type {relocationType} ({name}) rejected " +
|
||||||
|
$"(first at off=0x{offset:X16}); COPY requires dependency symbol storage and IRELATIVE requires resolver execution.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong DetermineRequestedImageBase(
|
private static ulong DetermineRequestedImageBase(
|
||||||
@@ -1851,16 +2133,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||||
|
|
||||||
var guestAddr = location + imageBase;
|
var guestAddr = location + imageBase;
|
||||||
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||||
{
|
{
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (virtualMemory.TryRead(location, tableBytes))
|
if (virtualMemory.TryRead(location, tableBytes))
|
||||||
{
|
{
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1868,11 +2150,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
var slice = elfData.Slice((int)location, (int)size);
|
var slice = elfData.Slice((int)location, (int)size);
|
||||||
tableBytes = slice.ToArray();
|
tableBytes = slice.ToArray();
|
||||||
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||||
tableBytes = Array.Empty<byte>();
|
tableBytes = Array.Empty<byte>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1929,17 +2211,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||||
{
|
{
|
||||||
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||||
|
|
||||||
if (address == 0)
|
if (address == 0)
|
||||||
{
|
{
|
||||||
Log.Debug("-> return 0 (null)");
|
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||||
{
|
{
|
||||||
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1948,18 +2230,18 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var rebased = address + imageBase;
|
var rebased = address + imageBase;
|
||||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||||
{
|
{
|
||||||
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
||||||
return resolvedRebased;
|
return resolvedRebased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address < 0x10000)
|
if (address < 0x10000)
|
||||||
{
|
{
|
||||||
Log.Debug($"-> reject small 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2049,6 +2331,106 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
return virtualMemory.TryWrite(address, buffer);
|
return virtualMemory.TryWrite(address, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeRelocationValue(RelocationDescriptor descriptor, ulong resolvedSymbolValue)
|
||||||
|
{
|
||||||
|
var baseValue = descriptor.ValueKind == RelocationValueKind.SymbolSize
|
||||||
|
? descriptor.SymbolValue
|
||||||
|
: resolvedSymbolValue;
|
||||||
|
var value = AddSigned(baseValue, descriptor.Addend);
|
||||||
|
return descriptor.ValueKind == RelocationValueKind.PcRelative
|
||||||
|
? unchecked(value - descriptor.TargetAddress)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteRelocationValue(
|
||||||
|
IVirtualMemory virtualMemory,
|
||||||
|
RelocationDescriptor descriptor,
|
||||||
|
ulong value,
|
||||||
|
out string? error)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
var length = sizeof(ulong);
|
||||||
|
switch (descriptor.WriteKind)
|
||||||
|
{
|
||||||
|
case RelocationWriteKind.UInt64:
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RelocationWriteKind.UInt32:
|
||||||
|
if (value > uint.MaxValue)
|
||||||
|
{
|
||||||
|
error = $"value 0x{value:X16} overflows an unsigned 32-bit relocation";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)value);
|
||||||
|
length = sizeof(uint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RelocationWriteKind.Int32:
|
||||||
|
var signedValue = unchecked((long)value);
|
||||||
|
if (signedValue is < int.MinValue or > int.MaxValue)
|
||||||
|
{
|
||||||
|
error = $"value {signedValue} overflows a signed 32-bit relocation";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(buffer, (int)signedValue);
|
||||||
|
length = sizeof(int);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error = $"unknown relocation write kind {descriptor.WriteKind}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = virtualMemory.TryWrite(descriptor.TargetAddress, buffer[..length])
|
||||||
|
? null
|
||||||
|
: "target memory is not writable";
|
||||||
|
return error is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetRelocationWriteSize(uint relocationType)
|
||||||
|
{
|
||||||
|
return relocationType is
|
||||||
|
RelocationTypePc32 or
|
||||||
|
RelocationTypePlt32 or
|
||||||
|
RelocationTypeUnsigned32 or
|
||||||
|
RelocationTypeSigned32 or
|
||||||
|
RelocationTypeSize32
|
||||||
|
? sizeof(uint)
|
||||||
|
: sizeof(ulong);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunRelocationSelfChecks()
|
||||||
|
{
|
||||||
|
var pc32 = new RelocationDescriptor(
|
||||||
|
TargetAddress: 0x1000,
|
||||||
|
Addend: -4,
|
||||||
|
ImportNid: null,
|
||||||
|
SymbolValue: 0x1800,
|
||||||
|
RelocationValueKind.PcRelative,
|
||||||
|
IsDataImport: false,
|
||||||
|
RelocationWriteKind.Int32);
|
||||||
|
Debug.Assert(
|
||||||
|
unchecked((long)ComputeRelocationValue(pc32, pc32.SymbolValue)) == 0x7FC,
|
||||||
|
"R_X86_64_PC32 did not apply S + A - P.");
|
||||||
|
|
||||||
|
var weak = new RelocationDescriptor(
|
||||||
|
TargetAddress: 0x2000,
|
||||||
|
Addend: 7,
|
||||||
|
ImportNid: "weak",
|
||||||
|
SymbolValue: 0,
|
||||||
|
RelocationValueKind.Pointer,
|
||||||
|
IsDataImport: false,
|
||||||
|
IsWeak: true);
|
||||||
|
Debug.Assert(
|
||||||
|
ComputeRelocationValue(weak, resolvedSymbolValue: 0) == 7,
|
||||||
|
"An unresolved weak relocation did not use S=0.");
|
||||||
|
Debug.Assert(
|
||||||
|
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
|
||||||
|
"An unresolved weak symbol incorrectly received a trap import stub.");
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
{
|
{
|
||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
@@ -2257,6 +2639,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
|
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CPU backend executes guest instructions natively, so a non
|
||||||
|
// x86-64 image can only fail deep in execution with an opaque SIGILL.
|
||||||
|
// Catch it here with an actionable message. (EM_X86_64 == 62.)
|
||||||
|
const ushort ElfMachineX86_64 = 62;
|
||||||
|
if (header.Machine != ElfMachineX86_64)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Unsupported ELF machine type 0x{header.Machine:X4}; SharpEmu only runs x86-64 (EM_X86_64) images.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
|
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
|
||||||
@@ -2337,10 +2729,25 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
public uint Type => (uint)(Info & uint.MaxValue);
|
public uint Type => (uint)(Info & uint.MaxValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct ModuleTlsInfo(ulong MemorySize, ulong StaticOffset);
|
||||||
|
|
||||||
private enum RelocationValueKind : byte
|
private enum RelocationValueKind : byte
|
||||||
{
|
{
|
||||||
Pointer = 0,
|
Pointer = 0,
|
||||||
TlsModuleId = 1
|
TlsModuleId = 1,
|
||||||
|
// A pre-computed TLS offset written verbatim (TPOFF64/DTPOFF64). Unlike
|
||||||
|
// Pointer it is a signed displacement, not a mapped address, so it is
|
||||||
|
// patched as-is without the low-address validity warning.
|
||||||
|
TlsOffset = 2,
|
||||||
|
PcRelative = 3,
|
||||||
|
SymbolSize = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum RelocationWriteKind : byte
|
||||||
|
{
|
||||||
|
UInt64 = 0,
|
||||||
|
UInt32 = 1,
|
||||||
|
Int32 = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RelocationDescriptor(
|
private readonly record struct RelocationDescriptor(
|
||||||
@@ -2349,7 +2756,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
string? ImportNid,
|
string? ImportNid,
|
||||||
ulong SymbolValue,
|
ulong SymbolValue,
|
||||||
RelocationValueKind ValueKind,
|
RelocationValueKind ValueKind,
|
||||||
bool IsDataImport);
|
bool IsDataImport,
|
||||||
|
RelocationWriteKind WriteKind = RelocationWriteKind.UInt64,
|
||||||
|
bool IsWeak = false);
|
||||||
|
|
||||||
private enum SelfSegmentResolveStatus
|
private enum SelfSegmentResolveStatus
|
||||||
{
|
{
|
||||||
@@ -2389,14 +2798,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
public ulong FileSize => _fileSize;
|
public ulong FileSize => _fileSize;
|
||||||
|
|
||||||
public bool HasKnownLayout =>
|
public bool HasKnownLayout =>
|
||||||
((_ident0 == 0x4F &&
|
_ident0 == 0x4F &&
|
||||||
_ident1 == 0x15 &&
|
_ident1 == 0x15 &&
|
||||||
_ident2 == 0x3D &&
|
_ident2 == 0x3D &&
|
||||||
_ident3 == 0x1D) ||
|
_ident3 == 0x1D &&
|
||||||
(_ident0 == 0x54 &&
|
|
||||||
_ident1 == 0x14 &&
|
|
||||||
_ident2 == 0xF5 &&
|
|
||||||
_ident3 == 0xEE)) &&
|
|
||||||
_ident4 == 0x00 &&
|
_ident4 == 0x00 &&
|
||||||
_ident5 == 0x01 &&
|
_ident5 == 0x01 &&
|
||||||
_ident6 == 0x01 &&
|
_ident6 == 0x01 &&
|
||||||
|
|||||||
@@ -48,7 +48,107 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
{
|
{
|
||||||
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
|
_hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CrossPlatformHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
public static readonly CrossPlatformHostMemory Instance = new();
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
unchecked((ulong)HostMemory.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT,
|
||||||
|
ToRawProtection(protection)));
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
unchecked((ulong)HostMemory.Alloc(
|
||||||
|
(void*)desiredAddress,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_RESERVE,
|
||||||
|
ToRawProtection(protection)));
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) =>
|
||||||
|
HostMemory.Alloc(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
HostMemory.MEM_COMMIT,
|
||||||
|
ToRawProtection(protection)) != null;
|
||||||
|
|
||||||
|
public bool Free(ulong address) =>
|
||||||
|
HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE);
|
||||||
|
|
||||||
|
public bool Protect(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
HostPageProtection protection,
|
||||||
|
out uint rawOldProtection) =>
|
||||||
|
HostMemory.Protect(
|
||||||
|
(void*)address,
|
||||||
|
(nuint)size,
|
||||||
|
ToRawProtection(protection),
|
||||||
|
out rawOldProtection);
|
||||||
|
|
||||||
|
public bool ProtectRaw(
|
||||||
|
ulong address,
|
||||||
|
ulong size,
|
||||||
|
uint rawProtection,
|
||||||
|
out uint rawOldProtection) =>
|
||||||
|
HostMemory.Protect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
|
||||||
|
|
||||||
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
if (HostMemory.Query((void*)address, out var raw) == 0)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = raw.State switch
|
||||||
|
{
|
||||||
|
HostMemory.MEM_FREE_STATE => HostRegionState.Free,
|
||||||
|
HostMemory.MEM_RESERVE => HostRegionState.Reserved,
|
||||||
|
_ => HostRegionState.Committed,
|
||||||
|
};
|
||||||
|
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
raw.BaseAddress,
|
||||||
|
raw.AllocationBase,
|
||||||
|
raw.RegionSize,
|
||||||
|
state,
|
||||||
|
raw.State,
|
||||||
|
FromRawProtection(raw.Protect),
|
||||||
|
raw.Protect,
|
||||||
|
raw.AllocationProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size) =>
|
||||||
|
HostMemory.FlushInstructionCache((void*)address, (nuint)size);
|
||||||
|
|
||||||
|
private static uint ToRawProtection(HostPageProtection protection) => protection switch
|
||||||
|
{
|
||||||
|
HostPageProtection.NoAccess => HostMemory.PAGE_NOACCESS,
|
||||||
|
HostPageProtection.ReadOnly => HostMemory.PAGE_READONLY,
|
||||||
|
HostPageProtection.ReadWrite => HostMemory.PAGE_READWRITE,
|
||||||
|
HostPageProtection.Execute => HostMemory.PAGE_EXECUTE,
|
||||||
|
HostPageProtection.ReadExecute => HostMemory.PAGE_EXECUTE_READ,
|
||||||
|
HostPageProtection.ReadWriteExecute => HostMemory.PAGE_EXECUTE_READWRITE,
|
||||||
|
HostPageProtection.ExecuteWriteCopy => 0x80,
|
||||||
|
_ => HostMemory.PAGE_NOACCESS,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static HostPageProtection FromRawProtection(uint protection) => protection switch
|
||||||
|
{
|
||||||
|
HostMemory.PAGE_READONLY => HostPageProtection.ReadOnly,
|
||||||
|
HostMemory.PAGE_READWRITE => HostPageProtection.ReadWrite,
|
||||||
|
HostMemory.PAGE_EXECUTE => HostPageProtection.Execute,
|
||||||
|
HostMemory.PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
|
||||||
|
HostMemory.PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
|
||||||
|
0x80 => HostPageProtection.ExecuteWriteCopy,
|
||||||
|
_ => HostPageProtection.NoAccess,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
@@ -249,42 +349,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
// Under Rosetta 2 the kernel can ignore placement hints for whole
|
// POSIX treats the requested address as a hint, and Rosetta may
|
||||||
// windows, so page-stepped exact probes are pathological on macOS.
|
// relocate whole guest windows. Over-allocate once so the returned
|
||||||
// Linux must keep using the exact-address search below: PS5 resource
|
// host range always contains an aligned guest-visible start.
|
||||||
// descriptors cannot represent ordinary 0x7F... host mappings. Linux
|
if (!OperatingSystem.IsWindows())
|
||||||
// HostMemory uses MAP_FIXED_NOREPLACE, making those low-address probes
|
|
||||||
// safe without clobbering existing host mappings.
|
|
||||||
if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
{
|
||||||
// Prefer the requested low address. Besides matching the guest
|
|
||||||
// address model, this keeps the allocation representable by every
|
|
||||||
// PS5 GPU descriptor (the strictest ones carry 40 address bits).
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var exactAddress = AllocateAt(
|
|
||||||
cursor,
|
|
||||||
alignedSize,
|
|
||||||
executable,
|
|
||||||
allowAlternative: false);
|
|
||||||
if (exactAddress == cursor)
|
|
||||||
{
|
|
||||||
actualAddress = exactAddress;
|
|
||||||
UpdateAllocationSearchCursor(
|
|
||||||
desiredAddress,
|
|
||||||
effectiveAlignment,
|
|
||||||
executable,
|
|
||||||
exactAddress + alignedSize);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// Over-allocate by the alignment so a kernel-chosen placement
|
|
||||||
// always contains an aligned start; the unused head/tail stays
|
|
||||||
// part of the tracked region and is simply never handed out.
|
|
||||||
var reserveSize = effectiveAlignment > PageSize
|
var reserveSize = effectiveAlignment > PageSize
|
||||||
? alignedSize + effectiveAlignment
|
? alignedSize + effectiveAlignment
|
||||||
: alignedSize;
|
: alignedSize;
|
||||||
@@ -294,10 +363,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
if (posixAddress != 0)
|
if (posixAddress != 0)
|
||||||
{
|
{
|
||||||
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
|
||||||
const ulong gpuAddressLimit = 1UL << 40;
|
if (alignedBase + alignedSize <= posixAddress + reserveSize)
|
||||||
if (alignedBase < gpuAddressLimit &&
|
|
||||||
alignedSize <= gpuAddressLimit - alignedBase &&
|
|
||||||
alignedBase + alignedSize <= posixAddress + reserveSize)
|
|
||||||
{
|
{
|
||||||
actualAddress = alignedBase;
|
actualAddress = alignedBase;
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
|
||||||
@@ -327,21 +393,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (TryAllocateAtExact(cursor, alignedSize, executable, out actualAddress))
|
||||||
{
|
|
||||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
|
||||||
if (actualAddress == cursor)
|
|
||||||
{
|
{
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualAddress = 0;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,25 +7,21 @@ using SharpEmu.Core.Cpu.Disasm;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
|
||||||
using SharpEmu.Libs.VideoOut;
|
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 SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
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, int Handle, bool StartAtBoot);
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -89,19 +85,14 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
|
||||||
moduleManager.Freeze();
|
moduleManager.Freeze();
|
||||||
|
|
||||||
// Resolve the host platform once at the composition root; on unsupported
|
var virtualMemory = new PhysicalVirtualMemory();
|
||||||
// OSes this throws PlatformNotSupportedException with a clear message
|
|
||||||
// instead of failing on the first native call.
|
|
||||||
var hostPlatform = HostPlatform.Current;
|
|
||||||
|
|
||||||
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
|
|
||||||
|
|
||||||
var fileSystem = new PhysicalFileSystem();
|
var fileSystem = new PhysicalFileSystem();
|
||||||
|
|
||||||
return new SharpEmuRuntime(
|
return new SharpEmuRuntime(
|
||||||
new SelfLoader(),
|
new SelfLoader(),
|
||||||
virtualMemory,
|
virtualMemory,
|
||||||
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
|
new CpuDispatcher(virtualMemory, moduleManager),
|
||||||
moduleManager,
|
moduleManager,
|
||||||
Aerolib.Instance,
|
Aerolib.Instance,
|
||||||
cpuExecutionOptions,
|
cpuExecutionOptions,
|
||||||
@@ -139,20 +130,20 @@ 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);
|
||||||
Log.Info($"Loading: {ebootPath}");
|
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
LastBasicBlockTrace = null;
|
LastBasicBlockTrace = null;
|
||||||
LastMilestoneLog = null;
|
LastMilestoneLog = null;
|
||||||
|
FiberExports.ResetRuntimeState();
|
||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
LogAppBundleInfo(normalizedEbootPath, image);
|
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] 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);
|
||||||
@@ -175,7 +166,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -183,8 +174,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Dispatching, gen: {generation}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -194,8 +185,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Log.Info($"DispatchEntry returned: {result}");
|
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||||
Log.Info($"Dispatch result: {result}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -366,66 +357,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
|
||||||
{
|
|
||||||
var executableName = Path.GetFileName(ebootPath);
|
|
||||||
if (string.IsNullOrWhiteSpace(executableName))
|
|
||||||
{
|
|
||||||
executableName = "eboot.bin";
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
|
||||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
|
||||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
|
||||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("App bundle info:");
|
|
||||||
builder.AppendLine($"- Display name: {displayName}");
|
|
||||||
builder.AppendLine($"- Version: {version}");
|
|
||||||
builder.AppendLine($"- Title ID: {titleId}");
|
|
||||||
builder.AppendLine($"- Content ID: {contentId}");
|
|
||||||
builder.AppendLine($"- Executable: {executableName}");
|
|
||||||
builder.Append("- Platform: PlayStation 5");
|
|
||||||
Log.Info(builder.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ResolveContentId(string? bundleRoot)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(bundleRoot))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in new[]
|
|
||||||
{
|
|
||||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
|
||||||
Path.Combine(bundleRoot, "param.json"),
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if (!File.Exists(candidate))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
|
||||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
|
||||||
{
|
|
||||||
return contentId.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
@@ -440,6 +371,20 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump tools commonly place decrypted executables in an app0/decrypted
|
||||||
|
// sidecar while leaving Unity data in the parent app0 directory. Keep
|
||||||
|
// loading code/modules beside the decrypted eboot, but mount /app0 at
|
||||||
|
// the content-bearing parent so boot.config, metadata and assets resolve.
|
||||||
|
if (string.Equals(Path.GetFileName(app0Root), "decrypted", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var parentRoot = Path.GetDirectoryName(app0Root);
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentRoot) &&
|
||||||
|
Directory.Exists(Path.Combine(parentRoot, "Media")))
|
||||||
|
{
|
||||||
|
app0Root = parentRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
|
||||||
return new App0BindingScope(app0VariableName);
|
return new App0BindingScope(app0VariableName);
|
||||||
}
|
}
|
||||||
@@ -485,6 +430,56 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetEhFrameInfo(
|
||||||
|
SelfImage image,
|
||||||
|
ulong imageSize,
|
||||||
|
out ulong ehFrameHeaderAddress,
|
||||||
|
out ulong ehFrameAddress,
|
||||||
|
out ulong ehFrameSize)
|
||||||
|
{
|
||||||
|
ehFrameHeaderAddress = 0;
|
||||||
|
ehFrameAddress = 0;
|
||||||
|
ehFrameSize = 0;
|
||||||
|
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
|
||||||
|
? image.EntryPoint - image.ElfHeader.EntryPoint
|
||||||
|
: 0UL;
|
||||||
|
for (var i = 0; i < image.ProgramHeaders.Count; i++)
|
||||||
|
{
|
||||||
|
var header = image.ProgramHeaders[i];
|
||||||
|
if (header.HeaderType != ProgramHeaderType.GnuEhFrame || header.MemorySize < 8)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerAddress = imageBase + header.VirtualAddress;
|
||||||
|
Span<byte> ehHeader = stackalloc byte[8];
|
||||||
|
if (!_virtualMemory.TryRead(headerAddress, ehHeader) ||
|
||||||
|
ehHeader[0] != 1 ||
|
||||||
|
ehHeader[1] != 0x1B)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativeOffset = BinaryPrimitives.ReadInt32LittleEndian(ehHeader[4..]);
|
||||||
|
ehFrameHeaderAddress = headerAddress;
|
||||||
|
ehFrameAddress = unchecked((ulong)((long)headerAddress + 4 + relativeOffset));
|
||||||
|
if (ehFrameAddress < 0x10000)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativeEhFrameAddress = ehFrameAddress - imageBase;
|
||||||
|
ehFrameSize = header.VirtualAddress > relativeEhFrameAddress
|
||||||
|
? header.VirtualAddress - relativeEhFrameAddress
|
||||||
|
: imageSize > header.VirtualAddress
|
||||||
|
? imageSize - header.VirtualAddress
|
||||||
|
: 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private OrbisGen2Result? RunPreloadedModuleInitializers(
|
private OrbisGen2Result? RunPreloadedModuleInitializers(
|
||||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||||
Generation generation,
|
Generation generation,
|
||||||
@@ -494,20 +489,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
for (var i = 0; i < loadedModuleImages.Count; i++)
|
for (var i = 0; i < loadedModuleImages.Count; i++)
|
||||||
{
|
{
|
||||||
var loadedModule = loadedModuleImages[i];
|
var loadedModule = loadedModuleImages[i];
|
||||||
|
if (!loadedModule.StartAtBoot)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
|
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
|
||||||
if (initEntryPoint < 0x10000)
|
if (initEntryPoint < 0x10000)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!KernelModuleRegistry.TryBeginModuleStart(loadedModule.Handle, out _))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var moduleName = Path.GetFileName(loadedModule.Path);
|
var moduleName = Path.GetFileName(loadedModule.Path);
|
||||||
if (string.IsNullOrWhiteSpace(moduleName))
|
if (string.IsNullOrWhiteSpace(moduleName))
|
||||||
{
|
{
|
||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -516,10 +521,13 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
activeRuntimeSymbols,
|
activeRuntimeSymbols,
|
||||||
moduleName,
|
moduleName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
KernelModuleRegistry.CompleteModuleStart(
|
||||||
|
loadedModule.Handle,
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Module start failed: {moduleName} -> {result}");
|
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,8 +548,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -580,8 +588,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -613,12 +621,17 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
var moduleDirectories = new[]
|
var moduleDirectories = new[]
|
||||||
{
|
{
|
||||||
Path.Combine(ebootDirectory, "sce_module"),
|
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
|
||||||
Path.Combine(ebootDirectory, "sce_modules"),
|
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
|
||||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
|
||||||
|
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
|
||||||
|
// them up front so the HLE loader can return a real module handle and dlsym
|
||||||
|
// can resolve their exports, but defer DT_INIT until the guest requests them.
|
||||||
|
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
|
||||||
}
|
}
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(Directory.Exists)
|
.Select(group => group.First())
|
||||||
|
.Where(entry => Directory.Exists(entry.Path))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (moduleDirectories.Length == 0)
|
if (moduleDirectories.Length == 0)
|
||||||
@@ -628,28 +641,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(directory => Directory
|
.SelectMany(directory => Directory
|
||||||
.EnumerateFiles(directory)
|
.EnumerateFiles(directory.Path)
|
||||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(path =>
|
.Select(path => (Path: path, directory.StartAtBoot)))
|
||||||
|
.Where(entry =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(entry.Path);
|
||||||
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
})
|
})
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group.First())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var modulePaths = allModulePaths
|
var modulePaths = allModulePaths
|
||||||
.Where(ShouldPreloadModule)
|
.Where(entry => ShouldPreloadModule(entry.Path))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var skippedModules = allModulePaths
|
var skippedModules = allModulePaths
|
||||||
.Where(path => !ShouldPreloadModule(path))
|
.Where(entry => !ShouldPreloadModule(entry.Path))
|
||||||
.Select(Path.GetFileName)
|
.Select(entry => Path.GetFileName(entry.Path))
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
if (skippedModules.Length > 0)
|
||||||
{
|
{
|
||||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulePaths.Length == 0)
|
if (modulePaths.Length == 0)
|
||||||
@@ -657,14 +672,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories.Select(entry => entry.Path))}");
|
||||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
var mergedSymbolCount = 0;
|
var mergedSymbolCount = 0;
|
||||||
foreach (var modulePath in modulePaths)
|
foreach (var moduleEntry in modulePaths)
|
||||||
{
|
{
|
||||||
|
var modulePath = moduleEntry.Path;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(modulePath);
|
var fileInfo = new FileInfo(modulePath);
|
||||||
@@ -689,25 +705,52 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
|
||||||
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
|
||||||
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
InstallNativePluginCompatibilityHooks(importStubs, moduleImage, modulePath);
|
||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
var moduleHandle = RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
|
||||||
|
var moduleName = Path.GetFileName(modulePath);
|
||||||
|
var startAtBoot = moduleEntry.StartAtBoot ||
|
||||||
|
string.Equals(moduleName, "libfmod.prx", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(moduleName, "libfmodstudio.prx", StringComparison.OrdinalIgnoreCase);
|
||||||
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage, moduleHandle, startAtBoot));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"[RUNTIME] 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++;
|
||||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void InstallNativePluginCompatibilityHooks(
|
||||||
|
IDictionary<ulong, string> importStubs,
|
||||||
|
SelfImage moduleImage,
|
||||||
|
string modulePath)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Path.GetFileName(modulePath), "libfmod.prx", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<string> compatibilityNids = ["uPLTdl3psGk"];
|
||||||
|
foreach (var nid in compatibilityNids)
|
||||||
|
{
|
||||||
|
if (moduleImage.RuntimeSymbols.TryGetValue(nid, out var address) && address >= 0x10000)
|
||||||
|
{
|
||||||
|
importStubs[address] = nid;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Installed FMOD compatibility hook: {nid} -> 0x{address:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RebindImportedDataSymbols(
|
private void RebindImportedDataSymbols(
|
||||||
SelfImage mainImage,
|
SelfImage mainImage,
|
||||||
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
|
||||||
@@ -724,8 +767,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,8 +800,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -770,8 +813,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -780,8 +823,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -817,8 +860,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -912,7 +955,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return !PreloadSkipModules.Contains(fileName);
|
return !PreloadSkipModules.Contains(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
private int RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
|
||||||
{
|
{
|
||||||
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
if (!TryComputeImageRange(image, out var baseAddress, out var size))
|
||||||
{
|
{
|
||||||
@@ -920,15 +963,27 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
size = 0;
|
size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = TryGetEhFrameInfo(
|
||||||
|
image,
|
||||||
|
size,
|
||||||
|
out var ehFrameHeaderAddress,
|
||||||
|
out var ehFrameAddress,
|
||||||
|
out var ehFrameSize);
|
||||||
var handle = KernelModuleRegistry.RegisterModule(
|
var handle = KernelModuleRegistry.RegisterModule(
|
||||||
modulePath,
|
modulePath,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
size,
|
size,
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
|
image.InitFunctionEntryPoint,
|
||||||
|
ehFrameHeaderAddress,
|
||||||
|
ehFrameAddress,
|
||||||
|
ehFrameSize,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Log.Info(
|
KernelModuleRegistry.RegisterModuleSymbols(handle, image.RuntimeSymbols);
|
||||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
Console.Error.WriteLine(
|
||||||
|
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||||
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
|
|
||||||
public ulong GsBase { get; set; }
|
public ulong GsBase { get; set; }
|
||||||
|
|
||||||
|
/// <summary>x87 control word observed at the current guest boundary.</summary>
|
||||||
|
public ushort FpuControlWord { get; set; } = 0x037F;
|
||||||
|
|
||||||
|
/// <summary>MXCSR observed at the current guest boundary.</summary>
|
||||||
|
public uint Mxcsr { get; set; } = 0x1F80;
|
||||||
|
|
||||||
public ulong this[CpuRegister register]
|
public ulong this[CpuRegister register]
|
||||||
{
|
{
|
||||||
get => _registers[(int)register];
|
get => _registers[(int)register];
|
||||||
|
|||||||
@@ -0,0 +1,613 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects guest CPU writes into memory that backs a host GPU image. On PS5
|
||||||
|
/// render targets alias unified memory, so games freely mix CPU writes and GPU
|
||||||
|
/// draws on the same surface (Chowdren titles memset their fog layers every
|
||||||
|
/// frame). Host GPU images are separate storage, so the video backend needs to
|
||||||
|
/// know when the guest CPU rewrote a surface to re-upload it. Ranges are
|
||||||
|
/// write-protected; the first write faults, the fault handler restores write
|
||||||
|
/// access and marks the range dirty, and the video backend consumes the dirty
|
||||||
|
/// flag once per flip and re-arms protection after re-uploading.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe class GuestImageWriteTracker
|
||||||
|
{
|
||||||
|
private const int ProtRead = 0x1;
|
||||||
|
private const int ProtWrite = 0x2;
|
||||||
|
private const int ClockMonotonicRaw = 4;
|
||||||
|
|
||||||
|
private sealed class TrackedRange
|
||||||
|
{
|
||||||
|
public ulong Address;
|
||||||
|
public ulong ByteCount;
|
||||||
|
public ulong Start;
|
||||||
|
public ulong End;
|
||||||
|
public int Dirty;
|
||||||
|
public int Armed;
|
||||||
|
public int FirstCpuWriteSeen;
|
||||||
|
public int PendingFirstCpuWrite;
|
||||||
|
public bool TraceLifetime;
|
||||||
|
public long SourceSequence;
|
||||||
|
public long FirstCpuWriteTraceSequence;
|
||||||
|
public long FirstCpuWriteTimestampNanoseconds;
|
||||||
|
public ulong FirstCpuWriteAddress;
|
||||||
|
public ulong FirstCpuWritePage;
|
||||||
|
public string Source = "unspecified";
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct Timespec
|
||||||
|
{
|
||||||
|
public long Seconds;
|
||||||
|
public long Nanoseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
|
||||||
|
|
||||||
|
// Snapshot array read lock-free from the signal handler; rebuilt on every
|
||||||
|
// mutation under the gate. Signal handlers must not take managed locks.
|
||||||
|
private static TrackedRange[] _rangeSnapshot = [];
|
||||||
|
|
||||||
|
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||||
|
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
||||||
|
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||||
|
private static readonly (bool Wildcard, string[] Sources) _lifetimeSourceTraceFilter =
|
||||||
|
ParseSourceList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME"));
|
||||||
|
private static readonly bool _lifetimeTraceEnabled =
|
||||||
|
_lifetimeTraceFilter.Wildcard ||
|
||||||
|
_lifetimeTraceFilter.Addresses.Length != 0 ||
|
||||||
|
_lifetimeSourceTraceFilter.Wildcard ||
|
||||||
|
_lifetimeSourceTraceFilter.Sources.Length != 0;
|
||||||
|
private static readonly long _lifetimeTraceEpochNanoseconds =
|
||||||
|
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
||||||
|
private static long _lifetimeTraceSequence;
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
||||||
|
private static extern int Mprotect(nint address, nuint length, int protection);
|
||||||
|
|
||||||
|
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
||||||
|
private static extern int ClockGetTime(int clockId, Timespec* time);
|
||||||
|
|
||||||
|
public static bool Enabled => _enabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exercises the fault-handling path once outside signal context so every
|
||||||
|
/// branch is JIT-compiled (and, under Rosetta 2, translated) before a real
|
||||||
|
/// fault arrives — a cold signal path is silently never entered there.
|
||||||
|
/// </summary>
|
||||||
|
public static void WarmUp()
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scratch = NativeMemory.AllocZeroed(4096);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
||||||
|
// capture path before a real protected-page write reaches it.
|
||||||
|
_ = GetMonotonicNanoseconds();
|
||||||
|
var address = (ulong)scratch;
|
||||||
|
Track(address, 4096);
|
||||||
|
_ = TryHandleWriteFault(address);
|
||||||
|
_ = ConsumeDirty(address);
|
||||||
|
Untrack(address);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMemory.Free(scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Registers a range and arms write protection on it.</summary>
|
||||||
|
public static void Track(
|
||||||
|
ulong address,
|
||||||
|
ulong byteCount,
|
||||||
|
long sourceSequence = 0,
|
||||||
|
string source = "unspecified")
|
||||||
|
{
|
||||||
|
if (!_enabled || address == 0 || byteCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (start, length) = PageAlign(address, byteCount);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_rangesByAddress.TryGetValue(address, out var range);
|
||||||
|
if (range is not null &&
|
||||||
|
(range.Start != start ||
|
||||||
|
range.End != start + length ||
|
||||||
|
range.ByteCount != byteCount))
|
||||||
|
{
|
||||||
|
// Never resize an object that is still reachable from the
|
||||||
|
// signal handler's lock-free snapshot. Retire it and publish
|
||||||
|
// a fresh immutable range.
|
||||||
|
DisarmLocked(range, "replace-range");
|
||||||
|
_rangesByAddress.Remove(address);
|
||||||
|
range = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range is null)
|
||||||
|
{
|
||||||
|
range = new TrackedRange
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
ByteCount = byteCount,
|
||||||
|
Start = start,
|
||||||
|
End = start + length,
|
||||||
|
TraceLifetime =
|
||||||
|
ShouldTraceRange(start, start + length) || ShouldTraceSource(source),
|
||||||
|
SourceSequence = sourceSequence,
|
||||||
|
Source = source,
|
||||||
|
};
|
||||||
|
_rangesByAddress[address] = range;
|
||||||
|
RebuildSnapshotLocked();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
range.SourceSequence = sourceSequence;
|
||||||
|
range.Source = source;
|
||||||
|
range.TraceLifetime =
|
||||||
|
ShouldTraceRange(range.Start, range.End) || ShouldTraceSource(source);
|
||||||
|
ArmLocked(range, "arm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Untrack(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
DisarmLocked(range, "untrack");
|
||||||
|
_rangesByAddress.Remove(address);
|
||||||
|
RebuildSnapshotLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the guest CPU wrote the range since the last call,
|
||||||
|
/// clearing the flag. The caller re-arms via <see cref="Rearm"/> after it
|
||||||
|
/// finished reading the guest bytes.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ConsumeDirty(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
return Interlocked.Exchange(ref range.Dirty, 0) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-consuming variant of <see cref="ConsumeDirty"/>: reports whether
|
||||||
|
/// the range has been written since it was last re-armed, leaving the
|
||||||
|
/// flag for the owner that evicts and re-uploads.
|
||||||
|
/// </summary>
|
||||||
|
public static bool PeekDirty(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
return Volatile.Read(ref range.Dirty) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Rearm(ulong address)
|
||||||
|
{
|
||||||
|
if (!_enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_rangesByAddress.TryGetValue(address, out var range))
|
||||||
|
{
|
||||||
|
ArmLocked(range, "rearm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares pages touched by a managed HLE memory write. Native guest
|
||||||
|
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
|
||||||
|
/// POSIX signal bridge, but a managed Buffer.MemoryCopy into a protected
|
||||||
|
/// page is surfaced by the runtime as a fatal AccessViolation instead of
|
||||||
|
/// a resumable guest fault. Visit every page in the write span up front so
|
||||||
|
/// all overlapping texture owners are dirtied and made writable.
|
||||||
|
/// </summary>
|
||||||
|
public static void NotifyManagedWrite(ulong address, ulong byteCount)
|
||||||
|
{
|
||||||
|
if (!_enabled || address == 0 || byteCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = address > ulong.MaxValue - byteCount
|
||||||
|
? ulong.MaxValue
|
||||||
|
: address + byteCount;
|
||||||
|
var candidate = address;
|
||||||
|
while (candidate < end)
|
||||||
|
{
|
||||||
|
_ = TryHandleWriteFault(candidate);
|
||||||
|
var nextPage = (candidate & ~0xFFFUL) + 0x1000UL;
|
||||||
|
if (nextPage <= candidate)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
candidate = nextPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes scalar first-write records captured by the POSIX signal handler.
|
||||||
|
/// Call only from ordinary managed execution, never from signal context.
|
||||||
|
/// </summary>
|
||||||
|
public static void FlushPendingDiagnostics()
|
||||||
|
{
|
||||||
|
if (!_enabled || !_lifetimeTraceEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var range in _rangesByAddress.Values)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal-handler entry: if the fault address lies in a tracked, armed
|
||||||
|
/// range, restore write access, mark the range dirty, and return true so
|
||||||
|
/// the faulting write can be retried. Must not allocate or lock.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryHandleWriteFault(ulong faultAddress)
|
||||||
|
{
|
||||||
|
if (!_enabled || faultAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranges = Volatile.Read(ref _rangeSnapshot);
|
||||||
|
var writableStart = ulong.MaxValue;
|
||||||
|
var writableEnd = 0UL;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (faultAddress < range.Start || faultAddress >= range.End)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writableStart = Math.Min(writableStart, range.Start);
|
||||||
|
writableEnd = Math.Max(writableEnd, range.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writableStart == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ranges are page-aligned and may overlap (font atlases and other
|
||||||
|
// suballocations commonly share pages). Unprotecting one range also
|
||||||
|
// makes every overlapping tracked page writable. Expand to the full
|
||||||
|
// transitive overlap and dirty/disarm every owner, otherwise only the
|
||||||
|
// first dictionary entry observes the write and the others retain a
|
||||||
|
// stale cached texture indefinitely.
|
||||||
|
var expanded = true;
|
||||||
|
while (expanded)
|
||||||
|
{
|
||||||
|
expanded = false;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = Math.Min(writableStart, range.Start);
|
||||||
|
var end = Math.Max(writableEnd, range.End);
|
||||||
|
if (start != writableStart || end != writableEnd)
|
||||||
|
{
|
||||||
|
writableStart = start;
|
||||||
|
writableEnd = end;
|
||||||
|
expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsUnprotect = false;
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start < writableEnd && range.End > writableStart &&
|
||||||
|
Volatile.Read(ref range.Armed) != 0)
|
||||||
|
{
|
||||||
|
needsUnprotect = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUnprotect &&
|
||||||
|
Mprotect(
|
||||||
|
(nint)writableStart,
|
||||||
|
(nuint)(writableEnd - writableStart),
|
||||||
|
ProtRead | ProtWrite) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < ranges.Length; index++)
|
||||||
|
{
|
||||||
|
var range = ranges[index];
|
||||||
|
if (range.Start >= writableEnd || range.End <= writableStart)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
|
||||||
|
if (wasArmed &&
|
||||||
|
range.TraceLifetime &&
|
||||||
|
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
|
||||||
|
{
|
||||||
|
// Signal context: capture preallocated scalar fields only.
|
||||||
|
// Formatting and I/O are deferred to a locked safe path.
|
||||||
|
range.FirstCpuWriteTraceSequence =
|
||||||
|
Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||||
|
range.FirstCpuWriteTimestampNanoseconds = GetMonotonicNanoseconds();
|
||||||
|
range.FirstCpuWriteAddress = faultAddress;
|
||||||
|
range.FirstCpuWritePage = faultAddress & ~0xFFFUL;
|
||||||
|
Volatile.Write(ref range.PendingFirstCpuWrite, 1);
|
||||||
|
Volatile.Write(ref range.FirstCpuWriteSeen, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Volatile.Write(ref range.Dirty, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ArmLocked(TrackedRange range, string operation)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
if (Interlocked.Exchange(ref range.Armed, 1) == 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new publication/rearm starts a new first-write lifetime.
|
||||||
|
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
||||||
|
var failed = Mprotect(
|
||||||
|
(nint)range.Start,
|
||||||
|
(nuint)(range.End - range.Start),
|
||||||
|
ProtRead) != 0;
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref range.Armed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.TraceLifetime)
|
||||||
|
{
|
||||||
|
TraceLifetime(
|
||||||
|
range,
|
||||||
|
failed ? $"{operation}-failed-errno-{Marshal.GetLastPInvokeError()}" : operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisarmLocked(TrackedRange range, string operation)
|
||||||
|
{
|
||||||
|
FlushPendingFirstCpuWrite(range);
|
||||||
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
|
||||||
|
if (wasArmed)
|
||||||
|
{
|
||||||
|
_ = Mprotect(
|
||||||
|
(nint)range.Start,
|
||||||
|
(nuint)(range.End - range.Start),
|
||||||
|
ProtRead | ProtWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.TraceLifetime)
|
||||||
|
{
|
||||||
|
TraceLifetime(range, wasArmed ? operation : $"{operation}-already-disarmed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildSnapshotLocked()
|
||||||
|
{
|
||||||
|
_rangeSnapshot = _rangesByAddress.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
|
||||||
|
{
|
||||||
|
const ulong pageMask = 0xFFFUL;
|
||||||
|
var start = address & ~pageMask;
|
||||||
|
var end = (address + byteCount + pageMask) & ~pageMask;
|
||||||
|
return (start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceRange(ulong start, ulong end)
|
||||||
|
{
|
||||||
|
if (_lifetimeTraceFilter.Wildcard)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses = _lifetimeTraceFilter.Addresses;
|
||||||
|
for (var index = 0; index < addresses.Length; index++)
|
||||||
|
{
|
||||||
|
if (addresses[index] >= start && addresses[index] < end)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool Wildcard, ulong[] Addresses) ParseAddressList(string? addresses)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(addresses))
|
||||||
|
{
|
||||||
|
return (false, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedAddresses = new List<ulong>();
|
||||||
|
foreach (var token in addresses.Split(
|
||||||
|
[',', ';', ' ', '\t'],
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token == "*")
|
||||||
|
{
|
||||||
|
return (true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = token.AsSpan();
|
||||||
|
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
span = span[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ulong.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
||||||
|
{
|
||||||
|
parsedAddresses.Add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, parsedAddresses.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceSource(string source)
|
||||||
|
{
|
||||||
|
if (_lifetimeSourceTraceFilter.Wildcard)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.IndexOf(_lifetimeSourceTraceFilter.Sources, source) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool Wildcard, string[] Sources) ParseSourceList(string? sources)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sources))
|
||||||
|
{
|
||||||
|
return (false, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedSources = new List<string>();
|
||||||
|
foreach (var token in sources.Split(
|
||||||
|
[',', ';'],
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token == "*")
|
||||||
|
{
|
||||||
|
return (true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSources.Add(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, parsedSources.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FlushPendingFirstCpuWrite(TrackedRange range)
|
||||||
|
{
|
||||||
|
var spin = new SpinWait();
|
||||||
|
while (Volatile.Read(ref range.FirstCpuWriteSeen) == 1)
|
||||||
|
{
|
||||||
|
spin.SpinOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range.TraceLifetime || Interlocked.Exchange(ref range.PendingFirstCpuWrite, 0) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceLifetime(
|
||||||
|
range,
|
||||||
|
"first-cpu-write-disarm",
|
||||||
|
range.FirstCpuWriteAddress,
|
||||||
|
range.FirstCpuWritePage,
|
||||||
|
range.FirstCpuWriteTraceSequence,
|
||||||
|
range.FirstCpuWriteTimestampNanoseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceLifetime(
|
||||||
|
TrackedRange range,
|
||||||
|
string operation,
|
||||||
|
ulong faultAddress = 0,
|
||||||
|
ulong faultPage = 0,
|
||||||
|
long traceSequence = 0,
|
||||||
|
long timestampNanoseconds = 0)
|
||||||
|
{
|
||||||
|
if (traceSequence == 0)
|
||||||
|
{
|
||||||
|
traceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestampNanoseconds == 0)
|
||||||
|
{
|
||||||
|
timestampNanoseconds = GetMonotonicNanoseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedMilliseconds =
|
||||||
|
(timestampNanoseconds - _lifetimeTraceEpochNanoseconds) / 1_000_000.0;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[WT][LIFETIME] seq={traceSequence} t_ms={elapsedMilliseconds:F3} " +
|
||||||
|
$"event={operation} source_seq={range.SourceSequence} source='{range.Source}' " +
|
||||||
|
$"requested=0x{range.Address:X16}+0x{range.ByteCount:X} " +
|
||||||
|
$"range=0x{range.Start:X16}..0x{range.End:X16} " +
|
||||||
|
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GetMonotonicNanoseconds()
|
||||||
|
{
|
||||||
|
Timespec time;
|
||||||
|
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
||||||
|
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,14 @@ public interface IGuestThreadScheduler
|
|||||||
{
|
{
|
||||||
bool SupportsGuestContextTransfer { get; }
|
bool SupportsGuestContextTransfer { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Associates a pthread identity created on the primary guest executor
|
||||||
|
/// with its live CPU context. Primary execution does not pass through
|
||||||
|
/// TryStartThread, but kernel exception delivery must still be able to
|
||||||
|
/// target it.
|
||||||
|
/// </summary>
|
||||||
|
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||||
|
|
||||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||||
|
|
||||||
bool TryJoinThread(
|
bool TryJoinThread(
|
||||||
@@ -53,6 +61,19 @@ public interface IGuestThreadScheduler
|
|||||||
|
|
||||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a new guest scheduling priority to a live thread, mapping it
|
||||||
|
/// onto the host thread if one is running. Returns false when the thread
|
||||||
|
/// handle is unknown.
|
||||||
|
/// </summary>
|
||||||
|
bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a new affinity mask for a guest thread and re-applies it to
|
||||||
|
/// the host thread where the platform supports it.
|
||||||
|
/// </summary>
|
||||||
|
bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask);
|
||||||
|
|
||||||
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
||||||
|
|
||||||
bool TryCallGuestFunction(
|
bool TryCallGuestFunction(
|
||||||
@@ -65,11 +86,36 @@ public interface IGuestThreadScheduler
|
|||||||
string reason,
|
string reason,
|
||||||
out string? error);
|
out string? error);
|
||||||
|
|
||||||
|
bool TryCallGuestFunction(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong entryPoint,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong arg2,
|
||||||
|
ulong stackAddress,
|
||||||
|
ulong stackSize,
|
||||||
|
string reason,
|
||||||
|
out ulong returnValue,
|
||||||
|
out string? error);
|
||||||
|
|
||||||
bool TryCallGuestContinuation(
|
bool TryCallGuestContinuation(
|
||||||
CpuContext callerContext,
|
CpuContext callerContext,
|
||||||
GuestCpuContinuation continuation,
|
GuestCpuContinuation continuation,
|
||||||
string reason,
|
string reason,
|
||||||
out string? error);
|
out string? error);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously invokes an installed kernel exception handler as the
|
||||||
|
/// target guest thread. This is used by IL2CPP's stop-the-world collector:
|
||||||
|
/// the handler acknowledges suspension and may remain blocked until the
|
||||||
|
/// collecting thread resumes it.
|
||||||
|
/// </summary>
|
||||||
|
bool TryRaiseGuestException(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong threadHandle,
|
||||||
|
ulong handler,
|
||||||
|
int exceptionType,
|
||||||
|
out string? error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct GuestImportCallFrame(
|
public readonly record struct GuestImportCallFrame(
|
||||||
@@ -94,13 +140,34 @@ public readonly record struct GuestCpuContinuation(
|
|||||||
ulong Rdi,
|
ulong Rdi,
|
||||||
ulong R8,
|
ulong R8,
|
||||||
ulong R9,
|
ulong R9,
|
||||||
|
ulong R10,
|
||||||
|
ulong R11,
|
||||||
ulong R12,
|
ulong R12,
|
||||||
ulong R13,
|
ulong R13,
|
||||||
ulong R14,
|
ulong R14,
|
||||||
ulong R15);
|
ulong R15,
|
||||||
|
ushort FpuControlWord,
|
||||||
|
uint Mxcsr,
|
||||||
|
bool RestoreFullFpuState);
|
||||||
|
|
||||||
public static class GuestThreadExecution
|
public static class GuestThreadExecution
|
||||||
{
|
{
|
||||||
|
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
|
||||||
|
{
|
||||||
|
private readonly Func<int> _resume;
|
||||||
|
private readonly Func<bool> _tryWake;
|
||||||
|
|
||||||
|
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
|
||||||
|
{
|
||||||
|
_resume = resume;
|
||||||
|
_tryWake = tryWake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Resume() => _resume();
|
||||||
|
|
||||||
|
public bool TryWake() => _tryWake();
|
||||||
|
}
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static ulong _currentGuestThreadHandle;
|
private static ulong _currentGuestThreadHandle;
|
||||||
|
|
||||||
@@ -246,6 +313,23 @@ public static class GuestThreadExecution
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compatibility bridge for exports that still describe blocked work as a
|
||||||
|
// resume/wake delegate pair. New hot paths should provide an
|
||||||
|
// IGuestThreadBlockWaiter directly to avoid allocating closures.
|
||||||
|
public static bool RequestCurrentThreadBlock(
|
||||||
|
CpuContext? context,
|
||||||
|
string reason,
|
||||||
|
string? wakeKey,
|
||||||
|
Func<int> resumeHandler,
|
||||||
|
Func<bool> wakeHandler,
|
||||||
|
long blockDeadlineTimestamp = 0) =>
|
||||||
|
RequestCurrentThreadBlock(
|
||||||
|
context,
|
||||||
|
reason,
|
||||||
|
wakeKey,
|
||||||
|
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
|
||||||
|
blockDeadlineTimestamp);
|
||||||
|
|
||||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||||
{
|
{
|
||||||
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||||
@@ -314,6 +398,27 @@ public static class GuestThreadExecution
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler,
|
||||||
|
out long blockDeadlineTimestamp)
|
||||||
|
{
|
||||||
|
var consumed = TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out wakeKey,
|
||||||
|
out var waiter,
|
||||||
|
out blockDeadlineTimestamp);
|
||||||
|
resumeHandler = waiter is null ? null : waiter.Resume;
|
||||||
|
wakeHandler = waiter is null ? null : waiter.TryWake;
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||||
{
|
{
|
||||||
if (timeout <= TimeSpan.Zero)
|
if (timeout <= TimeSpan.Zero)
|
||||||
@@ -360,10 +465,15 @@ public static class GuestThreadExecution
|
|||||||
context[CpuRegister.Rdi],
|
context[CpuRegister.Rdi],
|
||||||
context[CpuRegister.R8],
|
context[CpuRegister.R8],
|
||||||
context[CpuRegister.R9],
|
context[CpuRegister.R9],
|
||||||
|
context[CpuRegister.R10],
|
||||||
|
context[CpuRegister.R11],
|
||||||
context[CpuRegister.R12],
|
context[CpuRegister.R12],
|
||||||
context[CpuRegister.R13],
|
context[CpuRegister.R13],
|
||||||
context[CpuRegister.R14],
|
context[CpuRegister.R14],
|
||||||
context[CpuRegister.R15]);
|
context[CpuRegister.R15],
|
||||||
|
context.FpuControlWord,
|
||||||
|
context.Mxcsr,
|
||||||
|
RestoreFullFpuState: false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-wide registry of ELF PT_TLS templates and per-thread dynamic thread
|
||||||
|
/// vectors. AMD64 uses TLS Variant II: startup/static module blocks precede the
|
||||||
|
/// thread pointer, while modules introduced after a thread was initialized are
|
||||||
|
/// represented by separately allocated DTV entries.
|
||||||
|
/// </summary>
|
||||||
|
public static class GuestTlsTemplate
|
||||||
|
{
|
||||||
|
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
|
||||||
|
// modules can require more than one host page of Variant II static TLS;
|
||||||
|
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
|
||||||
|
public const ulong StartupStaticTlsReservation = 0x10000UL;
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
|
||||||
|
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
|
||||||
|
private static ulong _staticTlsSize;
|
||||||
|
private static ulong _maximumAlignment = 1;
|
||||||
|
private static ulong _generation;
|
||||||
|
|
||||||
|
static GuestTlsTemplate()
|
||||||
|
{
|
||||||
|
RunTlsLayoutSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ModuleTemplate
|
||||||
|
{
|
||||||
|
public required ulong ModuleId { get; init; }
|
||||||
|
public required byte[] InitImage { get; init; }
|
||||||
|
public required ulong MemorySize { get; init; }
|
||||||
|
public required ulong Alignment { get; init; }
|
||||||
|
public required ulong AlignmentBias { get; init; }
|
||||||
|
public required ulong StaticOffset { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThreadDtv
|
||||||
|
{
|
||||||
|
public ulong Generation { get; set; }
|
||||||
|
public Dictionary<ulong, DtvEntry> Entries { get; } = new();
|
||||||
|
public nint DtvAllocationBase { get; set; }
|
||||||
|
public ulong DtvAddress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DtvEntry
|
||||||
|
{
|
||||||
|
public required ulong Address { get; init; }
|
||||||
|
public nint AllocationBase { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main executable's initialized PT_TLS bytes.</summary>
|
||||||
|
public static byte[] InitImage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? (byte[])module.InitImage.Clone()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Aligned size of the main executable's static TLS block.</summary>
|
||||||
|
public static ulong BlockSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? module.StaticOffset
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main executable's PT_TLS alignment.</summary>
|
||||||
|
public static ulong Alignment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _modules.TryGetValue(1, out var module)
|
||||||
|
? module.Alignment
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Total Variant II static TLS span required below the TCB.</summary>
|
||||||
|
public static ulong StaticTlsSize
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _staticTlsSize; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Largest PT_TLS alignment required by the registered modules.</summary>
|
||||||
|
public static ulong MaximumAlignment
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _maximumAlignment; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Current DTV generation, incremented for every registered module.</summary>
|
||||||
|
public static ulong Generation
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _generation; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backwards-compatible main-module registration entry point.
|
||||||
|
/// </summary>
|
||||||
|
public static void Set(ReadOnlySpan<byte> initImage, ulong memorySize, ulong alignment)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
RegisterModule(1, initImage, memorySize, alignment, alignmentBias: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers one module's PT_TLS template and allocates its Variant II
|
||||||
|
/// static offset. Modules must be registered in loader module-ID order.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong RegisterModule(
|
||||||
|
ulong moduleId,
|
||||||
|
ReadOnlySpan<byte> initImage,
|
||||||
|
ulong memorySize,
|
||||||
|
ulong alignment,
|
||||||
|
ulong alignmentBias = 0)
|
||||||
|
{
|
||||||
|
if (moduleId == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(moduleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedAlignment = alignment == 0 ? 1 : alignment;
|
||||||
|
if ((normalizedAlignment & (normalizedAlignment - 1)) != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"PT_TLS alignment 0x{alignment:X} is not a power of two.");
|
||||||
|
}
|
||||||
|
if (memorySize > int.MaxValue || (ulong)initImage.Length > memorySize)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("PT_TLS template size is invalid or exceeds the supported process limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_modules.TryGetValue(moduleId, out var existing))
|
||||||
|
{
|
||||||
|
if (existing.MemorySize != memorySize ||
|
||||||
|
existing.Alignment != normalizedAlignment ||
|
||||||
|
existing.AlignmentBias != alignmentBias ||
|
||||||
|
!existing.InitImage.AsSpan().SequenceEqual(initImage))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"TLS module {moduleId} was registered with a different template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.StaticOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeBSD/AMD64 Variant II: choose the smallest offset satisfying
|
||||||
|
// offset - previous >= size and (-offset) % align == p_vaddr % align.
|
||||||
|
var staticOffset = CalculateStaticOffset(
|
||||||
|
_staticTlsSize,
|
||||||
|
memorySize,
|
||||||
|
normalizedAlignment,
|
||||||
|
alignmentBias);
|
||||||
|
if (staticOffset > StartupStaticTlsReservation)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Static TLS requires 0x{staticOffset:X} bytes, but startup maps only " +
|
||||||
|
$"0x{StartupStaticTlsReservation:X} bytes below the thread pointer.");
|
||||||
|
}
|
||||||
|
_modules.Add(moduleId, new ModuleTemplate
|
||||||
|
{
|
||||||
|
ModuleId = moduleId,
|
||||||
|
InitImage = initImage.ToArray(),
|
||||||
|
MemorySize = memorySize,
|
||||||
|
Alignment = normalizedAlignment,
|
||||||
|
AlignmentBias = alignmentBias,
|
||||||
|
StaticOffset = staticOffset,
|
||||||
|
});
|
||||||
|
_staticTlsSize = staticOffset;
|
||||||
|
_maximumAlignment = Math.Max(_maximumAlignment, normalizedAlignment);
|
||||||
|
_generation++;
|
||||||
|
return staticOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the static TP-relative block offset for a module.</summary>
|
||||||
|
public static bool TryGetStaticOffset(ulong moduleId, out ulong staticOffset)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_modules.TryGetValue(moduleId, out var module))
|
||||||
|
{
|
||||||
|
staticOffset = module.StaticOffset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticOffset = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Clears all module templates and frees dynamic DTV blocks.</summary>
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var dtv in _threadDtvs.Values)
|
||||||
|
{
|
||||||
|
FreeDynamicEntries(dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadDtvs.Clear();
|
||||||
|
_modules.Clear();
|
||||||
|
_staticTlsSize = 0;
|
||||||
|
_maximumAlignment = 1;
|
||||||
|
_generation++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes every currently registered startup TLS block for a thread
|
||||||
|
/// and records their addresses in that thread's DTV.
|
||||||
|
/// </summary>
|
||||||
|
public static void SeedThreadBlock(CpuContext context, ulong threadPointer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
if (threadPointer == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_threadDtvs.Remove(threadPointer, out var oldDtv))
|
||||||
|
{
|
||||||
|
FreeDynamicEntries(oldDtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dtv = new ThreadDtv();
|
||||||
|
_threadDtvs.Add(threadPointer, dtv);
|
||||||
|
foreach (var module in _modules.Values)
|
||||||
|
{
|
||||||
|
dtv.Entries[module.ModuleId] = CreateStaticOrDynamicEntry(context, threadPointer, module);
|
||||||
|
}
|
||||||
|
RebuildGuestDtv(context, threadPointer, dtv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the DTV lookup used by <c>__tls_get_addr</c>. Unknown module
|
||||||
|
/// IDs and offsets outside the module's TLS image are rejected with zero.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong ResolveAddress(CpuContext context, ulong moduleId, ulong offset)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
if (context.FsBase == 0 || moduleId == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modules.TryGetValue(moduleId, out var module) || offset >= module.MemorySize)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_threadDtvs.TryGetValue(context.FsBase, out var dtv))
|
||||||
|
{
|
||||||
|
dtv = new ThreadDtv();
|
||||||
|
_threadDtvs.Add(context.FsBase, dtv);
|
||||||
|
foreach (var startupModule in _modules.Values)
|
||||||
|
{
|
||||||
|
dtv.Entries[startupModule.ModuleId] = CreateStaticOrDynamicEntry(context, context.FsBase, startupModule);
|
||||||
|
}
|
||||||
|
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryAdded = false;
|
||||||
|
if (!dtv.Entries.TryGetValue(moduleId, out var entry))
|
||||||
|
{
|
||||||
|
// This module appeared after the thread's startup TLS layout was
|
||||||
|
// seeded. Model rtld's lazy DTV allocation instead of assuming
|
||||||
|
// unused space exists below the already-live thread pointer.
|
||||||
|
entry = CreateDynamicEntry(module);
|
||||||
|
dtv.Entries.Add(moduleId, entry);
|
||||||
|
entryAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryAdded || dtv.Generation != _generation)
|
||||||
|
{
|
||||||
|
RebuildGuestDtv(context, context.FsBase, dtv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return checked(entry.Address + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DtvEntry CreateStaticOrDynamicEntry(
|
||||||
|
CpuContext context,
|
||||||
|
ulong threadPointer,
|
||||||
|
ModuleTemplate module)
|
||||||
|
{
|
||||||
|
if (threadPointer >= module.StaticOffset)
|
||||||
|
{
|
||||||
|
var address = threadPointer - module.StaticOffset;
|
||||||
|
var zeroImage = module.MemorySize == 0 ? [] : new byte[(int)module.MemorySize];
|
||||||
|
if ((zeroImage.Length == 0 || context.Memory.TryWrite(address, zeroImage)) &&
|
||||||
|
(module.InitImage.Length == 0 || context.Memory.TryWrite(address, module.InitImage)))
|
||||||
|
{
|
||||||
|
return new DtvEntry { Address = address };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateDynamicEntry(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DtvEntry CreateDynamicEntry(ModuleTemplate module)
|
||||||
|
{
|
||||||
|
var size = Math.Max(1UL, module.MemorySize);
|
||||||
|
var allocationSize = checked(size + module.Alignment - 1);
|
||||||
|
if (allocationSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("TLS module allocation exceeds the supported host allocation size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationBase = Marshal.AllocHGlobal((int)allocationSize);
|
||||||
|
var alignedAddress = AlignUp(unchecked((ulong)allocationBase), module.Alignment);
|
||||||
|
Marshal.Copy(new byte[(int)size], 0, unchecked((nint)alignedAddress), (int)size);
|
||||||
|
if (module.InitImage.Length != 0)
|
||||||
|
{
|
||||||
|
Marshal.Copy(module.InitImage, 0, unchecked((nint)alignedAddress), module.InitImage.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DtvEntry
|
||||||
|
{
|
||||||
|
Address = alignedAddress,
|
||||||
|
AllocationBase = allocationBase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FreeDynamicEntries(ThreadDtv dtv)
|
||||||
|
{
|
||||||
|
foreach (var entry in dtv.Entries.Values)
|
||||||
|
{
|
||||||
|
if (entry.AllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(entry.AllocationBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dtv.DtvAllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||||
|
dtv.DtvAllocationBase = 0;
|
||||||
|
dtv.DtvAddress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildGuestDtv(CpuContext context, ulong threadPointer, ThreadDtv dtv)
|
||||||
|
{
|
||||||
|
var maximumModuleId = _modules.Count == 0 ? 0UL : _modules.Keys.Max();
|
||||||
|
var byteSize = checked(2UL * sizeof(ulong) + maximumModuleId * sizeof(ulong));
|
||||||
|
if (byteSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Guest DTV exceeds the supported host allocation size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAllocation = Marshal.AllocHGlobal((int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||||
|
var newAddress = unchecked((ulong)newAllocation);
|
||||||
|
Marshal.Copy(new byte[(int)Math.Max((ulong)sizeof(ulong) * 2, byteSize)], 0, newAllocation, (int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
|
||||||
|
Marshal.WriteInt64(newAllocation, 0, unchecked((long)_generation));
|
||||||
|
Marshal.WriteInt64(newAllocation, sizeof(ulong), unchecked((long)maximumModuleId));
|
||||||
|
foreach (var (moduleId, entry) in dtv.Entries)
|
||||||
|
{
|
||||||
|
var slotOffset = checked((int)(2UL * sizeof(ulong) + (moduleId - 1) * sizeof(ulong)));
|
||||||
|
Marshal.WriteInt64(newAllocation, slotOffset, unchecked((long)entry.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.TryWriteUInt64(threadPointer + sizeof(ulong), newAddress))
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(newAllocation);
|
||||||
|
throw new InvalidOperationException("Failed to install the guest DTV pointer in the thread control block.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dtv.DtvAllocationBase != 0)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
|
||||||
|
}
|
||||||
|
dtv.DtvAllocationBase = newAllocation;
|
||||||
|
dtv.DtvAddress = newAddress;
|
||||||
|
dtv.Generation = _generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
{
|
||||||
|
var mask = alignment - 1;
|
||||||
|
return checked((value + mask) & ~mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong CalculateStaticOffset(
|
||||||
|
ulong previousOffset,
|
||||||
|
ulong size,
|
||||||
|
ulong alignment,
|
||||||
|
ulong alignmentBias)
|
||||||
|
{
|
||||||
|
var result = checked(previousOffset + size + alignment - 1);
|
||||||
|
return result - ((result + alignmentBias) & (alignment - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunTlsLayoutSelfChecks()
|
||||||
|
{
|
||||||
|
var firstOffset = RegisterModule(1, [0x11, 0x22], 0x20, 0x10, 0);
|
||||||
|
var secondOffset = RegisterModule(2, [0x7A], 0x18, 0x20, 8);
|
||||||
|
Debug.Assert(firstOffset == 0x20, "First Variant II TLS offset is incorrect.");
|
||||||
|
Debug.Assert(secondOffset >= firstOffset + 0x18, "TLS modules overlap in the static layout.");
|
||||||
|
Debug.Assert((unchecked(0UL - secondOffset) & 0x1F) == 8, "PT_TLS virtual-address congruence was not preserved.");
|
||||||
|
|
||||||
|
var lateEntry = CreateDynamicEntry(_modules[2]);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Debug.Assert(lateEntry.AllocationBase != 0, "A late TLS module did not receive a dynamic DTV block.");
|
||||||
|
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address)) == 0x7A, "Late-module tdata was not initialized.");
|
||||||
|
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address + 1)) == 0, "Late-module tbss was not zero initialized.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(lateEntry.AllocationBase);
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ public static class HleDataSymbols
|
|||||||
private const string LibcNeedFlagNid = "P330P3dFF68";
|
private const string LibcNeedFlagNid = "P330P3dFF68";
|
||||||
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
|
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
|
||||||
private const int ProgNameMaxBytes = 511;
|
private const int ProgNameMaxBytes = 511;
|
||||||
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBABEUL;
|
// Terminator canaries reserve the low byte as NUL. Keep the process data
|
||||||
|
// symbol and every per-thread TLS copy byte-for-byte identical.
|
||||||
|
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBA00UL;
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ using System.Collections.Concurrent;
|
|||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs work on the real process main thread. GLFW windowing must live on
|
/// Runs work on the real process main thread. macOS only allows AppKit (and
|
||||||
/// that thread on macOS (AppKit) and Linux (X11's single event queue), so the
|
/// therefore GLFW windowing) on that thread, so the CLI moves emulation onto
|
||||||
/// CLI moves emulation onto a worker thread, parks the main thread in
|
/// a worker thread, parks the main thread in <see cref="Pump"/>, and the
|
||||||
/// <see cref="Pump"/>, and the video presenter posts its window loop here. On
|
/// video presenter posts its window loop here. On other platforms
|
||||||
/// Windows <see cref="IsAvailable"/> stays false and the window keeps its own
|
/// <see cref="IsAvailable"/> stays false and nothing changes.
|
||||||
/// thread.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostMainThread
|
public static class HostMainThread
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cross-platform host virtual memory API with Win32 semantics.
|
||||||
|
/// On Windows this forwards directly to kernel32. On POSIX systems it is
|
||||||
|
/// implemented over mmap/mprotect/munmap with a shadow region table that
|
||||||
|
/// answers VirtualQuery-style questions and tracks page protections.
|
||||||
|
/// POSIX anonymous mappings are demand-paged by the kernel, so Win32
|
||||||
|
/// "reserve-only" regions are mapped as committed memory directly and
|
||||||
|
/// commit requests become protection changes.
|
||||||
|
/// </summary>
|
||||||
|
public static unsafe class HostMemory
|
||||||
|
{
|
||||||
|
public const uint MEM_COMMIT = 0x1000;
|
||||||
|
public const uint MEM_RESERVE = 0x2000;
|
||||||
|
public const uint MEM_RELEASE = 0x8000;
|
||||||
|
public const uint MEM_FREE_STATE = 0x10000;
|
||||||
|
public const uint MEM_PRIVATE = 0x20000;
|
||||||
|
|
||||||
|
public const uint PAGE_NOACCESS = 0x01;
|
||||||
|
public const uint PAGE_READONLY = 0x02;
|
||||||
|
public const uint PAGE_READWRITE = 0x04;
|
||||||
|
public const uint PAGE_EXECUTE = 0x10;
|
||||||
|
public const uint PAGE_EXECUTE_READ = 0x20;
|
||||||
|
public const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||||
|
|
||||||
|
private const ulong PageSize = 0x1000;
|
||||||
|
|
||||||
|
/// <summary>Win32 MEMORY_BASIC_INFORMATION (64-bit) layout.</summary>
|
||||||
|
public struct BasicInfo
|
||||||
|
{
|
||||||
|
public ulong BaseAddress;
|
||||||
|
public ulong AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public uint Alignment1;
|
||||||
|
public ulong RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
public uint Alignment2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualAlloc(address, size, allocationType, protect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Alloc(address, size, allocationType, protect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Free(void* address, nuint size, uint freeType)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualFree(address, size, freeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Free(address, size, freeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualProtect(address, size, newProtect, out oldProtect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Protect(address, size, newProtect, out oldProtect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nuint Query(void* address, out BasicInfo info)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Win32VirtualQuery(address, out info, (nuint)sizeof(BasicInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Posix.Query(address, out info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FlushInstructionCache(void* address, nuint size)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
Win32FlushInstructionCache(Win32GetCurrentProcess(), address, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The emulator only executes x86-64 guest code, so a non-Windows host
|
||||||
|
// is either x86-64 (including Rosetta 2 translation) with a coherent
|
||||||
|
// instruction cache, or would need sys_icache_invalidate for a future
|
||||||
|
// arm64 recompiler. Nothing to do today.
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualAlloc", SetLastError = true)]
|
||||||
|
private static extern void* Win32VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualFree", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualProtect", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "VirtualQuery")]
|
||||||
|
private static extern nuint Win32VirtualQuery(void* lpAddress, out BasicInfo lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess")]
|
||||||
|
private static extern void* Win32GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "FlushInstructionCache")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool Win32FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
|
private static class Posix
|
||||||
|
{
|
||||||
|
private const int PROT_NONE = 0x0;
|
||||||
|
private const int PROT_READ = 0x1;
|
||||||
|
private const int PROT_WRITE = 0x2;
|
||||||
|
private const int PROT_EXEC = 0x4;
|
||||||
|
|
||||||
|
private const int MAP_PRIVATE = 0x02;
|
||||||
|
private const int MAP_FIXED = 0x10;
|
||||||
|
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
|
||||||
|
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
|
||||||
|
|
||||||
|
// Linux-only: fail instead of clobbering an existing mapping.
|
||||||
|
private const int MAP_FIXED_NOREPLACE = 0x100000;
|
||||||
|
|
||||||
|
private static readonly nint MAP_FAILED = -1;
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly SortedList<ulong, Region> Regions = new();
|
||||||
|
|
||||||
|
private sealed class Region
|
||||||
|
{
|
||||||
|
public ulong Base;
|
||||||
|
public ulong Size;
|
||||||
|
public uint DefaultProtect;
|
||||||
|
public Dictionary<ulong, uint>? PageProtects;
|
||||||
|
|
||||||
|
public ulong End => Base + Size;
|
||||||
|
|
||||||
|
public uint ProtectAt(ulong pageAddress)
|
||||||
|
{
|
||||||
|
if (PageProtects is not null && PageProtects.TryGetValue(pageAddress, out var overriden))
|
||||||
|
{
|
||||||
|
return overriden;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultProtect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
|
||||||
|
{
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedSize = AlignUp((ulong)size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (allocationType == MEM_COMMIT && address != null &&
|
||||||
|
TryFindRegionLocked((ulong)address, out var existing))
|
||||||
|
{
|
||||||
|
// Note: MEM_RESERVE requests that overlap an existing
|
||||||
|
// region must fail like Win32 does; only a pure commit
|
||||||
|
// may target pages inside a tracked mapping.
|
||||||
|
// Commit inside an existing mapping: the pages are already
|
||||||
|
// backed (demand paged), so only apply the protection.
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = Math.Min(existing.End, AlignUp((ulong)address + alignedSize, PageSize));
|
||||||
|
if (end <= start)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(protect)) != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(existing, start, end - start, protect);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((allocationType & MEM_RESERVE) == 0)
|
||||||
|
{
|
||||||
|
// MEM_COMMIT alone outside any known region is invalid here.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var posixProtect = ToPosixProtect(protect);
|
||||||
|
var flags = MAP_PRIVATE | MAP_ANON;
|
||||||
|
if ((allocationType & MEM_COMMIT) == 0)
|
||||||
|
{
|
||||||
|
// Reserve-only: keep the requested protection so the region
|
||||||
|
// is usable without a separate commit step, but tell the
|
||||||
|
// kernel not to account swap for it where supported.
|
||||||
|
flags |= MAP_NORESERVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
nint result;
|
||||||
|
if (address != null)
|
||||||
|
{
|
||||||
|
// Win32 maps at exactly the requested address or fails
|
||||||
|
// without touching existing mappings. Fail up front on
|
||||||
|
// any overlap we track, then place the mapping: Linux
|
||||||
|
// gets MAP_FIXED_NOREPLACE (fails cleanly on host
|
||||||
|
// mappings too). Darwin lacks NOREPLACE and plain
|
||||||
|
// MAP_FIXED would silently clobber untracked host
|
||||||
|
// memory (dyld, the runtime's JIT heap, Rosetta), so
|
||||||
|
// pass the address as a hint instead -- the kernel
|
||||||
|
// honors it when the range is free and relocates the
|
||||||
|
// mapping otherwise, which we treat as failure.
|
||||||
|
if (OverlapsTrackedRegionLocked((ulong)address, alignedSize))
|
||||||
|
{
|
||||||
|
Trace($"exact overlap: addr=0x{(ulong)address:X16} size=0x{alignedSize:X}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
|
||||||
|
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
|
||||||
|
if (result == MAP_FAILED || (ulong)result != (ulong)address)
|
||||||
|
{
|
||||||
|
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
if (result != MAP_FAILED)
|
||||||
|
{
|
||||||
|
munmap(result, (nuint)alignedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = mmap(0, (nuint)alignedSize, posixProtect, flags, -1, 0);
|
||||||
|
if (result == MAP_FAILED)
|
||||||
|
{
|
||||||
|
Trace($"mmap failed: size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions[(ulong)result] = new Region
|
||||||
|
{
|
||||||
|
Base = (ulong)result,
|
||||||
|
Size = alignedSize,
|
||||||
|
DefaultProtect = protect
|
||||||
|
};
|
||||||
|
|
||||||
|
return (void*)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Free(void* address, nuint size, uint freeType)
|
||||||
|
{
|
||||||
|
_ = size;
|
||||||
|
_ = freeType;
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!Regions.TryGetValue((ulong)address, out var region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Regions.Remove((ulong)address);
|
||||||
|
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
|
||||||
|
{
|
||||||
|
oldProtect = PAGE_NOACCESS;
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = AlignDown((ulong)address, PageSize);
|
||||||
|
var end = AlignUp((ulong)address + size, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (!TryFindRegionLocked(start, out var region) || end > region.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldProtect = region.ProtectAt(start);
|
||||||
|
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(newProtect)) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetProtectRangeLocked(region, start, end - start, newProtect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static nuint Query(void* address, out BasicInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (TryFindRegionLocked(pageAddress, out var region))
|
||||||
|
{
|
||||||
|
// Win32 VirtualQuery reports a run of pages sharing the
|
||||||
|
// same protection, so stop the run where it changes. Do
|
||||||
|
// not walk the whole mapping page-by-page here: PS5 GPU
|
||||||
|
// apertures can span hundreds of GiB, while protection
|
||||||
|
// overrides are sparse. A tiny read inside such a mapping
|
||||||
|
// otherwise performs tens of millions of dictionary
|
||||||
|
// lookups before it can return.
|
||||||
|
var pageProtects = region.PageProtects;
|
||||||
|
uint protect;
|
||||||
|
ulong runEnd;
|
||||||
|
if (pageProtects is null || pageProtects.Count == 0)
|
||||||
|
{
|
||||||
|
protect = region.DefaultProtect;
|
||||||
|
runEnd = region.End;
|
||||||
|
}
|
||||||
|
else if (pageProtects.TryGetValue(pageAddress, out protect))
|
||||||
|
{
|
||||||
|
runEnd = pageAddress + PageSize;
|
||||||
|
while (runEnd < region.End &&
|
||||||
|
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||||
|
nextProtect == protect)
|
||||||
|
{
|
||||||
|
runEnd += PageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protect = region.DefaultProtect;
|
||||||
|
runEnd = region.End;
|
||||||
|
foreach (var overrideAddress in pageProtects.Keys)
|
||||||
|
{
|
||||||
|
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||||
|
{
|
||||||
|
runEnd = overrideAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = region.Base;
|
||||||
|
info.AllocationProtect = region.DefaultProtect;
|
||||||
|
info.RegionSize = runEnd - pageAddress;
|
||||||
|
info.State = MEM_COMMIT;
|
||||||
|
info.Protect = protect;
|
||||||
|
info.Type = MEM_PRIVATE;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untracked host memory (runtime heaps, stacks, libraries) is
|
||||||
|
// reported as a free block reaching to the next tracked region
|
||||||
|
// so scanning callers keep advancing.
|
||||||
|
var nextBase = ulong.MaxValue;
|
||||||
|
foreach (var regionBase in Regions.Keys)
|
||||||
|
{
|
||||||
|
if (regionBase > pageAddress)
|
||||||
|
{
|
||||||
|
nextBase = regionBase;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseAddress = pageAddress;
|
||||||
|
info.AllocationBase = 0;
|
||||||
|
info.AllocationProtect = PAGE_NOACCESS;
|
||||||
|
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
|
||||||
|
info.State = MEM_FREE_STATE;
|
||||||
|
info.Protect = PAGE_NOACCESS;
|
||||||
|
info.Type = 0;
|
||||||
|
return (nuint)sizeof(BasicInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OverlapsTrackedRegionLocked(ulong start, ulong size)
|
||||||
|
{
|
||||||
|
var end = start + size;
|
||||||
|
foreach (var region in Regions.Values)
|
||||||
|
{
|
||||||
|
if (region.Base < end && start < region.End)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||||
|
{
|
||||||
|
region = null!;
|
||||||
|
var keys = Regions.Keys;
|
||||||
|
var low = 0;
|
||||||
|
var high = keys.Count - 1;
|
||||||
|
Region? candidate = null;
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
var middle = low + ((high - low) >> 1);
|
||||||
|
var entry = Regions.Values[middle];
|
||||||
|
if (entry.Base <= address)
|
||||||
|
{
|
||||||
|
candidate = entry;
|
||||||
|
low = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate is null || address >= candidate.End)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||||
|
{
|
||||||
|
if (start == region.Base && size >= region.Size)
|
||||||
|
{
|
||||||
|
region.DefaultProtect = protect;
|
||||||
|
region.PageProtects = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||||
|
var end = start + size;
|
||||||
|
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||||
|
{
|
||||||
|
if (protect == region.DefaultProtect)
|
||||||
|
{
|
||||||
|
region.PageProtects.Remove(pageAddress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
region.PageProtects[pageAddress] = protect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToPosixProtect(uint win32Protect)
|
||||||
|
{
|
||||||
|
return win32Protect switch
|
||||||
|
{
|
||||||
|
PAGE_NOACCESS => PROT_NONE,
|
||||||
|
PAGE_READONLY => PROT_READ,
|
||||||
|
PAGE_READWRITE => PROT_READ | PROT_WRITE,
|
||||||
|
PAGE_EXECUTE => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READ => PROT_READ | PROT_EXEC,
|
||||||
|
PAGE_EXECUTE_READWRITE => PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||||
|
_ => PROT_READ | PROT_WRITE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[HOSTMEM] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
|
||||||
|
|
||||||
|
private static ulong AlignUp(ulong value, ulong alignment) => checked((value + alignment - 1) & ~(alignment - 1));
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int munmap(nint addr, nuint length);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int mprotect(nint addr, nuint length, int prot);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6010
-1430
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
public static partial class AgcExports
|
||||||
|
{
|
||||||
|
// Exact Gen5 primary register defaults originally reverse-engineered by Kyty (MIT).
|
||||||
|
// Hashes, offsets, and values match the tables consumed by the PS5 AGC SDK.
|
||||||
|
private static RegisterDefaultGroup[] CreatePrimaryRegisterDefaults() =>
|
||||||
|
[
|
||||||
|
// context register groups
|
||||||
|
new(0u, 0u, 0xE24F806Du, [new(0x202u, 0x00CC0010u)]), // CB_COLOR_CONTROL
|
||||||
|
new(0u, 1u, 0xF6C28182u, [new(0x109u, 0x00000000u)]), // CB_DCC_CONTROL
|
||||||
|
new(0u, 2u, 0x6F6E55A5u, [new(0x104u, 0x00000000u)]), // CB_RMI_GL2_CACHE_CONTROL
|
||||||
|
new(0u, 3u, 0x0BC65DA4u, [new(0x08Fu, 0x00000000u)]), // CB_SHADER_MASK
|
||||||
|
new(0u, 4u, 0x9E5AD592u, [new(0x08Eu, 0x0000000Fu)]), // CB_TARGET_MASK
|
||||||
|
new(0u, 5u, 0xBB513B98u, [new(0x2DCu, 0x0000AA00u)]), // DB_ALPHA_TO_MASK
|
||||||
|
new(0u, 6u, 0xAB64B23Bu, [new(0x001u, 0x00000000u)]), // DB_COUNT_CONTROL
|
||||||
|
new(0u, 7u, 0x53C39964u, [new(0x200u, 0x00000000u)]), // DB_DEPTH_CONTROL
|
||||||
|
new(0u, 8u, 0x01396B11u, [new(0x201u, 0x00000000u)]), // DB_EQAA
|
||||||
|
new(0u, 9u, 0x7D42019Au, [new(0x000u, 0x00000000u)]), // DB_RENDER_CONTROL
|
||||||
|
new(0u, 10u, 0x3548F523u, [new(0x006u, 0x00000000u)]), // PS_SHADER_SAMPLE_EXCLUSION_MASK
|
||||||
|
new(0u, 11u, 0xF43AD28Au, [new(0x01Fu, 0x00000000u)]), // DB_RMI_L2_CACHE_CONTROL
|
||||||
|
new(0u, 12u, 0x6DE4C312u, [new(0x203u, 0x00000000u)]), // DB_SHADER_CONTROL
|
||||||
|
new(0u, 13u, 0x00A77AE0u, [new(0x2B0u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE0
|
||||||
|
new(0u, 14u, 0x00A779B7u, [new(0x2B1u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE1
|
||||||
|
new(0u, 15u, 0x5100100Cu, [new(0x10Cu, 0x00000000u)]), // DB_STENCILREFMASK
|
||||||
|
new(0u, 16u, 0x59958BBAu, [new(0x10Du, 0x00000000u)]), // DB_STENCILREFMASK_BF
|
||||||
|
new(0u, 17u, 0x0C06F17Cu, [new(0x10Bu, 0x00000000u)]), // DB_STENCIL_CONTROL
|
||||||
|
new(0u, 18u, 0x6F104B72u, [new(0x1FFu, 0x00000000u)]), // GE_MAX_OUTPUT_PER_SUBGROUP
|
||||||
|
new(0u, 19u, 0x25C70D9Cu, [new(0x204u, 0x00000000u)]), // PA_CL_CLIP_CNTL
|
||||||
|
new(0u, 20u, 0x3881201Eu, [new(0x20Du, 0x00000000u)]), // PA_CL_OBJPRIM_ID_CNTL
|
||||||
|
new(0u, 21u, 0x09AFDDAFu, [new(0x206u, 0x0000043Fu)]), // PA_CL_VTE_CNTL
|
||||||
|
new(0u, 22u, 0x367D63CFu, [new(0x2F8u, 0x00000000u)]), // PA_SC_AA_CONFIG
|
||||||
|
new(0u, 23u, 0x43707DB8u, [new(0x083u, 0x0000FFFFu)]), // PA_SC_CLIPRECT_RULE
|
||||||
|
new(0u, 24u, 0xF6AE26BAu, [new(0x313u, 0x00000000u)]), // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL
|
||||||
|
new(0u, 25u, 0x1B917652u, [new(0x800003FEu, 0x00000000u)]), // PA_SC_FSR_ENABLE
|
||||||
|
new(0u, 26u, 0x94B1E4F7u, [new(0x0EAu, 0x00000000u)]), // PA_SC_HORIZ_GRID
|
||||||
|
new(0u, 27u, 0xE3661B6Cu, [new(0x0E9u, 0x00000000u)]), // PA_SC_LEFT_VERT_GRID
|
||||||
|
new(0u, 28u, 0x1EB8D73Au, [new(0x292u, 0x00000002u)]), // PA_SC_MODE_CNTL_0
|
||||||
|
new(0u, 29u, 0x15051FA3u, [new(0x293u, 0x00000000u)]), // PA_SC_MODE_CNTL_1
|
||||||
|
new(0u, 30u, 0x9C51A7F1u, [new(0x0E8u, 0x00000000u)]), // PA_SC_RIGHT_VERT_GRID
|
||||||
|
new(0u, 31u, 0xA20EFC70u, [new(0x080u, 0x00000000u)]), // PA_SC_WINDOW_OFFSET
|
||||||
|
new(0u, 32u, 0x0EC09F6Eu, [new(0x211u, 0x00000000u)]), // PA_STATE_STEREO_X
|
||||||
|
new(0u, 33u, 0x34A7D6D3u, [new(0x210u, 0x00000000u)]), // PA_STEREO_CNTL
|
||||||
|
new(0u, 34u, 0xCE831B94u, [new(0x08Du, 0x00000000u)]), // PA_SU_HARDWARE_SCREEN_OFFSET
|
||||||
|
new(0u, 35u, 0x5CC72A74u, [new(0x282u, 0x00000008u)]), // PA_SU_LINE_CNTL
|
||||||
|
new(0u, 36u, 0x3B77713Cu, [new(0x281u, 0xFFFF0000u)]), // PA_SU_POINT_MINMAX
|
||||||
|
new(0u, 37u, 0x40F64410u, [new(0x280u, 0x00080008u)]), // PA_SU_POINT_SIZE
|
||||||
|
new(0u, 38u, 0x69441268u, [new(0x2DFu, 0x00000000u)]), // PA_SU_POLY_OFFSET_CLAMP
|
||||||
|
new(0u, 39u, 0x2E418B83u, [new(0x2DEu, 0x000001E9u)]), // PA_SU_POLY_OFFSET_DB_FMT_CNTL
|
||||||
|
new(0u, 40u, 0xA00D0C8Du, [new(0x205u, 0x00000240u)]), // PA_SU_SC_MODE_CNTL
|
||||||
|
new(0u, 41u, 0xB1289FB3u, [new(0x20Cu, 0x00000001u)]), // PA_SU_SMALL_PRIM_FILTER_CNTL
|
||||||
|
new(0u, 42u, 0x144832FBu, [new(0x2F9u, 0x0000002Du)]), // PA_SU_VTX_CNTL
|
||||||
|
new(0u, 43u, 0x9890D9FAu, [new(0x1BAu, 0x00000000u)]), // SPI_TMPRING_SIZE
|
||||||
|
new(0u, 44u, 0x9016FAF1u, [new(0x2A6u, 0x00000000u)]), // VGT_DRAW_PAYLOAD_CNTL
|
||||||
|
new(0u, 45u, 0x4B73CE27u, [new(0x2CEu, 0x00000400u)]), // VGT_GS_MAX_VERT_OUT
|
||||||
|
new(0u, 46u, 0x5F5A3E7Bu, [new(0x29Bu, 0x00000002u)]), // VGT_GS_OUT_PRIM_TYPE
|
||||||
|
new(0u, 47u, 0xD4AF3A51u, [new(0x2D6u, 0x00000000u)]), // VGT_LS_HS_CONFIG
|
||||||
|
new(0u, 48u, 0x6CF4F543u, [new(0x2A3u, 0xFFFFFFFFu)]), // VGT_PRIMITIVEID_RESET
|
||||||
|
new(0u, 49u, 0x5FB86CCBu, [new(0x2A1u, 0x00000000u)]), // VGT_PRIMITIVEID_EN
|
||||||
|
new(0u, 50u, 0xEDEFA188u, [new(0x2ADu, 0x00000000u)]), // VGT_REUSE_OFF
|
||||||
|
new(0u, 51u, 0xD0DE9EE6u, [new(0x2D5u, 0x00000000u)]), // VGT_SHADER_STAGES_EN
|
||||||
|
new(0u, 52u, 0xC5831803u, [new(0x2D4u, 0x88101000u)]), // VGT_TESS_DISTRIBUTION
|
||||||
|
new(0u, 53u, 0x8E6DE84Bu, [new(0x2DBu, 0x00000000u)]), // VGT_TF_PARAM
|
||||||
|
new(0u, 54u, 0xD0771662u, [new(0x2F5u, 0x00000000u), new(0x2F6u, 0x00000000u)]), // PA_SC_CENTROID_PRIORITY_0, PA_SC_CENTROID_PRIORITY_1
|
||||||
|
new(0u, 55u, 0x569F7444u, [new(0x2FEu, 0x00000000u)]), // PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_0
|
||||||
|
new(0u, 56u, 0x5C6637CDu, [new(0x30Eu, 0xFFFFFFFFu), new(0x30Fu, 0xFFFFFFFFu)]), // PA_SC_AA_MASK_X0Y0_X1Y0, PA_SC_AA_MASK_X0Y1_X1Y1
|
||||||
|
new(0u, 57u, 0xCAE3E690u, [new(0x311u, 0x00000002u), new(0x312u, 0x03FF0080u)]), // PA_SC_BINNER_CNTL_0, PA_SC_BINNER_CNTL_1
|
||||||
|
new(0u, 58u, 0x43FBD769u, [new(0x105u, 0x00000000u), new(0x107u, 0x00000000u), new(0x106u, 0x00000000u), new(0x108u, 0x00000000u)]), // CB_BLEND_RED, CB_BLEND_BLUE, CB_BLEND_GREEN, CB_BLEND_ALPHA
|
||||||
|
new(0u, 59u, 0xEF550356u, [new(0x1E0u, 0x20010001u)]), // CB_BLEND0_CONTROL
|
||||||
|
new(0u, 60u, 0x8F52E279u, [new(0x020u, 0x00000000u), new(0x021u, 0x00000000u)]), // TA_BC_BASE_ADDR, TA_BC_BASE_ADDR_HI
|
||||||
|
new(0u, 61u, 0x1F2D8149u, [new(0x084u, 0x00000000u), new(0x085u, 0x20002000u)]), // PA_SC_CLIPRECT_0_TL, PA_SC_CLIPRECT_0_BR
|
||||||
|
new(0u, 62u, 0x853D0614u, [new(0x800003FFu, 0x00000000u)]), // CX_NOP
|
||||||
|
new(0u, 63u, 0x4413C6F9u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // DB_DEPTH_BOUNDS_MIN, DB_DEPTH_BOUNDS_MAX
|
||||||
|
new(0u, 64u, 0x67096014u, [new(0x010u, 0x80000000u), new(0x011u, 0x20000000u), new(0x012u, 0x00000000u), new(0x013u, 0x00000000u), new(0x014u, 0x00000000u), new(0x015u, 0x00000000u), new(0x01Au, 0x00000000u), new(0x01Bu, 0x00000000u), new(0x01Cu, 0x00000000u), new(0x01Du, 0x00000000u), new(0x01Eu, 0x00000000u), new(0x002u, 0x00000000u), new(0x005u, 0x00000000u), new(0x007u, 0x00000000u), new(0x00Bu, 0x00000000u), new(0x00Au, 0x00000000u)]), // DB_Z_INFO, DB_STENCIL_INFO, DB_Z_READ_BASE, DB_STENCIL_READ_BASE, DB_Z_WRITE_BASE, DB_STENCIL_WRITE_BASE, DB_Z_READ_BASE_HI, DB_STENCIL_READ_BASE_HI, DB_Z_WRITE_BASE_HI, DB_STENCIL_WRITE_BASE_HI, DB_HTILE_DATA_BASE_HI, DB_DEPTH_VIEW, DB_HTILE_DATA_BASE, DB_DEPTH_SIZE_XY, DB_DEPTH_CLEAR, DB_STENCIL_CLEAR
|
||||||
|
new(0u, 65u, 0x88F5E915u, [new(0x0EBu, 0xFF00FF00u), new(0x0ECu, 0x00000000u)]), // PA_SC_FOV_WINDOW_LR, PA_SC_FOV_WINDOW_TB
|
||||||
|
new(0u, 66u, 0x033F1EFFu, [new(0x800003FCu, 0x00000000u), new(0x800003FDu, 0x00000000u)]), // FSR_RECURSIONS0, FSR_RECURSIONS1
|
||||||
|
new(0u, 67u, 0x918106BBu, [new(0x090u, 0x80000000u), new(0x091u, 0x40004000u)]), // PA_SC_GENERIC_SCISSOR_TL, PA_SC_GENERIC_SCISSOR_BR
|
||||||
|
new(0u, 68u, 0x95F0E7ACu, [new(0x2FAu, 0x4E7E0000u), new(0x2FBu, 0x4E7E0000u), new(0x2FCu, 0x4E7E0000u), new(0x2FDu, 0x4E7E0000u)]), // PA_CL_GB_VERT_CLIP_ADJ, PA_CL_GB_VERT_DISC_ADJ, PA_CL_GB_HORZ_CLIP_ADJ, PA_CL_GB_HORZ_DISC_ADJ
|
||||||
|
new(0u, 69u, 0xB48CBAB2u, [new(0x2E2u, 0x00000000u), new(0x2E3u, 0x00000000u)]), // PA_SU_POLY_OFFSET_BACK_SCALE, PA_SU_POLY_OFFSET_BACK_OFFSET
|
||||||
|
new(0u, 70u, 0x05BB3BC6u, [new(0x2E0u, 0x00000000u), new(0x2E1u, 0x00000000u)]), // PA_SU_POLY_OFFSET_FRONT_SCALE, PA_SU_POLY_OFFSET_FRONT_OFFSET
|
||||||
|
new(0u, 71u, 0x94FABA07u, [new(0x003u, 0x00000000u), new(0x004u, 0x00000000u)]), // DB_RENDER_OVERRIDE, DB_RENDER_OVERRIDE2
|
||||||
|
new(0u, 72u, 0x38E92C91u, [new(0x318u, 0x00000000u), new(0x31Bu, 0x00000000u), new(0x31Cu, 0x00000000u), new(0x31Du, 0x00000000u), new(0x31Eu, 0x00000048u), new(0x31Fu, 0x00000000u), new(0x321u, 0x00000000u), new(0x323u, 0x00000000u), new(0x324u, 0x00000000u), new(0x325u, 0x00000000u), new(0x390u, 0x00000000u), new(0x398u, 0x00000000u), new(0x3A0u, 0x00000000u), new(0x3A8u, 0x00000000u), new(0x3B0u, 0x00000000u), new(0x3B8u, 0x0006C000u)]), // CB_COLOR0_BASE, CB_COLOR0_VIEW, CB_COLOR0_INFO, CB_COLOR0_ATTRIB, CB_COLOR0_DCC_CONTROL, CB_COLOR0_CMASK, CB_COLOR0_FMASK, CB_COLOR0_CLEAR_WORD0, CB_COLOR0_CLEAR_WORD1, CB_COLOR0_DCC_BASE, CB_COLOR0_BASE_EXT, CB_COLOR0_CMASK_BASE_EXT, CB_COLOR0_FMASK_BASE_EXT, CB_COLOR0_DCC_BASE_EXT, CB_COLOR0_ATTRIB2, CB_COLOR0_ATTRIB3
|
||||||
|
new(0u, 73u, 0x0B177B43u, [new(0x00Cu, 0x00000000u), new(0x00Du, 0x40004000u)]), // PA_SC_SCREEN_SCISSOR_TL, PA_SC_SCREEN_SCISSOR_BR
|
||||||
|
new(0u, 74u, 0x48531062u, [new(0x191u, 0x00000000u)]), // SPI_PS_INPUT_CNTL_0
|
||||||
|
new(0u, 75u, 0xAAA964B9u, [new(0x16Fu, 0x00000000u), new(0x170u, 0x00000000u), new(0x171u, 0x00000000u), new(0x172u, 0x00000000u)]), // PA_CL_UCP_0_X, PA_CL_UCP_0_Y, PA_CL_UCP_0_Z, PA_CL_UCP_0_W
|
||||||
|
new(0u, 76u, 0x7690AF6Fu, [new(0x10Fu, 0x4E7E0000u), new(0x111u, 0x4E7E0000u), new(0x113u, 0x4E7E0000u), new(0x110u, 0x00000000u), new(0x112u, 0x00000000u), new(0x114u, 0x00000000u), new(0x094u, 0x80000000u), new(0x095u, 0x40004000u), new(0x0B4u, 0x00000000u), new(0x0B5u, 0x00000000u)]), // PA_CL_VPORT_XSCALE, PA_CL_VPORT_YSCALE, PA_CL_VPORT_ZSCALE, PA_CL_VPORT_XOFFSET, PA_CL_VPORT_YOFFSET, PA_CL_VPORT_ZOFFSET, PA_SC_VPORT_SCISSOR_0_TL, PA_SC_VPORT_SCISSOR_0_BR, PA_SC_VPORT_ZMIN_0, PA_SC_VPORT_ZMAX_0
|
||||||
|
new(0u, 77u, 0x078D7060u, [new(0x081u, 0x80000000u), new(0x082u, 0x40004000u)]), // PA_SC_WINDOW_SCISSOR_TL, PA_SC_WINDOW_SCISSOR_BR
|
||||||
|
// shader register groups
|
||||||
|
new(1u, 0u, 0x5D6E3EC7u, [new(0x212u, 0x00000000u)]), // COMPUTE_PGM_RSRC1
|
||||||
|
new(1u, 1u, 0x57E7079Au, [new(0x213u, 0x00000000u)]), // COMPUTE_PGM_RSRC2
|
||||||
|
new(1u, 2u, 0x7467FAFDu, [new(0x228u, 0x00000000u)]), // COMPUTE_PGM_RSRC3
|
||||||
|
new(1u, 3u, 0x9E826B50u, [new(0x215u, 0x00000000u)]), // COMPUTE_RESOURCE_LIMITS
|
||||||
|
new(1u, 4u, 0xDC484F18u, [new(0x218u, 0x00000000u)]), // COMPUTE_TMPRING_SIZE
|
||||||
|
new(1u, 5u, 0x5DA8BCA3u, [new(0x08Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_GS
|
||||||
|
new(1u, 6u, 0x5CA726D8u, [new(0x10Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_HS
|
||||||
|
new(1u, 7u, 0x5DD28360u, [new(0x00Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_PS
|
||||||
|
new(1u, 8u, 0x57EFA0BEu, [new(0x08Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_GS
|
||||||
|
new(1u, 9u, 0x502363D5u, [new(0x10Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_HS
|
||||||
|
new(1u, 10u, 0x506D14BDu, [new(0x00Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_PS
|
||||||
|
new(1u, 11u, 0xB2609506u, [new(0x224u, 0x00000000u)]), // COMPUTE_USER_ACCUM_0
|
||||||
|
new(1u, 12u, 0x9E5CFB8Au, [new(0x107u, 0x00000000u), new(0x087u, 0x00000000u), new(0x007u, 0x00000000u)]), // SPI_SHADER_PGM_RSRC3_HS, SPI_SHADER_PGM_RSRC3_GS, SPI_SHADER_PGM_RSRC3_PS
|
||||||
|
new(1u, 13u, 0xC918DF3Eu, [new(0x20Cu, 0x00000000u), new(0x20Du, 0x00000000u)]), // COMPUTE_PGM_LO, COMPUTE_PGM_HI
|
||||||
|
new(1u, 14u, 0xC9751C9Cu, [new(0x0C8u, 0x00000000u), new(0x0C9u, 0x00000000u)]), // SPI_SHADER_PGM_LO_ES, SPI_SHADER_PGM_HI_ES
|
||||||
|
new(1u, 15u, 0xC97EF77Au, [new(0x088u, 0x00000000u), new(0x089u, 0x00000000u)]), // SPI_SHADER_PGM_LO_GS, SPI_SHADER_PGM_HI_GS
|
||||||
|
new(1u, 16u, 0xC927C6B9u, [new(0x108u, 0x00000000u), new(0x109u, 0x00000000u)]), // SPI_SHADER_PGM_LO_HS, SPI_SHADER_PGM_HI_HS
|
||||||
|
new(1u, 17u, 0xC92A1EC5u, [new(0x148u, 0x00000000u), new(0x149u, 0x00000000u)]), // SPI_SHADER_PGM_LO_LS, SPI_SHADER_PGM_HI_LS
|
||||||
|
new(1u, 18u, 0xC9E01B31u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // SPI_SHADER_PGM_LO_PS, SPI_SHADER_PGM_HI_PS
|
||||||
|
new(1u, 19u, 0x50685F29u, [new(0x800002FFu, 0x00000000u)]), // SH_NOP
|
||||||
|
new(1u, 20u, 0xB26219CAu, [new(0x0B2u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_ESGS_0
|
||||||
|
new(1u, 21u, 0xB25B6CF9u, [new(0x132u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_LSHS_0
|
||||||
|
new(1u, 22u, 0xB2F86101u, [new(0x032u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_PS_0
|
||||||
|
new(1u, 23u, 0x07E3B155u, [new(0x082u, 0x00000000u), new(0x083u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_GS, SPI_SHADER_USER_DATA_ADDR_HI_GS
|
||||||
|
new(1u, 24u, 0x07E383C6u, [new(0x102u, 0x00000000u), new(0x103u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_HS, SPI_SHADER_USER_DATA_ADDR_HI_HS
|
||||||
|
new(1u, 25u, 0xBDA98653u, [new(0x240u, 0x00000000u)]), // COMPUTE_USER_DATA_0
|
||||||
|
new(1u, 26u, 0xBDBD1D0Fu, [new(0x08Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_GS_0
|
||||||
|
new(1u, 27u, 0xBD946FD4u, [new(0x10Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_HS_0
|
||||||
|
new(1u, 28u, 0xBDF02A4Cu, [new(0x00Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_PS_0
|
||||||
|
// uconfig register groups
|
||||||
|
new(2u, 0u, 0x19E93E85u, [new(0x41Fu, 0x00000000u)]), // GDS_OA_ADDRESS
|
||||||
|
new(2u, 1u, 0x3B5C2AF3u, [new(0x41Du, 0x00000000u)]), // GDS_OA_CNTL
|
||||||
|
new(2u, 2u, 0x47974A35u, [new(0x41Eu, 0x00000000u)]), // GDS_OA_COUNTER
|
||||||
|
new(2u, 3u, 0x105971C2u, [new(0x25Bu, 0x00000000u)]), // GE_CNTL
|
||||||
|
new(2u, 4u, 0x7D137765u, [new(0x24Au, 0x00000000u)]), // GE_INDX_OFFSET
|
||||||
|
new(2u, 5u, 0xD187FEBCu, [new(0x24Bu, 0x00000000u)]), // GE_MULTI_PRIM_IB_RESET_EN
|
||||||
|
new(2u, 6u, 0x12F854ACu, [new(0x25Fu, 0x00000000u)]), // GE_STEREO_CNTL
|
||||||
|
new(2u, 7u, 0x40D49AD1u, [new(0x262u, 0x00000000u)]), // GE_USER_VGPR_EN
|
||||||
|
new(2u, 8u, 0x8C0923DAu, [new(0x80003FF4u, 0x00000000u)]), // FSR_EXTEND_SUBPIXEL_ROUNDING
|
||||||
|
new(2u, 9u, 0xBB8DF494u, [new(0x80003FFDu, 0x00000000u)]), // TEXTURE_GRADIENT_CONTROL
|
||||||
|
new(2u, 10u, 0xF6D8A76Eu, [new(0x382u, 0x40000040u)]), // TEXTURE_GRADIENT_FACTORS
|
||||||
|
new(2u, 11u, 0x7620F1E9u, [new(0x248u, 0x00000000u)]), // VGT_OBJECT_ID
|
||||||
|
new(2u, 12u, 0x9EBFAB10u, [new(0x242u, 0x00000000u)]), // VGT_PRIMITIVE_TYPE
|
||||||
|
new(2u, 13u, 0x98A09D0Eu, [new(0x380u, 0x00000000u), new(0x381u, 0x00000000u)]), // TA_CS_BC_BASE_ADDR, TA_CS_BC_BASE_ADDR_HI
|
||||||
|
new(2u, 14u, 0x195D37D2u, [new(0x80003FF5u, 0x00000000u), new(0x80003FF6u, 0x00000000u)]), // FSR_ALPHA_VALUE0, FSR_ALPHA_VALUE1
|
||||||
|
new(2u, 15u, 0xF9EC4F85u, [new(0x80003FF7u, 0x00000000u), new(0x80003FF8u, 0x00000000u), new(0x80003FF9u, 0x00000000u), new(0x80003FFAu, 0x00000000u)]), // FSR_CONTROL_POINT0, FSR_CONTROL_POINT1, FSR_CONTROL_POINT2, FSR_CONTROL_POINT3
|
||||||
|
new(2u, 16u, 0x4626B750u, [new(0x80003FFBu, 0x00000000u), new(0x80003FFCu, 0x00000000u)]), // FSR_WINDOW0, FSR_WINDOW1
|
||||||
|
new(2u, 17u, 0x4CC673A0u, [new(0x80003FFEu, 0x00000000u)]), // MEMORY_MAPPING_MASK
|
||||||
|
new(2u, 18u, 0xDE5B3431u, [new(0x80003FFFu, 0x00000000u)]), // UC_NOP
|
||||||
|
new(2u, 19u, 0x036AC8A6u, [new(0x25Cu, 0x00000000u)]), // GE_USER_VGPR1
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -26,5 +27,7 @@ internal static class AgcShaderCompilerHooks
|
|||||||
{
|
{
|
||||||
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
|
||||||
|
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
|
||||||
|
VulkanVideoPresenter.GuestDataPool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
|
||||||
|
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
|
||||||
|
/// selected by the 5-bit SWIZZLE_MODE in the image descriptor; uploading those
|
||||||
|
/// bytes verbatim samples as garbage.
|
||||||
|
///
|
||||||
|
/// The GFX10 addressing uses power-of-two swizzle blocks (256 B / 4 KiB /
|
||||||
|
/// 64 KiB) whose internal element order follows the standard (S), display (D),
|
||||||
|
/// render (R), or z-order/depth (Z) equations. This implements the exact base
|
||||||
|
/// S and Z 2D single-sample modes plus the PS5's RB+ 64 KiB Z_X/R_X equations;
|
||||||
|
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||||
|
/// equations are being ported.
|
||||||
|
/// </summary>
|
||||||
|
internal static class GnmTiling
|
||||||
|
{
|
||||||
|
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
|
||||||
|
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
|
||||||
|
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||||
|
private readonly record struct AddressBit(uint XMask, uint YMask);
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KRenderX =
|
||||||
|
[
|
||||||
|
// 1 byte/element: nibble01=0, nibble2=307, nibble3=379.
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||||
|
// 2 bytes/element: nibble01=1, nibble2=307, nibble3=389.
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||||
|
// 4 bytes/element: nibble01=39, nibble2=307, nibble3=381.
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), X(2), Y(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||||
|
// 8 bytes/element: nibble01=6, nibble2=307, nibble3=382.
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), X(1), X(2), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||||
|
// 16 bytes/element: nibble01=7, nibble2=307, nibble3=390.
|
||||||
|
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KDepthX =
|
||||||
|
[
|
||||||
|
// 1 byte/element: nibble01=8, nibble2=306, nibble3=379.
|
||||||
|
[X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3), Y(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(6), Y(6), XY(7, 8), XY(8, 7)],
|
||||||
|
// 2 bytes/element: nibble01=9, nibble2=306, nibble3=389.
|
||||||
|
[Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(3), X(6), XY(7, 7), XY(8, 6)],
|
||||||
|
// 4 bytes/element: nibble01=10, nibble2=306, nibble3=381.
|
||||||
|
[Zero, Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(3), Y(3), XY(6, 7), XY(7, 6)],
|
||||||
|
// 8 bytes/element: nibble01=11, nibble2=307, nibble3=382.
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), X(1), Y(1), X(2),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
Y(2), X(3), XY(7, 3), XY(6, 6)],
|
||||||
|
// 16 bytes/element is identical to R_X for a 2D single-sample image.
|
||||||
|
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
|
||||||
|
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
|
||||||
|
X(2), Y(2), XY(6, 3), XY(3, 6)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly AddressBit[][] RbPlus64KStandard =
|
||||||
|
[
|
||||||
|
// GFX10_SW_64K_S_RBPLUS_PATINFO, 1 byte/element.
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
Y(4), X(4), Y(5), X(5), Y(6), X(6), Y(7), X(7)],
|
||||||
|
// 2 bytes/element.
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7)],
|
||||||
|
// 4 bytes/element.
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||||
|
Y(3), X(3), Y(4), X(4), Y(5), X(5), Y(6), X(6)],
|
||||||
|
// 8 bytes/element (also BC1/BC4 compressed blocks).
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||||
|
Y(2), X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6)],
|
||||||
|
// 16 bytes/element (also 16-byte BC compressed blocks).
|
||||||
|
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||||
|
Y(2), X(2), Y(3), X(3), Y(4), X(4), Y(5), X(5)],
|
||||||
|
];
|
||||||
|
|
||||||
|
// GFX10 4K_S has a separate 12-bit micro-tile equation. It is not the
|
||||||
|
// generic x/y interleave used by the 64K standard block; using that larger
|
||||||
|
// equation leaves a regular grid in linearized atlases.
|
||||||
|
private static readonly AddressBit[][] Standard4K =
|
||||||
|
[
|
||||||
|
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
|
||||||
|
Y(4), X(4), Y(5), X(5)],
|
||||||
|
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
|
||||||
|
Y(3), X(4), Y(4), X(5)],
|
||||||
|
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
|
||||||
|
Y(3), X(3), Y(4), X(4)],
|
||||||
|
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
|
||||||
|
Y(2), X(3), Y(3), X(4)],
|
||||||
|
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
|
||||||
|
Y(2), X(2), Y(3), X(3)],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly bool _enabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static readonly bool _disabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
|
||||||
|
"0",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static readonly HashSet<uint> _reportedModes = new();
|
||||||
|
|
||||||
|
public static bool Enabled => _enabled || !_disabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base S/Z modes and the Oberon RB+ 64 KiB Z_X/R_X modes for which this
|
||||||
|
/// implementation carries the exact AddrLib address equations. Other
|
||||||
|
/// bank/pipe-XOR variants remain opt-in.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsTrustedByDefault(uint swizzleMode) =>
|
||||||
|
// Exact base S/Z modes. D/R use different GFX10 swizzle equations and
|
||||||
|
// the T/X modes additionally apply pipe/bank XOR between blocks.
|
||||||
|
swizzleMode is 1 or 4 or 5 or 8 or 9 or 24 or 27;
|
||||||
|
|
||||||
|
// Detile a surface when it is verified-correct by default (trusted base mode),
|
||||||
|
// or when the user opts the approximate modes in with SHARPEMU_DETILE=1.
|
||||||
|
// SHARPEMU_DETILE=0 forces the old raw-upload behavior for everything.
|
||||||
|
private static bool ShouldDetile(uint swizzleMode) =>
|
||||||
|
swizzleMode != 0 && !_disabled && (_enabled || IsTrustedByDefault(swizzleMode));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when a surface with the given swizzle mode needs deswizzling.
|
||||||
|
/// Mode 0 is linear and never needs it.
|
||||||
|
/// </summary>
|
||||||
|
public static bool NeedsDetile(uint swizzleMode) => ShouldDetile(swizzleMode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the physical byte span occupied by a tiled mip. GNM allocates whole
|
||||||
|
/// swizzle blocks even when the logical image is much smaller than a block;
|
||||||
|
/// reading only the logical texel count truncates most source offsets during
|
||||||
|
/// detiling (a 64x64 BC1 mode-9 image is 2 KiB logically but occupies one
|
||||||
|
/// 64 KiB swizzle block).
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetTiledByteCount(
|
||||||
|
uint swizzleMode,
|
||||||
|
int elementsWide,
|
||||||
|
int elementsHigh,
|
||||||
|
int bytesPerElement,
|
||||||
|
out ulong byteCount)
|
||||||
|
{
|
||||||
|
byteCount = 0;
|
||||||
|
if (!ShouldDetile(swizzleMode) ||
|
||||||
|
elementsWide <= 0 ||
|
||||||
|
elementsHigh <= 0 ||
|
||||||
|
bytesPerElement <= 0 ||
|
||||||
|
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||||
|
if (bppLog2 < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockElements = blockBytes >> bppLog2;
|
||||||
|
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||||
|
if (blockWidth == 0 || blockHeight == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocksWide = ((ulong)elementsWide + (ulong)blockWidth - 1) / (ulong)blockWidth;
|
||||||
|
var blocksHigh = ((ulong)elementsHigh + (ulong)blockHeight - 1) / (ulong)blockHeight;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byteCount = checked(blocksWide * blocksHigh * (ulong)blockBytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
byteCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
|
||||||
|
/// Elements are pixels for uncompressed formats and 4x4 blocks for
|
||||||
|
/// block-compressed formats, so callers pass the element grid dimensions
|
||||||
|
/// and the bytes per element. Returns false (leaving output untouched) for
|
||||||
|
/// unsupported swizzle modes so the caller can fall back to the raw bytes.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryDetile(
|
||||||
|
ReadOnlySpan<byte> tiled,
|
||||||
|
Span<byte> linear,
|
||||||
|
uint swizzleMode,
|
||||||
|
int elementsWide,
|
||||||
|
int elementsHigh,
|
||||||
|
int bytesPerElement)
|
||||||
|
{
|
||||||
|
if (!ShouldDetile(swizzleMode) || elementsWide <= 0 || elementsHigh <= 0 || bytesPerElement <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
|
||||||
|
{
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||||
|
if (bppLog2 < 0)
|
||||||
|
{
|
||||||
|
// Non-power-of-two element size (e.g. 24-bit) is not swizzled in a
|
||||||
|
// way this equation models.
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block dimensions in elements: a swizzle block holds blockBytes bytes,
|
||||||
|
// laid out as a square-ish power-of-two element grid scaled by bpp.
|
||||||
|
var blockElements = blockBytes >> bppLog2;
|
||||||
|
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||||
|
if (blockWidth == 0 || blockHeight == 0)
|
||||||
|
{
|
||||||
|
ReportUnsupported(swizzleMode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||||
|
var requiredLinear = (long)elementsWide * elementsHigh * bytesPerElement;
|
||||||
|
if (linear.Length < requiredLinear)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompute the within-block element offset for each (x, y) inside a
|
||||||
|
// single block. The swizzle equation only depends on the in-block
|
||||||
|
// coordinates, so this table is reused for every block — turning the
|
||||||
|
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
|
||||||
|
// Detiling a 2048x2048 texture is millions of elements; without this the
|
||||||
|
// per-pixel math makes DETILE unusably slow during asset streaming.
|
||||||
|
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||||
|
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||||
|
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||||
|
{
|
||||||
|
for (var bx = 0; bx < blockWidth; bx++)
|
||||||
|
{
|
||||||
|
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||||
|
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||||
|
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y < elementsHigh; y++)
|
||||||
|
{
|
||||||
|
var blockY = y / blockHeight;
|
||||||
|
var inBlockY = y % blockHeight;
|
||||||
|
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||||
|
var tableRowBase = inBlockY * blockWidth;
|
||||||
|
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||||
|
for (var x = 0; x < elementsWide; x++)
|
||||||
|
{
|
||||||
|
var blockX = x / blockWidth;
|
||||||
|
var inBlockX = x % blockWidth;
|
||||||
|
|
||||||
|
var blockIndex = rowBlockBase + blockX;
|
||||||
|
var sourceByte = hasExactXorPattern
|
||||||
|
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
|
||||||
|
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||||
|
(long)bytesPerElement;
|
||||||
|
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||||
|
if (sourceByte + bytesPerElement > tiled.Length ||
|
||||||
|
destByte + bytesPerElement > linear.Length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tiled.Slice((int)sourceByte, bytesPerElement)
|
||||||
|
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SwizzleKind
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
ZOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly AddressBit Zero = new(0, 0);
|
||||||
|
|
||||||
|
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||||
|
|
||||||
|
private static AddressBit Y(int bit) => new(0, 1u << bit);
|
||||||
|
|
||||||
|
private static AddressBit XY(int xBit, int yBit) => new(1u << xBit, 1u << yBit);
|
||||||
|
|
||||||
|
private static AddressBit XY(int xBit, int yBit1, int yBit2) =>
|
||||||
|
new(1u << xBit, (1u << yBit1) | (1u << yBit2));
|
||||||
|
|
||||||
|
private static bool TryGetExactXorPattern(
|
||||||
|
uint swizzleMode,
|
||||||
|
int bytesPerElementLog2,
|
||||||
|
out AddressBit[] pattern)
|
||||||
|
{
|
||||||
|
pattern = [];
|
||||||
|
if ((uint)bytesPerElementLog2 >= RbPlus64KRenderX.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = swizzleMode switch
|
||||||
|
{
|
||||||
|
5 => Standard4K[bytesPerElementLog2],
|
||||||
|
9 => RbPlus64KStandard[bytesPerElementLog2],
|
||||||
|
24 => RbPlus64KDepthX[bytesPerElementLog2],
|
||||||
|
27 => RbPlus64KRenderX[bytesPerElementLog2],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
return pattern.Length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
|
||||||
|
{
|
||||||
|
uint offset = 0;
|
||||||
|
for (var bit = 0; bit < pattern.Length; bit++)
|
||||||
|
{
|
||||||
|
var equation = pattern[bit];
|
||||||
|
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
|
||||||
|
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
|
||||||
|
offset |= (uint)parity << bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSwizzleKind(uint swizzleMode, out SwizzleKind kind, out int blockBytes)
|
||||||
|
{
|
||||||
|
// GFX10 AddrLib SWIZZLE_MODE enumeration:
|
||||||
|
// 1-3 = 256 B S/D/R
|
||||||
|
// 4-7 = 4 KiB Z/S/D/R
|
||||||
|
// 8-11 = 64 KiB Z/S/D/R
|
||||||
|
// 16-19 = 64 KiB Z/S/D/R _T
|
||||||
|
// 20-23 = 4 KiB Z/S/D/R _X
|
||||||
|
// 24-27 = 64 KiB Z/S/D/R _X
|
||||||
|
// The pipe/bank XOR (_T/_X) affects which block a given tile lands in,
|
||||||
|
// but the *within-block* element order matches the base S/Z equation,
|
||||||
|
// which is what dominates visible correctness. We model the block
|
||||||
|
// interior and treat blocks as linear-ordered.
|
||||||
|
kind = SwizzleKind.Standard;
|
||||||
|
blockBytes = 0;
|
||||||
|
switch (swizzleMode)
|
||||||
|
{
|
||||||
|
case 4: case 8: case 16: case 20: case 24:
|
||||||
|
kind = SwizzleKind.ZOrder;
|
||||||
|
break;
|
||||||
|
case 1: case 2: case 3:
|
||||||
|
case 5: case 6: case 7:
|
||||||
|
case 9: case 10: case 11:
|
||||||
|
case 17: case 18: case 19:
|
||||||
|
case 21: case 22: case 23:
|
||||||
|
case 25: case 26: case 27:
|
||||||
|
kind = SwizzleKind.Standard;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockBytes = swizzleMode switch
|
||||||
|
{
|
||||||
|
>= 1 and <= 3 => 256,
|
||||||
|
>= 4 and <= 7 => 4096,
|
||||||
|
>= 20 and <= 23 => 4096,
|
||||||
|
_ => 65536,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard (S) 2D swizzle: within a block, the element order interleaves x
|
||||||
|
// and y bits with x taking the low bit — the AMD "standard" microtile.
|
||||||
|
private static long StandardSwizzleOffset(uint x, uint y, int blockWidth, int blockHeight)
|
||||||
|
{
|
||||||
|
var widthBits = BitLog2((uint)blockWidth);
|
||||||
|
var heightBits = BitLog2((uint)blockHeight);
|
||||||
|
long offset = 0;
|
||||||
|
var outBit = 0;
|
||||||
|
var xi = 0;
|
||||||
|
var yi = 0;
|
||||||
|
while (xi < widthBits || yi < heightBits)
|
||||||
|
{
|
||||||
|
if (xi < widthBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||||
|
xi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yi < heightBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||||
|
yi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z-order (Morton) swizzle: pure bit interleave, y taking the low bit.
|
||||||
|
private static long MortonInterleave(uint x, uint y, int blockWidth, int blockHeight)
|
||||||
|
{
|
||||||
|
var widthBits = BitLog2((uint)blockWidth);
|
||||||
|
var heightBits = BitLog2((uint)blockHeight);
|
||||||
|
long offset = 0;
|
||||||
|
var outBit = 0;
|
||||||
|
var xi = 0;
|
||||||
|
var yi = 0;
|
||||||
|
while (xi < widthBits || yi < heightBits)
|
||||||
|
{
|
||||||
|
if (yi < heightBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((y >> yi) & 1u) << outBit++;
|
||||||
|
yi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xi < widthBits)
|
||||||
|
{
|
||||||
|
offset |= (long)((x >> xi) & 1u) << outBit++;
|
||||||
|
xi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Width, int Height) SquareBlockDimensions(int blockElements)
|
||||||
|
{
|
||||||
|
if (blockElements <= 0 || (blockElements & (blockElements - 1)) != 0)
|
||||||
|
{
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBits = BitLog2((uint)blockElements);
|
||||||
|
// Split as evenly as possible with width >= height (x gets the extra bit).
|
||||||
|
var widthBits = (totalBits + 1) / 2;
|
||||||
|
var heightBits = totalBits - widthBits;
|
||||||
|
return (1 << widthBits, 1 << heightBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int BitLog2(uint value)
|
||||||
|
{
|
||||||
|
if (value == 0 || (value & (value - 1)) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Numerics.BitOperations.TrailingZeroCount(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportUnsupported(uint swizzleMode)
|
||||||
|
{
|
||||||
|
lock (_reportedModes)
|
||||||
|
{
|
||||||
|
if (!_reportedModes.Add(swizzleMode))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] GNM detile: unsupported swizzle mode {swizzleMode}; texture uploaded linear.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,17 @@
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
/// <summary>
|
||||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
/// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM
|
||||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
/// condition. AgcExports re-checks every waiter against guest memory on each
|
||||||
// DmaData packets or by direct CPU writes).
|
/// submit and resumes the ones whose condition became true (labels are advanced
|
||||||
|
/// by ReleaseMem / WriteData / DmaData packets, or by direct CPU writes).
|
||||||
|
///
|
||||||
|
/// This preserves cross-submit ordering: the work that follows a wait inside a
|
||||||
|
/// DCB is only queued once the awaited completion label is genuinely written,
|
||||||
|
/// instead of being force-satisfied at parse time and running ahead of the
|
||||||
|
/// compute/graphics work it depends on (which produced a black composite).
|
||||||
|
/// </summary>
|
||||||
internal static class GpuWaitRegistry
|
internal static class GpuWaitRegistry
|
||||||
{
|
{
|
||||||
public struct WaitingDcb
|
public struct WaitingDcb
|
||||||
@@ -19,13 +26,56 @@ internal static class GpuWaitRegistry
|
|||||||
public ulong ReferenceValue;
|
public ulong ReferenceValue;
|
||||||
public ulong Mask;
|
public ulong Mask;
|
||||||
public uint CompareFunction;
|
public uint CompareFunction;
|
||||||
|
public uint ControlValue;
|
||||||
public bool Is64Bit;
|
public bool Is64Bit;
|
||||||
|
public bool IsStandard;
|
||||||
|
public object? Memory;
|
||||||
|
public string? QueueName;
|
||||||
|
public ulong SubmissionId;
|
||||||
|
// Stopwatch timestamp captured at registration. Stale waiters remain
|
||||||
|
// registered; this only controls one-shot diagnostics.
|
||||||
|
public long RegisteredTicks;
|
||||||
|
public bool StaleReported;
|
||||||
public object? State;
|
public object? State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||||
|
|
||||||
|
public static int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
total += list.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CountForMemory(object memory)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
foreach (var waiter in list)
|
||||||
|
{
|
||||||
|
total += ReferenceEquals(waiter.Memory, memory) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Register(ulong address, WaitingDcb waiter)
|
public static void Register(ulong address, WaitingDcb waiter)
|
||||||
{
|
{
|
||||||
waiter.WaitAddress = address;
|
waiter.WaitAddress = address;
|
||||||
@@ -41,9 +91,15 @@ internal static class GpuWaitRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
/// <summary>
|
||||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
/// Re-evaluates every registered waiter. <paramref name="readValue"/>
|
||||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
/// receives (address, is64Bit) and returns null when the memory is
|
||||||
|
/// unreadable; such waiters are kept registered. Returns the waiters whose
|
||||||
|
/// condition is now satisfied (removed from the registry), or null.
|
||||||
|
/// </summary>
|
||||||
|
public static List<WaitingDcb>? CollectSatisfied(
|
||||||
|
object memory,
|
||||||
|
Func<ulong, bool, ulong?> readValue)
|
||||||
{
|
{
|
||||||
List<WaitingDcb>? woken = null;
|
List<WaitingDcb>? woken = null;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -53,6 +109,11 @@ internal static class GpuWaitRegistry
|
|||||||
{
|
{
|
||||||
for (var i = list.Count - 1; i >= 0; i--)
|
for (var i = list.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
|
if (!ReferenceEquals(list[i].Memory, memory))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var value = readValue(address, list[i].Is64Bit);
|
var value = readValue(address, list[i].Is64Bit);
|
||||||
if (value is null || !Compare(list[i], value.Value))
|
if (value is null || !Compare(list[i], value.Value))
|
||||||
{
|
{
|
||||||
@@ -71,7 +132,7 @@ internal static class GpuWaitRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emptied != null)
|
if (emptied is not null)
|
||||||
{
|
{
|
||||||
foreach (var address in emptied)
|
foreach (var address in emptied)
|
||||||
{
|
{
|
||||||
@@ -83,20 +144,122 @@ internal static class GpuWaitRegistry
|
|||||||
return woken;
|
return woken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns waiters that have remained unsatisfied longer than
|
||||||
|
/// <paramref name="maxAgeTicks"/> exactly once, without removing them or
|
||||||
|
/// changing their labels. Missing GPU work must fail closed: advancing a
|
||||||
|
/// command buffer without its real producer corrupts cross-queue ordering.
|
||||||
|
/// </summary>
|
||||||
|
public static List<WaitingDcb>? CollectUnreportedStale(
|
||||||
|
object memory,
|
||||||
|
long nowTicks,
|
||||||
|
long maxAgeTicks)
|
||||||
|
{
|
||||||
|
List<WaitingDcb>? stale = null;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var (_, list) in _waiters)
|
||||||
|
{
|
||||||
|
for (var i = list.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var waiter = list[i];
|
||||||
|
if (!ReferenceEquals(waiter.Memory, memory) ||
|
||||||
|
waiter.StaleReported ||
|
||||||
|
nowTicks - waiter.RegisteredTicks < maxAgeTicks)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stale ??= new List<WaitingDcb>();
|
||||||
|
waiter.StaleReported = true;
|
||||||
|
list[i] = waiter;
|
||||||
|
stale.Add(waiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns watched labels overlapped by a newly discovered producer. Used
|
||||||
|
/// only for diagnostics; producer completion still wakes through the
|
||||||
|
/// normal CollectSatisfied path after the ordered memory write executes.
|
||||||
|
/// </summary>
|
||||||
|
public static List<(ulong Address, int Count)> SnapshotInRange(
|
||||||
|
object memory,
|
||||||
|
ulong start,
|
||||||
|
ulong length)
|
||||||
|
{
|
||||||
|
var matches = new List<(ulong Address, int Count)>();
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = start > ulong.MaxValue - length ? ulong.MaxValue : start + length;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
foreach (var (address, list) in _waiters)
|
||||||
|
{
|
||||||
|
var matchingCount = 0;
|
||||||
|
var any64Bit = false;
|
||||||
|
foreach (var waiter in list)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(waiter.Memory, memory))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingCount++;
|
||||||
|
any64Bit |= waiter.Is64Bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = any64Bit
|
||||||
|
? sizeof(ulong)
|
||||||
|
: sizeof(uint);
|
||||||
|
var waitEnd = address > ulong.MaxValue - (ulong)width
|
||||||
|
? ulong.MaxValue
|
||||||
|
: address + (ulong)width;
|
||||||
|
if (start < waitEnd && address < end)
|
||||||
|
{
|
||||||
|
matches.Add((address, matchingCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||||
{
|
{
|
||||||
var masked = value & waiter.Mask;
|
var masked = value & waiter.Mask;
|
||||||
|
var reference = waiter.ReferenceValue & waiter.Mask;
|
||||||
return waiter.CompareFunction switch
|
return waiter.CompareFunction switch
|
||||||
{
|
{
|
||||||
1 => masked < waiter.ReferenceValue,
|
0 => true,
|
||||||
2 => masked <= waiter.ReferenceValue,
|
1 => masked < reference,
|
||||||
3 => masked == waiter.ReferenceValue,
|
2 => masked <= reference,
|
||||||
4 => masked != waiter.ReferenceValue,
|
3 => masked == reference,
|
||||||
5 => masked >= waiter.ReferenceValue,
|
4 => masked != reference,
|
||||||
6 => masked > waiter.ReferenceValue,
|
5 => masked >= reference,
|
||||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
6 => masked > reference,
|
||||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
// 7 is reserved; treating it as satisfied keeps a malformed packet
|
||||||
|
// from suspending forever.
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_waiters.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,11 @@ namespace SharpEmu.Libs.Ajm;
|
|||||||
|
|
||||||
public static class AjmExports
|
public static class AjmExports
|
||||||
{
|
{
|
||||||
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
|
|
||||||
private static int _nextContextId;
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "dl+4eHSzUu4",
|
Nid = "dl+4eHSzUu4",
|
||||||
ExportName = "sceAjmInitialize",
|
ExportName = "sceAjmInitialize",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceAjm")]
|
LibraryName = "libSceAjm")]
|
||||||
public static int Initialize(CpuContext ctx)
|
public static int Initialize(CpuContext ctx) =>
|
||||||
{
|
SharpEmu.Libs.Audio.AjmExports.AjmInitialize(ctx);
|
||||||
var reserved = ctx[CpuRegister.Rdi];
|
|
||||||
var outContextAddress = ctx[CpuRegister.Rsi];
|
|
||||||
|
|
||||||
// AJM requires a zero reserved argument.
|
|
||||||
if (reserved != 0 || outContextAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
|
||||||
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public static class AmprExports
|
|||||||
public ulong Buffer;
|
public ulong Buffer;
|
||||||
public ulong Size;
|
public ulong Size;
|
||||||
public ulong WriteOffset;
|
public ulong WriteOffset;
|
||||||
|
public ulong CommandCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CachedHostFile
|
private sealed class CachedHostFile
|
||||||
@@ -267,53 +268,19 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong bytesRead = 0;
|
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||||
|
|
||||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
|
||||||
// speculative reads and only consume the bytes on success paths.
|
|
||||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
|
||||||
{
|
{
|
||||||
if (destination != 0 && size > 0)
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
int chunkSize = (int)Math.Min(size, 4096);
|
|
||||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
|
||||||
zeros.Clear();
|
|
||||||
while (bytesRead < size)
|
|
||||||
{
|
|
||||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
|
||||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bytesRead += (ulong)currentChunk;
|
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset -1 means "continue after the previous read of this file id".
|
|
||||||
if (fileOffset == unchecked((ulong)(long)-1))
|
|
||||||
{
|
|
||||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
|
||||||
}
|
|
||||||
else if (fileOffset > long.MaxValue)
|
|
||||||
{
|
|
||||||
fileOffset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
|
||||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
|
||||||
|
|
||||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -418,6 +385,35 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gzndltBEzWc",
|
||||||
|
ExportName = "sceAmprCommandBufferGetNumCommands",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int CommandBufferGetNumCommands(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
|
if (commandBuffer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong commandCount;
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
commandCount = state.CommandCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "get_num_commands", commandBuffer, commandCount, 0);
|
||||||
|
ctx[CpuRegister.Rax] = commandCount;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "H896Pt-yB4I",
|
Nid = "H896Pt-yB4I",
|
||||||
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
|
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
|
||||||
@@ -533,7 +529,7 @@ public static class AmprExports
|
|||||||
var offset = 0UL;
|
var offset = 0UL;
|
||||||
while (offset < writeOffset)
|
while (offset < writeOffset)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -649,6 +645,7 @@ public static class AmprExports
|
|||||||
state.Buffer = buffer;
|
state.Buffer = buffer;
|
||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = writeOffset;
|
state.WriteOffset = writeOffset;
|
||||||
|
state.CommandCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,6 +678,7 @@ public static class AmprExports
|
|||||||
state.Buffer = buffer;
|
state.Buffer = buffer;
|
||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = 0;
|
state.WriteOffset = 0;
|
||||||
|
state.CommandCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -911,6 +909,7 @@ public static class AmprExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.WriteOffset += recordSize;
|
state.WriteOffset += recordSize;
|
||||||
|
state.CommandCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -963,6 +962,19 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||||
{
|
{
|
||||||
if (!_traceAmpr)
|
if (!_traceAmpr)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class AjmExports
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
|
||||||
|
private static int _nextContextId;
|
||||||
|
public static int AjmInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var reserved = ctx[CpuRegister.Rdi];
|
||||||
|
var outputAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (reserved != 0 || outputAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
|
||||||
|
if (!ctx.Memory.TryWrite(outputAddress, value))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contexts[contextId] = 0;
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MHur6qCsUus",
|
||||||
|
ExportName = "sceAjmFinalize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmFinalize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Q3dyFuwGn64",
|
||||||
|
ExportName = "sceAjmModuleRegister",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmModuleRegister(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var reserved = ctx[CpuRegister.Rdx];
|
||||||
|
if (reserved != 0 || !Contexts.ContainsKey(contextId))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x806A0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Wi7DtlLV+KI",
|
||||||
|
ExportName = "sceAjmModuleUnregister",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmModuleUnregister(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MmpF1XsQiHw",
|
||||||
|
ExportName = "sceAjmBatchInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAjm")]
|
||||||
|
public static int AjmBatchInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// The caller owns and initializes the batch storage. This API resets
|
||||||
|
// its submission cursor on hardware; FMOD does not consume a return
|
||||||
|
// value or an additional output object here.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,22 +3,70 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
public static class AudioOut2Exports
|
public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
|
||||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately
|
||||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
// following the 0x40-byte parameter block.
|
||||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
private const int AudioOut2ContextParamSize = 0x40;
|
||||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
|
||||||
// populate.
|
|
||||||
private const int AudioOut2ContextParamSize = 0x30;
|
|
||||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||||
|
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||||
private static long _nextContextHandle = 1;
|
private static long _nextContextHandle = 1;
|
||||||
private static long _nextUserHandle = 1;
|
private static long _nextUserHandle = 1;
|
||||||
private static int _nextPortId;
|
private static int _nextPortId;
|
||||||
|
private static long _pushTraceCount;
|
||||||
|
|
||||||
|
// Per-context audio parameters captured at ContextCreate so ContextAdvance
|
||||||
|
// can pace to the real playback cadence (grain samples at the sample rate).
|
||||||
|
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
|
||||||
|
|
||||||
|
private sealed class ContextState
|
||||||
|
{
|
||||||
|
private readonly object _paceGate = new();
|
||||||
|
private long _nextAdvanceTimestamp;
|
||||||
|
|
||||||
|
public ContextState(uint frequency, uint channels, uint grainSamples)
|
||||||
|
{
|
||||||
|
Frequency = frequency == 0 ? 48000 : frequency;
|
||||||
|
Channels = channels == 0 ? 2 : channels;
|
||||||
|
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint Frequency { get; }
|
||||||
|
public uint Channels { get; }
|
||||||
|
public uint GrainSamples { get; }
|
||||||
|
|
||||||
|
// Blocks the advancing thread until one grain worth of wall-clock time
|
||||||
|
// has elapsed since the previous advance, matching hardware timing so
|
||||||
|
// audio-gated titles neither spin nor drift ahead.
|
||||||
|
public void PaceAdvance()
|
||||||
|
{
|
||||||
|
long delay;
|
||||||
|
lock (_paceGate)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_nextAdvanceTimestamp < now)
|
||||||
|
{
|
||||||
|
_nextAdvanceTimestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = _nextAdvanceTimestamp - now;
|
||||||
|
_nextAdvanceTimestamp += checked(
|
||||||
|
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "g2tViFIohHE",
|
Nid = "g2tViFIohHE",
|
||||||
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
|
|||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
if (paramAddress == 0)
|
if (paramAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
@@ -52,8 +100,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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
|
|||||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||||
? ctx.SetReturn(0)
|
memoryInfo.Clear();
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -88,13 +143,34 @@ 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read channels/frequency/grain from the reset-param blob so the
|
||||||
|
// context can pace advances to the real audio cadence.
|
||||||
|
uint channels = 2;
|
||||||
|
uint frequency = 48000;
|
||||||
|
uint grain = 256;
|
||||||
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
|
if (ctx.Memory.TryRead(paramAddress, param))
|
||||||
|
{
|
||||||
|
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||||
|
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||||
|
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
|
||||||
|
if (pc is > 0 and <= 8) channels = pc;
|
||||||
|
if (pf is >= 8000 and <= 192000) frequency = pf;
|
||||||
|
// Values below one cache line are flags/counts in observed PS5
|
||||||
|
// callers, not audio grains. Keep the hardware-sized default.
|
||||||
|
if (pg is >= 64 and <= 0x4000) grain = pg;
|
||||||
|
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
Contexts[handle] = new ContextState(frequency, channels, grain);
|
||||||
? ctx.SetReturn(0)
|
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2ContextDestroy",
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DxGyV8dtOR8",
|
||||||
|
ExportName = "sceAudioOut2ContextBedWrite",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aII9h5nli9U",
|
||||||
|
ExportName = "sceAudioOut2ContextPush",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextPush(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var traceCount = Interlocked.Increment(ref _pushTraceCount);
|
||||||
|
if (traceCount <= 16)
|
||||||
|
{
|
||||||
|
TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Contexts.TryGetValue(handle, out var context))
|
||||||
|
{
|
||||||
|
// FMOD's PS5 output path uses ContextPush as the submission clock
|
||||||
|
// and does not call ContextAdvance. Pace pushes to one hardware
|
||||||
|
// grain so the feeder cannot outrun playback and starve the game.
|
||||||
|
context.PaceAdvance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PE2zHMqLSHs",
|
||||||
|
ExportName = "sceAudioOut2ContextAdvance",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextAdvance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Advancing renders one grain of audio on hardware; pace it to the same
|
||||||
|
// wall-clock cadence so the guest audio thread runs at the right speed.
|
||||||
|
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
|
||||||
|
{
|
||||||
|
context.PaceAdvance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "R7d0F1g2qsU",
|
||||||
|
ExportName = "sceAudioOut2ContextGetQueueLevel",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// The advance path paces synchronously, so the queue is always drained.
|
||||||
|
var levelAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (levelAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt64(ctx, levelAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JK2wamZPzwM",
|
Nid = "JK2wamZPzwM",
|
||||||
@@ -117,16 +263,23 @@ 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (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 TryWriteUInt64(ctx, outPortAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8XTArSPyWHk",
|
||||||
|
ExportName = "sceAudioOut2PortSetAttributes",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gatEUKG+Ea4",
|
Nid = "gatEUKG+Ea4",
|
||||||
ExportName = "sceAudioOut2PortGetState",
|
ExportName = "sceAudioOut2PortGetState",
|
||||||
@@ -138,7 +291,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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = (int)((handle >> 16) & 0xFF);
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
@@ -151,8 +304,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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
|
|||||||
var infoAddress = ctx[CpuRegister.Rdi];
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (infoAddress == 0)
|
if (infoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> info = stackalloc byte[0x40];
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
@@ -175,8 +328,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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -184,14 +337,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) => ctx.SetReturn(0);
|
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 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) => ctx.SetReturn(0);
|
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xywYcRB7nbQ",
|
Nid = "xywYcRB7nbQ",
|
||||||
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
|
|||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
|
||||||
|
outUserAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (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 TryWriteUInt64(ctx, outUserAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceAudioOut2(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public static class AudioOutExports
|
|||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
public IHostAudioStream? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
|
public volatile float Volume = 1.0f;
|
||||||
public int BufferByteLength =>
|
public int BufferByteLength =>
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
@@ -198,7 +199,8 @@ public static class AudioOutExports
|
|||||||
checked((int)port.BufferLength),
|
checked((int)port.BufferLength),
|
||||||
port.Channels,
|
port.Channels,
|
||||||
port.BytesPerSample,
|
port.BytesPerSample,
|
||||||
port.IsFloat);
|
port.IsFloat,
|
||||||
|
port.Volume);
|
||||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
@@ -225,10 +227,43 @@ 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 ctx.SetReturn(
|
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
Ports.ContainsKey(handle)
|
var volumeArrayAddress = ctx[CpuRegister.Rdx];
|
||||||
? 0
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int unityVolume = 32768;
|
||||||
|
var maxVolume = 0;
|
||||||
|
var found = false;
|
||||||
|
if (volumeArrayAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> raw = stackalloc byte[sizeof(int)];
|
||||||
|
for (var channel = 0; channel < 8; channel++)
|
||||||
|
{
|
||||||
|
if ((channelFlags & (1u << channel)) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
|
||||||
|
maxVolume = Math.Max(maxVolume, value);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ShutdownAllPorts()
|
public static void ShutdownAllPorts()
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
|
|||||||
int frames,
|
int frames,
|
||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat)
|
bool isFloat,
|
||||||
|
float volume)
|
||||||
{
|
{
|
||||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||||
for (var frame = 0; frame < frames; frame++)
|
for (var frame = 0; frame < frames; frame++)
|
||||||
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
|
|||||||
var right = channels == 1
|
var right = channels == 1
|
||||||
? left
|
? left
|
||||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
|
left = ApplyVolume(left, volume);
|
||||||
|
right = ApplyVolume(right, volume);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||||
}
|
}
|
||||||
@@ -52,4 +55,10 @@ internal static class AudioPcmConversion
|
|||||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||||
return checked((short)MathF.Round(value * short.MaxValue));
|
return checked((short)MathF.Round(value * short.MaxValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static short ApplyVolume(short sample, float volume)
|
||||||
|
{
|
||||||
|
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
|
||||||
|
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class FmodCompatExports
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uPLTdl3psGk",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libfmod")]
|
||||||
|
public static int FmodSystemSetOutput(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var system = ctx[CpuRegister.Rdi];
|
||||||
|
var output = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
|
||||||
|
var resetPrematureInit = callCount <= 2;
|
||||||
|
if (resetPrematureInit && system != 0)
|
||||||
|
{
|
||||||
|
Span<byte> zeroByte = stackalloc byte[1];
|
||||||
|
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
|
||||||
|
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
|
||||||
|
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PS5 FMOD object arrives with its initialized byte set before Unity has
|
||||||
|
// applied the startup output configuration. Preserve those settings while
|
||||||
|
// clearing the premature marker so Studio can execute the real core init.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,403 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Bink;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional host-side Bink 2 bridge for games that ship a static Bink player.
|
||||||
|
///
|
||||||
|
/// The game in that case never imports libSceVideodec, so an HLE video-decoder
|
||||||
|
/// export cannot see its movie frames. Kernel file opens identify the active
|
||||||
|
/// .bk2 file and the presenter requests BGRA frames from a tiny native adapter.
|
||||||
|
/// The adapter is deliberately a separate, user-supplied library: Bink 2 is a
|
||||||
|
/// proprietary SDK and SharpEmu must neither bundle it nor depend on its ABI.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Bink2MovieBridge
|
||||||
|
{
|
||||||
|
private const uint MaxDimension = 16384;
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static NativeAdapter? _adapter;
|
||||||
|
private static string? _activePath;
|
||||||
|
private static IntPtr _activeMovie;
|
||||||
|
private static Bink2MovieInfo _activeInfo;
|
||||||
|
private static byte[]? _frameBuffer;
|
||||||
|
private static bool _usingDummyMovie;
|
||||||
|
private static bool _loadAttempted;
|
||||||
|
private static bool _availabilityReported;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the guest should receive a normal "file not found"
|
||||||
|
/// result for a Bink movie. This is the safe default without a decoder:
|
||||||
|
/// games that treat movies as optional fall through to their next state
|
||||||
|
/// rather than submitting an empty Bink GPU texture forever.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool ShouldSkipGuestMovie(string hostPath) =>
|
||||||
|
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
ResolveMode() == MovieMode.Skip;
|
||||||
|
|
||||||
|
internal static void ObserveGuestMovie(string hostPath)
|
||||||
|
{
|
||||||
|
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!File.Exists(hostPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ResolveMode() == MovieMode.Dummy)
|
||||||
|
{
|
||||||
|
AttachDummyMovieLocked(hostPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var adapter = GetAdapterLocked();
|
||||||
|
if (adapter is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseActiveLocked();
|
||||||
|
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValid(info))
|
||||||
|
{
|
||||||
|
adapter.Close(movie);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePath = hostPath;
|
||||||
|
_activeMovie = movie;
|
||||||
|
_activeInfo = info;
|
||||||
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
|
info.Width + "x" + info.Height + " @ " +
|
||||||
|
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryDecodeNextFrame(
|
||||||
|
out byte[] pixels,
|
||||||
|
out uint width,
|
||||||
|
out uint height)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
pixels = [];
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||||
|
{
|
||||||
|
if (_usingDummyMovie && _frameBuffer is not null)
|
||||||
|
{
|
||||||
|
pixels = _frameBuffer;
|
||||||
|
width = _activeInfo.Width;
|
||||||
|
height = _activeInfo.Height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (byte* destination = _frameBuffer)
|
||||||
|
{
|
||||||
|
if (!_adapter.DecodeNextBgra(
|
||||||
|
_activeMovie,
|
||||||
|
(IntPtr)destination,
|
||||||
|
_activeInfo.Width * 4,
|
||||||
|
(uint)_frameBuffer.Length))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels = _frameBuffer;
|
||||||
|
width = _activeInfo.Width;
|
||||||
|
height = _activeInfo.Height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValid(Bink2MovieInfo info) =>
|
||||||
|
info.Width > 0 && info.Height > 0 &&
|
||||||
|
info.Width <= MaxDimension && info.Height <= MaxDimension &&
|
||||||
|
(ulong)info.Width * info.Height * 4 <= int.MaxValue;
|
||||||
|
|
||||||
|
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||||
|
checked((int)((ulong)info.Width * info.Height * 4));
|
||||||
|
|
||||||
|
private static MovieMode ResolveMode()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||||
|
if (string.Equals(configured, "dummy", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(configured, "native", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Native;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(configured, "skip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return MovieMode.Skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no SDK adapter present, returning "not found" makes optional
|
||||||
|
// cinematics advance. Supplying either an explicit path or the normal
|
||||||
|
// side-by-side adapter enables native playback automatically.
|
||||||
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||||
|
EnumerateAdapterCandidates().Any(File.Exists))
|
||||||
|
{
|
||||||
|
return MovieMode.Native;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MovieMode.Skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
|
{
|
||||||
|
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][WARN] Bink dummy could not read movie header '" +
|
||||||
|
Path.GetFileName(hostPath) + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseActiveLocked();
|
||||||
|
_activePath = hostPath;
|
||||||
|
_activeInfo = info;
|
||||||
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
|
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||||
|
_usingDummyMovie = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
|
info.Width + "x" + info.Height + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
Span<byte> header = stackalloc byte[32];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(path);
|
||||||
|
if (stream.Read(header) != header.Length ||
|
||||||
|
!header[..4].SequenceEqual("KB2j"u8))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new Bink2MovieInfo(
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||||
|
1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FillDummyFrame(byte[] pixels, uint width, uint height)
|
||||||
|
{
|
||||||
|
for (var y = 0u; y < height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0u; x < width; x++)
|
||||||
|
{
|
||||||
|
var offset = checked((int)(((ulong)y * width + x) * 4));
|
||||||
|
var band = ((x / 96) + (y / 96)) & 1;
|
||||||
|
pixels[offset] = band == 0 ? (byte)0x28 : (byte)0x18;
|
||||||
|
pixels[offset + 1] = band == 0 ? (byte)0x18 : (byte)0x28;
|
||||||
|
pixels[offset + 2] = 0x10;
|
||||||
|
pixels[offset + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeAdapter? GetAdapterLocked()
|
||||||
|
{
|
||||||
|
if (_loadAttempted)
|
||||||
|
{
|
||||||
|
return _adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadAttempted = true;
|
||||||
|
foreach (var candidate in EnumerateAdapterCandidates())
|
||||||
|
{
|
||||||
|
if (!NativeLibrary.TryLoad(candidate, out var library))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeAdapter.TryCreate(library, out var adapter))
|
||||||
|
{
|
||||||
|
_adapter = adapter;
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.Free(library);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_availabilityReported)
|
||||||
|
{
|
||||||
|
_availabilityReported = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EnumerateAdapterCandidates()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
|
||||||
|
if (!string.IsNullOrWhiteSpace(configured))
|
||||||
|
{
|
||||||
|
yield return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseDirectory = AppContext.BaseDirectory;
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CloseActiveLocked()
|
||||||
|
{
|
||||||
|
if (_activeMovie != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
_adapter?.Close(_activeMovie);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePath = null;
|
||||||
|
_activeMovie = IntPtr.Zero;
|
||||||
|
_activeInfo = default;
|
||||||
|
_frameBuffer = null;
|
||||||
|
_usingDummyMovie = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private readonly struct Bink2MovieInfo
|
||||||
|
{
|
||||||
|
public readonly uint Width;
|
||||||
|
public readonly uint Height;
|
||||||
|
public readonly uint FramesPerSecondNumerator;
|
||||||
|
public readonly uint FramesPerSecondDenominator;
|
||||||
|
|
||||||
|
internal Bink2MovieInfo(
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint framesPerSecondNumerator,
|
||||||
|
uint framesPerSecondDenominator)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||||
|
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MovieMode
|
||||||
|
{
|
||||||
|
Skip,
|
||||||
|
Dummy,
|
||||||
|
Native,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NativeAdapter
|
||||||
|
{
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void CloseDelegate(IntPtr movie);
|
||||||
|
|
||||||
|
private readonly OpenUtf8Delegate _openUtf8;
|
||||||
|
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
||||||
|
private readonly CloseDelegate _close;
|
||||||
|
|
||||||
|
private NativeAdapter(
|
||||||
|
OpenUtf8Delegate openUtf8,
|
||||||
|
DecodeNextBgraDelegate decodeNextBgra,
|
||||||
|
CloseDelegate close)
|
||||||
|
{
|
||||||
|
_openUtf8 = openUtf8;
|
||||||
|
_decodeNextBgra = decodeNextBgra;
|
||||||
|
_close = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||||
|
{
|
||||||
|
adapter = null;
|
||||||
|
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||||
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||||
|
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = new NativeAdapter(
|
||||||
|
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||||
|
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||||
|
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||||
|
{
|
||||||
|
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeCoTaskMem(utf8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
||||||
|
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
||||||
|
|
||||||
|
internal void Close(IntPtr movie) => _close(movie);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Codec;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
|
||||||
|
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
|
||||||
|
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
|
||||||
|
/// and report "no output produced" so guests advance instead of failing on
|
||||||
|
/// unresolved imports.
|
||||||
|
/// </summary>
|
||||||
|
public static class CodecExports
|
||||||
|
{
|
||||||
|
private const int Ok = 0;
|
||||||
|
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||||
|
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
|
||||||
|
private static long _nextHandle = 1;
|
||||||
|
|
||||||
|
// ---- Video decoder ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "qkgRiwHyheU", ExportName = "sceVideodecCreateDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecCreateDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (outHandleAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||||
|
VideoDecoders[handle] = 1;
|
||||||
|
return TryWriteHandle(ctx, outHandleAddress, handle) ? SetReturn(ctx, Ok) : SetReturn(ctx, VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "q0W5GJMovMs", ExportName = "sceVideodecDecode",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecDecode(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// No decoder is present: report success with no picture produced.
|
||||||
|
return SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "jeigLlKdp5I", ExportName = "sceVideodecFlush",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecFlush(CpuContext ctx) =>
|
||||||
|
SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "U0kpGF1cl90", ExportName = "sceVideodecDeleteDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
|
||||||
|
public static int VideodecDeleteDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
VideoDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Audio decoder ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecCreateDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
||||||
|
AudioDecoders[handle] = 1;
|
||||||
|
// sceAudiodec returns the handle directly (>= 0) or a negative error.
|
||||||
|
ctx[CpuRegister.Rax] = handle;
|
||||||
|
return unchecked((int)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecDecode(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// No decoder present: report success with zero output samples so the
|
||||||
|
// caller treats the frame as silent rather than erroring.
|
||||||
|
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecDeleteDecoder(CpuContext ctx)
|
||||||
|
{
|
||||||
|
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
|
||||||
|
ctx.TryWriteUInt64(address, handle);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// libSceErrorDialog. There is no host error dialog, so an opened dialog
|
||||||
|
/// immediately reports finished — this keeps guests that block on the dialog
|
||||||
|
/// status (a common fatal-error path) from spinning forever.
|
||||||
|
/// </summary>
|
||||||
|
public static class ErrorDialogExports
|
||||||
|
{
|
||||||
|
private const int AlreadyInitialized = unchecked((int)0x80ED0001);
|
||||||
|
private const int NotInitialized = unchecked((int)0x80ED0002);
|
||||||
|
private const int ArgNull = unchecked((int)0x80ED0005);
|
||||||
|
|
||||||
|
private const int StatusNone = 0;
|
||||||
|
private const int StatusInitialized = 1;
|
||||||
|
private const int StatusFinished = 3;
|
||||||
|
|
||||||
|
private static int _initialized;
|
||||||
|
private static int _status;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "I88KChlynSs",
|
||||||
|
ExportName = "sceErrorDialogInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = Interlocked.Exchange(ref _initialized, 1) == 0 ? 0 : AlreadyInitialized;
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "M2ZF-ClLhgY",
|
||||||
|
ExportName = "sceErrorDialogOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ArgNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Volatile.Read(ref _initialized) == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Volatile.Write(ref _status, StatusFinished);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "t2FvHRXzgqk",
|
||||||
|
ExportName = "sceErrorDialogGetStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WWiGuh9XfgQ",
|
||||||
|
ExportName = "sceErrorDialogUpdateStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ekXHb1kDBl0",
|
||||||
|
ExportName = "sceErrorDialogClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusFinished);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9XAxK2PMwk8",
|
||||||
|
ExportName = "sceErrorDialogTerminate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceErrorDialog")]
|
||||||
|
public static int ErrorDialogTerminate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _status, StatusNone);
|
||||||
|
Interlocked.Exchange(ref _initialized, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ public static class FiberExports
|
|||||||
private const uint FiberStateIdle = 2;
|
private const uint FiberStateIdle = 2;
|
||||||
private const uint FiberStateTerminated = 3;
|
private const uint FiberStateTerminated = 3;
|
||||||
private const uint FiberFlagContextSizeCheck = 0x10;
|
private const uint FiberFlagContextSizeCheck = 0x10;
|
||||||
|
private const uint FiberFlagSetFpuRegs = 0x100;
|
||||||
|
private const uint Firmware350BuildVersion = 0x03500000;
|
||||||
|
private const ushort InitialFpuControlWord = 0x037F;
|
||||||
|
private const uint InitialMxcsr = 0x9FC0;
|
||||||
|
|
||||||
private const int FiberErrorNull = unchecked((int)0x80590001);
|
private const int FiberErrorNull = unchecked((int)0x80590001);
|
||||||
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
||||||
@@ -45,13 +50,26 @@ public static class FiberExports
|
|||||||
|
|
||||||
private static int _contextSizeCheck;
|
private static int _contextSizeCheck;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static ulong _currentFiberAddress;
|
|
||||||
|
|
||||||
private static readonly object _fiberGate = new();
|
private static readonly object _fiberGate = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
|
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, FiberThreadState> _threadStates = new();
|
||||||
|
|
||||||
|
static FiberExports()
|
||||||
|
{
|
||||||
|
RunFiberSelfChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResetRuntimeState()
|
||||||
|
{
|
||||||
|
lock (_fiberGate)
|
||||||
|
{
|
||||||
|
_continuations.Clear();
|
||||||
|
_stackRanges.Clear();
|
||||||
|
_threadStates.Clear();
|
||||||
|
Volatile.Write(ref _contextSizeCheck, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hVYD7Ou2pCQ",
|
Nid = "hVYD7Ou2pCQ",
|
||||||
@@ -61,6 +79,7 @@ public static class FiberExports
|
|||||||
public static int FiberInitialize(CpuContext ctx)
|
public static int FiberInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var optParam = ReadStackArg64(ctx, 0);
|
var optParam = ReadStackArg64(ctx, 0);
|
||||||
|
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||||
return FiberInitializeCore(
|
return FiberInitializeCore(
|
||||||
ctx,
|
ctx,
|
||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
@@ -70,7 +89,8 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
optParam,
|
optParam,
|
||||||
flags: 0);
|
flags: 0,
|
||||||
|
buildVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -82,6 +102,7 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
var optParam = ReadStackArg64(ctx, 0);
|
var optParam = ReadStackArg64(ctx, 0);
|
||||||
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||||
|
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 2));
|
||||||
return FiberInitializeCore(
|
return FiberInitializeCore(
|
||||||
ctx,
|
ctx,
|
||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
@@ -91,7 +112,8 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
optParam,
|
optParam,
|
||||||
flags);
|
flags,
|
||||||
|
buildVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -104,17 +126,17 @@ public static class FiberExports
|
|||||||
var optParam = ctx[CpuRegister.Rdi];
|
var optParam = ctx[CpuRegister.Rdi];
|
||||||
if (optParam == 0)
|
if (optParam == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((optParam & 7) != 0)
|
if ((optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -127,24 +149,23 @@ 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 ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != FiberStateIdle)
|
if (state != FiberStateIdle)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -229,74 +250,47 @@ public static class FiberExports
|
|||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, 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 ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, 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))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
ulong previousFiber;
|
|
||||||
lock (_fiberGate)
|
lock (_fiberGate)
|
||||||
{
|
{
|
||||||
_continuations[fiberAddress] = new FiberContinuation(
|
_continuations[fiberAddress] = new FiberContinuation(
|
||||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
argOnRunAddress);
|
argOnRunAddress);
|
||||||
|
|
||||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
var threadKey = GetThreadKey(ctx);
|
||||||
|
if (!_threadStates.TryGetValue(threadKey, out var threadState) ||
|
||||||
|
!TryWriteResumeArgument(ctx, threadState.RootContinuation, returnArgument))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousFiber = returnTarget.PreviousFiber;
|
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
if (previousFiber != 0)
|
|
||||||
{
|
{
|
||||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
|
||||||
{
|
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
|
||||||
return ctx.SetReturn(FiberErrorState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
transferTarget = threadState.RootContinuation.Context with { Rax = 0 };
|
||||||
}
|
_threadStates.TryRemove(threadKey, out _);
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!returnTarget.ThreadContinuation.HasValue ||
|
|
||||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
|
||||||
{
|
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
|
||||||
return ctx.SetReturn(FiberErrorState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
_ = GuestThreadExecution.EnterFiber(0);
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentFiberAddress = previousFiber;
|
|
||||||
_ = GuestThreadExecution.EnterFiber(previousFiber);
|
|
||||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
$"return-to-thread fiber=0x{fiberAddress: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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -309,18 +303,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -334,35 +328,35 @@ public static class FiberExports
|
|||||||
var info = ctx[CpuRegister.Rsi];
|
var info = ctx[CpuRegister.Rsi];
|
||||||
if (info == 0)
|
if (info == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -376,22 +370,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 ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -403,12 +397,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (ctx[CpuRegister.Rdi] != 0)
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -419,8 +413,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
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -433,17 +427,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
if (ResolveCurrentFiberAddress(ctx) == 0 ||
|
||||||
|
!_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
return TryWriteUInt64(ctx, outAddress, threadState.RootContinuation.Context.Rbp)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberInitializeCore(
|
private static int FiberInitializeCore(
|
||||||
@@ -455,69 +450,70 @@ public static class FiberExports
|
|||||||
ulong contextAddress,
|
ulong contextAddress,
|
||||||
ulong contextSize,
|
ulong contextSize,
|
||||||
ulong optParam,
|
ulong optParam,
|
||||||
uint flags)
|
uint flags,
|
||||||
|
uint buildVersion)
|
||||||
{
|
{
|
||||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fiber & 7) != 0 ||
|
if ((fiber & 7) != 0 ||
|
||||||
(contextAddress & 15) != 0 ||
|
(contextAddress & 15) != 0 ||
|
||||||
(optParam & 7) != 0)
|
(optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorRange);
|
return SetReturn(ctx, 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 ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
if (optParam != 0 &&
|
||||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
flags = ApplyInitializationFlags(
|
||||||
{
|
flags,
|
||||||
flags |= FiberFlagContextSizeCheck;
|
buildVersion,
|
||||||
}
|
Volatile.Read(ref _contextSizeCheck) != 0);
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
if (contextAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||||
@@ -531,8 +527,10 @@ public static class FiberExports
|
|||||||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
TraceFiber(
|
||||||
return ctx.SetReturn(0);
|
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
|
||||||
|
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberRunCore(
|
private static int FiberRunCore(
|
||||||
@@ -547,12 +545,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||||
@@ -560,7 +558,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 ctx.SetReturn(attachResult);
|
return SetReturn(ctx, attachResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,78 +566,96 @@ public static class FiberExports
|
|||||||
if ((isSwitch && previousFiber == 0) ||
|
if ((isSwitch && previousFiber == 0) ||
|
||||||
(!isSwitch && previousFiber != 0))
|
(!isSwitch && previousFiber != 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
if (previousFiber == fiber)
|
if (previousFiber == fiber)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, 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 ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
var resumed = false;
|
var resumed = false;
|
||||||
lock (_fiberGate)
|
lock (_fiberGate)
|
||||||
{
|
{
|
||||||
|
var threadKey = GetThreadKey(ctx);
|
||||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, 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 ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
FiberContinuation targetContinuation;
|
FiberContinuation targetContinuation = default;
|
||||||
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
||||||
{
|
{
|
||||||
targetContinuation = savedContinuation;
|
targetContinuation = savedContinuation;
|
||||||
resumed = true;
|
resumed = true;
|
||||||
}
|
}
|
||||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
var callerContinuation = new FiberContinuation(
|
var callerContinuation = new FiberContinuation(
|
||||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
outArgumentAddress);
|
outArgumentAddress);
|
||||||
|
|
||||||
|
if (!resumed)
|
||||||
|
{
|
||||||
|
var rootStackTop = _threadStates.TryGetValue(threadKey, out var existingThreadState)
|
||||||
|
? existingThreadState.RootContinuation.Context.Rsp
|
||||||
|
: callerContinuation.Context.Rsp;
|
||||||
|
if (!TryCreateInitialContinuation(
|
||||||
|
ctx,
|
||||||
|
fields,
|
||||||
|
argOnRun,
|
||||||
|
rootStackTop,
|
||||||
|
out targetContinuation))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||||
previousState != FiberStateRun ||
|
previousState != FiberStateRun ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations[previousFiber] = callerContinuation;
|
_continuations[previousFiber] = callerContinuation;
|
||||||
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
if (_threadStates.ContainsKey(threadKey))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
_threadStates[threadKey] = new FiberThreadState(callerContinuation, fiber, previousFiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(previousFiber, out _);
|
_continuations.TryRemove(previousFiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
}
|
}
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
else
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
{
|
||||||
|
_threadStates.TryRemove(threadKey, out _);
|
||||||
|
}
|
||||||
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed)
|
if (resumed)
|
||||||
@@ -648,36 +664,67 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = targetContinuation.Context with { Rax = 0 };
|
transferTarget = targetContinuation.Context with { Rax = 0 };
|
||||||
|
if (_threadStates.TryGetValue(threadKey, out var activeState))
|
||||||
|
{
|
||||||
|
_threadStates[threadKey] = activeState with
|
||||||
|
{
|
||||||
|
CurrentFiber = fiber,
|
||||||
|
PreviousFiber = previousFiber,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentFiberAddress = fiber;
|
|
||||||
_ = GuestThreadExecution.EnterFiber(fiber);
|
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateInitialContinuation(
|
private static bool TryCreateInitialContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
FiberFields fields,
|
FiberFields fields,
|
||||||
ulong argOnRun,
|
ulong argOnRun,
|
||||||
|
ulong rootStackTop,
|
||||||
out FiberContinuation continuation)
|
out FiberContinuation continuation)
|
||||||
{
|
{
|
||||||
continuation = default;
|
continuation = default;
|
||||||
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
|
ulong stackEnd;
|
||||||
|
if (fields.ContextAddress == 0)
|
||||||
|
{
|
||||||
|
if (rootStackTop < 32)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
// Contextless fibers are specified to execute on the root
|
||||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
// sceFiberRun stack. Keep the suspended import return record
|
||||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
// above the entry stack and use a synthetic transfer slot below it.
|
||||||
|
stackEnd = rootStackTop & ~15UL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (fields.ContextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCalculateInitialStackLayout(stackEnd, out var entryRsp, out var transferSlot))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!TryWriteUInt64(ctx, transferSlot, fields.Entry) ||
|
||||||
|
!TryWriteUInt64(ctx, entryRsp, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var setFpuRegisters = (fields.Flags & FiberFlagSetFpuRegs) != 0;
|
||||||
|
|
||||||
continuation = new FiberContinuation(
|
continuation = new FiberContinuation(
|
||||||
new GuestCpuContinuation(
|
new GuestCpuContinuation(
|
||||||
fields.Entry,
|
fields.Entry,
|
||||||
@@ -698,7 +745,12 @@ public static class FiberExports
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0),
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
setFpuRegisters ? InitialFpuControlWord : ctx.FpuControlWord,
|
||||||
|
setFpuRegisters ? InitialMxcsr : ctx.Mxcsr,
|
||||||
|
RestoreFullFpuState: setFpuRegisters),
|
||||||
0);
|
0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -708,7 +760,7 @@ public static class FiberExports
|
|||||||
FiberContinuation continuation,
|
FiberContinuation continuation,
|
||||||
ulong argument) =>
|
ulong argument) =>
|
||||||
continuation.ArgOnRunAddress == 0 ||
|
continuation.ArgOnRunAddress == 0 ||
|
||||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||||
|
|
||||||
private static GuestCpuContinuation CaptureContinuation(
|
private static GuestCpuContinuation CaptureContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -731,26 +783,99 @@ public static class FiberExports
|
|||||||
ctx[CpuRegister.Rdi],
|
ctx[CpuRegister.Rdi],
|
||||||
ctx[CpuRegister.R8],
|
ctx[CpuRegister.R8],
|
||||||
ctx[CpuRegister.R9],
|
ctx[CpuRegister.R9],
|
||||||
|
ctx[CpuRegister.R10],
|
||||||
|
ctx[CpuRegister.R11],
|
||||||
ctx[CpuRegister.R12],
|
ctx[CpuRegister.R12],
|
||||||
ctx[CpuRegister.R13],
|
ctx[CpuRegister.R13],
|
||||||
ctx[CpuRegister.R14],
|
ctx[CpuRegister.R14],
|
||||||
ctx[CpuRegister.R15]);
|
ctx[CpuRegister.R15],
|
||||||
|
ctx.FpuControlWord,
|
||||||
|
ctx.Mxcsr,
|
||||||
|
RestoreFullFpuState: false);
|
||||||
|
|
||||||
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
||||||
{
|
{
|
||||||
if (_currentFiberAddress != 0)
|
|
||||||
{
|
|
||||||
return _currentFiberAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
||||||
{
|
{
|
||||||
return GuestThreadExecution.CurrentFiberAddress;
|
return GuestThreadExecution.CurrentFiberAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState) &&
|
||||||
|
threadState.CurrentFiber != 0)
|
||||||
|
{
|
||||||
|
return threadState.CurrentFiber;
|
||||||
|
}
|
||||||
|
|
||||||
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong GetThreadKey(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
|
if (handle != 0)
|
||||||
|
{
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main guest thread does not always have a scheduler handle. Its
|
||||||
|
// ABI thread pointer is stable across native/managed transitions.
|
||||||
|
return ctx.FsBase != 0 ? ctx.FsBase : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ApplyInitializationFlags(
|
||||||
|
uint flags,
|
||||||
|
uint buildVersion,
|
||||||
|
bool contextSizeCheck)
|
||||||
|
{
|
||||||
|
if (buildVersion >= Firmware350BuildVersion)
|
||||||
|
{
|
||||||
|
flags |= FiberFlagSetFpuRegs;
|
||||||
|
}
|
||||||
|
if (contextSizeCheck)
|
||||||
|
{
|
||||||
|
flags |= FiberFlagContextSizeCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCalculateInitialStackLayout(
|
||||||
|
ulong stackEnd,
|
||||||
|
out ulong entryRsp,
|
||||||
|
out ulong transferSlot)
|
||||||
|
{
|
||||||
|
var alignedEnd = stackEnd & ~15UL;
|
||||||
|
if (alignedEnd < 2 * sizeof(ulong))
|
||||||
|
{
|
||||||
|
entryRsp = 0;
|
||||||
|
transferSlot = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRsp = alignedEnd - sizeof(ulong);
|
||||||
|
transferSlot = entryRsp - sizeof(ulong);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void RunFiberSelfChecks()
|
||||||
|
{
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(0, Firmware350BuildVersion - 1, false) == 0,
|
||||||
|
"Pre-3.50 fibers unexpectedly enable SetFpuRegs.");
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(0, Firmware350BuildVersion, false) == FiberFlagSetFpuRegs,
|
||||||
|
"3.50+ fibers must initialize x87/MXCSR control state.");
|
||||||
|
Debug.Assert(
|
||||||
|
ApplyInitializationFlags(1, Firmware350BuildVersion, true) ==
|
||||||
|
(1u | FiberFlagSetFpuRegs | FiberFlagContextSizeCheck),
|
||||||
|
"Fiber initialization discarded caller/internal flags.");
|
||||||
|
Debug.Assert(
|
||||||
|
TryCalculateInitialStackLayout(0x1017, out var rsp, out var slot) &&
|
||||||
|
rsp == 0x1008 && slot == 0x1000 && (rsp & 15) == 8,
|
||||||
|
"Initial fiber transfer slot does not produce SysV entry alignment.");
|
||||||
|
}
|
||||||
|
|
||||||
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
||||||
ResolveCurrentFiberAddress(ctx);
|
ResolveCurrentFiberAddress(ctx);
|
||||||
|
|
||||||
@@ -779,11 +904,11 @@ public static class FiberExports
|
|||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
@@ -839,8 +964,8 @@ public static class FiberExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||||
magicStart != FiberSignature0 ||
|
magicStart != FiberSignature0 ||
|
||||||
magicEnd != FiberSignature1)
|
magicEnd != FiberSignature1)
|
||||||
{
|
{
|
||||||
@@ -855,12 +980,12 @@ public static class FiberExports
|
|||||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||||
{
|
{
|
||||||
fields = default;
|
fields = default;
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -898,6 +1023,46 @@ public static class FiberExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||||
@@ -925,6 +1090,37 @@ public static class FiberExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
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))
|
||||||
@@ -946,9 +1142,10 @@ public static class FiberExports
|
|||||||
GuestCpuContinuation Context,
|
GuestCpuContinuation Context,
|
||||||
ulong ArgOnRunAddress);
|
ulong ArgOnRunAddress);
|
||||||
|
|
||||||
private readonly record struct FiberReturnTarget(
|
private sealed record FiberThreadState(
|
||||||
ulong PreviousFiber,
|
FiberContinuation RootContinuation,
|
||||||
FiberContinuation? ThreadContinuation);
|
ulong CurrentFiber,
|
||||||
|
ulong PreviousFiber);
|
||||||
|
|
||||||
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Font;
|
||||||
|
|
||||||
|
public static class FontExports
|
||||||
|
{
|
||||||
|
private static readonly object AllocationGate = new();
|
||||||
|
private static ulong _librarySelectionAddress;
|
||||||
|
private static ulong _rendererSelectionAddress;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "whrS4oksXc4",
|
||||||
|
ExportName = "sceFontMemoryInit",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int MemoryInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var descriptorAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var regionAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var regionSize = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
var interfaceAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var mspaceAddress = ctx[CpuRegister.R8];
|
||||||
|
var destroyCallback = ctx[CpuRegister.R9];
|
||||||
|
if (descriptorAddress == 0 ||
|
||||||
|
!TryWriteUInt32(ctx, descriptorAddress, 0x00000F00) ||
|
||||||
|
!TryWriteUInt32(ctx, descriptorAddress + 0x04, regionSize) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x08, regionAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x10, mspaceAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x18, interfaceAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x20, destroyCallback) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x28, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x30, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(descriptorAddress + 0x38, mspaceAddress))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oM+XCzVG3oM",
|
||||||
|
ExportName = "sceFontSelectLibraryFt",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFontFt")]
|
||||||
|
public static int SelectLibraryFt(CpuContext ctx) =>
|
||||||
|
ReturnSelection(ctx, ref _librarySelectionAddress, 0x38);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Xx974EW-QFY",
|
||||||
|
ExportName = "sceFontSelectRendererFt",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFontFt")]
|
||||||
|
public static int SelectRendererFt(CpuContext ctx) =>
|
||||||
|
ReturnSelection(ctx, ref _rendererSelectionAddress, 0x100);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "n590hj5Oe-k",
|
||||||
|
ExportName = "sceFontCreateLibraryWithEdition",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int CreateLibraryWithEdition(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F01);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WaSFJoRWXaI",
|
||||||
|
ExportName = "sceFontCreateRendererWithEdition",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int CreateRendererWithEdition(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cKYtVmeSTcw",
|
||||||
|
ExportName = "sceFontOpenFontSet",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontSet(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KXUpebrFk1U",
|
||||||
|
ExportName = "sceFontOpenFontMemory",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontMemory(CpuContext ctx) =>
|
||||||
|
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JzCH3SCFnAU",
|
||||||
|
ExportName = "sceFontOpenFontInstance",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int OpenFontInstance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var sourceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var setupHandle = ctx[CpuRegister.Rsi];
|
||||||
|
var outputAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (outputAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupHandle != 0)
|
||||||
|
{
|
||||||
|
return ctx.TryWriteUInt64(outputAddress, setupHandle)
|
||||||
|
? SetSuccess(ctx)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateOpaque(ctx, 0x100, out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceHandle != 0)
|
||||||
|
{
|
||||||
|
Span<byte> source = stackalloc byte[0x100];
|
||||||
|
if (ctx.Memory.TryRead(sourceHandle, source))
|
||||||
|
{
|
||||||
|
_ = ctx.Memory.TryWrite(handle, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteUInt16(ctx, handle, 0x0F02);
|
||||||
|
return ctx.TryWriteUInt64(outputAddress, handle)
|
||||||
|
? SetSuccess(ctx)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SsRbbCiWoGw",
|
||||||
|
ExportName = "sceFontSupportSystemFonts",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SupportSystemFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mz2iTY0MK4A",
|
||||||
|
ExportName = "sceFontSupportExternalFonts",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int SupportExternalFonts(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CUKn5pX-NVY",
|
||||||
|
ExportName = "sceFontAttachDeviceCacheBuffer",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int AttachDeviceCacheBuffer(CpuContext ctx) => SetSuccess(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IQtleGLL5pQ",
|
||||||
|
ExportName = "sceFontGetRenderCharGlyphMetrics",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int GetRenderCharGlyphMetrics(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var metricsAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (metricsAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
|
||||||
|
for (var index = 0; index < values.Length; index++)
|
||||||
|
{
|
||||||
|
if (!TryWriteUInt32(
|
||||||
|
ctx,
|
||||||
|
metricsAddress + (ulong)(index * sizeof(float)),
|
||||||
|
BitConverter.SingleToUInt32Bits(values[index])))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gdUCnU0gHdI",
|
||||||
|
ExportName = "sceFontRenderSurfaceInit",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceFont")]
|
||||||
|
public static int RenderSurfaceInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var surfaceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var widthBytes = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
var pixelBytes = (uint)ctx[CpuRegister.Rcx] & 0xFF;
|
||||||
|
var width = (uint)ctx[CpuRegister.R8];
|
||||||
|
var height = (uint)ctx[CpuRegister.R9];
|
||||||
|
if (surfaceAddress == 0 ||
|
||||||
|
!ctx.TryWriteUInt64(surfaceAddress, bufferAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x08, widthBytes) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x0C, pixelBytes) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x10, width) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x14, height) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x18, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x1C, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x20, width) ||
|
||||||
|
!TryWriteUInt32(ctx, surfaceAddress + 0x24, height))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnSelection(CpuContext ctx, ref ulong selectionAddress, uint objectSize)
|
||||||
|
{
|
||||||
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (AllocationGate)
|
||||||
|
{
|
||||||
|
if (selectionAddress == 0)
|
||||||
|
{
|
||||||
|
if (!TryAllocateOpaque(ctx, 0x20, out selectionAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, selectionAddress, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, selectionAddress + 4, objectSize))
|
||||||
|
{
|
||||||
|
selectionAddress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = selectionAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CreateOpaqueHandle(CpuContext ctx, ulong outputAddress, int size, ushort magic)
|
||||||
|
{
|
||||||
|
if (outputAddress == 0 || !TryAllocateOpaque(ctx, size, out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteUInt16(ctx, handle, magic) || !ctx.TryWriteUInt64(outputAddress, handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetSuccess(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryAllocateOpaque(CpuContext ctx, int size, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
|
||||||
|
!allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[size];
|
||||||
|
bytes.Clear();
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetSuccess(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ internal sealed record GuestDrawTexture(
|
|||||||
bool IsStorage,
|
bool IsStorage,
|
||||||
uint MipLevels = 1,
|
uint MipLevels = 1,
|
||||||
uint MipLevel = 0,
|
uint MipLevel = 0,
|
||||||
|
uint BaseMipLevel = 0,
|
||||||
|
uint ResourceMipLevels = 1,
|
||||||
uint Pitch = 0,
|
uint Pitch = 0,
|
||||||
uint TileMode = 0,
|
uint TileMode = 0,
|
||||||
uint DstSelect = 0xFAC,
|
uint DstSelect = 0xFAC,
|
||||||
@@ -36,7 +38,11 @@ internal readonly record struct GuestSampler(
|
|||||||
|
|
||||||
internal sealed record GuestMemoryBuffer(
|
internal sealed record GuestMemoryBuffer(
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int Length,
|
||||||
|
bool Pooled,
|
||||||
|
bool Writable = false,
|
||||||
|
bool WriteBackToGuest = true);
|
||||||
|
|
||||||
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
|
||||||
internal sealed record GuestVertexBuffer(
|
internal sealed record GuestVertexBuffer(
|
||||||
@@ -47,11 +53,15 @@ internal sealed record GuestVertexBuffer(
|
|||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
uint Stride,
|
uint Stride,
|
||||||
uint OffsetBytes,
|
uint OffsetBytes,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int Length,
|
||||||
|
bool Pooled);
|
||||||
|
|
||||||
internal sealed record GuestIndexBuffer(
|
internal sealed record GuestIndexBuffer(
|
||||||
byte[] Data,
|
byte[] Data,
|
||||||
bool Is32Bit);
|
int Length,
|
||||||
|
bool Is32Bit,
|
||||||
|
bool Pooled);
|
||||||
|
|
||||||
internal readonly record struct GuestRect(
|
internal readonly record struct GuestRect(
|
||||||
int X,
|
int X,
|
||||||
@@ -67,6 +77,25 @@ internal readonly record struct GuestViewport(
|
|||||||
float MinDepth,
|
float MinDepth,
|
||||||
float MaxDepth);
|
float MaxDepth);
|
||||||
|
|
||||||
|
internal readonly record struct GuestRasterState(
|
||||||
|
bool CullFront,
|
||||||
|
bool CullBack,
|
||||||
|
bool FrontFaceClockwise,
|
||||||
|
bool Wireframe)
|
||||||
|
{
|
||||||
|
public static GuestRasterState Default { get; } = new(false, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareOp uses the GCN DB_DEPTH_CONTROL ZFUNC encoding, which matches the
|
||||||
|
// Vulkan CompareOp ordering (0=Never through 7=Always).
|
||||||
|
internal readonly record struct GuestDepthState(
|
||||||
|
bool TestEnable,
|
||||||
|
bool WriteEnable,
|
||||||
|
uint CompareOp)
|
||||||
|
{
|
||||||
|
public static GuestDepthState Default { get; } = new(false, false, 7);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
|
||||||
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
|
||||||
internal readonly record struct GuestBlendState(
|
internal readonly record struct GuestBlendState(
|
||||||
@@ -95,12 +124,16 @@ internal readonly record struct GuestBlendState(
|
|||||||
internal sealed record GuestRenderState(
|
internal sealed record GuestRenderState(
|
||||||
IReadOnlyList<GuestBlendState> Blends,
|
IReadOnlyList<GuestBlendState> Blends,
|
||||||
GuestRect? Scissor,
|
GuestRect? Scissor,
|
||||||
GuestViewport? Viewport)
|
GuestViewport? Viewport,
|
||||||
|
GuestRasterState Raster,
|
||||||
|
GuestDepthState Depth)
|
||||||
{
|
{
|
||||||
public static GuestRenderState Default { get; } = new(
|
public static GuestRenderState Default { get; } = new(
|
||||||
[GuestBlendState.Default],
|
[GuestBlendState.Default],
|
||||||
Scissor: null,
|
Scissor: null,
|
||||||
Viewport: null);
|
Viewport: null,
|
||||||
|
GuestRasterState.Default,
|
||||||
|
GuestDepthState.Default);
|
||||||
|
|
||||||
public GuestBlendState Blend =>
|
public GuestBlendState Blend =>
|
||||||
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
|
||||||
@@ -114,3 +147,17 @@ internal sealed record GuestRenderTarget(
|
|||||||
uint Format,
|
uint Format,
|
||||||
uint NumberType,
|
uint NumberType,
|
||||||
uint MipLevels = 1);
|
uint MipLevels = 1);
|
||||||
|
|
||||||
|
/// <summary>Guest DB surface bound alongside a color render target.</summary>
|
||||||
|
internal sealed record GuestDepthTarget(
|
||||||
|
ulong ReadAddress,
|
||||||
|
ulong WriteAddress,
|
||||||
|
uint Width,
|
||||||
|
uint Height,
|
||||||
|
uint GuestFormat,
|
||||||
|
uint SwizzleMode,
|
||||||
|
float ClearDepth,
|
||||||
|
bool ReadOnly)
|
||||||
|
{
|
||||||
|
public ulong Address => WriteAddress != 0 ? WriteAddress : ReadAddress;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ internal interface IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1);
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
int requiredVertexOutputCount = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
bool TryCompilePixelShader(
|
bool TryCompilePixelShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
@@ -44,7 +46,10 @@ internal interface IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1);
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
uint pixelInputEnable = 0,
|
||||||
|
uint pixelInputAddress = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
bool TryCompileComputeShader(
|
bool TryCompileComputeShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
@@ -53,7 +58,14 @@ internal interface IGuestGpuBackend
|
|||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
out IGuestCompiledShader? shader,
|
out IGuestCompiledShader? shader,
|
||||||
out string error);
|
out string error,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int initialScalarBufferIndex = -1,
|
||||||
|
uint waveLaneCount = 32,
|
||||||
|
ulong storageBufferOffsetAlignment = 1);
|
||||||
|
|
||||||
|
/// <summary>Returns the backend's no-color-output fragment shader.</summary>
|
||||||
|
IGuestCompiledShader GetDepthOnlyFragmentShader();
|
||||||
|
|
||||||
void HideSplashScreen();
|
void HideSplashScreen();
|
||||||
|
|
||||||
@@ -78,6 +90,21 @@ internal interface IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null);
|
GuestRenderState? renderState = null);
|
||||||
|
|
||||||
|
void SubmitDepthOnlyTranslatedDraw(
|
||||||
|
IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount,
|
||||||
|
GuestDepthTarget depthTarget,
|
||||||
|
IGuestCompiledShader? vertexShader = null,
|
||||||
|
uint vertexCount = 3,
|
||||||
|
uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4,
|
||||||
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
GuestRenderState? renderState = null,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitOffscreenTranslatedDraw(
|
void SubmitOffscreenTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
@@ -90,7 +117,9 @@ internal interface IGuestGpuBackend
|
|||||||
uint primitiveType = 4,
|
uint primitiveType = 4,
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null);
|
GuestRenderState? renderState = null,
|
||||||
|
GuestDepthTarget? depthTarget = null,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitStorageTranslatedDraw(
|
void SubmitStorageTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
@@ -98,16 +127,28 @@ internal interface IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint attributeCount,
|
uint attributeCount,
|
||||||
uint width,
|
uint width,
|
||||||
uint height);
|
uint height,
|
||||||
|
ulong shaderAddress = 0);
|
||||||
|
|
||||||
void SubmitComputeDispatch(
|
long SubmitComputeDispatch(
|
||||||
ulong shaderAddress,
|
ulong shaderAddress,
|
||||||
IGuestCompiledShader computeShader,
|
IGuestCompiledShader computeShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint groupCountX,
|
uint groupCountX,
|
||||||
uint groupCountY,
|
uint groupCountY,
|
||||||
uint groupCountZ);
|
uint groupCountZ,
|
||||||
|
uint baseGroupX,
|
||||||
|
uint baseGroupY,
|
||||||
|
uint baseGroupZ,
|
||||||
|
uint localSizeX,
|
||||||
|
uint localSizeY,
|
||||||
|
uint localSizeZ,
|
||||||
|
bool isIndirect,
|
||||||
|
bool writesGlobalMemory,
|
||||||
|
uint threadCountX = uint.MaxValue,
|
||||||
|
uint threadCountY = uint.MaxValue,
|
||||||
|
uint threadCountZ = uint.MaxValue);
|
||||||
|
|
||||||
bool TrySubmitGuestImage(
|
bool TrySubmitGuestImage(
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -115,6 +156,14 @@ internal interface IGuestGpuBackend
|
|||||||
uint height,
|
uint height,
|
||||||
uint pitchInPixel);
|
uint pitchInPixel);
|
||||||
|
|
||||||
|
bool TrySubmitOrderedGuestImageFlip(
|
||||||
|
int videoOutHandle,
|
||||||
|
int displayBufferIndex,
|
||||||
|
ulong address,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint pitchInPixel);
|
||||||
|
|
||||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||||
|
|
||||||
@@ -126,10 +175,12 @@ internal interface IGuestGpuBackend
|
|||||||
uint sourceWidth,
|
uint sourceWidth,
|
||||||
uint sourceHeight,
|
uint sourceHeight,
|
||||||
uint sourceFormat,
|
uint sourceFormat,
|
||||||
|
uint sourceNumberType,
|
||||||
ulong destinationAddress,
|
ulong destinationAddress,
|
||||||
uint destinationWidth,
|
uint destinationWidth,
|
||||||
uint destinationHeight,
|
uint destinationHeight,
|
||||||
uint destinationFormat);
|
uint destinationFormat,
|
||||||
|
uint destinationNumberType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the backend supports the guest render-target format, and how its pixel
|
/// Whether the backend supports the guest render-target format, and how its pixel
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ namespace SharpEmu.Libs.Gpu.Vulkan;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
||||||
{
|
{
|
||||||
|
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||||
|
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||||
|
|
||||||
public bool TryCompileVertexShader(
|
public bool TryCompileVertexShader(
|
||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
Gen5ShaderEvaluation evaluation,
|
Gen5ShaderEvaluation evaluation,
|
||||||
@@ -23,7 +26,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
int requiredVertexOutputCount = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
if (!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||||
@@ -34,7 +39,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase,
|
imageBindingBase,
|
||||||
scalarRegisterBufferIndex))
|
scalarRegisterBufferIndex,
|
||||||
|
requiredVertexOutputCount,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -52,7 +59,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0,
|
int imageBindingBase = 0,
|
||||||
int scalarRegisterBufferIndex = -1)
|
int scalarRegisterBufferIndex = -1,
|
||||||
|
uint pixelInputEnable = 0,
|
||||||
|
uint pixelInputAddress = 0,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
@@ -64,7 +74,10 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase,
|
imageBindingBase,
|
||||||
scalarRegisterBufferIndex))
|
scalarRegisterBufferIndex,
|
||||||
|
pixelInputEnable,
|
||||||
|
pixelInputAddress,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -80,7 +93,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint localSizeY,
|
uint localSizeY,
|
||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
out IGuestCompiledShader? shader,
|
out IGuestCompiledShader? shader,
|
||||||
out string error)
|
out string error,
|
||||||
|
int totalGlobalBufferCount = -1,
|
||||||
|
int initialScalarBufferIndex = -1,
|
||||||
|
uint waveLaneCount = 32,
|
||||||
|
ulong storageBufferOffsetAlignment = 1)
|
||||||
{
|
{
|
||||||
shader = null;
|
shader = null;
|
||||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
if (!Gen5SpirvTranslator.TryCompileComputeShader(
|
||||||
@@ -90,7 +107,11 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
localSizeY,
|
localSizeY,
|
||||||
localSizeZ,
|
localSizeZ,
|
||||||
out var compiled,
|
out var compiled,
|
||||||
out error))
|
out error,
|
||||||
|
totalGlobalBufferCount,
|
||||||
|
initialScalarBufferIndex,
|
||||||
|
waveLaneCount,
|
||||||
|
storageBufferOffsetAlignment))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -99,6 +120,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
|
||||||
|
DepthOnlyFragmentShader;
|
||||||
|
|
||||||
public void EnsureStarted(uint width, uint height) =>
|
public void EnsureStarted(uint width, uint height) =>
|
||||||
VulkanVideoPresenter.EnsureStarted(width, height);
|
VulkanVideoPresenter.EnsureStarted(width, height);
|
||||||
|
|
||||||
@@ -140,6 +164,35 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
vertexBuffers,
|
vertexBuffers,
|
||||||
renderState);
|
renderState);
|
||||||
|
|
||||||
|
public void SubmitDepthOnlyTranslatedDraw(
|
||||||
|
IGuestCompiledShader pixelShader,
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
|
uint attributeCount,
|
||||||
|
GuestDepthTarget depthTarget,
|
||||||
|
IGuestCompiledShader? vertexShader = null,
|
||||||
|
uint vertexCount = 3,
|
||||||
|
uint instanceCount = 1,
|
||||||
|
uint primitiveType = 4,
|
||||||
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
|
GuestRenderState? renderState = null,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
|
VulkanVideoPresenter.SubmitDepthOnlyTranslatedDraw(
|
||||||
|
Spirv(pixelShader),
|
||||||
|
textures,
|
||||||
|
globalMemoryBuffers,
|
||||||
|
attributeCount,
|
||||||
|
depthTarget,
|
||||||
|
vertexShader is null ? null : Spirv(vertexShader),
|
||||||
|
vertexCount,
|
||||||
|
instanceCount,
|
||||||
|
primitiveType,
|
||||||
|
indexBuffer,
|
||||||
|
vertexBuffers,
|
||||||
|
renderState,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitOffscreenTranslatedDraw(
|
public void SubmitOffscreenTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
@@ -152,7 +205,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint primitiveType = 4,
|
uint primitiveType = 4,
|
||||||
GuestIndexBuffer? indexBuffer = null,
|
GuestIndexBuffer? indexBuffer = null,
|
||||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||||
GuestRenderState? renderState = null) =>
|
GuestRenderState? renderState = null,
|
||||||
|
GuestDepthTarget? depthTarget = null,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
|
||||||
Spirv(pixelShader),
|
Spirv(pixelShader),
|
||||||
textures,
|
textures,
|
||||||
@@ -165,7 +220,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
primitiveType,
|
primitiveType,
|
||||||
indexBuffer,
|
indexBuffer,
|
||||||
vertexBuffers,
|
vertexBuffers,
|
||||||
renderState);
|
renderState,
|
||||||
|
depthTarget,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitStorageTranslatedDraw(
|
public void SubmitStorageTranslatedDraw(
|
||||||
IGuestCompiledShader pixelShader,
|
IGuestCompiledShader pixelShader,
|
||||||
@@ -173,23 +230,36 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint attributeCount,
|
uint attributeCount,
|
||||||
uint width,
|
uint width,
|
||||||
uint height) =>
|
uint height,
|
||||||
|
ulong shaderAddress = 0) =>
|
||||||
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
|
||||||
Spirv(pixelShader),
|
Spirv(pixelShader),
|
||||||
textures,
|
textures,
|
||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
attributeCount,
|
attributeCount,
|
||||||
width,
|
width,
|
||||||
height);
|
height,
|
||||||
|
shaderAddress);
|
||||||
|
|
||||||
public void SubmitComputeDispatch(
|
public long SubmitComputeDispatch(
|
||||||
ulong shaderAddress,
|
ulong shaderAddress,
|
||||||
IGuestCompiledShader computeShader,
|
IGuestCompiledShader computeShader,
|
||||||
IReadOnlyList<GuestDrawTexture> textures,
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||||
uint groupCountX,
|
uint groupCountX,
|
||||||
uint groupCountY,
|
uint groupCountY,
|
||||||
uint groupCountZ) =>
|
uint groupCountZ,
|
||||||
|
uint baseGroupX,
|
||||||
|
uint baseGroupY,
|
||||||
|
uint baseGroupZ,
|
||||||
|
uint localSizeX,
|
||||||
|
uint localSizeY,
|
||||||
|
uint localSizeZ,
|
||||||
|
bool isIndirect,
|
||||||
|
bool writesGlobalMemory,
|
||||||
|
uint threadCountX = uint.MaxValue,
|
||||||
|
uint threadCountY = uint.MaxValue,
|
||||||
|
uint threadCountZ = uint.MaxValue) =>
|
||||||
VulkanVideoPresenter.SubmitComputeDispatch(
|
VulkanVideoPresenter.SubmitComputeDispatch(
|
||||||
shaderAddress,
|
shaderAddress,
|
||||||
Spirv(computeShader),
|
Spirv(computeShader),
|
||||||
@@ -197,7 +267,18 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
globalMemoryBuffers,
|
globalMemoryBuffers,
|
||||||
groupCountX,
|
groupCountX,
|
||||||
groupCountY,
|
groupCountY,
|
||||||
groupCountZ);
|
groupCountZ,
|
||||||
|
baseGroupX,
|
||||||
|
baseGroupY,
|
||||||
|
baseGroupZ,
|
||||||
|
localSizeX,
|
||||||
|
localSizeY,
|
||||||
|
localSizeZ,
|
||||||
|
isIndirect,
|
||||||
|
writesGlobalMemory,
|
||||||
|
threadCountX,
|
||||||
|
threadCountY,
|
||||||
|
threadCountZ);
|
||||||
|
|
||||||
public bool TrySubmitGuestImage(
|
public bool TrySubmitGuestImage(
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -206,6 +287,21 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint pitchInPixel) =>
|
uint pitchInPixel) =>
|
||||||
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||||
|
|
||||||
|
public bool TrySubmitOrderedGuestImageFlip(
|
||||||
|
int videoOutHandle,
|
||||||
|
int displayBufferIndex,
|
||||||
|
ulong address,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
uint pitchInPixel) =>
|
||||||
|
VulkanVideoPresenter.TrySubmitOrderedGuestImageFlip(
|
||||||
|
videoOutHandle,
|
||||||
|
displayBufferIndex,
|
||||||
|
address,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pitchInPixel);
|
||||||
|
|
||||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
|
||||||
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||||
|
|
||||||
@@ -217,19 +313,23 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
|
|||||||
uint sourceWidth,
|
uint sourceWidth,
|
||||||
uint sourceHeight,
|
uint sourceHeight,
|
||||||
uint sourceFormat,
|
uint sourceFormat,
|
||||||
|
uint sourceNumberType,
|
||||||
ulong destinationAddress,
|
ulong destinationAddress,
|
||||||
uint destinationWidth,
|
uint destinationWidth,
|
||||||
uint destinationHeight,
|
uint destinationHeight,
|
||||||
uint destinationFormat) =>
|
uint destinationFormat,
|
||||||
|
uint destinationNumberType) =>
|
||||||
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
||||||
sourceAddress,
|
sourceAddress,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
sourceHeight,
|
sourceHeight,
|
||||||
sourceFormat,
|
sourceFormat,
|
||||||
|
sourceNumberType,
|
||||||
destinationAddress,
|
destinationAddress,
|
||||||
destinationWidth,
|
destinationWidth,
|
||||||
destinationHeight,
|
destinationHeight,
|
||||||
destinationFormat);
|
destinationFormat,
|
||||||
|
destinationNumberType);
|
||||||
|
|
||||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-resolution host sleeps for guest pacing. <see cref="Thread.Sleep(int)"/>
|
||||||
|
/// routinely overshoots by a scheduler quantum, which turns per-frame waits
|
||||||
|
/// (flip pacing, usleep-based game loops) into a hard frame-rate cap; these
|
||||||
|
/// helpers sleep coarsely for the bulk of the interval and yield-spin the
|
||||||
|
/// remainder so wakeups land within tens of microseconds of the target.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HostTiming
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks until <see cref="Stopwatch.GetTimestamp"/> reaches
|
||||||
|
/// <paramref name="targetTimestamp"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static void SleepUntil(long targetTimestamp)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var remainingTicks = targetTimestamp - Stopwatch.GetTimestamp();
|
||||||
|
if (remainingTicks <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTicks > Stopwatch.Frequency * 60)
|
||||||
|
{
|
||||||
|
// Far-future target: coarse sleep avoids overflowing the
|
||||||
|
// microsecond conversion below and needs no precision.
|
||||||
|
Thread.Sleep(30_000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingMicroseconds = remainingTicks * 1_000_000 / Stopwatch.Frequency;
|
||||||
|
if (remainingMicroseconds > 2200)
|
||||||
|
{
|
||||||
|
// Coarse sleep for the bulk; macOS overshoots ~0.5-1.5 ms.
|
||||||
|
Thread.Sleep((int)((remainingMicroseconds - 1200) / 1000));
|
||||||
|
}
|
||||||
|
else if (remainingMicroseconds > 1200)
|
||||||
|
{
|
||||||
|
// A 1 ms nap typically wakes within the margin and costs no
|
||||||
|
// CPU, unlike yield-spinning through the whole tail.
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
else if (remainingMicroseconds > 100)
|
||||||
|
{
|
||||||
|
Thread.Sleep(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.SpinWait(64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Blocks for the given number of microseconds.</summary>
|
||||||
|
public static void SleepMicroseconds(long microseconds)
|
||||||
|
{
|
||||||
|
if (microseconds <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microseconds >= 10_000_000)
|
||||||
|
{
|
||||||
|
// Long/sentinel sleeps (usleep(-1) parks): sub-millisecond
|
||||||
|
// precision is irrelevant and the tick conversion below would
|
||||||
|
// overflow, which used to turn the park into a hot spin.
|
||||||
|
Thread.Sleep((int)Math.Min(microseconds / 1000, int.MaxValue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticks = microseconds * Stopwatch.Frequency / 1_000_000;
|
||||||
|
SleepUntil(Stopwatch.GetTimestamp() + ticks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,34 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Json;
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
public static class JsonExports
|
public static class JsonExports
|
||||||
{
|
{
|
||||||
|
private const int ValueObjectSize = 0x20;
|
||||||
|
private const int StringObjectSize = 0x08;
|
||||||
|
private const ulong MaximumJsonBufferSize = 16 * 1024 * 1024;
|
||||||
|
private const int SceJsonParserErrorInvalidToken = unchecked((int)0x80920101);
|
||||||
|
private const int SceJsonParserErrorEmptyBuffer = unchecked((int)0x80920105);
|
||||||
|
|
||||||
|
private sealed record JsonValueState(JsonElement Element);
|
||||||
|
|
||||||
|
private sealed record JsonStringState(
|
||||||
|
string Value,
|
||||||
|
ulong GuestBufferAddress = 0,
|
||||||
|
int GuestBufferCapacity = 0);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, JsonValueState> _values = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, JsonStringState> _strings = new();
|
||||||
|
private static readonly JsonElement _nullElement = CreateNullElement();
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-hJRce8wn1U",
|
Nid = "-hJRce8wn1U",
|
||||||
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
|
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
|
||||||
@@ -78,9 +100,6 @@ public static class JsonExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
|
||||||
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
|
||||||
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "+drDFyAS6u4",
|
Nid = "+drDFyAS6u4",
|
||||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||||
@@ -91,26 +110,38 @@ public static class JsonExports
|
|||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
if (thisAddress == 0)
|
if (thisAddress == 0)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||||
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||||
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return SetReturn(ctx, 0);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "WSOuge5IsCg",
|
Nid = "WSOuge5IsCg",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2Constructor(CpuContext ctx)
|
public static int InitParameter2Constructor(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PS5 ABI object occupies 0x28 bytes in the caller's frame. Its
|
||||||
|
// setters below replace the allocator and file-buffer fields.
|
||||||
|
Span<byte> parameter = stackalloc byte[0x28];
|
||||||
|
parameter.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(thisAddress, parameter))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceJson("InitParameter2.ctor", thisAddress, 0);
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -118,12 +149,24 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "I2QC8PYhJWY",
|
Nid = "I2QC8PYhJWY",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2SetAllocator(CpuContext ctx)
|
public static int InitParameter2SetAllocator(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> fields = stackalloc byte[sizeof(ulong) * 2];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(fields, ctx[CpuRegister.Rsi]);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(fields[sizeof(ulong)..], ctx[CpuRegister.Rdx]);
|
||||||
|
if (!ctx.Memory.TryWrite(thisAddress, fields))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -131,12 +174,16 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Eu95jmqn5Rw",
|
Nid = "Eu95jmqn5Rw",
|
||||||
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
|
if (thisAddress == 0 || !ctx.TryWriteUInt64(thisAddress + 0x10, ctx[CpuRegister.Rsi]))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -144,14 +191,474 @@ public static class JsonExports
|
|||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IXW-z8pggfg",
|
Nid = "IXW-z8pggfg",
|
||||||
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceJson2")]
|
LibraryName = "libSceJson")]
|
||||||
public static int Initializer2Constructor(CpuContext ctx)
|
public static int InitializerInitialize2(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thisAddress = ctx[CpuRegister.Rdi];
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
|
var initParameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thisAddress == 0 || initParameterAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceJson("Initializer.initialize2", thisAddress, initParameterAddress);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int ValueConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ConstructValue(ctx);
|
||||||
|
return JsonValueExports.ValueDefaultConstructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-wa17B7TGnw",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueC2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBaseConstructor(CpuContext ctx) => ConstructValue(ctx);
|
||||||
|
public static int ValueDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = DestroyValue(ctx);
|
||||||
|
return JsonValueExports.ValueDestructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0eUrW9JAxM0",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueD2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueBaseDestructor(CpuContext ctx) => DestroyValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "S5JxQnoGF3E",
|
||||||
|
ExportName = "_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ParserParseBuffer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferSize = ctx[CpuRegister.Rdx];
|
||||||
|
if (valueAddress == 0 || bufferAddress == 0 || bufferSize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorEmptyBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferSize > MaximumJsonBufferSize || bufferSize > int.MaxValue)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[(int)bufferSize];
|
||||||
|
if (!ctx.Memory.TryRead(bufferAddress, buffer))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(buffer);
|
||||||
|
var element = document.RootElement.Clone();
|
||||||
|
StoreValue(ctx, valueAddress, element);
|
||||||
|
TraceJsonText("Parser.parse", valueAddress, Encoding.UTF8.GetString(buffer));
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SHtAad20YYM",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value7getTypeEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetType(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var element = GetValue(valueAddress);
|
||||||
|
ctx[CpuRegister.Rax] = (ulong)GetValueType(element);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RBw+4NukeGQ",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value5countEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueCount(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var element = GetValue(ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = element.ValueKind switch
|
||||||
|
{
|
||||||
|
System.Text.Json.JsonValueKind.Array => (ulong)element.GetArrayLength(),
|
||||||
|
System.Text.Json.JsonValueKind.Object => (ulong)element.EnumerateObject().Count(),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zTwZdI8AZ5Y",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value10getBooleanEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetBoolean(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DIxvoy7Ngvk",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value10getIntegerEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetInteger(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sn4HNCtNRzY",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value11getUIntegerEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetUnsignedInteger(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3qrge7L-AU4",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value7getRealEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetReal(CpuContext ctx) => ReturnValueStorage(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HwDt5lD9Bfo",
|
||||||
|
ExportName = "_ZNK3sce4Json5ValueixEPKc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIndexCString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var keyAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryReadUtf8CString(ctx, keyAddress, 4096, out var key) ||
|
||||||
|
!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = GetValue(valueAddress);
|
||||||
|
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Object &&
|
||||||
|
parent.TryGetProperty(key, out var property)
|
||||||
|
? property.Clone()
|
||||||
|
: _nullElement;
|
||||||
|
StoreValue(ctx, childAddress, child);
|
||||||
|
ctx[CpuRegister.Rax] = childAddress;
|
||||||
|
TraceJsonText("Value.index", valueAddress, key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XlWbvieLj2M",
|
||||||
|
ExportName = "_ZNK3sce4Json5ValueixEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueIndexPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0YqYAoO-+Uo",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value8getValueEm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueGetPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4zrm6VrgIAw",
|
||||||
|
ExportName = "_ZN3sce4Json5ValueaSERKS1_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueAssignment(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (destinationAddress != 0)
|
||||||
|
{
|
||||||
|
StoreValue(ctx, destinationAddress, GetValue(sourceAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destinationAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int StringConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ConstructString(ctx);
|
||||||
|
return JsonValueExports.StringDefaultConstructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "eG9E9M6XvTM",
|
||||||
|
ExportName = "_ZN3sce4Json6StringC2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringBaseConstructor(CpuContext ctx) => ConstructString(ctx);
|
||||||
|
public static int StringDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = DestroyString(ctx);
|
||||||
|
return JsonValueExports.StringDestructor(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ui7YFnSTCBw",
|
||||||
|
ExportName = "_ZN3sce4Json6StringD2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringBaseDestructor(CpuContext ctx) => DestroyString(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ncel8t2Rrpc",
|
||||||
|
ExportName = "_ZNK3sce4Json5Value8toStringERNS0_6StringE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int ValueToString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var stringAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (stringAddress != 0)
|
||||||
|
{
|
||||||
|
var element = GetValue(valueAddress);
|
||||||
|
var value = element.ValueKind == System.Text.Json.JsonValueKind.String
|
||||||
|
? element.GetString() ?? string.Empty
|
||||||
|
: element.GetRawText();
|
||||||
|
_strings[stringAddress] = new JsonStringState(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "L1KAkYWml-M",
|
||||||
|
ExportName = "_ZNK3sce4Json6String5c_strEv",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int StringCStr(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var stringAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (!_strings.TryGetValue(stringAddress, out var state))
|
||||||
|
{
|
||||||
|
state = new JsonStringState(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(state.Value + '\0');
|
||||||
|
var guestBufferAddress = state.GuestBufferAddress;
|
||||||
|
if (guestBufferAddress == 0 || state.GuestBufferCapacity < bytes.Length)
|
||||||
|
{
|
||||||
|
if (!TryAllocateGuestObject(ctx, bytes.Length, out guestBufferAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(guestBufferAddress, bytes))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_strings[stringAddress] = state with
|
||||||
|
{
|
||||||
|
GuestBufferAddress = guestBufferAddress,
|
||||||
|
GuestBufferCapacity = bytes.Length,
|
||||||
|
};
|
||||||
|
ctx.TryWriteUInt64(stringAddress, guestBufferAddress);
|
||||||
|
ctx[CpuRegister.Rax] = guestBufferAddress;
|
||||||
|
TraceJsonText("String.c_str", stringAddress, state.Value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ConstructValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
StoreValue(ctx, thisAddress, _nullElement);
|
||||||
ctx[CpuRegister.Rax] = thisAddress;
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnIndexedValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var valueAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var position = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = GetValue(valueAddress);
|
||||||
|
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Array &&
|
||||||
|
position < (ulong)parent.GetArrayLength()
|
||||||
|
? parent[(int)position].Clone()
|
||||||
|
: _nullElement;
|
||||||
|
StoreValue(ctx, childAddress, child);
|
||||||
|
ctx[CpuRegister.Rax] = childAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnValueStorage(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress == 0 ? 0 : thisAddress + 0x10;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DestroyValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
_values.TryRemove(thisAddress, out _);
|
||||||
|
if (thisAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> empty = stackalloc byte[ValueObjectSize];
|
||||||
|
empty.Clear();
|
||||||
|
ctx.Memory.TryWrite(thisAddress, empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ConstructString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_strings[thisAddress] = new JsonStringState(string.Empty);
|
||||||
|
ctx.TryWriteUInt64(thisAddress, 0);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DestroyString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
_strings.TryRemove(thisAddress, out _);
|
||||||
|
if (thisAddress != 0)
|
||||||
|
{
|
||||||
|
ctx.TryWriteUInt64(thisAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement CreateNullElement()
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse("null");
|
||||||
|
return document.RootElement.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement GetValue(ulong address) =>
|
||||||
|
address != 0 && _values.TryGetValue(address, out var state)
|
||||||
|
? state.Element
|
||||||
|
: _nullElement;
|
||||||
|
|
||||||
|
private static void StoreValue(CpuContext ctx, ulong address, JsonElement element)
|
||||||
|
{
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clone = element.Clone();
|
||||||
|
_values[address] = new JsonValueState(clone);
|
||||||
|
|
||||||
|
Span<byte> mirror = stackalloc byte[ValueObjectSize];
|
||||||
|
mirror.Clear();
|
||||||
|
var type = GetValueType(clone);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(mirror[0x1C..], type);
|
||||||
|
switch (clone.ValueKind)
|
||||||
|
{
|
||||||
|
case System.Text.Json.JsonValueKind.True:
|
||||||
|
mirror[0x10] = 1;
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number when clone.TryGetInt64(out var integer):
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(mirror[0x10..], integer);
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number when clone.TryGetUInt64(out var unsignedInteger):
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(mirror[0x10..], unsignedInteger);
|
||||||
|
break;
|
||||||
|
case System.Text.Json.JsonValueKind.Number:
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(
|
||||||
|
mirror[0x10..],
|
||||||
|
BitConverter.DoubleToInt64Bits(clone.GetDouble()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Memory.TryWrite(address, mirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetValueType(JsonElement element) => element.ValueKind switch
|
||||||
|
{
|
||||||
|
System.Text.Json.JsonValueKind.True or System.Text.Json.JsonValueKind.False => 1,
|
||||||
|
System.Text.Json.JsonValueKind.Number when element.TryGetInt64(out _) => 2,
|
||||||
|
System.Text.Json.JsonValueKind.Number when element.TryGetUInt64(out _) => 3,
|
||||||
|
System.Text.Json.JsonValueKind.Number => 4,
|
||||||
|
System.Text.Json.JsonValueKind.String => 5,
|
||||||
|
System.Text.Json.JsonValueKind.Array => 6,
|
||||||
|
System.Text.Json.JsonValueKind.Object => 7,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool TryAllocateGuestObject(CpuContext ctx, int size, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
return size > 0 &&
|
||||||
|
ctx.Memory is IGuestMemoryAllocator allocator &&
|
||||||
|
allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUtf8CString(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong address,
|
||||||
|
int maximumLength,
|
||||||
|
out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maximumLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[maximumLength];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
||||||
@@ -164,4 +671,16 @@ public static class JsonExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
|
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceJsonText(string operation, ulong thisAddress, string value)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var preview = value.Length <= 128 ? value : value[..128];
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} value={preview}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// 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.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
@@ -34,45 +34,6 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
|
||||||
{
|
|
||||||
public required CpuContext Ctx { get; init; }
|
|
||||||
public required EventFlagState State { get; init; }
|
|
||||||
public required ulong Pattern { get; init; }
|
|
||||||
public required uint WaitMode { get; init; }
|
|
||||||
public required ulong ResultAddress { get; init; }
|
|
||||||
public bool Timed { get; init; }
|
|
||||||
|
|
||||||
// Timed-wait completion state; unused when Timed is false.
|
|
||||||
public ulong TimeoutAddress { get; init; }
|
|
||||||
public long DeadlineTimestamp { get; init; }
|
|
||||||
|
|
||||||
public OrbisGen2Result? Result { get; set; }
|
|
||||||
|
|
||||||
// Untimed waits stash the prepared result here at wake and return it at resume.
|
|
||||||
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
|
|
||||||
public int Resume() => Timed
|
|
||||||
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
|
||||||
: (int)_blockedResult;
|
|
||||||
|
|
||||||
public bool TryWake()
|
|
||||||
{
|
|
||||||
if (Timed)
|
|
||||||
{
|
|
||||||
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_blockedResult = preparedResult;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -91,17 +52,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
optionAddress != 0 ||
|
optionAddress != 0 ||
|
||||||
!IsValidAttributes(attributes))
|
!IsValidAttributes(attributes))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
@@ -115,11 +76,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
{
|
{
|
||||||
_eventFlags.TryRemove(handle, out _);
|
_eventFlags.TryRemove(handle, out _);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,7 +93,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -141,7 +102,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -156,7 +117,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -167,7 +128,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -181,7 +142,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -190,7 +151,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -207,29 +168,29 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return SetReturn(ctx, 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,18 +210,18 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Enter(state.Gate);
|
Monitor.Enter(state.Gate);
|
||||||
@@ -268,65 +229,64 @@ 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 ctx.SetReturn(immediateWaitResult);
|
return SetReturn(ctx, immediateWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
// Timed waits block on a deadline instead of returning TIMED_OUT
|
||||||
{
|
// immediately; a zero-microsecond timeout still degrades to an
|
||||||
if (timeoutUsec == 0)
|
// instant poll because the deadline is already in the past.
|
||||||
{
|
var deadline = timeoutAddress != 0
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
: 0;
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
var hostDeadlineMs = timeoutAddress != 0
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
? Environment.TickCount64 + (timeoutUsec == 0
|
||||||
}
|
? 0L
|
||||||
|
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
: long.MaxValue;
|
||||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
|
||||||
var timedWaiter = new EventFlagWaiter
|
|
||||||
{
|
|
||||||
Ctx = ctx,
|
|
||||||
State = state,
|
|
||||||
Pattern = pattern,
|
|
||||||
WaitMode = waitMode,
|
|
||||||
ResultAddress = resultAddress,
|
|
||||||
Timed = true,
|
|
||||||
TimeoutAddress = timeoutAddress,
|
|
||||||
DeadlineTimestamp = deadline,
|
|
||||||
};
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitEventFlag",
|
|
||||||
GetEventFlagWakeKey(handle),
|
|
||||||
timedWaiter,
|
|
||||||
blockDeadlineTimestamp: deadline))
|
|
||||||
{
|
|
||||||
state.WaitingThreads++;
|
|
||||||
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
|
||||||
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
|
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
var satisfied = false;
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
new EventFlagWaiter
|
() =>
|
||||||
{
|
{
|
||||||
Ctx = ctx,
|
if (satisfied)
|
||||||
State = state,
|
{
|
||||||
Pattern = pattern,
|
return (int)blockedWaitResult;
|
||||||
WaitMode = waitMode,
|
}
|
||||||
ResultAddress = resultAddress,
|
|
||||||
});
|
// Deadline expiry: report timeout with the current bits.
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!TryPrepareBlockedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedWaitResult = preparedResult;
|
||||||
|
satisfied = true;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deadline);
|
||||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
if (!requestedBlock)
|
if (!requestedBlock)
|
||||||
@@ -334,7 +294,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is null)
|
if (scheduler is null)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -359,10 +319,21 @@ 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 ctx.SetReturn(pumpedWaitResult);
|
return SetReturn(ctx, pumpedWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
var remaining = hostDeadlineMs - Environment.TickCount64;
|
||||||
|
if (timeoutAddress != 0 && remaining <= 0)
|
||||||
|
{
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
releaseWaiter = false;
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -376,7 +347,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -396,24 +367,26 @@ 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (waiterCountAddress != 0 &&
|
if (waiterCountAddress != 0 &&
|
||||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Bits = setPattern;
|
state.Bits = setPattern;
|
||||||
state.WaitingThreads = 0;
|
state.WaitingThreads = 0;
|
||||||
Monitor.PulseAll(state.Gate);
|
Monitor.PulseAll(state.Gate);
|
||||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
TraceEventFlag(
|
||||||
|
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
|
||||||
|
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidAttributes(uint attributes)
|
private static bool IsValidAttributes(uint attributes)
|
||||||
@@ -509,98 +482,90 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCompleteBlockedTimedWait(
|
|
||||||
CpuContext ctx,
|
|
||||||
EventFlagState state,
|
|
||||||
EventFlagWaiter waiter,
|
|
||||||
ulong pattern,
|
|
||||||
uint waitMode,
|
|
||||||
ulong resultAddress)
|
|
||||||
{
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is not null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CompleteBlockedTimedWait(
|
|
||||||
CpuContext ctx,
|
|
||||||
EventFlagState state,
|
|
||||||
EventFlagWaiter waiter,
|
|
||||||
ulong pattern,
|
|
||||||
uint waitMode,
|
|
||||||
ulong resultAddress,
|
|
||||||
ulong timeoutAddress,
|
|
||||||
long deadlineTimestamp)
|
|
||||||
{
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is null)
|
|
||||||
{
|
|
||||||
if (IsSatisfied(state.Bits, pattern, waitMode))
|
|
||||||
{
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
ApplyClearMode(state, pattern, waitMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
|
||||||
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
|
||||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
|
||||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
|
||||||
var remainingMicros = remainingTicks <= 0
|
|
||||||
? 0u
|
|
||||||
: (uint)Math.Min(
|
|
||||||
uint.MaxValue,
|
|
||||||
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)waiter.Result.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
$"event_flag:0x{handle:X16}";
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
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 bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = buffer[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
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))
|
||||||
@@ -684,7 +649,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadByte(address, out var value))
|
if (TryReadByte(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X2}");
|
builder.Append($" {name}=0x{value:X2}");
|
||||||
}
|
}
|
||||||
@@ -692,7 +657,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt32(address, out var value))
|
if (TryReadUInt32(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X8}");
|
builder.Append($" {name}=0x{value:X8}");
|
||||||
}
|
}
|
||||||
@@ -700,7 +665,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt64(address, out var value))
|
if (TryReadUInt64(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X16}");
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,13 +395,13 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -649,6 +649,60 @@ public static class KernelEventQueueCompatExports
|
|||||||
return triggeredCount;
|
return triggeredCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queues one event for every registration using <paramref name="filter"/>.
|
||||||
|
/// Unlike <see cref="TriggerRegisteredEvents"/>, this preserves distinct
|
||||||
|
/// event identifiers registered on the same queue. AGC driver completion
|
||||||
|
/// queues use this form because the driver, rather than a packet-provided
|
||||||
|
/// identifier, announces that the whole submission reached end-of-pipe.
|
||||||
|
/// </summary>
|
||||||
|
public static int TriggerRegisteredEventsDistinct(short filter)
|
||||||
|
{
|
||||||
|
HashSet<ulong>? wakeHandles = null;
|
||||||
|
var triggeredCount = 0;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
foreach (var (handle, registrations) in _registeredEvents)
|
||||||
|
{
|
||||||
|
foreach (var registration in registrations.Values)
|
||||||
|
{
|
||||||
|
if (registration.Filter != filter)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new KernelEventDeque();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOrUpdateEvent(
|
||||||
|
queue,
|
||||||
|
new KernelQueuedEvent(
|
||||||
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
registration.Ident,
|
||||||
|
registration.UserData));
|
||||||
|
(wakeHandles ??= []).Add(handle);
|
||||||
|
triggeredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeHandles is not null)
|
||||||
|
{
|
||||||
|
foreach (var handle in wakeHandles)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggeredCount;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TriggerRegisteredEvent(
|
private static bool TriggerRegisteredEvent(
|
||||||
ulong handle,
|
ulong handle,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
@@ -752,7 +806,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong outCountAddress)
|
ulong outCountAddress)
|
||||||
{
|
{
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -865,8 +919,29 @@ public static class KernelEventQueueCompatExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,75 @@ public static class KernelExceptionCompatExports
|
|||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "il03nluKfMk",
|
||||||
|
ExportName = "sceKernelRaiseException",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int RaiseException(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var targetThread = ctx[CpuRegister.Rdi];
|
||||||
|
var exceptionType = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (targetThread == 0 || exceptionType is < 0 or >= 128)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong handler;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_installedHandlers.TryGetValue(exceptionType, out handler))
|
||||||
|
{
|
||||||
|
// The kernel accepts a raise for a type with no process
|
||||||
|
// handler; there is simply no user callback to deliver.
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_GUEST_EXCEPTIONS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
string? error = null;
|
||||||
|
if (scheduler is null ||
|
||||||
|
!scheduler.TryRaiseGuestException(
|
||||||
|
ctx,
|
||||||
|
targetThread,
|
||||||
|
handler,
|
||||||
|
exceptionType,
|
||||||
|
out error))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] sceKernelRaiseException delivery failed: " +
|
||||||
|
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
|
||||||
|
$"error={error ?? "scheduler unavailable"}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_EXCEPTIONS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] guest_exception.raise " +
|
||||||
|
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
|
||||||
|
$"handler=0x{handler:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ public static class KernelExports
|
|||||||
private static readonly object _coredumpGate = new();
|
private static readonly object _coredumpGate = new();
|
||||||
private static ulong _coredumpHandler;
|
private static ulong _coredumpHandler;
|
||||||
private static ulong _coredumpHandlerContext;
|
private static ulong _coredumpHandlerContext;
|
||||||
private const uint Gen4CompiledSdkVersion = 0x05000000;
|
|
||||||
private const uint Gen5CompiledSdkVersion = 0x09000000;
|
|
||||||
|
|
||||||
private readonly record struct CxaDestructorEntry(
|
private readonly record struct CxaDestructorEntry(
|
||||||
ulong Function,
|
ulong Function,
|
||||||
@@ -28,24 +26,7 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var versionAddress = ctx[CpuRegister.Rdi];
|
_ = ctx;
|
||||||
if (versionAddress == 0)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
|
|
||||||
? Gen5CompiledSdkVersion
|
|
||||||
: Gen4CompiledSdkVersion;
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,20 +52,12 @@ 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) => RequestProcessExit(ctx, "exit");
|
public static int Exit(CpuContext ctx)
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "L1SBTkC+Cvw",
|
|
||||||
ExportName = "abort",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libc")]
|
|
||||||
public static int Abort(CpuContext ctx)
|
|
||||||
{
|
{
|
||||||
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +172,11 @@ 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) => RequestProcessExit(ctx, "_exit");
|
public static int UnderscoreExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -218,12 +195,14 @@ public static class KernelExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadCreate(CpuContext ctx)
|
public static int PthreadCreate(CpuContext ctx)
|
||||||
|
=> PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
|
||||||
|
|
||||||
|
private static int PthreadCreateCore(CpuContext ctx, ulong nameAddress)
|
||||||
{
|
{
|
||||||
var threadIdAddress = ctx[CpuRegister.Rdi];
|
var threadIdAddress = ctx[CpuRegister.Rdi];
|
||||||
var attrAddress = ctx[CpuRegister.Rsi];
|
var attrAddress = ctx[CpuRegister.Rsi];
|
||||||
var entryAddress = ctx[CpuRegister.Rdx];
|
var entryAddress = ctx[CpuRegister.Rdx];
|
||||||
var argument = ctx[CpuRegister.Rcx];
|
var argument = ctx[CpuRegister.Rcx];
|
||||||
var nameAddress = ctx[CpuRegister.R8];
|
|
||||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||||
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
||||||
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
||||||
@@ -278,14 +257,20 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_create",
|
ExportName = "pthread_create",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreateCore(ctx, nameAddress: 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Jmi+9w9u0E4",
|
Nid = "Jmi+9w9u0E4",
|
||||||
ExportName = "pthread_create_name_np",
|
ExportName = "pthread_create_name_np",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3kg7rT0NQIs",
|
Nid = "3kg7rT0NQIs",
|
||||||
@@ -295,6 +280,9 @@ public static class KernelExports
|
|||||||
public static int PthreadExit(CpuContext ctx)
|
public static int PthreadExit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = ctx[CpuRegister.Rdi];
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
// Run cleanup on the still-executable thread before unwinding it.
|
||||||
|
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
|
||||||
|
KernelMemoryCompatExports.RunThreadDtors(ctx);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
||||||
ctx[CpuRegister.Rax] = value;
|
ctx[CpuRegister.Rax] = value;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -308,6 +296,8 @@ public static class KernelExports
|
|||||||
public static int PosixPthreadExit(CpuContext ctx)
|
public static int PosixPthreadExit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var value = ctx[CpuRegister.Rdi];
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
|
||||||
|
KernelMemoryCompatExports.RunThreadDtors(ctx);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
||||||
ctx[CpuRegister.Rax] = value;
|
ctx[CpuRegister.Rax] = value;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -362,7 +352,10 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_join",
|
ExportName = "pthread_join",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
public static int PosixPthreadJoin(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadJoin(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "wuCroIGjt2g",
|
Nid = "wuCroIGjt2g",
|
||||||
@@ -437,14 +430,21 @@ public static class KernelExports
|
|||||||
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
|
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
|
||||||
{
|
{
|
||||||
Span<byte> buf = stackalloc byte[maxLen];
|
Span<byte> buf = stackalloc byte[maxLen];
|
||||||
if (!ctx.Memory.TryRead(address, buf))
|
Span<byte> one = stackalloc byte[1];
|
||||||
return $"<unreadable 0x{address:X16}>";
|
var len = 0;
|
||||||
|
while (len < buf.Length)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)len, one))
|
||||||
|
return len == 0 ? $"<unreadable 0x{address:X16}>" : System.Text.Encoding.UTF8.GetString(buf[..len]);
|
||||||
|
|
||||||
int len = 0;
|
if (one[0] == 0)
|
||||||
while (len < buf.Length && buf[len] != 0) len++;
|
break;
|
||||||
|
|
||||||
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
|
buf[len++] = one[0];
|
||||||
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
|
}
|
||||||
|
|
||||||
|
try { return System.Text.Encoding.UTF8.GetString(buf[..len]); }
|
||||||
|
catch { return System.Text.Encoding.ASCII.GetString(buf[..len]); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldTracePthread()
|
private static bool ShouldTracePthread()
|
||||||
@@ -452,19 +452,18 @@ 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)
|
[SysAbiExport(
|
||||||
|
Nid = "L1SBTkC+Cvw",
|
||||||
|
ExportName = "abort",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Abort(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||||
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
|
||||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
|
||||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
|
||||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
|
||||||
// shadowing them breaks every C++ static-init guard in the game.
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,662 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Positional and scatter/gather file I/O, synchronization, renaming, fcntl,
|
||||||
|
/// polling and asynchronous I/O (AIO) that complement the base file exports.
|
||||||
|
/// These share the same guest FD table and path-resolution helpers via the
|
||||||
|
/// partial <see cref="KernelMemoryCompatExports"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public static partial class KernelMemoryCompatExports
|
||||||
|
{
|
||||||
|
// fcntl commands (FreeBSD numbering used by the PS4/PS5 libc).
|
||||||
|
private const int F_DUPFD = 0;
|
||||||
|
private const int F_GETFD = 1;
|
||||||
|
private const int F_SETFD = 2;
|
||||||
|
private const int F_GETFL = 3;
|
||||||
|
private const int F_SETFL = 4;
|
||||||
|
|
||||||
|
// poll event bits.
|
||||||
|
private const short POLLIN = 0x0001;
|
||||||
|
private const short POLLOUT = 0x0004;
|
||||||
|
|
||||||
|
// AIO request state (SCE_KERNEL_AIO_STATE_*).
|
||||||
|
private const uint AioStateCompleted = 3;
|
||||||
|
private const int SizeofAioRwRequest = 40;
|
||||||
|
|
||||||
|
private static long _nextAioSubmitId = 1;
|
||||||
|
private static readonly ConcurrentDictionary<uint, int> _aioResults = new();
|
||||||
|
|
||||||
|
private static FileStream? GetOpenFile(int fd)
|
||||||
|
{
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
return _openFiles.TryGetValue(fd, out var stream) ? stream : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Positional read/write (do not move the file offset) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelPread(CpuContext ctx) => KernelPreadCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelPreadCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||||
|
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||||
|
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
|
int read;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
read = RandomAccess.Read(stream.SafeFileHandle, buffer, offset);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelPwriteCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||||
|
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||||
|
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requested == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
|
if (!ctx.Memory.TryRead(bufferAddress, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RandomAccess.Write(stream.SafeFileHandle, payload, offset);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Synchronization ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFsync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFsyncCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
if (fd is 0 or 1 or 2)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Flush(flushToDisk: true);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "Y2OqwJQ3lr8", ExportName = "sync",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixSync(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
foreach (var stream in _openFiles.Values)
|
||||||
|
{
|
||||||
|
try { stream.Flush(flushToDisk: true); } catch (IOException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Truncation ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFtruncateCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var length = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (length < 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.SetLength(length);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelTruncateCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var length = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (length < 0 || !TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = ResolveGuestPath(guestPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||||
|
stream.SetLength(length);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Rename ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelRename(CpuContext ctx) => KernelRenameCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelRenameCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rdi], MaxGuestStringLength, out var fromGuest) ||
|
||||||
|
!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rsi], MaxGuestStringLength, out var toGuest))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReadOnlyGuestMutationPath(fromGuest) || IsReadOnlyGuestMutationPath(toGuest))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromHost = ResolveGuestPath(fromGuest);
|
||||||
|
var toHost = ResolveGuestPath(toGuest);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(fromHost))
|
||||||
|
{
|
||||||
|
Directory.Move(fromHost, toHost);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(fromHost, toHost, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateNegativeStatCacheForPathAndAncestors(toGuest);
|
||||||
|
AddNegativeStatCacheForGuestPath(fromGuest);
|
||||||
|
InvalidateAprFileSizeCache(fromHost);
|
||||||
|
InvalidateAprFileSizeCache(toHost);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Descriptor duplication ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "iiQjzvfWDq0", ExportName = "dup",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixDup(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(fd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSIX dup shares the open file description (and offset), which is
|
||||||
|
// exactly the shared FileStream reference.
|
||||||
|
var newFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "wdUufa9g-D8", ExportName = "dup2",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixDup2(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var oldFd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var newFd = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(oldFd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFd == newFd)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If newFd names an open file, dup2 closes it first.
|
||||||
|
if (_openFiles.TryGetValue(newFd, out var existing) && !ReferenceEquals(existing, stream))
|
||||||
|
{
|
||||||
|
try { existing.Dispose(); } catch (IOException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- fcntl ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
|
||||||
|
|
||||||
|
private static int KernelFcntlCore(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var command = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var argument = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case F_DUPFD:
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
if (!_openFiles.TryGetValue(fd, out var stream))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newFd = Math.Max((int)Interlocked.Increment(ref _nextFileDescriptor), argument);
|
||||||
|
_openFiles[newFd] = stream;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
case F_GETFD:
|
||||||
|
case F_GETFL:
|
||||||
|
// No close-on-exec / status flags are tracked; report cleared.
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
case F_SETFD:
|
||||||
|
case F_SETFL:
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- poll / select ----
|
||||||
|
// Regular files are always ready for both read and write, so report every
|
||||||
|
// requested descriptor as immediately ready.
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ku7D4q1Y9PI", ExportName = "poll",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixPoll(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var fdsAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var count = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (fdsAddress == 0 || count == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// struct pollfd { int fd; short events; short revents; } == 8 bytes.
|
||||||
|
var ready = 0;
|
||||||
|
Span<byte> buffer = stackalloc byte[8];
|
||||||
|
for (uint i = 0; i < count && i < 4096; i++)
|
||||||
|
{
|
||||||
|
var entry = fdsAddress + i * 8;
|
||||||
|
if (!ctx.Memory.TryRead(entry, buffer))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = BinaryPrimitives.ReadInt16LittleEndian(buffer[4..]);
|
||||||
|
var revents = (short)(events & (POLLIN | POLLOUT));
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(buffer[6..], revents);
|
||||||
|
if (revents != 0)
|
||||||
|
{
|
||||||
|
ready++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ctx.Memory.TryWrite(entry, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)ready);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "T8fER+tIGgk", ExportName = "select",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int PosixSelect(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// nfds in Rdi; the fd_sets are left as-is (all reported ready) and the
|
||||||
|
// ready count returned is nfds so callers proceed without blocking.
|
||||||
|
var nfds = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)Math.Max(nfds, 0));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Asynchronous I/O (executed synchronously) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "HgX7+AORI58", ExportName = "sceKernelAioSubmitReadCommands",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitReadCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "lXT0m3P-vs4", ExportName = "sceKernelAioSubmitReadCommandsMultiple",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitReadCommandsMultiple(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "XQ8C8y+de+E", ExportName = "sceKernelAioSubmitWriteCommands",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioSubmitWriteCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: true);
|
||||||
|
|
||||||
|
private static int KernelAioSubmit(CpuContext ctx, bool write)
|
||||||
|
{
|
||||||
|
var requestsAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var outIdAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (requestsAddress == 0 || count <= 0 || count > 0x10000)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each SceKernelAioRWRequest: offset(8) nbyte(8) buf(8) result(8) fd(4).
|
||||||
|
Span<byte> request = stackalloc byte[SizeofAioRwRequest];
|
||||||
|
Span<byte> result = stackalloc byte[16];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var entry = requestsAddress + (ulong)(i * SizeofAioRwRequest);
|
||||||
|
if (!ctx.Memory.TryRead(entry, request))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = BinaryPrimitives.ReadInt64LittleEndian(request[0..]);
|
||||||
|
var nbyte = (int)Math.Min(BinaryPrimitives.ReadUInt64LittleEndian(request[8..]), int.MaxValue);
|
||||||
|
var buf = BinaryPrimitives.ReadUInt64LittleEndian(request[16..]);
|
||||||
|
var resultPtr = BinaryPrimitives.ReadUInt64LittleEndian(request[24..]);
|
||||||
|
var fd = BinaryPrimitives.ReadInt32LittleEndian(request[32..]);
|
||||||
|
|
||||||
|
long transferred = KernelAioTransfer(ctx, fd, offset, nbyte, buf, write);
|
||||||
|
if (resultPtr != 0)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(result[0..], transferred);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(result[8..], AioStateCompleted);
|
||||||
|
_ = ctx.Memory.TryWrite(resultPtr, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var submitId = unchecked((uint)Interlocked.Increment(ref _nextAioSubmitId));
|
||||||
|
_aioResults[submitId] = 0;
|
||||||
|
if (outIdAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> idBuffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(idBuffer, submitId);
|
||||||
|
_ = ctx.Memory.TryWrite(outIdAddress, idBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long KernelAioTransfer(CpuContext ctx, int fd, long offset, int nbyte, ulong buf, bool write)
|
||||||
|
{
|
||||||
|
if (nbyte <= 0 || buf == 0 || offset < 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = GetOpenFile(fd);
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scratch = GC.AllocateUninitializedArray<byte>(nbyte);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(buf, scratch))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccess.Write(stream.SafeFileHandle, scratch, offset);
|
||||||
|
return nbyte;
|
||||||
|
}
|
||||||
|
|
||||||
|
var read = RandomAccess.Read(stream.SafeFileHandle, scratch, offset);
|
||||||
|
if (read > 0 && !ctx.Memory.TryWrite(buf, scratch.AsSpan(0, read)))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "o7O4z3jwKzo", ExportName = "sceKernelAioPollRequests",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioPollRequests(CpuContext ctx) => KernelAioComplete(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "lgK+oIWkJyA", ExportName = "sceKernelAioWaitRequests",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioWaitRequests(CpuContext ctx) => KernelAioComplete(ctx);
|
||||||
|
|
||||||
|
private static int KernelAioComplete(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Submission already performed the I/O synchronously, so every request
|
||||||
|
// reports completed. Rsi points at the state-out array, Rdx = count.
|
||||||
|
var statesAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (statesAddress != 0 && count > 0 && count <= 0x10000)
|
||||||
|
{
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
_ = ctx.Memory.TryWrite(statesAddress + (ulong)(i * sizeof(uint)), state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "fR521KIGgb8", ExportName = "sceKernelAioCancelRequest",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioCancelRequest(CpuContext ctx)
|
||||||
|
{
|
||||||
|
// Already completed; report the completed state if an out pointer given.
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (stateAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
|
||||||
|
_ = ctx.Memory.TryWrite(stateAddress, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "5TgME6AYty4", ExportName = "sceKernelAioDeleteRequest",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
|
||||||
|
public static int KernelAioDeleteRequest(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var submitId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
_aioResults.TryRemove(submitId, out _);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,16 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelModuleRegistry
|
public static class KernelModuleRegistry
|
||||||
{
|
{
|
||||||
|
public enum ModuleStartState
|
||||||
|
{
|
||||||
|
NotStarted,
|
||||||
|
Starting,
|
||||||
|
Started,
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
|
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
|
||||||
|
private static readonly Dictionary<int, Dictionary<string, ulong>> _symbolsByHandle = new();
|
||||||
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
|
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
|
||||||
@@ -32,6 +40,11 @@ public static class KernelModuleRegistry
|
|||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
ulong EndAddress,
|
ulong EndAddress,
|
||||||
ulong EntryPoint,
|
ulong EntryPoint,
|
||||||
|
ulong InitEntryPoint,
|
||||||
|
ulong EhFrameHeaderAddress,
|
||||||
|
ulong EhFrameAddress,
|
||||||
|
ulong EhFrameSize,
|
||||||
|
ModuleStartState StartState,
|
||||||
bool IsMain,
|
bool IsMain,
|
||||||
bool IsSystemModule);
|
bool IsSystemModule);
|
||||||
|
|
||||||
@@ -40,6 +53,7 @@ public static class KernelModuleRegistry
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
_modulesByHandle.Clear();
|
_modulesByHandle.Clear();
|
||||||
|
_symbolsByHandle.Clear();
|
||||||
_handleByPath.Clear();
|
_handleByPath.Clear();
|
||||||
_handleByName.Clear();
|
_handleByName.Clear();
|
||||||
_sysmoduleHandleById.Clear();
|
_sysmoduleHandleById.Clear();
|
||||||
@@ -52,6 +66,10 @@ public static class KernelModuleRegistry
|
|||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
|
ulong initEntryPoint,
|
||||||
|
ulong ehFrameHeaderAddress,
|
||||||
|
ulong ehFrameAddress,
|
||||||
|
ulong ehFrameSize,
|
||||||
bool isMain,
|
bool isMain,
|
||||||
bool isSystemModule = false)
|
bool isSystemModule = false)
|
||||||
{
|
{
|
||||||
@@ -67,6 +85,10 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress = baseAddress,
|
BaseAddress = baseAddress,
|
||||||
EndAddress = ComputeEnd(baseAddress, size),
|
EndAddress = ComputeEnd(baseAddress, size),
|
||||||
EntryPoint = entryPoint,
|
EntryPoint = entryPoint,
|
||||||
|
InitEntryPoint = initEntryPoint,
|
||||||
|
EhFrameHeaderAddress = ehFrameHeaderAddress,
|
||||||
|
EhFrameAddress = ehFrameAddress,
|
||||||
|
EhFrameSize = ehFrameSize,
|
||||||
IsMain = existing.IsMain || isMain,
|
IsMain = existing.IsMain || isMain,
|
||||||
IsSystemModule = existing.IsSystemModule || isSystemModule,
|
IsSystemModule = existing.IsSystemModule || isSystemModule,
|
||||||
};
|
};
|
||||||
@@ -84,6 +106,11 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress: baseAddress,
|
BaseAddress: baseAddress,
|
||||||
EndAddress: ComputeEnd(baseAddress, size),
|
EndAddress: ComputeEnd(baseAddress, size),
|
||||||
EntryPoint: entryPoint,
|
EntryPoint: entryPoint,
|
||||||
|
InitEntryPoint: initEntryPoint,
|
||||||
|
EhFrameHeaderAddress: ehFrameHeaderAddress,
|
||||||
|
EhFrameAddress: ehFrameAddress,
|
||||||
|
EhFrameSize: ehFrameSize,
|
||||||
|
StartState: ModuleStartState.NotStarted,
|
||||||
IsMain: isMain,
|
IsMain: isMain,
|
||||||
IsSystemModule: isSystemModule);
|
IsSystemModule: isSystemModule);
|
||||||
_modulesByHandle[handle] = entry;
|
_modulesByHandle[handle] = entry;
|
||||||
@@ -119,6 +146,11 @@ public static class KernelModuleRegistry
|
|||||||
BaseAddress: 0,
|
BaseAddress: 0,
|
||||||
EndAddress: 0,
|
EndAddress: 0,
|
||||||
EntryPoint: 0,
|
EntryPoint: 0,
|
||||||
|
InitEntryPoint: 0,
|
||||||
|
EhFrameHeaderAddress: 0,
|
||||||
|
EhFrameAddress: 0,
|
||||||
|
EhFrameSize: 0,
|
||||||
|
StartState: ModuleStartState.Started,
|
||||||
IsMain: false,
|
IsMain: false,
|
||||||
IsSystemModule: isSystemModule);
|
IsSystemModule: isSystemModule);
|
||||||
_modulesByHandle[handle] = entry;
|
_modulesByHandle[handle] = entry;
|
||||||
@@ -203,6 +235,97 @@ public static class KernelModuleRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically claims a module initializer. A module can be observed while
|
||||||
|
/// it is starting (for recursive loader calls), but its DT_INIT routine is
|
||||||
|
/// executed at most once after a successful start.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryBeginModuleStart(int handle, out ModuleEntry module)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.TryGetValue(handle, out module))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.StartState != ModuleStartState.NotStarted)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.InitEntryPoint < 0x10000)
|
||||||
|
{
|
||||||
|
module = module with { StartState = ModuleStartState.Started };
|
||||||
|
_modulesByHandle[handle] = module;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module = module with { StartState = ModuleStartState.Starting };
|
||||||
|
_modulesByHandle[handle] = module;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CompleteModuleStart(int handle, bool succeeded)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.TryGetValue(handle, out var module) ||
|
||||||
|
module.StartState != ModuleStartState.Starting)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_modulesByHandle[handle] = module with
|
||||||
|
{
|
||||||
|
StartState = succeeded ? ModuleStartState.Started : ModuleStartState.NotStarted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterModuleSymbols(int handle, IReadOnlyDictionary<string, ulong> symbols)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(symbols);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!_modulesByHandle.ContainsKey(handle))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_symbolsByHandle.TryGetValue(handle, out var destination))
|
||||||
|
{
|
||||||
|
destination = new Dictionary<string, ulong>(StringComparer.Ordinal);
|
||||||
|
_symbolsByHandle[handle] = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (name, address) in symbols)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(name) && address >= 0x10000)
|
||||||
|
{
|
||||||
|
destination.TryAdd(name, address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryResolveModuleSymbol(int handle, string symbolName, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(symbolName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _symbolsByHandle.TryGetValue(handle, out var symbols) &&
|
||||||
|
symbols.TryGetValue(symbolName, out address) &&
|
||||||
|
address >= 0x10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
|
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
|
||||||
{
|
{
|
||||||
module = default;
|
module = default;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,10 @@
|
|||||||
// 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;
|
||||||
@@ -15,6 +17,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private const int DefaultDetachState = 0;
|
private const int DefaultDetachState = 0;
|
||||||
private const ulong DefaultGuardSize = 0x1000UL;
|
private const ulong DefaultGuardSize = 0x1000UL;
|
||||||
private const ulong DefaultStackSize = 0x1_00000UL;
|
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||||
|
private const ulong NativeGuestStackSize = 0x20_0000UL;
|
||||||
|
private const ulong NativeGuestStackStride = 0x100_0000UL;
|
||||||
private const int DefaultInheritSched = 4;
|
private const int DefaultInheritSched = 4;
|
||||||
private const int DefaultSchedPolicy = 1;
|
private const int DefaultSchedPolicy = 1;
|
||||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||||
@@ -223,6 +227,13 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+U1R4WtXvoc",
|
||||||
|
ExportName = "pthread_detach",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadDetach(CpuContext ctx) => PthreadDetach(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "How7B8Oet6k",
|
Nid = "How7B8Oet6k",
|
||||||
ExportName = "scePthreadGetname",
|
ExportName = "scePthreadGetname",
|
||||||
@@ -273,6 +284,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state.Attributes = state.Attributes with { AffinityMask = mask };
|
state.Attributes = state.Attributes with { AffinityMask = mask };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadAffinity(thread, mask);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -326,7 +338,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -354,6 +366,9 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
GetOrCreateThreadStateLocked(thread).Priority = priority;
|
GetOrCreateThreadStateLocked(thread).Priority = priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply to the live scheduler thread so runtime priority changes take
|
||||||
|
// effect, not just the local bookkeeping snapshot.
|
||||||
|
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadPriority(thread, priority);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -373,7 +388,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -408,9 +423,9 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public static int PthreadGetschedparam(CpuContext ctx)
|
public static int PthreadGetschedparam(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var thread = ctx[CpuRegister.Rdi];
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
var policyAddress = ctx[CpuRegister.Rsi];
|
||||||
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
var schedParamAddress = ctx[CpuRegister.Rdx];
|
||||||
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
if (thread == 0 || policyAddress == 0 || schedParamAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
@@ -424,8 +439,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = state.Priority;
|
priority = state.Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
if (!TryWriteInt32(ctx, policyAddress, policy) ||
|
||||||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
!TryWriteInt32(ctx, schedParamAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -528,6 +543,22 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
var threadState = GetOrCreateThreadStateLocked(thread);
|
var threadState = GetOrCreateThreadStateLocked(thread);
|
||||||
|
|
||||||
|
// The native executor maps guest pthread stacks itself, after the
|
||||||
|
// kernel-facing thread object has been created. Report that live
|
||||||
|
// mapping when a thread asks for its own attributes. IL2CPP's
|
||||||
|
// conservative collector uses these two fields to register the stack;
|
||||||
|
// returning the default null address lets it recycle objects that are
|
||||||
|
// still reachable only from guest registers/stack frames.
|
||||||
|
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
|
||||||
|
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
|
||||||
|
{
|
||||||
|
threadState.Attributes = threadState.Attributes with
|
||||||
|
{
|
||||||
|
StackAddress = stackAddress,
|
||||||
|
StackSize = NativeGuestStackSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
_attrStates[outAttrAddress] = threadState.Attributes;
|
_attrStates[outAttrAddress] = threadState.Attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +566,28 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
|
||||||
|
{
|
||||||
|
stackAddress = 0;
|
||||||
|
var candidate = stackPointer & ~(NativeGuestStackStride - 1);
|
||||||
|
if (stackPointer - candidate >= NativeGuestStackSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var highestStack = OperatingSystem.IsWindows()
|
||||||
|
? 0x00007FFF_F000_0000UL
|
||||||
|
: 0x00006FFF_F000_0000UL;
|
||||||
|
var lowestStack = highestStack - (63 * NativeGuestStackStride);
|
||||||
|
if (candidate < lowestStack || candidate > highestStack)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stackAddress = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8+s5BzZjxSg",
|
Nid = "8+s5BzZjxSg",
|
||||||
ExportName = "scePthreadAttrGetaffinity",
|
ExportName = "scePthreadAttrGetaffinity",
|
||||||
@@ -584,7 +637,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -827,7 +880,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -850,7 +903,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1207,7 +1260,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1315,6 +1368,72 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
|
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
|
||||||
|
|
||||||
|
private const int PthreadDestructorIterations = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the current thread's pthread TLS-key destructors, as POSIX
|
||||||
|
/// requires on thread exit. Each key holding a non-null value with a
|
||||||
|
/// registered destructor has its value cleared first and the destructor
|
||||||
|
/// then invoked with the previous value; this repeats up to
|
||||||
|
/// PTHREAD_DESTRUCTOR_ITERATIONS times so destructors that set new
|
||||||
|
/// thread-local values are themselves cleaned up. Called on the exiting
|
||||||
|
/// guest thread while it is still executable.
|
||||||
|
/// </summary>
|
||||||
|
public static void RunThreadLocalDestructors(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (scheduler is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var threadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
if (!_threadLocalSpecific.TryGetValue(threadHandle, out var values))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var iteration = 0; iteration < PthreadDestructorIterations; iteration++)
|
||||||
|
{
|
||||||
|
var ranAny = false;
|
||||||
|
foreach (var entry in values)
|
||||||
|
{
|
||||||
|
var value = entry.Value;
|
||||||
|
if (value == 0 ||
|
||||||
|
!_tlsKeys.TryGetValue(entry.Key, out var keyState) ||
|
||||||
|
keyState.Destructor == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear before invoking, per POSIX, so a destructor that
|
||||||
|
// re-sets the key is handled on the next iteration.
|
||||||
|
if (!values.TryUpdate(entry.Key, 0, value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranAny = true;
|
||||||
|
_ = scheduler.TryCallGuestFunction(
|
||||||
|
ctx,
|
||||||
|
keyState.Destructor,
|
||||||
|
value,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"pthread_tls_destructor",
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ranAny)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadLocalSpecific.TryRemove(threadHandle, out _);
|
||||||
|
}
|
||||||
|
|
||||||
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
|
||||||
{
|
{
|
||||||
if (rwlockAddress == 0)
|
if (rwlockAddress == 0)
|
||||||
@@ -1680,4 +1799,24 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +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.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -13,9 +14,6 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||||
private static int _nextSemaphoreHandle = 1;
|
private static int _nextSemaphoreHandle = 1;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static int _semaPollBackoffCount;
|
|
||||||
|
|
||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
@@ -25,35 +23,9 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public int WaitingThreads { get; set; }
|
public int WaitingThreads { get; set; }
|
||||||
public int CancelEpoch { get; set; }
|
|
||||||
public bool Deleted { get; set; }
|
|
||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
|
||||||
{
|
|
||||||
public required KernelSemaphoreState Semaphore { get; init; }
|
|
||||||
public required int NeedCount { get; init; }
|
|
||||||
public required int CancelEpochAtBlock { get; init; }
|
|
||||||
public bool Timed { get; init; }
|
|
||||||
|
|
||||||
// Timed-wait completion state; unused when Timed is false.
|
|
||||||
public CpuContext? Ctx { get; init; }
|
|
||||||
public ulong TimeoutAddress { get; init; }
|
|
||||||
public long DeadlineTimestamp { get; init; }
|
|
||||||
|
|
||||||
// Written and read only under the owning semaphore's Gate.
|
|
||||||
public int? Result { get; set; }
|
|
||||||
|
|
||||||
public int Resume() => Timed
|
|
||||||
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
|
||||||
: CompleteBlockedSemaWait(Semaphore, this);
|
|
||||||
|
|
||||||
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "188x57JYp0g",
|
Nid = "188x57JYp0g",
|
||||||
ExportName = "sceKernelCreateSema",
|
ExportName = "sceKernelCreateSema",
|
||||||
@@ -76,12 +48,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
initialCount > maxCount ||
|
initialCount > maxCount ||
|
||||||
optionAddress != 0)
|
optionAddress != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
@@ -90,7 +62,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = new KernelSemaphoreState
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
WakeKey = GetSemaphoreWakeKey(handle),
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
@@ -98,28 +70,15 @@ public static class KernelSemaphoreCompatExports
|
|||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
};
|
};
|
||||||
_semaphores[handle] = state;
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
// Handles are sequential and guest-predictable, so a hostile guest can
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
// race a WaitSema onto the handle between publication above and this
|
|
||||||
// rollback. Strand-proof that waiter exactly like DeleteSema does.
|
|
||||||
lock (state.Gate)
|
|
||||||
{
|
|
||||||
state.Deleted = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
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(
|
||||||
@@ -135,124 +94,143 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint timeoutUsec = 0;
|
||||||
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pollTimedOut = false;
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
|
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutMicros == 0)
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.WaitingThreads++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block cooperatively: the wake predicate atomically acquires the
|
||||||
|
// tokens (so a wake commits the acquisition), while the resume
|
||||||
|
// handler distinguishes a real acquisition from a deadline expiry.
|
||||||
|
var acquired = false;
|
||||||
|
var deadline = timeoutAddress != 0
|
||||||
|
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
bool WakePredicate()
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
lock (semaphore.Gate)
|
||||||
if (_traceSema)
|
|
||||||
{
|
{
|
||||||
|
if (semaphore.Count >= needCount)
|
||||||
|
{
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
acquired = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ResumeWait()
|
||||||
|
{
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acquired)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
|
||||||
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 (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
}
|
}
|
||||||
pollTimedOut = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
|
||||||
var timedWaiter = new SemaphoreWaiter
|
|
||||||
{
|
|
||||||
Semaphore = semaphore,
|
|
||||||
NeedCount = needCount,
|
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
|
||||||
Timed = true,
|
|
||||||
Ctx = ctx,
|
|
||||||
TimeoutAddress = timeoutAddress,
|
|
||||||
DeadlineTimestamp = deadline,
|
|
||||||
};
|
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
semaphore.WakeKey,
|
GetSemaphoreWakeKey(handle),
|
||||||
timedWaiter,
|
ResumeWait,
|
||||||
blockDeadlineTimestamp: deadline))
|
WakePredicate,
|
||||||
|
deadline))
|
||||||
{
|
{
|
||||||
semaphore.WaitingThreads++;
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
|
||||||
if (_traceSema)
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
// Not a guest thread (or no scheduler): fall back to a host-thread
|
||||||
// immediate-timeout poll the callers already tolerate.
|
// wait so the semantics still hold on non-cooperative callers.
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
}
|
||||||
pollTimedOut = true;
|
|
||||||
|
private static int WaitSemaphoreOnHostThread(
|
||||||
|
CpuContext ctx,
|
||||||
|
KernelSemaphoreState semaphore,
|
||||||
|
uint handle,
|
||||||
|
int needCount,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
uint timeoutUsec)
|
||||||
|
{
|
||||||
|
var deadlineMs = timeoutAddress != 0
|
||||||
|
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
|
||||||
|
: long.MaxValue;
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
TraceSemaphore(
|
||||||
|
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
|
||||||
|
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
|
||||||
|
while (semaphore.Count < needCount)
|
||||||
|
{
|
||||||
|
var remaining = deadlineMs - Environment.TickCount64;
|
||||||
|
if (timeoutAddress != 0 && remaining <= 0)
|
||||||
|
{
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore(
|
||||||
|
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pollTimedOut)
|
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
|
||||||
{
|
|
||||||
var waiter = new SemaphoreWaiter
|
|
||||||
{
|
|
||||||
Semaphore = semaphore,
|
|
||||||
NeedCount = needCount,
|
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
|
||||||
};
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitSema",
|
|
||||||
semaphore.WakeKey,
|
|
||||||
waiter))
|
|
||||||
{
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
|
|
||||||
if ((++_semaPollBackoffCount & 255) == 0)
|
|
||||||
{
|
|
||||||
Thread.Sleep(0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Thread.Yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "12wOHk8ywb0",
|
Nid = "12wOHk8ywb0",
|
||||||
@@ -263,31 +241,25 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
{
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,34 +272,31 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signalCount <= 0)
|
if (signalCount <= 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
if (_traceSema)
|
// Wake host-thread waiters parked in the fallback path.
|
||||||
{
|
Monitor.PulseAll(semaphore.Gate);
|
||||||
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} {FormatCallSite(ctx)}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
// Wake cooperatively-blocked guest threads; their wake predicate
|
||||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
// acquires the tokens atomically, so this respects the new count.
|
||||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
// waiter can proceed.
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -339,35 +308,29 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setCount > semaphore.MaxCount)
|
if (setCount > semaphore.MaxCount)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, 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 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
semaphore.CancelEpoch++;
|
semaphore.WaitingThreads = 0;
|
||||||
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
|
Monitor.PulseAll(semaphore.Gate);
|
||||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
|
||||||
// and silently absorb the increment of a waiter that parks between this
|
|
||||||
// gate release and the wake-all below.
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
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}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -380,138 +343,238 @@ 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete succeeds even with blocked waiters; they wake with the deleted
|
|
||||||
// result (the SCE kernel wakes waiters with the EACCES-class code).
|
|
||||||
lock (semaphore.Gate)
|
|
||||||
{
|
|
||||||
semaphore.Deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
|
[SysAbiExport(
|
||||||
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
|
Nid = "pDuPEf3m4fI",
|
||||||
// result and should be re-readied; false leaves it parked.
|
ExportName = "sem_init",
|
||||||
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var initialCountValue = ctx[CpuRegister.Rdx];
|
||||||
|
if (semaphoreAddress == 0 || initialCountValue > int.MaxValue)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialCount = unchecked((int)initialCountValue);
|
||||||
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
|
{
|
||||||
|
Name = $"posix@0x{semaphoreAddress:X16}",
|
||||||
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
|
InitialCount = initialCount,
|
||||||
|
MaxCount = int.MaxValue,
|
||||||
|
Count = initialCount,
|
||||||
|
};
|
||||||
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
|
{
|
||||||
|
_semaphores.TryRemove(handle, out _);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSemaphore($"posix-init address=0x{semaphoreAddress:X16} handle=0x{handle:X8} count={initialCount}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "YCV5dGGBcCo",
|
||||||
|
ExportName = "sem_wait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemWait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
ctx[CpuRegister.Rdx] = 0;
|
||||||
|
return KernelWaitSema(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WBWzsRifCEA",
|
||||||
|
ExportName = "sem_trywait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemTryWait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
return KernelPollSema(ctx, handle, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w5IHyvahg-o",
|
||||||
|
ExportName = "sem_timedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemTimedWait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeoutAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
ctx[CpuRegister.Rdx] = timeoutAddress;
|
||||||
|
return KernelWaitSema(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IKP8typ0QUk",
|
||||||
|
ExportName = "sem_post",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemPost(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
ctx[CpuRegister.Rsi] = 1;
|
||||||
|
return KernelSignalSema(ctx, handle, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Bq+LRV-N6Hk",
|
||||||
|
ExportName = "sem_getvalue",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemGetValue(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (valueAddress == 0 ||
|
||||||
|
!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle) ||
|
||||||
|
!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int count;
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
|
count = semaphore.Count;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
return TryWriteUInt32(ctx, valueAddress, unchecked((uint)count))
|
||||||
{
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
if (waiter.Result is not null)
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (semaphore.Deleted)
|
[SysAbiExport(
|
||||||
|
Nid = "cDW233RAwWo",
|
||||||
|
ExportName = "sem_destroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixSemDestroy(CpuContext ctx)
|
||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
if (!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle))
|
||||||
if (_traceSema)
|
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
|
|
||||||
{
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (semaphore.Count >= waiter.NeedCount)
|
|
||||||
{
|
|
||||||
semaphore.Count -= waiter.NeedCount;
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume handler: runs on the woken guest thread outside the scheduler gate;
|
|
||||||
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
|
|
||||||
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
|
||||||
{
|
|
||||||
lock (semaphore.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
|
||||||
{
|
|
||||||
// Nothing readies a parked semaphore waiter without the wake handler
|
|
||||||
// resolving it, so reaching here means the scheduler contract changed.
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return waiter.Result!.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CompleteBlockedTimedSemaWait(
|
|
||||||
CpuContext ctx,
|
|
||||||
KernelSemaphoreState semaphore,
|
|
||||||
SemaphoreWaiter waiter,
|
|
||||||
ulong timeoutAddress,
|
|
||||||
long deadlineTimestamp)
|
|
||||||
{
|
|
||||||
int result;
|
|
||||||
lock (semaphore.Gate)
|
|
||||||
{
|
|
||||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
|
||||||
{
|
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
|
||||||
if (_traceSema)
|
|
||||||
{
|
|
||||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = waiter.Result!.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rdi] = handle;
|
||||||
|
var result = KernelDeleteSema(ctx);
|
||||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
_ = TryWriteUInt32(ctx, semaphoreAddress, 0);
|
||||||
var remainingMicros = remainingTicks <= 0
|
|
||||||
? 0u
|
|
||||||
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
|
||||||
|
{
|
||||||
|
handle = 0;
|
||||||
|
return semaphoreAddress != 0 &&
|
||||||
|
TryReadUInt32(ctx, semaphoreAddress, out handle) &&
|
||||||
|
handle != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maxLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[i] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Call sites must check this before building the interpolated message; the trace
|
// Call sites must check this before building the interpolated message; the trace
|
||||||
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||||
private static readonly bool _traceSema =
|
private static readonly bool _traceSema =
|
||||||
@@ -521,4 +584,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatCallSite(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnAddress);
|
||||||
|
return $"guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnAddress:X16}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ public static class LibcStdioExports
|
|||||||
private const int MaxPathLength = 4096;
|
private const int MaxPathLength = 4096;
|
||||||
private const int MaxModeLength = 16;
|
private const int MaxModeLength = 16;
|
||||||
private const int ReadChunkSize = 1024 * 1024;
|
private const int ReadChunkSize = 1024 * 1024;
|
||||||
|
private const ulong GuestFileObjectSize = 0x100;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
||||||
private static long _nextHandle = 0x1000 - 8;
|
|
||||||
|
|
||||||
private static readonly bool _traceStdio =
|
private static readonly bool _traceStdio =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
||||||
@@ -96,7 +96,21 @@ public static class LibcStdioExports
|
|||||||
stream.Seek(0, SeekOrigin.End);
|
stream.Seek(0, SeekOrigin.End);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
|
// A FILE* can cross between HLE stdio and a title's bundled libc.
|
||||||
|
// Back the handle with zeroed guest memory so native helpers such
|
||||||
|
// as _Lockfilelock can safely inspect the FILE object instead of
|
||||||
|
// dereferencing the old small integer token (0x1000, 0x1008...).
|
||||||
|
if (!KernelMemoryCompatExports.TryAllocateHleData(
|
||||||
|
ctx,
|
||||||
|
GuestFileObjectSize,
|
||||||
|
alignment: 0x10,
|
||||||
|
out var handle))
|
||||||
|
{
|
||||||
|
stream.Dispose();
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||||
|
}
|
||||||
|
|
||||||
_fileHandles[handle] = stream;
|
_fileHandles[handle] = stream;
|
||||||
|
|
||||||
if (_traceStdio)
|
if (_traceStdio)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -12,13 +15,29 @@ public static class NetExports
|
|||||||
{
|
{
|
||||||
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
||||||
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
||||||
|
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
|
||||||
|
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
|
||||||
|
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
|
||||||
|
private const int NetErrnoBadFileDescriptor = 9;
|
||||||
|
private const int NetErrnoInvalidArgument = 22;
|
||||||
|
private const int NetErrnoWouldBlock = 35;
|
||||||
|
private const int NetErrnoAddressInUse = 48;
|
||||||
|
private const int NetErrnoNotInitialized = 200;
|
||||||
private const int MaxNameLength = 256;
|
private const int MaxNameLength = 256;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||||
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||||
|
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
|
||||||
private static int _nextPoolId;
|
private static int _nextPoolId;
|
||||||
private static int _nextResolverId = 0x2000;
|
private static int _nextResolverId = 0x2000;
|
||||||
private static bool _initialized;
|
private static int _nextSocketId = 0x4000;
|
||||||
|
// The platform networking module is usable immediately after it is loaded.
|
||||||
|
// Games and middleware (notably FMOD) can create internal sockets before an
|
||||||
|
// explicit sceNetInit call reaches application code.
|
||||||
|
private static bool _initialized = true;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static nint _errnoAddress;
|
||||||
|
|
||||||
private sealed record NetPool(string Name, int Size, int Flags);
|
private sealed record NetPool(string Name, int Size, int Flags);
|
||||||
|
|
||||||
@@ -46,10 +65,232 @@ public static class NetExports
|
|||||||
_initialized = false;
|
_initialized = false;
|
||||||
_pools.Clear();
|
_pools.Clear();
|
||||||
_resolvers.Clear();
|
_resolvers.Clear();
|
||||||
|
foreach (var socket in _sockets.Values)
|
||||||
|
{
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
_sockets.Clear();
|
||||||
TraceNet("term", 0, 0, 0, 0);
|
TraceNet("term", 0, 0, 0, 0);
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Q4qBuN-c0ZM",
|
||||||
|
ExportName = "sceNetSocket",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSocket(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var family = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var protocol = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||||
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
|
? value
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
if (!TryTranslateSocketParameters(family, type, protocol, out var addressFamily, out var socketType, out var protocolType))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var socket = new Socket(addressFamily, socketType, protocolType);
|
||||||
|
var id = Interlocked.Increment(ref _nextSocketId);
|
||||||
|
_sockets[id] = socket;
|
||||||
|
TraceNet("socket.create", id, unchecked((ulong)family), unchecked((ulong)type), unchecked((ulong)protocol));
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "45ggEzakPJQ",
|
||||||
|
ExportName = "sceNetSocketClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSocketClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryRemove(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Dispose();
|
||||||
|
TraceNet("socket.close", id, 0, 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2mKX2Spso7I",
|
||||||
|
ExportName = "sceNetSetsockopt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetSetsockopt(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var level = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var option = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var valueAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var valueLength = unchecked((int)ctx[CpuRegister.R8]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORBIS_NET_SOL_SOCKET / ORBIS_NET_SO_NBIO. This is the first option
|
||||||
|
// used by FMOD's discovery socket and maps directly to host blocking.
|
||||||
|
if (level == 0xFFFF && option == 0x1200)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
||||||
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Blocking = BinaryPrimitives.ReadInt32LittleEndian(value) == 0;
|
||||||
|
TraceNet("socket.nonblocking", id, socket.Blocking ? 0UL : 1UL, 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORBIS_NET_SO_REUSEADDR uses the BSD value 0x0004.
|
||||||
|
if (level == 0xFFFF && option == 0x0004)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(int)];
|
||||||
|
if (valueLength < value.Length || valueAddress == 0 || !ctx.Memory.TryRead(valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.SetSocketOption(
|
||||||
|
SocketOptionLevel.Socket,
|
||||||
|
SocketOptionName.ReuseAddress,
|
||||||
|
BinaryPrimitives.ReadInt32LittleEndian(value) != 0);
|
||||||
|
TraceNet("socket.reuseaddr", id, BinaryPrimitives.ReadUInt32LittleEndian(value), 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bErx49PgxyY",
|
||||||
|
ExportName = "sceNetBind",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetBind(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
if (!TryReadSocketAddress(ctx, ctx[CpuRegister.Rsi], unchecked((int)ctx[CpuRegister.Rdx]), out var endpoint))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socket.Bind(endpoint);
|
||||||
|
TraceNet("socket.bind", id, unchecked((ulong)endpoint.Port), 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorAddressInUse, NetErrnoAddressInUse);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kOj1HiAGE54",
|
||||||
|
ExportName = "sceNetListen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetListen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
socket.Listen(Math.Max(0, unchecked((int)ctx[CpuRegister.Rsi])));
|
||||||
|
TraceNet("socket.listen", id, ctx[CpuRegister.Rsi], 0, 0);
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PIWqhn9oSxc",
|
||||||
|
ExportName = "sceNetAccept",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetAccept(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_sockets.TryGetValue(id, out var socket))
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accepted = socket.Accept();
|
||||||
|
var acceptedId = Interlocked.Increment(ref _nextSocketId);
|
||||||
|
_sockets[acceptedId] = accepted;
|
||||||
|
TraceNet("socket.accept", acceptedId, unchecked((ulong)id), 0, 0);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)acceptedId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode is SocketError.WouldBlock or SocketError.IOPending)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorWouldBlock, NetErrnoWouldBlock);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HQOwnfMGipQ",
|
||||||
|
ExportName = "sceNetErrnoLoc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetErrnoLoc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (_errnoAddress == 0)
|
||||||
|
{
|
||||||
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
||||||
|
Marshal.WriteInt32(_errnoAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)_errnoAddress);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "dgJBaeJnGpo",
|
Nid = "dgJBaeJnGpo",
|
||||||
ExportName = "sceNetPoolCreate",
|
ExportName = "sceNetPoolCreate",
|
||||||
@@ -207,6 +448,69 @@ public static class NetExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetNetError(CpuContext ctx, int result, int errno)
|
||||||
|
{
|
||||||
|
if (_errnoAddress == 0)
|
||||||
|
{
|
||||||
|
_errnoAddress = Marshal.AllocHGlobal(sizeof(int));
|
||||||
|
}
|
||||||
|
Marshal.WriteInt32(_errnoAddress, errno);
|
||||||
|
return ctx.SetReturn(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryTranslateSocketParameters(
|
||||||
|
int family,
|
||||||
|
int type,
|
||||||
|
int protocol,
|
||||||
|
out AddressFamily addressFamily,
|
||||||
|
out SocketType socketType,
|
||||||
|
out ProtocolType protocolType)
|
||||||
|
{
|
||||||
|
addressFamily = family switch
|
||||||
|
{
|
||||||
|
2 => AddressFamily.InterNetwork,
|
||||||
|
28 => AddressFamily.InterNetworkV6,
|
||||||
|
_ => AddressFamily.Unspecified,
|
||||||
|
};
|
||||||
|
socketType = type switch
|
||||||
|
{
|
||||||
|
1 => SocketType.Stream,
|
||||||
|
2 => SocketType.Dgram,
|
||||||
|
_ => SocketType.Unknown,
|
||||||
|
};
|
||||||
|
protocolType = protocol switch
|
||||||
|
{
|
||||||
|
0 when socketType == SocketType.Stream => ProtocolType.Tcp,
|
||||||
|
0 when socketType == SocketType.Dgram => ProtocolType.Udp,
|
||||||
|
6 => ProtocolType.Tcp,
|
||||||
|
17 => ProtocolType.Udp,
|
||||||
|
_ => ProtocolType.Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return addressFamily != AddressFamily.Unspecified &&
|
||||||
|
socketType != SocketType.Unknown &&
|
||||||
|
protocolType != ProtocolType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSocketAddress(CpuContext ctx, ulong address, int length, out IPEndPoint endpoint)
|
||||||
|
{
|
||||||
|
endpoint = new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
if (address == 0 || length < 16)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[16];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes) || bytes[1] != 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = BinaryPrimitives.ReadUInt16BigEndian(bytes[2..4]);
|
||||||
|
endpoint = new IPEndPoint(new IPAddress(bytes[4..8]), port);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ public static class Ngs2Exports
|
|||||||
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||||
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||||
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||||
private const ulong HandleStorageSize = 0x1000;
|
private const ulong HandleStorageSize = 0x20;
|
||||||
private const int RenderBufferInfoSize = 0x18;
|
private const int RenderBufferInfoSize = 0x18;
|
||||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -38,13 +39,13 @@ public static class Ngs2Exports
|
|||||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -52,7 +53,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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -67,7 +68,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.Remove(handle))
|
if (!Systems.Remove(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rackHandles = Racks
|
var rackHandles = Racks
|
||||||
@@ -80,7 +81,7 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -97,19 +98,19 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -117,7 +118,7 @@ public static class Ngs2Exports
|
|||||||
Racks[handle] = new RackState(systemHandle, rackId);
|
Racks[handle] = new RackState(systemHandle, rackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,13 +133,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(handle))
|
if (!Racks.ContainsKey(handle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveRackLocked(handle);
|
RemoveRackLocked(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -155,7 +156,7 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Racks.ContainsKey(rackHandle))
|
if (!Racks.ContainsKey(rackHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing = Voices.FirstOrDefault(
|
var existing = Voices.FirstOrDefault(
|
||||||
@@ -163,20 +164,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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outHandleAddress == 0)
|
if (outHandleAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
return SetReturn(ctx, 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
@@ -184,7 +185,7 @@ public static class Ngs2Exports
|
|||||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -196,19 +197,12 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(
|
return SetReturn(
|
||||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
ctx,
|
||||||
? 0
|
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
|
||||||
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
|
||||||
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
|
||||||
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
|
||||||
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
|
||||||
// their real libraries; the NIDs below are verified against the hash.
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "AbYvTOZ8Pts",
|
Nid = "AbYvTOZ8Pts",
|
||||||
ExportName = "sceNgs2VoiceRunCommands",
|
ExportName = "sceNgs2VoiceRunCommands",
|
||||||
@@ -216,48 +210,6 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "-TOuuAQ-buE",
|
|
||||||
ExportName = "sceNgs2VoiceGetState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "rEh728kXk3w",
|
|
||||||
ExportName = "sceNgs2VoiceGetStateFlags",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "xa8oL9dmXkM",
|
|
||||||
ExportName = "sceNgs2PanInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "1WsleK-MTkE",
|
|
||||||
ExportName = "sceNgs2GeomCalcListener",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "0lbbayqDNoE",
|
|
||||||
ExportName = "sceNgs2GeomResetSourceParam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7Lcfo8SmpsU",
|
|
||||||
ExportName = "sceNgs2GeomResetListenerParam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNgs2")]
|
|
||||||
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
@@ -272,13 +224,13 @@ public static class Ngs2Exports
|
|||||||
{
|
{
|
||||||
if (!Systems.ContainsKey(systemHandle))
|
if (!Systems.ContainsKey(systemHandle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint i = 0; i < bufferInfoCount; i++)
|
for (uint i = 0; i < bufferInfoCount; i++)
|
||||||
@@ -287,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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,11 +258,138 @@ 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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pgFAiLR5qT4",
|
||||||
|
ExportName = "sceNgs2SystemQueryBufferSize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0eFLVCfWVds",
|
||||||
|
ExportName = "sceNgs2RackQueryBufferSize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2RackQueryBufferSize(CpuContext ctx) => WriteBufferSize(ctx, ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
// Report a fixed working-memory footprint for the requested object. The
|
||||||
|
// out struct (SceNgs2BufferAllocator-style) begins with the size field.
|
||||||
|
private static int WriteBufferSize(CpuContext ctx, ulong outAddress)
|
||||||
|
{
|
||||||
|
if (outAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[RenderBufferInfoSize];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[0..8], 0x10000);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[8..16], 0x100);
|
||||||
|
return ctx.Memory.TryWrite(outAddress, info)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "l4Q2dWEH6UM",
|
||||||
|
ExportName = "sceNgs2SystemSetGrainSamples",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemSetGrainSamples(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-tbc2SxQD60",
|
||||||
|
ExportName = "sceNgs2SystemSetSampleRate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemSetSampleRate(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gThZqM5PYlQ",
|
||||||
|
ExportName = "sceNgs2SystemLock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemLock(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JXRC5n0RQls",
|
||||||
|
ExportName = "sceNgs2SystemUnlock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2SystemUnlock(CpuContext ctx) => ValidateSystem(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-TOuuAQ-buE",
|
||||||
|
ExportName = "sceNgs2VoiceGetState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var voiceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var stateSize = (int)Math.Min(ctx[CpuRegister.Rdx], 0x400);
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Voices.ContainsKey(voiceHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report an idle (not-in-use) voice: all-zero state block.
|
||||||
|
if (stateAddress != 0 && stateSize > 0)
|
||||||
|
{
|
||||||
|
if (!TryClearGuestBuffer(ctx, stateAddress, (ulong)stateSize))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rEh728kXk3w",
|
||||||
|
ExportName = "sceNgs2VoiceGetStateFlags",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetStateFlags(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var voiceHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var flagsAddress = ctx[CpuRegister.Rsi];
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Voices.ContainsKey(voiceHandle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No flags set: voice is idle.
|
||||||
|
if (flagsAddress != 0 && !ctx.TryWriteUInt64(flagsAddress, 0))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateSystem(CpuContext ctx)
|
||||||
|
{
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
Systems.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
handle = 0;
|
||||||
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -318,18 +397,11 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||||
if (!ctx.Memory.TryWrite(handle, data))
|
return ctx.Memory.TryWrite(handle, data);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
|
||||||
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
|
||||||
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||||
@@ -367,4 +439,37 @@ 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;
|
||||||
|
}
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xa8oL9dmXkM",
|
||||||
|
ExportName = "sceNgs2PanInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1WsleK-MTkE",
|
||||||
|
ExportName = "sceNgs2GeomCalcListener",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0lbbayqDNoE",
|
||||||
|
ExportName = "sceNgs2GeomResetSourceParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Lcfo8SmpsU",
|
||||||
|
ExportName = "sceNgs2GeomResetListenerParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public static class NpManagerExports
|
|||||||
{
|
{
|
||||||
private const int NpTitleIdSize = 16;
|
private const int NpTitleIdSize = 16;
|
||||||
private const int NpTitleSecretSize = 128;
|
private const int NpTitleSecretSize = 128;
|
||||||
private const int NpReachabilityStateUnavailable = 0;
|
private const int NpErrorInvalidArgument = unchecked((int)0x80550003);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3Zl8BePTh9Y",
|
Nid = "3Zl8BePTh9Y",
|
||||||
@@ -58,35 +58,6 @@ public static class NpManagerExports
|
|||||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "rbknaUjpqWo",
|
|
||||||
ExportName = "sceNpGetAccountIdA",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpGetOnlineIdA(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// The A variant takes a user ID before OnlineId.
|
|
||||||
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "e-ZuhGEoeC4",
|
|
||||||
ExportName = "sceNpGetNpReachabilityState",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceNpManager")]
|
|
||||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (stateAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.TryWriteInt32(stateAddress, NpReachabilityStateUnavailable)
|
|
||||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "VfRSmPmj8Q8",
|
Nid = "VfRSmPmj8Q8",
|
||||||
ExportName = "sceNpRegisterStateCallback",
|
ExportName = "sceNpRegisterStateCallback",
|
||||||
@@ -140,6 +111,77 @@ public static class NpManagerExports
|
|||||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rbknaUjpqWo",
|
||||||
|
ExportName = "sceNpGetAccountIdA",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetAccountIdA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var accountIdAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || accountIdAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The offline profile exposed by sceNpGetState is signed in. Keep the
|
||||||
|
// account query consistent with that state: Unity's PSN integration
|
||||||
|
// treats SIGNED_OUT as an exceptional state and retries it every frame.
|
||||||
|
// A stable local-only id is sufficient for titles which only use the
|
||||||
|
// value as a profile key.
|
||||||
|
Span<byte> accountId = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(accountId, 1);
|
||||||
|
return ctx.Memory.TryWrite(accountIdAddress, accountId)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JT+t00a3TxA",
|
||||||
|
ExportName = "sceNpGetAccountCountryA",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetAccountCountryA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var countryAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || countryAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> country = stackalloc byte[4];
|
||||||
|
country[0] = (byte)'U';
|
||||||
|
country[1] = (byte)'S';
|
||||||
|
country[2] = 0;
|
||||||
|
country[3] = 0;
|
||||||
|
return ctx.Memory.TryWrite(countryAddress, country)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "e-ZuhGEoeC4",
|
||||||
|
ExportName = "sceNpGetNpReachabilityState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId == -1 || stateAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> state = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(state, 0); // Unavailable while offline.
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
|
? SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ec63y59l9tw",
|
Nid = "Ec63y59l9tw",
|
||||||
ExportName = "sceNpSetNpTitleId",
|
ExportName = "sceNpSetNpTitleId",
|
||||||
@@ -166,6 +208,12 @@ public static class NpManagerExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||||
{
|
{
|
||||||
var length = 0;
|
var length = 0;
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ public static class HostWindowInput
|
|||||||
{
|
{
|
||||||
keyboard.KeyDown += (_, key, _) =>
|
keyboard.KeyDown += (_, key, _) =>
|
||||||
{
|
{
|
||||||
|
if (key == Key.F1)
|
||||||
|
{
|
||||||
|
VideoOut.PerfOverlay.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
Pressed.Add(key);
|
Pressed.Add(key);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
public static class ImeExports
|
||||||
|
{
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
|
private const int ImeErrorInvalidAddress = unchecked((int)0x80BC0001);
|
||||||
|
private const int ImeErrorInvalidUserId = unchecked((int)0x80BC0010);
|
||||||
|
private const int ImeErrorNotOpened = unchecked((int)0x80BC0005);
|
||||||
|
|
||||||
|
private static bool _keyboardOpen;
|
||||||
|
public static int ImeKeyboardOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ImeErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId != PrimaryUserId)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, ImeErrorInvalidUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_keyboardOpen = true;
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int ImeUpdate(CpuContext ctx) =>
|
||||||
|
SetReturn(ctx, _keyboardOpen ? 0 : ImeErrorNotOpened);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
public static class MouseExports
|
||||||
|
{
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
|
private const int MouseDataSize = 0x28;
|
||||||
|
private const int MouseErrorInvalidArgument = unchecked((int)0x80020002);
|
||||||
|
private const int MouseErrorInvalidHandle = unchecked((int)0x80020003);
|
||||||
|
private const int MouseErrorNotInitialized = unchecked((int)0x80020005);
|
||||||
|
private const int MouseErrorAlreadyOpened = unchecked((int)0x80020008);
|
||||||
|
|
||||||
|
private static readonly bool[] OpenHandles = new bool[2];
|
||||||
|
private static bool _initialized;
|
||||||
|
public static int MouseInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
Array.Clear(OpenHandles);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
public static int MouseOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (userId != PrimaryUserId || type != 0 || index is < 0 or > 1)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenHandles[index])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorAlreadyOpened);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenHandles[index] = true;
|
||||||
|
return SetReturn(ctx, index);
|
||||||
|
}
|
||||||
|
public static int MouseRead(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (dataAddress == 0 || count is < 1 or > 64)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle is < 0 or > 1 || !OpenHandles[handle])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> data = stackalloc byte[MouseDataSize];
|
||||||
|
data.Clear();
|
||||||
|
var ticks = Stopwatch.GetTimestamp();
|
||||||
|
var timestamp =
|
||||||
|
((ulong)(ticks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||||
|
((ulong)(ticks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(data, timestamp);
|
||||||
|
data[0x08] = 0; // No host mouse is exposed to the guest yet.
|
||||||
|
return ctx.Memory.TryWrite(dataAddress, data)
|
||||||
|
? SetReturn(ctx, 1)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cAnT0Rw-IwU",
|
||||||
|
ExportName = "sceMouseClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (handle is < 0 or > 1 || !OpenHandles[handle])
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, MouseErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenHandles[handle] = false;
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,11 @@ public static class PadExports
|
|||||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||||
private const int PrimaryUserId = 1000;
|
// Keep the pad session on the same retail user id returned by
|
||||||
|
// libSceUserService. A mismatched emulator-local id makes games pass a
|
||||||
|
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
|
||||||
|
// leaving every later keyboard/gamepad read on an invalid handle.
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
private const int StandardPortType = 0;
|
private const int StandardPortType = 0;
|
||||||
private const int PrimaryPadHandle = 1;
|
private const int PrimaryPadHandle = 1;
|
||||||
private const int ControllerInformationSize = 0x1C;
|
private const int ControllerInformationSize = 0x1C;
|
||||||
|
|||||||
@@ -30,19 +30,15 @@ public static class SaveDataExports
|
|||||||
private const uint MountModeCreate2 = 1u << 5;
|
private const uint MountModeCreate2 = 1u << 5;
|
||||||
private const int MountResultSize = 0x40;
|
private const int MountResultSize = 0x40;
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly HashSet<int> _transactionResources = [];
|
|
||||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||||
private static string? _titleId;
|
private static string? _titleId;
|
||||||
private static int _nextTransactionResource;
|
|
||||||
|
|
||||||
public static void ConfigureApplicationInfo(string? titleId)
|
public static void ConfigureApplicationInfo(string? titleId)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
_transactionResources.Clear();
|
|
||||||
_preparedTransactionResources.Clear();
|
_preparedTransactionResources.Clear();
|
||||||
_nextTransactionResource = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,15 +52,15 @@ public static class SaveDataExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,18 +75,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 ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (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 ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -102,7 +98,7 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||||
@@ -114,21 +110,21 @@ public static class SaveDataExports
|
|||||||
var setNum = result.DirNamesNum == 0
|
var setNum = result.DirNamesNum == 0
|
||||||
? 0
|
? 0
|
||||||
: Math.Min(result.DirNamesNum, entries.Count);
|
: Math.Min(result.DirNamesNum, entries.Count);
|
||||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.DirNamesAddress == 0)
|
if (result.DirNamesAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < setNum; i++)
|
for (var i = 0; i < setNum; i++)
|
||||||
@@ -144,20 +140,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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,25 +168,25 @@ 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 ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
|
if (!TryReadInt32(ctx, 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) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
|
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x28, out var mode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
|
||||||
dirNameAddress == 0 ||
|
dirNameAddress == 0 ||
|
||||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -205,12 +201,12 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
if (!existed && !create && !createIfMissing)
|
if (!existed && !create && !createIfMissing)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
|
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existed && create)
|
if (existed && create)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorExists);
|
return SetReturn(ctx, OrbisSaveDataErrorExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existed)
|
if (!existed)
|
||||||
@@ -227,29 +223,30 @@ 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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (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 ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int _nextTransactionResource;
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gjRZNnw0JPE",
|
Nid = "gjRZNnw0JPE",
|
||||||
ExportName = "sceSaveDataCreateTransactionResource",
|
ExportName = "sceSaveDataCreateTransactionResource",
|
||||||
@@ -257,16 +254,39 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var memorySize = ctx[CpuRegister.Rdi];
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
int resource;
|
var reserved = ctx[CpuRegister.Rsi];
|
||||||
lock (_stateGate)
|
|
||||||
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
|
// The resource-out pointer's argument slot varies by SDK revision: some
|
||||||
|
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
|
||||||
|
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
|
||||||
|
// real out-pointer in rcx. Probe the plausible candidates and write the
|
||||||
|
// handle to the first writable one instead of faulting on a bad rdx.
|
||||||
|
// This is a stub-level create (matches shadPS4's return-OK semantics);
|
||||||
|
// never return MEMORY_FAULT for it, or the guest treats savedata init as
|
||||||
|
// failed and never advances.
|
||||||
|
var resourceAddress = 0UL;
|
||||||
|
foreach (var candidate in new[]
|
||||||
{
|
{
|
||||||
resource = ++_nextTransactionResource;
|
ctx[CpuRegister.Rdx],
|
||||||
_transactionResources.Add(resource);
|
ctx[CpuRegister.Rcx],
|
||||||
|
ctx[CpuRegister.R8],
|
||||||
|
ctx[CpuRegister.R9],
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
||||||
|
{
|
||||||
|
resourceAddress = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"create_transaction_resource memory_size=0x{memorySize:X} resource={resource}");
|
TraceSaveData(
|
||||||
return ctx.SetReturn(resource);
|
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -279,70 +299,11 @@ public static class SaveDataExports
|
|||||||
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
|
var resource = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_transactionResources.Remove(resource);
|
|
||||||
_preparedTransactionResources.Remove(resource);
|
_preparedTransactionResources.Remove(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData($"delete_transaction_resource resource={resource}");
|
TraceSaveData($"delete_transaction_resource resource={resource}");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "sDCBrmc61XU",
|
|
||||||
ExportName = "sceSaveDataPrepare",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataPrepare(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
||||||
if (mountPointAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
if (resource != 0)
|
|
||||||
{
|
|
||||||
_preparedTransactionResources.Add(resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "ie7qhZ4X0Cc",
|
|
||||||
ExportName = "sceSaveDataCommit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceSaveData")]
|
|
||||||
public static int SaveDataCommit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var commitAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (commitAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
_preparedTransactionResources.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -352,40 +313,21 @@ public static class SaveDataExports
|
|||||||
LibraryName = "libSceSaveData")]
|
LibraryName = "libSceSaveData")]
|
||||||
public static int SaveDataUmount2(CpuContext ctx)
|
public static int SaveDataUmount2(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var mountPointAddress = ctx[CpuRegister.Rdi];
|
// Unmounting a save directory always succeeds in the stub filesystem;
|
||||||
if (mountPointAddress == 0)
|
// returning an error here makes the game's save flow stall before it
|
||||||
{
|
// hands control to the title/gameplay state.
|
||||||
mountPointAddress = ctx[CpuRegister.Rsi];
|
TraceSaveData($"umount2 user={unchecked((int)ctx[CpuRegister.Rdi])}");
|
||||||
}
|
return SetReturn(ctx, 0);
|
||||||
|
|
||||||
if (mountPointAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mountPoint))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
|
|
||||||
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
|
|
||||||
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 (!ctx.TryReadInt32(address, out var userId) ||
|
if (!TryReadInt32(ctx, 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) ||
|
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
|
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -408,7 +350,7 @@ public static class SaveDataExports
|
|||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||||
{
|
{
|
||||||
@@ -609,6 +551,45 @@ 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 bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
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))
|
||||||
@@ -631,4 +612,62 @@ public static class SaveDataExports
|
|||||||
ulong InfosAddress);
|
ulong InfosAddress);
|
||||||
|
|
||||||
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sDCBrmc61XU",
|
||||||
|
ExportName = "sceSaveDataPrepare",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataPrepare(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var mountPointAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (mountPointAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (resource != 0)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ie7qhZ4X0Cc",
|
||||||
|
ExportName = "sceSaveDataCommit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataCommit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commitAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (commitAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_preparedTransactionResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Stubs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success stubs for trophy, character-encoding and telemetry ABI calls that
|
||||||
|
/// Void Terrarium (and other titles) invoke during startup. They were
|
||||||
|
/// previously unresolved and returned NOT_FOUND, and a title that gates its
|
||||||
|
/// UI/text initialization on these succeeding then skips ahead and never draws
|
||||||
|
/// its content (a black screen with only a clear pass). These return success
|
||||||
|
/// (and a non-zero handle where an out pointer is expected) so init proceeds.
|
||||||
|
/// </summary>
|
||||||
|
public static class GameServiceStubs
|
||||||
|
{
|
||||||
|
private static int Ok(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a small non-zero handle to the pointer in the given register so
|
||||||
|
// the caller treats the object as created; returns success.
|
||||||
|
private static int OkWithHandle(CpuContext ctx, CpuRegister outPointerRegister)
|
||||||
|
{
|
||||||
|
var outAddress = ctx[outPointerRegister];
|
||||||
|
if (outAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> handle = stackalloc byte[sizeof(int)];
|
||||||
|
System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(handle, 1);
|
||||||
|
_ = ctx.Memory.TryWrite(outAddress, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NpTrophy2: trophy context/handle registration at boot ----
|
||||||
|
public static int NpTrophy2CreateContext(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpTrophy2CreateHandle(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpTrophy2RegisterContext(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "4IzqhhUQ3nk", ExportName = "sceNpTrophy2GetGameInfo",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2GetGameInfo(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
// ---- CES: Shift-JIS <-> Unicode conversion setup (Japanese text) ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "ZiDCxUUGbec", ExportName = "sceCesUcsProfileInitSJis1997Cp932",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
|
||||||
|
public static int CesUcsProfileInitSJis1997Cp932(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "538bRGc6Zo8", ExportName = "sceCesMbcsUcsContextInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceLibcInternal")]
|
||||||
|
public static int CesMbcsUcsContextInit(CpuContext ctx) => Ok(ctx);
|
||||||
|
|
||||||
|
// ---- NpUniversalDataSystem: gameplay telemetry events ----
|
||||||
|
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||||
|
public static int NpUniversalDataSystemPostEvent(CpuContext ctx) => Ok(ctx);
|
||||||
|
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx) => Ok(ctx);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ public static class SystemServiceExports
|
|||||||
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||||
private const int SystemServiceStatusSize = 0x0C;
|
private const int SystemServiceStatusSize = 0x0C;
|
||||||
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
||||||
|
private const int HdrToneMapLuminanceSize = sizeof(float) * 3;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "fZo48un7LK4",
|
Nid = "fZo48un7LK4",
|
||||||
@@ -42,6 +43,35 @@ public static class SystemServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SsC-m-S9JTA",
|
||||||
|
ExportName = "sceSystemServiceParamGetString",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceParamGetString(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = unchecked((int)ctx[CpuRegister.Rdi]); // parameter id (nickname, etc.)
|
||||||
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferSize = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (bufferAddress == 0 || bufferSize <= 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String params are typically the user nickname. Callers that gate UI
|
||||||
|
// or text setup on a successful read (and skip it on failure) stall on
|
||||||
|
// a black screen when this returns NOT_FOUND, so return a neutral
|
||||||
|
// non-empty default and success.
|
||||||
|
var value = System.Text.Encoding.UTF8.GetBytes("SharpEmu");
|
||||||
|
var writeLength = Math.Min(value.Length, bufferSize - 1);
|
||||||
|
Span<byte> output = stackalloc byte[writeLength + 1];
|
||||||
|
value.AsSpan(0, writeLength).CopyTo(output);
|
||||||
|
output[writeLength] = 0;
|
||||||
|
return ctx.Memory.TryWrite(bufferAddress, output)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rPo6tV8D9bM",
|
Nid = "rPo6tV8D9bM",
|
||||||
ExportName = "sceSystemServiceGetStatus",
|
ExportName = "sceSystemServiceGetStatus",
|
||||||
@@ -87,6 +117,28 @@ public static class SystemServiceExports
|
|||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mPpPxv5CZt4",
|
||||||
|
ExportName = "sceSystemServiceGetHdrToneMapLuminance",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetHdrToneMapLuminance(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var luminanceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (luminanceAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> luminance = stackalloc byte[HdrToneMapLuminanceSize];
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance, 1000.0f);
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance[sizeof(float)..], 1000.0f);
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(luminance[(sizeof(float) * 2)..], 0.01f);
|
||||||
|
return ctx.Memory.TryWrite(luminanceAddress, luminance)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Vo5V8KAwCmk",
|
Nid = "Vo5V8KAwCmk",
|
||||||
ExportName = "sceSystemServiceHideSplashScreen",
|
ExportName = "sceSystemServiceHideSplashScreen",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -13,15 +14,14 @@ public static class UserServiceExports
|
|||||||
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||||
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||||
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||||
// Retail user-service IDs begin at 0x3E8.
|
// Retail user ids encode their local user slot in the 0x10000000 range;
|
||||||
private const int PrimaryUserId = 1000;
|
// the first signed-in user maps to slot 0. Small emulator-local ids do
|
||||||
|
// not pass Unity's user-id-to-slot conversion and become slot -1.
|
||||||
|
private const int PrimaryUserId = 0x10000000;
|
||||||
private const int InvalidUserId = -1;
|
private const int InvalidUserId = -1;
|
||||||
private const string PrimaryUserName = "SharpEmu";
|
private const string PrimaryUserName = "SharpEmu";
|
||||||
private static int _loginEventDelivered;
|
private static int _loginEventDelivered;
|
||||||
|
|
||||||
private static readonly bool _traceUserService =
|
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "j3YMu1MVNNo",
|
Nid = "j3YMu1MVNNo",
|
||||||
ExportName = "sceUserServiceInitialize",
|
ExportName = "sceUserServiceInitialize",
|
||||||
@@ -29,6 +29,7 @@ public static class UserServiceExports
|
|||||||
LibraryName = "libSceUserService")]
|
LibraryName = "libSceUserService")]
|
||||||
public static int UserServiceInitialize(CpuContext ctx)
|
public static int UserServiceInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
Trace("initialize");
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -43,14 +44,12 @@ public static class UserServiceExports
|
|||||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdAddress == 0)
|
if (userIdAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||||
? 0
|
? SetReturnWithTrace(ctx, 0, $"get_initial_user user={PrimaryUserId} out=0x{userIdAddress:X16}")
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
|
|
||||||
return ctx.SetReturn(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -63,7 +62,7 @@ public static class UserServiceExports
|
|||||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||||
if (userIdListAddress == 0)
|
if (userIdListAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||||
@@ -72,8 +71,12 @@ 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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturnWithTrace(
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_login_user_id_list users=[{PrimaryUserId},{InvalidUserId},{InvalidUserId},{InvalidUserId}] " +
|
||||||
|
$"out=0x{userIdListAddress:X16}")
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -86,20 +89,23 @@ public static class UserServiceExports
|
|||||||
var eventAddress = ctx[CpuRegister.Rdi];
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
if (eventAddress == 0)
|
if (eventAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
|
return SetReturn(ctx, 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)
|
||||||
? ctx.SetReturn(0)
|
? SetReturnWithTrace(
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_event type=login user={PrimaryUserId} out=0x{eventAddress:X16}")
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -112,53 +118,30 @@ public static class UserServiceExports
|
|||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
var capacity = ctx[CpuRegister.Rdx];
|
var capacity = ctx[CpuRegister.Rdx];
|
||||||
// Zero selects the current user.
|
if (userId != PrimaryUserId)
|
||||||
if (userId != 0 && userId != PrimaryUserId)
|
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||||
if (capacity <= (ulong)nameBytes.Length)
|
if (capacity <= (ulong)nameBytes.Length)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
|
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||||
nameBytes.CopyTo(output);
|
nameBytes.CopyTo(output);
|
||||||
var result = ctx.Memory.TryWrite(nameAddress, output)
|
return ctx.Memory.TryWrite(nameAddress, output)
|
||||||
? 0
|
? SetReturnWithTrace(
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
ctx,
|
||||||
TraceUserService(
|
0,
|
||||||
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
|
$"get_user_name user={userId} name='{PrimaryUserName}' out=0x{nameAddress:X16}")
|
||||||
return ctx.SetReturn(result);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "-sD02mFDBh4",
|
|
||||||
ExportName = "sceUserServiceGetGamePresets",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceUserService")]
|
|
||||||
public static int UserServiceGetGamePresets(CpuContext ctx)
|
|
||||||
{
|
|
||||||
// Return deterministic defaults for the offline profile.
|
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var resultAddress = ctx[CpuRegister.Rcx];
|
|
||||||
Span<byte> defaults = stackalloc byte[0x18];
|
|
||||||
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceUserService(
|
|
||||||
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
|
|
||||||
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
|
||||||
@@ -174,50 +157,165 @@ public static class UserServiceExports
|
|||||||
var valueAddress = ctx[CpuRegister.Rsi];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (parameterId != 1000)
|
if (parameterId != 1000)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteInt32(valueAddress, 0)
|
return TryWriteInt32(ctx, valueAddress, 0)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
#pragma warning restore SHEM006
|
#pragma warning restore SHEM006
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "woNpu+45RLk",
|
Nid = "woNpu+45RLk",
|
||||||
ExportName = "sceUserServiceGetAgeLevel",
|
ExportName = "sceUserServiceGetAgeLevel",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceUserService")]
|
LibraryName = "libSceUserService")]
|
||||||
public static int UserServiceGetAgeLevel(CpuContext ctx)
|
public static int UserServiceGetAgeLevel(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 18, "get_age_level");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-sD02mFDBh4",
|
||||||
|
ExportName = "sceUserServiceGetGamePresets",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetGamePresets(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var ageLevelAddress = ctx[CpuRegister.Rsi];
|
var presetsAddress = ctx[CpuRegister.Rsi];
|
||||||
if (userId != 1000)
|
if (userId != PrimaryUserId)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ageLevelAddress == 0)
|
if (presetsAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report an adult account so titles skip parental-restriction paths.
|
Span<byte> presets = stackalloc byte[0x28];
|
||||||
return ctx.TryWriteInt32(ageLevelAddress, 21)
|
presets.Clear();
|
||||||
? ctx.SetReturn(0)
|
BinaryPrimitives.WriteUInt64LittleEndian(presets, (ulong)presets.Length);
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
if (!ctx.Memory.TryWrite(presetsAddress, presets))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceUserService(string message)
|
return SetReturnWithTrace(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
$"get_game_presets user={userId} out=0x{presetsAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rnEhHqG-4xo",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityChatTranscription",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityChatTranscription(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_chat_transcription");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ZKJtxdgvzwg",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityPressAndHoldDelay",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityPressAndHoldDelay(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_press_and_hold_delay");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-3Y5GO+-i78",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityTriggerEffect",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityTriggerEffect(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_trigger_effect");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "qWYHOFwqCxY",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityVibration",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityVibration(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 1, "get_accessibility_vibration");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hD-H81EN9Vg",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityZoomEnabled",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityZoomEnabled(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_enabled");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "O6IW1-Dwm-w",
|
||||||
|
ExportName = "sceUserServiceGetAccessibilityZoomFollowFocus",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetAccessibilityZoomFollowFocus(CpuContext ctx) =>
|
||||||
|
WriteUserSettingInt32(ctx, 0, "get_accessibility_zoom_follow_focus");
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
{
|
{
|
||||||
if (_traceUserService)
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteUserSettingInt32(CpuContext ctx, int value, string operation)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (userId != PrimaryUserId)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, valueAddress, value))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturnWithTrace(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
$"{operation} user={userId} value={value} out=0x{valueAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturnWithTrace(CpuContext ctx, int result, string message)
|
||||||
|
{
|
||||||
|
Trace(message);
|
||||||
|
return SetReturn(ctx, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
|
||||||
|
? frame.ReturnRip
|
||||||
|
: 0;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] user_service.{message} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,363 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-window performance HUD. The panel is rasterized on the CPU into a
|
||||||
|
/// small BGRA buffer each frame (embedded 5x7 font, no assets) and the
|
||||||
|
/// presenter blits it onto the swapchain image, so it needs no pipelines,
|
||||||
|
/// descriptors or blending state. Toggled with F1; SHARPEMU_OVERLAY=0
|
||||||
|
/// starts it hidden.
|
||||||
|
/// </summary>
|
||||||
|
public static class PerfOverlay
|
||||||
|
{
|
||||||
|
public const int PanelWidth = 376;
|
||||||
|
public const int PanelHeight = 176;
|
||||||
|
|
||||||
|
private const int GlyphColumns = 5;
|
||||||
|
private const int GlyphRows = 7;
|
||||||
|
private const int Scale = 2;
|
||||||
|
private const int CellWidth = (GlyphColumns + 1) * Scale;
|
||||||
|
private const int LineHeight = (GlyphRows + 2) * Scale;
|
||||||
|
private const int FrameHistorySize = 128;
|
||||||
|
|
||||||
|
private static volatile bool _enabled = !string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_OVERLAY"),
|
||||||
|
"0",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static long _lastPresentTimestamp;
|
||||||
|
private static readonly double[] _frameMilliseconds = new double[FrameHistorySize];
|
||||||
|
private static int _frameHistoryIndex;
|
||||||
|
private static long _presentedInWindow;
|
||||||
|
private static long _submittedInWindow;
|
||||||
|
private static long _drawsInWindow;
|
||||||
|
|
||||||
|
// Refreshed once per second so per-frame fills never allocate.
|
||||||
|
private static long _statsWindowStart = Stopwatch.GetTimestamp();
|
||||||
|
private static double _fps;
|
||||||
|
private static double _submittedFps;
|
||||||
|
private static double _drawsPerSecond;
|
||||||
|
private static double _averageFrameMs;
|
||||||
|
private static double _allocatedMbPerSecond;
|
||||||
|
private static long _lastAllocatedBytes = GC.GetTotalAllocatedBytes(precise: false);
|
||||||
|
private static int _lastGen0 = GC.CollectionCount(0);
|
||||||
|
private static int _lastGen1 = GC.CollectionCount(1);
|
||||||
|
private static int _lastGen2 = GC.CollectionCount(2);
|
||||||
|
private static int _gen0PerWindow;
|
||||||
|
private static int _gen1PerWindow;
|
||||||
|
private static int _gen2PerWindow;
|
||||||
|
private static double _cpuPercent;
|
||||||
|
private static TimeSpan _lastCpuTime = GetProcessCpuTime();
|
||||||
|
private static string _line1 = string.Empty;
|
||||||
|
private static string _line2 = string.Empty;
|
||||||
|
private static string _line3 = string.Empty;
|
||||||
|
private static string _line4 = string.Empty;
|
||||||
|
|
||||||
|
public static bool Enabled => _enabled;
|
||||||
|
|
||||||
|
public static void Toggle() => _enabled = !_enabled;
|
||||||
|
|
||||||
|
/// <summary>Called by the presenter after each successful present.</summary>
|
||||||
|
public static void RecordPresent()
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
var last = _lastPresentTimestamp;
|
||||||
|
_lastPresentTimestamp = now;
|
||||||
|
Interlocked.Increment(ref _presentedInWindow);
|
||||||
|
if (last != 0)
|
||||||
|
{
|
||||||
|
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
|
||||||
|
if (milliseconds < 1000.0)
|
||||||
|
{
|
||||||
|
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
||||||
|
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called on every guest flip submission.</summary>
|
||||||
|
public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
|
||||||
|
|
||||||
|
/// <summary>Called per translated draw/dispatch executed.</summary>
|
||||||
|
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
|
||||||
|
/// Runs on the render thread.
|
||||||
|
/// </summary>
|
||||||
|
public static void Fill(Span<byte> bgra, int pendingWork, int inFlightSubmissions)
|
||||||
|
{
|
||||||
|
RefreshStatsIfDue(pendingWork, inFlightSubmissions);
|
||||||
|
|
||||||
|
// Background: opaque dark slate.
|
||||||
|
for (var i = 0; i < bgra.Length; i += 4)
|
||||||
|
{
|
||||||
|
bgra[i] = 0x20;
|
||||||
|
bgra[i + 1] = 0x18;
|
||||||
|
bgra[i + 2] = 0x14;
|
||||||
|
bgra[i + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
var y = 6;
|
||||||
|
DrawString(bgra, 8, y, _line1, 0x60, 0xFF, 0x60);
|
||||||
|
y += LineHeight;
|
||||||
|
DrawString(bgra, 8, y, _line2, 0xFF, 0xFF, 0xFF);
|
||||||
|
y += LineHeight;
|
||||||
|
DrawString(bgra, 8, y, _line3, 0xB0, 0xD0, 0xFF);
|
||||||
|
y += LineHeight;
|
||||||
|
DrawString(bgra, 8, y, _line4, 0xB0, 0xB0, 0xB0);
|
||||||
|
y += LineHeight + 4;
|
||||||
|
DrawFrameGraph(bgra, 8, y, PanelWidth - 16, PanelHeight - y - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RefreshStatsIfDue(int pendingWork, int inFlightSubmissions)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
var elapsedTicks = now - _statsWindowStart;
|
||||||
|
if (elapsedTicks >= Stopwatch.Frequency)
|
||||||
|
{
|
||||||
|
var seconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||||
|
_statsWindowStart = now;
|
||||||
|
_fps = Interlocked.Exchange(ref _presentedInWindow, 0) / seconds;
|
||||||
|
_submittedFps = Interlocked.Exchange(ref _submittedInWindow, 0) / seconds;
|
||||||
|
_drawsPerSecond = Interlocked.Exchange(ref _drawsInWindow, 0) / seconds;
|
||||||
|
|
||||||
|
double totalMs = 0;
|
||||||
|
var samples = 0;
|
||||||
|
foreach (var ms in _frameMilliseconds)
|
||||||
|
{
|
||||||
|
if (ms > 0)
|
||||||
|
{
|
||||||
|
totalMs += ms;
|
||||||
|
samples++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_averageFrameMs = samples > 0 ? totalMs / samples : 0;
|
||||||
|
|
||||||
|
var allocated = GC.GetTotalAllocatedBytes(precise: false);
|
||||||
|
_allocatedMbPerSecond = (allocated - _lastAllocatedBytes) / seconds / (1024.0 * 1024.0);
|
||||||
|
_lastAllocatedBytes = allocated;
|
||||||
|
|
||||||
|
var gen0 = GC.CollectionCount(0);
|
||||||
|
var gen1 = GC.CollectionCount(1);
|
||||||
|
var gen2 = GC.CollectionCount(2);
|
||||||
|
_gen0PerWindow = gen0 - _lastGen0;
|
||||||
|
_gen1PerWindow = gen1 - _lastGen1;
|
||||||
|
_gen2PerWindow = gen2 - _lastGen2;
|
||||||
|
_lastGen0 = gen0;
|
||||||
|
_lastGen1 = gen1;
|
||||||
|
_lastGen2 = gen2;
|
||||||
|
|
||||||
|
var cpuTime = GetProcessCpuTime();
|
||||||
|
_cpuPercent = (cpuTime - _lastCpuTime).TotalSeconds / seconds * 100.0;
|
||||||
|
_lastCpuTime = cpuTime;
|
||||||
|
|
||||||
|
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
|
||||||
|
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
|
||||||
|
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
|
||||||
|
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
|
||||||
|
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetProcessCpuTime()
|
||||||
|
{
|
||||||
|
var usage = Environment.CpuUsage;
|
||||||
|
return usage.UserTime + usage.PrivilegedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawFrameGraph(Span<byte> bgra, int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
if (height < 8)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference lines at 16.7ms and 33.3ms of a 40ms-tall scale.
|
||||||
|
const double maxMs = 40.0;
|
||||||
|
DrawHorizontalLine(bgra, x, y + height - (int)(16.7 / maxMs * height), width, 0x30, 0x60, 0x30);
|
||||||
|
DrawHorizontalLine(bgra, x, y + height - (int)(33.3 / maxMs * height), width, 0x30, 0x30, 0x60);
|
||||||
|
|
||||||
|
var barWidth = Math.Max(1, width / FrameHistorySize);
|
||||||
|
for (var i = 0; i < FrameHistorySize; i++)
|
||||||
|
{
|
||||||
|
var ms = _frameMilliseconds[(_frameHistoryIndex + i) % FrameHistorySize];
|
||||||
|
if (ms <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var barHeight = (int)Math.Min(height, ms / maxMs * height);
|
||||||
|
var barX = x + i * barWidth;
|
||||||
|
if (barX + barWidth > x + width)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte r, g, b;
|
||||||
|
if (ms <= 17.5)
|
||||||
|
{
|
||||||
|
(r, g, b) = ((byte)0x40, (byte)0xE0, (byte)0x40);
|
||||||
|
}
|
||||||
|
else if (ms <= 34.0)
|
||||||
|
{
|
||||||
|
(r, g, b) = ((byte)0xE0, (byte)0xC0, (byte)0x30);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(r, g, b) = ((byte)0xE8, (byte)0x40, (byte)0x40);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var py = 0; py < barHeight; py++)
|
||||||
|
{
|
||||||
|
var rowY = y + height - 1 - py;
|
||||||
|
for (var px = 0; px < barWidth; px++)
|
||||||
|
{
|
||||||
|
SetPixel(bgra, barX + px, rowY, r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHorizontalLine(Span<byte> bgra, int x, int y, int width, byte r, byte g, byte b)
|
||||||
|
{
|
||||||
|
if (y < 0 || y >= PanelHeight)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var px = 0; px < width; px++)
|
||||||
|
{
|
||||||
|
SetPixel(bgra, x + px, y, r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawString(Span<byte> bgra, int x, int y, string text, byte r, byte g, byte b)
|
||||||
|
{
|
||||||
|
var penX = x;
|
||||||
|
foreach (var rawChar in text)
|
||||||
|
{
|
||||||
|
var c = char.ToUpperInvariant(rawChar);
|
||||||
|
if (c < ' ' || c > 'Z')
|
||||||
|
{
|
||||||
|
c = '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
var glyph = Font.Slice((c - ' ') * GlyphColumns, GlyphColumns);
|
||||||
|
for (var column = 0; column < GlyphColumns; column++)
|
||||||
|
{
|
||||||
|
var bits = glyph[column];
|
||||||
|
for (var row = 0; row < GlyphRows; row++)
|
||||||
|
{
|
||||||
|
if ((bits & (1 << row)) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var sy = 0; sy < Scale; sy++)
|
||||||
|
{
|
||||||
|
for (var sx = 0; sx < Scale; sx++)
|
||||||
|
{
|
||||||
|
SetPixel(
|
||||||
|
bgra,
|
||||||
|
penX + column * Scale + sx,
|
||||||
|
y + row * Scale + sy,
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
penX += CellWidth;
|
||||||
|
if (penX + CellWidth > PanelWidth)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetPixel(Span<byte> bgra, int x, int y, byte r, byte g, byte b)
|
||||||
|
{
|
||||||
|
if ((uint)x >= PanelWidth || (uint)y >= PanelHeight)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = (y * PanelWidth + x) * 4;
|
||||||
|
bgra[offset] = b;
|
||||||
|
bgra[offset + 1] = g;
|
||||||
|
bgra[offset + 2] = r;
|
||||||
|
bgra[offset + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classic 5x7 column-encoded font (bit 0 = top row), ASCII 32..90.
|
||||||
|
private static ReadOnlySpan<byte> Font =>
|
||||||
|
[
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, // ' '
|
||||||
|
0x00, 0x00, 0x5F, 0x00, 0x00, // '!'
|
||||||
|
0x00, 0x07, 0x00, 0x07, 0x00, // '"'
|
||||||
|
0x14, 0x7F, 0x14, 0x7F, 0x14, // '#'
|
||||||
|
0x24, 0x2A, 0x7F, 0x2A, 0x12, // '$'
|
||||||
|
0x23, 0x13, 0x08, 0x64, 0x62, // '%'
|
||||||
|
0x36, 0x49, 0x55, 0x22, 0x50, // '&'
|
||||||
|
0x00, 0x05, 0x03, 0x00, 0x00, // '''
|
||||||
|
0x00, 0x1C, 0x22, 0x41, 0x00, // '('
|
||||||
|
0x00, 0x41, 0x22, 0x1C, 0x00, // ')'
|
||||||
|
0x08, 0x2A, 0x1C, 0x2A, 0x08, // '*'
|
||||||
|
0x08, 0x08, 0x3E, 0x08, 0x08, // '+'
|
||||||
|
0x00, 0x50, 0x30, 0x00, 0x00, // ','
|
||||||
|
0x08, 0x08, 0x08, 0x08, 0x08, // '-'
|
||||||
|
0x00, 0x60, 0x60, 0x00, 0x00, // '.'
|
||||||
|
0x20, 0x10, 0x08, 0x04, 0x02, // '/'
|
||||||
|
0x3E, 0x51, 0x49, 0x45, 0x3E, // '0'
|
||||||
|
0x00, 0x42, 0x7F, 0x40, 0x00, // '1'
|
||||||
|
0x42, 0x61, 0x51, 0x49, 0x46, // '2'
|
||||||
|
0x21, 0x41, 0x45, 0x4B, 0x31, // '3'
|
||||||
|
0x18, 0x14, 0x12, 0x7F, 0x10, // '4'
|
||||||
|
0x27, 0x45, 0x45, 0x45, 0x39, // '5'
|
||||||
|
0x3C, 0x4A, 0x49, 0x49, 0x30, // '6'
|
||||||
|
0x01, 0x71, 0x09, 0x05, 0x03, // '7'
|
||||||
|
0x36, 0x49, 0x49, 0x49, 0x36, // '8'
|
||||||
|
0x06, 0x49, 0x49, 0x29, 0x1E, // '9'
|
||||||
|
0x00, 0x36, 0x36, 0x00, 0x00, // ':'
|
||||||
|
0x00, 0x56, 0x36, 0x00, 0x00, // ';'
|
||||||
|
0x00, 0x08, 0x14, 0x22, 0x41, // '<'
|
||||||
|
0x14, 0x14, 0x14, 0x14, 0x14, // '='
|
||||||
|
0x41, 0x22, 0x14, 0x08, 0x00, // '>'
|
||||||
|
0x02, 0x01, 0x51, 0x09, 0x06, // '?'
|
||||||
|
0x32, 0x49, 0x79, 0x41, 0x3E, // '@'
|
||||||
|
0x7E, 0x11, 0x11, 0x11, 0x7E, // 'A'
|
||||||
|
0x7F, 0x49, 0x49, 0x49, 0x36, // 'B'
|
||||||
|
0x3E, 0x41, 0x41, 0x41, 0x22, // 'C'
|
||||||
|
0x7F, 0x41, 0x41, 0x22, 0x1C, // 'D'
|
||||||
|
0x7F, 0x49, 0x49, 0x49, 0x41, // 'E'
|
||||||
|
0x7F, 0x09, 0x09, 0x09, 0x01, // 'F'
|
||||||
|
0x3E, 0x41, 0x49, 0x49, 0x7A, // 'G'
|
||||||
|
0x7F, 0x08, 0x08, 0x08, 0x7F, // 'H'
|
||||||
|
0x00, 0x41, 0x7F, 0x41, 0x00, // 'I'
|
||||||
|
0x20, 0x40, 0x41, 0x3F, 0x01, // 'J'
|
||||||
|
0x7F, 0x08, 0x14, 0x22, 0x41, // 'K'
|
||||||
|
0x7F, 0x40, 0x40, 0x40, 0x40, // 'L'
|
||||||
|
0x7F, 0x02, 0x0C, 0x02, 0x7F, // 'M'
|
||||||
|
0x7F, 0x04, 0x08, 0x10, 0x7F, // 'N'
|
||||||
|
0x3E, 0x41, 0x41, 0x41, 0x3E, // 'O'
|
||||||
|
0x7F, 0x09, 0x09, 0x09, 0x06, // 'P'
|
||||||
|
0x3E, 0x41, 0x51, 0x21, 0x5E, // 'Q'
|
||||||
|
0x7F, 0x09, 0x19, 0x29, 0x46, // 'R'
|
||||||
|
0x46, 0x49, 0x49, 0x49, 0x31, // 'S'
|
||||||
|
0x01, 0x01, 0x7F, 0x01, 0x01, // 'T'
|
||||||
|
0x3F, 0x40, 0x40, 0x40, 0x3F, // 'U'
|
||||||
|
0x1F, 0x20, 0x40, 0x20, 0x1F, // 'V'
|
||||||
|
0x3F, 0x40, 0x38, 0x40, 0x3F, // 'W'
|
||||||
|
0x63, 0x14, 0x08, 0x14, 0x63, // 'X'
|
||||||
|
0x07, 0x08, 0x70, 0x08, 0x07, // 'Y'
|
||||||
|
0x61, 0x51, 0x49, 0x45, 0x43, // 'Z'
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -164,4 +164,100 @@ public static class SpirvFixedShaders
|
|||||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
return module.Build();
|
return module.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] CreateSolidFragment(float red, float green, float blue, float alpha)
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var floatType = module.TypeFloat(32);
|
||||||
|
var vec4Type = module.TypeVector(floatType, 4);
|
||||||
|
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||||
|
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(output, "outColor");
|
||||||
|
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||||
|
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
var color = module.ConstantComposite(
|
||||||
|
vec4Type,
|
||||||
|
module.ConstantFloat(floatType, red),
|
||||||
|
module.ConstantFloat(floatType, green),
|
||||||
|
module.ConstantFloat(floatType, blue),
|
||||||
|
module.ConstantFloat(floatType, alpha));
|
||||||
|
module.AddStatement(SpirvOp.Store, output, color);
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
|
||||||
|
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [output]);
|
||||||
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic fragment stage that exposes one interpolated vertex output
|
||||||
|
/// directly as color. This keeps the real guest vertex/index/depth path
|
||||||
|
/// intact while isolating fragment-shader translation from interface data.
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] CreateAttributeFragment(uint location)
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var floatType = module.TypeFloat(32);
|
||||||
|
var vec4Type = module.TypeVector(floatType, 4);
|
||||||
|
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||||
|
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||||
|
var input = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
|
||||||
|
module.AddName(input, $"attr{location}");
|
||||||
|
module.AddDecoration(input, SpirvDecoration.Location, location);
|
||||||
|
var output = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(output, "outColor");
|
||||||
|
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||||
|
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
var value = module.AddInstruction(SpirvOp.Load, vec4Type, input);
|
||||||
|
module.AddStatement(SpirvOp.Store, output, value);
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
|
||||||
|
module.AddEntryPoint(
|
||||||
|
SpirvExecutionModel.Fragment,
|
||||||
|
main,
|
||||||
|
"main",
|
||||||
|
[input, output]);
|
||||||
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal fragment stage for fixed-function depth-only passes. The
|
||||||
|
/// guest has no pixel shader and therefore cannot export colour; keeping
|
||||||
|
/// this stage output-free preserves that contract while allowing Vulkan
|
||||||
|
/// to run early/late depth tests for the translated vertex shader.
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] CreateDepthOnlyFragment()
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
|
||||||
|
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", []);
|
||||||
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ public enum SpirvOp : ushort
|
|||||||
ControlBarrier = 224,
|
ControlBarrier = 224,
|
||||||
MemoryBarrier = 225,
|
MemoryBarrier = 225,
|
||||||
AtomicIAdd = 234,
|
AtomicIAdd = 234,
|
||||||
|
AtomicUMax = 239,
|
||||||
Phi = 245,
|
Phi = 245,
|
||||||
LoopMerge = 246,
|
LoopMerge = 246,
|
||||||
SelectionMerge = 247,
|
SelectionMerge = 247,
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ public sealed record Gen5ImageControl(
|
|||||||
uint Dimension,
|
uint Dimension,
|
||||||
bool IsArray,
|
bool IsArray,
|
||||||
bool Glc,
|
bool Glc,
|
||||||
bool Slc) : Gen5InstructionControl
|
bool Slc,
|
||||||
|
bool A16,
|
||||||
|
bool D16) : Gen5InstructionControl
|
||||||
{
|
{
|
||||||
public uint GetAddressRegister(int component) =>
|
public uint GetAddressRegister(int component) =>
|
||||||
component < AddressRegisters.Count
|
component < AddressRegisters.Count
|
||||||
@@ -219,16 +221,21 @@ public sealed record Gen5Vop3Control(
|
|||||||
uint NegateMask,
|
uint NegateMask,
|
||||||
uint OutputModifier,
|
uint OutputModifier,
|
||||||
bool Clamp,
|
bool Clamp,
|
||||||
|
uint OperandSelect,
|
||||||
uint? ScalarDestination) : Gen5InstructionControl;
|
uint? ScalarDestination) : Gen5InstructionControl;
|
||||||
|
|
||||||
public sealed record Gen5SdwaControl(
|
public sealed record Gen5SdwaControl(
|
||||||
uint DestinationSelect,
|
uint DestinationSelect,
|
||||||
|
uint DestinationUnused,
|
||||||
uint Source0Select,
|
uint Source0Select,
|
||||||
uint Source1Select,
|
uint Source1Select,
|
||||||
|
bool Source0SignExtend,
|
||||||
|
bool Source1SignExtend,
|
||||||
uint AbsoluteMask,
|
uint AbsoluteMask,
|
||||||
uint NegateMask,
|
uint NegateMask,
|
||||||
uint OutputModifier,
|
uint OutputModifier,
|
||||||
bool Clamp) : Gen5InstructionControl;
|
bool Clamp,
|
||||||
|
uint? ScalarDestination) : Gen5InstructionControl;
|
||||||
|
|
||||||
public sealed record Gen5DppControl(
|
public sealed record Gen5DppControl(
|
||||||
uint Control,
|
uint Control,
|
||||||
@@ -239,6 +246,10 @@ public sealed record Gen5DppControl(
|
|||||||
uint BankMask,
|
uint BankMask,
|
||||||
uint RowMask) : Gen5InstructionControl;
|
uint RowMask) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
public sealed record Gen5Dpp8Control(
|
||||||
|
uint LaneSelectors,
|
||||||
|
bool FetchInactive) : Gen5InstructionControl;
|
||||||
|
|
||||||
public sealed record Gen5ScalarMemoryControl(
|
public sealed record Gen5ScalarMemoryControl(
|
||||||
uint DestinationCount,
|
uint DestinationCount,
|
||||||
int ImmediateOffsetBytes,
|
int ImmediateOffsetBytes,
|
||||||
@@ -257,11 +268,27 @@ public sealed record Gen5ImageBinding(
|
|||||||
IReadOnlyList<uint> SamplerDescriptor,
|
IReadOnlyList<uint> SamplerDescriptor,
|
||||||
uint? MipLevel);
|
uint? MipLevel);
|
||||||
|
|
||||||
|
// Data arrays may be rented from ArrayPool (oversized): always slice with
|
||||||
|
// DataLength, never Data.Length. Ownership transfers to the presenter, which
|
||||||
|
// returns pooled arrays after uploading them into host-visible buffers.
|
||||||
public sealed record Gen5GlobalMemoryBinding(
|
public sealed record Gen5GlobalMemoryBinding(
|
||||||
uint ScalarAddress,
|
uint ScalarAddress,
|
||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
IReadOnlyList<uint> InstructionPcs,
|
IReadOnlyList<uint> InstructionPcs,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int DataLength,
|
||||||
|
bool DataPooled)
|
||||||
|
{
|
||||||
|
public bool Writable { get; set; }
|
||||||
|
|
||||||
|
// Writable describes shader access and is also used to decide whether a
|
||||||
|
// compute dispatch has observable work. A statically reachable resource
|
||||||
|
// can nevertheless be unbound for the current scalar path; the evaluator
|
||||||
|
// supplies zero-filled storage for Vulkan in that case. Such synthetic
|
||||||
|
// storage must remain shader-writable, but must never be copied to the
|
||||||
|
// descriptor's unmapped guest address.
|
||||||
|
public bool WriteBackToGuest { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record Gen5VertexInputBinding(
|
public sealed record Gen5VertexInputBinding(
|
||||||
uint Pc,
|
uint Pc,
|
||||||
@@ -272,12 +299,13 @@ public sealed record Gen5VertexInputBinding(
|
|||||||
ulong BaseAddress,
|
ulong BaseAddress,
|
||||||
uint Stride,
|
uint Stride,
|
||||||
uint OffsetBytes,
|
uint OffsetBytes,
|
||||||
byte[] Data);
|
byte[] Data,
|
||||||
|
int DataLength,
|
||||||
|
bool DataPooled);
|
||||||
|
|
||||||
public sealed record Gen5ShaderEvaluation(
|
public sealed record Gen5ShaderEvaluation(
|
||||||
IReadOnlyList<uint> InitialScalarRegisters,
|
IReadOnlyList<uint> InitialScalarRegisters,
|
||||||
IReadOnlyList<uint> ScalarRegisters,
|
IReadOnlyList<uint> ScalarRegisters,
|
||||||
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
|
|
||||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||||
@@ -297,8 +325,58 @@ public sealed record Gen5ShaderProgram(
|
|||||||
ulong Address,
|
ulong Address,
|
||||||
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||||
{
|
{
|
||||||
|
private const int ScalarRegisterCount = 256;
|
||||||
|
private IReadOnlySet<uint>? _runtimeScalarRegisters;
|
||||||
|
|
||||||
public IEnumerable<Gen5ImageControl> ImageResources =>
|
public IEnumerable<Gen5ImageControl> ImageResources =>
|
||||||
Instructions
|
Instructions
|
||||||
.Select(instruction => instruction.Control)
|
.Select(instruction => instruction.Control)
|
||||||
.OfType<Gen5ImageControl>();
|
.OfType<Gen5ImageControl>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The set of scalar registers the program reads or writes as runtime
|
||||||
|
/// values. It depends only on the (cached) decoded program, so it is
|
||||||
|
/// computed once and shared read-only across every draw that uses this
|
||||||
|
/// shader — the evaluator previously rebuilt this HashSet by scanning
|
||||||
|
/// every instruction on every draw, one of the largest per-draw
|
||||||
|
/// allocation and CPU sources.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlySet<uint> RuntimeScalarRegisters =>
|
||||||
|
_runtimeScalarRegisters ??= ComputeRuntimeScalarRegisters();
|
||||||
|
|
||||||
|
private IReadOnlySet<uint> ComputeRuntimeScalarRegisters()
|
||||||
|
{
|
||||||
|
var registers = new HashSet<uint>();
|
||||||
|
foreach (var instruction in Instructions)
|
||||||
|
{
|
||||||
|
foreach (var operand in instruction.Sources)
|
||||||
|
{
|
||||||
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
||||||
|
operand.Value < ScalarRegisterCount)
|
||||||
|
{
|
||||||
|
registers.Add(operand.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var operand in instruction.Destinations)
|
||||||
|
{
|
||||||
|
if (operand.Kind == Gen5OperandKind.ScalarRegister &&
|
||||||
|
operand.Value < ScalarRegisterCount)
|
||||||
|
{
|
||||||
|
registers.Add(operand.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction.Control is Gen5ScalarMemoryControl
|
||||||
|
{
|
||||||
|
DynamicOffsetRegister: { } offsetRegister,
|
||||||
|
} &&
|
||||||
|
offsetRegister < ScalarRegisterCount)
|
||||||
|
{
|
||||||
|
registers.Add(offsetRegister);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 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.ShaderCompiler;
|
namespace SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
@@ -17,9 +18,9 @@ public static class Gen5ShaderMetadataReader
|
|||||||
out Gen5ShaderMetadata metadata)
|
out Gen5ShaderMetadata metadata)
|
||||||
{
|
{
|
||||||
metadata = default!;
|
metadata = default!;
|
||||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||||
userDataAddress == 0 ||
|
userDataAddress == 0 ||
|
||||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,8 @@ public static class Gen5ShaderMetadataReader
|
|||||||
var resourceOffsets = new ulong[ResourceClassCount];
|
var resourceOffsets = new ulong[ResourceClassCount];
|
||||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(
|
if (!TryReadUInt64(
|
||||||
|
ctx,
|
||||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||||
out resourceOffsets[resourceClass]))
|
out resourceOffsets[resourceClass]))
|
||||||
{
|
{
|
||||||
@@ -35,9 +37,9 @@ public static class Gen5ShaderMetadataReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||||
directResourceCount > MaxMetadataEntries)
|
directResourceCount > MaxMetadataEntries)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -46,7 +48,8 @@ public 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 (!ctx.TryReadUInt16(
|
if (!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)
|
||||||
@@ -65,7 +68,7 @@ public static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint type = 0; type < directResourceCount; type++)
|
for (uint type = 0; type < directResourceCount; type++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,8 @@ public static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint slot = 0; slot < count; slot++)
|
for (uint slot = 0; slot < count; slot++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt16(
|
if (!TryReadUInt16(
|
||||||
|
ctx,
|
||||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||||
out var sharp))
|
out var sharp))
|
||||||
{
|
{
|
||||||
@@ -121,4 +125,30 @@ public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -10,9 +11,83 @@ namespace SharpEmu.ShaderCompiler;
|
|||||||
|
|
||||||
public static class Gen5ShaderTranslator
|
public static class Gen5ShaderTranslator
|
||||||
{
|
{
|
||||||
|
private static int _dppVectorsValidated;
|
||||||
|
/// <summary>
|
||||||
|
/// Bitmask (256 bits) of scalar registers whose values the program can
|
||||||
|
/// observe: scalar source operands (widened for 64-bit pairs), the
|
||||||
|
/// descriptor/sampler/address ranges named by instruction controls, and
|
||||||
|
/// the implicit state registers. Deterministic per instruction stream, so
|
||||||
|
/// the SPIR-V that loads exactly these registers from the per-draw
|
||||||
|
/// initial-state buffer is byte-stable across draws.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong[] ComputeConsumedScalarMask(Gen5ShaderProgram program)
|
||||||
|
{
|
||||||
|
var mask = new ulong[4];
|
||||||
|
AddConsumedScalar(mask, 106, 2);
|
||||||
|
AddConsumedScalar(mask, 124, 2);
|
||||||
|
AddConsumedScalar(mask, 126, 2);
|
||||||
|
foreach (var instruction in program.Instructions)
|
||||||
|
{
|
||||||
|
foreach (var source in instruction.Sources)
|
||||||
|
{
|
||||||
|
if (source.Kind == Gen5OperandKind.ScalarRegister)
|
||||||
|
{
|
||||||
|
AddConsumedScalar(mask, source.Value, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar memory bases can be a 4-dword buffer descriptor.
|
||||||
|
if (instruction.Encoding is Gen5ShaderEncoding.Smem or Gen5ShaderEncoding.Smrd &&
|
||||||
|
instruction.Sources.Count > 0 &&
|
||||||
|
instruction.Sources[0].Kind == Gen5OperandKind.ScalarRegister)
|
||||||
|
{
|
||||||
|
AddConsumedScalar(mask, instruction.Sources[0].Value, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (instruction.Control)
|
||||||
|
{
|
||||||
|
case Gen5ImageControl image:
|
||||||
|
AddConsumedScalar(mask, image.ScalarResource, 8);
|
||||||
|
AddConsumedScalar(mask, image.ScalarSampler, 4);
|
||||||
|
break;
|
||||||
|
case Gen5ScalarMemoryControl { DynamicOffsetRegister: { } offsetRegister }:
|
||||||
|
AddConsumedScalar(mask, offsetRegister, 2);
|
||||||
|
break;
|
||||||
|
case Gen5GlobalMemoryControl global:
|
||||||
|
AddConsumedScalar(mask, global.ScalarAddress, 2);
|
||||||
|
break;
|
||||||
|
case Gen5BufferMemoryControl buffer:
|
||||||
|
AddConsumedScalar(mask, buffer.ScalarResource, 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddConsumedScalar(ulong[] mask, uint register, uint count)
|
||||||
|
{
|
||||||
|
for (uint index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var target = register + index;
|
||||||
|
if (target < 256)
|
||||||
|
{
|
||||||
|
mask[target >> 6] |= 1UL << (int)(target & 63);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsScalarConsumed(ulong[] mask, uint register) =>
|
||||||
|
register < 256 && (mask[register >> 6] & (1UL << (int)(register & 63))) != 0;
|
||||||
|
|
||||||
private const int MaxInstructions = 4096;
|
private const int MaxInstructions = 4096;
|
||||||
private const int MinimumUserDataDwords = 16;
|
private const uint PsUserDataRegister = 0x0C;
|
||||||
private const int MaximumUserDataDwords = 256;
|
private const uint VsUserDataRegister = 0x4C;
|
||||||
|
private const uint GsUserDataRegister = 0x8C;
|
||||||
|
private const uint EsUserDataRegister = 0xCC;
|
||||||
|
private const uint ComputeUserDataRegister = 0x240;
|
||||||
|
private const uint ComputePgmRsrc2Register = 0x213;
|
||||||
|
private const int MaximumHardwareUserSgprs = 64;
|
||||||
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
|
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
|
||||||
|
|
||||||
private sealed class ShaderDecodeCache
|
private sealed class ShaderDecodeCache
|
||||||
@@ -124,6 +199,7 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5ComputeSystemRegisters? computeSystemRegisters = null,
|
Gen5ComputeSystemRegisters? computeSystemRegisters = null,
|
||||||
uint userDataScalarRegisterBase = 0)
|
uint userDataScalarRegisterBase = 0)
|
||||||
{
|
{
|
||||||
|
ValidateUserSgprCountDecoding();
|
||||||
state = default!;
|
state = default!;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
|
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
|
||||||
@@ -172,7 +248,20 @@ public static class Gen5ShaderTranslator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userData = new uint[GetUserDataDwordCount(metadata)];
|
if (!TryGetUserSgprCount(
|
||||||
|
shaderRegisters,
|
||||||
|
userDataBaseRegister,
|
||||||
|
out var userSgprCount,
|
||||||
|
out var rsrc2Register,
|
||||||
|
out _))
|
||||||
|
{
|
||||||
|
error =
|
||||||
|
$"missing-user-sgpr-count ud_reg=0x{userDataBaseRegister:X} " +
|
||||||
|
$"rsrc2_reg=0x{rsrc2Register:X}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = new uint[userSgprCount];
|
||||||
for (uint index = 0; index < userData.Length; index++)
|
for (uint index = 0; index < userData.Length; index++)
|
||||||
{
|
{
|
||||||
shaderRegisters.TryGetValue(userDataBaseRegister + index, out userData[index]);
|
shaderRegisters.TryGetValue(userDataBaseRegister + index, out userData[index]);
|
||||||
@@ -187,35 +276,72 @@ public static class Gen5ShaderTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetUserDataDwordCount(Gen5ShaderMetadata? metadata)
|
private static bool TryGetUserSgprCount(
|
||||||
|
IReadOnlyDictionary<uint, uint> shaderRegisters,
|
||||||
|
uint userDataBaseRegister,
|
||||||
|
out int count,
|
||||||
|
out uint rsrc2Register,
|
||||||
|
out uint rsrc2)
|
||||||
{
|
{
|
||||||
var count = MinimumUserDataDwords;
|
rsrc2Register = userDataBaseRegister == ComputeUserDataRegister
|
||||||
if (metadata is null)
|
? ComputePgmRsrc2Register
|
||||||
|
: userDataBaseRegister - 1;
|
||||||
|
if (!shaderRegisters.TryGetValue(rsrc2Register, out rsrc2))
|
||||||
{
|
{
|
||||||
return count;
|
count = 0;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
count = Math.Max(count, checked((int)Math.Min(metadata.ExtendedUserDataSizeDwords, MaximumUserDataDwords)));
|
count = checked((int)((rsrc2 >> 1) & 0x1Fu));
|
||||||
count = Math.Max(count, checked((int)Math.Min(metadata.ShaderResourceTableSizeDwords, MaximumUserDataDwords)));
|
// GFX10 PS/VS/GS expose a sixth USER_SGPR bit. ES and compute do not.
|
||||||
|
// AGC's logical user-data layout (including its SRT pointer and back
|
||||||
foreach (var offset in metadata.DirectResources.Values)
|
// user data) describes memory reached through these SGPRs; it does not
|
||||||
|
// increase the hardware register window.
|
||||||
|
var hasUserSgprMsb = userDataBaseRegister is
|
||||||
|
PsUserDataRegister or VsUserDataRegister or GsUserDataRegister;
|
||||||
|
if (hasUserSgprMsb &&
|
||||||
|
(rsrc2 & (1u << 27)) != 0)
|
||||||
{
|
{
|
||||||
count = Math.Max(count, checked((int)Math.Min(offset + 1u, MaximumUserDataDwords)));
|
count |= 0x20;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var resource in metadata.Resources)
|
if (userDataBaseRegister is not (PsUserDataRegister or
|
||||||
|
VsUserDataRegister or
|
||||||
|
GsUserDataRegister or
|
||||||
|
EsUserDataRegister or
|
||||||
|
ComputeUserDataRegister) ||
|
||||||
|
count > MaximumHardwareUserSgprs)
|
||||||
{
|
{
|
||||||
var size = resource.Kind switch
|
count = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void ValidateUserSgprCountDecoding()
|
||||||
{
|
{
|
||||||
Gen5ShaderResourceKind.ReadOnlyTexture or Gen5ShaderResourceKind.ReadWriteTexture => 8u,
|
static int Decode(uint baseRegister, uint rsrc2)
|
||||||
Gen5ShaderResourceKind.Sampler => 4u,
|
{
|
||||||
Gen5ShaderResourceKind.ConstantBuffer => 4u,
|
var registers = new Dictionary<uint, uint>
|
||||||
_ => 1u,
|
{
|
||||||
|
[baseRegister == ComputeUserDataRegister
|
||||||
|
? ComputePgmRsrc2Register
|
||||||
|
: baseRegister - 1] = rsrc2,
|
||||||
};
|
};
|
||||||
count = Math.Max(count, checked((int)Math.Min(resource.OffsetDwords + size, MaximumUserDataDwords)));
|
var decoded =
|
||||||
|
TryGetUserSgprCount(registers, baseRegister, out var count, out _, out _);
|
||||||
|
Debug.Assert(decoded);
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
Debug.Assert(Decode(PsUserDataRegister, 2u << 1) == 2);
|
||||||
|
Debug.Assert(Decode(PsUserDataRegister, (3u << 1) | (1u << 27)) == 35);
|
||||||
|
Debug.Assert(Decode(VsUserDataRegister, (1u << 1) | (1u << 27)) == 33);
|
||||||
|
Debug.Assert(Decode(GsUserDataRegister, (4u << 1) | (1u << 27)) == 36);
|
||||||
|
Debug.Assert(Decode(EsUserDataRegister, (7u << 1) | (1u << 27)) == 7);
|
||||||
|
Debug.Assert(Decode(ComputeUserDataRegister, (11u << 1) | (1u << 27)) == 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DescribeState(Gen5ShaderState state)
|
public static string DescribeState(Gen5ShaderState state)
|
||||||
@@ -229,7 +355,8 @@ public static class Gen5ShaderTranslator
|
|||||||
if (state.Metadata is not { } metadata)
|
if (state.Metadata is not { } metadata)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
$"ud_base=s{state.UserDataScalarRegisterBase} ud[{userData}]" +
|
$"ud_base=s{state.UserDataScalarRegisterBase} hw_ud={state.UserData.Count} " +
|
||||||
|
$"ud[{userData}]" +
|
||||||
$"{systemRegisters} metadata=missing";
|
$"{systemRegisters} metadata=missing";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +369,8 @@ public static class Gen5ShaderTranslator
|
|||||||
$"{resource.Kind}[{resource.Slot}]@{resource.OffsetDwords}" +
|
$"{resource.Kind}[{resource.Slot}]@{resource.OffsetDwords}" +
|
||||||
(resource.SizeFlag ? "+" : string.Empty)));
|
(resource.SizeFlag ? "+" : string.Empty)));
|
||||||
return
|
return
|
||||||
$"ud_base=s{state.UserDataScalarRegisterBase} ud[{userData}]" +
|
$"ud_base=s{state.UserDataScalarRegisterBase} hw_ud={state.UserData.Count} " +
|
||||||
|
$"ud[{userData}]" +
|
||||||
$"{systemRegisters} metadata[eud={metadata.ExtendedUserDataSizeDwords}," +
|
$"{systemRegisters} metadata[eud={metadata.ExtendedUserDataSizeDwords}," +
|
||||||
$"srt={metadata.ShaderResourceTableSizeDwords},direct={direct},resources={resources}]";
|
$"srt={metadata.ShaderResourceTableSizeDwords},direct={direct},resources={resources}]";
|
||||||
}
|
}
|
||||||
@@ -283,6 +411,7 @@ public static class Gen5ShaderTranslator
|
|||||||
out Gen5ShaderProgram program,
|
out Gen5ShaderProgram program,
|
||||||
out string error)
|
out string error)
|
||||||
{
|
{
|
||||||
|
ValidateDppControlVectors();
|
||||||
program = new Gen5ShaderProgram(address, []);
|
program = new Gen5ShaderProgram(address, []);
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
if (address == 0)
|
if (address == 0)
|
||||||
@@ -295,7 +424,7 @@ public static class Gen5ShaderTranslator
|
|||||||
var instructionCount = 0;
|
var instructionCount = 0;
|
||||||
for (uint pc = 0; instructionCount < MaxInstructions;)
|
for (uint pc = 0; instructionCount < MaxInstructions;)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc, out var word))
|
if (!TryReadUInt32(ctx, address + pc, out var word))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc:X}";
|
error = $"read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -318,7 +447,7 @@ public static class Gen5ShaderTranslator
|
|||||||
words[0] = word;
|
words[0] = word;
|
||||||
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -340,6 +469,52 @@ public static class Gen5ShaderTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void ValidateDppControlVectors()
|
||||||
|
{
|
||||||
|
if (System.Threading.Interlocked.Exchange(ref _dppVectorsValidated, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static (uint Lane, bool InRange) Resolve(uint control, uint lane)
|
||||||
|
{
|
||||||
|
var rowBase = lane & ~15u;
|
||||||
|
var rowLane = lane & 15u;
|
||||||
|
return control switch
|
||||||
|
{
|
||||||
|
>= 0x101 and <= 0x10F => (
|
||||||
|
rowBase + ((rowLane + (control & 15)) & 15),
|
||||||
|
rowLane + (control & 15) < 16),
|
||||||
|
>= 0x111 and <= 0x11F => (
|
||||||
|
rowBase + ((rowLane - (control & 15)) & 15),
|
||||||
|
rowLane >= (control & 15)),
|
||||||
|
>= 0x121 and <= 0x12F => (
|
||||||
|
rowBase + ((rowLane - (control & 15)) & 15),
|
||||||
|
true),
|
||||||
|
0x140 => (rowBase + 15 - rowLane, true),
|
||||||
|
0x141 => ((lane & ~7u) + 7 - (lane & 7), true),
|
||||||
|
>= 0x150 and <= 0x15F => (rowBase + (control & 15), true),
|
||||||
|
>= 0x160 and <= 0x16F => (rowBase + (rowLane ^ (control & 15)), true),
|
||||||
|
_ => (lane, false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(Resolve(0x101, 0) == (1u, true));
|
||||||
|
Debug.Assert(Resolve(0x101, 15) == (0u, false));
|
||||||
|
Debug.Assert(Resolve(0x112, 1) == (15u, false));
|
||||||
|
Debug.Assert(Resolve(0x123, 0) == (13u, true));
|
||||||
|
Debug.Assert(Resolve(0x140, 18) == (29u, true));
|
||||||
|
Debug.Assert(Resolve(0x141, 9) == (14u, true));
|
||||||
|
Debug.Assert(Resolve(0x153, 20) == (19u, true));
|
||||||
|
Debug.Assert(Resolve(0x163, 22) == (21u, true));
|
||||||
|
const uint dpp8 =
|
||||||
|
(7u << 0) | (6u << 3) | (5u << 6) | (4u << 9) |
|
||||||
|
(3u << 12) | (2u << 15) | (1u << 18) | (0u << 21);
|
||||||
|
Debug.Assert(((dpp8 >> (0 * 3)) & 7) == 7);
|
||||||
|
Debug.Assert(((dpp8 >> (7 * 3)) & 7) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryDecodeInstruction(
|
private static bool TryDecodeInstruction(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
@@ -398,7 +573,7 @@ public static class Gen5ShaderTranslator
|
|||||||
case 0x34:
|
case 0x34:
|
||||||
case 0x35:
|
case 0x35:
|
||||||
encoding = Gen5ShaderEncoding.Vop3;
|
encoding = Gen5ShaderEncoding.Vop3;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3Extra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
|
||||||
{
|
{
|
||||||
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -419,7 +594,7 @@ public static class Gen5ShaderTranslator
|
|||||||
return DecodeFlat(word, out name, out sizeDwords, out error);
|
return DecodeFlat(word, out name, out sizeDwords, out error);
|
||||||
case 0x38:
|
case 0x38:
|
||||||
encoding = Gen5ShaderEncoding.Mubuf;
|
encoding = Gen5ShaderEncoding.Mubuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mubufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
|
||||||
{
|
{
|
||||||
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -428,7 +603,7 @@ public static class Gen5ShaderTranslator
|
|||||||
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
||||||
case 0x3A:
|
case 0x3A:
|
||||||
encoding = Gen5ShaderEncoding.Mtbuf;
|
encoding = Gen5ShaderEncoding.Mtbuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
||||||
{
|
{
|
||||||
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -455,6 +630,26 @@ public static class Gen5ShaderTranslator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kept beside the production decoder so offline compatibility tools use
|
||||||
|
// precisely the same opcode tables and instruction-width rules as runtime
|
||||||
|
// shader translation.
|
||||||
|
internal static bool TryDecodeInstructionForPreflight(
|
||||||
|
CpuContext ctx,
|
||||||
|
uint pc,
|
||||||
|
uint word,
|
||||||
|
out string name,
|
||||||
|
out uint sizeDwords,
|
||||||
|
out string error) =>
|
||||||
|
TryDecodeInstruction(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
pc,
|
||||||
|
word,
|
||||||
|
out _,
|
||||||
|
out name,
|
||||||
|
out sizeDwords,
|
||||||
|
out error);
|
||||||
|
|
||||||
private static bool DecodeSop(uint word, out string name, out uint sizeDwords, out string error)
|
private static bool DecodeSop(uint word, out string name, out uint sizeDwords, out string error)
|
||||||
{
|
{
|
||||||
var opcode = (word >> 23) & 0x7F;
|
var opcode = (word >> 23) & 0x7F;
|
||||||
@@ -498,6 +693,16 @@ public static class Gen5ShaderTranslator
|
|||||||
0x2B => "SXnorSaveexecB64",
|
0x2B => "SXnorSaveexecB64",
|
||||||
0x37 => "SAndn1SaveexecB64",
|
0x37 => "SAndn1SaveexecB64",
|
||||||
0x38 => "SOrn1SaveexecB64",
|
0x38 => "SOrn1SaveexecB64",
|
||||||
|
0x3C => "SAndSaveexecB32",
|
||||||
|
0x3D => "SOrSaveexecB32",
|
||||||
|
0x3E => "SXorSaveexecB32",
|
||||||
|
0x3F => "SAndn2SaveexecB32",
|
||||||
|
0x40 => "SOrn2SaveexecB32",
|
||||||
|
0x41 => "SNandSaveexecB32",
|
||||||
|
0x42 => "SNorSaveexecB32",
|
||||||
|
0x43 => "SXnorSaveexecB32",
|
||||||
|
0x44 => "SAndn1SaveexecB32",
|
||||||
|
0x45 => "SOrn1SaveexecB32",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -662,7 +867,7 @@ public static class Gen5ShaderTranslator
|
|||||||
{
|
{
|
||||||
var opcode = (word >> 9) & 0xFF;
|
var opcode = (word >> 9) & 0xFF;
|
||||||
var src0 = word & 0x1FF;
|
var src0 = word & 0x1FF;
|
||||||
sizeDwords = src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
sizeDwords = src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
@@ -721,7 +926,8 @@ public static class Gen5ShaderTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var src0 = word & 0x1FF;
|
var src0 = word & 0x1FF;
|
||||||
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D ||
|
||||||
|
src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
@@ -748,6 +954,7 @@ public static class Gen5ShaderTranslator
|
|||||||
0x1B => "VAndB32",
|
0x1B => "VAndB32",
|
||||||
0x1C => "VOrB32",
|
0x1C => "VOrB32",
|
||||||
0x1D => "VXorB32",
|
0x1D => "VXorB32",
|
||||||
|
0x1E => "VXnorB32",
|
||||||
0x1F => "VMacF32",
|
0x1F => "VMacF32",
|
||||||
0x20 => "VMadMkF32",
|
0x20 => "VMadMkF32",
|
||||||
0x21 => "VMadAkF32",
|
0x21 => "VMadAkF32",
|
||||||
@@ -761,8 +968,8 @@ public static class Gen5ShaderTranslator
|
|||||||
0x29 => "VSubbU32",
|
0x29 => "VSubbU32",
|
||||||
0x2A => "VSubbrevU32",
|
0x2A => "VSubbrevU32",
|
||||||
0x2B => "VFmacF32",
|
0x2B => "VFmacF32",
|
||||||
0x2C => "VFmamkF32",
|
0x2C => "VFmaMkF32",
|
||||||
0x2D => "VFmaakF32",
|
0x2D => "VFmaAkF32",
|
||||||
0x2F => "VCvtPkrtzF16F32",
|
0x2F => "VCvtPkrtzF16F32",
|
||||||
0x30 => "VCvtPkU16U32",
|
0x30 => "VCvtPkU16U32",
|
||||||
0x31 => "VCvtPkI16I32",
|
0x31 => "VCvtPkI16I32",
|
||||||
@@ -776,7 +983,7 @@ public static class Gen5ShaderTranslator
|
|||||||
{
|
{
|
||||||
var opcode = (word >> 17) & 0xFF;
|
var opcode = (word >> 17) & 0xFF;
|
||||||
var src0 = word & 0x1FF;
|
var src0 = word & 0x1FF;
|
||||||
sizeDwords = src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
sizeDwords = src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
@@ -868,9 +1075,11 @@ public static class Gen5ShaderTranslator
|
|||||||
name = isVop3B
|
name = isVop3B
|
||||||
? opcode switch
|
? opcode switch
|
||||||
{
|
{
|
||||||
|
0x128 => "VAddCoCiU32",
|
||||||
0x30F => "VAddCoU32",
|
0x30F => "VAddCoU32",
|
||||||
0x310 => "VSubCoU32",
|
0x310 => "VSubCoU32",
|
||||||
0x319 => "VSubrevCoU32",
|
0x319 => "VSubrevCoU32",
|
||||||
|
0x176 => "VMadU64U32",
|
||||||
_ => $"Vop3bRaw{opcode:X3}",
|
_ => $"Vop3bRaw{opcode:X3}",
|
||||||
}
|
}
|
||||||
: opcode switch
|
: opcode switch
|
||||||
@@ -914,6 +1123,12 @@ public static class Gen5ShaderTranslator
|
|||||||
0x360 => "VReadlaneB32",
|
0x360 => "VReadlaneB32",
|
||||||
0x361 => "VWritelaneB32",
|
0x361 => "VWritelaneB32",
|
||||||
0x362 => "VLdexpF32",
|
0x362 => "VLdexpF32",
|
||||||
|
0x363 => "VBfmB32",
|
||||||
|
0x364 => "VBcntU32B32",
|
||||||
|
0x365 => "VMbcntLoU32B32",
|
||||||
|
0x366 => "VMbcntHiU32B32",
|
||||||
|
0x368 => "VCvtPknormI16F32",
|
||||||
|
0x369 => "VCvtPknormU16F32",
|
||||||
0x373 => "VMadU32U16",
|
0x373 => "VMadU32U16",
|
||||||
0x346 => "VLshlAddU32",
|
0x346 => "VLshlAddU32",
|
||||||
0x347 => "VAddLshlU32",
|
0x347 => "VAddLshlU32",
|
||||||
@@ -930,7 +1145,7 @@ public static class Gen5ShaderTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsVop3BOpcode(uint opcode) =>
|
private static bool IsVop3BOpcode(uint opcode) =>
|
||||||
opcode is 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319;
|
opcode is 0x128 or 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319;
|
||||||
|
|
||||||
private static bool DecodeRaw2(
|
private static bool DecodeRaw2(
|
||||||
uint word,
|
uint word,
|
||||||
@@ -956,6 +1171,7 @@ public static class Gen5ShaderTranslator
|
|||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
|
0x00 => "DsAddU32",
|
||||||
0x0D => "DsWriteB32",
|
0x0D => "DsWriteB32",
|
||||||
0x0E => "DsWrite2B32",
|
0x0E => "DsWrite2B32",
|
||||||
0x0F => "DsWrite2St64B32",
|
0x0F => "DsWrite2St64B32",
|
||||||
@@ -964,6 +1180,10 @@ public static class Gen5ShaderTranslator
|
|||||||
0x37 => "DsRead2B32",
|
0x37 => "DsRead2B32",
|
||||||
0x38 => "DsRead2St64B32",
|
0x38 => "DsRead2St64B32",
|
||||||
0x4D => "DsWriteB64",
|
0x4D => "DsWriteB64",
|
||||||
|
0xDE => "DsWriteB96",
|
||||||
|
0xDF => "DsWriteB128",
|
||||||
|
0xFE => "DsReadB96",
|
||||||
|
0xFF => "DsReadB128",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1026,15 +1246,34 @@ public static class Gen5ShaderTranslator
|
|||||||
0x01 => "BufferLoadFormatXy",
|
0x01 => "BufferLoadFormatXy",
|
||||||
0x02 => "BufferLoadFormatXyz",
|
0x02 => "BufferLoadFormatXyz",
|
||||||
0x03 => "BufferLoadFormatXyzw",
|
0x03 => "BufferLoadFormatXyzw",
|
||||||
|
0x04 => "BufferStoreFormatX",
|
||||||
|
0x05 => "BufferStoreFormatXy",
|
||||||
|
0x06 => "BufferStoreFormatXyz",
|
||||||
|
0x07 => "BufferStoreFormatXyzw",
|
||||||
|
0x08 => "BufferLoadUbyte",
|
||||||
|
0x09 => "BufferLoadSbyte",
|
||||||
|
0x0A => "BufferLoadUshort",
|
||||||
|
0x0B => "BufferLoadSshort",
|
||||||
0x0C => "BufferLoadDword",
|
0x0C => "BufferLoadDword",
|
||||||
0x0D => "BufferLoadDwordx2",
|
0x0D => "BufferLoadDwordx2",
|
||||||
0x0E => "BufferLoadDwordx4",
|
0x0E => "BufferLoadDwordx4",
|
||||||
0x0F => "BufferLoadDwordx3",
|
0x0F => "BufferLoadDwordx3",
|
||||||
|
0x18 => "BufferStoreByte",
|
||||||
|
0x19 => "BufferStoreByteD16Hi",
|
||||||
|
0x1A => "BufferStoreShort",
|
||||||
|
0x1B => "BufferStoreShortD16Hi",
|
||||||
0x1C => "BufferStoreDword",
|
0x1C => "BufferStoreDword",
|
||||||
0x1D => "BufferStoreDwordx2",
|
0x1D => "BufferStoreDwordx2",
|
||||||
0x1E => "BufferStoreDwordx4",
|
0x1E => "BufferStoreDwordx4",
|
||||||
0x1F => "BufferStoreDwordx3",
|
0x1F => "BufferStoreDwordx3",
|
||||||
|
0x20 => "BufferLoadUbyteD16",
|
||||||
|
0x21 => "BufferLoadUbyteD16Hi",
|
||||||
|
0x22 => "BufferLoadSbyteD16",
|
||||||
|
0x23 => "BufferLoadSbyteD16Hi",
|
||||||
|
0x24 => "BufferLoadShortD16",
|
||||||
|
0x25 => "BufferLoadShortD16Hi",
|
||||||
0x32 => "BufferAtomicAdd",
|
0x32 => "BufferAtomicAdd",
|
||||||
|
0x38 => "BufferAtomicUMax",
|
||||||
_ => $"MubufRaw{opcode:X2}",
|
_ => $"MubufRaw{opcode:X2}",
|
||||||
};
|
};
|
||||||
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
|
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
|
||||||
@@ -1055,10 +1294,30 @@ public static class Gen5ShaderTranslator
|
|||||||
name = segment == 0x2
|
name = segment == 0x2
|
||||||
? opcode switch
|
? opcode switch
|
||||||
{
|
{
|
||||||
|
0x08 => "GlobalLoadUbyte",
|
||||||
|
0x09 => "GlobalLoadSbyte",
|
||||||
|
0x0A => "GlobalLoadUshort",
|
||||||
|
0x0B => "GlobalLoadSshort",
|
||||||
0x0C => "GlobalLoadDword",
|
0x0C => "GlobalLoadDword",
|
||||||
0x0D => "GlobalLoadDwordx2",
|
0x0D => "GlobalLoadDwordx2",
|
||||||
0x0E => "GlobalLoadDwordx4",
|
0x0E => "GlobalLoadDwordx4",
|
||||||
0x0F => "GlobalLoadDwordx3",
|
0x0F => "GlobalLoadDwordx3",
|
||||||
|
0x18 => "GlobalStoreByte",
|
||||||
|
0x19 => "GlobalStoreByteD16Hi",
|
||||||
|
0x1A => "GlobalStoreShort",
|
||||||
|
0x1B => "GlobalStoreShortD16Hi",
|
||||||
|
0x1C => "GlobalStoreDword",
|
||||||
|
0x1D => "GlobalStoreDwordx2",
|
||||||
|
0x1E => "GlobalStoreDwordx4",
|
||||||
|
0x1F => "GlobalStoreDwordx3",
|
||||||
|
0x20 => "GlobalLoadUbyteD16",
|
||||||
|
0x21 => "GlobalLoadUbyteD16Hi",
|
||||||
|
0x22 => "GlobalLoadSbyteD16",
|
||||||
|
0x23 => "GlobalLoadSbyteD16Hi",
|
||||||
|
0x24 => "GlobalLoadShortD16",
|
||||||
|
0x25 => "GlobalLoadShortD16Hi",
|
||||||
|
0x32 => "GlobalAtomicAdd",
|
||||||
|
0x38 => "GlobalAtomicUMax",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
}
|
}
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
@@ -1132,6 +1391,7 @@ public static class Gen5ShaderTranslator
|
|||||||
0x10 => "ImageAtomicCmpswap",
|
0x10 => "ImageAtomicCmpswap",
|
||||||
0x1C => "ImageAtomicDec",
|
0x1C => "ImageAtomicDec",
|
||||||
0x20 => "ImageSample",
|
0x20 => "ImageSample",
|
||||||
|
0x22 => "ImageSampleD",
|
||||||
0x24 => "ImageSampleL",
|
0x24 => "ImageSampleL",
|
||||||
0x25 => "ImageSampleB",
|
0x25 => "ImageSampleB",
|
||||||
0x27 => "ImageSampleLz",
|
0x27 => "ImageSampleLz",
|
||||||
@@ -1143,6 +1403,7 @@ public static class Gen5ShaderTranslator
|
|||||||
0x47 => "ImageGather4Lz",
|
0x47 => "ImageGather4Lz",
|
||||||
0x48 => "ImageGather4C",
|
0x48 => "ImageGather4C",
|
||||||
0x4E => "ImageGather4CBCl",
|
0x4E => "ImageGather4CBCl",
|
||||||
|
0x57 => "ImageGather4LzO",
|
||||||
0x5F => "ImageGather4CLzO",
|
0x5F => "ImageGather4CLzO",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
@@ -1223,6 +1484,7 @@ public static class Gen5ShaderTranslator
|
|||||||
name.StartsWith("Image", StringComparison.Ordinal);
|
name.StartsWith("Image", StringComparison.Ordinal);
|
||||||
|
|
||||||
public static bool IsStorageImageOperation(string name) =>
|
public static bool IsStorageImageOperation(string name) =>
|
||||||
|
name.StartsWith("ImageLoad", StringComparison.Ordinal) ||
|
||||||
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
|
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
|
||||||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
|
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
|
||||||
|
|
||||||
@@ -1239,7 +1501,11 @@ public static class Gen5ShaderTranslator
|
|||||||
var isDpp =
|
var isDpp =
|
||||||
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
|
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
|
||||||
(word & 0x1FF) == 0xFA;
|
(word & 0x1FF) == 0xFA;
|
||||||
var literal = !isSdwa && !isDpp && words.Length > MinimumEncodingDwords(encoding)
|
var isDpp8 =
|
||||||
|
encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc &&
|
||||||
|
(word & 0x1FF) is 0xE9 or 0xEA;
|
||||||
|
var literal = !isSdwa && !isDpp && !isDpp8 &&
|
||||||
|
words.Length > MinimumEncodingDwords(encoding)
|
||||||
? words[^1]
|
? words[^1]
|
||||||
: (uint?)null;
|
: (uint?)null;
|
||||||
IReadOnlyList<Gen5Operand> sources = [];
|
IReadOnlyList<Gen5Operand> sources = [];
|
||||||
@@ -1314,20 +1580,36 @@ public static class Gen5ShaderTranslator
|
|||||||
var scalarOffset = (extra >> 25) & 0x7F;
|
var scalarOffset = (extra >> 25) & 0x7F;
|
||||||
var offset = SignExtend(extra & 0x1FFFFF, 21);
|
var offset = SignExtend(extra & 0x1FFFFF, 21);
|
||||||
var count = ScalarLoadDwordCount(opcode);
|
var count = ScalarLoadDwordCount(opcode);
|
||||||
|
var scalarOffsetOperand = Gen5Operand.Source(scalarOffset);
|
||||||
|
var dynamicOffsetRegister = scalarOffsetOperand.Kind ==
|
||||||
|
Gen5OperandKind.ScalarRegister
|
||||||
|
? scalarOffsetOperand.Value
|
||||||
|
: (uint?)null;
|
||||||
sources =
|
sources =
|
||||||
[
|
[
|
||||||
Gen5Operand.Scalar(scalarBase),
|
Gen5Operand.Scalar(scalarBase),
|
||||||
Gen5Operand.Scalar(scalarOffset),
|
scalarOffsetOperand,
|
||||||
];
|
];
|
||||||
destinations = Enumerable
|
destinations = Enumerable
|
||||||
.Range((int)scalarDestination, checked((int)count))
|
.Range((int)scalarDestination, checked((int)count))
|
||||||
.Select(index => Gen5Operand.Scalar((uint)index))
|
.Select(index => Gen5Operand.Scalar((uint)index))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
control = new Gen5ScalarMemoryControl(count, offset, scalarOffset);
|
control = new Gen5ScalarMemoryControl(
|
||||||
|
count,
|
||||||
|
offset,
|
||||||
|
dynamicOffsetRegister);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Gen5ShaderEncoding.Vop1:
|
case Gen5ShaderEncoding.Vop1:
|
||||||
if (isDpp)
|
if (isDpp8)
|
||||||
|
{
|
||||||
|
var extra = words[1];
|
||||||
|
sources = [Gen5Operand.Vector(extra & 0xFF)];
|
||||||
|
control = new Gen5Dpp8Control(
|
||||||
|
extra >> 8,
|
||||||
|
(word & 0x1FF) == 0xEA);
|
||||||
|
}
|
||||||
|
else if (isDpp)
|
||||||
{
|
{
|
||||||
var extra = words[1];
|
var extra = words[1];
|
||||||
sources = [Gen5Operand.Vector(extra & 0xFF)];
|
sources = [Gen5Operand.Vector(extra & 0xFF)];
|
||||||
@@ -1339,24 +1621,36 @@ public static class Gen5ShaderTranslator
|
|||||||
var source0 = (extra & 0xFF) +
|
var source0 = (extra & 0xFF) +
|
||||||
((((extra >> 23) & 1) == 0) ? 256u : 0u);
|
((((extra >> 23) & 1) == 0) ? 256u : 0u);
|
||||||
sources = [Gen5Operand.Source(source0)];
|
sources = [Gen5Operand.Source(source0)];
|
||||||
control = new Gen5SdwaControl(
|
control = CreateSdwaControl(extra, isCompare: false, hasSource1: false);
|
||||||
(extra >> 8) & 0x7,
|
|
||||||
(extra >> 16) & 0x7,
|
|
||||||
6,
|
|
||||||
(extra >> 21) & 1,
|
|
||||||
(extra >> 20) & 1,
|
|
||||||
(extra >> 14) & 0x3,
|
|
||||||
((extra >> 13) & 1) != 0);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sources = [Gen5Operand.Source(word & 0x1FF, literal)];
|
sources = [Gen5Operand.Source(word & 0x1FF, literal)];
|
||||||
}
|
}
|
||||||
|
|
||||||
destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)];
|
// V_READFIRSTLANE_B32 is encoded as VOP1, but its destination
|
||||||
|
// field names an SGPR rather than a VGPR. Treating it like an
|
||||||
|
// ordinary vector destination leaves every invocation with a
|
||||||
|
// different value and corrupts scalar addresses derived from
|
||||||
|
// lane data.
|
||||||
|
destinations = opcode == "VReadfirstlaneB32"
|
||||||
|
? [Gen5Operand.Scalar((word >> 17) & 0x7F)]
|
||||||
|
: [Gen5Operand.Vector((word >> 17) & 0xFF)];
|
||||||
break;
|
break;
|
||||||
case Gen5ShaderEncoding.Vop2:
|
case Gen5ShaderEncoding.Vop2:
|
||||||
if (isDpp)
|
if (isDpp8)
|
||||||
|
{
|
||||||
|
var extra = words[1];
|
||||||
|
sources =
|
||||||
|
[
|
||||||
|
Gen5Operand.Vector(extra & 0xFF),
|
||||||
|
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||||
|
];
|
||||||
|
control = new Gen5Dpp8Control(
|
||||||
|
extra >> 8,
|
||||||
|
(word & 0x1FF) == 0xEA);
|
||||||
|
}
|
||||||
|
else if (isDpp)
|
||||||
{
|
{
|
||||||
var extra = words[1];
|
var extra = words[1];
|
||||||
sources =
|
sources =
|
||||||
@@ -1378,14 +1672,7 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Source(source0),
|
Gen5Operand.Source(source0),
|
||||||
Gen5Operand.Source(source1),
|
Gen5Operand.Source(source1),
|
||||||
];
|
];
|
||||||
control = new Gen5SdwaControl(
|
control = CreateSdwaControl(extra, isCompare: false, hasSource1: true);
|
||||||
(extra >> 8) & 0x7,
|
|
||||||
(extra >> 16) & 0x7,
|
|
||||||
(extra >> 24) & 0x7,
|
|
||||||
((extra >> 21) & 1) | (((extra >> 29) & 1) << 1),
|
|
||||||
((extra >> 20) & 1) | (((extra >> 28) & 1) << 1),
|
|
||||||
(extra >> 14) & 0x3,
|
|
||||||
((extra >> 13) & 1) != 0);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1394,7 +1681,7 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Source(word & 0x1FF, literal),
|
Gen5Operand.Source(word & 0x1FF, literal),
|
||||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||||
];
|
];
|
||||||
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
|
if ((opcode is "VMadMkF32" or "VFmaMkF32") && literal.HasValue)
|
||||||
{
|
{
|
||||||
sources =
|
sources =
|
||||||
[
|
[
|
||||||
@@ -1403,7 +1690,7 @@ public static class Gen5ShaderTranslator
|
|||||||
sources[1],
|
sources[1],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
|
else if ((opcode is "VMadAkF32" or "VFmaAkF32") && literal.HasValue)
|
||||||
{
|
{
|
||||||
sources =
|
sources =
|
||||||
[
|
[
|
||||||
@@ -1416,7 +1703,19 @@ public static class Gen5ShaderTranslator
|
|||||||
destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)];
|
destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)];
|
||||||
break;
|
break;
|
||||||
case Gen5ShaderEncoding.Vopc:
|
case Gen5ShaderEncoding.Vopc:
|
||||||
if (isDpp)
|
if (isDpp8)
|
||||||
|
{
|
||||||
|
var extra = words[1];
|
||||||
|
sources =
|
||||||
|
[
|
||||||
|
Gen5Operand.Vector(extra & 0xFF),
|
||||||
|
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||||
|
];
|
||||||
|
control = new Gen5Dpp8Control(
|
||||||
|
extra >> 8,
|
||||||
|
(word & 0x1FF) == 0xEA);
|
||||||
|
}
|
||||||
|
else if (isDpp)
|
||||||
{
|
{
|
||||||
var extra = words[1];
|
var extra = words[1];
|
||||||
sources =
|
sources =
|
||||||
@@ -1439,14 +1738,13 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Source(source0),
|
Gen5Operand.Source(source0),
|
||||||
Gen5Operand.Source(source1),
|
Gen5Operand.Source(source1),
|
||||||
];
|
];
|
||||||
control = new Gen5SdwaControl(
|
var sdwa = CreateSdwaControl(extra, isCompare: true, hasSource1: true);
|
||||||
(extra >> 8) & 0x7,
|
control = sdwa;
|
||||||
(extra >> 16) & 0x7,
|
if (sdwa.ScalarDestination is { } scalarDestination &&
|
||||||
(extra >> 24) & 0x7,
|
scalarDestination != 106)
|
||||||
((extra >> 21) & 1) | (((extra >> 29) & 1) << 1),
|
{
|
||||||
((extra >> 20) & 1) | (((extra >> 28) & 1) << 1),
|
destinations = [Gen5Operand.Scalar(scalarDestination)];
|
||||||
(extra >> 14) & 0x3,
|
}
|
||||||
((extra >> 13) & 1) != 0);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1479,6 +1777,7 @@ public static class Gen5ShaderTranslator
|
|||||||
(extra >> 29) & 0x7,
|
(extra >> 29) & 0x7,
|
||||||
(extra >> 27) & 0x3,
|
(extra >> 27) & 0x3,
|
||||||
((word >> 15) & 1) != 0,
|
((word >> 15) & 1) != 0,
|
||||||
|
isVop3B ? 0 : (word >> 11) & 0xF,
|
||||||
isVop3B ? (word >> 8) & 0x7F : null);
|
isVop3B ? (word >> 8) & 0x7F : null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1495,6 +1794,10 @@ public static class Gen5ShaderTranslator
|
|||||||
((word >> 17) & 1) != 0);
|
((word >> 17) & 1) != 0);
|
||||||
sources = opcode switch
|
sources = opcode switch
|
||||||
{
|
{
|
||||||
|
"DsAddU32" => [
|
||||||
|
Gen5Operand.Vector(vectorAddress),
|
||||||
|
Gen5Operand.Vector(vectorData0),
|
||||||
|
],
|
||||||
"DsWriteB32" => [
|
"DsWriteB32" => [
|
||||||
Gen5Operand.Vector(vectorAddress),
|
Gen5Operand.Vector(vectorAddress),
|
||||||
Gen5Operand.Vector(vectorData0),
|
Gen5Operand.Vector(vectorData0),
|
||||||
@@ -1504,6 +1807,19 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Vector(vectorData0),
|
Gen5Operand.Vector(vectorData0),
|
||||||
Gen5Operand.Vector(vectorData0 + 1),
|
Gen5Operand.Vector(vectorData0 + 1),
|
||||||
],
|
],
|
||||||
|
"DsWriteB96" => [
|
||||||
|
Gen5Operand.Vector(vectorAddress),
|
||||||
|
Gen5Operand.Vector(vectorData0),
|
||||||
|
Gen5Operand.Vector(vectorData0 + 1),
|
||||||
|
Gen5Operand.Vector(vectorData0 + 2),
|
||||||
|
],
|
||||||
|
"DsWriteB128" => [
|
||||||
|
Gen5Operand.Vector(vectorAddress),
|
||||||
|
Gen5Operand.Vector(vectorData0),
|
||||||
|
Gen5Operand.Vector(vectorData0 + 1),
|
||||||
|
Gen5Operand.Vector(vectorData0 + 2),
|
||||||
|
Gen5Operand.Vector(vectorData0 + 3),
|
||||||
|
],
|
||||||
"DsWrite2B32" or "DsWrite2St64B32" => [
|
"DsWrite2B32" or "DsWrite2St64B32" => [
|
||||||
Gen5Operand.Vector(vectorAddress),
|
Gen5Operand.Vector(vectorAddress),
|
||||||
Gen5Operand.Vector(vectorData0),
|
Gen5Operand.Vector(vectorData0),
|
||||||
@@ -1521,6 +1837,17 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Vector(vectorDestination),
|
Gen5Operand.Vector(vectorDestination),
|
||||||
Gen5Operand.Vector(vectorDestination + 1),
|
Gen5Operand.Vector(vectorDestination + 1),
|
||||||
],
|
],
|
||||||
|
"DsReadB96" => [
|
||||||
|
Gen5Operand.Vector(vectorDestination),
|
||||||
|
Gen5Operand.Vector(vectorDestination + 1),
|
||||||
|
Gen5Operand.Vector(vectorDestination + 2),
|
||||||
|
],
|
||||||
|
"DsReadB128" => [
|
||||||
|
Gen5Operand.Vector(vectorDestination),
|
||||||
|
Gen5Operand.Vector(vectorDestination + 1),
|
||||||
|
Gen5Operand.Vector(vectorDestination + 2),
|
||||||
|
Gen5Operand.Vector(vectorDestination + 3),
|
||||||
|
],
|
||||||
_ => [],
|
_ => [],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -1540,10 +1867,30 @@ public static class Gen5ShaderTranslator
|
|||||||
var scalarAddress = (extra >> 16) & 0x7F;
|
var scalarAddress = (extra >> 16) & 0x7F;
|
||||||
var dwordCount = opcode switch
|
var dwordCount = opcode switch
|
||||||
{
|
{
|
||||||
|
"GlobalLoadUbyte" or
|
||||||
|
"GlobalLoadSbyte" or
|
||||||
|
"GlobalLoadUshort" or
|
||||||
|
"GlobalLoadSshort" or
|
||||||
|
"GlobalLoadUbyteD16" or
|
||||||
|
"GlobalLoadUbyteD16Hi" or
|
||||||
|
"GlobalLoadSbyteD16" or
|
||||||
|
"GlobalLoadSbyteD16Hi" or
|
||||||
|
"GlobalLoadShortD16" or
|
||||||
|
"GlobalLoadShortD16Hi" or
|
||||||
|
"GlobalStoreByte" or
|
||||||
|
"GlobalStoreByteD16Hi" or
|
||||||
|
"GlobalStoreShort" or
|
||||||
|
"GlobalStoreShortD16Hi" or
|
||||||
|
"GlobalStoreDword" or
|
||||||
|
"GlobalAtomicAdd" or
|
||||||
|
"GlobalAtomicUMax" => 1u,
|
||||||
"GlobalLoadDword" => 1u,
|
"GlobalLoadDword" => 1u,
|
||||||
"GlobalLoadDwordx2" => 2u,
|
"GlobalLoadDwordx2" => 2u,
|
||||||
"GlobalLoadDwordx3" => 3u,
|
"GlobalLoadDwordx3" => 3u,
|
||||||
"GlobalLoadDwordx4" => 4u,
|
"GlobalLoadDwordx4" => 4u,
|
||||||
|
"GlobalStoreDwordx2" => 2u,
|
||||||
|
"GlobalStoreDwordx3" => 3u,
|
||||||
|
"GlobalStoreDwordx4" => 4u,
|
||||||
_ => 0u,
|
_ => 0u,
|
||||||
};
|
};
|
||||||
sources =
|
sources =
|
||||||
@@ -1551,10 +1898,12 @@ public static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Vector(vectorAddress),
|
Gen5Operand.Vector(vectorAddress),
|
||||||
Gen5Operand.Scalar(scalarAddress),
|
Gen5Operand.Scalar(scalarAddress),
|
||||||
];
|
];
|
||||||
destinations = Enumerable
|
destinations = opcode.StartsWith("GlobalLoad", StringComparison.Ordinal)
|
||||||
|
? Enumerable
|
||||||
.Range((int)vectorData, checked((int)dwordCount))
|
.Range((int)vectorData, checked((int)dwordCount))
|
||||||
.Select(index => Gen5Operand.Vector((uint)index))
|
.Select(index => Gen5Operand.Vector((uint)index))
|
||||||
.ToArray();
|
.ToArray()
|
||||||
|
: [];
|
||||||
control = new Gen5GlobalMemoryControl(
|
control = new Gen5GlobalMemoryControl(
|
||||||
dwordCount,
|
dwordCount,
|
||||||
vectorAddress,
|
vectorAddress,
|
||||||
@@ -1578,6 +1927,24 @@ public static class Gen5ShaderTranslator
|
|||||||
"BufferLoadFormatXy" => 2u,
|
"BufferLoadFormatXy" => 2u,
|
||||||
"BufferLoadFormatXyz" => 3u,
|
"BufferLoadFormatXyz" => 3u,
|
||||||
"BufferLoadFormatXyzw" => 4u,
|
"BufferLoadFormatXyzw" => 4u,
|
||||||
|
"BufferStoreFormatX" => 1u,
|
||||||
|
"BufferStoreFormatXy" => 2u,
|
||||||
|
"BufferStoreFormatXyz" => 3u,
|
||||||
|
"BufferStoreFormatXyzw" => 4u,
|
||||||
|
"BufferLoadUbyte" or
|
||||||
|
"BufferLoadSbyte" or
|
||||||
|
"BufferLoadUshort" or
|
||||||
|
"BufferLoadSshort" or
|
||||||
|
"BufferStoreByte" or
|
||||||
|
"BufferStoreByteD16Hi" or
|
||||||
|
"BufferStoreShort" or
|
||||||
|
"BufferStoreShortD16Hi" or
|
||||||
|
"BufferLoadUbyteD16" or
|
||||||
|
"BufferLoadUbyteD16Hi" or
|
||||||
|
"BufferLoadSbyteD16" or
|
||||||
|
"BufferLoadSbyteD16Hi" or
|
||||||
|
"BufferLoadShortD16" or
|
||||||
|
"BufferLoadShortD16Hi" => 1u,
|
||||||
"BufferLoadDword" => 1u,
|
"BufferLoadDword" => 1u,
|
||||||
"BufferLoadDwordx2" => 2u,
|
"BufferLoadDwordx2" => 2u,
|
||||||
"BufferLoadDwordx3" => 3u,
|
"BufferLoadDwordx3" => 3u,
|
||||||
@@ -1587,6 +1954,7 @@ public static class Gen5ShaderTranslator
|
|||||||
"BufferStoreDwordx3" => 3u,
|
"BufferStoreDwordx3" => 3u,
|
||||||
"BufferStoreDwordx4" => 4u,
|
"BufferStoreDwordx4" => 4u,
|
||||||
"BufferAtomicAdd" => 1u,
|
"BufferAtomicAdd" => 1u,
|
||||||
|
"BufferAtomicUMax" => 1u,
|
||||||
_ => 0u,
|
_ => 0u,
|
||||||
};
|
};
|
||||||
sources =
|
sources =
|
||||||
@@ -1690,7 +2058,9 @@ public static class Gen5ShaderTranslator
|
|||||||
dimension,
|
dimension,
|
||||||
dimension is 4 or 5 or 7,
|
dimension is 4 or 5 or 7,
|
||||||
((word >> 13) & 1) != 0,
|
((word >> 13) & 1) != 0,
|
||||||
((word >> 25) & 1) != 0);
|
((word >> 25) & 1) != 0,
|
||||||
|
((extra >> 30) & 1) != 0,
|
||||||
|
((extra >> 31) & 1) != 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Gen5ShaderEncoding.Exp:
|
case Gen5ShaderEncoding.Exp:
|
||||||
@@ -1726,6 +2096,30 @@ public static class Gen5ShaderTranslator
|
|||||||
(word >> 24) & 0xF,
|
(word >> 24) & 0xF,
|
||||||
(word >> 28) & 0xF);
|
(word >> 28) & 0xF);
|
||||||
|
|
||||||
|
private static Gen5SdwaControl CreateSdwaControl(
|
||||||
|
uint word,
|
||||||
|
bool isCompare,
|
||||||
|
bool hasSource1)
|
||||||
|
{
|
||||||
|
var scalarDestination = isCompare
|
||||||
|
? ((word >> 15) & 1) != 0
|
||||||
|
? (word >> 8) & 0x7Fu
|
||||||
|
: 106u
|
||||||
|
: (uint?)null;
|
||||||
|
return new Gen5SdwaControl(
|
||||||
|
isCompare ? 6u : (word >> 8) & 0x7u,
|
||||||
|
isCompare ? 0u : (word >> 11) & 0x3u,
|
||||||
|
(word >> 16) & 0x7u,
|
||||||
|
hasSource1 ? (word >> 24) & 0x7u : 6u,
|
||||||
|
((word >> 19) & 1) != 0,
|
||||||
|
hasSource1 && ((word >> 27) & 1) != 0,
|
||||||
|
((word >> 21) & 1) | (hasSource1 ? ((word >> 29) & 1) << 1 : 0),
|
||||||
|
((word >> 20) & 1) | (hasSource1 ? ((word >> 28) & 1) << 1 : 0),
|
||||||
|
isCompare ? 0u : (word >> 14) & 0x3u,
|
||||||
|
!isCompare && ((word >> 13) & 1) != 0,
|
||||||
|
scalarDestination);
|
||||||
|
}
|
||||||
|
|
||||||
private static int MinimumEncodingDwords(Gen5ShaderEncoding encoding) => encoding switch
|
private static int MinimumEncodingDwords(Gen5ShaderEncoding encoding) => encoding switch
|
||||||
{
|
{
|
||||||
Gen5ShaderEncoding.Vop3 or
|
Gen5ShaderEncoding.Vop3 or
|
||||||
@@ -1762,6 +2156,19 @@ public static class Gen5ShaderTranslator
|
|||||||
counts[key] = count + 1;
|
counts[key] = count + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct ShaderDecodeInfo(
|
private readonly record struct ShaderDecodeInfo(
|
||||||
int InstructionCount,
|
int InstructionCount,
|
||||||
Dictionary<string, int> Counts,
|
Dictionary<string, int> Counts,
|
||||||
@@ -1824,6 +2231,7 @@ public static class Gen5ShaderTranslator
|
|||||||
$"va={addressRegisters},vd=v{image.VectorData}," +
|
$"va={addressRegisters},vd=v{image.VectorData}," +
|
||||||
$"sr=s{image.ScalarResource},ss=s{image.ScalarSampler}," +
|
$"sr=s{image.ScalarResource},ss=s{image.ScalarSampler}," +
|
||||||
$"dim={image.Dimension},da={(image.IsArray ? 1 : 0)}," +
|
$"dim={image.Dimension},da={(image.IsArray ? 1 : 0)}," +
|
||||||
|
$"a16={(image.A16 ? 1 : 0)},d16={(image.D16 ? 1 : 0)}," +
|
||||||
$"glc={(image.Glc ? 1 : 0)}," +
|
$"glc={(image.Glc ? 1 : 0)}," +
|
||||||
$"slc={(image.Slc ? 1 : 0)}";
|
$"slc={(image.Slc ? 1 : 0)}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the RDNA2 unified FORMAT field used by GFX10 buffer and image
|
||||||
|
/// descriptors into the data-format/number-format pair used by the rest of
|
||||||
|
/// SharpEmu's AGC pipeline.
|
||||||
|
/// </summary>
|
||||||
|
public static class Gfx10UnifiedFormat
|
||||||
|
{
|
||||||
|
public static bool TryDecode(
|
||||||
|
uint unifiedFormat,
|
||||||
|
out uint dataFormat,
|
||||||
|
out uint numberFormat)
|
||||||
|
{
|
||||||
|
// RDNA2 ISA table 47 is intentionally sparse. In particular, the
|
||||||
|
// integer/normalized spellings that some assemblers expose for
|
||||||
|
// 10_11_11 and 11_11_10 are reserved by the hardware data-format
|
||||||
|
// table; so are 10_10_10_2 USCALED/SSCALED. Keep this an exact table
|
||||||
|
// instead of deriving ranges that accidentally make reserved encodings
|
||||||
|
// look valid.
|
||||||
|
(dataFormat, numberFormat) = unifiedFormat switch
|
||||||
|
{
|
||||||
|
0 => (0u, 0u),
|
||||||
|
1 => (1u, 0u),
|
||||||
|
2 => (1u, 1u),
|
||||||
|
3 => (1u, 2u),
|
||||||
|
4 => (1u, 3u),
|
||||||
|
5 => (1u, 4u),
|
||||||
|
6 => (1u, 5u),
|
||||||
|
7 => (2u, 0u),
|
||||||
|
8 => (2u, 1u),
|
||||||
|
9 => (2u, 2u),
|
||||||
|
10 => (2u, 3u),
|
||||||
|
11 => (2u, 4u),
|
||||||
|
12 => (2u, 5u),
|
||||||
|
13 => (2u, 7u),
|
||||||
|
14 => (3u, 0u),
|
||||||
|
15 => (3u, 1u),
|
||||||
|
16 => (3u, 2u),
|
||||||
|
17 => (3u, 3u),
|
||||||
|
18 => (3u, 4u),
|
||||||
|
19 => (3u, 5u),
|
||||||
|
20 => (4u, 4u),
|
||||||
|
21 => (4u, 5u),
|
||||||
|
22 => (4u, 7u),
|
||||||
|
23 => (5u, 0u),
|
||||||
|
24 => (5u, 1u),
|
||||||
|
25 => (5u, 2u),
|
||||||
|
26 => (5u, 3u),
|
||||||
|
27 => (5u, 4u),
|
||||||
|
28 => (5u, 5u),
|
||||||
|
29 => (5u, 7u),
|
||||||
|
36 => (6u, 7u),
|
||||||
|
43 => (7u, 7u),
|
||||||
|
44 => (8u, 0u),
|
||||||
|
45 => (8u, 1u),
|
||||||
|
48 => (8u, 4u),
|
||||||
|
49 => (8u, 5u),
|
||||||
|
50 => (9u, 0u),
|
||||||
|
51 => (9u, 1u),
|
||||||
|
52 => (9u, 2u),
|
||||||
|
53 => (9u, 3u),
|
||||||
|
54 => (9u, 4u),
|
||||||
|
55 => (9u, 5u),
|
||||||
|
56 => (10u, 0u),
|
||||||
|
57 => (10u, 1u),
|
||||||
|
58 => (10u, 2u),
|
||||||
|
59 => (10u, 3u),
|
||||||
|
60 => (10u, 4u),
|
||||||
|
61 => (10u, 5u),
|
||||||
|
62 => (11u, 4u),
|
||||||
|
63 => (11u, 5u),
|
||||||
|
64 => (11u, 7u),
|
||||||
|
65 => (12u, 0u),
|
||||||
|
66 => (12u, 1u),
|
||||||
|
67 => (12u, 2u),
|
||||||
|
68 => (12u, 3u),
|
||||||
|
69 => (12u, 4u),
|
||||||
|
70 => (12u, 5u),
|
||||||
|
71 => (12u, 7u),
|
||||||
|
72 => (13u, 4u),
|
||||||
|
73 => (13u, 5u),
|
||||||
|
74 => (13u, 7u),
|
||||||
|
75 => (14u, 4u),
|
||||||
|
76 => (14u, 5u),
|
||||||
|
77 => (14u, 7u),
|
||||||
|
128 => (1u, 9u),
|
||||||
|
129 => (3u, 9u),
|
||||||
|
130 => (10u, 9u),
|
||||||
|
// Image-only encodings without a legacy DATA_FORMAT equivalent
|
||||||
|
// retain the unified identifier for exact downstream dispatch.
|
||||||
|
131 => (131u, 7u),
|
||||||
|
132 => (34u, 7u),
|
||||||
|
133 => (16u, 0u),
|
||||||
|
134 => (17u, 0u),
|
||||||
|
135 => (18u, 0u),
|
||||||
|
136 => (19u, 0u),
|
||||||
|
137 => (137u, 0u),
|
||||||
|
138 => (138u, 0u),
|
||||||
|
139 => (139u, 0u),
|
||||||
|
140 => (4u, 7u),
|
||||||
|
141 => (20u, 0u),
|
||||||
|
142 => (20u, 4u),
|
||||||
|
143 => (21u, 0u),
|
||||||
|
144 => (21u, 4u),
|
||||||
|
145 => (22u, 4u),
|
||||||
|
146 => (22u, 7u),
|
||||||
|
147 => (32u, 0u),
|
||||||
|
148 => (32u, 1u),
|
||||||
|
149 => (32u, 4u),
|
||||||
|
150 => (32u, 9u),
|
||||||
|
151 => (33u, 0u),
|
||||||
|
152 => (33u, 1u),
|
||||||
|
153 => (33u, 4u),
|
||||||
|
154 => (33u, 9u),
|
||||||
|
169 => (169u, 0u),
|
||||||
|
170 => (170u, 9u),
|
||||||
|
171 => (171u, 0u),
|
||||||
|
172 => (172u, 9u),
|
||||||
|
173 => (173u, 0u),
|
||||||
|
174 => (174u, 9u),
|
||||||
|
175 => (175u, 0u),
|
||||||
|
176 => (176u, 1u),
|
||||||
|
177 => (177u, 0u),
|
||||||
|
178 => (178u, 1u),
|
||||||
|
179 => (179u, 7u),
|
||||||
|
180 => (180u, 7u),
|
||||||
|
181 => (181u, 0u),
|
||||||
|
182 => (182u, 9u),
|
||||||
|
_ => (0u, 0u),
|
||||||
|
};
|
||||||
|
|
||||||
|
return unifiedFormat == 0 || dataFormat != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Tls;
|
||||||
|
|
||||||
|
public sealed class GuestTlsTemplateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void StartupReservationAcceptsTlsSpansLargerThanOneHostPage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GuestTlsTemplate.Reset();
|
||||||
|
|
||||||
|
var staticOffset = GuestTlsTemplate.RegisterModule(
|
||||||
|
moduleId: 1,
|
||||||
|
initImage: new byte[0x20],
|
||||||
|
memorySize: 0x1870,
|
||||||
|
alignment: 0x10);
|
||||||
|
|
||||||
|
Assert.Equal(0x1870UL, staticOffset);
|
||||||
|
Assert.True(staticOffset <= GuestTlsTemplate.StartupStaticTlsReservation);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GuestTlsTemplate.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -192,14 +192,22 @@ foreach (var (name, expectTranslate, words) in testPrograms)
|
|||||||
// the hand-assembled buffer_store words, and the 64-byte backing store
|
// the hand-assembled buffer_store words, and the 64-byte backing store
|
||||||
// must cover every hand-assembled store offset.
|
// must cover every hand-assembled store offset.
|
||||||
var globalBindings = storePcs.Count > 0
|
var globalBindings = storePcs.Count > 0
|
||||||
? new[] { new Gen5GlobalMemoryBinding(8u, 0UL, storePcs, new byte[64]) }
|
? new[]
|
||||||
|
{
|
||||||
|
new Gen5GlobalMemoryBinding(
|
||||||
|
8u,
|
||||||
|
0UL,
|
||||||
|
storePcs,
|
||||||
|
new byte[64],
|
||||||
|
64,
|
||||||
|
false),
|
||||||
|
}
|
||||||
: Array.Empty<Gen5GlobalMemoryBinding>();
|
: Array.Empty<Gen5GlobalMemoryBinding>();
|
||||||
|
|
||||||
var state = new Gen5ShaderState(program, new uint[16], Metadata: null);
|
var state = new Gen5ShaderState(program, new uint[16], Metadata: null);
|
||||||
var evaluation = new Gen5ShaderEvaluation(
|
var evaluation = new Gen5ShaderEvaluation(
|
||||||
new uint[256],
|
new uint[256],
|
||||||
new uint[256],
|
new uint[256],
|
||||||
new Dictionary<uint, IReadOnlyList<uint>>(),
|
|
||||||
Array.Empty<Gen5ImageBinding>(),
|
Array.Empty<Gen5ImageBinding>(),
|
||||||
globalBindings);
|
globalBindings);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user