Compare commits
64 Commits
fix/render
...
1016_part3
| Author | SHA1 | Date |
|---|---|---|
|
|
0666071f79 | |
|
|
ee9660d81d | |
|
|
28b2f5709d | |
|
|
f7e8f54dbc | |
|
|
afd0dffcf6 | |
|
|
cde859c5e5 | |
|
|
757567bd15 | |
|
|
62bbf51bbc | |
|
|
c1a0da742f | |
|
|
ad0fd8be7b | |
|
|
6f2937936a | |
|
|
13b77eedcb | |
|
|
a7bfef97ac | |
|
|
10254a3c7e | |
|
|
3d65921eb7 | |
|
|
cd77a68c87 | |
|
|
a662295409 | |
|
|
4d8f5a75d5 | |
|
|
d3deab28aa | |
|
|
8c0ca1a2a7 | |
|
|
9d530f63f9 | |
|
|
f945215935 | |
|
|
c37f76f12d | |
|
|
73666f529e | |
|
|
0e55fa924c | |
|
|
d3f584a2f1 | |
|
|
dd6a936a12 | |
|
|
c0028d304b | |
|
|
ef10b4a1d8 | |
|
|
7be8a4b389 | |
|
|
1984901a31 | |
|
|
7b30868b03 | |
|
|
dc3b1d305d | |
|
|
7866bd4217 | |
|
|
30429400ed | |
|
|
78792d7a5a | |
|
|
7c08757173 | |
|
|
2a2ea5a35a | |
|
|
34e8900035 | |
|
|
923f75d340 | |
|
|
946bd0a777 | |
|
|
d31138fcca | |
|
|
077c9a940f | |
|
|
ea1dcbcea1 | |
|
|
53f233da9b | |
|
|
b368247d64 | |
|
|
c566dba5a1 | |
|
|
8d884f2692 | |
|
|
679ea0f355 | |
|
|
124dfa8afd | |
|
|
1b66707daf | |
|
|
619ac2b367 | |
|
|
a1c0d7d36d | |
|
|
6cc753e0fc | |
|
|
beedb746d9 | |
|
|
82c9731f74 | |
|
|
d708227541 | |
|
|
b02c06181a | |
|
|
e1edda7772 | |
|
|
775141d58c | |
|
|
38f7196470 | |
|
|
8acc081205 | |
|
|
98f223ce69 | |
|
|
8b631d3b03 |
|
|
@ -1,32 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Rendering resolution
|
||||
|
||||
Options → Rendering exposes two independent sizes, globally and per game:
|
||||
|
||||
- **Resolution** sets the host window size or exclusive fullscreen mode.
|
||||
- **Game display mode** reports a 1080p display to the game by default. Select
|
||||
3840×2160 to request a 4K display. A registered presentation buffer cannot
|
||||
change this setting.
|
||||
|
||||
The CLI equivalents are `--resolution=1280x720` and
|
||||
`--guest-resolution=1920x1080` (or `3840x2160`). For example, a 1080p game display
|
||||
can be presented in a 720p window; changing the window alone does not reduce the
|
||||
work the game renders.
|
||||
|
||||
The game chooses its internal render targets. Some games adapt those targets to
|
||||
the reported display mode; others render at a fixed resolution and may still
|
||||
register 4K buffers with a 1080p display. Silent Hill: The Short Message does so
|
||||
in current testing. The size of that presentation buffer alone also does not
|
||||
establish the sizes of its intermediate scene targets.
|
||||
|
||||
Arbitrary internal resolution scaling is not implemented. The previous
|
||||
`RenderResolutionScale` setting and its `SHARPEMU_RENDER_SCALE` export had no
|
||||
renderer consumer; they have been removed. They did not change rendering work.
|
||||
A correct implementation must preserve guest memory layouts while scaling host
|
||||
attachments, viewports, scissors, image fetches, compute accesses, aliases and
|
||||
readbacks consistently. Reducing only the output image or window is not an
|
||||
internal-resolution implementation.
|
||||
|
|
@ -45,7 +45,6 @@ internal static partial class Program
|
|||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
RosettaGcBudget.ReexecIfNeeded(args);
|
||||
ConfigureManagedPluginResolution();
|
||||
|
||||
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
|
||||
|
|
@ -1011,7 +1010,7 @@ internal static partial class Program
|
|||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--guest-resolution=<1920x1080|3840x2160>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
|
||||
}
|
||||
|
|
@ -1076,7 +1075,6 @@ internal static partial class Program
|
|||
var systemLanguage = 1;
|
||||
HostWindowMode? windowModeOverride = null;
|
||||
HostScalingMode? scalingModeOverride = null;
|
||||
GuestDisplayResolution? guestResolutionOverride = null;
|
||||
int? windowWidthOverride = null;
|
||||
int? windowHeightOverride = null;
|
||||
int? displayIndexOverride = null;
|
||||
|
|
@ -1136,18 +1134,6 @@ internal static partial class Program
|
|||
windowHeightOverride = windowHeight;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--guest-resolution", out var guestResolutionText))
|
||||
{
|
||||
if (guestResolutionText is not ("1920x1080" or "3840x2160"))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
guestResolutionOverride = guestResolutionText == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--display", out var displayText))
|
||||
{
|
||||
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
|
||||
|
|
@ -1405,7 +1391,6 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
|
||||
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
|
||||
GuestResolution = guestResolutionOverride ?? configuredVideoOptions.GuestResolution,
|
||||
Width = windowWidthOverride ?? configuredVideoOptions.Width,
|
||||
Height = windowHeightOverride ?? configuredVideoOptions.Height,
|
||||
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
|
||||
|
|
@ -1446,8 +1431,6 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowMode,
|
||||
ScalingMode = scalingMode,
|
||||
GuestResolution = effective.GuestResolution == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd,
|
||||
Width = hasResolution ? configuredWidth : defaults.Width,
|
||||
Height = hasResolution ? configuredHeight : defaults.Height,
|
||||
DisplayIndex = effective.DisplayIndex,
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.CLI;
|
||||
|
||||
/// <summary>
|
||||
/// Under Rosetta 2 every garbage collection suspends the runtime with
|
||||
/// FlushProcessWriteBuffers, which reads each thread's registers through a Mach call that
|
||||
/// Rosetta services by parking the thread in its runtime. A signal that reaches a parked
|
||||
/// thread (guest memory tracking raises thousands per second) trips Rosetta's "expected saved
|
||||
/// LR to be in translated code" assertion and wedges the process. With the default gen0 budget
|
||||
/// the emulator collects about twice a second; a 64 MiB budget makes that rare.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The GC reads its gen0 budget only from the environment when the runtime starts, so the
|
||||
/// process re-executes itself once with the variable set. execve keeps the process id.
|
||||
/// </remarks>
|
||||
internal static class RosettaGcBudget
|
||||
{
|
||||
private const string BudgetVariable = "DOTNET_GCgen0size";
|
||||
// 64 MiB: a larger budget turns every collection into a blocking gen1/gen2 pass of
|
||||
// about a second (512 MiB measured 8 such pauses in 150 s of Demon's Souls).
|
||||
private const string Budget = "0x4000000";
|
||||
private const string OptOutVariable = "SHARPEMU_ROSETTA_GC_BUDGET";
|
||||
|
||||
public static void ReexecIfNeeded(string[] args)
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
|
||||
Environment.GetEnvironmentVariable(BudgetVariable) is not null ||
|
||||
Environment.GetEnvironmentVariable(OptOutVariable) == "0" ||
|
||||
!IsTranslated())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var environment = new List<string>();
|
||||
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
||||
{
|
||||
environment.Add($"{entry.Key}={entry.Value}");
|
||||
}
|
||||
environment.Add($"{BudgetVariable}={Budget}");
|
||||
|
||||
var argv = BuildArguments(path, Environment.GetCommandLineArgs()[0], args);
|
||||
var nativeArguments = ToNullTerminated(argv);
|
||||
var nativeEnvironment = ToNullTerminated(environment);
|
||||
// Only returns on failure; the emulator then runs with the default budget.
|
||||
_ = Execve(path, nativeArguments, nativeEnvironment);
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
foreach (var pointer in nativeArguments) Marshal.FreeCoTaskMem(pointer);
|
||||
foreach (var pointer in nativeEnvironment) Marshal.FreeCoTaskMem(pointer);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Could not restart with a larger GC budget under Rosetta: errno={error}");
|
||||
}
|
||||
|
||||
internal static string[] BuildArguments(string executable, string entryAssembly, string[] args) =>
|
||||
Path.GetFileNameWithoutExtension(executable).Equals("dotnet", StringComparison.OrdinalIgnoreCase)
|
||||
? [executable, entryAssembly, .. args]
|
||||
: [executable, .. args];
|
||||
|
||||
private static bool IsTranslated()
|
||||
{
|
||||
var value = 0;
|
||||
var size = (nint)sizeof(int);
|
||||
return SysctlByName("sysctl.proc_translated", ref value, ref size, 0, 0) == 0 && value == 1;
|
||||
}
|
||||
|
||||
private static nint[] ToNullTerminated(IReadOnlyList<string> values)
|
||||
{
|
||||
var pointers = new nint[values.Count + 1];
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
pointers[i] = Marshal.StringToCoTaskMemUTF8(values[i]);
|
||||
}
|
||||
return pointers;
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "execve", SetLastError = true)]
|
||||
private static extern int Execve(
|
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] string path,
|
||||
nint[] argv,
|
||||
nint[] envp);
|
||||
|
||||
[DllImport("libc", EntryPoint = "sysctlbyname")]
|
||||
private static extern int SysctlByName(
|
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
|
||||
ref int value,
|
||||
ref nint size,
|
||||
nint newValue,
|
||||
nint newSize);
|
||||
}
|
||||
|
|
@ -261,40 +261,23 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
return code;
|
||||
}
|
||||
|
||||
// SHARPEMU_MEMCPY_MANAGED_MIN overrides the size from which guest memcpy takes the managed path.
|
||||
internal static readonly int ManagedCopyMinimumBytes =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_MEMCPY_MANAGED_MIN"), out var minimum) && minimum > 0 ? minimum : 4096;
|
||||
|
||||
/// <summary>
|
||||
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
|
||||
/// export copies with memmove semantics, so a destination that starts inside the source
|
||||
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
|
||||
/// So does a copy of at least <see cref="ManagedCopyMinimumBytes"/>: the native copy takes
|
||||
/// one write-tracking fault per GPU-watched page it writes, a signal round trip of about
|
||||
/// 0.2 ms under Rosetta, while the managed copy marks the whole destination written first.
|
||||
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
|
||||
/// flag is clear on entry per the SysV ABI.
|
||||
/// </summary>
|
||||
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
|
||||
{
|
||||
var code = new List<byte>(48);
|
||||
var slowPathFixups = new List<int>();
|
||||
var code = new List<byte>(40);
|
||||
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
|
||||
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
|
||||
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
|
||||
slowPathFixups.Add(code.Count - 1);
|
||||
// cmp rdx, imm32 / jae slow
|
||||
code.AddRange([0x48, 0x81, 0xFA]);
|
||||
code.AddRange(BitConverter.GetBytes(ManagedCopyMinimumBytes));
|
||||
code.AddRange([0x73, 0x00]);
|
||||
slowPathFixups.Add(code.Count - 1);
|
||||
var slowPathFixup = code.Count - 1;
|
||||
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
|
||||
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
|
||||
foreach (var fixup in slowPathFixups)
|
||||
{
|
||||
code[fixup] = checked((byte)(code.Count - (fixup + 1)));
|
||||
}
|
||||
|
||||
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
|
||||
// jmp qword [rip+0] with the absolute trampoline address after it.
|
||||
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
|
||||
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
|
||||
|
|
|
|||
|
|
@ -1925,6 +1925,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
return HasUsableLleLibcExport("tcVi5SivF7Q", "sprintf");
|
||||
}
|
||||
if (IsLibcFileObjectExport(exportName))
|
||||
{
|
||||
return HasUsableLleLibcExport(ComputePsNid(exportName), exportName);
|
||||
}
|
||||
if (string.Equals(value, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
|
|
@ -3978,11 +3982,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
// The handle and context this host thread registered last; a repeat registration (every
|
||||
// scePthreadSelf on an external guest thread, tens of thousands a second) then skips the
|
||||
// scheduler gate.
|
||||
[ThreadStatic] private static ulong _registeredExternalHandle;
|
||||
[ThreadStatic] private static CpuContext? _registeredExternalContext;
|
||||
public bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
|
|
@ -4017,12 +4016,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return;
|
||||
}
|
||||
|
||||
if (_registeredExternalHandle == threadHandle && ReferenceEquals(_registeredExternalContext, context))
|
||||
{
|
||||
Volatile.Write(ref _currentExternalGuestThreadHandle, threadHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
_currentExternalGuestThreadHandle = threadHandle;
|
||||
|
|
@ -4036,19 +4029,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
existing.Context = context;
|
||||
Volatile.Write(ref existing.HostThreadId, hostThreadId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
_registeredExternalHandle = threadHandle;
|
||||
_registeredExternalContext = context;
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5386,7 +5375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
internal static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
GuestCpuContinuation continuation,
|
||||
|
|
@ -5397,11 +5386,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
|
||||
|
||||
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
|
||||
// Orbis ucontext_t has a 0x10-byte signal mask and 0x30 bytes of
|
||||
// private fields before its amd64 mcontext. These offsets match the
|
||||
// platform ABI used by libScePs5Util and Unity's Boehm GC. Supplying a
|
||||
// bare mcontext here makes the collector miss live register roots.
|
||||
const int mcontext = 0x40;
|
||||
const int mcontext = 0;
|
||||
Write64(mcontext + 0x08, hasContinuation ? continuation.Rdi : context[CpuRegister.Rdi]);
|
||||
Write64(mcontext + 0x10, hasContinuation ? continuation.Rsi : context[CpuRegister.Rsi]);
|
||||
Write64(mcontext + 0x18, hasContinuation ? continuation.Rdx : context[CpuRegister.Rdx]);
|
||||
|
|
@ -5420,9 +5405,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
var rip = hasContinuation ? continuation.Rip : context.Rip;
|
||||
var rsp = hasContinuation ? continuation.Rsp : context[CpuRegister.Rsp];
|
||||
Write64(mcontext + 0xA0, rip);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : context.Rflags);
|
||||
Write64(mcontext + 0xB8, rsp);
|
||||
Write64(mcontext + 0xC8, 0x480); // sizeof(Orbis mcontext_t)
|
||||
Write64(mcontext + 0xF8, rsp);
|
||||
Write64(mcontext + 0x440, hasContinuation ? continuation.FsBase : context.FsBase);
|
||||
Write64(mcontext + 0x448, hasContinuation ? continuation.GsBase : context.GsBase);
|
||||
return context.Memory.TryWrite(address, bytes);
|
||||
|
|
@ -6397,15 +6383,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
const uint stubSize = 512u;
|
||||
// The stub page and host-RSP slot are kept per host thread and nesting depth: allocating
|
||||
// and releasing them on every resume cost an mmap/munmap pair and two acquisitions of the
|
||||
// host-memory gate, which the GPU write tracker holds across its own protection changes.
|
||||
var depth = _continuationDepth;
|
||||
if (!TryAcquireContinuationStub(depth, stubSize, out var ptr, out var hostRspStorage, out reason))
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (ptr == null)
|
||||
{
|
||||
reason = "failed to allocate executable memory for guest thread stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
void* hostRspStorage = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
|
||||
if (hostRspStorage == null)
|
||||
{
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
_continuationDepth = depth + 1;
|
||||
var previousActiveBackend = _activeExecutionBackend;
|
||||
var previousActiveContext = _activeCpuContext;
|
||||
var previousSentinel = _activeEntryReturnSentinelRip;
|
||||
|
|
@ -6479,7 +6469,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
reason = $"failed to patch guest continuation return slot at 0x{returnSlotAddress:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
if (!ProtectContinuationStub(ptr, stubSize, executable: true))
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
reason = "failed to seal guest continuation stub execute-read";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
|
|
@ -6539,85 +6530,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
_continuationDepth = depth;
|
||||
NativeMemory.Free(hostRspStorage);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic] private static nint[]? _continuationStubPages;
|
||||
[ThreadStatic] private static nint[]? _continuationHostRspSlots;
|
||||
[ThreadStatic] private static int _continuationDepth;
|
||||
|
||||
// Returns this thread's stub page for the given nesting depth, writable and with a zeroed
|
||||
// host-RSP slot; the first use at a depth allocates them, and they live as long as the thread.
|
||||
private unsafe bool TryAcquireContinuationStub(int depth, uint stubSize, out void* stub, out void* hostRspStorage, out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
stub = null;
|
||||
hostRspStorage = null;
|
||||
var pages = _continuationStubPages ??= new nint[4];
|
||||
var slots = _continuationHostRspSlots ??= new nint[4];
|
||||
if (depth >= pages.Length)
|
||||
{
|
||||
Array.Resize(ref pages, depth + 2);
|
||||
Array.Resize(ref slots, depth + 2);
|
||||
_continuationStubPages = pages;
|
||||
_continuationHostRspSlots = slots;
|
||||
}
|
||||
|
||||
if (pages[depth] == 0)
|
||||
{
|
||||
var page = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (page == null)
|
||||
{
|
||||
reason = "failed to allocate executable memory for guest thread stub";
|
||||
return false;
|
||||
}
|
||||
|
||||
var slot = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
|
||||
if (slot == null)
|
||||
{
|
||||
VirtualFree(page, 0u, 32768u);
|
||||
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
|
||||
return false;
|
||||
}
|
||||
|
||||
pages[depth] = (nint)page;
|
||||
slots[depth] = (nint)slot;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ProtectContinuationStub((void*)pages[depth], stubSize, executable: false))
|
||||
{
|
||||
reason = "failed to reopen guest continuation stub for writing";
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMemory.Clear((void*)slots[depth], (nuint)HostStackStateBytes);
|
||||
}
|
||||
|
||||
stub = (void*)pages[depth];
|
||||
hostRspStorage = (void*)slots[depth];
|
||||
return true;
|
||||
}
|
||||
|
||||
// The stub page is emulator-private, so on POSIX its protection changes bypass the tracked
|
||||
// host-memory layer and its gate.
|
||||
private static unsafe bool ProtectContinuationStub(void* stub, uint stubSize, bool executable)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
uint oldProtect = 0;
|
||||
return VirtualProtect(stub, stubSize, executable ? 32u : 4u, &oldProtect);
|
||||
}
|
||||
|
||||
var pageSize = (nint)Environment.SystemPageSize;
|
||||
var page = (nint)stub & ~(pageSize - 1);
|
||||
return PosixMprotect(page, (nuint)pageSize, executable ? 1 | 4 : 1 | 2) == 0;
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "mprotect")]
|
||||
private static extern int PosixMprotect(nint address, nuint length, int protection);
|
||||
|
||||
// The continuation trampoline is rebuilt on every blocked-thread resume.
|
||||
// Keep its tiny writer on the stack: capturing local emit functions create a
|
||||
// managed display-class allocation on this extremely hot path.
|
||||
|
|
|
|||
|
|
@ -322,43 +322,20 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
|
||||
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
|
||||
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
|
||||
// on an ignored error turned UE's idle HTTP thread into a hot spin.
|
||||
var nanosleep = ResolveLibcExport("nanosleep");
|
||||
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
|
||||
// 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, 0x53); // push rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
|
||||
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
|
||||
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
|
||||
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
|
||||
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
|
||||
// loop:
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
|
||||
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
|
||||
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
|
||||
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, 0x85, 0xC0); // test eax, eax
|
||||
Emit(page, ref offset, 0x74, 0x26); // jz done
|
||||
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
|
||||
Emit(page, ref offset, 0x75, 0x15); // jne done
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
|
||||
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
|
||||
// done:
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
|
||||
Emit(page, ref offset, 0x5B); // pop rbx
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,44 @@ internal static class GuestRedZonePatcher
|
|||
private const ulong PageSize = 0x1000;
|
||||
private const ulong AllocationAlignment = 0x10000;
|
||||
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
|
||||
private const int MaximumJumpTableEntries = 4096;
|
||||
private const int MaximumJumpTableSetupInstructions = 6;
|
||||
private const ulong LargeExecutableBytes = 32UL << 20;
|
||||
private static readonly TimeSpan ProgressInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
private sealed class Progress(string hostName, string phase, int total)
|
||||
{
|
||||
private readonly long _started = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
private long _lastReport = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
public void Report(int done)
|
||||
{
|
||||
var now = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (System.Diagnostics.Stopwatch.GetElapsedTime(_lastReport, now) < ProgressInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReport = now;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone {phase}: {done}/{total} ({(total == 0 ? 100 : done * 100L / total)}%) " +
|
||||
$"after {System.Diagnostics.Stopwatch.GetElapsedTime(_started, now).TotalSeconds:F0} s");
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ExecutableBytes(IReadOnlyList<ProgramHeader> programHeaders)
|
||||
{
|
||||
var bytes = 0UL;
|
||||
foreach (var header in programHeaders)
|
||||
{
|
||||
if (header.HeaderType == ProgramHeaderType.Load && (header.Flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
bytes += header.FileSize;
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal enum SpanRefusal
|
||||
{
|
||||
|
|
@ -27,6 +65,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetAfter,
|
||||
StackAfter,
|
||||
TooShort,
|
||||
IndirectBranch,
|
||||
}
|
||||
|
||||
public static PatchResult Patch(
|
||||
|
|
@ -62,8 +101,19 @@ internal static class GuestRedZonePatcher
|
|||
return default;
|
||||
}
|
||||
|
||||
var executableBytes = ExecutableBytes(programHeaders);
|
||||
var large = executableBytes >= LargeExecutableBytes;
|
||||
if (large)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] {hostName} red-zone patch: scanning {functionStarts.Length} functions in {executableBytes >> 20} MB of code; " +
|
||||
"a large executable takes a while before the game window opens.");
|
||||
}
|
||||
|
||||
var sites = CollectPatchSites(
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan,
|
||||
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
|
||||
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
if (sites.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -94,8 +144,10 @@ internal static class GuestRedZonePatcher
|
|||
var patched = 0;
|
||||
var failed = 0;
|
||||
var previousEnd = 0UL;
|
||||
var patchProgress = large ? new Progress(hostName, "patch", sites.Count) : null;
|
||||
foreach (var site in sites)
|
||||
{
|
||||
patchProgress?.Report(patched + failed);
|
||||
// Sites are produced in increasing address order. Patching two that
|
||||
// overlap would write one jump over another and send the trampoline
|
||||
// into whatever follows, so refuse rather than corrupt guest code.
|
||||
|
|
@ -128,6 +180,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = scan.BranchTargetRefusals,
|
||||
StackRefusals = scan.StackRefusals,
|
||||
TooShortRefusals = scan.TooShortRefusals,
|
||||
IndirectBranchRefusals = scan.IndirectBranchRefusals,
|
||||
TrampolineBytes = trampolineCursor - trampolineBase,
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -135,7 +188,8 @@ internal static class GuestRedZonePatcher
|
|||
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
|
||||
$"unrelocatable={result.UnrelocatableSites} " +
|
||||
$"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals} " +
|
||||
$"indirect_branch={result.IndirectBranchRefusals}) " +
|
||||
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
|
||||
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
|
||||
return result;
|
||||
|
|
@ -149,15 +203,17 @@ internal static class GuestRedZonePatcher
|
|||
bool protectRedZone,
|
||||
bool splitVectorStores,
|
||||
bool rewriteSha,
|
||||
out PatchResult result)
|
||||
out PatchResult result,
|
||||
Progress? progress = null)
|
||||
{
|
||||
var sites = new List<PatchSite>();
|
||||
var scannedFunctions = 0;
|
||||
var functionCount = 0;
|
||||
var redZoneFunctionCount = 0;
|
||||
var instructionCount = 0;
|
||||
var vectorStoreCount = 0;
|
||||
var unrelocatableSites = 0;
|
||||
var refusalCounts = new int[5];
|
||||
var refusalCounts = new int[6];
|
||||
var shaInstructionCount = 0;
|
||||
|
||||
foreach (var header in programHeaders)
|
||||
|
|
@ -190,6 +246,7 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
for (var functionIndex = 0; functionIndex < segmentFunctions.Length; functionIndex++)
|
||||
{
|
||||
progress?.Report(scannedFunctions++);
|
||||
var functionStart = segmentFunctions[functionIndex];
|
||||
var functionEnd = functionIndex + 1 < segmentFunctions.Length
|
||||
? segmentFunctions[functionIndex + 1]
|
||||
|
|
@ -222,6 +279,14 @@ internal static class GuestRedZonePatcher
|
|||
redZoneFunctionCount++;
|
||||
}
|
||||
var branchTargets = CollectBranchTargets(decoded);
|
||||
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
branchTargets);
|
||||
|
||||
var lastSiteEnd = 0UL;
|
||||
|
||||
|
|
@ -236,27 +301,32 @@ internal static class GuestRedZonePatcher
|
|||
continue;
|
||||
}
|
||||
|
||||
// A backward span reaches instructions that an earlier site
|
||||
// may already have replaced with its own jump. Two jumps
|
||||
// written over each other corrupt control flow, so a span is
|
||||
// only usable when it starts at or after the end of the last
|
||||
// one. Forward spans cannot overlap by construction; the
|
||||
// check costs nothing and documents the invariant.
|
||||
var backward = false;
|
||||
if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
|
||||
var span = default(PatchSite);
|
||||
var refusal = SpanRefusal.TooShort;
|
||||
var forward = TryBuildPatchSpan(
|
||||
decoded,
|
||||
instructionIndex,
|
||||
branchTargets,
|
||||
canBorrowFollowingInstructions,
|
||||
out span,
|
||||
out refusal);
|
||||
var backward = !forward &&
|
||||
canBorrowFollowingInstructions &&
|
||||
TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
if (!forward && !backward)
|
||||
{
|
||||
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
}
|
||||
|
||||
if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
|
||||
{
|
||||
// Refused spans never enter the site list, so they are
|
||||
// invisible in FailedSites. Count them separately.
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (span.Address < lastSiteEnd)
|
||||
{
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)SpanRefusal.TooShort]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
sites.Add(span);
|
||||
lastSiteEnd = span.Address + (ulong)span.ByteLength;
|
||||
if (splitVectorStores)
|
||||
|
|
@ -268,8 +338,6 @@ internal static class GuestRedZonePatcher
|
|||
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
|
||||
}
|
||||
|
||||
// A backward span already ends at the current instruction, so
|
||||
// only a forward one consumes the instructions that follow.
|
||||
if (!backward)
|
||||
{
|
||||
instructionIndex += span.Instructions.Count - 1;
|
||||
|
|
@ -290,6 +358,7 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
|
||||
StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
|
||||
TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
|
||||
IndirectBranchRefusals = refusalCounts[(int)SpanRefusal.IndirectBranch],
|
||||
ShaInstructionCount = shaInstructionCount,
|
||||
};
|
||||
return sites;
|
||||
|
|
@ -347,6 +416,321 @@ internal static class GuestRedZonePatcher
|
|||
return targets;
|
||||
}
|
||||
|
||||
private static bool TryCollectJumpTableTargets(
|
||||
IVirtualMemory memory,
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
ulong functionStart,
|
||||
ulong functionEnd,
|
||||
HashSet<ulong> branchTargets)
|
||||
{
|
||||
List<ulong>? resolvedTargets = null;
|
||||
HashSet<ulong>? instructionBoundaries = null;
|
||||
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
|
||||
{
|
||||
if (decoded[instructionIndex].Instruction.FlowControl != FlowControl.IndirectBranch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instructionBoundaries ??= decoded
|
||||
.Select(static entry => entry.Instruction.IP)
|
||||
.ToHashSet();
|
||||
resolvedTargets ??= new List<ulong>();
|
||||
if (!TryResolveJumpTable(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
instructionIndex,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
instructionBoundaries,
|
||||
branchTargets,
|
||||
resolvedTargets))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedTargets is not null)
|
||||
{
|
||||
branchTargets.UnionWith(resolvedTargets);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveJumpTable(
|
||||
IVirtualMemory memory,
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int jumpIndex,
|
||||
ulong functionStart,
|
||||
ulong functionEnd,
|
||||
IReadOnlySet<ulong> instructionBoundaries,
|
||||
IReadOnlySet<ulong> knownBranchTargets,
|
||||
List<ulong> targets)
|
||||
{
|
||||
if (jumpIndex < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var jump = decoded[jumpIndex].Instruction;
|
||||
var add = decoded[jumpIndex - 1].Instruction;
|
||||
var load = decoded[jumpIndex - 2].Instruction;
|
||||
if (jump.OpCount != 1 || jump.GetOpKind(0) != OpKind.Register ||
|
||||
add.Mnemonic != Mnemonic.Add || add.OpCount != 2 ||
|
||||
add.GetOpKind(0) != OpKind.Register || add.GetOpKind(1) != OpKind.Register ||
|
||||
load.Mnemonic != Mnemonic.Movsxd || load.OpCount != 2 ||
|
||||
load.GetOpKind(0) != OpKind.Register || load.GetOpKind(1) != OpKind.Memory ||
|
||||
load.MemorySize.GetSize() != sizeof(int) || load.MemoryIndexScale != sizeof(int) ||
|
||||
load.MemoryBase == Register.None || load.MemoryIndex == Register.None ||
|
||||
load.MemoryDisplacement64 != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetRegister = jump.GetOpRegister(0).GetFullRegister();
|
||||
var tableRegister = add.GetOpRegister(1).GetFullRegister();
|
||||
var selectorRegister = load.MemoryIndex.GetFullRegister();
|
||||
if (targetRegister != add.GetOpRegister(0).GetFullRegister() ||
|
||||
targetRegister != load.GetOpRegister(0).GetFullRegister() ||
|
||||
tableRegister != load.MemoryBase.GetFullRegister() ||
|
||||
targetRegister.GetSize() != sizeof(ulong) ||
|
||||
tableRegister.GetSize() != sizeof(ulong) ||
|
||||
selectorRegister.GetSize() != sizeof(ulong) ||
|
||||
tableRegister == selectorRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var branchIndex = -1;
|
||||
for (var index = jumpIndex - 3;
|
||||
index >= 1 && index >= jumpIndex - MaximumJumpTableSetupInstructions;
|
||||
index--)
|
||||
{
|
||||
var candidate = decoded[index].Instruction;
|
||||
if (candidate.Mnemonic == Mnemonic.Ja)
|
||||
{
|
||||
branchIndex = index;
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidate.Mnemonic != Mnemonic.Lea)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (branchIndex < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = branchIndex + 1; index <= jumpIndex; index++)
|
||||
{
|
||||
if (knownBranchTargets.Contains(decoded[index].Instruction.IP))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var boundsBranch = decoded[branchIndex].Instruction;
|
||||
var boundsCompare = decoded[branchIndex - 1].Instruction;
|
||||
if (boundsBranch.FlowControl != FlowControl.ConditionalBranch ||
|
||||
!TryGetNearBranchTarget(boundsBranch, out var defaultTarget) ||
|
||||
defaultTarget < functionStart || defaultTarget >= functionEnd ||
|
||||
boundsCompare.Mnemonic != Mnemonic.Cmp || boundsCompare.OpCount != 2 ||
|
||||
boundsCompare.GetOpKind(0) != OpKind.Register ||
|
||||
boundsCompare.GetOpRegister(0).GetFullRegister() != selectorRegister ||
|
||||
boundsCompare.GetOpRegister(0).GetSize() != sizeof(int) ||
|
||||
!TryGetNonNegativeImmediate(boundsCompare, 1, out var maximumIndex) ||
|
||||
maximumIndex >= MaximumJumpTableEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tableAddress = 0UL;
|
||||
var tableDefinitionFound = false;
|
||||
for (var index = branchIndex + 1; index <= jumpIndex - 3; index++)
|
||||
{
|
||||
var candidate = decoded[index].Instruction;
|
||||
if (candidate.OpCount < 1 || candidate.GetOpKind(0) != OpKind.Register)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var writtenRegister = candidate.GetOpRegister(0).GetFullRegister();
|
||||
if (writtenRegister == selectorRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (writtenRegister != tableRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tableDefinitionFound || candidate.OpCount != 2 ||
|
||||
candidate.GetOpKind(1) != OpKind.Memory || !candidate.IsIPRelativeMemoryOperand)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tableAddress = candidate.IPRelativeMemoryAddress;
|
||||
tableDefinitionFound = true;
|
||||
}
|
||||
|
||||
if (!tableDefinitionFound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entryCount = checked((int)maximumIndex + 1);
|
||||
var tableBytes = checked(entryCount * sizeof(int));
|
||||
if (!IsRangeInReadableLoad(programHeaders, imageBase, tableAddress, (ulong)tableBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entries = GC.AllocateUninitializedArray<byte>(tableBytes);
|
||||
if (!memory.TryRead(tableAddress, entries))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstTarget = targets.Count;
|
||||
for (var index = 0; index < entryCount; index++)
|
||||
{
|
||||
var relative = BinaryPrimitives.ReadInt32LittleEndian(entries.AsSpan(index * sizeof(int)));
|
||||
if (!TryAddSignedOffset(tableAddress, relative, out var target) ||
|
||||
target < functionStart || target >= functionEnd ||
|
||||
!instructionBoundaries.Contains(target))
|
||||
{
|
||||
targets.RemoveRange(firstTarget, targets.Count - firstTarget);
|
||||
return false;
|
||||
}
|
||||
|
||||
targets.Add(target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsRangeInReadableLoad(
|
||||
IReadOnlyList<ProgramHeader> programHeaders,
|
||||
ulong imageBase,
|
||||
ulong address,
|
||||
ulong length)
|
||||
{
|
||||
foreach (var header in programHeaders)
|
||||
{
|
||||
if (header.HeaderType != ProgramHeaderType.Load ||
|
||||
(header.Flags & ProgramHeaderFlags.Read) == 0 ||
|
||||
header.FileSize == 0 ||
|
||||
header.VirtualAddress > ulong.MaxValue - imageBase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = imageBase + header.VirtualAddress;
|
||||
if (address < start || address - start > header.FileSize ||
|
||||
length > header.FileSize - (address - start))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAddSignedOffset(ulong address, int offset, out ulong result)
|
||||
{
|
||||
if (offset >= 0)
|
||||
{
|
||||
var positive = (ulong)offset;
|
||||
if (positive <= ulong.MaxValue - address)
|
||||
{
|
||||
result = address + positive;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var negative = (ulong)-(long)offset;
|
||||
if (negative <= address)
|
||||
{
|
||||
result = address - negative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetNearBranchTarget(in Instruction instruction, out ulong target)
|
||||
{
|
||||
for (var operand = 0; operand < instruction.OpCount; operand++)
|
||||
{
|
||||
switch (instruction.GetOpKind(operand))
|
||||
{
|
||||
case OpKind.NearBranch16:
|
||||
target = instruction.NearBranch16;
|
||||
return true;
|
||||
case OpKind.NearBranch32:
|
||||
target = instruction.NearBranch32;
|
||||
return true;
|
||||
case OpKind.NearBranch64:
|
||||
target = instruction.NearBranch64;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetNonNegativeImmediate(in Instruction instruction, int operand, out ulong value)
|
||||
{
|
||||
switch (instruction.GetOpKind(operand))
|
||||
{
|
||||
case OpKind.Immediate8:
|
||||
value = instruction.Immediate8;
|
||||
return true;
|
||||
case OpKind.Immediate16:
|
||||
value = instruction.Immediate16;
|
||||
return true;
|
||||
case OpKind.Immediate32:
|
||||
value = instruction.Immediate32;
|
||||
return true;
|
||||
case OpKind.Immediate64:
|
||||
value = instruction.Immediate64;
|
||||
return true;
|
||||
case OpKind.Immediate8to16 when instruction.Immediate8to16 >= 0:
|
||||
value = (ulong)instruction.Immediate8to16;
|
||||
return true;
|
||||
case OpKind.Immediate8to32 when instruction.Immediate8to32 >= 0:
|
||||
value = (ulong)instruction.Immediate8to32;
|
||||
return true;
|
||||
case OpKind.Immediate8to64 when instruction.Immediate8to64 >= 0:
|
||||
value = (ulong)instruction.Immediate8to64;
|
||||
return true;
|
||||
case OpKind.Immediate32to64 when instruction.Immediate32to64 >= 0:
|
||||
value = (ulong)instruction.Immediate32to64;
|
||||
return true;
|
||||
default:
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the enclosing-span search over a raw instruction range, so the
|
||||
/// behaviour can be pinned on the byte sequences that occur in real guest
|
||||
|
|
@ -376,7 +760,7 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
if (siteIndex < 0 ||
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -417,7 +801,10 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
var hasIndirectBranch = decoded.Any(static entry =>
|
||||
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
|
||||
if (hasIndirectBranch ||
|
||||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -551,12 +938,13 @@ internal static class GuestRedZonePatcher
|
|||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site) =>
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
bool canBorrowFollowingInstructions,
|
||||
out PatchSite site,
|
||||
out SpanRefusal refusal)
|
||||
{
|
||||
|
|
@ -568,6 +956,7 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (index != startIndex &&
|
||||
canBorrowFollowingInstructions &&
|
||||
!branchTargets.Contains(instruction.IP) &&
|
||||
IsRelocatableDirectBranch(instruction))
|
||||
{
|
||||
|
|
@ -578,7 +967,11 @@ internal static class GuestRedZonePatcher
|
|||
break;
|
||||
}
|
||||
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
if (index != startIndex && !canBorrowFollowingInstructions)
|
||||
{
|
||||
refusal = SpanRefusal.IndirectBranch;
|
||||
}
|
||||
else if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
}
|
||||
|
|
@ -1089,6 +1482,8 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
public int TooShortRefusals { get; init; }
|
||||
|
||||
public int IndirectBranchRefusals { get; init; }
|
||||
|
||||
public ulong TrampolineBytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
FiberExports.ResetRuntimeState();
|
||||
Http2Exports.ResetRuntimeState();
|
||||
NpAuthExports.ResetRuntimeState();
|
||||
NpUniversalDataSystemExports.ResetRuntimeState();
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
|
|
|
|||
|
|
@ -70,11 +70,6 @@ public sealed class GuiSettings
|
|||
|
||||
public string Resolution { get; set; } = "1920x1080";
|
||||
|
||||
public string GuestResolution { get; set; } = "1920x1080";
|
||||
|
||||
internal static string NormalizeGuestResolution(string? value) =>
|
||||
string.Equals(value, "3840x2160", StringComparison.OrdinalIgnoreCase) ? "3840x2160" : "1920x1080";
|
||||
|
||||
public int DisplayIndex { get; set; }
|
||||
|
||||
public int RefreshRate { get; set; }
|
||||
|
|
@ -94,6 +89,9 @@ public sealed class GuiSettings
|
|||
|
||||
public string BinkPlaybackMode { get; set; } = "Guest";
|
||||
|
||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
||||
public double RenderResolutionScale { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
|
|
@ -186,7 +184,6 @@ public sealed class GuiSettings
|
|||
"Ukrainian");
|
||||
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
||||
settings.Resolution = NormalizeResolution(settings.Resolution);
|
||||
settings.GuestResolution = NormalizeGuestResolution(settings.GuestResolution);
|
||||
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
||||
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
|
||||
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "حصري",
|
||||
"Options.Resolution.Label": "الدقة",
|
||||
"Options.Resolution.Desc": "حجم النافذة الأولي أو دقة ملء الشاشة الحصرية.",
|
||||
"Options.GuestResolution.Label": "وضع عرض اللعبة",
|
||||
"Options.GuestResolution.Desc": "دقة العرض المُبلّغ عنها للعبة. الافتراضي 1080p. تختار الألعاب دقة التصيير الداخلية؛ وقد تتجاهل الألعاب ذات الدقة الثابتة هذا الإعداد.",
|
||||
"Options.Display.Label": "الشاشة",
|
||||
"Options.Display.Desc": "الشاشة المستخدمة للتوسيط وملء الشاشة.",
|
||||
"Options.RefreshRate.Label": "معدل التحديث",
|
||||
|
|
|
|||
|
|
@ -74,8 +74,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklüziv",
|
||||
"Options.Resolution.Label": "Təsvir ölçüsü",
|
||||
"Options.Resolution.Desc": "Pəncərənin başlanğıc ölçüsü və ya eksklüziv tam ekranın təsvir ölçüsü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran rejimi",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilən ekran təsvir ölçüsü. Standart 1080p-dir. Daxili render ölçüsünü oyun seçir; sabit ölçülü oyunlar bu ayarı nəzərə almaya bilər.",
|
||||
"Options.Display.Label": "Ekran",
|
||||
"Options.Display.Desc": "Pəncərənin mərkəzə yerləşdirilməsi və tam ekran üçün istifadə olunan monitor.",
|
||||
"Options.RefreshRate.Label": "Yenilənmə tezliyi",
|
||||
|
|
|
|||
|
|
@ -220,8 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de tela cheia exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de exibição do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução informada ao jogo. O padrão é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.Display.Label": "Tela",
|
||||
"Options.Display.Desc": "Monitor usado para centralização e tela cheia.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exklusiv",
|
||||
"Options.Resolution.Label": "Auflösung",
|
||||
"Options.Resolution.Desc": "Anfängliche Fenstergröße oder exklusive Vollbildauflösung.",
|
||||
"Options.GuestResolution.Label": "Anzeigemodus des Spiels",
|
||||
"Options.GuestResolution.Desc": "Dem Spiel gemeldete Bildschirmauflösung. Standard ist 1080p. Spiele wählen ihre interne Renderauflösung selbst; Spiele mit fester Auflösung können diese Einstellung ignorieren.",
|
||||
"Options.Display.Label": "Anzeige",
|
||||
"Options.Display.Desc": "Monitor für Zentrierung und Vollbilddarstellung.",
|
||||
"Options.RefreshRate.Label": "Bildwiederholrate",
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiv",
|
||||
"Options.Resolution.Label": "Opløsning",
|
||||
"Options.Resolution.Desc": "Oprindelig vinduesstørrelse eller opløsning i eksklusiv fuldskærm.",
|
||||
"Options.GuestResolution.Label": "Spillets skærmtilstand",
|
||||
"Options.GuestResolution.Desc": "Skærmopløsning oplyst til spillet. Standard er 1080p. Spillet vælger den interne renderopløsning; spil med fast opløsning kan ignorere denne indstilling.",
|
||||
"Options.Display.Label": "Skærm",
|
||||
"Options.Display.Desc": "Skærm, der bruges til centrering og fuldskærm.",
|
||||
"Options.RefreshRate.Label": "Opdateringshastighed",
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiivne",
|
||||
"Options.Resolution.Label": "Eraldusvõime",
|
||||
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
|
||||
"Options.GuestResolution.Label": "Mängu kuvarežiim",
|
||||
"Options.GuestResolution.Desc": "Mängule teatatud ekraani eraldusvõime. Vaikimisi 1080p. Mäng valib sisemise renderduse eraldusvõime; fikseeritud eraldusvõimega mängud võivad seda sätet eirata.",
|
||||
"Options.Display.Label": "Kuvar",
|
||||
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
|
||||
"Options.RefreshRate.Label": "Värskendussagedus",
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusive",
|
||||
"Options.Resolution.Label": "Resolution",
|
||||
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
|
||||
"Options.GuestResolution.Label": "Game display mode",
|
||||
"Options.GuestResolution.Desc": "Display resolution reported to the game. Defaults to 1080p. Games choose their internal rendering resolution; fixed-resolution games may ignore this setting.",
|
||||
"Options.Display.Label": "Display",
|
||||
"Options.Display.Desc": "Monitor used for centering and fullscreen.",
|
||||
"Options.RefreshRate.Label": "Refresh rate",
|
||||
|
|
|
|||
|
|
@ -220,8 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolución",
|
||||
"Options.Resolution.Desc": "Tamaño inicial de la ventana o resolución de pantalla completa exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de pantalla del juego",
|
||||
"Options.GuestResolution.Desc": "Resolución de pantalla comunicada al juego. El valor predeterminado es 1080p. El juego elige su resolución interna de renderizado; los juegos con resolución fija pueden ignorar este ajuste.",
|
||||
"Options.Display.Label": "Pantalla",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar y mostrar a pantalla completa.",
|
||||
"Options.RefreshRate.Label": "Frecuencia de actualización",
|
||||
|
|
|
|||
|
|
@ -220,8 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusif",
|
||||
"Options.Resolution.Label": "Résolution",
|
||||
"Options.Resolution.Desc": "Taille initiale de la fenêtre ou résolution du plein écran exclusif.",
|
||||
"Options.GuestResolution.Label": "Mode d’affichage du jeu",
|
||||
"Options.GuestResolution.Desc": "Résolution d’écran indiquée au jeu. La valeur par défaut est 1080p. Le jeu choisit sa résolution de rendu interne ; les jeux à résolution fixe peuvent ignorer ce réglage.",
|
||||
"Options.Display.Label": "Écran",
|
||||
"Options.Display.Desc": "Moniteur utilisé pour le centrage et le plein écran.",
|
||||
"Options.RefreshRate.Label": "Fréquence de rafraîchissement",
|
||||
|
|
|
|||
|
|
@ -220,8 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Kizárólagos",
|
||||
"Options.Resolution.Label": "Felbontás",
|
||||
"Options.Resolution.Desc": "Kezdeti ablakméret vagy kizárólagos teljes képernyős felbontás.",
|
||||
"Options.GuestResolution.Label": "A játék megjelenítési módja",
|
||||
"Options.GuestResolution.Desc": "A játéknak jelzett képernyőfelbontás. Alapértelmezés: 1080p. A belső renderelési felbontást a játék választja; a rögzített felbontású játékok figyelmen kívül hagyhatják ezt a beállítást.",
|
||||
"Options.Display.Label": "Kijelző",
|
||||
"Options.Display.Desc": "A középre igazításhoz és teljes képernyőhöz használt monitor.",
|
||||
"Options.RefreshRate.Label": "Frissítési gyakoriság",
|
||||
|
|
|
|||
|
|
@ -224,8 +224,6 @@
|
|||
"Options.WindowMode.Exclusive": "Esclusiva",
|
||||
"Options.Resolution.Label": "Risoluzione",
|
||||
"Options.Resolution.Desc": "Dimensione iniziale della finestra o risoluzione dello schermo intero esclusivo.",
|
||||
"Options.GuestResolution.Label": "Modalità schermo del gioco",
|
||||
"Options.GuestResolution.Desc": "Risoluzione dello schermo comunicata al gioco. Il valore predefinito è 1080p. Il gioco sceglie la risoluzione interna di rendering; i giochi con risoluzione fissa possono ignorare questa impostazione.",
|
||||
"Options.Display.Label": "Schermo",
|
||||
"Options.Display.Desc": "Monitor utilizzato per il centraggio e lo schermo intero.",
|
||||
"Options.RefreshRate.Label": "Frequenza di aggiornamento",
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "排他",
|
||||
"Options.Resolution.Label": "解像度",
|
||||
"Options.Resolution.Desc": "初期ウィンドウサイズまたは排他フルスクリーンの解像度。",
|
||||
"Options.GuestResolution.Label": "ゲームの表示モード",
|
||||
"Options.GuestResolution.Desc": "ゲームに通知する画面解像度。既定値は1080pです。内部描画解像度はゲームが決定するため、解像度が固定されたゲームではこの設定が反映されない場合があります。",
|
||||
"Options.Display.Label": "ディスプレイ",
|
||||
"Options.Display.Desc": "中央配置とフルスクリーンに使用するモニター。",
|
||||
"Options.RefreshRate.Label": "リフレッシュレート",
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "독점",
|
||||
"Options.Resolution.Label": "해상도",
|
||||
"Options.Resolution.Desc": "초기 창 크기 또는 독점 전체 화면 해상도.",
|
||||
"Options.GuestResolution.Label": "게임 디스플레이 모드",
|
||||
"Options.GuestResolution.Desc": "게임에 전달하는 화면 해상도입니다. 기본값은 1080p입니다. 내부 렌더링 해상도는 게임이 결정하며, 해상도가 고정된 게임은 이 설정을 무시할 수 있습니다.",
|
||||
"Options.Display.Label": "디스플레이",
|
||||
"Options.Display.Desc": "가운데 배치와 전체 화면에 사용할 모니터.",
|
||||
"Options.RefreshRate.Label": "새로 고침 빈도",
|
||||
|
|
|
|||
|
|
@ -219,8 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusief",
|
||||
"Options.Resolution.Label": "Resolutie",
|
||||
"Options.Resolution.Desc": "Initiële venstergrootte of resolutie voor exclusief volledig scherm.",
|
||||
"Options.GuestResolution.Label": "Weergavemodus van het spel",
|
||||
"Options.GuestResolution.Desc": "Schermresolutie die aan het spel wordt doorgegeven. Standaard 1080p. Het spel kiest de interne renderresolutie; spellen met een vaste resolutie kunnen deze instelling negeren.",
|
||||
"Options.Display.Label": "Beeldscherm",
|
||||
"Options.Display.Desc": "Monitor die wordt gebruikt voor centrering en volledig scherm.",
|
||||
"Options.RefreshRate.Label": "Verversingssnelheid",
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Wyłączne",
|
||||
"Options.Resolution.Label": "Rozdzielczość",
|
||||
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
|
||||
"Options.GuestResolution.Label": "Tryb wyświetlania gry",
|
||||
"Options.GuestResolution.Desc": "Rozdzielczość ekranu zgłaszana grze. Domyślnie 1080p. Gra wybiera wewnętrzną rozdzielczość renderowania; gry ze stałą rozdzielczością mogą zignorować to ustawienie.",
|
||||
"Options.Display.Label": "Wyświetlacz",
|
||||
"Options.Display.Desc": "Monitor używany do centrowania i trybu pełnoekranowego.",
|
||||
"Options.RefreshRate.Label": "Częstotliwość odświeżania",
|
||||
|
|
|
|||
|
|
@ -220,8 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusivo",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de ecrã inteiro exclusivo.",
|
||||
"Options.GuestResolution.Label": "Modo de apresentação do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução do ecrã comunicada ao jogo. A predefinição é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.Display.Label": "Ecrã",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar e apresentar em ecrã inteiro.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -81,8 +81,6 @@
|
|||
"Options.WindowMode.Exclusive": "Эксклюзивный",
|
||||
"Options.Resolution.Label": "Разрешение",
|
||||
"Options.Resolution.Desc": "Начальный размер окна или разрешение эксклюзивного полноэкранного режима.",
|
||||
"Options.GuestResolution.Label": "Режим экрана игры",
|
||||
"Options.GuestResolution.Desc": "Разрешение экрана, сообщаемое игре. По умолчанию 1080p. Игра выбирает внутреннее разрешение рендеринга; игры с фиксированным разрешением могут игнорировать эту настройку.",
|
||||
"Options.Display.Label": "Монитор",
|
||||
"Options.Display.Desc": "Монитор, используемый для центрирования окна и полноэкранного режима.",
|
||||
"Options.RefreshRate.Label": "Частота обновления",
|
||||
|
|
|
|||
|
|
@ -63,8 +63,6 @@
|
|||
"Options.WindowMode.Desc": "Normal pencere, kenarlıksız masaüstü veya özel tam ekran.",
|
||||
"Options.Resolution.Label": "Çözünürlük",
|
||||
"Options.Resolution.Desc": "Başlangıç pencere boyutu veya özel tam ekran çözünürlüğü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran modu",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilen ekran çözünürlüğü. Varsayılan 1080p’dir. Dahili işleme çözünürlüğünü oyun seçer; sabit çözünürlüklü oyunlar bu ayarı yok sayabilir.",
|
||||
"Options.Display.Label": "Ekran",
|
||||
"Options.Display.Desc": "Ortalama ve tam ekran için kullanılan monitör.",
|
||||
"Options.RefreshRate.Label": "Yenileme hızı",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ public partial class MainWindow
|
|||
GameLogLevelBox.ItemsSource = _logLevelChoices;
|
||||
GameWindowModeBox.ItemsSource = _windowModeChoices;
|
||||
GameScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GameGuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
GameHdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
GameOverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
GameOverlayCornerBox.ItemsSource = _overlayCornerChoices;
|
||||
|
|
@ -116,7 +115,6 @@ public partial class MainWindow
|
|||
GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameGuestResolutionBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayEnabledToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
|
|
@ -218,7 +216,6 @@ public partial class MainWindow
|
|||
_scalingModeChoices,
|
||||
effective.ScalingMode,
|
||||
"Fit");
|
||||
GameGuestResolutionBox.SelectedItem = effective.GuestResolution;
|
||||
GameVSyncToggle.IsChecked = effective.VSync;
|
||||
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
|
||||
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
|
||||
|
|
@ -280,7 +277,6 @@ public partial class MainWindow
|
|||
LogToFile = GameLogToFileToggle.IsChecked == true,
|
||||
WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"),
|
||||
Resolution = SelectedComboText(GameResolutionBox, "1920x1080"),
|
||||
GuestResolution = SelectedComboText(GameGuestResolutionBox, "1920x1080"),
|
||||
DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display
|
||||
? display.Index
|
||||
: 0,
|
||||
|
|
|
|||
|
|
@ -856,14 +856,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="GameGuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GameGuestResolutionBox"
|
||||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GameDisplayRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.Display.Label],
|
||||
|
|
@ -1498,12 +1490,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GuestResolutionBox" Classes="optionValue" VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="DisplayRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.Display.Label], Source={x:Static local:Localization.Instance}}"
|
||||
|
|
|
|||
|
|
@ -322,7 +322,6 @@ public partial class MainWindow : Window
|
|||
WindowModeBox.SelectionChanged += (_, _) => _settings.WindowMode = SelectedComboText(WindowModeBox, "Windowed");
|
||||
DisplayBox.SelectionChanged += (_, _) => OnHostDisplayChanged();
|
||||
ResolutionBox.SelectionChanged += (_, _) => OnHostResolutionChanged();
|
||||
GuestResolutionBox.SelectionChanged += (_, _) => _settings.GuestResolution = SelectedComboText(GuestResolutionBox, "1920x1080");
|
||||
RefreshRateBox.SelectionChanged += (_, _) => OnHostRefreshRateChanged();
|
||||
ScalingModeBox.SelectionChanged += (_, _) => _settings.ScalingMode = SelectedComboText(ScalingModeBox, "Fit");
|
||||
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
|
||||
|
|
@ -1250,7 +1249,6 @@ public partial class MainWindow : Window
|
|||
LogLevelBox.ItemsSource = _logLevelChoices;
|
||||
WindowModeBox.ItemsSource = _windowModeChoices;
|
||||
ScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
HdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
BinkPlaybackBox.ItemsSource = _binkPlaybackChoices;
|
||||
OverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
|
|
@ -1326,7 +1324,6 @@ public partial class MainWindow : Window
|
|||
LoadHostDisplayOptions();
|
||||
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
||||
VSyncToggle.IsChecked = _settings.VSync;
|
||||
GuestResolutionBox.SelectedItem = GuiSettings.NormalizeGuestResolution(_settings.GuestResolution);
|
||||
HdrModeBox.SelectedIndex = ChoiceIndex(_settings.HdrMode, "Auto", "On", "Off");
|
||||
OverlayEnabledToggle.IsChecked = _settings.OverlayEnabled;
|
||||
OverlayModeBox.SelectedIndex = ChoiceIndex(_settings.OverlayMode, "Full", "Minimal", "TitleBar");
|
||||
|
|
@ -2563,6 +2560,13 @@ public partial class MainWindow : Window
|
|||
});
|
||||
_appliedEnvironmentVariables.Add(BinkModeEnvironmentName);
|
||||
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_RENDER_SCALE",
|
||||
_settings.RenderResolutionScale.ToString(
|
||||
"0.###",
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
_appliedEnvironmentVariables.Add("SHARPEMU_RENDER_SCALE");
|
||||
|
||||
foreach (var entry in customEnvironment)
|
||||
{
|
||||
if (!CustomEnvironmentVariables.TryParseEntry(entry, out var name, out var value))
|
||||
|
|
@ -2780,7 +2784,6 @@ public partial class MainWindow : Window
|
|||
|
||||
arguments.Add($"--window-mode={launch.Settings.WindowMode.ToLowerInvariant()}");
|
||||
arguments.Add($"--resolution={launch.Settings.Resolution}");
|
||||
arguments.Add($"--guest-resolution={launch.Settings.GuestResolution}");
|
||||
arguments.Add($"--display={launch.Settings.DisplayIndex}");
|
||||
arguments.Add($"--refresh-rate={launch.Settings.RefreshRate}");
|
||||
arguments.Add($"--scaling={launch.Settings.ScalingMode.ToLowerInvariant()}");
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ public sealed class PerGameSettings
|
|||
|
||||
public string? Resolution { get; set; }
|
||||
|
||||
public string? GuestResolution { get; set; }
|
||||
|
||||
public int? DisplayIndex { get; set; }
|
||||
|
||||
public int? RefreshRate { get; set; }
|
||||
|
|
@ -54,7 +52,6 @@ public sealed class PerGameSettings
|
|||
LogToFile is null &&
|
||||
WindowMode is null &&
|
||||
Resolution is null &&
|
||||
GuestResolution is null &&
|
||||
DisplayIndex is null &&
|
||||
RefreshRate is null &&
|
||||
ScalingMode is null &&
|
||||
|
|
@ -148,11 +145,6 @@ public sealed class PerGameSettings
|
|||
Resolution = null;
|
||||
}
|
||||
|
||||
if (string.Equals(GuestResolution, global.GuestResolution, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
GuestResolution = null;
|
||||
}
|
||||
|
||||
if (DisplayIndex == global.DisplayIndex)
|
||||
{
|
||||
DisplayIndex = null;
|
||||
|
|
@ -280,7 +272,6 @@ public sealed record EffectiveLaunchSettings(
|
|||
bool LogToFile,
|
||||
string WindowMode,
|
||||
string Resolution,
|
||||
string GuestResolution,
|
||||
int DisplayIndex,
|
||||
int RefreshRate,
|
||||
string ScalingMode,
|
||||
|
|
@ -298,7 +289,6 @@ public sealed record EffectiveLaunchSettings(
|
|||
perGame?.LogToFile ?? global.LogToFile,
|
||||
perGame?.WindowMode ?? global.WindowMode,
|
||||
perGame?.Resolution ?? global.Resolution,
|
||||
GuiSettings.NormalizeGuestResolution(perGame?.GuestResolution ?? global.GuestResolution),
|
||||
Math.Max(0, perGame?.DisplayIndex ?? global.DisplayIndex),
|
||||
Math.Clamp(perGame?.RefreshRate ?? global.RefreshRate, 0, 1000),
|
||||
perGame?.ScalingMode ?? global.ScalingMode,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public static class HleDataSymbols
|
|||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||
private static readonly nint _progNameBufferAddress = Allocate(ProgNameMaxBytes + 1);
|
||||
private static readonly nint _progNamePointerAddress = Allocate(nint.Size);
|
||||
private static readonly nint _argumentVectorAddress = Allocate(nint.Size * 2);
|
||||
private static readonly nint _libcNeedFlagAddress = Allocate(sizeof(uint));
|
||||
private static readonly nint _libcInternalNeedFlagAddress = Allocate(sizeof(uint));
|
||||
|
||||
|
|
@ -55,6 +56,12 @@ public static class HleDataSymbols
|
|||
yield return LibcInternalNeedFlagNid;
|
||||
}
|
||||
|
||||
public static int ProcessArgumentCount =>
|
||||
_progNameBufferAddress != 0 && _argumentVectorAddress != 0 ? 1 : 0;
|
||||
|
||||
public static ulong ProcessArgumentVectorAddress =>
|
||||
unchecked((ulong)_argumentVectorAddress);
|
||||
|
||||
public static void ConfigureProcessImageName(string? processImageName)
|
||||
{
|
||||
var effectiveName = string.IsNullOrWhiteSpace(processImageName)
|
||||
|
|
@ -65,7 +72,9 @@ public static class HleDataSymbols
|
|||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
|
||||
if (_progNameBufferAddress == 0 ||
|
||||
_progNamePointerAddress == 0 ||
|
||||
_argumentVectorAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,6 +86,8 @@ public static class HleDataSymbols
|
|||
|
||||
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
|
||||
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
|
||||
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
|
||||
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,43 +462,20 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
|
||||
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
|
||||
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
|
||||
// on an ignored error turned UE's idle HTTP thread into a hot spin.
|
||||
var nanosleep = ResolveLibcExport("nanosleep");
|
||||
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
|
||||
// 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, 0x53); // push rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
|
||||
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
|
||||
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
|
||||
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
|
||||
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
|
||||
// loop:
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
|
||||
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
|
||||
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
|
||||
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, 0x85, 0xC0); // test eax, eax
|
||||
Emit(page, ref offset, 0x74, 0x26); // jz done
|
||||
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
|
||||
Emit(page, ref offset, 0x75, 0x15); // jne done
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
|
||||
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
|
||||
// done:
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
|
||||
Emit(page, ref offset, 0x5B); // pop rbx
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
// View mappings bypass the anonymous-allocation tables. Keep their query state together.
|
||||
|
|
@ -13,12 +11,6 @@ internal static class PosixViewRegions
|
|||
uint[]? PageProtections, ulong PageBase);
|
||||
private static readonly List<Region> Regions = new();
|
||||
|
||||
// The regions in address order, replaced whenever the list changes. Queries read it without
|
||||
// Gate: every HLE import queries the guest stack's protection first, and Gate is held across
|
||||
// mprotect by the GPU write tracker, so a locked query made each import wait on page traffic.
|
||||
// Page protections change in place and are read racily, as a concurrent change always was.
|
||||
private static Region[] _snapshot = [];
|
||||
|
||||
internal static void Replace(ulong address, ulong size, uint state, uint protection)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
|
|
@ -54,8 +46,6 @@ internal static class PosixViewRegions
|
|||
else
|
||||
Regions.Add(region);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _snapshot, [.. Regions]);
|
||||
}
|
||||
|
||||
// Mapping creates the storage. Fault-time protection updates must not allocate.
|
||||
|
|
@ -76,8 +66,9 @@ internal static class PosixViewRegions
|
|||
|
||||
internal static bool TryQuery(ulong address, out HostMemory.BasicInfo info)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
foreach (var region in Volatile.Read(ref _snapshot))
|
||||
foreach (var region in Regions)
|
||||
{
|
||||
if (region.Start > address) break;
|
||||
if (address >= region.End) continue;
|
||||
|
|
|
|||
|
|
@ -149,14 +149,6 @@ public static unsafe class HostMemory
|
|||
private static readonly object Gate = new();
|
||||
private static readonly SortedList<ulong, Region> Regions = new();
|
||||
|
||||
// The regions in address order, replaced under Gate whenever a region is added or removed.
|
||||
// Queries read it without the lock: Protect holds Gate across its mprotect call, and GPU
|
||||
// write tracking protects pages constantly, so a locked query (one per HLE call reading
|
||||
// stack arguments) waited tens of microseconds behind those syscalls.
|
||||
private static Region[] RegionSnapshot = [];
|
||||
|
||||
private static void PublishRegionsLocked() => Volatile.Write(ref RegionSnapshot, [.. Regions.Values]);
|
||||
|
||||
private sealed class Region
|
||||
{
|
||||
public ulong Base;
|
||||
|
|
@ -164,10 +156,6 @@ public static unsafe class HostMemory
|
|||
public uint DefaultProtect;
|
||||
public Dictionary<ulong, uint>? PageProtects;
|
||||
|
||||
// The overridden pages in address order, so a query finds where a protection run ends
|
||||
// without visiting every override: write tracking leaves tens of thousands of them.
|
||||
public SortedSet<ulong>? OverridePages;
|
||||
|
||||
public ulong End => Base + Size;
|
||||
|
||||
public uint ProtectAt(ulong pageAddress)
|
||||
|
|
@ -281,7 +269,6 @@ public static unsafe class HostMemory
|
|||
Size = alignedSize,
|
||||
DefaultProtect = protect
|
||||
};
|
||||
PublishRegionsLocked();
|
||||
|
||||
return (void*)result;
|
||||
}
|
||||
|
|
@ -301,7 +288,6 @@ public static unsafe class HostMemory
|
|||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions.Remove((ulong)address);
|
||||
PublishRegionsLocked();
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -339,10 +325,6 @@ public static unsafe class HostMemory
|
|||
{
|
||||
info = default;
|
||||
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||
if (TryQueryUnlocked(pageAddress, ref info))
|
||||
{
|
||||
return (nuint)sizeof(BasicInfo);
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -358,37 +340,30 @@ public static unsafe class HostMemory
|
|||
var pageProtects = region.PageProtects;
|
||||
uint protect;
|
||||
ulong runEnd;
|
||||
if (pageProtects is null || pageProtects.Count == 0 || region.OverridePages is not { } overridePages)
|
||||
if (pageProtects is null || pageProtects.Count == 0)
|
||||
{
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
}
|
||||
else if (pageProtects.TryGetValue(pageAddress, out protect))
|
||||
{
|
||||
// Overrides run while the next page is overridden with the same protection.
|
||||
runEnd = pageAddress + PageSize;
|
||||
foreach (var overrideAddress in runEnd < region.End ? overridePages.GetViewBetween(runEnd, region.End - 1) : [])
|
||||
while (runEnd < region.End &&
|
||||
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||
nextProtect == protect)
|
||||
{
|
||||
if (overrideAddress != runEnd || pageProtects[overrideAddress] != protect)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runEnd += PageSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default protection runs to the next override.
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
if (pageAddress + PageSize < region.End)
|
||||
foreach (var overrideAddress in pageProtects.Keys)
|
||||
{
|
||||
// The first element only: a view's Count walks the whole view.
|
||||
foreach (var overrideAddress in overridePages.GetViewBetween(pageAddress + PageSize, region.End - 1))
|
||||
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||
{
|
||||
runEnd = overrideAddress;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -407,18 +382,12 @@ public static unsafe class HostMemory
|
|||
// reported as a free block reaching to the next tracked region
|
||||
// so scanning callers keep advancing.
|
||||
var nextBase = ulong.MaxValue;
|
||||
var keys = Regions.Keys;
|
||||
for (int low = 0, high = keys.Count - 1; low <= high;)
|
||||
foreach (var regionBase in Regions.Keys)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (keys[middle] > pageAddress)
|
||||
if (regionBase > pageAddress)
|
||||
{
|
||||
nextBase = keys[middle];
|
||||
high = middle - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
low = middle + 1;
|
||||
nextBase = regionBase;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,56 +416,6 @@ public static unsafe class HostMemory
|
|||
return false;
|
||||
}
|
||||
|
||||
// Answers without Gate for untracked memory and for regions without per-page overrides;
|
||||
// a region with overrides needs their dictionary, which only Gate protects. A concurrent
|
||||
// Protect may land just before or after either way, as with the locked query.
|
||||
private static bool TryQueryUnlocked(ulong pageAddress, ref BasicInfo info)
|
||||
{
|
||||
var regions = Volatile.Read(ref RegionSnapshot);
|
||||
Region? candidate = null;
|
||||
var nextBase = ulong.MaxValue;
|
||||
for (int low = 0, high = regions.Length - 1; low <= high;)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (regions[middle].Base <= pageAddress)
|
||||
{
|
||||
candidate = regions[middle];
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextBase = regions[middle].Base;
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate is not null && pageAddress < candidate.End)
|
||||
{
|
||||
if (Volatile.Read(ref candidate.PageProtects) is { Count: > 0 })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
info.BaseAddress = pageAddress;
|
||||
info.AllocationBase = candidate.Base;
|
||||
info.AllocationProtect = candidate.DefaultProtect;
|
||||
info.RegionSize = candidate.End - pageAddress;
|
||||
info.State = MEM_COMMIT;
|
||||
info.Protect = candidate.DefaultProtect;
|
||||
info.Type = MEM_PRIVATE;
|
||||
return true;
|
||||
}
|
||||
|
||||
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 true;
|
||||
}
|
||||
|
||||
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||
{
|
||||
region = null!;
|
||||
|
|
@ -535,26 +454,20 @@ public static unsafe class HostMemory
|
|||
{
|
||||
region.DefaultProtect = protect;
|
||||
region.PageProtects = null;
|
||||
region.OverridePages = null;
|
||||
return;
|
||||
}
|
||||
|
||||
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||
region.OverridePages ??= new SortedSet<ulong>();
|
||||
var end = start + size;
|
||||
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||
{
|
||||
if (protect == region.DefaultProtect)
|
||||
{
|
||||
if (region.PageProtects.Remove(pageAddress))
|
||||
{
|
||||
region.OverridePages.Remove(pageAddress);
|
||||
}
|
||||
region.PageProtects.Remove(pageAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
region.PageProtects[pageAddress] = protect;
|
||||
region.OverridePages.Add(pageAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
|
|
@ -134,15 +133,13 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var indexAddress = ctx[CpuRegister.Rdx];
|
||||
var modifier = ctx[CpuRegister.Rcx];
|
||||
var modifier = (uint)ctx[CpuRegister.Rcx];
|
||||
|
||||
if (commandBufferAddress == 0)
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndex", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
|
||||
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
||||
|
|
@ -242,13 +239,11 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var modifier = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0)
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndexAuto", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
||||
|
|
@ -462,19 +457,4 @@ public static partial class AgcExports
|
|||
#pragma warning restore SHEM006
|
||||
|
||||
private static long _indirectDrawEmitRejectCount;
|
||||
|
||||
private static readonly ConcurrentDictionary<(string, ulong), byte> _reportedDrawModifiers = new();
|
||||
|
||||
// The modifier names the shader stage that receives the draw and the user registers the
|
||||
// command processor fills for indirect draws. A direct draw emits the same packet for every
|
||||
// modifier; rejecting the unfamiliar ones dropped whole passes, such as Unreal's volume
|
||||
// rasterization through the geometry stage, without a trace.
|
||||
private static void NoteDrawModifier(string export, ulong modifier)
|
||||
{
|
||||
if (modifier != 0x4000_0000 && _reportedDrawModifiers.TryAdd((export, modifier), 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] agc.draw_modifier {export} modifier=0x{modifier:X} stage={(uint)modifier >> 29} flags=0x{(uint)modifier & 7u:X}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,24 +64,14 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||
var eventAddress = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || (eventAddress != 0 && (eventType & ~1u) != 0x38))
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
// Counter selection and counter dumps carry a payload in the address fields.
|
||||
var hasAddress = (eventType & ~1u) == 0x38;
|
||||
var packetDwords = hasAddress ? 4u : 2u;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, hasAddress ? eventType | 0x100u : eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (hasAddress &&
|
||||
(!TryWriteUInt32(ctx, commandAddress + 8, (uint)eventAddress & ~7u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(eventAddress >> 32))))
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ namespace SharpEmu.Libs.AppContent;
|
|||
public static class AppContentExports
|
||||
{
|
||||
private const ulong FallbackAvailableSpaceKb = 1024UL * 1024UL; // 1 GiB
|
||||
private const int AppContentErrorParameter = unchecked((int)0x80D90002);
|
||||
private const int AppContentErrorDrmNoEntitlement = unchecked((int)0x80D90007);
|
||||
private const int MountPointSize = 16;
|
||||
private const ulong BootParamAttrOffset = 4;
|
||||
private const string Temp0MountPoint = "/temp0";
|
||||
private const uint AppParamSkuFlag = 0;
|
||||
|
|
@ -99,6 +102,29 @@ public static class AppContentExports
|
|||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VANhIWcqYak",
|
||||
ExportName = "sceAppContentAddcontMount",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAppContent")]
|
||||
public static int AppContentAddcontMount(CpuContext ctx)
|
||||
{
|
||||
var entitlementLabelAddress = ctx[CpuRegister.Rsi];
|
||||
var mountPointAddress = ctx[CpuRegister.Rdx];
|
||||
if (entitlementLabelAddress == 0 || mountPointAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(AppContentErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> emptyMountPoint = stackalloc byte[MountPointSize];
|
||||
if (!ctx.Memory.TryWrite(mountPointAddress, emptyMountPoint))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(AppContentErrorDrmNoEntitlement);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "buYbeLOGWmA",
|
||||
ExportName = "sceAppContentTemporaryDataMount2",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ public static class AudioOut2Exports
|
|||
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
|
||||
private const int AudioOut2ContextParamSize = 0x40;
|
||||
private const int AudioOut2ContextMemorySize = 0x4000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x100;
|
||||
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
|
||||
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
|
||||
private const int SpeakerArrayHeaderSize = 0x40;
|
||||
|
|
@ -36,6 +35,17 @@ public static class AudioOut2Exports
|
|||
private const int SpeakerArrayDivisorFieldOffset = 0x34;
|
||||
private const int SpeakerArrayResultFieldOffset = 0x3C;
|
||||
private const uint SpeakerArrayDefaultDivisor = 1;
|
||||
private const int SpeakerArrayCoefficientBytes = 0x400;
|
||||
|
||||
private static readonly string _stackOutBufferModes =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_OUT2_STACK_WRITES") ?? "1";
|
||||
|
||||
private static bool AllowStackOut(string which) =>
|
||||
string.Equals(_stackOutBufferModes, "1", StringComparison.Ordinal) ||
|
||||
_stackOutBufferModes
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Contains(which, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
|
||||
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
|
||||
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
|
||||
|
|
@ -60,6 +70,7 @@ public static class AudioOut2Exports
|
|||
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
||||
private const int AttributeEntrySize = 0x18;
|
||||
private const uint PortAttributeIdPcm = 0;
|
||||
private const ushort AudioOut2PortTypeVibration = 6;
|
||||
private const uint PortAttributeIdGain = 1;
|
||||
private const uint PortAttributeIdPassthrough = 5;
|
||||
private const uint PortAttributeIdAmbisonics = 8;
|
||||
|
|
@ -88,6 +99,11 @@ public static class AudioOut2Exports
|
|||
private static long _pushTraceCount;
|
||||
private static long _submitTraceCount;
|
||||
private static long _submitSkipTraceCount;
|
||||
private static long _bedWriteTraceCount;
|
||||
private static long _portCreateTraceCount;
|
||||
private static long _portDestroyTraceCount;
|
||||
private static long _portStateTraceCount;
|
||||
private static long _attributeCallTraceCount;
|
||||
private static long _attributePcmTraceCount;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
|
||||
|
|
@ -212,7 +228,7 @@ public static class AudioOut2Exports
|
|||
public uint Ambisonics = AmbisonicsNone;
|
||||
public uint Passthrough;
|
||||
|
||||
public int PcmPending;
|
||||
public byte[]? PcmData;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +242,7 @@ public static class AudioOut2Exports
|
|||
private static string SecondaryBackendName = "none";
|
||||
private static ulong PrimaryContextHandle;
|
||||
private static readonly object HostSubmitGate = new();
|
||||
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "g2tViFIohHE",
|
||||
|
|
@ -252,6 +269,16 @@ public static class AudioOut2Exports
|
|||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v8iOE+j8a5o",
|
||||
ExportName = "sceAudioOut2MasteringSetParam",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2MasteringSetParam(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
||||
// tune, but failure here makes Yotei tear down its whole ACM context and
|
||||
// abort audio arena bring-up.
|
||||
|
|
@ -305,8 +332,8 @@ public static class AudioOut2Exports
|
|||
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
|
||||
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
var memorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memorySizeAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -324,13 +351,12 @@ public static class AudioOut2Exports
|
|||
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
|
||||
}
|
||||
|
||||
// The output is a single size_t wherever it lives. Titles keep other locals right after it
|
||||
// (Octopath Traveler II divides by the dword at +12), so nothing else may be written.
|
||||
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory " +
|
||||
$"out=0x{memorySizeAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -391,8 +417,20 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||
{
|
||||
// Shared backend lifetime is process-wide; just drop the context entry.
|
||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
var contextHandle = ctx[CpuRegister.Rdi];
|
||||
Contexts.TryRemove(contextHandle, out _);
|
||||
foreach (var (portHandle, port) in Ports)
|
||||
{
|
||||
if (port.ContextHandle != contextHandle ||
|
||||
!Ports.TryRemove(portHandle, out var removedPort))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref removedPort.PcmData, null);
|
||||
removedPort.PcmAddress = 0;
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +439,38 @@ public static class AudioOut2Exports
|
|||
ExportName = "sceAudioOut2ContextBedWrite",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _bedWriteTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"context-bed-write#{n} context=0x{ctx[CpuRegister.Rdi]:X} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} " +
|
||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X} r8=0x{ctx[CpuRegister.R8]:X} " +
|
||||
$"r9=0x{ctx[CpuRegister.R9]:X}");
|
||||
|
||||
Span<byte> probe = stackalloc byte[0x40];
|
||||
foreach (var (name, address) in new[]
|
||||
{
|
||||
("rsi", ctx[CpuRegister.Rsi]),
|
||||
("rdx", ctx[CpuRegister.Rdx]),
|
||||
("rcx", ctx[CpuRegister.Rcx]),
|
||||
("r8", ctx[CpuRegister.R8]),
|
||||
("r9", ctx[CpuRegister.R9]),
|
||||
})
|
||||
{
|
||||
if (address != 0 && ctx.Memory.TryRead(address, probe))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"context-bed-write-probe#{n} {name}=0x{address:X} " +
|
||||
$"bytes={Convert.ToHexString(probe)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aII9h5nli9U",
|
||||
|
|
@ -494,11 +563,6 @@ public static class AudioOut2Exports
|
|||
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
|
||||
var outLevelAddress = ctx[CpuRegister.Rsi];
|
||||
var outAvailableAddress = ctx[CpuRegister.Rdx];
|
||||
if (outLevelAddress == 0)
|
||||
{
|
||||
outLevelAddress = outAvailableAddress;
|
||||
outAvailableAddress = 0;
|
||||
}
|
||||
|
||||
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
|
||||
// update and pushes them non-blocking, so a queue always reported empty made it render a
|
||||
|
|
@ -550,6 +614,41 @@ public static class AudioOut2Exports
|
|||
var portHandle = ctx[CpuRegister.Rdi];
|
||||
var attributesAddress = ctx[CpuRegister.Rsi];
|
||||
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
var attributeStride = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||
var traceCall = Interlocked.Increment(ref _attributeCallTraceCount);
|
||||
if (traceCall <= 8 && attributesAddress != 0)
|
||||
{
|
||||
Span<byte> descriptorProbe = stackalloc byte[0x40];
|
||||
if (ctx.Memory.TryRead(attributesAddress, descriptorProbe))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-raw#{traceCall} port=0x{portHandle:X} " +
|
||||
$"count={attributeCount} stride={attributeStride} address=0x{attributesAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(descriptorProbe)}");
|
||||
|
||||
Span<byte> targetProbe = stackalloc byte[0x20];
|
||||
for (var offset = 0; offset <= descriptorProbe.Length - sizeof(ulong); offset += sizeof(ulong))
|
||||
{
|
||||
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(descriptorProbe[offset..]);
|
||||
if (!IsWritableOutBuffer(candidate) ||
|
||||
!ctx.Memory.TryRead(candidate, targetProbe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-target#{traceCall} offset=0x{offset:X} " +
|
||||
$"address=0x{candidate:X} bytes={Convert.ToHexString(targetProbe)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-attributes-raw#{traceCall} read-failed address=0x{attributesAddress:X} " +
|
||||
$"count={attributeCount} stride={attributeStride}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Ports.TryGetValue(portHandle, out var port))
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
|
|
@ -622,14 +721,38 @@ public static class AudioOut2Exports
|
|||
continue;
|
||||
}
|
||||
|
||||
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
|
||||
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
byte[]? pcmData = null;
|
||||
if (pcmAddress != 0 &&
|
||||
TryDecodeDataFormat(port.DataFormat, out var channels, out var bytesPerSample, out _) &&
|
||||
IsSnapshotPcmPort(port.PortType, channels))
|
||||
{
|
||||
int byteLength;
|
||||
try
|
||||
{
|
||||
byteLength = checked((int)port.GrainSamples * channels * bytesPerSample);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
pcmData = new byte[byteLength];
|
||||
if (!ctx.Memory.TryRead(pcmAddress, pcmData))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
port.PcmAddress = pcmAddress;
|
||||
Volatile.Write(ref port.PcmData, pcmData);
|
||||
var n = Interlocked.Increment(ref _attributePcmTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " +
|
||||
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
|
||||
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
||||
$"grains={port.GrainSamples}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -645,10 +768,10 @@ public static class AudioOut2Exports
|
|||
{
|
||||
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
|
||||
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
|
||||
// rdx is the only out pointer.
|
||||
var contextHandle = ctx[CpuRegister.Rdi];
|
||||
var paramAddress = ctx[CpuRegister.Rsi];
|
||||
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
||||
if (outPortAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
|
|
@ -669,6 +792,14 @@ public static class AudioOut2Exports
|
|||
Span<byte> param = stackalloc byte[PortParamSize];
|
||||
if (ctx.Memory.TryRead(paramAddress, param))
|
||||
{
|
||||
if (traceCreate <= 8 || traceCreate % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-create-param#{traceCreate} context=0x{contextHandle:X} " +
|
||||
$"address=0x{paramAddress:X} out=0x{outPortAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(param)}");
|
||||
}
|
||||
|
||||
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
|
||||
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
||||
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
||||
|
|
@ -719,13 +850,16 @@ public static class AudioOut2Exports
|
|||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
if (IsGuestStackAddress(stateAddress) &&
|
||||
!(AllowStackOut("portstate") && Ports.ContainsKey(portHandle)))
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
Span<byte> state = stackalloc byte[PortStateSize];
|
||||
state.Clear();
|
||||
// +0x00 u16 output = OUTPUT_PRIMARY (1)
|
||||
// +0x02 u8 numChannels = from port format when known, else 2
|
||||
// +0x04 s16 volume = -1 (not applicable outside PADSPK)
|
||||
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
|
||||
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
|
||||
byte channels = 2;
|
||||
if (Ports.TryGetValue(portHandle, out var port) &&
|
||||
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
|
||||
|
|
@ -735,15 +869,21 @@ public static class AudioOut2Exports
|
|||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
||||
state[0x02] = channels;
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
|
||||
|
||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
||||
var traceState = Interlocked.Increment(ref _portStateTraceCount);
|
||||
if (traceState <= 8 || traceState % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-get-state#{traceState} handle=0x{portHandle:X} state=0x{stateAddress:X} " +
|
||||
$"bytes={Convert.ToHexString(state)}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -972,7 +1112,17 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx)
|
||||
{
|
||||
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
var portHandle = ctx[CpuRegister.Rdi];
|
||||
var removed = Ports.TryRemove(portHandle, out var port);
|
||||
var n = Interlocked.Increment(ref _portDestroyTraceCount);
|
||||
if (n <= 8 || n % 500 == 0)
|
||||
{
|
||||
TraceAudioOut2(
|
||||
$"port-destroy#{n} handle=0x{portHandle:X} removed={removed} " +
|
||||
$"context=0x{port?.ContextHandle ?? 0:X} pcm=0x{port?.PcmAddress ?? 0:X} " +
|
||||
$"remaining={Ports.Count}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1064,13 +1214,20 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
// Deeper host queue than classic AudioOut: FMOD's bursty
|
||||
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
|
||||
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
PrimaryBackendName = audio.BackendName + "-primary";
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
PrimaryBackend = streamFactory(context.Frequency);
|
||||
PrimaryBackendName = PrimaryBackend is null ? "silent" : "test-primary";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
PrimaryBackendName = audio.BackendName + "-primary";
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1088,11 +1245,20 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
SecondaryBackend = streamFactory(context.Frequency);
|
||||
SecondaryBackendName = SecondaryBackend is null ? "silent" : "test-secondary";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1155,7 +1321,6 @@ public static class AudioOut2Exports
|
|||
lock (HostSubmitGate)
|
||||
{
|
||||
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
|
||||
var source = ArrayPool<byte>.Shared.Rent(frames * 16 * sizeof(float));
|
||||
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
|
||||
try
|
||||
{
|
||||
|
|
@ -1164,29 +1329,42 @@ public static class AudioOut2Exports
|
|||
foreach (var port in Ports.Values)
|
||||
{
|
||||
if (port.ContextHandle != context.Handle ||
|
||||
port.PcmAddress == 0 ||
|
||||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
|
||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) ||
|
||||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
|
||||
out var leftGain, out var rightGain))
|
||||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pcmData = TakePortPcmSnapshot(port);
|
||||
if (pcmData is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetStereoGains(
|
||||
port.PortType,
|
||||
ch,
|
||||
port.Gain,
|
||||
port.Ambisonics,
|
||||
port.Passthrough,
|
||||
out var leftGain,
|
||||
out var rightGain))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var byteLength = checked(frames * ch * bps);
|
||||
if (byteLength <= 0 || byteLength > source.Length)
|
||||
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceSpan = source.AsSpan(0, byteLength);
|
||||
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
|
||||
if (pcmData.Length < byteLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MixPortIntoStereo(
|
||||
sourceSpan,
|
||||
pcmData.AsSpan(0, byteLength),
|
||||
mix.AsSpan(0, frames * 2),
|
||||
frames,
|
||||
ch,
|
||||
|
|
@ -1238,12 +1416,70 @@ public static class AudioOut2Exports
|
|||
finally
|
||||
{
|
||||
ArrayPool<float>.Shared.Return(mix);
|
||||
ArrayPool<byte>.Shared.Return(source);
|
||||
ArrayPool<byte>.Shared.Return(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[]? GetPortPcmSnapshotForTests(ulong portHandle)
|
||||
{
|
||||
if (!Ports.TryGetValue(portHandle, out var port))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var snapshot = Volatile.Read(ref port.PcmData);
|
||||
return snapshot?.ToArray();
|
||||
}
|
||||
|
||||
internal static bool HasPortForTests(ulong portHandle) => Ports.ContainsKey(portHandle);
|
||||
|
||||
internal static byte[]? TakePortPcmSnapshotForTests(ulong portHandle)
|
||||
{
|
||||
return Ports.TryGetValue(portHandle, out var port)
|
||||
? TakePortPcmSnapshot(port)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static byte[]? TakePortPcmSnapshot(PortState port) =>
|
||||
Interlocked.Exchange(ref port.PcmData, null);
|
||||
|
||||
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Ports.Clear();
|
||||
SpeakerArrays.Clear();
|
||||
Interlocked.Exchange(ref _nextContextHandle, 1);
|
||||
Interlocked.Exchange(ref _nextUserHandle, 1);
|
||||
Interlocked.Exchange(ref _nextPortId, 0);
|
||||
Interlocked.Exchange(ref _pushTraceCount, 0);
|
||||
Interlocked.Exchange(ref _submitTraceCount, 0);
|
||||
Interlocked.Exchange(ref _submitSkipTraceCount, 0);
|
||||
Interlocked.Exchange(ref _attributePcmTraceCount, 0);
|
||||
|
||||
lock (HostBackendGate)
|
||||
{
|
||||
PrimaryBackend?.Dispose();
|
||||
SecondaryBackend?.Dispose();
|
||||
PrimaryBackend = null;
|
||||
SecondaryBackend = null;
|
||||
PrimaryBackendName = "none";
|
||||
SecondaryBackendName = "none";
|
||||
PrimaryContextHandle = 0;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _streamFactoryForTests, null);
|
||||
}
|
||||
|
||||
private static bool IsMainOrBgmPort(ushort portType)
|
||||
{
|
||||
var kind = portType & 0xFF;
|
||||
return kind is 0 or 1;
|
||||
}
|
||||
|
||||
internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2;
|
||||
|
||||
internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
|
||||
|
|
@ -1298,6 +1534,11 @@ public static class AudioOut2Exports
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
|
||||
RoutesToSpeakers(portType) &&
|
||||
(portType & 0xFF) != AudioOut2PortTypeVibration &&
|
||||
(!IsObjectPort(portType) || channels == 1);
|
||||
|
||||
internal static void MixPortIntoStereo(
|
||||
ReadOnlySpan<byte> source,
|
||||
Span<float> mix,
|
||||
|
|
@ -1453,13 +1694,19 @@ public static class AudioOut2Exports
|
|||
private static bool IsDirectMemoryWindowAddress(ulong value) =>
|
||||
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
|
||||
|
||||
// Only sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel still
|
||||
// pick an out buffer by host address range; both are left untouched here so they do
|
||||
// not collide with the changes already in flight for them, and these two predicates
|
||||
// go away with the last of their callers.
|
||||
private static bool IsGuestStackAddress(ulong value) =>
|
||||
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
||||
|
||||
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
|
||||
{
|
||||
if (IsWritableOutBuffer(primary))
|
||||
{
|
||||
return primary;
|
||||
}
|
||||
|
||||
return IsWritableOutBuffer(secondary) ? secondary : 0;
|
||||
}
|
||||
|
||||
private static bool IsWritableOutBuffer(ulong value) =>
|
||||
value != 0 &&
|
||||
value != 0x10000UL &&
|
||||
|
|
|
|||
|
|
@ -881,6 +881,32 @@ public static class AvPlayerExports
|
|||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N6Oy-EjduiY",
|
||||
ExportName = "sceAvPlayerSetAvailableBandwidth",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
[SysAbiExport(
|
||||
Nid = "+7xJ+lFxmkQ",
|
||||
ExportName = "sceAvPlayerSetAvailableBW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerSetAvailableBandwidth(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var availableBandwidth = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Players.ContainsKey(handle))
|
||||
{
|
||||
return SetReturn(ctx, InvalidParameters);
|
||||
}
|
||||
|
||||
Trace($"set_available_bandwidth handle=0x{handle:X16} bandwidth={availableBandwidth}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ODJK2sn9w4A",
|
||||
ExportName = "sceAvPlayerEnableStream",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut;
|
|||
|
||||
namespace SharpEmu.Libs.Codec;
|
||||
|
||||
internal sealed record Videodec2DecodedFrame(
|
||||
byte[] Nv12,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Pitch,
|
||||
bool ErrorFrame)
|
||||
{
|
||||
public ulong Pts { get; init; } = ulong.MaxValue;
|
||||
public ulong Dts { get; init; } = ulong.MaxValue;
|
||||
public ulong AttachedData { get; init; }
|
||||
public bool KeyFrame { get; init; }
|
||||
public uint Profile { get; init; }
|
||||
public uint Level { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
|
||||
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
|
||||
|
|
@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
// Enough lookahead to absorb decode jitter without adding visible latency.
|
||||
private const int FrameQueueCapacity = 4;
|
||||
|
||||
private const int WorkQueueCapacity = 64;
|
||||
|
||||
// Fallback when the stream doesn't declare a usable framerate.
|
||||
private const double FallbackFps = 30.0;
|
||||
|
||||
|
|
@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private bool _disposed;
|
||||
|
||||
// Unbounded: access units are small, backpressure lives on the frame queue below.
|
||||
private readonly Channel<byte[]?> _workChannel =
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private uint _lastWidth;
|
||||
private uint _lastHeight;
|
||||
|
||||
private long _workerInputCount;
|
||||
private long _workerFrameCount;
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
|
||||
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
|
||||
{
|
||||
_codecContext = codecContext;
|
||||
|
|
@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
|
||||
if (codec == null)
|
||||
{
|
||||
Trace("create failed: H.264 decoder not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(codec);
|
||||
if (codecContext == null)
|
||||
{
|
||||
Trace("create failed: avcodec_alloc_context3 returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: avcodec_open2 rejected H.264 decoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
}
|
||||
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
|
||||
{
|
||||
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
|
||||
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
|
||||
if (codecContext != null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
|
|
@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public void RequestDrain()
|
||||
public bool RequestDrain()
|
||||
{
|
||||
_workChannel.Writer.TryWrite(null);
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
}
|
||||
|
||||
public bool DecodeAccessUnit(
|
||||
byte[] accessUnit,
|
||||
ulong pts,
|
||||
ulong dts,
|
||||
ulong attachedData,
|
||||
out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed || accessUnit.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
|
||||
if (packetResult < 0)
|
||||
{
|
||||
Trace($"decode failed: av_new_packet={packetResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* source = accessUnit)
|
||||
{
|
||||
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
|
||||
}
|
||||
|
||||
_packet->pts = ToAvTimestamp(pts);
|
||||
_packet->dts = ToAvTimestamp(dts);
|
||||
|
||||
var havePendingFrame = false;
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (pendingResult < 0)
|
||||
{
|
||||
Trace($"decode failed: receive-before-resend={pendingResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
havePendingFrame = true;
|
||||
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
}
|
||||
|
||||
if (sendResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_send_packet={sendResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!havePendingFrame)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
decodedFrame = ConvertFrameToNv12Locked();
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ffmpeg.avcodec_flush_buffers(_codecContext);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
|
||||
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
|
||||
ulong fallbackPts = ulong.MaxValue,
|
||||
ulong fallbackDts = ulong.MaxValue,
|
||||
ulong attachedData = 0)
|
||||
{
|
||||
if (_frame->width <= 0 || _frame->height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var width = checked((uint)_frame->width);
|
||||
var height = checked((uint)_frame->height);
|
||||
uint pitch;
|
||||
ulong lumaBytes;
|
||||
ulong requiredBytes;
|
||||
try
|
||||
{
|
||||
pitch = checked((width + 255u) & ~255u);
|
||||
lumaBytes = checked((ulong)pitch * height);
|
||||
var chromaRows = ((ulong)height + 1UL) / 2UL;
|
||||
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredBytes > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
AVPixelFormat.AV_PIX_FMT_NV12,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nv12 = new byte[(int)requiredBytes];
|
||||
fixed (byte* destination = nv12)
|
||||
{
|
||||
var destinationPlanes = new byte*[4]
|
||||
{
|
||||
destination,
|
||||
destination + checked((int)lumaBytes),
|
||||
null,
|
||||
null,
|
||||
};
|
||||
var destinationStrides = new int[4]
|
||||
{
|
||||
checked((int)pitch),
|
||||
checked((int)pitch),
|
||||
0,
|
||||
0,
|
||||
};
|
||||
var rows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
if (rows != _frame->height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Videodec2DecodedFrame(
|
||||
nv12,
|
||||
width,
|
||||
height,
|
||||
pitch,
|
||||
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
|
||||
{
|
||||
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
|
||||
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
|
||||
AttachedData = attachedData,
|
||||
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
|
||||
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
|
||||
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static long ToAvTimestamp(ulong timestamp) =>
|
||||
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
|
||||
|
||||
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
|
||||
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
|
||||
|
||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
||||
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
|
||||
{
|
||||
|
|
@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
|
||||
var decodedOk = item is null
|
||||
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
|
||||
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
|
||||
if (ShouldTrace(inputOrdinal))
|
||||
{
|
||||
Trace(
|
||||
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
|
||||
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
|
||||
}
|
||||
|
||||
if (!decodedOk || !hasPicture || bgraFrame is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
|
||||
if (ShouldTrace(frameOrdinal))
|
||||
{
|
||||
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
|
||||
|
|
@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
|
|
@ -15,10 +16,18 @@ public static class Videodec2Exports
|
|||
{
|
||||
private const int Ok = 0;
|
||||
|
||||
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
|
||||
private const uint FrameBufferAlignment = 0x100;
|
||||
|
||||
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
|
||||
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
|
||||
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
private static long _decodeCallCount;
|
||||
private static long _decodedPictureCount;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RnDibcGCPKw",
|
||||
ExportName = "sceVideodec2QueryComputeMemoryInfo",
|
||||
|
|
@ -27,24 +36,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
// Success needs no memory writes; the game initializes from its own fields.
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
|
||||
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
|
||||
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
|
||||
|
||||
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
|
||||
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
|
||||
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
|
||||
|
||||
// Opaque token the game hands back unmodified to later Videodec2 calls.
|
||||
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eD+X2SmxUt4",
|
||||
ExportName = "sceVideodec2AllocateComputeQueue",
|
||||
|
|
@ -52,8 +62,14 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var queueAddress = ctx[CpuRegister.Rdx];
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
|
||||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
|
||||
computeMemoryAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(
|
||||
queueAddress, computeMemoryAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -61,7 +77,18 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "UvtA3FAiF4Y",
|
||||
ExportName = "sceVideodec2ReleaseComputeQueue",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -69,12 +96,18 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
|
||||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -99,7 +132,9 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -111,7 +146,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
|
||||
[SysAbiExport(
|
||||
Nid = "l1hXwscLuCY",
|
||||
ExportName = "sceVideodec2Flush",
|
||||
|
|
@ -120,33 +154,25 @@ public static class Videodec2Exports
|
|||
public static int Videodec2Flush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rsi];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
{
|
||||
// Drain in order: report an already-finished frame before queuing a new drain request.
|
||||
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
|
||||
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decoder.RequestDrain();
|
||||
}
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -154,6 +180,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2Reset(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
decoder.Reset();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +205,6 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
|
||||
// uninitialized stack and must always be written explicitly.
|
||||
[SysAbiExport(
|
||||
Nid = "852F5+q6+iM",
|
||||
ExportName = "sceVideodec2Decode",
|
||||
|
|
@ -186,8 +216,9 @@ public static class Videodec2Exports
|
|||
var inputAuStruct = ctx[CpuRegister.Rsi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rdx];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rcx];
|
||||
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
|
||||
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -195,6 +226,7 @@ public static class Videodec2Exports
|
|||
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
|
||||
{
|
||||
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
|
||||
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -208,35 +240,235 @@ public static class Videodec2Exports
|
|||
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
|
||||
{
|
||||
// Nothing sane to feed/fill this call; not an error.
|
||||
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
|
||||
|
||||
var auBuffer = new byte[auDataSize];
|
||||
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
|
||||
{
|
||||
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
var decodeOk = decoder.DecodeAccessUnit(
|
||||
auBuffer, pts, dts, attachedData, out var decodedFrame);
|
||||
if (ShouldTrace(callOrdinal))
|
||||
{
|
||||
var prefixBytes = Math.Min(auBuffer.Length, 8);
|
||||
Trace(
|
||||
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
|
||||
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
|
||||
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
|
||||
$"picture={decodedFrame is not null}");
|
||||
}
|
||||
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
[SysAbiExport(
|
||||
Nid = "NtXRa3dRzU0",
|
||||
ExportName = "sceVideodec2GetPictureInfo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2GetPictureInfo(CpuContext ctx)
|
||||
{
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdi];
|
||||
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
|
||||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
|
||||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
|
||||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
|
||||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
|
||||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorOutputInfo);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
|
||||
(requestedSize != 104 && requestedSize != 120))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
var picture = new byte[(int)requestedSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
|
||||
picture[0x08] = 1;
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
|
||||
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
|
||||
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
|
||||
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
|
||||
picture[0x34] = 1;
|
||||
picture[0x4E] = 1;
|
||||
picture[0x4F] = 5;
|
||||
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (secondPictureInfoAddress != 0 &&
|
||||
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"picture_info buffer=0x{pixelsAddress:X16} " +
|
||||
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NotAccepted = [0];
|
||||
private static readonly byte[] Accepted = [1];
|
||||
private static readonly byte[] NoPicture = [0, 0, 0, 0];
|
||||
private static readonly byte[] PictureReady = [1, 0, 1, 0];
|
||||
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
|
||||
|
||||
private static bool TryInitializeNoPicture(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress)
|
||||
{
|
||||
return frameBufferAddress != 0 &&
|
||||
outputInfoAddress != 0 &&
|
||||
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
|
||||
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
|
||||
}
|
||||
|
||||
internal static bool TryWriteDecodedFrame(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress,
|
||||
Videodec2DecodedFrame decodedFrame)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
|
||||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
|
||||
pixelsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
|
||||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputInfoBytes >= 0x38 &&
|
||||
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
|
||||
!ctx.Memory.TryWrite(
|
||||
outputInfoAddress + 0x08,
|
||||
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PictureInfos[pixelsAddress] = decodedFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var output = new byte[(int)size];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
|
||||
return ctx.Memory.TryWrite(address, output);
|
||||
}
|
||||
|
||||
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 IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void TraceDecode(long ordinal, string message)
|
||||
{
|
||||
if (ShouldTrace(ordinal))
|
||||
{
|
||||
Trace($"decode call={ordinal} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Headless lifecycle for the system web-browser dialog. SharpEmu does not yet
|
||||
/// provide a host browser, so an opened dialog completes on its first status
|
||||
/// poll instead of leaving games in an infinite RUNNING loop.
|
||||
/// </summary>
|
||||
public static class WebBrowserDialogExports
|
||||
{
|
||||
internal const int StatusNone = 0;
|
||||
internal const int StatusInitialized = 1;
|
||||
internal const int StatusRunning = 2;
|
||||
internal const int StatusFinished = 3;
|
||||
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jqb7HntFQFc",
|
||||
ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FraP7debcdg",
|
||||
ExportName = "sceWebBrowserDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpen(CpuContext ctx) => Open(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "O7dIZQrwVFY",
|
||||
ExportName = "sceWebBrowserDialogOpenForPredeterminedContent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpenForPredeterminedContent(CpuContext ctx) => Open(ctx);
|
||||
|
||||
private static int Open(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _status, StatusRunning);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CFTG6a8TjOU",
|
||||
ExportName = "sceWebBrowserDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "h1dR-t5ISgg",
|
||||
ExportName = "sceWebBrowserDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
private static int PollStatus()
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||
return Volatile.Read(ref _status);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PSK+Eik919Q",
|
||||
ExportName = "sceWebBrowserDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogClose(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vCaW0fgVQmc",
|
||||
ExportName = "sceWebBrowserDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||
result.Clear();
|
||||
if (!ctx.Memory.TryWrite(ctx[CpuRegister.Rdi], result))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wit4LjeoeX4",
|
||||
ExportName = "sceWebBrowserDialogGetEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetEvent(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uYELOMVnmNQ",
|
||||
ExportName = "sceWebBrowserDialogNavigate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogNavigate(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RLhKBOoNyXY",
|
||||
ExportName = "sceWebBrowserDialogSetZoom",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogSetZoom(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ocHtyBwHfys",
|
||||
ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
internal static int StatusForTests => Volatile.Read(ref _status);
|
||||
|
||||
internal static void ResetForTests() => Interlocked.Exchange(ref _status, StatusNone);
|
||||
}
|
||||
|
|
@ -149,9 +149,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
public bool IsCoherent { get; }
|
||||
|
||||
// The host mapping of the whole buffer, or null for memory the host cannot map.
|
||||
public byte* MappedPointer => _mapped;
|
||||
|
||||
public GpuBufferUsage Usage { get; }
|
||||
|
||||
public ulong CpuAddress { get; }
|
||||
|
|
@ -342,7 +339,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
var (required, preferred, avoided) = usage switch
|
||||
{
|
||||
GpuBufferUsage.DeviceLocal => (MemoryPropertyFlags.None, MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Unified => (MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Upload => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
GpuBufferUsage.Download => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
_ => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,4 @@ public enum GpuBufferUsage : byte
|
|||
Upload,
|
||||
Download,
|
||||
Stream,
|
||||
// Device-local memory the host can map: guest buffers on unified-memory devices, read back without a copy.
|
||||
Unified,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly IGpuQueueRelay _relay;
|
||||
private readonly GuestBufferUploader _uploader;
|
||||
private readonly bool _unifiedBuffers;
|
||||
private readonly IGuestBackedSpace _backing;
|
||||
private readonly BdaFaultProcessor _faults;
|
||||
private readonly GpuBuffer _gds;
|
||||
|
|
@ -73,7 +72,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_relay = relay;
|
||||
_backing = backing;
|
||||
_faults = new BdaFaultProcessor(device, scheduler, this, CachingPageBits, CachingPageCount);
|
||||
_unifiedBuffers = UnifiedBuffersEnabled && HasUnifiedMemoryType(device);
|
||||
_gds = new GpuBuffer(device, scheduler, GpuBufferUsage.Stream, 0, GpuBuffer.AllFlags, GdsBufferSize);
|
||||
_bdaPageTable = new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, BdaPageTableSize);
|
||||
_tracker = new GuestPageTracker(pages);
|
||||
|
|
@ -898,45 +896,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
|
||||
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
{
|
||||
// A GPU write that lands after the copy was recorded makes the read stale. Retrying
|
||||
// synchronously stalls the command worker until the GPU drains, so re-issue the read and
|
||||
// wait here again; only the last attempt falls back to the synchronous read.
|
||||
for (var attempt = 1; ; attempt++)
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (!_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pending.Ticket is { } ticket)
|
||||
{
|
||||
(AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.")).Wait(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scheduler.WaitForSubmittedTick(pending.MainQueueTick);
|
||||
}
|
||||
|
||||
var finalAttempt = attempt >= PendingReadAttempts;
|
||||
var applied = false;
|
||||
if (!_relay.TryRunOnGpuQueue(() => applied = CompleteReadMemoryOnGpu(pending, retrySynchronously: finalAttempt)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (applied || finalAttempt)
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -1167,14 +1144,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
|
||||
|
||||
// Ticket is set when the readback queue carries the download; otherwise the main queue
|
||||
// does, into MainQueueBuffer, and MainQueueTick is the submission the guest thread waits for.
|
||||
private sealed record PendingDownload
|
||||
{
|
||||
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
|
||||
public ulong MainQueueTick { get; init; }
|
||||
public GpuBuffer? MainQueueBuffer { get; init; }
|
||||
public ulong[] MainQueueOffsets { get; init; } = [];
|
||||
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
|
||||
public required List<DownloadPiece> Copies { get; init; }
|
||||
public required ulong[] WriteTicks { get; init; }
|
||||
public required ulong WindowBegin { get; init; }
|
||||
|
|
@ -1184,9 +1156,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public required bool IsWrite { get; init; }
|
||||
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
|
||||
public required long Started { get; init; }
|
||||
|
||||
// Read from the buffers' own host mappings once the writer tick retires; no copy was recorded.
|
||||
public bool Mapped { get; init; }
|
||||
}
|
||||
|
||||
private PendingDownload? BeginReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source,
|
||||
|
|
@ -1210,53 +1179,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
{
|
||||
NoteHotReadback(guestAddress, size);
|
||||
}
|
||||
if (copies.Count != 0 && _unifiedBuffers && AreMapped(copies, out var writer))
|
||||
{
|
||||
if (writer == 0 || _scheduler.IsTickComplete(writer))
|
||||
{
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allowPending && writer < _scheduler.CurrentTick)
|
||||
{
|
||||
// A guest thread had to wait for this writer: submit later writes to the window right
|
||||
// after their command, so the next wait ends with them instead of with their whole batch.
|
||||
NoteHotReadback(guestAddress, size);
|
||||
|
||||
// The writer is already submitted: wait for it alone, not for the work queued after it.
|
||||
var mappedTicks = new ulong[copies.Count];
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
mappedTicks[index] = copies[index].Buffer.LastGpuWriteTick;
|
||||
copies[index].Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
Mapped = true,
|
||||
MainQueueTick = writer,
|
||||
Copies = copies,
|
||||
WriteTicks = mappedTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (!allowPending)
|
||||
{
|
||||
// On the command worker: submit the writer if it is still recording, then read the mapping.
|
||||
_scheduler.Wait(writer);
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending && TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
|
|
@ -1279,31 +1201,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending &&
|
||||
TryBeginDownloadOnMainQueue(copies, out var mainTick, out var mainBuffer, out var mainOffsets, out var mainWriteTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
copy.Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
MainQueueTick = mainTick,
|
||||
MainQueueBuffer = mainBuffer,
|
||||
MainQueueOffsets = mainOffsets,
|
||||
Copies = copies,
|
||||
WriteTicks = mainWriteTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0)
|
||||
{
|
||||
DownloadBufferMemory(copies);
|
||||
|
|
@ -1319,75 +1216,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue copies the pieces into a buffer of their own and
|
||||
// submits at once; the guest thread then waits for that submission instead of the worker
|
||||
// waiting for the whole GPU (a guest read of GPU-written memory otherwise idled the worker
|
||||
// for tens of milliseconds, several times per frame in Silent Hill: The Short Message).
|
||||
private bool TryBeginDownloadOnMainQueue(List<DownloadPiece> copies, out ulong tick, out GpuBuffer buffer, out ulong[] offsets, out ulong[] writeTicks)
|
||||
{
|
||||
tick = 0;
|
||||
buffer = null!;
|
||||
offsets = [];
|
||||
writeTicks = [];
|
||||
if (!MainQueuePendingReads || copies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ticks = new ulong[copies.Count];
|
||||
var placements = new ulong[copies.Count];
|
||||
var total = 0UL;
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var written = copies[index].Buffer.LastGpuWriteTick;
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ticks[index] = written;
|
||||
placements[index] = total;
|
||||
total += (copies[index].Size + BufferDownloadBatchPlanner.Alignment - 1) & ~(BufferDownloadBatchPlanner.Alignment - 1);
|
||||
}
|
||||
|
||||
if (total > AsyncReadbackLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, total);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
buffer.CopyFrom(
|
||||
_scheduler.Current, copies[index].Buffer, copies[index].SourceOffset, placements[index], copies[index].Size,
|
||||
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
||||
}
|
||||
|
||||
tick = _scheduler.Flush();
|
||||
offsets = placements;
|
||||
writeTicks = ticks;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool MainQueuePendingReads =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_MAIN_QUEUE_PENDING_READS") != "0";
|
||||
|
||||
// Pending reads a guest thread re-issues before it lets the worker read synchronously.
|
||||
private const int PendingReadAttempts = 4;
|
||||
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
if (pending.Mapped)
|
||||
{
|
||||
return CompleteMappedRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
if (pending.Ticket is null)
|
||||
{
|
||||
return CompleteMainQueueRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
|
||||
try
|
||||
{
|
||||
|
|
@ -1438,155 +1269,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Guest buffers live in host-visible device memory on unified-memory devices (Apple GPUs), so a
|
||||
// readback can copy straight out of the buffer once the last GPU write to it retired. That skips
|
||||
// the copy command at the tail of the queue and the wait for every draw recorded after the writer.
|
||||
// SHARPEMU_UNIFIED_BUFFERS=0 keeps device-local buffers and copy readbacks.
|
||||
private static readonly bool UnifiedBuffersEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_UNIFIED_BUFFERS") != "0";
|
||||
|
||||
private static bool HasUnifiedMemoryType(GpuDeviceInfo device)
|
||||
{
|
||||
const MemoryPropertyFlags unified = MemoryPropertyFlags.DeviceLocalBit | MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit;
|
||||
for (uint index = 0; index < device.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((device.GetMemoryTypeFlags(index) & unified) == unified)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AreMapped(List<DownloadPiece> copies, out ulong writer)
|
||||
{
|
||||
writer = 0;
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
if (copy.Buffer.MappedPointer == null || !copy.Buffer.IsCoherent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
writer = Math.Max(writer, copy.Buffer.LastGpuWriteTick);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ApplyMappedRead(List<DownloadPiece> copies, ulong windowBegin, ulong windowEnd, ulong guestAddress, ulong size,
|
||||
bool isWrite, long started, GuestMemoryProfile.ReadbackSource source)
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
var bytes = new ReadOnlySpan<byte>(copy.Buffer.MappedPointer + copy.SourceOffset, checked((int)copy.Size));
|
||||
if (!_backing.TryWriteBacking(copy.Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{copy.Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(windowBegin, windowEnd - windowBegin);
|
||||
if (isWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(guestAddress, size);
|
||||
}
|
||||
|
||||
MappedReadbacks++;
|
||||
RecordReadback(windowBegin, windowEnd, isWrite, copies, started, source);
|
||||
}
|
||||
|
||||
// Readbacks served from a buffer's own mapping, without a copy command.
|
||||
internal long MappedReadbacks { get; private set; }
|
||||
|
||||
internal bool UnifiedBuffers => _unifiedBuffers;
|
||||
|
||||
private bool CompleteMappedRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
ApplyMappedRead(pending.Copies, pending.WindowBegin, pending.WindowEnd, pending.GuestAddress, pending.Size,
|
||||
pending.IsWrite, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CompleteMainQueueRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
var buffer = pending.MainQueueBuffer ?? throw SubmissionScheduler.Fatal("The pending readback lost its buffer.");
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
var copies = pending.Copies;
|
||||
buffer.Invalidate(0, buffer.Size);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var bytes = buffer.Mapped.Slice((int)pending.MainQueueOffsets[index], (int)copies[index].Size);
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{copies[index].Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
|
||||
if (pending.IsWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
|
||||
}
|
||||
|
||||
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The guest thread waited for the submission, so the GPU is done with the buffer.
|
||||
buffer.Dispose();
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPendingDownloadCurrent(PendingDownload pending)
|
||||
{
|
||||
var total = 0UL;
|
||||
|
|
@ -2004,7 +1686,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
|
||||
_device, _scheduler, _unifiedBuffers ? GpuBufferUsage.Unified : GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin, allowSlab: true), overlap.Begin, overlap.End - overlap.Begin);
|
||||
foreach (var oldId in overlapping)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -103,16 +103,6 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
private void DrawIndirectArguments(ulong packetAddress, uint opcode, ulong argumentsAddress, bool indexed)
|
||||
{
|
||||
if (!indexed && _host.ResolvesNonIndexedIndirectDrawOnGpu)
|
||||
{
|
||||
// The host reads the four arguments on the GPU, so a GPU-written argument buffer
|
||||
// costs no wait for the GPU here; it reads them itself when the draw needs counts.
|
||||
_deferredInstanceCountAddress = argumentsAddress + 4;
|
||||
_host.DrawAuto(SubmitId, new DrawAutoArguments(packetAddress, opcode, 1, 1, 0, 0,
|
||||
DrawOffsetSource.IndirectArguments, argumentsAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indexed)
|
||||
{
|
||||
var vertexCount = ReadDword(argumentsAddress);
|
||||
|
|
|
|||
|
|
@ -435,21 +435,15 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
|
||||
}
|
||||
|
||||
// Until real sample queries are implemented, conservatively report visibility.
|
||||
// A one-sample delta can disappear when the guest scales or reduces the counts.
|
||||
const ulong visibleCounterStep = 0x2FFFFFF;
|
||||
// Publish visible occlusion results for each depth block.
|
||||
const ulong readyBit = 1UL << 63;
|
||||
var result = readyBit | SyntheticOcclusionCounter;
|
||||
for (var depthBlock = 0u; depthBlock < 16u; depthBlock++)
|
||||
{
|
||||
// Query reducers can read a GPU-cached copy of this range. Use the same
|
||||
// ordered transfer path as clears instead of changing only CPU backing.
|
||||
var destination = eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong));
|
||||
_host.FillBuffer(destination, sizeof(uint), (uint)result, false);
|
||||
_host.FillBuffer(destination + sizeof(uint), sizeof(uint), (uint)(result >> 32), false);
|
||||
WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result);
|
||||
}
|
||||
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1);
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ public readonly record struct DrawAutoArguments(
|
|||
uint InstanceCount,
|
||||
uint FirstVertex,
|
||||
uint FirstInstance,
|
||||
DrawOffsetSource OffsetSource,
|
||||
// Nonzero when the counts are still in guest memory at this address, laid out as
|
||||
// (vertexCount, instanceCount, firstVertex, firstInstance); the other counts are then 1/0.
|
||||
ulong IndirectArgumentsAddress = 0);
|
||||
DrawOffsetSource OffsetSource);
|
||||
|
||||
public enum EndOfPipeWriteKind
|
||||
{
|
||||
|
|
@ -139,9 +136,6 @@ public interface ICommandStreamHost
|
|||
// guest memory (DrawIndexedArguments.IndirectArgumentsAddress).
|
||||
bool ResolvesIndirectDrawOnGpu => false;
|
||||
|
||||
// The host also draws non-indexed indirect draws from their arguments in guest memory.
|
||||
bool ResolvesNonIndexedIndirectDrawOnGpu => false;
|
||||
|
||||
// Called when a queue reset packet clears the processor.
|
||||
void OnQueueReset(int queueId);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,6 @@ public sealed unsafe partial class CachedImage
|
|||
{
|
||||
private const AccessFlags WriteAccess = AccessFlags.TransferWriteBit | AccessFlags.ShaderWriteBit | AccessFlags.MemoryWriteBit;
|
||||
private const AccessFlags TransferAccess = AccessFlags.TransferReadBit | AccessFlags.TransferWriteBit;
|
||||
private const AccessFlags AnyWriteAccess = WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit;
|
||||
|
||||
// Reads after reads in one layout need no barrier: the state keeps every reader, so the next
|
||||
// writer still waits for all of them. (A sampled depth attachment followed by a draw that only
|
||||
// tests depth would otherwise end the rendering scope.)
|
||||
private static bool IsReadAfterRead(ImageAccessState state, ImageLayout layout, AccessFlags access) =>
|
||||
state.Layout == layout && state.Layout != ImageLayout.Undefined && access != 0 && state.Access != 0 &&
|
||||
(state.Access & AnyWriteAccess) == 0 && (access & AnyWriteAccess) == 0;
|
||||
private const AccessFlags MemoryAccess = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit;
|
||||
private const ImageLayout ReadyLayout = ImageLayout.General;
|
||||
private const AccessFlags ReadyAccess = AccessFlags.ShaderReadBit | AccessFlags.TransferReadBit;
|
||||
|
|
@ -89,11 +81,7 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
var state = states[index];
|
||||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
{
|
||||
states[index] = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
|
||||
}
|
||||
else if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
{
|
||||
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
sourceStages |= state.Stage;
|
||||
|
|
@ -116,12 +104,6 @@ public sealed unsafe partial class CachedImage
|
|||
return (barriers, sourceStages);
|
||||
}
|
||||
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
{
|
||||
Backing.State = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
|
||||
return (barriers, sourceStages);
|
||||
}
|
||||
|
||||
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
sourceStages |= state.Stage;
|
||||
}
|
||||
|
|
@ -130,39 +112,6 @@ public sealed unsafe partial class CachedImage
|
|||
return (barriers, sourceStages);
|
||||
}
|
||||
|
||||
// The layout every subresource in the range is in, or null when they differ.
|
||||
public ImageLayout? UniformLayout(SubresourceRange range)
|
||||
{
|
||||
var states = Backing.SubresourceStates;
|
||||
if (states == null)
|
||||
{
|
||||
return Backing.State.Layout;
|
||||
}
|
||||
|
||||
var layers = Description.Resources.Layers;
|
||||
if (Description.IsVolume)
|
||||
{
|
||||
range = range with { BaseLayer = 0, LayerCount = 1 };
|
||||
}
|
||||
|
||||
ImageLayout? layout = null;
|
||||
for (var level = range.BaseLevel; level < range.BaseLevel + range.LevelCount; level++)
|
||||
{
|
||||
for (var layer = range.BaseLayer; layer < range.BaseLayer + range.LayerCount; layer++)
|
||||
{
|
||||
var index = (int)(level * layers + layer);
|
||||
if (index >= states.Count || (layout is { } known && known != states[index].Layout))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
layout = states[index].Layout;
|
||||
}
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
public void Transition(ImageLayout layout, AccessFlags access, SubresourceRange? range, CommandBuffer command)
|
||||
{
|
||||
PipelineStageFlags stage = 0;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public struct TileTransferArguments
|
|||
// Runs the tiling, detiling, depth conversion and channel swap compute passes.
|
||||
public sealed unsafe class GpuTiler : IDisposable
|
||||
{
|
||||
private const uint KindCount = 9;
|
||||
private const uint KindCount = 10;
|
||||
private const uint ElementSizeCount = 5;
|
||||
private const uint DirectionCount = 2;
|
||||
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,271 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// Surface metadata (HTile, DCC, CMask, FMask) keyed by its guest address.
|
||||
public sealed partial class GuestImageCache
|
||||
{
|
||||
private unsafe void MaterializeDccClear(
|
||||
ResourceSlotIdentifier imageIdentifier,
|
||||
in ImageRequest request,
|
||||
uint metadataBaseLayer)
|
||||
{
|
||||
if (request.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var description = request.Description;
|
||||
var range = description.Metadata.Range;
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
var image = _slots.TryGet(imageIdentifier);
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
image.Description.Metadata = description.Metadata;
|
||||
if (range.Size == 0 || description.Resources.Levels != 1 ||
|
||||
image.Description.Resources.Levels != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_surfaceMetadata.Remove(range.Address);
|
||||
}
|
||||
|
||||
var layers = description.TransferLayers;
|
||||
const ulong metadataBlockSize = 0x1000;
|
||||
if (!ImageDescription.IsValidRange(range) ||
|
||||
range.Address % metadataBlockSize != 0 ||
|
||||
layers == 0 ||
|
||||
range.Size % layers != 0 ||
|
||||
(range.Size / layers) % metadataBlockSize != 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"DCC slices must contain aligned 4 KiB blocks: address=0x{range.Address:X16} size=0x{range.Size:X} layers={layers}.");
|
||||
}
|
||||
|
||||
var view = request.View;
|
||||
var volume = description.IsVolume && view.Type == ImageViewType.Type3D;
|
||||
var first = volume ? 0u : metadataBaseLayer;
|
||||
var imageFirst = volume ? 0u : view.BaseLayer;
|
||||
var count = volume ? description.Extent.Depth : view.LayerCount;
|
||||
if (first >= layers || count == 0 || count > layers - first)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC view exceeds its metadata slices: first={first} count={count} layers={layers}.");
|
||||
}
|
||||
|
||||
var sliceSize = range.Size / layers;
|
||||
var synchronizedAddress = range.Address + sliceSize * first;
|
||||
var synchronizedSize = sliceSize * count;
|
||||
|
||||
if (_bufferCache.HasGpuDirtyBytes(synchronizedAddress, synchronizedSize))
|
||||
{
|
||||
_bufferCache.ReadMemory(synchronizedAddress, synchronizedSize, isWrite: false);
|
||||
}
|
||||
|
||||
const int metadataScanChunkSize = 64 * 1024;
|
||||
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent(
|
||||
checked((int)Math.Min(sliceSize, (ulong)metadataScanChunkSize)));
|
||||
try
|
||||
{
|
||||
Span<byte> firstByte = stackalloc byte[1];
|
||||
for (uint slice = 0; slice < count; slice++)
|
||||
{
|
||||
var address = range.Address + sliceSize * (first + slice);
|
||||
if (!_backing.TryReadBacking(address, firstByte))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"Could not read DCC metadata key: address=0x{address:X16}.");
|
||||
}
|
||||
|
||||
var code = firstByte[0];
|
||||
if (!PackedClearValue.TryDecodeDccColor(
|
||||
request.View.Format,
|
||||
code * 0x01010101u,
|
||||
description.Metadata,
|
||||
allowClearToRegister: request.Role == ImageRole.ColorTarget,
|
||||
out var clear))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var uniform = true;
|
||||
for (ulong position = 0; position < sliceSize && uniform;)
|
||||
{
|
||||
var length = checked((int)Math.Min(
|
||||
sliceSize - position,
|
||||
(ulong)Math.Min(bytes.Length, metadataScanChunkSize)));
|
||||
var chunk = bytes.AsSpan(0, length);
|
||||
if (!_backing.TryReadBacking(address + position, chunk))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"Could not read DCC metadata backing: address=0x{address + position:X16} size=0x{length:X}.");
|
||||
}
|
||||
|
||||
for (var index = 0; index < chunk.Length; index++)
|
||||
{
|
||||
if (chunk[index] != code)
|
||||
{
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
position += (ulong)length;
|
||||
}
|
||||
|
||||
if (!uniform)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
if (_slots.TryGet(imageIdentifier) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearDccImage(
|
||||
imageIdentifier,
|
||||
request,
|
||||
new SubresourceRange(view.BaseLevel, view.LevelCount, imageFirst + slice, 1),
|
||||
clear);
|
||||
}
|
||||
|
||||
if (request.Role != ImageRole.DisplaySurface)
|
||||
{
|
||||
_bufferCache.FillBuffer(address, sliceSize, uint.MaxValue, isGds: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ClearDccImage(
|
||||
ResourceSlotIdentifier imageIdentifier,
|
||||
in ImageRequest request,
|
||||
in SubresourceRange range,
|
||||
in ClearColorValue clear)
|
||||
{
|
||||
var image = _slots[imageIdentifier];
|
||||
ref readonly var description = ref image.Description;
|
||||
if (range.BaseLevel >= description.Resources.Levels || range.LevelCount == 0 ||
|
||||
range.LevelCount > description.Resources.Levels - range.BaseLevel)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear mip range is invalid: base={range.BaseLevel} count={range.LevelCount} levels={description.Resources.Levels}.");
|
||||
}
|
||||
|
||||
var layers = description.IsVolume
|
||||
? Math.Max(description.Extent.Depth >> (int)range.BaseLevel, 1)
|
||||
: image.Backing.Layers;
|
||||
if (range.LayerCount == 0 || range.BaseLayer >= layers || range.LayerCount > layers - range.BaseLayer)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear layer range is invalid: base={range.BaseLayer} count={range.LayerCount} layers={layers}.");
|
||||
}
|
||||
|
||||
var fullImage = range.BaseLevel == 0 && range.LevelCount == description.Resources.Levels &&
|
||||
range.BaseLayer == 0 && range.LayerCount == layers;
|
||||
WatchImage(imageIdentifier);
|
||||
if (!fullImage && (image.IsBufferModified || image.IsCpuDirty))
|
||||
{
|
||||
PopulateFromGuest(imageIdentifier, RefreshRequest(image), "before-dcc-clear");
|
||||
if (image.Description.Samples == 1 && (image.IsBufferModified || image.IsCpuDirty))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The DCC clear left guest ownership in place: address=0x{description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty}.");
|
||||
}
|
||||
}
|
||||
|
||||
var scheduled = _scheduler.Current;
|
||||
if (scheduled.IsInvalid)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("A DCC clear needs a command buffer that is recording.");
|
||||
}
|
||||
|
||||
scheduled.EndRendering();
|
||||
var command = new CommandBuffer(scheduled.Handle);
|
||||
var needsAttachmentClear = request.View.Format != image.Backing.Format ||
|
||||
(description.IsVolume && !fullImage);
|
||||
if (needsAttachmentClear)
|
||||
{
|
||||
var viewDescription = new ImageViewDescription(
|
||||
request.View.Format,
|
||||
range.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray,
|
||||
ImageAspectFlags.ColorBit,
|
||||
range.BaseLevel,
|
||||
range.LevelCount,
|
||||
range.BaseLayer,
|
||||
range.LayerCount,
|
||||
default,
|
||||
ImageUsageFlags.ColorAttachmentBit);
|
||||
var view = image.GetOrCreateView(viewDescription);
|
||||
image.Transition(
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
AccessFlags.ColorAttachmentWriteBit,
|
||||
range,
|
||||
command);
|
||||
var attachment = new RenderingAttachmentInfo
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = view,
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = AttachmentLoadOp.Clear,
|
||||
StoreOp = AttachmentStoreOp.Store,
|
||||
ClearValue = new ClearValue { Color = clear },
|
||||
};
|
||||
var rendering = new RenderingInfo
|
||||
{
|
||||
SType = StructureType.RenderingInfo,
|
||||
RenderArea = new Rect2D(
|
||||
new Offset2D(0, 0),
|
||||
new Extent2D(
|
||||
Math.Max(description.Extent.Width >> (int)range.BaseLevel, 1),
|
||||
Math.Max(description.Extent.Height >> (int)range.BaseLevel, 1))),
|
||||
LayerCount = range.LayerCount,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &attachment,
|
||||
};
|
||||
_device.Vk.CmdBeginRendering(command, &rendering);
|
||||
_device.Vk.CmdEndRendering(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
|
||||
var nativeRange = new ImageSubresourceRange(
|
||||
ImageAspectFlags.ColorBit,
|
||||
range.BaseLevel,
|
||||
range.LevelCount,
|
||||
description.IsVolume ? 0u : range.BaseLayer,
|
||||
description.IsVolume ? 1u : range.LayerCount);
|
||||
var nativeClear = clear;
|
||||
_device.Vk.CmdClearColorImage(
|
||||
command,
|
||||
image.Backing.Handle,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
&nativeClear,
|
||||
1,
|
||||
&nativeRange);
|
||||
}
|
||||
|
||||
TakeGpuOwnership(image);
|
||||
}
|
||||
|
||||
public bool IsMetadata(ulong address)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
|
|
@ -57,7 +315,6 @@ public sealed partial class GuestImageCache
|
|||
return true;
|
||||
}
|
||||
|
||||
// True when registered DCC absorbed the fill and the guest dispatch can be skipped.
|
||||
public bool TryAbsorbDccFill(ulong address, ulong size, uint fillValue)
|
||||
{
|
||||
if (!IsValidRange(address, size))
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public sealed partial class GuestImageCache
|
|||
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
|
||||
{
|
||||
ref readonly var info = ref image.Description;
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -703,39 +703,11 @@ public sealed unsafe partial class GuestImageCache
|
|||
}
|
||||
}
|
||||
|
||||
// The mip layout can end before the tile plan does (Silent Hill: a 128x128 slice planned at
|
||||
// the exact end of the mip chain aborted the process). Cover the whole plan when the buffer
|
||||
// holds it; otherwise keep the guest bytes instead of copying a partial image.
|
||||
if (!plan.Depth && plan.Color.Tiled)
|
||||
{
|
||||
var tiledEnd = TiledEndOf(plan.Color.Tiles);
|
||||
if (tiledEnd > copySize)
|
||||
{
|
||||
if (tiledEnd > available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
copySize = tiledEnd;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
|
||||
image.MarkBufferHoldsGpuContents();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong TiledEndOf(List<TileTransfer> tiles)
|
||||
{
|
||||
var end = 0UL;
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
end = Math.Max(end, tile.TiledOffset + tile.TiledSize);
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
// Publishes a GPU-owned image to guest memory after its tick completes; false when it cannot.
|
||||
private bool TryDownloadToGuest(ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,28 +123,48 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
return GetNullImage(request);
|
||||
}
|
||||
|
||||
var found = LookUpImage(ref request, exactFormat);
|
||||
var metadataBaseLayer = request.View.BaseLayer;
|
||||
var result = LookUpImage(ref request, exactFormat);
|
||||
|
||||
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
|
||||
{
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
ulong metadataAddress;
|
||||
ulong sliceSize;
|
||||
using (var held = _lock.Hold())
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
var image = _slots[found];
|
||||
if (image.Description.Metadata.Kind == MetadataKind.None)
|
||||
var image = _slots[result];
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0 &&
|
||||
image.Description.Metadata.Kind == MetadataKind.None)
|
||||
{
|
||||
image.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
image.Description.Metadata.Range = request.Description.Metadata.Range;
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
}
|
||||
|
||||
ref readonly var description = ref image.Description;
|
||||
metadataAddress = description.Metadata.Range.Address;
|
||||
sliceSize = description.DccSliceSize;
|
||||
}
|
||||
|
||||
ref readonly var description = ref _slots[found].Description;
|
||||
if (description.DccSliceSize is var sliceSize and not 0)
|
||||
if (sliceSize != 0)
|
||||
{
|
||||
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
MaterializeDccClear(result, request, metadataBaseLayer);
|
||||
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
var image = _slots[result];
|
||||
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
|
||||
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
|
||||
if (!nativeCurrent)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
|
||||
|
|
@ -231,17 +251,6 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
}
|
||||
|
||||
var image = _slots[result];
|
||||
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
|
||||
{
|
||||
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
|
||||
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
|
||||
if (!nativeCurrent)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (viewMip >= 0)
|
||||
{
|
||||
request.View = request.View with { BaseLevel = (uint)viewMip };
|
||||
|
|
@ -353,8 +362,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
image.MarkGpuModified();
|
||||
image.Uses.RenderTarget = true;
|
||||
RefreshFromGuest(imageIdentifier, request);
|
||||
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
var address = request.Description.Metadata.Range.Address;
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ public struct MetadataDescription
|
|||
public GuestSpan Range;
|
||||
public MetadataKind Kind;
|
||||
public uint Control;
|
||||
public uint DccClearWord;
|
||||
public DisplayCompression Compression;
|
||||
public bool StencilCompressed;
|
||||
public bool DccClearRegisterValid;
|
||||
public bool DccAlphaMsb;
|
||||
}
|
||||
|
||||
public readonly record struct SubresourceCount(uint Levels, uint Layers) : IComparable<SubresourceCount>
|
||||
|
|
@ -366,7 +369,8 @@ public struct ImageDescription
|
|||
{
|
||||
case MetadataKind.None:
|
||||
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
|
||||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
|
||||
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
|
||||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
|
||||
}
|
||||
|
|
@ -378,6 +382,11 @@ public struct ImageDescription
|
|||
throw SubmissionScheduler.Fatal($"The HTile metadata is invalid: address=0x{Metadata.Range.Address:X16} size=0x{Metadata.Range.Size:X16}.");
|
||||
}
|
||||
|
||||
if (Metadata.DccClearWord != 0 || Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("HTile metadata carries DCC clear state.");
|
||||
}
|
||||
|
||||
break;
|
||||
case MetadataKind.Dcc:
|
||||
if (Metadata.Range.Address == 0 || Metadata.Range.Address >= TrackerLayout.SpaceBytes ||
|
||||
|
|
@ -507,6 +516,87 @@ public static class DisplayFormatRule
|
|||
// Decoders for the packed 32-bit clear values guest compute fills carry.
|
||||
public static class PackedClearValue
|
||||
{
|
||||
public static bool SupportsDccFixedColor(Format format) => DccComponentCount(format) != 0;
|
||||
|
||||
public static bool TryDecodeDccColor(
|
||||
Format format,
|
||||
uint metadataFill,
|
||||
in MetadataDescription metadata,
|
||||
bool allowClearToRegister,
|
||||
out ClearColorValue clear)
|
||||
{
|
||||
clear = default;
|
||||
var code = (byte)metadataFill;
|
||||
if (metadataFill != code * 0x01010101u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code == 0x20)
|
||||
{
|
||||
return allowClearToRegister && metadata.DccClearRegisterValid &&
|
||||
TryDecodeColor(format, metadata.DccClearWord, out clear);
|
||||
}
|
||||
|
||||
if (code == 0x00)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var components = DccComponentCount(format);
|
||||
if (components == 0 || code is not (0x40 or 0x80 or 0xc0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rgb = (code & 0x80) != 0 ? 1.0f : 0.0f;
|
||||
var alpha = (code & 0x40) != 0 ? 1.0f : 0.0f;
|
||||
Span<float> channels = stackalloc float[4] { rgb, rgb, rgb, alpha };
|
||||
if (!metadata.DccAlphaMsb)
|
||||
{
|
||||
(channels[0], channels[3]) = (channels[3], channels[0]);
|
||||
}
|
||||
|
||||
if (components == 1)
|
||||
{
|
||||
channels[0] = channels[3];
|
||||
}
|
||||
else if (components == 2)
|
||||
{
|
||||
channels[1] = channels[3];
|
||||
}
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case Format.B8G8R8A8Unorm:
|
||||
case Format.B8G8R8A8Srgb:
|
||||
case Format.A2R10G10B10UnormPack32:
|
||||
case Format.A1R5G5B5UnormPack16:
|
||||
case Format.R5G6B5UnormPack16:
|
||||
(channels[0], channels[2]) = (channels[2], channels[0]);
|
||||
break;
|
||||
case Format.R4G4B4A4UnormPack16:
|
||||
(channels[0], channels[3]) = (channels[3], channels[0]);
|
||||
(channels[1], channels[2]) = (channels[2], channels[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
clear = new ClearColorValue(channels[0], channels[1], channels[2], channels[3]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint DccComponentCount(Format format) => format switch
|
||||
{
|
||||
Format.R8Unorm or Format.R16Unorm or Format.R16Sfloat or Format.R32Sfloat => 1,
|
||||
Format.R8G8Unorm or Format.R16G16Unorm or Format.R16G16Sfloat or Format.R32G32Sfloat => 2,
|
||||
Format.R5G6B5UnormPack16 or Format.B10G11R11UfloatPack32 => 3,
|
||||
Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
|
||||
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.A1R5G5B5UnormPack16 or
|
||||
Format.R4G4B4A4UnormPack16 or Format.R16G16B16A16Unorm or Format.R16G16B16A16Sfloat or
|
||||
Format.R32G32B32A32Sfloat => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static bool TryDecodeColor(Format format, uint packed, out ClearColorValue clear)
|
||||
{
|
||||
static float Unorm8(uint value) => (value & 0xff) / 255.0f;
|
||||
|
|
|
|||
|
|
@ -43,24 +43,38 @@ public static partial class ImageRequestBuilders
|
|||
|
||||
public static uint SampleCount(uint encodedLog2) => encodedLog2 <= 3 ? 1u << (int)encodedLog2 : 0;
|
||||
|
||||
private static bool DccAlphaOnMsb(in ColorTargetWords words)
|
||||
{
|
||||
switch (words.Layout)
|
||||
{
|
||||
case ChannelLayout.Bits10_10_10_2:
|
||||
case ChannelLayout.Bits10_10_10_2Float:
|
||||
case ChannelLayout.Bits5_5_5_1:
|
||||
return true;
|
||||
case ChannelLayout.Bits2_10_10_10:
|
||||
case ChannelLayout.Bits1_5_5_5:
|
||||
return false;
|
||||
}
|
||||
|
||||
var components = GuestPixelFormats.ResolveRenderTargetEncoding(words.Layout, words.NumberType).Components;
|
||||
if (components == 1)
|
||||
{
|
||||
return words.Order != ChannelOrder.Standard;
|
||||
}
|
||||
|
||||
return components == 3 || words.Order is ChannelOrder.Standard or ChannelOrder.Alternate;
|
||||
}
|
||||
|
||||
// DCC clears use the target's packed clear word; the fixed-clear set is a format allow-list.
|
||||
private static (bool Supported, bool FixedSupported, ClearColorValue Value) DccClearInfo(Format format, bool hasDcc, uint packedClear)
|
||||
{
|
||||
ClearColorValue value = default;
|
||||
var supported = hasDcc && PackedClearValue.TryDecodeColor(format, packedClear, out value);
|
||||
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
|
||||
var fixedSupported = hasDcc && PackedClearValue.SupportsDccFixedColor(format);
|
||||
return (supported, fixedSupported, supported ? value : default);
|
||||
}
|
||||
|
||||
public static bool SupportsDccFixedClear(Format format) => format switch
|
||||
{
|
||||
Format.R8Unorm or Format.R8G8Unorm or Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
|
||||
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.R5G6B5UnormPack16 or Format.A1R5G5B5UnormPack16 or
|
||||
Format.R4G4B4A4UnormPack16 or Format.R16Unorm or Format.R16G16Unorm or Format.R16G16B16A16Unorm or Format.R16Sfloat or
|
||||
Format.R16G16Sfloat or Format.R16G16B16A16Sfloat or Format.R32Sfloat or Format.R32G32Sfloat or Format.R32G32B32A32Sfloat or
|
||||
Format.B10G11R11UfloatPack32 => true,
|
||||
_ => false,
|
||||
};
|
||||
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
|
||||
|
||||
// Builds the request for a bound color target. Null when the slot carries no target.
|
||||
public static ColorTargetResolution? ColorTarget(in ColorTargetWords words, uint targetMask, uint drawLayerOffset, bool ignoreTargetMask)
|
||||
|
|
@ -324,8 +338,14 @@ public static partial class ImageRequestBuilders
|
|||
if (hasDcc)
|
||||
{
|
||||
// DCC lives in its own allocation; the address lets the cache match fills seen before the target.
|
||||
_ = TileGeometry.TryGetDccSize(
|
||||
width, height, volume ? depth : view.ImageLayers, bytesPerElement, levels, tileMode,
|
||||
out var metadataSize, words.FragmentsLog2);
|
||||
description.Metadata.Kind = MetadataKind.Dcc;
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, metadataSize.Size);
|
||||
description.Metadata.DccClearWord = words.ClearWord0;
|
||||
description.Metadata.DccClearRegisterValid = true;
|
||||
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
|
||||
}
|
||||
|
||||
for (var level = 0; level < levels; level++)
|
||||
|
|
|
|||
|
|
@ -300,12 +300,15 @@ public static partial class ImageRequestBuilders
|
|||
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
|
||||
description.Samples = samples;
|
||||
description.TileMode = tile;
|
||||
var metadataAddress = descriptor.MetadataAddress << 8;
|
||||
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
|
||||
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
|
||||
if (!shape.R128 && words.Length >= 8 && descriptor.MetadataCompress &&
|
||||
tile != GuestTileMode.Depth && !description.IsDepth)
|
||||
{
|
||||
_ = TileGeometry.TryGetDccSize(
|
||||
width, height, volume ? depth : imageLayers, description.BytesPerBlock, levels, tile,
|
||||
out var metadataSize, (uint)System.Numerics.BitOperations.TrailingZeroCount(samples));
|
||||
description.Metadata.Kind = MetadataKind.Dcc;
|
||||
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
|
||||
description.Metadata.Range = new GuestSpan(descriptor.MetadataAddress << 8, metadataSize.Size);
|
||||
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
|
||||
}
|
||||
|
||||
if (samples > 1)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ public readonly struct TextureDescriptorWords
|
|||
|
||||
public bool MetadataCompress => ((Fields[6] >> 21) & 0x1) == 1;
|
||||
|
||||
public bool DccAlphaMsb => ((Fields[6] >> 22) & 0x1) == 1;
|
||||
|
||||
public ulong MetadataAddress => ((Fields[6] >> 24) & 0xFF) | ((ulong)Fields[7] << 8);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -452,7 +452,9 @@ public sealed class TextureTransferLayout
|
|||
transfer.Width = Math.Max((region.ImageExtent.Width + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Height = Math.Max((logicalHeight + texture.TexelHeight - 1) / texture.TexelHeight, 1);
|
||||
transfer.Depth = 1;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB ? region.ImageSubresource.BaseArrayLayer : 0;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
|
||||
? region.ImageSubresource.BaseArrayLayer
|
||||
: 0;
|
||||
transfer.Pitch = Math.Max((pitch + texture.TexelWidth - 1) / texture.TexelWidth, 1);
|
||||
transfer.Tail = tail;
|
||||
transfer.TailX = tail ? mip.TailX : 0;
|
||||
|
|
|
|||
|
|
@ -230,6 +230,75 @@ public static partial class TileGeometry
|
|||
}
|
||||
}
|
||||
|
||||
private static uint Gen5RenderTargetOffset(uint x, uint y, uint bytesPerElement)
|
||||
{
|
||||
uint offset = 0;
|
||||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
offset ^= (y << 2) & 0x0008;
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 3) & 0x00a0;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 6) & 0x1000;
|
||||
offset ^= (y << 7) & 0x4000;
|
||||
offset ^= x & 0x0007;
|
||||
offset ^= (x << 3) & 0x0040;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 2:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 8) & 0x5000;
|
||||
offset ^= (x << 1) & 0x000e;
|
||||
offset ^= (x << 4) & 0x0480;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 7) & 0x2000;
|
||||
offset ^= (x << 8) & 0x8000;
|
||||
return offset;
|
||||
case 4:
|
||||
offset ^= (y << 4) & 0x0070;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 9) & 0x1000;
|
||||
offset ^= (y << 8) & 0x4000;
|
||||
offset ^= (x << 2) & 0x000c;
|
||||
offset ^= (x << 5) & 0x0380;
|
||||
offset ^= (x << 4) & 0x0400;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 9) & 0xa000;
|
||||
return offset;
|
||||
case 8:
|
||||
offset ^= (y << 4) & 0x0010;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 5) & 0x0f00;
|
||||
offset ^= (y << 10) & 0x5000;
|
||||
offset ^= (x << 3) & 0x0008;
|
||||
offset ^= (x << 4) & 0x0460;
|
||||
offset ^= (x << 5) & 0x0300;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 10) & 0x2000;
|
||||
offset ^= (x << 9) & 0x8000;
|
||||
return offset;
|
||||
case 16:
|
||||
offset ^= (x << 4) & 0x0410;
|
||||
offset ^= (x << 5) & 0x0340;
|
||||
offset ^= (x << 6) & 0x0800;
|
||||
offset ^= (x << 11) & 0xa000;
|
||||
offset ^= (y << 5) & 0x0f20;
|
||||
offset ^= (y << 6) & 0x0080;
|
||||
offset ^= (y << 10) & 0x1000;
|
||||
offset ^= (y << 11) & 0x4000;
|
||||
return offset;
|
||||
default:
|
||||
throw SubmissionScheduler.Fatal($"The Gen5 render-target tile block does not support this element size: bytes={bytesPerElement}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Depth64KB8X(uint x) =>
|
||||
(x & 0x0001) ^ ((x << 1) & 0x0004) ^ ((x << 2) & 0x0010) ^ ((x << 3) & 0x0040) ^ ((x << 5) & 0x0300) ^
|
||||
((x << 4) & 0x0400) ^ ((x << 6) & 0x0800) ^ ((x << 7) & 0x2000) ^ ((x << 8) & 0x8000);
|
||||
|
|
@ -319,6 +388,14 @@ public static partial class TileGeometry
|
|||
|
||||
offset = RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.RenderTarget64KBGen5:
|
||||
if (bytes > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = Gen5RenderTargetOffset(x, y, bytes);
|
||||
break;
|
||||
case TileBlockKind.Depth64KB:
|
||||
switch (bytes)
|
||||
{
|
||||
|
|
@ -364,17 +441,19 @@ public static partial class TileGeometry
|
|||
|
||||
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
|
||||
}
|
||||
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
|
||||
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
|
||||
{
|
||||
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
|
||||
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
|
||||
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
}
|
||||
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
|
||||
{
|
||||
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public enum TileBlockKind : uint
|
|||
Prt64KB3D,
|
||||
RenderTarget64KB,
|
||||
Depth64KB,
|
||||
RenderTarget64KBGen5,
|
||||
}
|
||||
|
||||
public readonly record struct TileBlockLayout(TileBlockKind Kind, uint BytesPerElement, uint BlockSize, uint BlockWidth, uint BlockHeight, uint BlockDepth)
|
||||
|
|
@ -109,6 +110,7 @@ public static partial class TileGeometry
|
|||
new(65536, Thick64KB, 16),
|
||||
new(65536, Thin64KB, 16),
|
||||
new(65536, Thin64KB, 8),
|
||||
new(65536, Thin64KB, 16),
|
||||
];
|
||||
|
||||
private static readonly Log2BlockDimensions[][] MsaaBlocks =
|
||||
|
|
@ -247,6 +249,7 @@ public static partial class TileGeometry
|
|||
case TileBlockKind.Standard64KB:
|
||||
case TileBlockKind.Prt64KB:
|
||||
case TileBlockKind.RenderTarget64KB:
|
||||
case TileBlockKind.RenderTarget64KBGen5:
|
||||
rule = new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -297,7 +300,7 @@ public static partial class TileGeometry
|
|||
kind = TileBlockKind.Depth64KB;
|
||||
break;
|
||||
case GuestTileMode.RenderTarget:
|
||||
kind = TileBlockKind.RenderTarget64KB;
|
||||
kind = TileBlockKind.RenderTarget64KBGen5;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -308,7 +311,7 @@ public static partial class TileGeometry
|
|||
return false;
|
||||
}
|
||||
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
|
||||
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -605,6 +608,39 @@ public static partial class TileGeometry
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetDccSize(
|
||||
uint width,
|
||||
uint height,
|
||||
uint slices,
|
||||
uint bytesPerElement,
|
||||
uint levels,
|
||||
GuestTileMode tile,
|
||||
out TileSizeAndAlignment totalSize,
|
||||
uint fragmentsLog2 = 0)
|
||||
{
|
||||
totalSize = default;
|
||||
if (width == 0 || height == 0 || slices == 0 || levels != 1 || fragmentsLog2 != 0 ||
|
||||
!BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16 ||
|
||||
tile is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var coverageBits = 20 - BitOperations.TrailingZeroCount(bytesPerElement);
|
||||
var blockWidth = 1u << ((coverageBits + 1) / 2);
|
||||
var blockHeight = 1u << (coverageBits / 2);
|
||||
var blocksX = ((ulong)width + blockWidth - 1) / blockWidth;
|
||||
var blocksY = ((ulong)height + blockHeight - 1) / blockHeight;
|
||||
var blocks = blocksX * blocksY;
|
||||
if (blocks == 0 || blocks > uint.MaxValue / 4096u / slices)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
totalSize = new TileSizeAndAlignment((uint)(blocks * slices * 4096u), 4096);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetRenderTargetMipLayout(uint width, uint height, uint pitch, uint bytesPerElement, uint levels, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
|
||||
{
|
||||
totalSize = default;
|
||||
|
|
|
|||
|
|
@ -166,8 +166,6 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
|
||||
// Metal has no 64-bit workgroup atomics here; keep the non-atomic 32-bit pair.
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => false;
|
||||
// The Metal backend runs no f16 conversion probe, so it keeps the exact emulation.
|
||||
bool IShaderPipelineHost.NativeHalfConversionExact => false;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => new(MaxDimension, MaxDimension, MaxDimension, MaxDimension);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,19 +41,12 @@ internal interface IShaderPipelineHost
|
|||
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
|
||||
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
|
||||
bool SharedInt64AtomicsEnabled { get; }
|
||||
bool ShaderSignedZeroInfNanPreserveFloat32Supported => false;
|
||||
bool ExecGuardElisionEnabled => true;
|
||||
ShaderPrewarmList? ShaderPrewarm => null;
|
||||
bool PerVertexPixelInputsSupported => true;
|
||||
bool ClipDistanceEnabled => false;
|
||||
|
||||
// True when this device's GLSL UnpackHalf2x16 / PackHalf2x16 were measured bit-exact
|
||||
// against the translator's own f16 conversion. False for every host that did not measure it.
|
||||
bool NativeHalfConversionExact => false;
|
||||
|
||||
// True only when the host measured that a storage-buffer read past its descriptor range
|
||||
// returns zero on this device; the translator then stops bounds-checking every guest word.
|
||||
bool ZeroOutOfBoundsBufferReads => false;
|
||||
|
||||
RenderHostLimits Limits { get; }
|
||||
|
||||
// The sample counts a pipeline without attachments can rasterize at.
|
||||
|
|
|
|||
|
|
@ -210,22 +210,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
// One past the highest parameter location the pixel program reads, resolved as its translator does.
|
||||
// The attributes a pixel program interpolates, ascending; scanned once per decoded program
|
||||
// instead of on every draw.
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Gen5ShaderProgram, uint[]> _interpolatedAttributes = new();
|
||||
|
||||
private static uint[] InterpolatedAttributes(Gen5ShaderProgram program) =>
|
||||
_interpolatedAttributes.GetValue(program, static program => program.Instructions
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = pixelProgram.Instructions
|
||||
.Select(static instruction => instruction.Control)
|
||||
.OfType<Gen5InterpolationControl>()
|
||||
.Select(static control => control.Attribute)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray());
|
||||
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = InterpolatedAttributes(pixelProgram);
|
||||
.ToArray();
|
||||
if (attributes.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
|
|
@ -244,8 +237,16 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
|
||||
{
|
||||
var attributes = InterpolatedAttributes(program);
|
||||
return attributes.Length == 0 ? 0u : attributes[^1] + 1;
|
||||
var maxAttribute = -1;
|
||||
foreach (var instruction in program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5InterpolationControl interpolation)
|
||||
{
|
||||
maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return (uint)(maxAttribute + 1);
|
||||
}
|
||||
|
||||
// The bound colour slots in order; each output mode names the kind the pixel program exports.
|
||||
|
|
|
|||
|
|
@ -593,8 +593,7 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
var nativeHalfConversion = _host.NativeHalfConversionExact;
|
||||
var zeroOutOfBoundsReads = _host.ZeroOutOfBoundsBufferReads;
|
||||
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -607,8 +606,7 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -636,8 +634,7 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -649,7 +646,7 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -666,16 +663,15 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
|
||||
bool nativeHalfConversion, bool zeroOutOfBoundsReads) =>
|
||||
bool signedZeroInfNanPreserve) =>
|
||||
new(plan, resources, layout)
|
||||
{
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
WaveSize = info.WaveSize,
|
||||
EnableExecGuardElision = info.WaveSize != 64 || execGuardElision,
|
||||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
ComputeSystemRegisters = systemRegisters,
|
||||
LocalDataShareDwords = info.LocalDataShareDwords,
|
||||
LocalSizeX = Math.Max(info.ThreadsX, 1),
|
||||
|
|
@ -684,8 +680,8 @@ internal sealed class ShaderProgramCache
|
|||
};
|
||||
|
||||
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool nativeHalfConversion, bool zeroOutOfBoundsReads,
|
||||
out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
|
||||
out BindingLayout? layout, out string error)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -702,7 +698,7 @@ internal sealed class ShaderProgramCache
|
|||
layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor,
|
||||
record.Info.DispatchThreadDimensions);
|
||||
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
|
||||
sharedInt64Atomics, execGuardElision, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -360,24 +360,7 @@ public static class VertexInputResolver
|
|||
for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++)
|
||||
{
|
||||
var buffer = buffers[bufferIndex];
|
||||
uint minimumFetchBytes = 0;
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
var descriptor = resource.Descriptor;
|
||||
// OOB_SELECT=2 only tests for an unbound (zero-record) descriptor.
|
||||
// A constant stream still reads the whole formatted element, even
|
||||
// when NUM_RECORDS is smaller than that element's byte width.
|
||||
if (descriptor.Stride == 0 && descriptor.OutOfBounds == 2 && descriptor.RecordCount != 0 &&
|
||||
VertexAttributeFormats.TryResolve(in descriptor, 4, out var format, out _))
|
||||
{
|
||||
minimumFetchBytes = Math.Max(minimumFetchBytes, Images.ViewFormatRules.BlockBytes(format));
|
||||
}
|
||||
}
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0)
|
||||
{
|
||||
MinimumFetchBytes = minimumFetchBytes,
|
||||
};
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0);
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
|
|
|
|||
|
|
@ -29,13 +29,6 @@ public static class ColorTargetResolver
|
|||
public static ColorTargetResolution? Resolve(ContextRegisters context, uint slot, uint drawLayerOffset, bool ignoreTargetMask, out uint resolvedSlot)
|
||||
{
|
||||
resolvedSlot = slot == FirstBoundSlot ? FirstBound(context) : slot;
|
||||
// Disabling CB takes precedence over stale target addresses and masks.
|
||||
// Depth-only passes may leave the preceding color targets bound.
|
||||
if (context.ColorControl.Mode == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ImageRequestBuilders.ColorTarget(in context.ColorTargets[resolvedSlot], context.RenderTargetMaskForSlot(resolvedSlot), drawLayerOffset, ignoreTargetMask);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,20 +76,6 @@ public readonly record struct DepthStencilState(
|
|||
: ImageLayout.DepthAttachmentOptimal;
|
||||
}
|
||||
|
||||
// A sampled attachment needs the read-only layouts; a draw that writes needs the writable
|
||||
// one. A draw that neither samples nor writes keeps the read-only layout it is already in,
|
||||
// so sampling and depth-testing draws can alternate inside one rendering scope.
|
||||
public static ImageLayout AttachmentLayoutFor(DepthStencilState state, Format format, bool sampled, ImageLayout? current)
|
||||
{
|
||||
var readOnly = state.AttachmentLayout(format);
|
||||
if (sampled)
|
||||
{
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
return state.IsReadOnly(format) && current == readOnly ? readOnly : WritableAttachmentLayout(format);
|
||||
}
|
||||
|
||||
public ImageLayout AttachmentLayout(Format format)
|
||||
{
|
||||
var available = ViewFormatRules.DepthAspects(format);
|
||||
|
|
|
|||
|
|
@ -140,9 +140,6 @@ public interface IRenderHost
|
|||
// Whether a texture bound for the draw being prepared reads the depth attachment's subresources.
|
||||
bool SamplesDepthAttachment(in DepthAttachmentState depth) => true;
|
||||
|
||||
// The layout the depth attachment's subresources are in now, or null when unknown or mixed.
|
||||
ImageLayout? DepthAttachmentLayout(in DepthAttachmentState depth) => null;
|
||||
|
||||
// The next draw stores to buffers or storage images; called before its BeginRendering.
|
||||
void PrepareMemoryWritingDraw() { }
|
||||
|
||||
|
|
@ -156,10 +153,6 @@ public interface IRenderHost
|
|||
void DrawIndexedIndirect(BufferBinding arguments) =>
|
||||
throw new NotSupportedException("The render host does not draw from indirect arguments.");
|
||||
|
||||
// One draw whose counts the GPU reads from the buffer (VkDrawIndirectCommand layout).
|
||||
void DrawIndirect(BufferBinding arguments) =>
|
||||
throw new NotSupportedException("The render host does not draw from indirect arguments.");
|
||||
|
||||
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
|
||||
|
||||
// Executes a dispatch from three uint32 group counts in guest memory when the backend can
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ public sealed partial class RenderExecutor
|
|||
var vertexBuffers = AcquireVertexBuffers(vertexInput);
|
||||
var indexBuffer = AcquireIndexBuffer(in indexSource);
|
||||
var indirectArguments = emission.IndirectArgumentsAddress != 0
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, emission.Indexed ? IndexedIndirectArgumentsSize : AutoIndirectArgumentsSize, isWritten: false)
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
|
||||
: default;
|
||||
DropUnwrittenColorTargets(context, ref state, pixelProgram);
|
||||
state.Rendering = AcquireAttachments(ref state);
|
||||
|
|
@ -314,14 +314,7 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
// Uploads and shader writes end with barriers to all commands, so the
|
||||
// indirect read sees them.
|
||||
if (emission.Indexed)
|
||||
{
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
_host.DrawIndirect(indirectArguments);
|
||||
}
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -310,11 +310,9 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var loadState = depth.LoadState;
|
||||
var layout = DepthStencilState.AttachmentLayoutFor(
|
||||
loadState,
|
||||
target.Format,
|
||||
_host.SamplesDepthAttachment(in depth),
|
||||
_host.DepthAttachmentLayout(in depth));
|
||||
var layout = _host.SamplesDepthAttachment(in depth)
|
||||
? loadState.AttachmentLayout(target.Format)
|
||||
: DepthStencilState.WritableAttachmentLayout(target.Format);
|
||||
_host.TransitionDepthAttachment(in depth, layout, loadState.AttachmentWriteAspects(target.Format));
|
||||
var view = target.Request.View;
|
||||
rendering.Width = Math.Min(rendering.Width, target.Width);
|
||||
|
|
|
|||
|
|
@ -309,48 +309,6 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
public void DrawAuto(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawAutoIndirectOnGpu(banks))
|
||||
{
|
||||
DrawAutoWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawAutoCore(submitId, banks, in arguments);
|
||||
}
|
||||
|
||||
// Fans, polygons and legacy rectangle/quad lists are drawn from their vertex counts, so
|
||||
// those read the indirect arguments on the CPU. Rectangle lists draw through the host's
|
||||
// general rectangle-list variant, as indexed indirect draws do.
|
||||
private static bool CanDrawAutoIndirectOnGpu(RegisterBanks banks) =>
|
||||
(GuestPrimitiveType)banks.UserConfig.PrimitiveType is
|
||||
GuestPrimitiveType.PointList or GuestPrimitiveType.LineList or GuestPrimitiveType.LineStrip or
|
||||
GuestPrimitiveType.TriangleList or GuestPrimitiveType.TriangleStrip or GuestPrimitiveType.RectangleList;
|
||||
|
||||
private void DrawAutoWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
|
||||
Span<byte> bytes = stackalloc byte[(int)AutoIndirectArgumentsSize];
|
||||
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
|
||||
{
|
||||
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
|
||||
}
|
||||
|
||||
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
|
||||
var resolved = arguments with
|
||||
{
|
||||
VertexCount = words[0],
|
||||
InstanceCount = words[1],
|
||||
FirstVertex = words[2],
|
||||
FirstInstance = words[3],
|
||||
IndirectArgumentsAddress = 0,
|
||||
};
|
||||
DrawAutoCore(submitId, banks, in resolved);
|
||||
}
|
||||
|
||||
private const ulong AutoIndirectArgumentsSize = 16;
|
||||
|
||||
private void DrawAutoCore(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
|
||||
if (!_host.IsRecording)
|
||||
|
|
@ -406,13 +364,6 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
|
||||
{
|
||||
// A targetless draw may be retained and replayed later; it needs its counts.
|
||||
DrawAutoWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveShaderPrograms(banks, ref state);
|
||||
var vertexInput = state.Programs.VertexInput;
|
||||
var pixelInput = state.Programs.PixelInput;
|
||||
|
|
@ -445,8 +396,7 @@ public sealed partial class RenderExecutor
|
|||
false,
|
||||
0,
|
||||
(uint)vertexOffset,
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput),
|
||||
arguments.IndirectArgumentsAddress);
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
|
||||
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
|
||||
_host.ResetBindings();
|
||||
}
|
||||
|
|
@ -606,8 +556,7 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
var context = banks.Context;
|
||||
var shaderInterface = context.ShaderInterface;
|
||||
var hasColorOutput = context.ColorControl.Mode != 0 &&
|
||||
(context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,9 +100,7 @@ public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z);
|
|||
// The vertex buffer words of one fetch slot as the vertex program declares them.
|
||||
public readonly record struct VertexInputBuffer(ulong Address, uint Stride, uint RecordCount, bool PerInstance = false)
|
||||
{
|
||||
public uint MinimumFetchBytes { get; init; }
|
||||
|
||||
public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes);
|
||||
public ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount;
|
||||
}
|
||||
|
||||
// One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot.
|
||||
|
|
|
|||
|
|
@ -272,18 +272,6 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
|
|||
_lastMemoryDrainTick = nextTick;
|
||||
}
|
||||
|
||||
// Waits for a tick that has already been submitted; safe from any thread, since it only
|
||||
// waits on the timeline and records nothing.
|
||||
public void WaitForSubmittedTick(ulong tick)
|
||||
{
|
||||
if (tick >= CurrentTick)
|
||||
{
|
||||
throw Fatal($"Cannot wait off the queue for a tick that is still recording: {tick} >= {CurrentTick}.");
|
||||
}
|
||||
|
||||
_timeline.Wait(tick);
|
||||
}
|
||||
|
||||
public void Wait(ulong tick)
|
||||
{
|
||||
if (tick > CurrentTick)
|
||||
|
|
|
|||
|
|
@ -1,392 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
// The CPU mirror of the two integer f16 conversion sequences the Gen5 translator emits by
|
||||
// default, plus the vectors the device probe measures the native GLSL ext instructions with.
|
||||
// Kept next to the probe because it is the reference the probe compares against: native is
|
||||
// only used when it reproduces these results bit for bit.
|
||||
internal static class HalfConversionReference
|
||||
{
|
||||
// Mirrors Gen5SpirvTranslator.EmitHalfToFloat: the f32 bits of the f16 in the low 16 bits.
|
||||
public static uint HalfToFloatBits(uint halfBits)
|
||||
{
|
||||
var sign = (halfBits & 0x8000u) << 16;
|
||||
var exponent = (halfBits >> 10) & 0x1Fu;
|
||||
var mantissa = halfBits & 0x3FFu;
|
||||
|
||||
var normal = ((exponent + 112u) << 23) | (mantissa << 13);
|
||||
var infinityNan = 0x7F80_0000u | (mantissa << 13);
|
||||
|
||||
var highBit = (uint)BitOperations.Log2(mantissa | 1u);
|
||||
var shift = (int)(23u - highBit);
|
||||
var subFraction = (mantissa << shift) & 0x7F_FFFFu;
|
||||
var subnormal = mantissa != 0 ? (((highBit + 103u) << 23) | subFraction) : 0u;
|
||||
|
||||
var magnitude = exponent == 0 ? subnormal : exponent == 31 ? infinityNan : normal;
|
||||
return sign | magnitude;
|
||||
}
|
||||
|
||||
// Mirrors Gen5SpirvTranslator.EmitFloatToHalf: the f16 bits, round to nearest even.
|
||||
// NaN collapses to one quiet payload (sign | 0x7C00 | 0x200) by construction.
|
||||
public static uint FloatToHalfBits(uint bits)
|
||||
{
|
||||
var sign = (bits >> 16) & 0x8000u;
|
||||
var absolute = bits & 0x7FFF_FFFFu;
|
||||
|
||||
var isInfinityNan = absolute >= 0x7F80_0000u;
|
||||
var isNan = absolute > 0x7F80_0000u;
|
||||
var infinityNan = sign | 0x7C00u | (isNan ? 0x200u : 0u);
|
||||
|
||||
var exponent = absolute >> 23;
|
||||
var mantissa = absolute & 0x7F_FFFFu;
|
||||
var significand = mantissa | 0x80_0000u;
|
||||
|
||||
var roundBit = (significand >> 13) & 1u;
|
||||
var rounded = (significand + 0xFFFu + roundBit) >> 13;
|
||||
var halfExponent = exponent - 112u;
|
||||
var normalBits = (halfExponent << 10) + (rounded - 0x400u);
|
||||
var normal = exponent >= 113u ? normalBits >= 0x7C00u ? 0x7C00u : normalBits : 0u;
|
||||
|
||||
var distance = 126u - exponent;
|
||||
var shift = (int)(distance > 25u ? 25u : distance);
|
||||
var shiftMask = (1u << shift) - 1u;
|
||||
var halfWay = 1u << (shift - 1);
|
||||
var lowBits = significand & shiftMask;
|
||||
var quotient = significand >> shift;
|
||||
var roundUp = lowBits > halfWay || (lowBits == halfWay && (quotient & 1u) != 0u);
|
||||
var subnormal = quotient + (roundUp ? 1u : 0u);
|
||||
|
||||
var finite = exponent <= 112u ? subnormal : normal;
|
||||
return isInfinityNan ? infinityNan : sign | finite;
|
||||
}
|
||||
|
||||
// Every f16 bit pattern, every f16 value widened to f32, the midpoint between adjacent
|
||||
// f16 values and that midpoint one f32 ulp either way (the rounding decisions), the
|
||||
// overflow boundary, signed zeroes, infinities, NaNs and f32 subnormals. Padded to a
|
||||
// multiple of 64 so the probe dispatch needs no bounds check. Each entry is measured
|
||||
// twice: as the f16 in its low 16 bits, and as an f32 bit pattern.
|
||||
public static uint[] BuildTestVectors()
|
||||
{
|
||||
var values = new List<uint>(1 << 19);
|
||||
for (uint bits = 0; bits <= 0xFFFFu; bits++)
|
||||
{
|
||||
values.Add(bits);
|
||||
values.Add(HalfToFloatBits(bits));
|
||||
}
|
||||
|
||||
// 0x7BFF is the largest finite f16; pair it with its successor to cover the overflow edge.
|
||||
for (uint bits = 0; bits <= 0x7BFFu; bits++)
|
||||
{
|
||||
var low = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits));
|
||||
var high = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits + 1));
|
||||
var midpoint = BitConverter.SingleToUInt32Bits((float)(low + (high - low) / 2));
|
||||
for (var offset = -1; offset <= 1; offset++)
|
||||
{
|
||||
var candidate = (uint)(midpoint + offset);
|
||||
values.Add(candidate);
|
||||
values.Add(candidate | 0x8000_0000u);
|
||||
}
|
||||
}
|
||||
|
||||
float[] specials =
|
||||
[
|
||||
0f, -0f, 1f, -1f, 65504f, -65504f, 65520f, -65520f, 65536f, -65536f,
|
||||
1e30f, -1e30f, float.MaxValue, float.MinValue, float.Epsilon, -float.Epsilon,
|
||||
float.PositiveInfinity, float.NegativeInfinity,
|
||||
5.96046448e-8f, 6.09755516e-5f, -6.09755516e-5f, 3.05175781e-5f, 2.98023224e-8f,
|
||||
];
|
||||
foreach (var value in specials)
|
||||
{
|
||||
values.Add(BitConverter.SingleToUInt32Bits(value));
|
||||
}
|
||||
|
||||
uint[] rawSpecials =
|
||||
[
|
||||
0x7F80_0001u, 0xFF80_0001u, 0x7FC0_0000u, 0xFFC0_0000u, 0x7FFF_FFFFu, 0xFFFF_FFFFu,
|
||||
0x0000_0001u, 0x8000_0001u, 0x007F_FFFFu, 0x807F_FFFFu, 0x0080_0000u,
|
||||
];
|
||||
values.AddRange(rawSpecials);
|
||||
|
||||
while (values.Count % 64 != 0)
|
||||
{
|
||||
values.Add(0);
|
||||
}
|
||||
|
||||
return [.. values];
|
||||
}
|
||||
|
||||
// The probe's match rule. Every finite value, every zero and every infinity must be
|
||||
// bit-exact, sign included. A NaN only has to stay a NaN: the payload and the sign of a
|
||||
// NaN are not defined by either side. The emulation picks its own payload for f32->f16
|
||||
// (sign | 0x7C00 | 0x200) and carries the f16 payload through for f16->f32, while the
|
||||
// measured hardware convert canonicalizes to a positive quiet NaN and drops the sign
|
||||
// (Apple M4: every one of the 2314 disagreements out of 643200 is a negative NaN whose
|
||||
// sign the hardware cleared). Nothing a shader computes from a NaN can see that
|
||||
// difference short of inspecting the bits, and the alternative is paying ~20 integer
|
||||
// ops on every f16 conversion in the program to define a NaN payload the ISA leaves open.
|
||||
public static bool Matches(uint expected, uint actual, bool asHalf)
|
||||
{
|
||||
if (expected == actual)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return asHalf
|
||||
? IsHalfNan(expected) && IsHalfNan(actual)
|
||||
: IsFloatNan(expected) && IsFloatNan(actual);
|
||||
}
|
||||
|
||||
private static bool IsHalfNan(uint bits) => (bits & 0x7C00u) == 0x7C00u && (bits & 0x3FFu) != 0;
|
||||
|
||||
private static bool IsFloatNan(uint bits) => (bits & 0x7F80_0000u) == 0x7F80_0000u && (bits & 0x7F_FFFFu) != 0;
|
||||
}
|
||||
|
||||
// Runs the fixed f16 conversion probe once per device: the native GLSL UnpackHalf2x16 /
|
||||
// PackHalf2x16 are used by the translator only when this says the device reproduces the
|
||||
// emulator's own conversion. Never fatal: a failure leaves the exact emulation in place.
|
||||
internal static unsafe class NativeHalfConversionProbe
|
||||
{
|
||||
private const int ReportedExamples = 6;
|
||||
|
||||
// Note carries the reason the probe could not run at all; Detail the first few disagreements.
|
||||
public readonly record struct Result(bool Exact, int Mismatches, int Tested, string? Note, string? Detail = null);
|
||||
|
||||
// SHARPEMU_NATIVE_HALF=0 forces the emulation, =1 forces native without probing.
|
||||
public static bool? ReadOverride() =>
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_HALF") switch
|
||||
{
|
||||
"0" => false,
|
||||
"1" => true,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
var vectors = HalfConversionReference.BuildTestVectors();
|
||||
try
|
||||
{
|
||||
var results = Measure(device, scheduler, beginCommand, vectors);
|
||||
var mismatches = 0;
|
||||
var examples = new List<string>(ReportedExamples);
|
||||
for (var index = 0; index < vectors.Length; index++)
|
||||
{
|
||||
var wide = HalfConversionReference.HalfToFloatBits(vectors[index] & 0xFFFFu);
|
||||
if (!HalfConversionReference.Matches(wide, results[2 * index], asHalf: false))
|
||||
{
|
||||
mismatches++;
|
||||
if (examples.Count < ReportedExamples)
|
||||
{
|
||||
examples.Add($"h2f(0x{vectors[index] & 0xFFFF:X4})=0x{results[2 * index]:X8} want 0x{wide:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
var narrow = HalfConversionReference.FloatToHalfBits(vectors[index]);
|
||||
if (!HalfConversionReference.Matches(narrow, results[2 * index + 1], asHalf: true))
|
||||
{
|
||||
mismatches++;
|
||||
if (examples.Count < ReportedExamples)
|
||||
{
|
||||
examples.Add($"f2h(0x{vectors[index]:X8})=0x{results[2 * index + 1]:X4} want 0x{narrow:X4}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(
|
||||
mismatches == 0,
|
||||
mismatches,
|
||||
2 * vectors.Length,
|
||||
null,
|
||||
examples.Count == 0 ? null : string.Join("; ", examples));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return new Result(false, -1, 2 * vectors.Length, exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Measure(
|
||||
GpuDeviceInfo device,
|
||||
SubmissionScheduler scheduler,
|
||||
Func<CommandBuffer> beginCommand,
|
||||
uint[] vectors)
|
||||
{
|
||||
var vk = device.Vk;
|
||||
var inputBytes = (ulong)vectors.Length * sizeof(uint);
|
||||
var outputBytes = 2 * inputBytes;
|
||||
|
||||
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, inputBytes);
|
||||
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
|
||||
MemoryMarshal.AsBytes<uint>(vectors).CopyTo(input.Mapped);
|
||||
input.Flush(0, inputBytes);
|
||||
output.Mapped.Clear();
|
||||
output.Flush(0, outputBytes);
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[2];
|
||||
for (uint index = 0; index < 2; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 2,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
|
||||
|
||||
DescriptorPool pool = default;
|
||||
PipelineLayout pipelineLayout = default;
|
||||
Pipeline pipeline = default;
|
||||
try
|
||||
{
|
||||
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = 1,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
|
||||
|
||||
var boundLayout = setLayout;
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
|
||||
|
||||
var infos = stackalloc DescriptorBufferInfo[2];
|
||||
infos[0] = new DescriptorBufferInfo(input.Handle, 0, inputBytes);
|
||||
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
|
||||
var writes = stackalloc WriteDescriptorSet[2];
|
||||
for (uint binding = 0; binding < 2; binding++)
|
||||
{
|
||||
writes[binding] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = set,
|
||||
DstBinding = binding,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = infos + binding,
|
||||
};
|
||||
}
|
||||
|
||||
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
|
||||
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateHalfConversionProbe();
|
||||
ShaderModule module;
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
Silk.NET.Vulkan.Result created;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = module,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
|
||||
}
|
||||
|
||||
vk.DestroyShaderModule(device.Device, module, null);
|
||||
Require(created, "compute pipeline");
|
||||
|
||||
var command = beginCommand();
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
|
||||
var boundSet = set;
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
|
||||
vk.CmdDispatch(command, (uint)vectors.Length / 64, 1, 1);
|
||||
var barrier = new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = output.Handle,
|
||||
Offset = 0,
|
||||
Size = outputBytes,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(vk, command,
|
||||
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
|
||||
0, null, 1, &barrier, 0, null);
|
||||
scheduler.FlushAndWait();
|
||||
|
||||
output.Invalidate(0, outputBytes);
|
||||
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pipeline.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipeline(device.Device, pipeline, null);
|
||||
}
|
||||
|
||||
if (pipelineLayout.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (pool.Handle != 0)
|
||||
{
|
||||
vk.DestroyDescriptorPool(device.Device, pool, null);
|
||||
}
|
||||
|
||||
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(Silk.NET.Vulkan.Result result, string operation)
|
||||
{
|
||||
if (result != Silk.NET.Vulkan.Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"The f16 conversion probe could not create its {operation}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
// Measures whether a storage-buffer read past the end of its descriptor range reads zero on this
|
||||
// device. Every guest buffer word the translator emits carries a range test, an address clamp and
|
||||
// a zero select, which exist only to produce that zero; with robustBufferAccess2 the device owes
|
||||
// it already and the three operations are pure overhead. The capability bit alone does not settle
|
||||
// it — MoltenVK runs on Metal, which has no hardware bounds checking — so the probe reads a bound
|
||||
// range it deliberately undershoots and checks the words past it. Never run on a device that did
|
||||
// not enable robustBufferAccess2: an out-of-range read there is undefined and can fault.
|
||||
internal static unsafe class ZeroOutOfBoundsReadProbe
|
||||
{
|
||||
// The descriptor binds this many words of a larger allocation; the words past it must read
|
||||
// zero, and the words inside it must still read the sentinel the probe wrote.
|
||||
private const uint BoundWords = 256;
|
||||
private const uint ProbeWords = 1024;
|
||||
private const uint Sentinel = 0xA5C3_F00Du;
|
||||
|
||||
public readonly record struct Result(bool Zeroed, int Mismatches, int Tested, string? Note);
|
||||
|
||||
// SHARPEMU_ZERO_OOB_READS=0 keeps the per-access checks, =1 drops them without measuring.
|
||||
public static bool? ReadOverride() =>
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_ZERO_OOB_READS") switch
|
||||
{
|
||||
"0" => false,
|
||||
"1" => true,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
try
|
||||
{
|
||||
var results = Measure(device, scheduler, beginCommand);
|
||||
var mismatches = 0;
|
||||
for (var index = 0u; index < ProbeWords; index++)
|
||||
{
|
||||
var expected = index < BoundWords ? Sentinel + index : 0u;
|
||||
if (results[index] != expected)
|
||||
{
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(mismatches == 0, mismatches, (int)ProbeWords, null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return new Result(false, -1, (int)ProbeWords, exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Measure(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
var vk = device.Vk;
|
||||
var allocationBytes = (ulong)ProbeWords * sizeof(uint);
|
||||
var boundBytes = (ulong)BoundWords * sizeof(uint);
|
||||
var outputBytes = allocationBytes;
|
||||
|
||||
// The allocation is larger than the bound range and carries a sentinel everywhere, so a
|
||||
// device that simply ignores the range reads the sentinel back instead of zero.
|
||||
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, allocationBytes);
|
||||
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
|
||||
var words = new uint[ProbeWords];
|
||||
for (var index = 0u; index < ProbeWords; index++)
|
||||
{
|
||||
words[index] = Sentinel + index;
|
||||
}
|
||||
|
||||
MemoryMarshal.AsBytes<uint>(words).CopyTo(input.Mapped);
|
||||
input.Flush(0, allocationBytes);
|
||||
output.Mapped.Clear();
|
||||
output.Flush(0, outputBytes);
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[2];
|
||||
for (uint index = 0; index < 2; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 2,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
|
||||
|
||||
DescriptorPool pool = default;
|
||||
PipelineLayout pipelineLayout = default;
|
||||
Pipeline pipeline = default;
|
||||
try
|
||||
{
|
||||
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = 1,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
|
||||
|
||||
var boundLayout = setLayout;
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
|
||||
|
||||
var infos = stackalloc DescriptorBufferInfo[2];
|
||||
infos[0] = new DescriptorBufferInfo(input.Handle, 0, boundBytes);
|
||||
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
|
||||
var writes = stackalloc WriteDescriptorSet[2];
|
||||
for (uint binding = 0; binding < 2; binding++)
|
||||
{
|
||||
writes[binding] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = set,
|
||||
DstBinding = binding,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = infos + binding,
|
||||
};
|
||||
}
|
||||
|
||||
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
|
||||
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateUncheckedBufferCopyProbe();
|
||||
ShaderModule module;
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
Silk.NET.Vulkan.Result created;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = module,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
|
||||
}
|
||||
|
||||
vk.DestroyShaderModule(device.Device, module, null);
|
||||
Require(created, "compute pipeline");
|
||||
|
||||
var command = beginCommand();
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
|
||||
var boundSet = set;
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
|
||||
vk.CmdDispatch(command, ProbeWords / 64, 1, 1);
|
||||
var barrier = new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = output.Handle,
|
||||
Offset = 0,
|
||||
Size = outputBytes,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(vk, command,
|
||||
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
|
||||
0, null, 1, &barrier, 0, null);
|
||||
scheduler.FlushAndWait();
|
||||
|
||||
output.Invalidate(0, outputBytes);
|
||||
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pipeline.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipeline(device.Device, pipeline, null);
|
||||
}
|
||||
|
||||
if (pipelineLayout.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (pool.Handle != 0)
|
||||
{
|
||||
vk.DestroyDescriptorPool(device.Device, pool, null);
|
||||
}
|
||||
|
||||
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(Silk.NET.Vulkan.Result result, string operation)
|
||||
{
|
||||
if (result != Silk.NET.Vulkan.Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"The out-of-range read probe could not create its {operation}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +141,25 @@ public static class JsonExports
|
|||
InitializerSetGlobalNullAccessCallback(ctx);
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i1393UBWu1U",
|
||||
ExportName = "_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalSpecialFloatHandler(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
JsonObjectHeap.GlobalSpecialFloatHandler = ctx[CpuRegister.Rsi];
|
||||
JsonObjectHeap.GlobalSpecialFloatHandlerContext = ctx[CpuRegister.Rdx];
|
||||
TraceJson("Initializer.setGlobalSpecialFloatHandler", thisAddress, ctx[CpuRegister.Rsi]);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||
|
|
@ -168,6 +187,14 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "GvGvswb0v34",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C2Ev",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameter2BaseConstructor(CpuContext ctx) =>
|
||||
InitParameter2Constructor(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "I2QC8PYhJWY",
|
||||
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
|
||||
|
|
@ -193,6 +220,14 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "W72B9ylU2JA",
|
||||
ExportName = "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameterRtti2SetAllocatorRtti(CpuContext ctx) =>
|
||||
InitParameter2SetAllocator(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Eu95jmqn5Rw",
|
||||
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
|
||||
|
|
@ -210,6 +245,30 @@ public static class JsonExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WVZBP4IyM+E",
|
||||
ExportName = "_ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitParameter2SetSpecialFloatFormatType(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> field = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(field, unchecked((uint)ctx[CpuRegister.Rsi]));
|
||||
if (!ctx.Memory.TryWrite(thisAddress + 0x18, field))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = thisAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IXW-z8pggfg",
|
||||
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ internal static class JsonObjectHeap
|
|||
|
||||
public static ulong GlobalNullAccessCallbackContext;
|
||||
|
||||
public static ulong GlobalSpecialFloatHandler;
|
||||
|
||||
public static ulong GlobalSpecialFloatHandlerContext;
|
||||
|
||||
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||
|
||||
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||
|
|
@ -106,5 +110,7 @@ internal static class JsonObjectHeap
|
|||
Strings.Clear();
|
||||
GlobalNullAccessCallback = 0;
|
||||
GlobalNullAccessCallbackContext = 0;
|
||||
GlobalSpecialFloatHandler = 0;
|
||||
GlobalSpecialFloatHandlerContext = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ public static class KernelExports
|
|||
ExportName = "sceKernelOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenCore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqQMh1zPPT8",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
|
||||
stream.SetLength(length);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ using System.Threading;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static partial class KernelMemoryCompatExports
|
||||
{
|
||||
private const int MaxGuestStringLength = 4096;
|
||||
internal const FileShare GuestFileShare = FileShare.ReadWrite | FileShare.Delete;
|
||||
private const int WideCharSize = sizeof(ushort);
|
||||
private const int MemsetChunkSize = 16 * 1024;
|
||||
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||
|
|
@ -96,6 +98,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
private static readonly object _fdGate = new();
|
||||
private static readonly Dictionary<int, FileStream> _openFiles = new();
|
||||
private static readonly HashSet<int> _randomDeviceDescriptors = new();
|
||||
private static readonly Dictionary<int, HostMovieBridge.BinkGuestCompletionShim>
|
||||
_binkGuestCompletionShims = new();
|
||||
private static readonly Dictionary<int, string> _observedBinkGuestFiles = new();
|
||||
|
|
@ -1439,7 +1442,9 @@ public static partial class KernelMemoryCompatExports
|
|||
ExportName = "_open",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpenUnderscore(CpuContext ctx)
|
||||
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
|
||||
|
||||
internal static int KernelOpenCore(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
|
@ -1450,6 +1455,19 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (IsRandomDevicePath(guestPath) && (flags & O_DIRECTORY) == 0)
|
||||
{
|
||||
var randomFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
lock (_fdGate)
|
||||
{
|
||||
_randomDeviceDescriptors.Add(randomFd);
|
||||
}
|
||||
|
||||
LogOpenTrace($"_open random-device path='{guestPath}' flags=0x{flags:X8} fd={randomFd}");
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)randomFd);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var hostPath = ResolveGuestPath(guestPath);
|
||||
var access = ResolveOpenAccess(flags);
|
||||
var mode = ResolveOpenMode(flags, access);
|
||||
|
|
@ -1519,7 +1537,7 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
|
||||
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
|
||||
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
|
||||
if ((flags & O_APPEND) != 0)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1677,11 +1695,11 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
||||
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
|
||||
// KernelOpenCore already writes the fd into RAX (the import bridge
|
||||
// prefers a written RAX over the return value), so returning 0 is correct.
|
||||
public static int PosixOpen(CpuContext ctx)
|
||||
{
|
||||
var result = KernelOpenUnderscore(ctx);
|
||||
var result = KernelOpenCore(ctx);
|
||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
? 0
|
||||
: PosixFailure(ctx, result);
|
||||
|
|
@ -1734,7 +1752,8 @@ public static partial class KernelMemoryCompatExports
|
|||
{
|
||||
// Stop at the first miss and report its index.
|
||||
// The caller can then use its normal file-open fallback.
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
|
||||
if (sizesAddress != 0 &&
|
||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||
{
|
||||
|
|
@ -2254,7 +2273,12 @@ public static partial class KernelMemoryCompatExports
|
|||
string? observedBinkPath = null;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_openFiles.Remove(fd, out stream))
|
||||
if (_randomDeviceDescriptors.Remove(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
else if (_openFiles.Remove(fd, out stream))
|
||||
{
|
||||
_binkGuestCompletionShims.Remove(fd);
|
||||
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
|
||||
|
|
@ -2307,6 +2331,25 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
bool isRandomDevice;
|
||||
lock (_fdGate)
|
||||
{
|
||||
isRandomDevice = _randomDeviceDescriptors.Contains(fd);
|
||||
}
|
||||
|
||||
if (isRandomDevice)
|
||||
{
|
||||
var randomBytes = GC.AllocateUninitializedArray<byte>(requested);
|
||||
RandomNumberGenerator.Fill(randomBytes);
|
||||
if (!ctx.Memory.TryWrite(bufferAddress, randomBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryReadSocketFd(
|
||||
ctx,
|
||||
fd,
|
||||
|
|
@ -2842,15 +2885,23 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
|
||||
var searchEnd = searchEndRaw <= 0
|
||||
? GuestMemoryLayout.DirectBytes
|
||||
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
if (!ctx.TryWriteUInt64(outAddress, 0) || !ctx.TryWriteUInt64(outSize, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (searchStartRaw < 0 || searchEndRaw < 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var searchStart = (ulong)searchStartRaw;
|
||||
var searchEnd = Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
}
|
||||
|
||||
bool foundSpan;
|
||||
ulong candidate;
|
||||
ulong rangeAvailable;
|
||||
|
|
@ -2866,7 +2917,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
if (!foundSpan)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return MemoryNoSpace;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
|
||||
|
|
@ -6901,9 +6952,14 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
string? hostPath = null;
|
||||
bool isDirectory = false;
|
||||
bool isRandomDevice = false;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
if (_randomDeviceDescriptors.Contains(fd))
|
||||
{
|
||||
isRandomDevice = true;
|
||||
}
|
||||
else if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
{
|
||||
hostPath = directory.Path;
|
||||
isDirectory = true;
|
||||
|
|
@ -6914,6 +6970,14 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
}
|
||||
|
||||
if (isRandomDevice)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
LogIoTrace("fstat", "/dev/urandom", $"fd={fd} size=0 device=random");
|
||||
return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0,
|
||||
now, now, now, "/dev/urandom");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hostPath))
|
||||
{
|
||||
long size = 0;
|
||||
|
|
@ -6935,6 +6999,10 @@ public static partial class KernelMemoryCompatExports
|
|||
return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory);
|
||||
}
|
||||
|
||||
private static bool IsRandomDevicePath(string path) =>
|
||||
string.Equals(path, "/dev/random", StringComparison.Ordinal) ||
|
||||
string.Equals(path, "/dev/urandom", StringComparison.Ordinal);
|
||||
|
||||
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath)
|
||||
{
|
||||
var isDirectory = Directory.Exists(hostPath);
|
||||
|
|
|
|||
|
|
@ -1988,7 +1988,7 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
var waitMilliseconds = 10;
|
||||
var waitDuration = TimeSpan.FromMilliseconds(10);
|
||||
if (timed)
|
||||
{
|
||||
var remaining = GetRemainingTimeout(deadline);
|
||||
|
|
@ -1998,14 +1998,13 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
// Monitor.Wait truncates to whole milliseconds, so a sub-millisecond remainder
|
||||
// became a zero timeout and the wait spun until the deadline. UE's game thread
|
||||
// waits for the render fence in 1 ms slices, which kept a core busy. A timed wait
|
||||
// may wake late, so round up instead.
|
||||
waitMilliseconds = (int)Math.Min(waitMilliseconds, Math.Ceiling(remaining.TotalMilliseconds));
|
||||
if (remaining < waitDuration)
|
||||
{
|
||||
waitDuration = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
_ = Monitor.Wait(state.SyncRoot, waitMilliseconds);
|
||||
_ = Monitor.Wait(state.SyncRoot, waitDuration);
|
||||
if (waiter.CompletionState == 0 &&
|
||||
timed &&
|
||||
GetRemainingTimeout(deadline) <= TimeSpan.Zero)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ public static class KernelRuntimeCompatExports
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate ulong RdtscDelegate();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uvT2iYBBnkY",
|
||||
ExportName = "sceKernelSync",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSync(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1jfXLRVzisc",
|
||||
ExportName = "sceKernelUsleep",
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
{
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
// Round up so a sub-millisecond remainder does not spin.
|
||||
Monitor.Wait(_hostGate, (int)Math.Ceiling(waitSlice.TotalMilliseconds));
|
||||
Monitor.Wait(_hostGate, waitSlice);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
/// <summary>
|
||||
/// Process-argument accessors exported by libKernel for the platform libc.
|
||||
/// </summary>
|
||||
public static class LibcProcessArgumentExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "iKJMWrAumPE",
|
||||
ExportName = "getargc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int GetArgc(CpuContext ctx) =>
|
||||
ctx.SetReturn(HleDataSymbols.ProcessArgumentCount);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FJmglmTMdr4",
|
||||
ExportName = "getargv",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int GetArgv(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = HleDataSymbols.ProcessArgumentVectorAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,20 @@ public static class LibcStdioExports
|
|||
private static readonly object _ctypeTableGate = new();
|
||||
private static nint _ctypeTableBase;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vZkmJmvqueY",
|
||||
ExportName = "_Lockfilelock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int LockFileLock(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0x7rx8TKy2Y",
|
||||
ExportName = "_Unlockfilelock",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int UnlockFileLock(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xeYO4u7uyJ0",
|
||||
ExportName = "fopen",
|
||||
|
|
@ -133,7 +147,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1049,7 +1063,7 @@ public static class LibcStdioExports
|
|||
}
|
||||
}
|
||||
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
var replacement = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
|
||||
lock (file.Gate)
|
||||
{
|
||||
if (file.IsClosed)
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ internal static class HostMovieBridge
|
|||
private static bool _frameBufferPresented;
|
||||
private static MediaFramePlayback? _playback;
|
||||
private static Timer? _playbackWatchdog;
|
||||
private static long _watchdogFrameIndex = -1;
|
||||
private static readonly TimeSpan StalledPlaybackTimeout = TimeSpan.FromSeconds(60);
|
||||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
|
|
@ -328,7 +326,6 @@ internal static class HostMovieBridge
|
|||
private static void ArmPlaybackWatchdogLocked(string hostPath)
|
||||
{
|
||||
_playbackWatchdog?.Dispose();
|
||||
_watchdogFrameIndex = -1;
|
||||
var timeout = TryReadBinkInfo(hostPath, out var info)
|
||||
? GetPlaybackWatchdogTimeout(info)
|
||||
: TimeSpan.FromSeconds(150);
|
||||
|
|
@ -343,17 +340,6 @@ internal static class HostMovieBridge
|
|||
return;
|
||||
}
|
||||
|
||||
// Playback follows the guest's own decode and audio clock, so a slow host plays a
|
||||
// movie slower than real time. Expire only a movie that stopped advancing: forcing EOF
|
||||
// under a guest decoder that is still mid-stream wedges it on the last frame.
|
||||
var frameIndex = _playback?.PlaybackProgress.FrameIndex ?? -1;
|
||||
if (_playback is not null && frameIndex > _watchdogFrameIndex)
|
||||
{
|
||||
_watchdogFrameIndex = frameIndex;
|
||||
_playbackWatchdog?.Change(StalledPlaybackTimeout, Timeout.InfiniteTimeSpan);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 host watchdog expired for " +
|
||||
Path.GetFileName(path) + "; advancing to the next movie.");
|
||||
|
|
|
|||
|
|
@ -12,29 +12,82 @@ public static class Http2Exports
|
|||
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
|
||||
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
|
||||
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
|
||||
private const int Http2ErrorNullPointer = unchecked((int)0x817B1225);
|
||||
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
|
||||
|
||||
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
|
||||
private static readonly ConcurrentDictionary<int, Http2CookieBox> CookieBoxes = new();
|
||||
private static int _nextContextId;
|
||||
private static int _nextTemplateId = 0x1000;
|
||||
private static int _nextRequestId = 0x2000;
|
||||
private static int _nextCookieBoxId = 0x3000;
|
||||
|
||||
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
|
||||
|
||||
private sealed record Http2Template(int ContextId, string UserAgent, int HttpVersion, bool AutoProxyConfig);
|
||||
private sealed record Http2CookieBox;
|
||||
|
||||
private sealed record Http2Request(int TemplateId, string Method, string Url, ulong ContentLength);
|
||||
private sealed record Http2Options(
|
||||
uint MinimumSslVersion,
|
||||
uint SslOptions,
|
||||
ulong RedirectCallback,
|
||||
ulong RedirectUserArgument,
|
||||
bool AutoRedirect,
|
||||
int CookieBoxId,
|
||||
uint ConnectTimeoutMicroseconds,
|
||||
uint ReceiveTimeoutMicroseconds,
|
||||
uint SendTimeoutMicroseconds,
|
||||
bool AuthEnabled,
|
||||
ulong SslCallback,
|
||||
ulong SslUserArgument)
|
||||
{
|
||||
public static readonly Http2Options Default = new(0, 0, 0, 0, true, 0, 0, 0, 0, false, 0, 0);
|
||||
}
|
||||
|
||||
private sealed record Http2Header(string Name, string Value, uint Mode);
|
||||
|
||||
private sealed record Http2Template(
|
||||
int ContextId,
|
||||
string UserAgent,
|
||||
int HttpVersion,
|
||||
bool AutoProxyConfig,
|
||||
Http2Options Options);
|
||||
|
||||
private sealed record Http2Request(
|
||||
int TemplateId,
|
||||
string Method,
|
||||
string Url,
|
||||
ulong ContentLength,
|
||||
Http2Options Options,
|
||||
Http2Header[] Headers);
|
||||
|
||||
internal readonly record struct Http2OptionsSnapshot(
|
||||
uint MinimumSslVersion,
|
||||
uint SslOptions,
|
||||
ulong RedirectCallback,
|
||||
ulong RedirectUserArgument,
|
||||
bool AutoRedirect,
|
||||
int CookieBoxId,
|
||||
uint ConnectTimeoutMicroseconds,
|
||||
uint ReceiveTimeoutMicroseconds,
|
||||
uint SendTimeoutMicroseconds,
|
||||
bool AuthEnabled,
|
||||
ulong SslCallback,
|
||||
ulong SslUserArgument);
|
||||
|
||||
internal readonly record struct Http2HeaderSnapshot(string Name, string Value, uint Mode);
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Templates.Clear();
|
||||
Requests.Clear();
|
||||
CookieBoxes.Clear();
|
||||
_nextContextId = 0;
|
||||
_nextTemplateId = 0x1000;
|
||||
_nextRequestId = 0x2000;
|
||||
_nextCookieBoxId = 0x3000;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -119,7 +172,12 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextTemplateId);
|
||||
Templates[id] = new Http2Template(contextId, userAgent, httpVersion, autoProxyConfig != 0);
|
||||
Templates[id] = new Http2Template(
|
||||
contextId,
|
||||
userAgent,
|
||||
httpVersion,
|
||||
autoProxyConfig != 0,
|
||||
Http2Options.Default);
|
||||
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -140,6 +198,175 @@ public static class Http2Exports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "09tk+kIA1Ns",
|
||||
ExportName = "sceHttp2SetMinSslVersion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetMinSslVersion(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var minimumSslVersion = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
|
||||
return TryUpdateOptions(id, options => options with { MinimumSslVersion = minimumSslVersion })
|
||||
? TraceAndReturn(ctx, "set_min_ssl_version", id, minimumSslVersion, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "B37SruheQ5Y",
|
||||
ExportName = "sceHttp2SslDisableOption",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SslDisableOption(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions & ~flags })
|
||||
? TraceAndReturn(ctx, "ssl_disable_option", id, flags, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EWcwMpbr5F8",
|
||||
ExportName = "sceHttp2SslEnableOption",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SslEnableOption(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions | flags })
|
||||
? TraceAndReturn(ctx, "ssl_enable_option", id, flags, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "BJgi0CH7al4",
|
||||
ExportName = "sceHttp2SetRedirectCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRedirectCallback(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var callback = ctx[CpuRegister.Rsi];
|
||||
var userArgument = ctx[CpuRegister.Rdx];
|
||||
return TryUpdateOptions(
|
||||
id,
|
||||
options => options with
|
||||
{
|
||||
RedirectCallback = callback,
|
||||
RedirectUserArgument = userArgument,
|
||||
})
|
||||
? TraceAndReturn(ctx, "set_redirect_callback", id, callback, userArgument, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b9AvoIaOuHI",
|
||||
ExportName = "sceHttp2SetAutoRedirect",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetAutoRedirect(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||
return TryUpdateOptions(id, options => options with { AutoRedirect = enabled })
|
||||
? TraceAndReturn(ctx, "set_auto_redirect", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N4UfjvWJsMw",
|
||||
ExportName = "sceHttp2CreateCookieBox",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2CreateCookieBox(CpuContext ctx)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextCookieBoxId);
|
||||
CookieBoxes[id] = new Http2CookieBox();
|
||||
TraceHttp2("create_cookie_box", id, 0, 0, 0, 0);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jrVHsKCXA0g",
|
||||
ExportName = "sceHttp2SetCookieBox",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetCookieBox(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var cookieBoxId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (cookieBoxId != 0 && !CookieBoxes.ContainsKey(cookieBoxId))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
return TryUpdateOptions(id, options => options with { CookieBoxId = cookieBoxId })
|
||||
? TraceAndReturn(ctx, "set_cookie_box", id, unchecked((ulong)cookieBoxId), 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-HIO4VT87v8",
|
||||
ExportName = "sceHttp2SetConnectTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetConnectTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { ConnectTimeoutMicroseconds = value }, "connect");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "izvHhqgDt44",
|
||||
ExportName = "sceHttp2SetRecvTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRecvTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { ReceiveTimeoutMicroseconds = value }, "recv");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XPtW45xiLHk",
|
||||
ExportName = "sceHttp2SetSendTimeOut",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetSendTimeOut(CpuContext ctx) =>
|
||||
SetTimeout(ctx, static (options, value) => options with { SendTimeoutMicroseconds = value }, "send");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jjFahkBPCYs",
|
||||
ExportName = "sceHttp2SetAuthEnabled",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetAuthEnabled(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
|
||||
return TryUpdateOptions(id, options => options with { AuthEnabled = enabled })
|
||||
? TraceAndReturn(ctx, "set_auth_enabled", id, enabled ? 1UL : 0UL, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YrWX+DhPHQY",
|
||||
ExportName = "sceHttp2SetSslCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetSslCallback(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var callback = ctx[CpuRegister.Rsi];
|
||||
var userArgument = ctx[CpuRegister.Rdx];
|
||||
return TryUpdateOptions(
|
||||
id,
|
||||
options => options with
|
||||
{
|
||||
SslCallback = callback,
|
||||
SslUserArgument = userArgument,
|
||||
})
|
||||
? TraceAndReturn(ctx, "set_ssl_callback", id, callback, userArgument, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceHttp2CreateRequestWithURL",
|
||||
Target = Generation.Gen5,
|
||||
|
|
@ -147,7 +374,7 @@ public static class Http2Exports
|
|||
public static int Http2CreateRequestWithUrl(CpuContext ctx)
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Templates.ContainsKey(templateId))
|
||||
if (!Templates.TryGetValue(templateId, out var template))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
|
@ -159,11 +386,73 @@ public static class Http2Exports
|
|||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
Requests[id] = new Http2Request(templateId, method, url, ctx[CpuRegister.Rcx]);
|
||||
Requests[id] = new Http2Request(
|
||||
templateId,
|
||||
method,
|
||||
url,
|
||||
ctx[CpuRegister.Rcx],
|
||||
template.Options,
|
||||
[]);
|
||||
TraceHttp2("create_request", id, unchecked((ulong)templateId), ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nrPfOE8TQu0",
|
||||
ExportName = "sceHttp2AddRequestHeader",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2AddRequestHeader(CpuContext ctx)
|
||||
{
|
||||
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var valueAddress = ctx[CpuRegister.Rdx];
|
||||
var mode = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||
if (nameAddress == 0 || valueAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorNullPointer);
|
||||
}
|
||||
|
||||
if (!TryReadUtf8Z(ctx, nameAddress, 4096, out var name) ||
|
||||
!TryReadUtf8Z(ctx, valueAddress, 16384, out var value))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidValue);
|
||||
}
|
||||
|
||||
while (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
var headers = new Http2Header[request.Headers.Length + 1];
|
||||
Array.Copy(request.Headers, headers, request.Headers.Length);
|
||||
headers[^1] = new Http2Header(name, value, mode);
|
||||
if (Requests.TryUpdate(requestId, request with { Headers = headers }, request))
|
||||
{
|
||||
return TraceAndReturn(ctx, "add_request_header", requestId, nameAddress, valueAddress, mode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FSAFOzi0FpM",
|
||||
ExportName = "sceHttp2SetRequestContentLength",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceHttp2")]
|
||||
public static int Http2SetRequestContentLength(CpuContext ctx)
|
||||
{
|
||||
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var contentLength = ctx[CpuRegister.Rsi];
|
||||
while (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
if (Requests.TryUpdate(requestId, request with { ContentLength = contentLength }, request))
|
||||
{
|
||||
return TraceAndReturn(ctx, "set_request_content_length", requestId, contentLength, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceHttp2DeleteRequest",
|
||||
Target = Generation.Gen5,
|
||||
|
|
@ -202,6 +491,100 @@ public static class Http2Exports
|
|||
}
|
||||
}
|
||||
|
||||
private static int SetTimeout(
|
||||
CpuContext ctx,
|
||||
Func<Http2Options, uint, Http2Options> update,
|
||||
string timeoutName)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var microseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
return TryUpdateOptions(id, options => update(options, microseconds))
|
||||
? TraceAndReturn(ctx, $"set_{timeoutName}_timeout", id, microseconds, 0, 0, 0)
|
||||
: ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
private static bool TryUpdateOptions(int id, Func<Http2Options, Http2Options> update)
|
||||
{
|
||||
while (Templates.TryGetValue(id, out var template))
|
||||
{
|
||||
if (Templates.TryUpdate(id, template with { Options = update(template.Options) }, template))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
while (Requests.TryGetValue(id, out var request))
|
||||
{
|
||||
if (Requests.TryUpdate(id, request with { Options = update(request.Options) }, request))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryGetOptionsForTests(int id, out Http2OptionsSnapshot snapshot)
|
||||
{
|
||||
var options = Templates.TryGetValue(id, out var template)
|
||||
? template.Options
|
||||
: Requests.TryGetValue(id, out var request)
|
||||
? request.Options
|
||||
: null;
|
||||
if (options is null)
|
||||
{
|
||||
snapshot = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = new Http2OptionsSnapshot(
|
||||
options.MinimumSslVersion,
|
||||
options.SslOptions,
|
||||
options.RedirectCallback,
|
||||
options.RedirectUserArgument,
|
||||
options.AutoRedirect,
|
||||
options.CookieBoxId,
|
||||
options.ConnectTimeoutMicroseconds,
|
||||
options.ReceiveTimeoutMicroseconds,
|
||||
options.SendTimeoutMicroseconds,
|
||||
options.AuthEnabled,
|
||||
options.SslCallback,
|
||||
options.SslUserArgument);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static Http2HeaderSnapshot[] GetRequestHeadersForTests(int requestId) =>
|
||||
Requests.TryGetValue(requestId, out var request)
|
||||
? request.Headers
|
||||
.Select(static header => new Http2HeaderSnapshot(header.Name, header.Value, header.Mode))
|
||||
.ToArray()
|
||||
: [];
|
||||
|
||||
internal static bool TryGetRequestContentLengthForTests(int requestId, out ulong contentLength)
|
||||
{
|
||||
if (Requests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
contentLength = request.ContentLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
contentLength = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int TraceAndReturn(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
int id,
|
||||
ulong arg0,
|
||||
ulong arg1,
|
||||
ulong arg2,
|
||||
ulong arg3)
|
||||
{
|
||||
TraceHttp2(operation, id, arg0, arg1, arg2, arg3);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
|
|
|
|||
|
|
@ -14,28 +14,42 @@ namespace SharpEmu.Libs.Network;
|
|||
|
||||
public static class NetExports
|
||||
{
|
||||
private const int NetErrorNoEntry = unchecked((int)0x80410102);
|
||||
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
||||
private const int NetErrorFault = unchecked((int)0x8041010E);
|
||||
private const int NetErrorAlreadyExists = unchecked((int)0x80410111);
|
||||
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
||||
private const int NetErrorTooManyFiles = unchecked((int)0x80410118);
|
||||
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
|
||||
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
|
||||
private const int NetErrorTooManyOpenFiles = unchecked((int)0x80410118);
|
||||
private const int NetErrorResolverNoDns = unchecked((int)0x804101E1);
|
||||
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
|
||||
private const int NetErrnoNoEntry = 2;
|
||||
private const int NetErrnoBadFileDescriptor = 9;
|
||||
private const int NetErrnoFault = 14;
|
||||
private const int NetErrnoAlreadyExists = 17;
|
||||
private const int NetErrnoInvalidArgument = 22;
|
||||
private const int NetErrnoTooManyFiles = 24;
|
||||
private const int NetErrnoWouldBlock = 35;
|
||||
private const int NetErrnoAddressInUse = 48;
|
||||
private const int NetErrnoTooManyOpenFiles = 24;
|
||||
private const int NetErrnoNotInitialized = 200;
|
||||
private const int NetErrnoResolverNoDns = 225;
|
||||
private const int MaxNameLength = 256;
|
||||
private const int EpollIdBase = 1024;
|
||||
private const int EpollCapacity = 32;
|
||||
private const int NetEpollEventSize = 24;
|
||||
private const uint NetEpollIn = 0x00000001;
|
||||
private const uint NetEpollOut = 0x00000002;
|
||||
private const uint NetEpollError = 0x00000008;
|
||||
private static ReadOnlySpan<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
|
||||
|
||||
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
|
||||
private static readonly object EpollGate = new();
|
||||
private static readonly Dictionary<int, NetEpollContext> _epolls = new();
|
||||
private static int _nextPoolId;
|
||||
private static int _nextResolverId = 0x2000;
|
||||
private static int _nextSocketId = 255;
|
||||
|
|
@ -52,6 +66,26 @@ public static class NetExports
|
|||
|
||||
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
|
||||
|
||||
private sealed class NetEpollContext(string name)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
||||
public List<NetEpollRegistration> Registrations { get; } = [];
|
||||
}
|
||||
|
||||
private readonly record struct NetEpollRegistration(int SocketId, NetEpollEvent Event);
|
||||
|
||||
private readonly record struct NetEpollEvent(uint Events, ulong Data);
|
||||
|
||||
internal static void ResetEpollsForTests()
|
||||
{
|
||||
lock (EpollGate)
|
||||
{
|
||||
_epolls.Clear();
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetReadEventState(
|
||||
int socketId,
|
||||
ulong lowWater,
|
||||
|
|
@ -116,6 +150,11 @@ public static class NetExports
|
|||
_initialized = false;
|
||||
_pools.Clear();
|
||||
_resolvers.Clear();
|
||||
lock (EpollGate)
|
||||
{
|
||||
_epolls.Clear();
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
foreach (var socket in _sockets.Values)
|
||||
{
|
||||
socket.Dispose();
|
||||
|
|
@ -195,6 +234,12 @@ public static class NetExports
|
|||
public static int NetSocketClose(CpuContext ctx)
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (KernelSocketCompatExports.TryCloseSocketFd(id))
|
||||
{
|
||||
TraceNet("socket.close.posix", id, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (!_sockets.TryRemove(id, out var socket))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
|
|
@ -715,6 +760,291 @@ public static class NetExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "SF47kB2MNTo",
|
||||
ExportName = "sceNetEpollCreate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollCreate(CpuContext ctx)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
|
||||
}
|
||||
|
||||
var nameAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (nameAddress == 0 || flags != 0 ||
|
||||
!TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var name))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
lock (EpollGate)
|
||||
{
|
||||
for (var index = 0; index < EpollCapacity; index++)
|
||||
{
|
||||
var id = EpollIdBase + index;
|
||||
if (_epolls.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_epolls.Add(id, new NetEpollContext(name));
|
||||
TraceNet("epoll.create", id, nameAddress, 0, 0);
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
}
|
||||
|
||||
return SetNetError(ctx, NetErrorTooManyOpenFiles, NetErrnoTooManyOpenFiles);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZVw46bsasAk",
|
||||
ExportName = "sceNetEpollControl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollControl(CpuContext ctx)
|
||||
{
|
||||
const int EpollControlAdd = 1;
|
||||
const int EpollControlModify = 2;
|
||||
const int EpollControlDelete = 3;
|
||||
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var socketId = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var eventAddress = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (operation is < EpollControlAdd or > EpollControlDelete ||
|
||||
(operation is EpollControlAdd or EpollControlModify && eventAddress == 0) ||
|
||||
(operation == EpollControlDelete && eventAddress != 0))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_sockets.ContainsKey(socketId))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
NetEpollEvent requestedEvent = default;
|
||||
if (eventAddress != 0 && !TryReadNetEpollEvent(ctx, eventAddress, out requestedEvent))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.TryGetValue(epollId, out var epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
var registrationIndex = epoll.Registrations.FindIndex(value => value.SocketId == socketId);
|
||||
switch (operation)
|
||||
{
|
||||
case EpollControlAdd when registrationIndex >= 0:
|
||||
return SetNetError(ctx, NetErrorAlreadyExists, NetErrnoAlreadyExists);
|
||||
case EpollControlAdd:
|
||||
epoll.Registrations.Add(new NetEpollRegistration(socketId, requestedEvent));
|
||||
break;
|
||||
case EpollControlModify when registrationIndex < 0:
|
||||
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
|
||||
case EpollControlModify:
|
||||
epoll.Registrations[registrationIndex] =
|
||||
new NetEpollRegistration(socketId, requestedEvent);
|
||||
break;
|
||||
case EpollControlDelete when registrationIndex < 0:
|
||||
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
|
||||
case EpollControlDelete:
|
||||
epoll.Registrations.RemoveAt(registrationIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
|
||||
TraceNet(
|
||||
"epoll.control",
|
||||
epollId,
|
||||
unchecked((uint)operation),
|
||||
unchecked((uint)socketId),
|
||||
requestedEvent.Events);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "drjIbDbA7UQ",
|
||||
ExportName = "sceNetEpollWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollWait(CpuContext ctx)
|
||||
{
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var eventsAddress = ctx[CpuRegister.Rsi];
|
||||
var maxEvents = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var timeoutMicroseconds = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||
|
||||
if (eventsAddress == 0)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
if (maxEvents <= 0 || timeoutMicroseconds < -1)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
NetEpollRegistration[] registrations;
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.TryGetValue(epollId, out var epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
if (epoll.Registrations.Count == 0 && timeoutMicroseconds != 0)
|
||||
{
|
||||
if (timeoutMicroseconds < 0)
|
||||
{
|
||||
while (_epolls.TryGetValue(epollId, out epoll) && epoll.Registrations.Count == 0)
|
||||
{
|
||||
Monitor.Wait(EpollGate);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Monitor.Wait(
|
||||
EpollGate,
|
||||
TimeSpan.FromTicks((long)timeoutMicroseconds * TimeSpan.TicksPerMicrosecond));
|
||||
}
|
||||
|
||||
if (!_epolls.TryGetValue(epollId, out epoll))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
registrations = epoll.Registrations.ToArray();
|
||||
}
|
||||
|
||||
if (registrations.Length == 0)
|
||||
{
|
||||
TraceNet("epoll.wait", epollId, unchecked((uint)maxEvents), unchecked((uint)timeoutMicroseconds), 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var readSockets = new List<Socket>();
|
||||
var writeSockets = new List<Socket>();
|
||||
var errorSockets = new List<Socket>();
|
||||
var hostRegistrations = new Dictionary<Socket, NetEpollRegistration>();
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
if (!_sockets.TryGetValue(registration.SocketId, out var socket))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hostRegistrations[socket] = registration;
|
||||
if ((registration.Event.Events & NetEpollIn) != 0)
|
||||
{
|
||||
readSockets.Add(socket);
|
||||
}
|
||||
if ((registration.Event.Events & NetEpollOut) != 0)
|
||||
{
|
||||
writeSockets.Add(socket);
|
||||
}
|
||||
errorSockets.Add(socket);
|
||||
}
|
||||
|
||||
if (hostRegistrations.Count == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Socket.Select(readSockets, writeSockets, errorSockets, timeoutMicroseconds);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
var readyCount = 0;
|
||||
foreach (var (socket, registration) in hostRegistrations)
|
||||
{
|
||||
uint readyEvents = 0;
|
||||
if (readSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollIn;
|
||||
}
|
||||
if (writeSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollOut;
|
||||
}
|
||||
if (errorSockets.Contains(socket))
|
||||
{
|
||||
readyEvents |= NetEpollError;
|
||||
}
|
||||
if (readyEvents == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputAddress = eventsAddress + (ulong)(readyCount * NetEpollEventSize);
|
||||
if (!TryWriteNetEpollEvent(
|
||||
ctx,
|
||||
outputAddress,
|
||||
readyEvents,
|
||||
registration.SocketId,
|
||||
registration.Event.Data))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
|
||||
}
|
||||
|
||||
readyCount++;
|
||||
if (readyCount == maxEvents)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TraceNet(
|
||||
"epoll.wait",
|
||||
epollId,
|
||||
unchecked((uint)maxEvents),
|
||||
unchecked((uint)timeoutMicroseconds),
|
||||
unchecked((uint)readyCount));
|
||||
return ctx.SetReturn(readyCount);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Inp1lfL+Jdw",
|
||||
ExportName = "sceNetEpollDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEpollDestroy(CpuContext ctx)
|
||||
{
|
||||
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
lock (EpollGate)
|
||||
{
|
||||
if (!_epolls.Remove(epollId))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(EpollGate);
|
||||
}
|
||||
|
||||
TraceNet("epoll.destroy", epollId, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dgJBaeJnGpo",
|
||||
ExportName = "sceNetPoolCreate",
|
||||
|
|
@ -816,6 +1146,53 @@ public static class NetExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v6M4txecCuo",
|
||||
ExportName = "sceNetEtherNtostr",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetEtherNtostr(CpuContext ctx)
|
||||
{
|
||||
const int EtherStringLength = 18;
|
||||
const string HexDigits = "0123456789abcdef";
|
||||
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var destination = ctx[CpuRegister.Rsi];
|
||||
var destinationLength = ctx[CpuRegister.Rdx];
|
||||
if (address == 0 || destination == 0 || destinationLength < EtherStringLength)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> macAddress = stackalloc byte[6];
|
||||
if (!ctx.Memory.TryRead(address, macAddress))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> text = stackalloc byte[EtherStringLength];
|
||||
for (var index = 0; index < macAddress.Length; index++)
|
||||
{
|
||||
var value = macAddress[index];
|
||||
var offset = index * 3;
|
||||
text[offset] = unchecked((byte)HexDigits[value >> 4]);
|
||||
text[offset + 1] = unchecked((byte)HexDigits[value & 0xF]);
|
||||
if (index != macAddress.Length - 1)
|
||||
{
|
||||
text[offset + 2] = (byte)':';
|
||||
}
|
||||
}
|
||||
text[^1] = 0;
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination, text))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
TraceNet("ether_ntostr", 0, address, destination, destinationLength);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C4UgDHHPvdw",
|
||||
ExportName = "sceNetResolverCreate",
|
||||
|
|
@ -1025,6 +1402,39 @@ public static class NetExports
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNetEpollEvent(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
out NetEpollEvent epollEvent)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
epollEvent = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
epollEvent = new NetEpollEvent(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(bytes[16..]));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteNetEpollEvent(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
uint events,
|
||||
int socketId,
|
||||
ulong data)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
|
||||
bytes.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, events);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], unchecked((uint)socketId));
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[16..], data);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Kcp5d-q1Uo",
|
||||
ExportName = "sceNetInetPton",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal reliable-UDP poll object support. SharpEmu does not expose a host
|
||||
/// RUDP transport yet, but callers still require a distinct non-zero poll ID
|
||||
/// for their offline/event-processing path.
|
||||
/// </summary>
|
||||
public static class RudpExports
|
||||
{
|
||||
private const int MaxLivePolls = 4096;
|
||||
private const ulong PollEventSize = 8;
|
||||
private const int RudpErrorInvalidArgument = unchecked((int)0x80770004);
|
||||
private const int RudpErrorInvalidPollId = unchecked((int)0x8077001F);
|
||||
private const int RudpErrorOutOfMemory = unchecked((int)0x80770003);
|
||||
private static readonly object PollGate = new();
|
||||
private static readonly Dictionary<int, uint> PollCapacities = [];
|
||||
private static int _nextPollId;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MVbmLASjn5M",
|
||||
ExportName = "sceRudpPollCreate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRudp")]
|
||||
public static int RudpPollCreate(CpuContext ctx)
|
||||
{
|
||||
var requestedEvents = ctx[CpuRegister.Rdi];
|
||||
if (requestedEvents is 0 or > uint.MaxValue)
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidArgument);
|
||||
}
|
||||
|
||||
lock (PollGate)
|
||||
{
|
||||
if (PollCapacities.Count >= MaxLivePolls)
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorOutOfMemory);
|
||||
}
|
||||
|
||||
int pollId;
|
||||
do
|
||||
{
|
||||
_nextPollId = _nextPollId == int.MaxValue ? 1 : _nextPollId + 1;
|
||||
pollId = _nextPollId;
|
||||
}
|
||||
while (PollCapacities.ContainsKey(pollId));
|
||||
|
||||
PollCapacities.Add(pollId, unchecked((uint)requestedEvents));
|
||||
return ctx.SetReturn(pollId);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "M6ggviwXpLs",
|
||||
ExportName = "sceRudpPollWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRudp")]
|
||||
public static int RudpPollWait(CpuContext ctx)
|
||||
{
|
||||
var pollId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var events = ctx[CpuRegister.Rsi];
|
||||
var maxEvents = ctx[CpuRegister.Rdx];
|
||||
var timeoutMicroseconds = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (!TryGetPollCapacity(pollId, out _))
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidPollId);
|
||||
}
|
||||
|
||||
if (events == 0 ||
|
||||
maxEvents == 0 ||
|
||||
maxEvents > uint.MaxValue ||
|
||||
maxEvents > ulong.MaxValue / PollEventSize ||
|
||||
!ctx.Memory.CanRead(events, maxEvents * PollEventSize))
|
||||
{
|
||||
return ctx.SetReturn(RudpErrorInvalidArgument);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
if (timeoutMicroseconds == 0 ||
|
||||
!GuestThreadExecution.IsGuestThread ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out _))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var timeout = timeoutMicroseconds > (ulong)(TimeSpan.MaxValue.Ticks / 10)
|
||||
? TimeSpan.MaxValue
|
||||
: TimeSpan.FromTicks(checked((long)timeoutMicroseconds * 10));
|
||||
_ = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceRudpPollWait",
|
||||
$"rudp-poll:{pollId}",
|
||||
waiter: null,
|
||||
blockDeadlineTimestamp: GuestThreadExecution.ComputeDeadlineTimestamp(timeout));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
lock (PollGate)
|
||||
{
|
||||
PollCapacities.Clear();
|
||||
_nextPollId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetPollCapacity(int pollId, out uint capacity)
|
||||
{
|
||||
lock (PollGate)
|
||||
{
|
||||
return PollCapacities.TryGetValue(pollId, out capacity);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
}
|
||||
|
|
@ -13,15 +13,22 @@ public static class NpAuthExports
|
|||
private const int NpAuthErrorAborted = unchecked((int)0x80550304);
|
||||
private const int NpAuthErrorRequestMaximum = unchecked((int)0x80550305);
|
||||
private const int NpAuthErrorRequestNotFound = unchecked((int)0x80550306);
|
||||
private const int NpAuthErrorServiceDown = unchecked((int)0x80550401);
|
||||
private const int NpAuthErrorInvalidArgument = unchecked((int)0x80550301);
|
||||
private const int NpErrorSignedOut = unchecked((int)0x80550006);
|
||||
private const int AuthorizationCodeSize = 136;
|
||||
private const int IdTokenSize = 4104;
|
||||
private const ulong AsyncParameterSize = 24;
|
||||
|
||||
private static readonly ConcurrentDictionary<int, AuthRequest> Requests = new();
|
||||
private static readonly object RequestGate = new();
|
||||
private static int _nextRequestId;
|
||||
|
||||
private sealed class AuthRequest
|
||||
private sealed class AuthRequest(bool async)
|
||||
{
|
||||
public bool Async { get; } = async;
|
||||
|
||||
public bool Completed { get; set; }
|
||||
|
||||
public bool Aborted { get; set; }
|
||||
|
||||
public int Result { get; set; }
|
||||
|
|
@ -47,7 +54,7 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthCreateRequest",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx);
|
||||
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx, async: false);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthCreateAsyncRequest",
|
||||
|
|
@ -63,7 +70,7 @@ public static class NpAuthExports
|
|||
return ctx.SetReturn(NpAuthErrorInvalidSize);
|
||||
}
|
||||
|
||||
return CreateRequest(ctx);
|
||||
return CreateRequest(ctx, async: true);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -134,21 +141,23 @@ public static class NpAuthExports
|
|||
ExportName = "sceNpAuthGetAuthorizationCodeV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], AuthorizationCodeSize, ctx[CpuRegister.Rcx]);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetIdTokenV3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetIdTokenV3(CpuContext ctx) =>
|
||||
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], IdTokenSize, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
ExportName = "sceNpAuthGetAuthorizedAppCode",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpAuth")]
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => RejectOnlineOperation(ctx);
|
||||
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => CompleteSignedOut(ctx, 0, 0, 0);
|
||||
|
||||
private static int CreateRequest(CpuContext ctx)
|
||||
private static int CreateRequest(CpuContext ctx, bool async)
|
||||
{
|
||||
lock (RequestGate)
|
||||
{
|
||||
|
|
@ -160,7 +169,7 @@ public static class NpAuthExports
|
|||
while (true)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
if (Requests.TryAdd(id, new AuthRequest()))
|
||||
if (Requests.TryAdd(id, new AuthRequest(async)))
|
||||
{
|
||||
return ctx.SetReturn(id);
|
||||
}
|
||||
|
|
@ -168,8 +177,20 @@ public static class NpAuthExports
|
|||
}
|
||||
}
|
||||
|
||||
private static int RejectOnlineOperation(CpuContext ctx)
|
||||
private static int CompleteSignedOut(CpuContext ctx, ulong outputAddress, int outputSize, ulong issuerAddress)
|
||||
{
|
||||
if (outputAddress != 0 && outputSize != 0)
|
||||
{
|
||||
Span<byte> cleared = stackalloc byte[outputSize];
|
||||
cleared.Clear();
|
||||
ctx.Memory.TryWrite(outputAddress, cleared);
|
||||
}
|
||||
|
||||
if (issuerAddress != 0)
|
||||
{
|
||||
ctx.TryWriteUInt32(issuerAddress, 0);
|
||||
}
|
||||
|
||||
if (!Requests.TryGetValue(unchecked((int)ctx[CpuRegister.Rdi]), out var request))
|
||||
{
|
||||
return ctx.SetReturn(NpAuthErrorRequestNotFound);
|
||||
|
|
@ -177,8 +198,21 @@ public static class NpAuthExports
|
|||
|
||||
lock (request)
|
||||
{
|
||||
request.Result = request.Aborted ? NpAuthErrorAborted : NpAuthErrorServiceDown;
|
||||
return ctx.SetReturn(request.Result);
|
||||
if (request.Aborted)
|
||||
{
|
||||
request.Result = NpAuthErrorAborted;
|
||||
return ctx.SetReturn(NpAuthErrorAborted);
|
||||
}
|
||||
|
||||
if (request.Completed)
|
||||
{
|
||||
request.Result = NpAuthErrorInvalidArgument;
|
||||
return ctx.SetReturn(NpAuthErrorInvalidArgument);
|
||||
}
|
||||
|
||||
request.Completed = true;
|
||||
request.Result = NpErrorSignedOut;
|
||||
return ctx.SetReturn(request.Async ? 0 : NpErrorSignedOut);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,26 @@ public static class NpEntitlementAccessExports
|
|||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "y7JD1gfwyfA",
|
||||
ExportName = "sceNpEntitlementAccessGetGameTrialsFlag",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessGetGameTrialsFlag(CpuContext ctx)
|
||||
{
|
||||
var flagAddress = ctx[CpuRegister.Rdi];
|
||||
if (flagAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NpEntitlementAccessErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> flagBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(flagBytes, 0);
|
||||
return ctx.Memory.TryWrite(flagAddress, flagBytes)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TFyU+KFBv54",
|
||||
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public static class NpUniversalDataSystemExports
|
|||
private const int ErrorInvalidArgument = unchecked((int)0x80553102);
|
||||
private const int MaximumGuestStringBytes = 64 * 1024;
|
||||
|
||||
public static void ResetRuntimeState() => NpUniversalDataSystemState.ResetRuntimeState();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sjaobBgqeB4",
|
||||
ExportName = "sceNpUniversalDataSystemInitialize",
|
||||
|
|
|
|||
|
|
@ -64,9 +64,14 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
lock (Gate)
|
||||
{
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
if (!_initialized)
|
||||
{
|
||||
ResetLocked();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
_poolSize = poolSize;
|
||||
UpdateMaximumInUseLocked();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -85,8 +90,7 @@ internal static class NpUniversalDataSystemState
|
|||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
context = 0;
|
||||
return false;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
context = NextPositiveId(ref _nextContext);
|
||||
|
|
@ -480,7 +484,7 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
internal static void ResetRuntimeState()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -488,6 +492,8 @@ internal static class NpUniversalDataSystemState
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private static ulong CreateObjectLocked(UdsObjectNode node)
|
||||
{
|
||||
var handle = ObjectHandleBase | ++_nextObject;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace SharpEmu.Libs.Pad;
|
|||
public static class PadExports
|
||||
{
|
||||
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
|
||||
private const int OrbisPadErrorInvalidArgument = unchecked((int)0x80920001);
|
||||
private const int OrbisPadErrorAlreadyOpened = unchecked((int)0x80920004);
|
||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||
|
|
@ -19,8 +21,10 @@ public static class PadExports
|
|||
// 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 SystemUserId = 0xFF;
|
||||
private const int StandardPortType = 0;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int SpecialPortType = 2;
|
||||
private const int RemotePortType = 16;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
private const float StandardGravity = 9.80665f;
|
||||
|
|
@ -31,11 +35,32 @@ public static class PadExports
|
|||
// user slots and treats every valid handle as a connected controller, so accepting
|
||||
// 0 there reports phantom pads that repeat the primary pad's buttons.
|
||||
private static int _padOpened;
|
||||
private static readonly object PadHandleGate = new();
|
||||
private static readonly Dictionary<PadOpenKey, int> PadHandleByKey = new();
|
||||
private static readonly Dictionary<int, PadOpenKey> PadKeyByHandle = new();
|
||||
private static int _nextPadHandle = 1;
|
||||
|
||||
private static bool IsPrimaryPadHandle(int handle) =>
|
||||
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0);
|
||||
private readonly record struct PadOpenKey(int UserId, int Type, int Index);
|
||||
|
||||
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
|
||||
private static bool IsOpenPadHandle(int handle)
|
||||
{
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
return PadKeyByHandle.ContainsKey(handle) || (handle == 0 && _padOpened == 0);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetOpenedPadForTests()
|
||||
{
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
PadHandleByKey.Clear();
|
||||
PadKeyByHandle.Clear();
|
||||
_nextPadHandle = 1;
|
||||
_padOpened = 0;
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||
|
||||
[ThreadStatic]
|
||||
|
|
@ -45,6 +70,9 @@ public static class PadExports
|
|||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
private static readonly bool LogPadState =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PAD_STATE"), "1", StringComparison.Ordinal);
|
||||
private static long _loggedPadSignature = -1;
|
||||
// Motion data is reported until a title turns it off: Astro Bot reads it for
|
||||
// shake/tilt without ever importing scePadSetMotionSensorState.
|
||||
private static int _motionSensorEnabled = 1;
|
||||
|
|
@ -62,6 +90,18 @@ public static class PadExports
|
|||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "n3kSX62fgNo",
|
||||
ExportName = "scePadUnknownN3kSX62fgNo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadUnknownN3kSX62fgNo(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xk0AcarP3V4",
|
||||
ExportName = "scePadOpen",
|
||||
|
|
@ -76,12 +116,6 @@ public static class PadExports
|
|||
LibraryName = "libScePad")]
|
||||
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||
|
||||
// scePadGetHandle(userId, type, index): returns the handle of an already-open
|
||||
// pad without opening a new one. Dead Cells calls it every frame to poll
|
||||
// input; leaving it unresolved returned a garbage handle so the input path
|
||||
// (and the game loop that drives it) misbehaved. Same validation as
|
||||
// scePadOpen — the one primary pad — returning its handle or a not-connected
|
||||
// error, never opening or logging.
|
||||
[SysAbiExport(
|
||||
Nid = "u1GRHp+oWoY",
|
||||
ExportName = "scePadGetHandle",
|
||||
|
|
@ -97,22 +131,24 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
|
||||
if (userId == -1)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
|
||||
? ctx.SetReturn(handle)
|
||||
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
|
|
@ -123,8 +159,7 @@ public static class PadExports
|
|||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||
if (!IsSupportedOpenTuple(userId, type, index, extended))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
|
@ -138,8 +173,42 @@ public static class PadExports
|
|||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||
}
|
||||
|
||||
Volatile.Write(ref _padOpened, 1);
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
var key = new PadOpenKey(userId, type, index);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
if (PadHandleByKey.ContainsKey(key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorAlreadyOpened);
|
||||
}
|
||||
|
||||
var handle = _nextPadHandle++;
|
||||
PadHandleByKey.Add(key, handle);
|
||||
PadKeyByHandle.Add(handle, key);
|
||||
_padOpened = 1;
|
||||
return ctx.SetReturn(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedOpenTuple(int userId, int type, int index, bool extended)
|
||||
{
|
||||
if (index != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userId == SystemUserId && type == RemotePortType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extended
|
||||
? type is 0 or 1 or 2
|
||||
: type is StandardPortType or SpecialPortType;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -150,9 +219,16 @@ public static class PadExports
|
|||
public static int PadClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
lock (PadHandleGate)
|
||||
{
|
||||
if (!PadKeyByHandle.Remove(handle, out var key))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
PadHandleByKey.Remove(key);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -163,7 +239,7 @@ public static class PadExports
|
|||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -180,7 +256,7 @@ public static class PadExports
|
|||
public static int PadSetTiltCorrectionState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -193,7 +269,7 @@ public static class PadExports
|
|||
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -207,7 +283,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -258,7 +334,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -272,7 +348,7 @@ public static class PadExports
|
|||
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
|
||||
// the class-data union stays zeroed — the guest treats it as a plain
|
||||
// controller with no extended capabilities.
|
||||
Span<byte> information = stackalloc byte[0x20];
|
||||
Span<byte> information = stackalloc byte[0x14];
|
||||
information.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
|
||||
|
||||
|
|
@ -281,6 +357,57 @@ public static class PadExports
|
|||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IHPqcbc0zCA",
|
||||
ExportName = "scePadDeviceClassParseData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadDeviceClassParseData(CpuContext ctx)
|
||||
{
|
||||
const int deviceClassDataSize = 24;
|
||||
const int deviceUniqueDataLengthOffset = 0x6B;
|
||||
const int deviceUniqueDataOffset = 0x6C;
|
||||
const int maximumDeviceUniqueDataLength = 12;
|
||||
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var padDataAddress = ctx[CpuRegister.Rsi];
|
||||
var classDataAddress = ctx[CpuRegister.Rdx];
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (padDataAddress == 0 || classDataAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> padData = stackalloc byte[PadDataSize];
|
||||
if (!ctx.Memory.TryRead(padDataAddress, padData))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Span<byte> classData = stackalloc byte[deviceClassDataSize];
|
||||
classData.Clear();
|
||||
classData[0x04] = padData[0x4C] != 0 ? (byte)1 : (byte)0;
|
||||
|
||||
var uniqueDataLength = Math.Min(
|
||||
(int)padData[deviceUniqueDataLengthOffset],
|
||||
maximumDeviceUniqueDataLength);
|
||||
if (uniqueDataLength > 0)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(classData[0x00..], -1);
|
||||
classData[0x08] = (byte)uniqueDataLength;
|
||||
padData.Slice(deviceUniqueDataOffset, uniqueDataLength)
|
||||
.CopyTo(classData.Slice(0x0C, uniqueDataLength));
|
||||
}
|
||||
|
||||
return ctx.Memory.TryWrite(classDataAddress, classData)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YndgXqQVV7c",
|
||||
ExportName = "scePadReadState",
|
||||
|
|
@ -290,7 +417,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -315,7 +442,7 @@ public static class PadExports
|
|||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -349,7 +476,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -387,7 +514,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -575,7 +702,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -605,7 +732,7 @@ public static class PadExports
|
|||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -634,7 +761,7 @@ public static class PadExports
|
|||
public static int PadResetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (!IsOpenPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -651,7 +778,7 @@ public static class PadExports
|
|||
public static int PadResetOrientation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -664,7 +791,7 @@ public static class PadExports
|
|||
public static int PadIsRemoteController(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return IsOpenPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
|
@ -765,6 +892,17 @@ public static class PadExports
|
|||
buttons |= 0x4000;
|
||||
}
|
||||
|
||||
if (LogPadState)
|
||||
{
|
||||
var signature = ((ulong)buttons << 8) | ((ulong)(acceptsKeyboardInput ? 1 : 0) << 4) | (uint)gamepadCount;
|
||||
if (Interlocked.Exchange(ref _loggedPadSignature, (long)signature) != (long)signature)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] pad state: focused={acceptsKeyboardInput} gamepads={gamepadCount} buttons=0x{buttons:X8} " +
|
||||
$"left={leftX},{leftY} thread={Environment.CurrentManagedThreadId}");
|
||||
}
|
||||
}
|
||||
|
||||
_cachedInputState = new PadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public static class PlayGoExports
|
|||
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
|
||||
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
|
||||
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
|
||||
private const int OrbisPlayGoErrorBadOptionalType = unchecked((int)0x80B20024);
|
||||
private const ulong PlayGoInitBufAddrOffset = 0;
|
||||
private const ulong PlayGoInitBufSizeOffset = 8;
|
||||
private const uint PlayGoMinimumInitBufferSize = 0x200000;
|
||||
|
|
@ -29,6 +30,10 @@ public static class PlayGoExports
|
|||
private const int PlayGoInstallSpeedSuspended = 0;
|
||||
private const int PlayGoInstallSpeedTrickle = 1;
|
||||
private const int PlayGoInstallSpeedFull = 2;
|
||||
private const int PlayGoOptionalTypeLanguage = 0;
|
||||
private const int PlayGoOptionalTypeScenario = 1;
|
||||
private const ulong PlayGoLanguageMaskAll = ulong.MaxValue;
|
||||
private const ulong PlayGoScenarioMaskAll = 0x1f;
|
||||
private const uint MaxPlayGoQueryEntries = 0x4000;
|
||||
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
|
||||
|
||||
|
|
@ -231,7 +236,7 @@ public static class PlayGoExports
|
|||
chunkIds = _metadata.ChunkIds;
|
||||
}
|
||||
|
||||
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||
var availableEntries = (uint)chunkIds.Length;
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||
|
|
@ -249,7 +254,7 @@ public static class PlayGoExports
|
|||
|
||||
for (uint i = 0; i < entriesToWrite; i++)
|
||||
{
|
||||
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||
var chunkId = chunkIds[i];
|
||||
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
|
@ -262,6 +267,91 @@ public static class PlayGoExports
|
|||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8-e7E989rCU",
|
||||
ExportName = "scePlayGoGetInstallChunkId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetInstallChunkId(CpuContext ctx)
|
||||
{
|
||||
return PlayGoGetChunkId(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "g4AZyxpSAlA",
|
||||
ExportName = "scePlayGoGetOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var outOption = ctx[CpuRegister.Rdx];
|
||||
|
||||
var validation = ValidateHandle(handle);
|
||||
if (validation != 0)
|
||||
{
|
||||
return validation;
|
||||
}
|
||||
|
||||
if (outOption == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
var option = type switch
|
||||
{
|
||||
PlayGoOptionalTypeLanguage => PlayGoLanguageMaskAll,
|
||||
PlayGoOptionalTypeScenario => PlayGoScenarioMaskAll,
|
||||
_ => 0UL,
|
||||
};
|
||||
if (type is not PlayGoOptionalTypeLanguage and not PlayGoOptionalTypeScenario)
|
||||
{
|
||||
return OrbisPlayGoErrorBadOptionalType;
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outOption, option)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IfiN+-oeVWI",
|
||||
ExportName = "scePlayGoGetSupportedOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetSupportedOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
return PlayGoGetOptionalChunk(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HVAa744ecdw",
|
||||
ExportName = "scePlayGoPrefetchOptionalChunk",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoPrefetchOptionalChunk(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var option = ctx[CpuRegister.Rdx];
|
||||
|
||||
var validation = ValidateHandle(handle);
|
||||
if (validation != 0)
|
||||
{
|
||||
return validation;
|
||||
}
|
||||
|
||||
if (option == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
return type is PlayGoOptionalTypeLanguage or PlayGoOptionalTypeScenario
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisPlayGoErrorBadOptionalType;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "v6EZ-YWRdMs",
|
||||
ExportName = "scePlayGoGetEta",
|
||||
|
|
@ -685,10 +775,7 @@ public static class PlayGoExports
|
|||
{
|
||||
// No app0 override to probe for sidecar files: same fully-installed
|
||||
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
[(ushort)0],
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
return CreateBaseChunkMetadata(PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
|
|
@ -718,14 +805,27 @@ public static class PlayGoExports
|
|||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
if (chunkIds.Length == 0)
|
||||
{
|
||||
TracePlayGo("metadata_has_no_chunk_definitions; installed chunk ids unknown");
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
Array.Empty<ushort>(),
|
||||
PlayGoChunkIdKnowledge.Unknown);
|
||||
}
|
||||
|
||||
return new PlayGoMetadata(
|
||||
true,
|
||||
chunkIds,
|
||||
chunkIds.Length == 0
|
||||
? PlayGoChunkIdKnowledge.Unknown
|
||||
: PlayGoChunkIdKnowledge.Authoritative);
|
||||
PlayGoChunkIdKnowledge.Authoritative);
|
||||
}
|
||||
|
||||
private static PlayGoMetadata CreateBaseChunkMetadata(PlayGoChunkIdKnowledge knowledge) =>
|
||||
new(
|
||||
true,
|
||||
[(ushort)0],
|
||||
knowledge);
|
||||
|
||||
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
|
||||
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
|
||||
// is the base chunk and must resolve even for a title with no pak files at
|
||||
|
|
@ -831,7 +931,7 @@ public static class PlayGoExports
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
public static void ResetRuntimeState()
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
|
|
@ -846,6 +946,8 @@ public static class PlayGoExports
|
|||
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||
}
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
|
||||
private enum PlayGoChunkIdKnowledge
|
||||
{
|
||||
Unknown,
|
||||
|
|
|
|||
|
|
@ -74,9 +74,11 @@ public static class SaveDataExports
|
|||
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
|
||||
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
|
||||
// SceSaveDataEventType
|
||||
private const uint EventTypeBackupEnd = 2;
|
||||
private const uint EventTypeSaveDataMemorySyncEnd = 3;
|
||||
private const int SaveDataEventSize = 0x60;
|
||||
private const int MountInfoSize = 0x40;
|
||||
private const int SaveDataTitleIdStructSize = 0x10;
|
||||
private const int SaveDataEventSize = 0x68;
|
||||
private const int MountInfoSize = 0x30;
|
||||
private const uint SaveDataBlockSize = 65536;
|
||||
private const ulong SaveDataBlocksMax = 16384;
|
||||
|
||||
|
|
@ -86,16 +88,79 @@ public static class SaveDataExports
|
|||
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
|
||||
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
|
||||
private readonly record struct SaveDataEvent(
|
||||
uint Type,
|
||||
int ErrorCode,
|
||||
int UserId,
|
||||
string TitleId,
|
||||
string DirName);
|
||||
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
|
||||
|
||||
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
|
||||
private static void EnqueueEvent(
|
||||
uint type,
|
||||
int userId,
|
||||
string titleId,
|
||||
string dirName,
|
||||
int errorCode = 0)
|
||||
{
|
||||
lock (_eventGate)
|
||||
{
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
|
||||
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, titleId, dirName));
|
||||
}
|
||||
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
|
||||
TraceSaveData($"event.enqueue type={type} user={userId} title='{titleId}' dir='{dirName}' err=0x{errorCode:X}");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "z1JA8-iJt3k",
|
||||
ExportName = "sceSaveDataBackup",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataBackup(CpuContext ctx)
|
||||
{
|
||||
var backupAddress = ctx[CpuRegister.Rdi];
|
||||
if (backupAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, backupAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(backupAddress + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(backupAddress + 0x10, out var dirNameAddress))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || dirNameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
string titleId;
|
||||
if (titleIdAddress == 0)
|
||||
{
|
||||
titleId = ResolveConfiguredTitleId();
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, titleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(titleId))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
EnqueueEvent(EventTypeBackupEnd, userId, titleId, dirName);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -131,7 +196,14 @@ public static class SaveDataExports
|
|||
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
|
||||
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x10, SaveDataTitleIdStructSize),
|
||||
pending.TitleId,
|
||||
SaveDataTitleIdSize);
|
||||
WriteFixedAscii(
|
||||
ev.Slice(0x20, SaveDataDirNameSize),
|
||||
pending.DirName,
|
||||
SaveDataDirNameSize);
|
||||
if (!ctx.Memory.TryWrite(eventAddress, ev))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -617,6 +689,11 @@ public static class SaveDataExports
|
|||
ExportName = "sceSaveDataDirNameSearchPs4",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
[SysAbiExport(
|
||||
Nid = "PHnuI4LhuRk",
|
||||
ExportName = "sceSaveDataDirNameSearch2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||
{
|
||||
var condAddress = ctx[CpuRegister.Rdi];
|
||||
|
|
@ -1241,6 +1318,19 @@ public static class SaveDataExports
|
|||
}
|
||||
}
|
||||
|
||||
private static void WriteFixedAscii(
|
||||
Span<byte> destination,
|
||||
string value,
|
||||
int maxContentLength)
|
||||
{
|
||||
var count = Math.Min(value.Length, Math.Min(destination.Length, maxContentLength));
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var ch = value[i];
|
||||
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
|
|
@ -1486,7 +1576,11 @@ public static class SaveDataExports
|
|||
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
|
||||
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
|
||||
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
|
||||
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
|
||||
EnqueueEvent(
|
||||
EventTypeSaveDataMemorySyncEnd,
|
||||
userId,
|
||||
ResolveConfiguredTitleId(),
|
||||
"sce_sdmemory");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Share;
|
||||
|
||||
public static class SharePlayExports
|
||||
{
|
||||
private static int _controllerProhibited;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FzQS6DREDfk",
|
||||
ExportName = "sceSharePlayProhibitController",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSharePlay")]
|
||||
public static int SharePlayProhibitController(CpuContext ctx)
|
||||
{
|
||||
var prohibited = ctx[CpuRegister.Rdi];
|
||||
if (prohibited > 1)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _controllerProhibited, unchecked((int)prohibited));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
public static void ResetRuntimeState() => Volatile.Write(ref _controllerProhibited, 0);
|
||||
|
||||
internal static bool IsControllerProhibited => Volatile.Read(ref _controllerProhibited) != 0;
|
||||
|
||||
internal static void ResetForTests() => ResetRuntimeState();
|
||||
}
|
||||
|
|
@ -64,14 +64,6 @@ public static class GameServiceStubs
|
|||
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
|
||||
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "jqb7HntFQFc", ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "ocHtyBwHfys", ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "mlYGfmqE3fQ", ExportName = "sceSigninDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
|
||||
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
|
@ -114,6 +106,14 @@ public static class GameServiceStubs
|
|||
public static int TextToSpeech2Open(CpuContext ctx) =>
|
||||
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
|
||||
[SysAbiExport(Nid = "2jiIxUmcsGo", ExportName = "sceTextToSpeech2Cancel",
|
||||
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
|
||||
public static int TextToSpeech2Cancel(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "08JSg9p6bgQ", ExportName = "sceTextToSpeech2GetSpeechStatus",
|
||||
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
|
||||
public static int TextToSpeech2GetSpeechStatus(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")]
|
||||
public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue