Compare commits
2 Commits
1016_part3
...
xnetcat/ho
| Author | SHA1 | Date |
|---|---|---|
|
|
c284720e39 | |
|
|
ad92a314ae |
|
|
@ -0,0 +1,112 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Rendering resolution
|
||||
|
||||
Options → Rendering exposes three 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.
|
||||
- **Internal resolution scale** multiplies the host size of the game's
|
||||
screen-sized render targets. 100% is native and changes nothing.
|
||||
|
||||
The CLI equivalents are `--resolution=1280x720`,
|
||||
`--guest-resolution=1920x1080` (or `3840x2160`) and
|
||||
`--render-scale=0.5`. `SHARPEMU_RENDER_SCALE=0.5` does the same for scripted
|
||||
runs and overrides the stored setting. The accepted range is 0.25 to 2,
|
||||
rounded to a hundredth.
|
||||
|
||||
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. Changing the window alone
|
||||
does not reduce the work the game renders; the internal resolution scale does.
|
||||
|
||||
## How the scale is applied
|
||||
|
||||
Guest memory layouts never change. A scaled image keeps its guest geometry in
|
||||
its description - extent, pitch, tile mode, mip layout - and only its host
|
||||
backing is created at `ceil(guest × scale)`. Everything that would notice:
|
||||
|
||||
- **Rasterization.** The render area, the viewport and the scissor are scaled
|
||||
with the attachments. Scissor edges round outward so no guest pixel loses its
|
||||
host coverage. With clipping disabled the vertex program bakes the viewport
|
||||
transform against a reference extent that is divided by an upscale, so the
|
||||
same program maps guest pixels onto host positions at either resolution.
|
||||
- **Shaders.** `gl_FragCoord` is divided by the attachments' factor as it is
|
||||
loaded, so the pixel position inputs and everything derived from them - screen
|
||||
UVs above all - stay in guest pixels. Integer image coordinates are mapped
|
||||
onto host texels through the bound image's factor, and resource-info queries
|
||||
report guest sizes. Dynamic sample offsets are converted from guest texels
|
||||
to host texels before normalization. The factors and a mask of scaled image resources travel in
|
||||
shader data, so no shader is specialized per scale.
|
||||
- **Guest transfers.** An upload fills a guest-resolution twin of the image from
|
||||
the copy regions the tiler built and blits into the backing; a download blits
|
||||
the backing into the twin and copies out of it. A copy between two images that
|
||||
disagree on host resolution runs at guest resolution the same way.
|
||||
- **Presentation.** The flip path already works from the host extent of the
|
||||
captured image, so a scaled flip buffer still fills the window.
|
||||
|
||||
## What scales and what does not
|
||||
|
||||
Only screen-class, single-level, single-sample, uncompressed 2D render and depth
|
||||
targets scale: width ≥ 1280, height ≥ 720, not square, render or depth tiling,
|
||||
guest-placed, in a format the device can blit. The decision is a pure function
|
||||
of the guest image description, so every view, alias and copy of one guest range
|
||||
agrees on it.
|
||||
|
||||
Everything else keeps its guest resolution:
|
||||
|
||||
- block-compressed images, multisample footprints, mip chains, cube and array
|
||||
faces, 3D images;
|
||||
- depth targets with a stencil plane, because Vulkan cannot blit stencil and the
|
||||
guest-resolution twin could not be filled;
|
||||
- HTile, which encodes guest depth blocks the renderer reads back as clear
|
||||
state. DCC only describes guest bytes the host never stores, so it does scale;
|
||||
- asset tilings (linear, standard), shadow maps, lookup tables, probe atlases
|
||||
and eye-adaptation readbacks, which are below the size threshold or square;
|
||||
- storage images the moment a compute program writes one. The guest dispatch
|
||||
grid does not shrink with the host image, so a scaled image would be written
|
||||
with holes above 100% and redundantly below. The guest range is remembered and
|
||||
every later image over that memory stays native;
|
||||
- every attachment of a pass whose attachments disagree, which happens when one
|
||||
of them falls into a case above. The draw drops the scaled attachments back to
|
||||
guest resolution and resolves its targets again.
|
||||
|
||||
Only the Vulkan backend scales; the Metal backend ignores the setting.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **Compute-written surfaces never scale.** In a deferred renderer that is a
|
||||
large part of the frame, so the saving is smaller than the scale suggests.
|
||||
- **Depth with stencil never scales**, and it takes its pass with it.
|
||||
- **Tone and screen-space effects shift.** Measured on Silent Hill: The Short
|
||||
Message, the title scene is correct in composition, coverage and UI placement
|
||||
at 50% and 200%, but slightly brighter than native at both, and at 50% the
|
||||
character's coat picks up colour speckle. Effects that reason about texels
|
||||
rather than about the screen - screen-space shadows, subsurface scattering,
|
||||
dithering - change their footprint with the host resolution.
|
||||
- **Guest stencil uploads and downloads** of a scaled image would lose the
|
||||
stencil plane; this cannot happen today only because stencil images never
|
||||
scale.
|
||||
|
||||
## Measured effect
|
||||
|
||||
Silent Hill: The Short Message at the title menu, 1080p display mode, GPU time
|
||||
per draw scope (averaged over a whole run):
|
||||
|
||||
| scope | 50% | 100% | 200% |
|
||||
|---|---|---|---|
|
||||
| `vs=CBE40391 ps=E0D7BA42` | 1.68 ms | 7.60 ms | 41.81 ms |
|
||||
| `vs=9A9C9C47 ps=B2FD2C67` | 0.71 ms | 2.44 ms | 10.52 ms |
|
||||
| `vs=9A9C9C47 ps=E5A1AB7F` | 0.78 ms | 2.35 ms | 8.27 ms |
|
||||
|
||||
Pixel work follows the area, as it should. Frame rate does not: this title is
|
||||
bound by guest command translation and compute, not by pixels, and stays at
|
||||
roughly 2.5 fps at every scale. Dreaming Sarah holds 60 fps at 50% with one
|
||||
scaled target.
|
||||
|
|
@ -9,6 +9,7 @@ using SharpEmu.Libs.VideoOut;
|
|||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ 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)
|
||||
|
|
@ -1010,7 +1012,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>] [--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>] [--guest-resolution=<1920x1080|3840x2160>] [--render-scale=<0.5|0.75|1|1.5|2>] [--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.");
|
||||
}
|
||||
|
|
@ -1075,6 +1077,8 @@ internal static partial class Program
|
|||
var systemLanguage = 1;
|
||||
HostWindowMode? windowModeOverride = null;
|
||||
HostScalingMode? scalingModeOverride = null;
|
||||
GuestDisplayResolution? guestResolutionOverride = null;
|
||||
float? renderScaleOverride = null;
|
||||
int? windowWidthOverride = null;
|
||||
int? windowHeightOverride = null;
|
||||
int? displayIndexOverride = null;
|
||||
|
|
@ -1134,6 +1138,31 @@ 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, "--render-scale", out var renderScaleText))
|
||||
{
|
||||
if (!float.TryParse(renderScaleText, NumberStyles.Float, CultureInfo.InvariantCulture, out var renderScale) ||
|
||||
!float.IsFinite(renderScale) ||
|
||||
renderScale < HostVideoOptions.MinimumRenderScale || renderScale > HostVideoOptions.MaximumRenderScale)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
renderScaleOverride = renderScale;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--display", out var displayText))
|
||||
{
|
||||
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
|
||||
|
|
@ -1391,6 +1420,8 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
|
||||
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
|
||||
GuestResolution = guestResolutionOverride ?? configuredVideoOptions.GuestResolution,
|
||||
RenderScale = renderScaleOverride ?? configuredVideoOptions.RenderScale,
|
||||
Width = windowWidthOverride ?? configuredVideoOptions.Width,
|
||||
Height = windowHeightOverride ?? configuredVideoOptions.Height,
|
||||
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
|
||||
|
|
@ -1431,6 +1462,9 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowMode,
|
||||
ScalingMode = scalingMode,
|
||||
GuestResolution = effective.GuestResolution == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd,
|
||||
RenderScale = effective.RenderScale,
|
||||
Width = hasResolution ? configuredWidth : defaults.Width,
|
||||
Height = hasResolution ? configuredHeight : defaults.Height,
|
||||
DisplayIndex = effective.DisplayIndex,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
// 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,23 +261,40 @@ 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>(40);
|
||||
var code = new List<byte>(48);
|
||||
var slowPathFixups = new List<int>();
|
||||
// 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]);
|
||||
var slowPathFixup = code.Count - 1;
|
||||
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);
|
||||
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
|
||||
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
|
||||
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
|
||||
foreach (var fixup in slowPathFixups)
|
||||
{
|
||||
code[fixup] = checked((byte)(code.Count - (fixup + 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,10 +1925,6 @@ 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;
|
||||
|
|
@ -3982,6 +3978,11 @@ 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,
|
||||
|
|
@ -4016,6 +4017,12 @@ 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;
|
||||
|
|
@ -4029,15 +4036,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
existing.Context = context;
|
||||
Volatile.Write(ref existing.HostThreadId, hostThreadId);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
}
|
||||
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
_registeredExternalHandle = threadHandle;
|
||||
_registeredExternalContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5375,7 +5386,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryWriteGuestExceptionContext(
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
GuestCpuContinuation continuation,
|
||||
|
|
@ -5386,7 +5397,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
|
||||
|
||||
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
|
||||
const int mcontext = 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;
|
||||
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]);
|
||||
|
|
@ -5405,10 +5420,9 @@ 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 : context.Rflags);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
|
||||
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);
|
||||
|
|
@ -6383,19 +6397,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
const uint stubSize = 512u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (ptr == null)
|
||||
// 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))
|
||||
{
|
||||
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;
|
||||
|
|
@ -6469,8 +6479,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
reason = $"failed to patch guest continuation return slot at 0x{returnSlotAddress:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
if (!ProtectContinuationStub(ptr, stubSize, executable: true))
|
||||
{
|
||||
reason = "failed to seal guest continuation stub execute-read";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
|
|
@ -6530,11 +6539,85 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
NativeMemory.Free(hostRspStorage);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
_continuationDepth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
[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,20 +322,43 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||
var usleep = ResolveLibcExport("usleep");
|
||||
// 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");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 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, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
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, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
|
|
|
|||
|
|
@ -19,44 +19,6 @@ 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
|
||||
{
|
||||
|
|
@ -65,7 +27,6 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetAfter,
|
||||
StackAfter,
|
||||
TooShort,
|
||||
IndirectBranch,
|
||||
}
|
||||
|
||||
public static PatchResult Patch(
|
||||
|
|
@ -101,19 +62,8 @@ 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,
|
||||
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
|
||||
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
|
||||
if (sites.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -144,10 +94,8 @@ 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.
|
||||
|
|
@ -180,7 +128,6 @@ internal static class GuestRedZonePatcher
|
|||
BranchTargetRefusals = scan.BranchTargetRefusals,
|
||||
StackRefusals = scan.StackRefusals,
|
||||
TooShortRefusals = scan.TooShortRefusals,
|
||||
IndirectBranchRefusals = scan.IndirectBranchRefusals,
|
||||
TrampolineBytes = trampolineCursor - trampolineBase,
|
||||
};
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -188,8 +135,7 @@ 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} " +
|
||||
$"indirect_branch={result.IndirectBranchRefusals}) " +
|
||||
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
|
||||
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
|
||||
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
|
||||
return result;
|
||||
|
|
@ -203,17 +149,15 @@ internal static class GuestRedZonePatcher
|
|||
bool protectRedZone,
|
||||
bool splitVectorStores,
|
||||
bool rewriteSha,
|
||||
out PatchResult result,
|
||||
Progress? progress = null)
|
||||
out PatchResult result)
|
||||
{
|
||||
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[6];
|
||||
var refusalCounts = new int[5];
|
||||
var shaInstructionCount = 0;
|
||||
|
||||
foreach (var header in programHeaders)
|
||||
|
|
@ -246,7 +190,6 @@ 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]
|
||||
|
|
@ -279,14 +222,6 @@ internal static class GuestRedZonePatcher
|
|||
redZoneFunctionCount++;
|
||||
}
|
||||
var branchTargets = CollectBranchTargets(decoded);
|
||||
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
|
||||
memory,
|
||||
programHeaders,
|
||||
imageBase,
|
||||
decoded,
|
||||
functionStart,
|
||||
functionEnd,
|
||||
branchTargets);
|
||||
|
||||
var lastSiteEnd = 0UL;
|
||||
|
||||
|
|
@ -301,29 +236,24 @@ internal static class GuestRedZonePatcher
|
|||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
// 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))
|
||||
{
|
||||
unrelocatableSites++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
|
||||
}
|
||||
|
||||
if (span.Address < lastSiteEnd)
|
||||
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)SpanRefusal.TooShort]++;
|
||||
refusalCounts[(int)refusal]++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -338,6 +268,8 @@ 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;
|
||||
|
|
@ -358,7 +290,6 @@ 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;
|
||||
|
|
@ -416,321 +347,6 @@ 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
|
||||
|
|
@ -760,7 +376,7 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
if (siteIndex < 0 ||
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
|
||||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -801,10 +417,7 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
var hasIndirectBranch = decoded.Any(static entry =>
|
||||
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
|
||||
if (hasIndirectBranch ||
|
||||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -938,13 +551,12 @@ internal static class GuestRedZonePatcher
|
|||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
out PatchSite site) =>
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
|
||||
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
|
||||
|
||||
private static bool TryBuildPatchSpan(
|
||||
IReadOnlyList<DecodedInstruction> decoded,
|
||||
int startIndex,
|
||||
IReadOnlySet<ulong> branchTargets,
|
||||
bool canBorrowFollowingInstructions,
|
||||
out PatchSite site,
|
||||
out SpanRefusal refusal)
|
||||
{
|
||||
|
|
@ -956,7 +568,6 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (index != startIndex &&
|
||||
canBorrowFollowingInstructions &&
|
||||
!branchTargets.Contains(instruction.IP) &&
|
||||
IsRelocatableDirectBranch(instruction))
|
||||
{
|
||||
|
|
@ -967,11 +578,7 @@ internal static class GuestRedZonePatcher
|
|||
break;
|
||||
}
|
||||
|
||||
if (index != startIndex && !canBorrowFollowingInstructions)
|
||||
{
|
||||
refusal = SpanRefusal.IndirectBranch;
|
||||
}
|
||||
else if (instruction.FlowControl != FlowControl.Next)
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
}
|
||||
|
|
@ -1482,8 +1089,6 @@ internal static class GuestRedZonePatcher
|
|||
|
||||
public int TooShortRefusals { get; init; }
|
||||
|
||||
public int IndirectBranchRefusals { get; init; }
|
||||
|
||||
public ulong TrampolineBytes { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ 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,6 +70,28 @@ 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";
|
||||
|
||||
// Host resolution multiplier of the render targets the renderer is allowed to scale.
|
||||
public float RenderScale { get; set; } = 1.0f;
|
||||
|
||||
internal static float NormalizeRenderScale(float value) =>
|
||||
SharpEmu.Libs.VideoOut.HostVideoOptions.NormalizeRenderScale(value);
|
||||
|
||||
internal static readonly string[] RenderScaleChoices = ["50%", "75%", "100%", "125%", "150%", "200%"];
|
||||
|
||||
internal static string RenderScaleText(float value) =>
|
||||
((int)MathF.Round(NormalizeRenderScale(value) * 100)).ToString(System.Globalization.CultureInfo.InvariantCulture) + "%";
|
||||
|
||||
internal static float ParseRenderScale(string? text) =>
|
||||
int.TryParse(text?.TrimEnd('%'), System.Globalization.NumberStyles.Integer,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var percent)
|
||||
? NormalizeRenderScale(percent / 100.0f)
|
||||
: 1.0f;
|
||||
|
||||
public int DisplayIndex { get; set; }
|
||||
|
||||
public int RefreshRate { get; set; }
|
||||
|
|
@ -89,9 +111,6 @@ 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
|
||||
|
|
@ -184,6 +203,8 @@ public sealed class GuiSettings
|
|||
"Ukrainian");
|
||||
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
||||
settings.Resolution = NormalizeResolution(settings.Resolution);
|
||||
settings.GuestResolution = NormalizeGuestResolution(settings.GuestResolution);
|
||||
settings.RenderScale = NormalizeRenderScale(settings.RenderScale);
|
||||
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,6 +219,10 @@
|
|||
"Options.WindowMode.Exclusive": "حصري",
|
||||
"Options.Resolution.Label": "الدقة",
|
||||
"Options.Resolution.Desc": "حجم النافذة الأولي أو دقة ملء الشاشة الحصرية.",
|
||||
"Options.GuestResolution.Label": "وضع عرض اللعبة",
|
||||
"Options.GuestResolution.Desc": "دقة العرض المُبلّغ عنها للعبة. الافتراضي 1080p. تختار الألعاب دقة التصيير الداخلية؛ وقد تتجاهل الألعاب ذات الدقة الثابتة هذا الإعداد.",
|
||||
"Options.RenderScale.Label": "مقياس الدقة الداخلية",
|
||||
"Options.RenderScale.Desc": "مضاعف دقة المضيف المطبّق على أهداف التصيير بحجم الشاشة في اللعبة. أقل من 100% يقايض جودة الصورة بالسرعة، وأكثر من 100% يفعل العكس. تبقى خرائط الظلال وجداول البحث والمواد المضغوطة والأسطح التي تكتبها مظللات الحوسبة بحجمها الأصلي دائمًا.",
|
||||
"Options.Display.Label": "الشاشة",
|
||||
"Options.Display.Desc": "الشاشة المستخدمة للتوسيط وملء الشاشة.",
|
||||
"Options.RefreshRate.Label": "معدل التحديث",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@
|
|||
"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.RenderScale.Label": "Daxili təsvir ölçüsü əmsalı",
|
||||
"Options.RenderScale.Desc": "Oyunun ekran ölçüsündəki render hədəflərinə tətbiq olunan host təsvir ölçüsü vuruğu. 100%-dən aşağı təsvir keyfiyyətini sürətə dəyişir, yuxarı isə əksinə. Kölgə xəritələri, axtarış cədvəlləri, sıxılmış teksturlar və hesablama şeyderlərinin yazdığı səthlər həmişə öz ölçüsündə qalı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,6 +220,10 @@
|
|||
"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.RenderScale.Label": "Escala de resolução interna",
|
||||
"Options.RenderScale.Desc": "Multiplicador da resolução do host aplicado aos alvos de renderização do tamanho da tela. Abaixo de 100% troca qualidade de imagem por velocidade; acima de 100% faz o contrário. Mapas de sombra, tabelas de consulta, texturas comprimidas e superfícies escritas por shaders de computação sempre mantêm o tamanho nativo.",
|
||||
"Options.Display.Label": "Tela",
|
||||
"Options.Display.Desc": "Monitor usado para centralização e tela cheia.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@
|
|||
"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.RenderScale.Label": "Interne Auflösungsskalierung",
|
||||
"Options.RenderScale.Desc": "Host-Auflösungsfaktor für die bildschirmgroßen Renderziele des Spiels. Unter 100 % wird Bildqualität gegen Geschwindigkeit getauscht, über 100 % umgekehrt. Shadow Maps, Lookup-Tabellen, komprimierte Texturen und von Compute-Shadern beschriebene Oberflächen behalten immer ihre native Größe.",
|
||||
"Options.Display.Label": "Anzeige",
|
||||
"Options.Display.Desc": "Monitor für Zentrierung und Vollbilddarstellung.",
|
||||
"Options.RefreshRate.Label": "Bildwiederholrate",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@
|
|||
"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.RenderScale.Label": "Intern opløsningsskalering",
|
||||
"Options.RenderScale.Desc": "Værtsopløsningsfaktor for spillets skærmstore rendermål. Under 100 % byttes billedkvalitet for hastighed; over 100 % omvendt. Skyggekort, opslagstabeller, komprimerede teksturer og overflader skrevet af compute-shaders beholder altid deres oprindelige størrelse.",
|
||||
"Options.Display.Label": "Skærm",
|
||||
"Options.Display.Desc": "Skærm, der bruges til centrering og fuldskærm.",
|
||||
"Options.RefreshRate.Label": "Opdateringshastighed",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
"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.RenderScale.Label": "Sisemise eraldusvõime skaala",
|
||||
"Options.RenderScale.Desc": "Hosti eraldusvõime kordaja mängu ekraanisuuruste renderdussihtide jaoks. Alla 100% vahetab pildikvaliteedi kiiruse vastu, üle 100% vastupidi. Varjukaardid, otsingutabelid, pakitud tekstuurid ja arvutusvarjundajate kirjutatud pinnad jäävad alati oma algsesse suurusesse.",
|
||||
"Options.Display.Label": "Kuvar",
|
||||
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
|
||||
"Options.RefreshRate.Label": "Värskendussagedus",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
"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.RenderScale.Label": "Internal resolution scale",
|
||||
"Options.RenderScale.Desc": "Host resolution multiplier applied to the game's screen-sized render targets. Below 100% trades image quality for speed; above 100% does the opposite. Shadow maps, lookup tables, compressed textures and compute-written surfaces always stay at their native size.",
|
||||
"Options.Display.Label": "Display",
|
||||
"Options.Display.Desc": "Monitor used for centering and fullscreen.",
|
||||
"Options.RefreshRate.Label": "Refresh rate",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"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.RenderScale.Label": "Escala de resolución interna",
|
||||
"Options.RenderScale.Desc": "Multiplicador de resolución del anfitrión aplicado a los destinos de renderizado del tamaño de la pantalla. Por debajo del 100 % se cambia calidad de imagen por velocidad; por encima, lo contrario. Los mapas de sombras, las tablas de consulta, las texturas comprimidas y las superficies escritas por sombreadores de cómputo conservan siempre su tamaño nativo.",
|
||||
"Options.Display.Label": "Pantalla",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar y mostrar a pantalla completa.",
|
||||
"Options.RefreshRate.Label": "Frecuencia de actualización",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@
|
|||
"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.RenderScale.Label": "Échelle de résolution interne",
|
||||
"Options.RenderScale.Desc": "Multiplicateur de résolution hôte appliqué aux cibles de rendu plein écran du jeu. En dessous de 100 %, la qualité d’image est échangée contre de la vitesse ; au-dessus, l’inverse. Les cartes d’ombres, les tables de correspondance, les textures compressées et les surfaces écrites par les shaders de calcul gardent toujours leur taille native.",
|
||||
"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,6 +220,10 @@
|
|||
"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.RenderScale.Label": "Belső felbontás skálázása",
|
||||
"Options.RenderScale.Desc": "A gazdagép felbontásszorzója a játék képernyőméretű rendercéljaira. 100% alatt a képminőséget cseréli sebességre, 100% fölött fordítva. Az árnyéktérképek, a keresőtáblák, a tömörített textúrák és a compute shaderek által írt felületek mindig natív méretükben maradnak.",
|
||||
"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,6 +224,10 @@
|
|||
"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.RenderScale.Label": "Scala della risoluzione interna",
|
||||
"Options.RenderScale.Desc": "Moltiplicatore della risoluzione host applicato ai target di rendering a schermo intero del gioco. Sotto il 100% si scambia qualità d'immagine con velocità; sopra il 100% avviene il contrario. Shadow map, tabelle di consultazione, texture compresse e superfici scritte dai compute shader restano sempre alla dimensione nativa.",
|
||||
"Options.Display.Label": "Schermo",
|
||||
"Options.Display.Desc": "Monitor utilizzato per il centraggio e lo schermo intero.",
|
||||
"Options.RefreshRate.Label": "Frequenza di aggiornamento",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@
|
|||
"Options.WindowMode.Exclusive": "排他",
|
||||
"Options.Resolution.Label": "解像度",
|
||||
"Options.Resolution.Desc": "初期ウィンドウサイズまたは排他フルスクリーンの解像度。",
|
||||
"Options.GuestResolution.Label": "ゲームの表示モード",
|
||||
"Options.GuestResolution.Desc": "ゲームに通知する画面解像度。既定値は1080pです。内部描画解像度はゲームが決定するため、解像度が固定されたゲームではこの設定が反映されない場合があります。",
|
||||
"Options.RenderScale.Label": "内部解像度スケール",
|
||||
"Options.RenderScale.Desc": "ゲームの画面サイズのレンダーターゲットに適用するホスト解像度の倍率です。100%未満は画質と引き換えに速度を得て、100%超はその逆になります。シャドウマップ、ルックアップテーブル、圧縮テクスチャ、コンピュートシェーダーが書き込むサーフェスは常に元のサイズのままです。",
|
||||
"Options.Display.Label": "ディスプレイ",
|
||||
"Options.Display.Desc": "中央配置とフルスクリーンに使用するモニター。",
|
||||
"Options.RefreshRate.Label": "リフレッシュレート",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@
|
|||
"Options.WindowMode.Exclusive": "독점",
|
||||
"Options.Resolution.Label": "해상도",
|
||||
"Options.Resolution.Desc": "초기 창 크기 또는 독점 전체 화면 해상도.",
|
||||
"Options.GuestResolution.Label": "게임 디스플레이 모드",
|
||||
"Options.GuestResolution.Desc": "게임에 전달하는 화면 해상도입니다. 기본값은 1080p입니다. 내부 렌더링 해상도는 게임이 결정하며, 해상도가 고정된 게임은 이 설정을 무시할 수 있습니다.",
|
||||
"Options.RenderScale.Label": "내부 해상도 배율",
|
||||
"Options.RenderScale.Desc": "게임의 화면 크기 렌더 타깃에 적용되는 호스트 해상도 배율입니다. 100% 미만은 화질을 속도와 맞바꾸고 100% 초과는 그 반대입니다. 섀도 맵, 룩업 테이블, 압축 텍스처, 컴퓨트 셰이더가 쓰는 표면은 항상 원래 크기를 유지합니다.",
|
||||
"Options.Display.Label": "디스플레이",
|
||||
"Options.Display.Desc": "가운데 배치와 전체 화면에 사용할 모니터.",
|
||||
"Options.RefreshRate.Label": "새로 고침 빈도",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@
|
|||
"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.RenderScale.Label": "Interne resolutieschaal",
|
||||
"Options.RenderScale.Desc": "Hostresolutievermenigvuldiger voor de schermgrote rendertargets van het spel. Onder 100% ruil je beeldkwaliteit in voor snelheid; boven 100% andersom. Schaduwkaarten, opzoektabellen, gecomprimeerde texturen en door compute-shaders beschreven oppervlakken behouden altijd hun oorspronkelijke grootte.",
|
||||
"Options.Display.Label": "Beeldscherm",
|
||||
"Options.Display.Desc": "Monitor die wordt gebruikt voor centrering en volledig scherm.",
|
||||
"Options.RefreshRate.Label": "Verversingssnelheid",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
"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.RenderScale.Label": "Skala rozdzielczości wewnętrznej",
|
||||
"Options.RenderScale.Desc": "Mnożnik rozdzielczości hosta dla pełnoekranowych buforów renderowania gry. Poniżej 100% oddaje jakość obrazu za wydajność; powyżej 100% odwrotnie. Mapy cieni, tablice LUT, tekstury skompresowane i powierzchnie zapisywane przez shadery obliczeniowe zawsze zachowują natywny rozmiar.",
|
||||
"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,6 +220,10 @@
|
|||
"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.RenderScale.Label": "Escala de resolução interna",
|
||||
"Options.RenderScale.Desc": "Multiplicador da resolução do anfitrião aplicado aos alvos de renderização do tamanho do ecrã. Abaixo de 100% troca qualidade de imagem por velocidade; acima de 100% faz o contrário. Mapas de sombras, tabelas de consulta, texturas comprimidas e superfícies escritas por sombreadores de computação mantêm sempre o tamanho nativo.",
|
||||
"Options.Display.Label": "Ecrã",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar e apresentar em ecrã inteiro.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@
|
|||
"Options.WindowMode.Exclusive": "Эксклюзивный",
|
||||
"Options.Resolution.Label": "Разрешение",
|
||||
"Options.Resolution.Desc": "Начальный размер окна или разрешение эксклюзивного полноэкранного режима.",
|
||||
"Options.GuestResolution.Label": "Режим экрана игры",
|
||||
"Options.GuestResolution.Desc": "Разрешение экрана, сообщаемое игре. По умолчанию 1080p. Игра выбирает внутреннее разрешение рендеринга; игры с фиксированным разрешением могут игнорировать эту настройку.",
|
||||
"Options.RenderScale.Label": "Масштаб внутреннего разрешения",
|
||||
"Options.RenderScale.Desc": "Множитель разрешения хоста для полноэкранных целей рендеринга игры. Ниже 100% качество изображения обменивается на скорость, выше 100% — наоборот. Карты теней, таблицы поиска, сжатые текстуры и поверхности, записываемые вычислительными шейдерами, всегда сохраняют исходный размер.",
|
||||
"Options.Display.Label": "Монитор",
|
||||
"Options.Display.Desc": "Монитор, используемый для центрирования окна и полноэкранного режима.",
|
||||
"Options.RefreshRate.Label": "Частота обновления",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@
|
|||
"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.RenderScale.Label": "Dahili çözünürlük ölçeği",
|
||||
"Options.RenderScale.Desc": "Oyunun ekran boyutundaki işleme hedeflerine uygulanan ana makine çözünürlük çarpanı. %100'ün altında görüntü kalitesi hıza takas edilir, üzerinde ise tersi olur. Gölge haritaları, arama tabloları, sıkıştırılmış dokular ve hesaplama gölgelendiricilerinin yazdığı yüzeyler her zaman kendi boyutlarında kalır.",
|
||||
"Options.Display.Label": "Ekran",
|
||||
"Options.Display.Desc": "Ortalama ve tam ekran için kullanılan monitör.",
|
||||
"Options.RefreshRate.Label": "Yenileme hızı",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public partial class MainWindow
|
|||
GameLogLevelBox.ItemsSource = _logLevelChoices;
|
||||
GameWindowModeBox.ItemsSource = _windowModeChoices;
|
||||
GameScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GameGuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
GameRenderScaleBox.ItemsSource = GuiSettings.RenderScaleChoices;
|
||||
GameHdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
GameOverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
GameOverlayCornerBox.ItemsSource = _overlayCornerChoices;
|
||||
|
|
@ -115,6 +117,8 @@ public partial class MainWindow
|
|||
GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameGuestResolutionBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameRenderScaleBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayEnabledToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
|
|
@ -216,6 +220,8 @@ public partial class MainWindow
|
|||
_scalingModeChoices,
|
||||
effective.ScalingMode,
|
||||
"Fit");
|
||||
GameGuestResolutionBox.SelectedItem = effective.GuestResolution;
|
||||
GameRenderScaleBox.SelectedItem = GuiSettings.RenderScaleText(effective.RenderScale);
|
||||
GameVSyncToggle.IsChecked = effective.VSync;
|
||||
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
|
||||
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
|
||||
|
|
@ -277,6 +283,8 @@ public partial class MainWindow
|
|||
LogToFile = GameLogToFileToggle.IsChecked == true,
|
||||
WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"),
|
||||
Resolution = SelectedComboText(GameResolutionBox, "1920x1080"),
|
||||
GuestResolution = SelectedComboText(GameGuestResolutionBox, "1920x1080"),
|
||||
RenderScale = GuiSettings.ParseRenderScale(SelectedComboText(GameRenderScaleBox, "100%")),
|
||||
DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display
|
||||
? display.Index
|
||||
: 0,
|
||||
|
|
|
|||
|
|
@ -856,6 +856,22 @@ 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="GameRenderScaleRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.RenderScale.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.RenderScale.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GameRenderScaleBox"
|
||||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GameDisplayRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.Display.Label],
|
||||
|
|
@ -1490,6 +1506,18 @@ 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="RenderScaleRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.RenderScale.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.RenderScale.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="RenderScaleBox" 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,6 +322,9 @@ 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");
|
||||
RenderScaleBox.SelectionChanged += (_, _) =>
|
||||
_settings.RenderScale = GuiSettings.ParseRenderScale(SelectedComboText(RenderScaleBox, "100%"));
|
||||
RefreshRateBox.SelectionChanged += (_, _) => OnHostRefreshRateChanged();
|
||||
ScalingModeBox.SelectionChanged += (_, _) => _settings.ScalingMode = SelectedComboText(ScalingModeBox, "Fit");
|
||||
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
|
||||
|
|
@ -1249,6 +1252,8 @@ public partial class MainWindow : Window
|
|||
LogLevelBox.ItemsSource = _logLevelChoices;
|
||||
WindowModeBox.ItemsSource = _windowModeChoices;
|
||||
ScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
RenderScaleBox.ItemsSource = GuiSettings.RenderScaleChoices;
|
||||
HdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
BinkPlaybackBox.ItemsSource = _binkPlaybackChoices;
|
||||
OverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
|
|
@ -1324,6 +1329,8 @@ 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);
|
||||
RenderScaleBox.SelectedItem = GuiSettings.RenderScaleText(_settings.RenderScale);
|
||||
HdrModeBox.SelectedIndex = ChoiceIndex(_settings.HdrMode, "Auto", "On", "Off");
|
||||
OverlayEnabledToggle.IsChecked = _settings.OverlayEnabled;
|
||||
OverlayModeBox.SelectedIndex = ChoiceIndex(_settings.OverlayMode, "Full", "Minimal", "TitleBar");
|
||||
|
|
@ -2560,13 +2567,6 @@ 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))
|
||||
|
|
@ -2784,6 +2784,9 @@ 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(
|
||||
$"--render-scale={launch.Settings.RenderScale.ToString(System.Globalization.CultureInfo.InvariantCulture)}");
|
||||
arguments.Add($"--display={launch.Settings.DisplayIndex}");
|
||||
arguments.Add($"--refresh-rate={launch.Settings.RefreshRate}");
|
||||
arguments.Add($"--scaling={launch.Settings.ScalingMode.ToLowerInvariant()}");
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ public sealed class PerGameSettings
|
|||
|
||||
public string? Resolution { get; set; }
|
||||
|
||||
public string? GuestResolution { get; set; }
|
||||
|
||||
public float? RenderScale { get; set; }
|
||||
|
||||
public int? DisplayIndex { get; set; }
|
||||
|
||||
public int? RefreshRate { get; set; }
|
||||
|
|
@ -52,6 +56,8 @@ public sealed class PerGameSettings
|
|||
LogToFile is null &&
|
||||
WindowMode is null &&
|
||||
Resolution is null &&
|
||||
GuestResolution is null &&
|
||||
RenderScale is null &&
|
||||
DisplayIndex is null &&
|
||||
RefreshRate is null &&
|
||||
ScalingMode is null &&
|
||||
|
|
@ -145,6 +151,17 @@ public sealed class PerGameSettings
|
|||
Resolution = null;
|
||||
}
|
||||
|
||||
if (string.Equals(GuestResolution, global.GuestResolution, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
GuestResolution = null;
|
||||
}
|
||||
|
||||
if (RenderScale is { } renderScale &&
|
||||
GuiSettings.NormalizeRenderScale(renderScale) == GuiSettings.NormalizeRenderScale(global.RenderScale))
|
||||
{
|
||||
RenderScale = null;
|
||||
}
|
||||
|
||||
if (DisplayIndex == global.DisplayIndex)
|
||||
{
|
||||
DisplayIndex = null;
|
||||
|
|
@ -272,6 +289,8 @@ public sealed record EffectiveLaunchSettings(
|
|||
bool LogToFile,
|
||||
string WindowMode,
|
||||
string Resolution,
|
||||
string GuestResolution,
|
||||
float RenderScale,
|
||||
int DisplayIndex,
|
||||
int RefreshRate,
|
||||
string ScalingMode,
|
||||
|
|
@ -289,6 +308,8 @@ public sealed record EffectiveLaunchSettings(
|
|||
perGame?.LogToFile ?? global.LogToFile,
|
||||
perGame?.WindowMode ?? global.WindowMode,
|
||||
perGame?.Resolution ?? global.Resolution,
|
||||
GuiSettings.NormalizeGuestResolution(perGame?.GuestResolution ?? global.GuestResolution),
|
||||
GuiSettings.NormalizeRenderScale(perGame?.RenderScale ?? global.RenderScale),
|
||||
Math.Max(0, perGame?.DisplayIndex ?? global.DisplayIndex),
|
||||
Math.Clamp(perGame?.RefreshRate ?? global.RefreshRate, 0, 1000),
|
||||
perGame?.ScalingMode ?? global.ScalingMode,
|
||||
|
|
|
|||
|
|
@ -242,50 +242,108 @@ public sealed class GuestPageTracker
|
|||
}
|
||||
}
|
||||
|
||||
// Region locks stay held across uploadFunc only for a written range, which turns GPU-dirty.
|
||||
// Receives the CPU-dirty runs an upload copies, then performs the upload while the regions are held.
|
||||
public interface IUploadRangeSink
|
||||
{
|
||||
void Range(ulong address, ulong size);
|
||||
|
||||
void Upload();
|
||||
}
|
||||
|
||||
private struct DelegateUploadSink(Action<ulong, ulong> rangeFunc, Action uploadFunc) : IUploadRangeSink
|
||||
{
|
||||
public readonly void Range(ulong address, ulong size) => rangeFunc(address, size);
|
||||
|
||||
public readonly void Upload() => uploadFunc();
|
||||
}
|
||||
|
||||
// Forwards a region's upload runs to the sink and remembers the runs it cleared, for rollback.
|
||||
private struct RegionUploadVisitor<TSink>(TSink sink, List<(TrackedRegion Region, ulong Address, ulong Size)> cleared)
|
||||
: TrackedRegion.ICpuUploadVisitor
|
||||
where TSink : struct, IUploadRangeSink
|
||||
{
|
||||
public TSink Sink = sink;
|
||||
|
||||
public readonly void Cleared(TrackedRegion region, ulong address, ulong size) => cleared.Add((region, address, size));
|
||||
|
||||
public void Upload(ulong address, ulong size) => Sink.Range(address, size);
|
||||
}
|
||||
|
||||
// Uploads run on every bound buffer of every draw, so the bookkeeping lists are reused per thread.
|
||||
[ThreadStatic]
|
||||
private static List<TrackedRegion>? _scratchHeld;
|
||||
|
||||
[ThreadStatic]
|
||||
private static List<(TrackedRegion Region, ulong Address, ulong Size)>? _scratchCleared;
|
||||
|
||||
public void ForEachUploadRange(ulong vaddr, ulong size, bool isWritten, Action<ulong, ulong> rangeFunc, Action uploadFunc,
|
||||
bool preserveCpuWriteHotPages = true)
|
||||
{
|
||||
var sink = new DelegateUploadSink(rangeFunc, uploadFunc);
|
||||
ForEachUploadRange(vaddr, size, isWritten, ref sink, preserveCpuWriteHotPages);
|
||||
}
|
||||
|
||||
// Region locks stay held across the upload only for a written range, which turns GPU-dirty.
|
||||
public void ForEachUploadRange<TSink>(ulong vaddr, ulong size, bool isWritten, ref TSink sink, bool preserveCpuWriteHotPages = true)
|
||||
where TSink : struct, IUploadRangeSink
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
VisitRegions(vaddr, size, create: true, static (_, _, _) => false);
|
||||
var previousOwner = _uploadOwner;
|
||||
_uploadOwner = this;
|
||||
var held = new List<TrackedRegion>();
|
||||
var cleared = new List<(TrackedRegion Region, ulong Address, ulong Size)>();
|
||||
// Another tracker may upload from this callback. It needs its own rollback
|
||||
// state; the reentry guard only rejects reentry into this tracker.
|
||||
var held = previousOwner is null
|
||||
? _scratchHeld ??= new List<TrackedRegion>()
|
||||
: new List<TrackedRegion>();
|
||||
var cleared = previousOwner is null
|
||||
? _scratchCleared ??= new List<(TrackedRegion Region, ulong Address, ulong Size)>()
|
||||
: new List<(TrackedRegion Region, ulong Address, ulong Size)>();
|
||||
held.Clear();
|
||||
cleared.Clear();
|
||||
try
|
||||
{
|
||||
using (GpuMemoryAccessProfile.Measure(GpuMemoryAccessProfile.Operation.UploadTracking, size))
|
||||
{
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
var visitor = new RegionUploadVisitor<TSink>(sink, cleared);
|
||||
var preserveHotPages = !isWritten && preserveCpuWriteHotPages;
|
||||
for (var (index, offset, remaining) = (vaddr / BlockBytes, vaddr % BlockBytes, size); remaining != 0; index++, offset = 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
remaining -= bytes;
|
||||
if (Volatile.Read(ref _regions[index]) is not { } region)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
region.Lock.Enter();
|
||||
held.Add(region);
|
||||
var address = region.BaseAddress + offset;
|
||||
region.ForEachCpuUploadRange(
|
||||
preserveHotPages: !isWritten && preserveCpuWriteHotPages,
|
||||
address,
|
||||
bytes,
|
||||
(runAddress, runSize) => cleared.Add((region, runAddress, runSize)),
|
||||
rangeFunc);
|
||||
region.ForEachCpuUploadRange(preserveHotPages, region.BaseAddress + offset, bytes, ref visitor);
|
||||
if (!isWritten)
|
||||
{
|
||||
region.Lock.Exit();
|
||||
held.Remove(region);
|
||||
held.RemoveAt(held.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
sink = visitor.Sink;
|
||||
}
|
||||
uploadFunc();
|
||||
sink.Upload();
|
||||
if (isWritten)
|
||||
{
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
for (var (index, offset, remaining) = (vaddr / BlockBytes, vaddr % BlockBytes, size); remaining != 0; index++, offset = 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
remaining -= bytes;
|
||||
if (Volatile.Read(ref _regions[index]) is not { } region)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
region.ChangeState(WriteOrigin.Gpu, enable: true, region.BaseAddress + offset, bytes);
|
||||
region.Lock.Exit();
|
||||
held.Remove(region);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
|
@ -333,6 +391,8 @@ public sealed class GuestPageTracker
|
|||
region.Lock.Exit();
|
||||
}
|
||||
|
||||
held.Clear();
|
||||
cleared.Clear();
|
||||
_uploadOwner = previousOwner;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,20 +144,24 @@ public sealed class TrackedRegion
|
|||
}
|
||||
}
|
||||
|
||||
// Receives the runs one upload clears and the runs it copies; a struct visitor keeps the walk allocation-free.
|
||||
public interface ICpuUploadVisitor
|
||||
{
|
||||
void Cleared(TrackedRegion region, ulong address, ulong size);
|
||||
|
||||
void Upload(ulong address, ulong size);
|
||||
}
|
||||
|
||||
// Hot read-only pages stay writable and dirty, so each obtain observes current CPU bytes.
|
||||
public void ForEachCpuUploadRange(
|
||||
bool preserveHotPages,
|
||||
ulong address,
|
||||
ulong size,
|
||||
Action<ulong, ulong> visitCleared,
|
||||
Action<ulong, ulong> visitUpload)
|
||||
public void ForEachCpuUploadRange<TVisitor>(bool preserveHotPages, ulong address, ulong size, ref TVisitor visitor)
|
||||
where TVisitor : struct, ICpuUploadVisitor
|
||||
{
|
||||
var (start, end) = GetPageRange(address, size);
|
||||
var upload = new PageMask(_cpuDirty, start, end);
|
||||
var cleared = preserveHotPages ? upload & ~_hotCpuWrites : upload;
|
||||
foreach (var (runStart, runEnd) in cleared)
|
||||
{
|
||||
visitCleared(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitor.Cleared(this, BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
_cpuDirty.UnsetRange(runStart, runEnd);
|
||||
_recentCpuUploads.SetRange(runStart, runEnd);
|
||||
}
|
||||
|
|
@ -165,7 +169,7 @@ public sealed class TrackedRegion
|
|||
UpdateCpuProtection(track: true);
|
||||
foreach (var (runStart, runEnd) in upload)
|
||||
{
|
||||
visitUpload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitor.Upload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ 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));
|
||||
|
||||
|
|
@ -56,12 +55,6 @@ 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)
|
||||
|
|
@ -72,9 +65,7 @@ public static class HleDataSymbols
|
|||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_progNameBufferAddress == 0 ||
|
||||
_progNamePointerAddress == 0 ||
|
||||
_argumentVectorAddress == 0)
|
||||
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -86,8 +77,6 @@ public static class HleDataSymbols
|
|||
|
||||
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
|
||||
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
|
||||
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
|
||||
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,20 +462,43 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||
var usleep = ResolveLibcExport("usleep");
|
||||
// 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");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 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, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
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, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// 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.
|
||||
|
|
@ -11,6 +13,12 @@ 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();
|
||||
|
|
@ -46,6 +54,8 @@ internal static class PosixViewRegions
|
|||
else
|
||||
Regions.Add(region);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _snapshot, [.. Regions]);
|
||||
}
|
||||
|
||||
// Mapping creates the storage. Fault-time protection updates must not allocate.
|
||||
|
|
@ -66,9 +76,8 @@ internal static class PosixViewRegions
|
|||
|
||||
internal static bool TryQuery(ulong address, out HostMemory.BasicInfo info)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
foreach (var region in Regions)
|
||||
foreach (var region in Volatile.Read(ref _snapshot))
|
||||
{
|
||||
if (region.Start > address) break;
|
||||
if (address >= region.End) continue;
|
||||
|
|
|
|||
|
|
@ -149,6 +149,14 @@ 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;
|
||||
|
|
@ -156,6 +164,10 @@ 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)
|
||||
|
|
@ -269,6 +281,7 @@ public static unsafe class HostMemory
|
|||
Size = alignedSize,
|
||||
DefaultProtect = protect
|
||||
};
|
||||
PublishRegionsLocked();
|
||||
|
||||
return (void*)result;
|
||||
}
|
||||
|
|
@ -288,6 +301,7 @@ public static unsafe class HostMemory
|
|||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions.Remove((ulong)address);
|
||||
PublishRegionsLocked();
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -325,6 +339,10 @@ public static unsafe class HostMemory
|
|||
{
|
||||
info = default;
|
||||
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||
if (TryQueryUnlocked(pageAddress, ref info))
|
||||
{
|
||||
return (nuint)sizeof(BasicInfo);
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -340,30 +358,37 @@ public static unsafe class HostMemory
|
|||
var pageProtects = region.PageProtects;
|
||||
uint protect;
|
||||
ulong runEnd;
|
||||
if (pageProtects is null || pageProtects.Count == 0)
|
||||
if (pageProtects is null || pageProtects.Count == 0 || region.OverridePages is not { } overridePages)
|
||||
{
|
||||
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;
|
||||
while (runEnd < region.End &&
|
||||
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||
nextProtect == protect)
|
||||
foreach (var overrideAddress in runEnd < region.End ? overridePages.GetViewBetween(runEnd, region.End - 1) : [])
|
||||
{
|
||||
if (overrideAddress != runEnd || pageProtects[overrideAddress] != protect)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runEnd += PageSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default protection runs to the next override.
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
foreach (var overrideAddress in pageProtects.Keys)
|
||||
if (pageAddress + PageSize < region.End)
|
||||
{
|
||||
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||
// The first element only: a view's Count walks the whole view.
|
||||
foreach (var overrideAddress in overridePages.GetViewBetween(pageAddress + PageSize, region.End - 1))
|
||||
{
|
||||
runEnd = overrideAddress;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -382,12 +407,18 @@ 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;
|
||||
foreach (var regionBase in Regions.Keys)
|
||||
var keys = Regions.Keys;
|
||||
for (int low = 0, high = keys.Count - 1; low <= high;)
|
||||
{
|
||||
if (regionBase > pageAddress)
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (keys[middle] > pageAddress)
|
||||
{
|
||||
nextBase = regionBase;
|
||||
break;
|
||||
nextBase = keys[middle];
|
||||
high = middle - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +447,56 @@ 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!;
|
||||
|
|
@ -454,20 +535,26 @@ 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)
|
||||
{
|
||||
region.PageProtects.Remove(pageAddress);
|
||||
if (region.PageProtects.Remove(pageAddress))
|
||||
{
|
||||
region.OverridePages.Remove(pageAddress);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
region.PageProtects[pageAddress] = protect;
|
||||
region.OverridePages.Add(pageAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
|
||||
|
|
@ -133,13 +134,15 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var indexAddress = ctx[CpuRegister.Rdx];
|
||||
var modifier = (uint)ctx[CpuRegister.Rcx];
|
||||
var modifier = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
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) ||
|
||||
|
|
@ -239,11 +242,13 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var modifier = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
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) ||
|
||||
|
|
@ -457,4 +462,19 @@ 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,14 +64,24 @@ 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)
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || (eventAddress != 0 && (eventType & ~1u) != 0x38))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||
// 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))))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ 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;
|
||||
|
|
@ -102,29 +99,6 @@ 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,6 +22,7 @@ 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;
|
||||
|
|
@ -35,17 +36,6 @@ 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).
|
||||
|
|
@ -70,7 +60,6 @@ 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;
|
||||
|
|
@ -99,11 +88,6 @@ 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();
|
||||
|
|
@ -228,7 +212,7 @@ public static class AudioOut2Exports
|
|||
public uint Ambisonics = AmbisonicsNone;
|
||||
public uint Passthrough;
|
||||
|
||||
public byte[]? PcmData;
|
||||
public int PcmPending;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +226,6 @@ 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",
|
||||
|
|
@ -269,16 +252,6 @@ 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.
|
||||
|
|
@ -332,8 +305,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 memorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memorySizeAddress == 0)
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -351,12 +324,13 @@ 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{memorySizeAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
|
||||
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
|
@ -417,20 +391,8 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Shared backend lifetime is process-wide; just drop the context entry.
|
||||
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -439,38 +401,7 @@ public static class AudioOut2Exports
|
|||
ExportName = "sceAudioOut2ContextBedWrite",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
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);
|
||||
}
|
||||
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aII9h5nli9U",
|
||||
|
|
@ -563,6 +494,11 @@ 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
|
||||
|
|
@ -614,41 +550,6 @@ 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);
|
||||
|
|
@ -721,38 +622,14 @@ public static class AudioOut2Exports
|
|||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
|
||||
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
|
||||
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} " +
|
||||
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
||||
$"grains={port.GrainSamples}");
|
||||
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -768,10 +645,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 = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
||||
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
||||
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||
if (outPortAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
|
|
@ -792,14 +669,6 @@ 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..]);
|
||||
|
|
@ -850,16 +719,13 @@ 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 _))
|
||||
|
|
@ -869,21 +735,15 @@ public static class AudioOut2Exports
|
|||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
||||
state[0x02] = channels;
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||
{
|
||||
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
||||
}
|
||||
|
||||
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)}");
|
||||
}
|
||||
|
||||
TraceAudioOut2(
|
||||
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1112,17 +972,7 @@ public static class AudioOut2Exports
|
|||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
|
||||
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -1214,20 +1064,13 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
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";
|
||||
}
|
||||
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";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1245,20 +1088,11 @@ public static class AudioOut2Exports
|
|||
{
|
||||
try
|
||||
{
|
||||
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";
|
||||
}
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
||||
context.Frequency,
|
||||
maxQueuedPcmBytes: 128 * 1024);
|
||||
SecondaryBackendName = audio.BackendName + "-secondary";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -1321,6 +1155,7 @@ 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
|
||||
{
|
||||
|
|
@ -1329,42 +1164,29 @@ public static class AudioOut2Exports
|
|||
foreach (var port in Ports.Values)
|
||||
{
|
||||
if (port.ContextHandle != context.Handle ||
|
||||
!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))
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var byteLength = checked(frames * ch * bps);
|
||||
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
|
||||
if (byteLength <= 0 || byteLength > source.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pcmData.Length < byteLength)
|
||||
var sourceSpan = source.AsSpan(0, byteLength);
|
||||
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MixPortIntoStereo(
|
||||
pcmData.AsSpan(0, byteLength),
|
||||
sourceSpan,
|
||||
mix.AsSpan(0, frames * 2),
|
||||
frames,
|
||||
ch,
|
||||
|
|
@ -1416,70 +1238,12 @@ 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) =>
|
||||
|
|
@ -1534,11 +1298,6 @@ 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,
|
||||
|
|
@ -1694,19 +1453,13 @@ 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,32 +881,6 @@ 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,21 +7,6 @@ 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.
|
||||
|
|
@ -44,8 +29,6 @@ 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;
|
||||
|
||||
|
|
@ -62,12 +45,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.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
SingleWriter = true,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -91,10 +74,6 @@ 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;
|
||||
|
|
@ -127,22 +106,18 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -161,18 +136,14 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
}
|
||||
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);
|
||||
|
|
@ -203,252 +174,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public bool RequestDrain()
|
||||
public void RequestDrain()
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
_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)
|
||||
{
|
||||
|
|
@ -496,28 +233,15 @@ 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.
|
||||
|
|
@ -840,22 +564,4 @@ 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,7 +1,6 @@
|
|||
// 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;
|
||||
|
||||
|
|
@ -16,18 +15,10 @@ 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",
|
||||
|
|
@ -36,25 +27,24 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
if (paramAddress == 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",
|
||||
|
|
@ -62,14 +52,8 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
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))
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -77,18 +61,7 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -96,18 +69,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
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))
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -132,9 +99,7 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -146,6 +111,7 @@ 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",
|
||||
|
|
@ -154,25 +120,33 @@ 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 (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -180,12 +154,6 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +173,8 @@ 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",
|
||||
|
|
@ -216,9 +186,8 @@ 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 (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -226,7 +195,6 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -240,235 +208,35 @@ 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);
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
{
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[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 readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
// 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,6 +149,9 @@ 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; }
|
||||
|
|
@ -339,6 +342,7 @@ 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,4 +9,6 @@ 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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
|||
MaxMemoryAllocationCount = properties.Limits.MaxMemoryAllocationCount;
|
||||
MaxComputeWorkGroupCount = (properties.Limits.MaxComputeWorkGroupCount[0], properties.Limits.MaxComputeWorkGroupCount[1], properties.Limits.MaxComputeWorkGroupCount[2]);
|
||||
Slabs = new GpuMemorySlabs(this);
|
||||
const FormatFeatureFlags blit = FormatFeatureFlags.BlitSrcBit | FormatFeatureFlags.BlitDstBit;
|
||||
Images.RenderScalePolicy.ConfigureFormatSupport(format => (GetFormatProperties(format).OptimalTilingFeatures & blit) == blit);
|
||||
}
|
||||
|
||||
// Shared chunks the small buffers are carved from; freed at device teardown.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ 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;
|
||||
|
|
@ -72,6 +73,7 @@ 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);
|
||||
|
|
@ -326,6 +328,18 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return (owner, owner.Offset(guestAddress));
|
||||
}
|
||||
|
||||
// The command worker can read the stable backing alias without changing CPU page
|
||||
// permissions. A GPU counter elsewhere on the same page is not a dependency of
|
||||
// the packet bytes being decoded. Actual GPU-written bytes retain the slow path.
|
||||
private static readonly bool CommandBackingReadsEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_COMMAND_BACKING_READS") != "0";
|
||||
|
||||
public bool TryReadCommandBacking(ulong address, Span<byte> destination) =>
|
||||
CommandBackingReadsEnabled && destination.Length != 0 &&
|
||||
!_gpuModifiedRanges.Overlaps(address, (ulong)destination.Length) &&
|
||||
ImageCache is { } images && !images.HasGpuModifiedImageBytes(address, (ulong)destination.Length) &&
|
||||
_backing.TryReadBacking(address, destination);
|
||||
|
||||
public void WriteHostMemory(ulong guestAddress, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (guestAddress == 0 || data.IsEmpty || (ulong)data.Length > ulong.MaxValue - guestAddress)
|
||||
|
|
@ -340,6 +354,27 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
// Registered buffers are ordered and disjoint: walk only the ones the write overlaps.
|
||||
var end = guestAddress + (ulong)data.Length;
|
||||
if (DeferHostWrites)
|
||||
{
|
||||
// When no GPU-written byte shares the written pages, the pages can simply turn
|
||||
// CPU-dirty: every later GPU reader synchronizes CPU-dirty pages first (bound buffers
|
||||
// on obtain, device-address programs at the next visibility point), so the bytes
|
||||
// arrive in command order without a staging copy that ends the rendering scope.
|
||||
var firstPage = guestAddress & ~(TrackerLayout.PageBytes - 1);
|
||||
var pagesEnd = (end + TrackerLayout.PageBytes - 1) & ~(TrackerLayout.PageBytes - 1);
|
||||
if (!_gpuModifiedRanges.Overlaps(firstPage, pagesEnd - firstPage) &&
|
||||
!_tracker.HasGpuDirtyPages(firstPage, pagesEnd - firstPage))
|
||||
{
|
||||
if (IsRegionRegistered(guestAddress, (ulong)data.Length))
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(guestAddress, (ulong)data.Length);
|
||||
NoteMemoryVisibilityPoint();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = _registry.FindFirstOverlappingIndex(guestAddress);
|
||||
index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end;
|
||||
index++)
|
||||
|
|
@ -359,6 +394,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly bool DeferHostWrites =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DEFER_HOST_WRITES") != "0";
|
||||
|
||||
public void FillBuffer(ulong guestAddress, ulong size, uint value, bool isGds)
|
||||
{
|
||||
if ((guestAddress & 3) != 0 || size == 0 || (size & 3) != 0 || size > ulong.MaxValue - guestAddress)
|
||||
|
|
@ -441,11 +479,15 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The fill pattern staged per thread; DCC fills arrive with every compressed target clear.
|
||||
[ThreadStatic]
|
||||
private static uint[]? _fillPattern;
|
||||
|
||||
private bool TryWriteFillToBacking(ulong guestAddress, ulong size, uint value)
|
||||
{
|
||||
var values = new uint[(int)Math.Min(size / sizeof(uint), 4096)];
|
||||
Array.Fill(values, value);
|
||||
var bytes = MemoryMarshal.AsBytes<uint>(values);
|
||||
var values = (_fillPattern ??= new uint[4096]).AsSpan(0, (int)Math.Min(size / sizeof(uint), 4096));
|
||||
values.Fill(value);
|
||||
var bytes = MemoryMarshal.AsBytes(values);
|
||||
for (ulong offset = 0; offset < size;)
|
||||
{
|
||||
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
|
||||
|
|
@ -661,13 +703,28 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_bdaTouchMapping = mapping;
|
||||
}
|
||||
|
||||
var epoch = _tracker.CpuDirtyEpoch;
|
||||
if (epoch != _bdaSweepEpoch || mapping != _bdaSweepMapping)
|
||||
// Device-address programs may read any mapped page, so CPU writes are swept into the
|
||||
// buffers - but only at memory visibility points (a submission slice starting, a wait
|
||||
// packet satisfied). A CPU write after the submission began carries no ordering
|
||||
// guarantee for its commands, and sweeping on every draw would end the render pass
|
||||
// for every page the guest streams while the stream is being translated.
|
||||
// Hot pages remain writable, so later CPU writes need not advance CpuDirtyEpoch.
|
||||
// Every visibility point must revisit their dirty ranges even with an unchanged epoch.
|
||||
if (mapping != _bdaSweepMapping || _bdaVisibilityPending || SweepBdaOnEveryDraw)
|
||||
{
|
||||
foreach (var span in spans)
|
||||
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
|
||||
_bdaSweepEpoch = epoch;
|
||||
BeginUploadBatch();
|
||||
try
|
||||
{
|
||||
foreach (var span in spans)
|
||||
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndUploadBatch();
|
||||
}
|
||||
|
||||
_bdaSweepMapping = mapping;
|
||||
_bdaVisibilityPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -678,7 +735,15 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
private Action<ulong, ulong>? _uploadDirtyBuffersInRange;
|
||||
private long _bdaSweepEpoch = -1;
|
||||
|
||||
private static readonly bool PreserveHotPagesInSweeps =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_BDA_SWEEP_REPROTECT_HOT") != "1";
|
||||
private bool _bdaVisibilityPending = true;
|
||||
|
||||
private static readonly bool SweepBdaOnEveryDraw =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_BDA_SWEEP_EVERY_DRAW") == "1";
|
||||
|
||||
public void NoteMemoryVisibilityPoint() => _bdaVisibilityPending = true;
|
||||
private ulong _bdaSweepMapping;
|
||||
|
||||
private void TouchBuffersInRange(ulong guestAddress, ulong size)
|
||||
|
|
@ -699,8 +764,10 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(index));
|
||||
var start = Math.Max(buffer.CpuAddress, guestAddress);
|
||||
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
|
||||
// Hot pages stay writable and are copied again on the next sweep: a few hundred KiB of
|
||||
// ring memory per sweep costs far less than a protection fault per page per frame.
|
||||
if (start < finish && _tracker.MayHaveCpuDirtyPages(start, finish - start))
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: PreserveHotPagesInSweeps);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -726,7 +793,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
// SynchronizeBuffer takes for this read-only call), and the block summary
|
||||
// answers that without a lock for the common all-clean case.
|
||||
if (_tracker.HasCpuDirtyPages(start, finish - start))
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: PreserveHotPagesInSweeps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -896,24 +963,45 @@ 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 && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
// 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)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: 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)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -1144,9 +1232,14 @@ 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 required VulkanAsyncReadback.Ticket Ticket { get; init; }
|
||||
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
|
||||
public ulong MainQueueTick { get; init; }
|
||||
public GpuBuffer? MainQueueBuffer { get; init; }
|
||||
public ulong[] MainQueueOffsets { get; init; } = [];
|
||||
public required List<DownloadPiece> Copies { get; init; }
|
||||
public required ulong[] WriteTicks { get; init; }
|
||||
public required ulong WindowBegin { get; init; }
|
||||
|
|
@ -1156,6 +1249,9 @@ 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,
|
||||
|
|
@ -1179,6 +1275,53 @@ 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)
|
||||
|
|
@ -1201,6 +1344,31 @@ 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);
|
||||
|
|
@ -1216,9 +1384,75 @@ 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
|
||||
{
|
||||
|
|
@ -1269,6 +1503,155 @@ 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;
|
||||
|
|
@ -1686,7 +2069,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
|
||||
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
_device, _scheduler, _unifiedBuffers ? GpuBufferUsage.Unified : 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)
|
||||
{
|
||||
|
|
@ -1713,20 +2096,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
profileScope.SwitchPhase(isWritten
|
||||
? RenderPhaseProfile.Phase.BufferDirtySyncWritten
|
||||
: isTexelBuffer ? RenderPhaseProfile.Phase.BufferDirtySyncTexel : RenderPhaseProfile.Phase.BufferDirtySyncUpload);
|
||||
var copies = new List<BufferCopy>();
|
||||
var totalSize = 0UL;
|
||||
GpuBuffer? source = null;
|
||||
_tracker.ForEachUploadRange(
|
||||
guestAddress,
|
||||
size,
|
||||
isWritten,
|
||||
(address, bytes) =>
|
||||
{
|
||||
copies.Add(new BufferCopy(totalSize, buffer.Offset(address), bytes));
|
||||
totalSize += bytes;
|
||||
},
|
||||
() => source = _uploader.PrepareSource(buffer.CpuAddress, CollectionsMarshal.AsSpan(copies), totalSize, guestAddress, size),
|
||||
preserveCpuWriteHotPages);
|
||||
var copies = _syncCopies ??= new List<BufferCopy>();
|
||||
copies.Clear();
|
||||
var sink = new UploadSink(_uploader, buffer, copies, guestAddress, size);
|
||||
_tracker.ForEachUploadRange(guestAddress, size, isWritten, ref sink, preserveCpuWriteHotPages);
|
||||
var source = sink.Source;
|
||||
if (source != null)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
|
|
@ -1734,6 +2108,17 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
command.EndRendering();
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var vk = _device.Vk;
|
||||
if (_uploadBatchActive)
|
||||
{
|
||||
OpenUploadBatch(native);
|
||||
var batched = CollectionsMarshal.AsSpan(copies);
|
||||
fixed (BufferCopy* pointer = batched)
|
||||
{
|
||||
vk.CmdCopyBuffer(native, source.Handle, buffer.Handle, (uint)batched.Length, pointer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var before = new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
|
|
@ -1760,6 +2145,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
VulkanSynchronization.PipelineBarrier(vk,
|
||||
native, PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit, DependencyFlags.ByRegionBit,
|
||||
0, null, 1, &after, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (BufferUploadProfile.Enabled)
|
||||
|
|
@ -1768,7 +2154,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
ulong hotBytes = 0;
|
||||
foreach (var copy in copies)
|
||||
hotBytes += _tracker.CountCpuWriteHotBytes(buffer.CpuAddress + copy.DstOffset, copy.Size);
|
||||
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
|
||||
BufferUploadProfile.Record(guestAddress, size, copies.Count, sink.TotalSize, hotBytes, elapsedTicks);
|
||||
}
|
||||
|
||||
if (isTexelBuffer)
|
||||
|
|
@ -1785,6 +2171,81 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
// A sweep uploads many buffers back to back: one global barrier before the first copy and one
|
||||
// after the last replace a barrier pair per buffer, so the copies share one transfer pass.
|
||||
private bool _uploadBatchActive;
|
||||
private ulong _uploadBatchCommand;
|
||||
|
||||
private void BeginUploadBatch()
|
||||
{
|
||||
_uploadBatchActive = true;
|
||||
_uploadBatchCommand = 0;
|
||||
}
|
||||
|
||||
private void OpenUploadBatch(CommandBuffer native)
|
||||
{
|
||||
if (_uploadBatchCommand == (ulong)native.Handle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A new command buffer (or the first copy) orders the copies after all earlier work.
|
||||
var barrier = new MemoryBarrier2
|
||||
{
|
||||
SType = StructureType.MemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.MemoryReadBit | AccessFlags2.MemoryWriteBit,
|
||||
DstAccessMask = AccessFlags2.TransferWriteBit,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(_device.Vk,
|
||||
native, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit, 0, 1, &barrier, 0, null, 0, null);
|
||||
_uploadBatchCommand = (ulong)native.Handle;
|
||||
}
|
||||
|
||||
private void EndUploadBatch()
|
||||
{
|
||||
_uploadBatchActive = false;
|
||||
if (_uploadBatchCommand == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Recorded on the current command buffer; submission order covers copies recorded on an
|
||||
// earlier one.
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
var barrier = new MemoryBarrier2
|
||||
{
|
||||
SType = StructureType.MemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags2.MemoryReadBit | AccessFlags2.MemoryWriteBit,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(_device.Vk,
|
||||
new CommandBuffer(command.Handle), PipelineStageFlags.TransferBit, PipelineStageFlags.AllCommandsBit, 0, 1, &barrier, 0, null, 0, null);
|
||||
_uploadBatchCommand = 0;
|
||||
}
|
||||
|
||||
// The copies of one synchronization; the tracker forbids nesting, so one list per thread is reused.
|
||||
[ThreadStatic]
|
||||
private static List<BufferCopy>? _syncCopies;
|
||||
|
||||
// Collects a buffer's CPU-dirty runs as copies, then stages them while the tracker holds the regions.
|
||||
private struct UploadSink(GuestBufferUploader uploader, GpuBuffer buffer, List<BufferCopy> copies, ulong guestAddress, ulong size)
|
||||
: GuestPageTracker.IUploadRangeSink
|
||||
{
|
||||
public ulong TotalSize { get; private set; }
|
||||
|
||||
public GpuBuffer? Source { get; private set; }
|
||||
|
||||
public void Range(ulong address, ulong bytes)
|
||||
{
|
||||
copies.Add(new BufferCopy(TotalSize, buffer.Offset(address), bytes));
|
||||
TotalSize += bytes;
|
||||
}
|
||||
|
||||
public void Upload() =>
|
||||
Source = uploader.PrepareSource(buffer.CpuAddress, CollectionsMarshal.AsSpan(copies), TotalSize, guestAddress, size);
|
||||
}
|
||||
|
||||
private GpuBuffer? FindOwner(ulong guestAddress, ulong size)
|
||||
{
|
||||
return _registry.TryGetRegisteredBuffer(_registry.FindContainingBuffer(guestAddress, size));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public interface IGuestImageCache
|
|||
{
|
||||
ImageRegionInfo QueryRegion(ulong address, ulong size);
|
||||
|
||||
bool HasGpuModifiedImageBytes(ulong address, ulong size) => QueryRegion(address, size).GpuImageBytes;
|
||||
|
||||
bool ClearMetadata(ulong address);
|
||||
|
||||
bool OverlapsDccMetadata(ulong address, ulong size);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,16 @@ 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,15 +435,26 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
|
||||
}
|
||||
|
||||
// Publish visible occlusion results for each depth block.
|
||||
if (_host.TryRecordOcclusionCounterDump(eventAddress))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Without host sample queries, conservatively report visibility.
|
||||
// A one-sample delta can disappear when the guest scales or reduces the counts.
|
||||
const ulong visibleCounterStep = 0x2FFFFFF;
|
||||
const ulong readyBit = 1UL << 63;
|
||||
var result = readyBit | SyntheticOcclusionCounter;
|
||||
for (var depthBlock = 0u; depthBlock < 16u; depthBlock++)
|
||||
{
|
||||
WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result);
|
||||
// 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);
|
||||
}
|
||||
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1);
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ public sealed partial class GpuCommandInterpreter
|
|||
if (!satisfied)
|
||||
{
|
||||
Suspend();
|
||||
return;
|
||||
}
|
||||
|
||||
_host.NoteMemoryVisibilityPoint();
|
||||
}
|
||||
|
||||
// The wait bit is PredicationZPassWaitOp: 0 (kWaitForQueryResults) stalls the command
|
||||
|
|
|
|||
|
|
@ -443,6 +443,8 @@ public sealed partial class GpuCommandInterpreter
|
|||
{
|
||||
throw _host.Fatal($"The command stream cannot write guest memory: address=0x{address:X16} size={source.Length}.");
|
||||
}
|
||||
// Later commands in this stream may read the bytes through device addresses.
|
||||
_host.NoteMemoryVisibilityPoint();
|
||||
_lastWriteLength = 0;
|
||||
if (source.Length is sizeof(uint) or sizeof(ulong))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ public readonly record struct DrawAutoArguments(
|
|||
uint InstanceCount,
|
||||
uint FirstVertex,
|
||||
uint FirstInstance,
|
||||
DrawOffsetSource OffsetSource);
|
||||
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);
|
||||
|
||||
public enum EndOfPipeWriteKind
|
||||
{
|
||||
|
|
@ -102,8 +105,19 @@ public interface ICommandStreamHost
|
|||
|
||||
void FillBuffer(ulong address, ulong size, uint value, bool isGds);
|
||||
|
||||
// Records a PIXEL_PIPE_STAT_DUMP occlusion counter dump with real sample counts; false when
|
||||
// the host cannot count samples and the interpreter must publish a conservative result.
|
||||
bool TryRecordOcclusionCounterDump(ulong address) => false;
|
||||
|
||||
void CopyBuffer(ulong destination, ulong source, ulong size, bool destinationIsGds, bool sourceIsGds);
|
||||
|
||||
// A satisfied wait packet or a command-processor memory write: CPU writes released by the
|
||||
// waited-on value, or the written bytes, must become visible to the commands that follow,
|
||||
// the same as at the start of a submission slice.
|
||||
void NoteMemoryVisibilityPoint()
|
||||
{
|
||||
}
|
||||
|
||||
void ReadGds(Span<uint> destination, uint wordOffset, uint wordCount);
|
||||
|
||||
void RecordEndOfPipe(in EndOfPipeWrite write);
|
||||
|
|
@ -136,6 +150,9 @@ 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// The guest-resolution twin of a scaled image and the blits that bridge the two resolutions.
|
||||
//
|
||||
// Guest memory layouts never change. Every path that moves guest bytes, and every copy that
|
||||
// pairs this image with one at another host resolution, runs against the twin at the guest
|
||||
// extent and then blits across, so callers keep describing rectangles in guest pixels.
|
||||
public sealed unsafe partial class CachedImage
|
||||
{
|
||||
private CachedImage? _guestSizedTwin;
|
||||
|
||||
private CachedImage GuestSizedTwin()
|
||||
{
|
||||
if (_guestSizedTwin is { } existing)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
// The twin is host-owned: dropping the guest placement keeps the image cache from
|
||||
// tracking it and keeps RenderScalePolicy from scaling it in turn.
|
||||
var description = Description;
|
||||
description.Data = default;
|
||||
description.Stencil = default;
|
||||
description.Metadata = default;
|
||||
description.HtileClearMask = uint.MaxValue;
|
||||
_guestSizedTwin = new CachedImage(_device, _scheduler, _guestBacking, description);
|
||||
return _guestSizedTwin;
|
||||
}
|
||||
|
||||
// The image at guest resolution: this image when it is not scaled, otherwise a twin
|
||||
// holding a downscaled copy of its current contents.
|
||||
internal CachedImage AtGuestResolution()
|
||||
{
|
||||
if (!IsScaled)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var twin = GuestSizedTwin();
|
||||
twin.BlitFrom(this);
|
||||
return twin;
|
||||
}
|
||||
|
||||
private void BlitFrom(CachedImage source)
|
||||
{
|
||||
var aspect = ViewFormatRules.FullAspects(Backing.Format);
|
||||
if (source.Backing.ImageType != ImageType.Type2D || Backing.ImageType != ImageType.Type2D ||
|
||||
source.Backing.Samples != 1 || Backing.Samples != 1 ||
|
||||
source.Backing.Format != Backing.Format || (aspect & ImageAspectFlags.StencilBit) != 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"A scaled image blit needs single-sample 2D images of one format without stencil: " +
|
||||
$"sourceFormat={(int)source.Backing.Format} format={(int)Backing.Format} " +
|
||||
$"sourceType={(int)source.Backing.ImageType} type={(int)Backing.ImageType} " +
|
||||
$"sourceSamples={source.Backing.Samples} samples={Backing.Samples}.");
|
||||
}
|
||||
|
||||
_scheduler.EndRendering();
|
||||
var command = new CommandBuffer(_scheduler.Current.Handle);
|
||||
source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, null, command);
|
||||
Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, command);
|
||||
var levels = Math.Min(source.Backing.MipLevels, Backing.MipLevels);
|
||||
var layers = Math.Min(source.Backing.Layers, Backing.Layers);
|
||||
var regions = new ImageBlit[levels];
|
||||
for (uint level = 0; level < levels; level++)
|
||||
{
|
||||
var subresource = new ImageSubresourceLayers(aspect, level, 0, layers);
|
||||
regions[level] = new ImageBlit
|
||||
{
|
||||
SrcSubresource = subresource,
|
||||
DstSubresource = subresource,
|
||||
SrcOffsets = new ImageBlit.SrcOffsetsBuffer
|
||||
{
|
||||
Element1 = new Offset3D(
|
||||
(int)Math.Max(source.Backing.Extent.Width >> (int)level, 1),
|
||||
(int)Math.Max(source.Backing.Extent.Height >> (int)level, 1),
|
||||
1),
|
||||
},
|
||||
DstOffsets = new ImageBlit.DstOffsetsBuffer
|
||||
{
|
||||
Element1 = new Offset3D(
|
||||
(int)Math.Max(Backing.Extent.Width >> (int)level, 1),
|
||||
(int)Math.Max(Backing.Extent.Height >> (int)level, 1),
|
||||
1),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Depth values must not be interpolated, and Vulkan forbids a filtered depth blit.
|
||||
var filter = (aspect & ImageAspectFlags.DepthBit) != 0 ? Filter.Nearest : Filter.Linear;
|
||||
fixed (ImageBlit* blits = regions)
|
||||
{
|
||||
_device.Vk.CmdBlitImage(command, source.Backing.Handle, ImageLayout.TransferSrcOptimal,
|
||||
Backing.Handle, ImageLayout.TransferDstOptimal, (uint)regions.Length, blits, filter);
|
||||
}
|
||||
|
||||
Transition(ReadyLayout, ReadyAccess, null, command);
|
||||
}
|
||||
|
||||
// True when a copy between the two images would cross host resolutions. Host-owned
|
||||
// helpers are built to match the image they mirror, so only guest-placed pairs qualify.
|
||||
private bool CrossesRenderScale(CachedImage other) =>
|
||||
RenderScale != other.RenderScale && IsGuestPlaced && other.IsGuestPlaced;
|
||||
|
||||
// Runs the copy at guest resolution: the scaled side contributes or receives its twin,
|
||||
// so the rectangles the caller describes keep their guest meaning.
|
||||
private bool TryCopyAcrossScales(CachedImage source, Action<CachedImage, CachedImage> copy)
|
||||
{
|
||||
if (!CrossesRenderScale(source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var guestSource = source.AtGuestResolution();
|
||||
if (!IsScaled)
|
||||
{
|
||||
copy(this, guestSource);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The copy may touch only part of the image, so the twin starts from the current
|
||||
// contents and the result is blitted back over the whole image.
|
||||
var twin = GuestSizedTwin();
|
||||
twin.BlitFrom(this);
|
||||
copy(twin, guestSource);
|
||||
BlitFrom(twin);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,14 @@ 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;
|
||||
|
|
@ -32,9 +40,12 @@ public sealed unsafe partial class CachedImage
|
|||
};
|
||||
|
||||
// Barriers for the requested state; a repeated write always gets a barrier.
|
||||
private static readonly List<ImageMemoryBarrier2> NoBarriers = new(0);
|
||||
|
||||
public (List<ImageMemoryBarrier2> Barriers, PipelineStageFlags SourceStages) GetBarriers(ImageLayout layout, AccessFlags access, PipelineStageFlags stage, SubresourceRange? range)
|
||||
{
|
||||
var barriers = new List<ImageMemoryBarrier2>();
|
||||
// Most requests need no barrier; the list exists only once one does. Callers never add to it.
|
||||
List<ImageMemoryBarrier2>? barriers = null;
|
||||
PipelineStageFlags sourceStages = 0;
|
||||
if ((access & (WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit)) != 0)
|
||||
{
|
||||
|
|
@ -81,9 +92,13 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
var state = states[index];
|
||||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
{
|
||||
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
states[index] = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
|
||||
}
|
||||
else if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
{
|
||||
(barriers ??= new List<ImageMemoryBarrier2>(1)).Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
sourceStages |= state.Stage;
|
||||
states[index] = new ImageAccessState(stage, access, layout);
|
||||
}
|
||||
|
|
@ -101,15 +116,54 @@ public sealed unsafe partial class CachedImage
|
|||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout == layout && state.Access == access && !repeatedWrite)
|
||||
{
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
{
|
||||
Backing.State = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
(barriers ??= new List<ImageMemoryBarrier2>(1)).Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
sourceStages |= state.Stage;
|
||||
}
|
||||
|
||||
Backing.State = new ImageAccessState(stage, access, layout);
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, 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)
|
||||
|
|
@ -166,7 +220,7 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
private void RecordBarriers(CommandBuffer command, PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, BufferMemoryBarrier2* bufferBarrier, List<ImageMemoryBarrier2> imageBarriers)
|
||||
{
|
||||
var images = imageBarriers.ToArray();
|
||||
var images = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(imageBarriers);
|
||||
fixed (ImageMemoryBarrier2* imagePointer = images)
|
||||
{
|
||||
VulkanSynchronization.PipelineBarrier(_device.Vk,
|
||||
|
|
@ -242,6 +296,16 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
public void UploadFromBuffer(ReadOnlySpan<BufferImageCopy> copies, VkBuffer buffer, ulong offset, ulong size)
|
||||
{
|
||||
// Guest bytes describe guest geometry, so a scaled image receives them through its
|
||||
// guest-resolution twin and takes the result as a blit.
|
||||
if (IsScaled)
|
||||
{
|
||||
var twin = GuestSizedTwin();
|
||||
twin.UploadFromBuffer(copies, buffer, offset, size);
|
||||
BlitFrom(twin);
|
||||
return;
|
||||
}
|
||||
|
||||
var command = BeginTransfer(copies, buffer, size);
|
||||
var sanitized = SanitizeUploadCopies(copies);
|
||||
var uploadCopies = sanitized is null ? copies : sanitized.AsSpan();
|
||||
|
|
@ -260,6 +324,12 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
public void DownloadToBuffer(ReadOnlySpan<BufferImageCopy> copies, VkBuffer buffer, ulong offset, ulong size)
|
||||
{
|
||||
if (IsScaled)
|
||||
{
|
||||
AtGuestResolution().DownloadToBuffer(copies, buffer, offset, size);
|
||||
return;
|
||||
}
|
||||
|
||||
var command = BeginTransfer(copies, buffer, size);
|
||||
var bufferBarrier = BufferBarrier(buffer, offset, size, MemoryAccess, AccessFlags.TransferWriteBit);
|
||||
var (imageBarriers, sourceStages) = GetBarriers(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, PipelineStageFlags.TransferBit, null);
|
||||
|
|
@ -298,6 +368,11 @@ public sealed unsafe partial class CachedImage
|
|||
// Copies every shared mip level of the source; stencil is never copied here.
|
||||
public void CopyFrom(CachedImage source)
|
||||
{
|
||||
if (TryCopyAcrossScales(source, static (destination, origin) => destination.CopyFrom(origin)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.Backing.Samples != Backing.Samples)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"An image copy needs equal sample counts: source={source.Backing.Samples} destination={Backing.Samples}.");
|
||||
|
|
@ -367,6 +442,13 @@ public sealed unsafe partial class CachedImage
|
|||
// Resolves or copies one mip of the source into one mip of this single-sample image.
|
||||
public void ResolveFrom(CachedImage source, in SubresourceRange sourceRange, in SubresourceRange destinationRange)
|
||||
{
|
||||
var fromRange = sourceRange;
|
||||
var toRange = destinationRange;
|
||||
if (TryCopyAcrossScales(source, (destination, origin) => destination.ResolveFrom(origin, fromRange, toRange)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Backing.Samples != 1 || source.Backing.ImageType != ImageType.Type2D || Backing.ImageType != ImageType.Type2D ||
|
||||
sourceRange.LevelCount != 1 || destinationRange.LevelCount != 1 ||
|
||||
sourceRange.BaseLevel >= source.Backing.MipLevels || destinationRange.BaseLevel >= Backing.MipLevels ||
|
||||
|
|
@ -383,16 +465,18 @@ public sealed unsafe partial class CachedImage
|
|||
var destinationHeight = Math.Max(Backing.Extent.Height >> (int)destinationRange.BaseLevel, 1);
|
||||
var copy = source.Backing.Samples == 1;
|
||||
var formatsAgree = copy ? ViewFormatRules.AreCompatible(source.Backing.Format, Backing.Format) : source.Backing.Format == Backing.Format;
|
||||
if (layers == 0 || Description.Extent.Width > sourceWidth || Description.Extent.Height > sourceHeight ||
|
||||
Description.Extent.Width > destinationWidth || Description.Extent.Height > destinationHeight || !formatsAgree)
|
||||
// The resolve moves host texels, so the guest extent crosses into this image's resolution.
|
||||
var resolveExtent = RenderScalePolicy.ScaleExtent(Description.Extent, RenderScale);
|
||||
if (layers == 0 || resolveExtent.Width > sourceWidth || resolveExtent.Height > sourceHeight ||
|
||||
resolveExtent.Width > destinationWidth || resolveExtent.Height > destinationHeight || !formatsAgree)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"The resolve extent or formats do not agree: extent={Description.Extent.Width}x{Description.Extent.Height} source={sourceWidth}x{sourceHeight} destination={destinationWidth}x{destinationHeight} layers={layers} sourceFormat={(int)source.Backing.Format} destinationFormat={(int)Backing.Format}.");
|
||||
$"The resolve extent or formats do not agree: extent={resolveExtent.Width}x{resolveExtent.Height} source={sourceWidth}x{sourceHeight} destination={destinationWidth}x{destinationHeight} layers={layers} sourceFormat={(int)source.Backing.Format} destinationFormat={(int)Backing.Format}.");
|
||||
}
|
||||
|
||||
var resolvedSource = sourceRange with { LayerCount = layers };
|
||||
var resolvedDestination = destinationRange with { LayerCount = layers };
|
||||
var extent = new Extent3D(Description.Extent.Width, Description.Extent.Height, 1);
|
||||
var extent = new Extent3D(resolveExtent.Width, resolveExtent.Height, 1);
|
||||
_scheduler.EndRendering();
|
||||
var command = new CommandBuffer(_scheduler.Current.Handle);
|
||||
source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, resolvedSource, command);
|
||||
|
|
@ -413,6 +497,13 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
public void CopyDepthStencilFrom(CachedImage source, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects)
|
||||
{
|
||||
var copyRange = range;
|
||||
var copyExtent = extent;
|
||||
if (TryCopyAcrossScales(source, (destination, origin) => destination.CopyDepthStencilFrom(origin, copyRange, copyExtent, aspects)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requested = aspects & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit);
|
||||
var available = ViewFormatRules.FullAspects(Backing.Format);
|
||||
if (requested == 0 || (requested & ~available) != 0 || source.Backing.Format != Backing.Format ||
|
||||
|
|
@ -510,6 +601,11 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
private void CopyThroughBuffer(CachedImage source, GpuBuffer buffer, ImageAspectFlags sourceAspect, ImageAspectFlags destinationAspect)
|
||||
{
|
||||
if (TryCopyAcrossScales(source, (destination, origin) => destination.CopyThroughBuffer(origin, buffer, sourceAspect, destinationAspect)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.Handle.Handle == 0 || source.Backing.Samples != 1 || Backing.Samples != 1)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"A copy through a buffer needs single-sample images and a buffer: sourceSamples={source.Backing.Samples} destinationSamples={Backing.Samples}.");
|
||||
|
|
@ -579,6 +675,11 @@ public sealed unsafe partial class CachedImage
|
|||
// Copies a whole standalone image into one mip and layer of this image.
|
||||
public void CopyMipFrom(CachedImage source, uint mip, uint layer)
|
||||
{
|
||||
if (TryCopyAcrossScales(source, (destination, origin) => destination.CopyMipFrom(origin, mip, layer)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.Backing.Samples != Backing.Samples || mip >= Backing.MipLevels || layer >= Backing.Layers)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The mip copy target is invalid: mip={mip} layer={layer} levels={Backing.MipLevels} layers={Backing.Layers} sourceSamples={source.Backing.Samples} destinationSamples={Backing.Samples}.");
|
||||
|
|
@ -627,6 +728,11 @@ public sealed unsafe partial class CachedImage
|
|||
// Copies a rectangle of the source's first mip into one mip of this image, at its origin.
|
||||
public void CopyRegionFrom(CachedImage source, uint sourceX, uint sourceY, uint mip, uint width, uint height)
|
||||
{
|
||||
if (TryCopyAcrossScales(source, (destination, origin) => destination.CopyRegionFrom(origin, sourceX, sourceY, mip, width, height)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (mip >= Backing.MipLevels || sourceX + width > source.Backing.Extent.Width || sourceY + height > source.Backing.Extent.Height)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
private bool _bufferHoldsGpuContents;
|
||||
|
||||
public ImageDescription Description;
|
||||
|
||||
// The host resolution multiplier of this image. Guest geometry in Description never
|
||||
// changes; only the backing is larger or smaller, and guest bytes move through a
|
||||
// guest-resolution twin (CachedImage.Transfers).
|
||||
public readonly float RenderScale = 1.0f;
|
||||
public readonly ImageBacking Backing = new();
|
||||
public readonly List<CachedImageView> Views = new();
|
||||
public ImageUses Uses;
|
||||
|
|
@ -86,7 +91,7 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
public int RecencyEntryIndex;
|
||||
|
||||
public CachedImage(GpuDeviceInfo device, SubmissionScheduler scheduler, IGuestBackedSpace guestBacking, in ImageDescription description,
|
||||
ImageBackingPool? pool = null)
|
||||
ImageBackingPool? pool = null, bool allowScaling = true)
|
||||
{
|
||||
_device = device;
|
||||
_scheduler = scheduler;
|
||||
|
|
@ -94,6 +99,7 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
_pool = pool;
|
||||
Description = description;
|
||||
Description.Validate();
|
||||
RenderScale = allowScaling ? RenderScalePolicy.ScaleFor(Description) : 1.0f;
|
||||
_cpuDirty = !ImageDescription.IsEmptyRange(Description.Data) && Description.Metadata.Compression == DisplayCompression.Uncompressed;
|
||||
if (Description.PixelFormat == Format.Undefined)
|
||||
{
|
||||
|
|
@ -102,7 +108,7 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
|
||||
Backing.Format = Description.PixelFormat;
|
||||
Backing.ImageType = HostImageType(Description.Type);
|
||||
Backing.Extent = Description.Extent;
|
||||
Backing.Extent = RenderScalePolicy.ScaleExtent(Description.Extent, RenderScale);
|
||||
Backing.GuestPitch = Description.Pitch;
|
||||
Backing.Layers = Description.IsVolume ? 1 : Description.Resources.Layers;
|
||||
Backing.MipLevels = Description.Resources.Levels;
|
||||
|
|
@ -320,6 +326,13 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
return usage;
|
||||
}
|
||||
|
||||
public bool IsScaled => RenderScale != 1.0f;
|
||||
|
||||
// Host-owned helpers - feedback snapshots, the stencil proxy, the guest-resolution twin -
|
||||
// carry no guest placement and are created to match the image they mirror, so a copy
|
||||
// between them and that image never crosses a resolution.
|
||||
internal bool IsGuestPlaced => !ImageDescription.IsEmptyRange(Description.Data);
|
||||
|
||||
public void AssociateDepth(ResourceSlotIdentifier depthImage) => DepthOwner = depthImage;
|
||||
|
||||
internal ulong LastCpuWriteAddress { get; private set; }
|
||||
|
|
@ -604,6 +617,8 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
// Immediate destruction; the owner waits for GPU work before disposing.
|
||||
public void Dispose()
|
||||
{
|
||||
_guestSizedTwin?.Dispose();
|
||||
_guestSizedTwin = null;
|
||||
foreach (var cached in Views)
|
||||
{
|
||||
_device.Vk.DestroyImageView(_device.Device, cached.View, null);
|
||||
|
|
|
|||
|
|
@ -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 = 10;
|
||||
private const uint KindCount = 9;
|
||||
private const uint ElementSizeCount = 5;
|
||||
private const uint DirectionCount = 2;
|
||||
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;
|
||||
|
|
|
|||
|
|
@ -1,271 +1,13 @@
|
|||
// 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();
|
||||
|
|
@ -315,6 +57,7 @@ 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))
|
||||
|
|
|
|||
|
|
@ -421,7 +421,9 @@ public sealed partial class GuestImageCache
|
|||
}
|
||||
|
||||
PrepareCopyTarget(replacement);
|
||||
_blit.Reinterpret(cached, replacement);
|
||||
// The conversion renders at the destination's guest extent, and a multisample
|
||||
// destination never scales, so a scaled source contributes its guest-sized twin.
|
||||
_blit.Reinterpret(cached.AtGuestResolution(), replacement);
|
||||
TakeGpuOwnership(replacement);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -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) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -703,11 +703,39 @@ 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,48 +123,28 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
return GetNullImage(request);
|
||||
}
|
||||
|
||||
var metadataBaseLayer = request.View.BaseLayer;
|
||||
var result = LookUpImage(ref request, exactFormat);
|
||||
|
||||
var found = LookUpImage(ref request, exactFormat);
|
||||
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
|
||||
{
|
||||
ulong metadataAddress;
|
||||
ulong sliceSize;
|
||||
using (var held = _lock.Hold())
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
{
|
||||
var image = _slots[result];
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0 &&
|
||||
image.Description.Metadata.Kind == MetadataKind.None)
|
||||
using var held = _lock.Hold();
|
||||
var image = _slots[found];
|
||||
if (image.Description.Metadata.Kind == MetadataKind.None)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
image.Description.Metadata.Kind = MetadataKind.Dcc;
|
||||
image.Description.Metadata.Range = request.Description.Metadata.Range;
|
||||
}
|
||||
|
||||
ref readonly var description = ref image.Description;
|
||||
metadataAddress = description.Metadata.Range.Address;
|
||||
sliceSize = description.DccSliceSize;
|
||||
}
|
||||
|
||||
if (sliceSize != 0)
|
||||
ref readonly var description = ref _slots[found].Description;
|
||||
if (description.DccSliceSize is var sliceSize and not 0)
|
||||
{
|
||||
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return found;
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
|
||||
|
|
@ -240,6 +220,14 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
}
|
||||
}
|
||||
|
||||
// A compute shader indexes a storage image in guest pixels and its dispatch grid does
|
||||
// not shrink with the host resolution, so a scaled image would be written with holes
|
||||
// (upscale) or redundantly (downscale). Such memory drops back to guest resolution.
|
||||
if (result.IsValid && request.ShaderWrite && _slots[result].IsScaled)
|
||||
{
|
||||
result = ReplaceScaled(result);
|
||||
}
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
result = InsertImage(request.Description);
|
||||
|
|
@ -251,6 +239,17 @@ 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 };
|
||||
|
|
@ -362,7 +361,8 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
image.MarkGpuModified();
|
||||
image.Uses.RenderTarget = true;
|
||||
RefreshFromGuest(imageIdentifier, request);
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
|
||||
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
|
||||
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
|
||||
{
|
||||
image.Description.Metadata = request.Description.Metadata;
|
||||
var address = request.Description.Metadata.Range.Address;
|
||||
|
|
@ -606,10 +606,61 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
return imageIdentifier;
|
||||
}
|
||||
|
||||
// Guest ranges a compute shader has written through a storage image; see the storage
|
||||
// branch of FindImage. Every image over such a range keeps its guest resolution.
|
||||
private readonly List<GuestSpan> _unscaledRanges = new();
|
||||
|
||||
private bool IsUnscaledRange(in GuestSpan data)
|
||||
{
|
||||
if (ImageDescription.IsEmptyRange(data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var range in _unscaledRanges)
|
||||
{
|
||||
if (GuestRangeOverlap.Bytes(range, data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drops one image back to guest resolution for good; later images over the same guest
|
||||
// memory are created unscaled too, so the decision cannot oscillate.
|
||||
public void DemoteRenderScale(ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
if (_slots[imageIdentifier].IsScaled)
|
||||
{
|
||||
ReplaceScaled(imageIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier ReplaceScaled(ResourceSlotIdentifier cachedImageIdentifier)
|
||||
{
|
||||
var cached = _slots[cachedImageIdentifier];
|
||||
_unscaledRanges.Add(cached.Description.Data);
|
||||
var replacementImageIdentifier = InsertImage(cached.Description);
|
||||
var replacement = _slots[replacementImageIdentifier];
|
||||
replacement.Uses = cached.Uses;
|
||||
if (cached.Binding.IsBound || cached.Binding.IsTarget)
|
||||
{
|
||||
cached.Binding.NeedsRebind = true;
|
||||
}
|
||||
|
||||
CopyWholeImage(replacementImageIdentifier, cachedImageIdentifier);
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
return replacementImageIdentifier;
|
||||
}
|
||||
|
||||
private ResourceSlotIdentifier InsertImage(in ImageDescription description)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageCreate);
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description, _backingPool));
|
||||
var allowScaling = !IsUnscaledRange(description.Data);
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description, _backingPool, allowScaling));
|
||||
if (!ImageDescription.IsEmptyRange(description.Data))
|
||||
{
|
||||
AddToIndex(imageIdentifier);
|
||||
|
|
|
|||
|
|
@ -28,11 +28,8 @@ 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>
|
||||
|
|
@ -369,8 +366,7 @@ public struct ImageDescription
|
|||
{
|
||||
case MetadataKind.None:
|
||||
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
|
||||
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
|
||||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
|
||||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
|
||||
}
|
||||
|
|
@ -382,11 +378,6 @@ 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 ||
|
||||
|
|
@ -516,87 +507,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ public struct ImageRequest
|
|||
public ImageViewDescription View;
|
||||
public ImageRole Role;
|
||||
|
||||
// The bound program stores into this image, rather than only reading it.
|
||||
public bool ShaderWrite;
|
||||
|
||||
public ImageRequest(in ImageDescription description, in ImageViewDescription view, ImageRole role)
|
||||
{
|
||||
Description = description;
|
||||
|
|
|
|||
|
|
@ -43,38 +43,24 @@ 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 && PackedClearValue.SupportsDccFixedColor(format);
|
||||
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
|
||||
return (supported, fixedSupported, supported ? value : default);
|
||||
}
|
||||
|
||||
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
|
||||
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,
|
||||
};
|
||||
|
||||
// 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)
|
||||
|
|
@ -338,14 +324,8 @@ 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, metadataSize.Size);
|
||||
description.Metadata.DccClearWord = words.ClearWord0;
|
||||
description.Metadata.DccClearRegisterValid = true;
|
||||
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
|
||||
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
|
||||
}
|
||||
|
||||
for (var level = 0; level < levels; level++)
|
||||
|
|
|
|||
|
|
@ -300,15 +300,12 @@ public static partial class ImageRequestBuilders
|
|||
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
|
||||
description.Samples = samples;
|
||||
description.TileMode = tile;
|
||||
if (!shape.R128 && words.Length >= 8 && descriptor.MetadataCompress &&
|
||||
tile != GuestTileMode.Depth && !description.IsDepth)
|
||||
var metadataAddress = descriptor.MetadataAddress << 8;
|
||||
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
|
||||
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
|
||||
{
|
||||
_ = 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(descriptor.MetadataAddress << 8, metadataSize.Size);
|
||||
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
|
||||
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
|
||||
}
|
||||
|
||||
if (samples > 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Globalization;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// Picks the host resolution of one guest image.
|
||||
//
|
||||
// The choice is a pure function of the guest description, so every view, alias, copy and
|
||||
// descriptor of the same guest memory reaches the same answer without consulting the cache.
|
||||
// Guest memory layouts never change: a scaled image only differs in the size of its host
|
||||
// backing, and the transfer paths route guest bytes through a guest-sized intermediate.
|
||||
public static class RenderScalePolicy
|
||||
{
|
||||
public const string VariableName = "SHARPEMU_RENDER_SCALE";
|
||||
|
||||
// Below this a target is a shadow map, a LUT, a probe atlas or an eye-adaptation
|
||||
// readback, and scaling it changes what the game computes rather than how it looks.
|
||||
public const uint MinimumScaledWidth = 1280;
|
||||
public const uint MinimumScaledHeight = 720;
|
||||
|
||||
private static readonly float? _environmentScale = ReadEnvironmentScale();
|
||||
private static float _scale = _environmentScale ?? 1.0f;
|
||||
|
||||
public static float Scale => Volatile.Read(ref _scale);
|
||||
|
||||
public static bool Enabled => Scale != 1.0f;
|
||||
|
||||
// The environment variable wins so scripted runs do not depend on stored settings.
|
||||
public static void Configure(float scale)
|
||||
{
|
||||
if (_environmentScale is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _scale, VideoOut.HostVideoOptions.NormalizeRenderScale(scale));
|
||||
}
|
||||
|
||||
private static float? ReadEnvironmentScale()
|
||||
{
|
||||
var text = Environment.GetEnvironmentVariable(VariableName);
|
||||
if (string.IsNullOrWhiteSpace(text) ||
|
||||
!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return VideoOut.HostVideoOptions.NormalizeRenderScale(parsed);
|
||||
}
|
||||
|
||||
public static uint ScaleLength(uint length, float scale)
|
||||
{
|
||||
if (scale == 1.0f || length <= 1)
|
||||
{
|
||||
return length;
|
||||
}
|
||||
|
||||
var scaled = (uint)Math.Ceiling(length * (double)scale);
|
||||
return Math.Clamp(scaled, 1u, 32768u);
|
||||
}
|
||||
|
||||
// The guest-pixel extent the clip-disable vertex transform maps onto. Dividing the device
|
||||
// limit by an upscale keeps the host viewport (reference x scale) inside that limit while
|
||||
// the transform the vertex program bakes stays scale independent.
|
||||
public static uint ClipSpaceReferenceExtent(uint limit)
|
||||
{
|
||||
var scale = Scale;
|
||||
return scale <= 1.0f ? limit : Math.Max(1u, (uint)(limit / scale));
|
||||
}
|
||||
|
||||
public static Extent3D ScaleExtent(Extent3D extent, float scale) => scale == 1.0f
|
||||
? extent
|
||||
: new Extent3D(ScaleLength(extent.Width, scale), ScaleLength(extent.Height, scale), extent.Depth);
|
||||
|
||||
private static Func<Format, bool>? _formatSupport;
|
||||
|
||||
// Installed once with the device: a scaled image is resized with vkCmdBlitImage whenever
|
||||
// guest bytes or a differently scaled alias cross into it, so a format that cannot blit
|
||||
// must keep its guest resolution.
|
||||
public static void ConfigureFormatSupport(Func<Format, bool> supportsScaledResize) =>
|
||||
Volatile.Write(ref _formatSupport, supportsScaledResize);
|
||||
|
||||
public static float ScaleFor(in ImageDescription description) => ScaleFor(description, Scale);
|
||||
|
||||
// Only screen-class, single-level, single-sample, uncompressed 2D render and depth
|
||||
// targets scale. Everything the host cannot reproduce at another size - block grids,
|
||||
// multisample footprints, HTile and DCC block coverage, mip chains whose levels would
|
||||
// round apart, cube and array faces - keeps its guest resolution.
|
||||
public static float ScaleFor(in ImageDescription description, float scale)
|
||||
{
|
||||
if (scale == 1.0f || !IsScalable(description))
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
public static bool IsScalable(in ImageDescription description)
|
||||
{
|
||||
// A stencil-association entry carries no host image, and an image without guest
|
||||
// memory is an internal helper whose extent must track whatever it mirrors.
|
||||
if (description.PixelFormat == Format.Undefined || !ImageDescription.IsValidRange(description.Data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (description.Type != GuestImageType.Color2D || description.Samples != 1 ||
|
||||
description.Resources.Levels != 1 || description.Resources.Layers != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// DCC only describes guest bytes the host never stores, so a scaled backing keeps it
|
||||
// meaningful. HTile encodes guest depth blocks the renderer reads back as clear state.
|
||||
if (description.IsBlock || description.HasStencil || description.Metadata.Kind == MetadataKind.Htile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Render and depth tilings mark the surfaces a game draws into. Asset textures use
|
||||
// the standard tilings and must stay at their authored size.
|
||||
if (description.TileMode is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (description.Extent.Width < MinimumScaledWidth || description.Extent.Height < MinimumScaledHeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A square target at screen size is a cube face or a shadow atlas far more often
|
||||
// than it is a scene buffer.
|
||||
if (description.Extent.Width == description.Extent.Height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only a backend that installed the predicate (the Vulkan device) can resize an image.
|
||||
return Volatile.Read(ref _formatSupport) is { } supported && supported(description.PixelFormat);
|
||||
}
|
||||
}
|
||||
|
|
@ -78,8 +78,6 @@ 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,9 +452,7 @@ 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.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
|
||||
? region.ImageSubresource.BaseArrayLayer
|
||||
: 0;
|
||||
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB 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,75 +230,6 @@ 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);
|
||||
|
|
@ -388,14 +319,6 @@ 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)
|
||||
{
|
||||
|
|
@ -441,19 +364,17 @@ public static partial class TileGeometry
|
|||
|
||||
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
|
||||
}
|
||||
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
|
||||
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
|
||||
{
|
||||
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
|
||||
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
|
||||
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
|
||||
}
|
||||
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
|
||||
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
|
||||
{
|
||||
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ 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)
|
||||
|
|
@ -110,7 +109,6 @@ 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 =
|
||||
|
|
@ -249,7 +247,6 @@ 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:
|
||||
|
|
@ -300,7 +297,7 @@ public static partial class TileGeometry
|
|||
kind = TileBlockKind.Depth64KB;
|
||||
break;
|
||||
case GuestTileMode.RenderTarget:
|
||||
kind = TileBlockKind.RenderTarget64KBGen5;
|
||||
kind = TileBlockKind.RenderTarget64KB;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -311,7 +308,7 @@ public static partial class TileGeometry
|
|||
return false;
|
||||
}
|
||||
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
|
||||
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
|
||||
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -608,39 +605,6 @@ 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,6 +166,8 @@ 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);
|
||||
|
||||
|
|
@ -278,6 +280,18 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
_ = image;
|
||||
}
|
||||
|
||||
// The Metal backend never scales an image, so its attachments always agree.
|
||||
void IRenderHost.DemoteRenderScale(ResourceSlotIdentifier image)
|
||||
{
|
||||
_ = image;
|
||||
}
|
||||
|
||||
float IRenderHost.GetRenderScale(ResourceSlotIdentifier image)
|
||||
{
|
||||
_ = image;
|
||||
return 1f;
|
||||
}
|
||||
|
||||
// The records of one draw or dispatch live until the executor resets; the submitted copy owns its bytes.
|
||||
void IRenderHost.ResetBindings()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,12 +41,19 @@ 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.
|
||||
|
|
|
|||
|
|
@ -201,8 +201,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
viewport.YScale,
|
||||
viewport.XOffset,
|
||||
viewport.YOffset,
|
||||
Math.Min(limits.MaxViewportWidth, MaxViewportDimension) * 0.5f,
|
||||
Math.Min(limits.MaxViewportHeight, MaxViewportDimension) * 0.5f);
|
||||
Images.RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportWidth, MaxViewportDimension)) * 0.5f,
|
||||
Images.RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportHeight, MaxViewportDimension)) * 0.5f);
|
||||
}
|
||||
|
||||
return VertexInputResolver.ResolveVertexInputs(_context, source.Registered, source.UserData,
|
||||
|
|
@ -210,15 +210,22 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
// One past the highest parameter location the pixel program reads, resolved as its translator does.
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = pixelProgram.Instructions
|
||||
// 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
|
||||
.Select(static instruction => instruction.Control)
|
||||
.OfType<Gen5InterpolationControl>()
|
||||
.Select(static control => control.Attribute)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
.ToArray());
|
||||
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = InterpolatedAttributes(pixelProgram);
|
||||
if (attributes.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
|
|
@ -237,16 +244,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
|
||||
{
|
||||
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);
|
||||
var attributes = InterpolatedAttributes(program);
|
||||
return attributes.Length == 0 ? 0u : attributes[^1] + 1;
|
||||
}
|
||||
|
||||
// The bound colour slots in order; each output mode names the kind the pixel program exports.
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
resources = ResourceMaterializer.ApplyTo(plan, specialization);
|
||||
layout = AllocateLayout(program, plan, resources, source.UserDataBase, (uint)source.UserData.Length, pushDataCursor,
|
||||
source.Stage == ShaderStage.Compute && options.ComputeInfo!.DispatchThreadDimensions);
|
||||
source.Stage == ShaderStage.Compute && options.ComputeInfo!.DispatchThreadDimensions, source.Stage);
|
||||
}
|
||||
catch (ResourcePlanException exception)
|
||||
{
|
||||
|
|
@ -593,7 +593,8 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
|
||||
var nativeHalfConversion = _host.NativeHalfConversionExact;
|
||||
var zeroOutOfBoundsReads = _host.ZeroOutOfBoundsBufferReads;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -606,7 +607,8 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -634,7 +636,8 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -646,12 +649,12 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
}
|
||||
}
|
||||
|
||||
private static BindingLayout AllocateLayout(Gen5ShaderProgram program, ShaderResourcePlan plan, SpecializedResourceInfo resources,
|
||||
uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits) =>
|
||||
uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits, ShaderStage stage) =>
|
||||
BindingLayout.Allocate(
|
||||
resources.Info,
|
||||
BindingLayout.CollectUserDataRegisters(program, userDataBase, userDataCount),
|
||||
|
|
@ -659,19 +662,24 @@ internal sealed class ShaderProgramCache
|
|||
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
|
||||
BindingLayout.ReadsShaderBase(program),
|
||||
pushDataCursor,
|
||||
usesDispatchThreadLimits: usesDispatchThreadLimits);
|
||||
usesDispatchThreadLimits: usesDispatchThreadLimits,
|
||||
// A pixel program reads its position in guest pixels; any program that samples or
|
||||
// fetches an image needs to know which of them the host holds at another size.
|
||||
usesRenderScale: Images.RenderScalePolicy.Enabled &&
|
||||
(stage == ShaderStage.Pixel || resources.Info.Images.Count != 0));
|
||||
|
||||
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
|
||||
bool signedZeroInfNanPreserve) =>
|
||||
bool nativeHalfConversion, bool zeroOutOfBoundsReads) =>
|
||||
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),
|
||||
|
|
@ -680,8 +688,8 @@ internal sealed class ShaderProgramCache
|
|||
};
|
||||
|
||||
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
|
||||
out BindingLayout? layout, out string error)
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool nativeHalfConversion, bool zeroOutOfBoundsReads,
|
||||
out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -696,9 +704,9 @@ internal sealed class ShaderProgramCache
|
|||
waveSize: record.Info.WaveSize);
|
||||
var resources = ResourceMaterializer.ApplyTo(plan, record.Specialization);
|
||||
layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor,
|
||||
record.Info.DispatchThreadDimensions);
|
||||
record.Info.DispatchThreadDimensions, ShaderStage.Compute);
|
||||
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
|
||||
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
|
||||
sharedInt64Atomics, execGuardElision, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -360,7 +360,24 @@ public static class VertexInputResolver
|
|||
for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++)
|
||||
{
|
||||
var buffer = buffers[bufferIndex];
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0);
|
||||
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,
|
||||
};
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ 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,6 +76,20 @@ 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);
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ public struct ColorTargetState
|
|||
public ResourceSlotIdentifier Image;
|
||||
public ImageView View;
|
||||
|
||||
public ColorTargetState(in ColorTargetResolution resolution, uint slot, ResourceSlotIdentifier image)
|
||||
// The host resolution the cache actually gave this image, which is one once the image
|
||||
// has been dropped back to guest resolution. Asking the scaling rules again would miss
|
||||
// that, so the render area, viewport and scissor read it from here.
|
||||
public float RenderScale;
|
||||
|
||||
public ColorTargetState(in ColorTargetResolution resolution, uint slot, ResourceSlotIdentifier image, float renderScale)
|
||||
{
|
||||
Resolution = resolution;
|
||||
Slot = slot;
|
||||
Image = image;
|
||||
View = default;
|
||||
RenderScale = renderScale;
|
||||
}
|
||||
|
||||
public readonly Extent2D HostExtent => new(
|
||||
RenderScalePolicy.ScaleLength(Resolution.Extent.Width, RenderScale),
|
||||
RenderScalePolicy.ScaleLength(Resolution.Extent.Height, RenderScale));
|
||||
}
|
||||
|
||||
// The depth target of a draw; HasTarget is false when no depth or stencil state is active.
|
||||
|
|
@ -32,14 +42,22 @@ public struct DepthAttachmentState
|
|||
public ImageView View;
|
||||
public bool MetadataClear;
|
||||
|
||||
public DepthAttachmentState(in DepthTargetState target, ResourceSlotIdentifier image)
|
||||
// See ColorTargetState.RenderScale.
|
||||
public float RenderScale;
|
||||
|
||||
public DepthAttachmentState(in DepthTargetState target, ResourceSlotIdentifier image, float renderScale)
|
||||
{
|
||||
Target = target;
|
||||
Image = image;
|
||||
View = default;
|
||||
MetadataClear = false;
|
||||
RenderScale = renderScale;
|
||||
}
|
||||
|
||||
public readonly uint HostWidth => RenderScalePolicy.ScaleLength(Target.Target.Width, RenderScale);
|
||||
|
||||
public readonly uint HostHeight => RenderScalePolicy.ScaleLength(Target.Target.Height, RenderScale);
|
||||
|
||||
public readonly bool HasTarget => Image.IsValid && Target.Target.Format != Format.Undefined;
|
||||
|
||||
// The attachment loads a clear when the registers or the metadata say so.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,13 @@ public interface IRenderHost
|
|||
|
||||
void BindRenderTarget(ResourceSlotIdentifier image);
|
||||
|
||||
// The attachments of one pass must share a host resolution; a disagreement drops the
|
||||
// scaled image back to guest resolution for good and the draw resolves its targets again.
|
||||
void DemoteRenderScale(ResourceSlotIdentifier image);
|
||||
|
||||
// The host resolution multiplier of one cached image; one when it is at guest resolution.
|
||||
float GetRenderScale(ResourceSlotIdentifier image);
|
||||
|
||||
void ResetBindings();
|
||||
|
||||
// Finds the image again when it changed, makes the view, sets the layout and resolves a metadata clear.
|
||||
|
|
@ -140,6 +147,9 @@ 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() { }
|
||||
|
||||
|
|
@ -153,6 +163,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Rendering;
|
||||
|
||||
// The 256-byte block a guest occlusion query reads: per depth block, a begin counter at
|
||||
// 16 * db and an end counter at 16 * db + 8, each valid once bit 63 is set. The guest sums
|
||||
// end - begin over the depth blocks, so host segment k is published as depth block k's count.
|
||||
internal static class OcclusionResultBlock
|
||||
{
|
||||
public const int Bytes = 256;
|
||||
public const int DepthBlocks = 16;
|
||||
public const ulong ReadyBit = 1UL << 63;
|
||||
|
||||
// Reported when the samples could not be counted, so the guest keeps the object visible.
|
||||
public const ulong ConservativeCount = 0x00FF_FFFF;
|
||||
|
||||
public static byte[] Build(int segmentCount, bool conservative, ReadOnlySpan<ulong> counts)
|
||||
{
|
||||
if (segmentCount is < 0 or > DepthBlocks || (!conservative && counts.Length < segmentCount))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(segmentCount));
|
||||
}
|
||||
|
||||
var block = new byte[Bytes];
|
||||
var slots = MemoryMarshal.Cast<byte, ulong>(block.AsSpan());
|
||||
slots.Fill(ReadyBit);
|
||||
if (conservative)
|
||||
{
|
||||
slots[1] = ReadyBit | ConservativeCount;
|
||||
return block;
|
||||
}
|
||||
|
||||
for (var segment = 0; segment < segmentCount; segment++)
|
||||
{
|
||||
slots[segment * 2 + 1] = ReadyBit | (counts[segment] & (ReadyBit - 1));
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
|
|
@ -28,6 +29,18 @@ public sealed partial class RenderExecutor
|
|||
return hostDepthBits == 0 ? guestFactor : MathF.ScaleB(guestFactor, hostDepthBits + offset.NegativeDepthBits);
|
||||
}
|
||||
|
||||
// The host resolution of the attachments a draw writes; all of them share one rule, so
|
||||
// the first bound target decides.
|
||||
private static float AttachmentRenderScale(in DrawState state)
|
||||
{
|
||||
if (state.ColorCount > 0 && state.Colors[0].Image.IsValid)
|
||||
{
|
||||
return state.Colors[0].RenderScale;
|
||||
}
|
||||
|
||||
return state.Depth.Image.IsValid ? state.Depth.RenderScale : 1f;
|
||||
}
|
||||
|
||||
// The dynamic state of a draw from the context bank and the resolved targets.
|
||||
private DynamicDrawState BuildDynamicState(ContextRegisters context, in DrawState state)
|
||||
{
|
||||
|
|
@ -36,6 +49,9 @@ public sealed partial class RenderExecutor
|
|||
var limits = _host.Limits;
|
||||
uint framebufferWidth;
|
||||
uint framebufferHeight;
|
||||
// The guest describes its viewport and scissor in guest pixels; the attachments decide
|
||||
// how many host texels one of those covers.
|
||||
var renderScale = AttachmentRenderScale(in state);
|
||||
if (state.ColorCount > 0 && state.Colors[0].Image.IsValid)
|
||||
{
|
||||
framebufferWidth = state.Colors[0].Resolution.Extent.Width;
|
||||
|
|
@ -53,6 +69,13 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var scissor = ResolveScissor(viewportRegisters, context.ScanMode, framebufferWidth, framebufferHeight);
|
||||
if (renderScale != 1f)
|
||||
{
|
||||
scissor = ScaleScissor(scissor, renderScale,
|
||||
RenderScalePolicy.ScaleLength(framebufferWidth, renderScale),
|
||||
RenderScalePolicy.ScaleLength(framebufferHeight, renderScale));
|
||||
}
|
||||
|
||||
ref readonly var viewport = ref viewportRegisters.Viewports[0];
|
||||
float viewportX;
|
||||
float viewportY;
|
||||
|
|
@ -60,17 +83,19 @@ public sealed partial class RenderExecutor
|
|||
float viewportHeight;
|
||||
if (context.Clip.ClipDisable)
|
||||
{
|
||||
// The vertex program bakes the transform onto the same reference extent, so
|
||||
// multiplying only the host viewport puts guest pixels at host positions.
|
||||
viewportX = 0;
|
||||
viewportY = 0;
|
||||
viewportWidth = Math.Min(limits.MaxViewportWidth, MaxViewportDimension);
|
||||
viewportHeight = Math.Min(limits.MaxViewportHeight, MaxViewportDimension);
|
||||
viewportWidth = RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportWidth, MaxViewportDimension)) * renderScale;
|
||||
viewportHeight = RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportHeight, MaxViewportDimension)) * renderScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
viewportX = viewport.XOffset - viewport.XScale;
|
||||
viewportY = viewport.YOffset - viewport.YScale;
|
||||
viewportWidth = viewport.XScale * 2f;
|
||||
viewportHeight = viewport.YScale * 2f;
|
||||
viewportX = (viewport.XOffset - viewport.XScale) * renderScale;
|
||||
viewportY = (viewport.YOffset - viewport.YScale) * renderScale;
|
||||
viewportWidth = viewport.XScale * 2f * renderScale;
|
||||
viewportHeight = viewport.YScale * 2f * renderScale;
|
||||
}
|
||||
|
||||
var lineWidth = context.LineWidth;
|
||||
|
|
@ -139,6 +164,21 @@ public sealed partial class RenderExecutor
|
|||
colorWriteMask);
|
||||
}
|
||||
|
||||
// A guest scissor in host texels. The edges round outward so no guest pixel the game
|
||||
// asked for loses its host coverage, and the result stays inside the attachment.
|
||||
public static ScissorRectangle ScaleScissor(ScissorRectangle scissor, float scale, uint hostWidth, uint hostHeight)
|
||||
{
|
||||
static int Floor(int value, float scale) => (int)MathF.Floor(value * scale);
|
||||
static int Ceiling(int value, float scale) => (int)MathF.Ceiling(value * scale);
|
||||
var left = Math.Clamp(Floor(scissor.Left, scale), 0, (int)hostWidth);
|
||||
var top = Math.Clamp(Floor(scissor.Top, scale), 0, (int)hostHeight);
|
||||
return new ScissorRectangle(
|
||||
left,
|
||||
top,
|
||||
Math.Clamp(Ceiling(scissor.Right, scale), left, (int)hostWidth),
|
||||
Math.Clamp(Ceiling(scissor.Bottom, scale), top, (int)hostHeight));
|
||||
}
|
||||
|
||||
// The screen, window, generic, viewport and clip rectangles intersected and clamped to the extent.
|
||||
public static ScissorRectangle ResolveScissor(ScreenViewportRegisters viewport, in ScanModeRegisters scanMode, uint width, uint height)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -217,8 +217,13 @@ public sealed partial class RenderExecutor
|
|||
IPreparedBindings? pixelBindings;
|
||||
try
|
||||
{
|
||||
vertexBindings = PrepareBindings(vertexInput.Stage);
|
||||
pixelBindings = state.PixelActive ? PrepareBindings(pixelInput.Stage) : null;
|
||||
// The pixel program reads its position in host texels; the attachments say how
|
||||
// many of those one guest pixel covers.
|
||||
var attachmentScale = AttachmentRenderScale(in state);
|
||||
vertexBindings = PrepareBindings(vertexInput.Stage with { AttachmentRenderScale = attachmentScale });
|
||||
pixelBindings = state.PixelActive
|
||||
? PrepareBindings(pixelInput.Stage with { AttachmentRenderScale = attachmentScale })
|
||||
: null;
|
||||
}
|
||||
catch (DrawImageTypeMismatchException rejection)
|
||||
{
|
||||
|
|
@ -251,7 +256,7 @@ public sealed partial class RenderExecutor
|
|||
var vertexBuffers = AcquireVertexBuffers(vertexInput);
|
||||
var indexBuffer = AcquireIndexBuffer(in indexSource);
|
||||
var indirectArguments = emission.IndirectArgumentsAddress != 0
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, emission.Indexed ? IndexedIndirectArgumentsSize : AutoIndirectArgumentsSize, isWritten: false)
|
||||
: default;
|
||||
DropUnwrittenColorTargets(context, ref state, pixelProgram);
|
||||
state.Rendering = AcquireAttachments(ref state);
|
||||
|
|
@ -314,7 +319,14 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
// Uploads and shader writes end with barriers to all commands, so the
|
||||
// indirect read sees them.
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
if (emission.Indexed)
|
||||
{
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
_host.DrawIndirect(indirectArguments);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,30 +24,18 @@ public sealed partial class RenderExecutor
|
|||
return false;
|
||||
}
|
||||
|
||||
for (var slot = 0u; slot < ContextRegisters.ColorTargetCount; slot++)
|
||||
for (var attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
if (slot != 0 && (context.RenderTargetMaskForSlot(slot) == 0 || context.ColorTargets[slot].BaseAddress == 0))
|
||||
ResolveTargetImages(context, ref state);
|
||||
if (!TryDemoteMismatchedRenderScales(ref state))
|
||||
{
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: false, out var resolvedSlot) is not { } resolution)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var request = resolution.Request;
|
||||
var image = _host.FindImage(ref request, exactFormat: false);
|
||||
_host.BindRenderTarget(image);
|
||||
state.Colors[(int)state.ColorCount++] = new ColorTargetState(in resolution, resolvedSlot, image);
|
||||
}
|
||||
|
||||
if (DepthTargetResolver.Resolve(context, _host.FormatSupport, _host.Fatal) is { } depthTarget)
|
||||
{
|
||||
var request = depthTarget.Target.Request;
|
||||
var image = _host.FindImage(ref request, exactFormat: false);
|
||||
_host.BindRenderTarget(image);
|
||||
state.Depth = new DepthAttachmentState(in depthTarget, image);
|
||||
// Every attachment is back at guest resolution now; resolve them again so the
|
||||
// draw sees the replacements instead of the images that were just released.
|
||||
state.ColorCount = 0;
|
||||
state.Depth = default;
|
||||
}
|
||||
|
||||
state.PixelActive = HasActivePixelShader(banks);
|
||||
|
|
@ -78,6 +66,73 @@ public sealed partial class RenderExecutor
|
|||
return true;
|
||||
}
|
||||
|
||||
private void ResolveTargetImages(ContextRegisters context, ref DrawState state)
|
||||
{
|
||||
for (var slot = 0u; slot < ContextRegisters.ColorTargetCount; slot++)
|
||||
{
|
||||
if (slot != 0 && (context.RenderTargetMaskForSlot(slot) == 0 || context.ColorTargets[slot].BaseAddress == 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: false, out var resolvedSlot) is not { } resolution)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var request = resolution.Request;
|
||||
var image = _host.FindImage(ref request, exactFormat: false);
|
||||
_host.BindRenderTarget(image);
|
||||
state.Colors[(int)state.ColorCount++] = new ColorTargetState(in resolution, resolvedSlot, image, _host.GetRenderScale(image));
|
||||
}
|
||||
|
||||
if (DepthTargetResolver.Resolve(context, _host.FormatSupport, _host.Fatal) is { } depthTarget)
|
||||
{
|
||||
var request = depthTarget.Target.Request;
|
||||
var image = _host.FindImage(ref request, exactFormat: false);
|
||||
_host.BindRenderTarget(image);
|
||||
state.Depth = new DepthAttachmentState(in depthTarget, image, _host.GetRenderScale(image));
|
||||
}
|
||||
}
|
||||
|
||||
// One rendering scope has one render area, one viewport and one scissor, so its
|
||||
// attachments have to agree on a host resolution. They disagree when one of them cannot
|
||||
// be scaled at all, and then the whole pass goes back to guest resolution.
|
||||
private bool TryDemoteMismatchedRenderScales(ref DrawState state)
|
||||
{
|
||||
if (!RenderScalePolicy.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var scale = AttachmentRenderScale(in state);
|
||||
var mismatched = state.Depth.HasTarget && state.Depth.RenderScale != scale;
|
||||
for (var i = 0; i < state.ColorCount && !mismatched; i++)
|
||||
{
|
||||
mismatched = state.Colors[i].RenderScale != scale;
|
||||
}
|
||||
|
||||
if (!mismatched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < state.ColorCount; i++)
|
||||
{
|
||||
if (state.Colors[i].RenderScale != 1f)
|
||||
{
|
||||
_host.DemoteRenderScale(state.Colors[i].Image);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.Depth.HasTarget && state.Depth.RenderScale != 1f)
|
||||
{
|
||||
_host.DemoteRenderScale(state.Depth.Image);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether the bound vertex (or emulated geometry) stage stores to a buffer or a
|
||||
// storage image, which is the only observable effect a draw without attachments
|
||||
// and without a pixel stage can have.
|
||||
|
|
@ -175,7 +230,12 @@ public sealed partial class RenderExecutor
|
|||
readRequest.View.LevelCount,
|
||||
readRequest.View.BaseLayer,
|
||||
readRequest.View.LayerCount);
|
||||
var extent = new Extent3D(write.Width, write.Height, 1);
|
||||
// The copy moves host texels, so the guest extent crosses into the images' resolution.
|
||||
var copyScale = _host.GetRenderScale(destination);
|
||||
var extent = new Extent3D(
|
||||
RenderScalePolicy.ScaleLength(write.Width, copyScale),
|
||||
RenderScalePolicy.ScaleLength(write.Height, copyScale),
|
||||
1);
|
||||
var aspects = (depthCopy ? ImageAspectFlags.DepthBit : 0) | (stencilCopy ? ImageAspectFlags.StencilBit : 0);
|
||||
_host.MarkGpuWritten(destination);
|
||||
_host.CopyDepthStencilImage(source, destination, in range, in extent, aspects);
|
||||
|
|
@ -192,7 +252,7 @@ public sealed partial class RenderExecutor
|
|||
var request = resolution.Request;
|
||||
var image = _host.FindImage(ref request, exactFormat: true);
|
||||
_host.BindRenderTarget(image);
|
||||
return image.IsValid ? new ColorTargetState(in resolution, resolvedSlot, image) : null;
|
||||
return image.IsValid ? new ColorTargetState(in resolution, resolvedSlot, image, _host.GetRenderScale(image)) : null;
|
||||
}
|
||||
|
||||
private static bool IsSupportedSampleCount(uint samples) => samples is 1 or 2 or 4 or 8;
|
||||
|
|
@ -269,8 +329,10 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var view = target.Resolution.Request.View;
|
||||
rendering.Width = Math.Min(rendering.Width, target.Resolution.Extent.Width);
|
||||
rendering.Height = Math.Min(rendering.Height, target.Resolution.Extent.Height);
|
||||
// The render area covers host texels, which a scaled attachment has more or fewer of.
|
||||
var hostExtent = target.HostExtent;
|
||||
rendering.Width = Math.Min(rendering.Width, hostExtent.Width);
|
||||
rendering.Height = Math.Min(rendering.Height, hostExtent.Height);
|
||||
rendering.Layers = Math.Min(rendering.Layers, view.LayerCount);
|
||||
var clear = acquired.MetadataClear ? acquired.MetadataClearValue : target.Resolution.ColorClearValue;
|
||||
rendering.ColorAttachments[i] = new RenderingAttachment(
|
||||
|
|
@ -310,13 +372,15 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var loadState = depth.LoadState;
|
||||
var layout = _host.SamplesDepthAttachment(in depth)
|
||||
? loadState.AttachmentLayout(target.Format)
|
||||
: DepthStencilState.WritableAttachmentLayout(target.Format);
|
||||
var layout = DepthStencilState.AttachmentLayoutFor(
|
||||
loadState,
|
||||
target.Format,
|
||||
_host.SamplesDepthAttachment(in depth),
|
||||
_host.DepthAttachmentLayout(in depth));
|
||||
_host.TransitionDepthAttachment(in depth, layout, loadState.AttachmentWriteAspects(target.Format));
|
||||
var view = target.Request.View;
|
||||
rendering.Width = Math.Min(rendering.Width, target.Width);
|
||||
rendering.Height = Math.Min(rendering.Height, target.Height);
|
||||
rendering.Width = Math.Min(rendering.Width, depth.HostWidth);
|
||||
rendering.Height = Math.Min(rendering.Height, depth.HostHeight);
|
||||
rendering.Layers = Math.Min(rendering.Layers, view.LayerCount);
|
||||
var aspects = ViewFormatRules.DepthAspects(target.Format);
|
||||
rendering.DepthStencilAttachment = new RenderingAttachment(
|
||||
|
|
|
|||
|
|
@ -309,6 +309,48 @@ 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)
|
||||
|
|
@ -364,6 +406,13 @@ 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;
|
||||
|
|
@ -396,7 +445,8 @@ public sealed partial class RenderExecutor
|
|||
false,
|
||||
0,
|
||||
(uint)vertexOffset,
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput),
|
||||
arguments.IndirectArgumentsAddress);
|
||||
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
|
||||
_host.ResetBindings();
|
||||
}
|
||||
|
|
@ -556,7 +606,8 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
var context = banks.Context;
|
||||
var shaderInterface = context.ShaderInterface;
|
||||
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
var hasColorOutput = context.ColorControl.Mode != 0 &&
|
||||
(context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ public readonly record struct ShaderStageResources(ShaderProgramInfo? Program, R
|
|||
|
||||
public DispatchThreadLimits? ThreadLimits { get; init; }
|
||||
|
||||
// The host resolution of the attachments this draw writes; null outside a draw.
|
||||
public float? AttachmentRenderScale { get; init; }
|
||||
|
||||
public void WriteDispatchThreadLimits(Span<uint> shaderData)
|
||||
{
|
||||
if (Program?.Bindings is not { UsesDispatchThreadLimits: true } layout) return;
|
||||
|
|
@ -93,6 +96,25 @@ public readonly record struct ShaderStageResources(ShaderProgramInfo? Program, R
|
|||
shaderData[offset + 1] = limits.Y;
|
||||
shaderData[offset + 2] = limits.Z;
|
||||
}
|
||||
|
||||
// The internal-resolution dwords: the factor that maps one guest pixel onto host texels,
|
||||
// its reciprocal, which image resources the host holds at that factor, and the factor the
|
||||
// pixel position is divided by so the program keeps reading guest pixels.
|
||||
public void WriteRenderScale(Span<uint> shaderData, ulong scaledImages, float imageScale)
|
||||
{
|
||||
if (Program?.Bindings is not { UsesRenderScale: true } layout) return;
|
||||
if (shaderData.Length != layout.ShaderDataDwordCount || !float.IsFinite(imageScale) || imageScale <= 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("The draw has invalid shader data or an invalid scale for its render scale dwords.");
|
||||
}
|
||||
|
||||
var offset = (int)layout.RenderScaleDword;
|
||||
shaderData[offset] = BitConverter.SingleToUInt32Bits(imageScale);
|
||||
shaderData[offset + 1] = BitConverter.SingleToUInt32Bits(1.0f / imageScale);
|
||||
shaderData[offset + 2] = (uint)scaledImages;
|
||||
shaderData[offset + 3] = (uint)(scaledImages >> 32);
|
||||
shaderData[offset + 4] = BitConverter.SingleToUInt32Bits(1.0f / (AttachmentRenderScale ?? 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z);
|
||||
|
|
@ -100,7 +122,9 @@ 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 ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount;
|
||||
public uint MinimumFetchBytes { get; init; }
|
||||
|
||||
public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes);
|
||||
}
|
||||
|
||||
// One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot.
|
||||
|
|
|
|||
|
|
@ -272,6 +272,18 @@ 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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,392 @@
|
|||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
// 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,25 +141,6 @@ 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",
|
||||
|
|
@ -187,14 +168,6 @@ 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",
|
||||
|
|
@ -220,14 +193,6 @@ 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",
|
||||
|
|
@ -245,30 +210,6 @@ 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,10 +87,6 @@ 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 _);
|
||||
|
|
@ -110,7 +106,5 @@ 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.KernelOpenCore(ctx);
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqQMh1zPPT8",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
stream.SetLength(length);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ 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];
|
||||
|
|
@ -98,7 +96,6 @@ 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();
|
||||
|
|
@ -1442,9 +1439,7 @@ public static partial class KernelMemoryCompatExports
|
|||
ExportName = "_open",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
|
||||
|
||||
internal static int KernelOpenCore(CpuContext ctx)
|
||||
public static int KernelOpenUnderscore(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
|
@ -1455,19 +1450,6 @@ 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);
|
||||
|
|
@ -1537,7 +1519,7 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
|
||||
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
|
||||
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
|
||||
if ((flags & O_APPEND) != 0)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
|
@ -1695,11 +1677,11 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// POSIX open(2): translates a failed raw open into -1/errno. On success
|
||||
// KernelOpenCore already writes the fd into RAX (the import bridge
|
||||
// KernelOpenUnderscore 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 = KernelOpenCore(ctx);
|
||||
var result = KernelOpenUnderscore(ctx);
|
||||
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
? 0
|
||||
: PosixFailure(ctx, result);
|
||||
|
|
@ -1752,8 +1734,7 @@ public static partial class KernelMemoryCompatExports
|
|||
{
|
||||
// Stop at the first miss and report its index.
|
||||
// The caller can then use its normal file-open fallback.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
||||
if (sizesAddress != 0 &&
|
||||
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
|
||||
{
|
||||
|
|
@ -2273,12 +2254,7 @@ public static partial class KernelMemoryCompatExports
|
|||
string? observedBinkPath = null;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_randomDeviceDescriptors.Remove(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
else if (_openFiles.Remove(fd, out stream))
|
||||
if (_openFiles.Remove(fd, out stream))
|
||||
{
|
||||
_binkGuestCompletionShims.Remove(fd);
|
||||
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
|
||||
|
|
@ -2331,25 +2307,6 @@ 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,
|
||||
|
|
@ -2401,41 +2358,53 @@ public static partial class KernelMemoryCompatExports
|
|||
positionBefore = -1;
|
||||
}
|
||||
|
||||
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||
var read = stream.Read(buffer, 0, requested);
|
||||
if (read > 0 && useBinkCompletionShim)
|
||||
{
|
||||
// The patched NumFrames field is what tells the guest "this
|
||||
// movie is fully consumed" - hold that specific read until the
|
||||
// host has actually finished showing it, so guest-side game
|
||||
// logic can't race ahead of what's still on screen.
|
||||
if (completionShim.Patch(positionBefore, buffer.AsSpan(0, read)))
|
||||
{
|
||||
HostMovieBridge.WaitForHostPlaybackToFinish(stream.Name);
|
||||
}
|
||||
}
|
||||
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
long positionAfter;
|
||||
// Guest reads are frequent and mostly small: a pooled buffer spares each one an allocation.
|
||||
var pooled = requested <= MaxPooledReadBytes;
|
||||
var buffer = pooled ? ArrayPool<byte>.Shared.Rent(requested) : GC.AllocateUninitializedArray<byte>(requested);
|
||||
try
|
||||
{
|
||||
positionAfter = stream.Position;
|
||||
var read = stream.Read(buffer, 0, requested);
|
||||
if (read > 0 && useBinkCompletionShim)
|
||||
{
|
||||
// The patched NumFrames field is what tells the guest "this
|
||||
// movie is fully consumed" - hold that specific read until the
|
||||
// host has actually finished showing it, so guest-side game
|
||||
// logic can't race ahead of what's still on screen.
|
||||
if (completionShim.Patch(positionBefore, buffer.AsSpan(0, read)))
|
||||
{
|
||||
HostMovieBridge.WaitForHostPlaybackToFinish(stream.Name);
|
||||
}
|
||||
}
|
||||
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
long positionAfter;
|
||||
try
|
||||
{
|
||||
positionAfter = stream.Position;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
positionAfter = -1;
|
||||
}
|
||||
|
||||
LogIoTrace(
|
||||
"read",
|
||||
stream.Name,
|
||||
$"fd={fd} req={requested} read={read} pos={positionBefore}->{positionAfter} preview='{PreviewIoBytes(buffer, read, 64)}' hex={PreviewIoHex(buffer, read, 32)} guest_tail={PreviewGuestHex(ctx, bufferAddress + (ulong)Math.Max(read, 0), 32)}");
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
finally
|
||||
{
|
||||
positionAfter = -1;
|
||||
if (pooled)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
LogIoTrace(
|
||||
"read",
|
||||
stream.Name,
|
||||
$"fd={fd} req={requested} read={read} pos={positionBefore}->{positionAfter} preview='{PreviewIoBytes(buffer, read, 64)}' hex={PreviewIoHex(buffer, read, 32)} guest_tail={PreviewGuestHex(ctx, bufferAddress + (ulong)Math.Max(read, 0), 32)}");
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -2885,21 +2854,13 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
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);
|
||||
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
|
||||
var searchEnd = searchEndRaw <= 0
|
||||
? GuestMemoryLayout.DirectBytes
|
||||
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
|
||||
if (searchStart >= searchEnd)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
bool foundSpan;
|
||||
|
|
@ -2917,7 +2878,7 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
if (!foundSpan)
|
||||
{
|
||||
return MemoryNoSpace;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
|
||||
|
|
@ -6952,14 +6913,9 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
string? hostPath = null;
|
||||
bool isDirectory = false;
|
||||
bool isRandomDevice = false;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_randomDeviceDescriptors.Contains(fd))
|
||||
{
|
||||
isRandomDevice = true;
|
||||
}
|
||||
else if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
if (_openDirectories.TryGetValue(fd, out var directory))
|
||||
{
|
||||
hostPath = directory.Path;
|
||||
isDirectory = true;
|
||||
|
|
@ -6970,14 +6926,6 @@ 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;
|
||||
|
|
@ -6999,10 +6947,6 @@ 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);
|
||||
|
|
@ -7274,9 +7218,41 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// Read once: file-heavy titles call LogIoTrace for every resolved path.
|
||||
private const int MaxPooledReadBytes = 1 << 20;
|
||||
|
||||
private static readonly bool _logIo =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Builds an I/O trace detail only while SHARPEMU_LOG_IO is on: its holes (byte previews of
|
||||
// every read) are otherwise never evaluated.
|
||||
[System.Runtime.CompilerServices.InterpolatedStringHandler]
|
||||
private ref struct IoTraceDetail
|
||||
{
|
||||
private System.Runtime.CompilerServices.DefaultInterpolatedStringHandler _inner;
|
||||
|
||||
public IoTraceDetail(int literalLength, int formattedCount, out bool enabled)
|
||||
{
|
||||
enabled = _logIo;
|
||||
_inner = enabled ? new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(literalLength, formattedCount) : default;
|
||||
}
|
||||
|
||||
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
|
||||
|
||||
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
|
||||
|
||||
public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
|
||||
|
||||
public string ToStringAndClear() => _inner.ToStringAndClear();
|
||||
}
|
||||
|
||||
private static void LogIoTrace(string operation, string path, IoTraceDetail detail)
|
||||
{
|
||||
if (_logIo)
|
||||
{
|
||||
LogIoTrace(operation, path, detail.ToStringAndClear());
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIoTrace(string operation, string path, string detail)
|
||||
{
|
||||
if (!_logIo)
|
||||
|
|
|
|||
|
|
@ -1054,12 +1054,7 @@ public static class KernelPthreadCompatExports
|
|||
}
|
||||
|
||||
if (canCooperativelyBlock && waiter is not null &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
RequestBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
|
@ -1070,6 +1065,21 @@ public static class KernelPthreadCompatExports
|
|||
return hostResult;
|
||||
}
|
||||
|
||||
// The continuations capture the lock's arguments in a closure; keeping them out of
|
||||
// PthreadMutexLockCore spares every uncontended lock that allocation.
|
||||
private static bool RequestBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter) =>
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter));
|
||||
|
||||
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
|
|
@ -1988,7 +1998,7 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
var waitDuration = TimeSpan.FromMilliseconds(10);
|
||||
var waitMilliseconds = 10;
|
||||
if (timed)
|
||||
{
|
||||
var remaining = GetRemainingTimeout(deadline);
|
||||
|
|
@ -1998,13 +2008,14 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
if (remaining < waitDuration)
|
||||
{
|
||||
waitDuration = remaining;
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
|
||||
_ = Monitor.Wait(state.SyncRoot, waitDuration);
|
||||
_ = Monitor.Wait(state.SyncRoot, waitMilliseconds);
|
||||
if (waiter.CompletionState == 0 &&
|
||||
timed &&
|
||||
GetRemainingTimeout(deadline) <= TimeSpan.Zero)
|
||||
|
|
|
|||
|
|
@ -80,13 +80,6 @@ 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,7 +144,8 @@ public static class KernelSyncOnAddressCompatExports
|
|||
{
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
Monitor.Wait(_hostGate, waitSlice);
|
||||
// Round up so a sub-millisecond remainder does not spin.
|
||||
Monitor.Wait(_hostGate, (int)Math.Ceiling(waitSlice.TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue