Revert the 18 Silent Hill rendering and performance merges (#1065)
Restore review-only delivery at the user request. Reverts #1024, #1025, #1031, #1032, #1033, #1034, #1035, #1036, #1037, #1038, #1039, #1040, #1041, #1051, #1054, #1055, #1058, and #1059. The resulting tree matches 3e2ad633 exactly.
This commit is contained in:
parent
775141d58c
commit
e1edda7772
|
|
@ -1,112 +0,0 @@
|
|||
<!--
|
||||
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,7 +9,6 @@ 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;
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ internal static partial class Program
|
|||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
RosettaGcBudget.ReexecIfNeeded(args);
|
||||
ConfigureManagedPluginResolution();
|
||||
|
||||
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
|
||||
|
|
@ -1012,7 +1010,7 @@ internal static partial class Program
|
|||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--guest-resolution=<1920x1080|3840x2160>] [--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("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
|
||||
}
|
||||
|
|
@ -1077,8 +1075,6 @@ 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;
|
||||
|
|
@ -1138,31 +1134,6 @@ internal static partial class Program
|
|||
windowHeightOverride = windowHeight;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--guest-resolution", out var guestResolutionText))
|
||||
{
|
||||
if (guestResolutionText is not ("1920x1080" or "3840x2160"))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
guestResolutionOverride = guestResolutionText == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--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)
|
||||
|
|
@ -1420,8 +1391,6 @@ 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,
|
||||
|
|
@ -1462,9 +1431,6 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.CLI;
|
||||
|
||||
/// <summary>
|
||||
/// Under Rosetta 2 every garbage collection suspends the runtime with
|
||||
/// FlushProcessWriteBuffers, which reads each thread's registers through a Mach call that
|
||||
/// Rosetta services by parking the thread in its runtime. A signal that reaches a parked
|
||||
/// thread (guest memory tracking raises thousands per second) trips Rosetta's "expected saved
|
||||
/// LR to be in translated code" assertion and wedges the process. With the default gen0 budget
|
||||
/// the emulator collects about twice a second; a 64 MiB budget makes that rare.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The GC reads its gen0 budget only from the environment when the runtime starts, so the
|
||||
/// process re-executes itself once with the variable set. execve keeps the process id.
|
||||
/// </remarks>
|
||||
internal static class RosettaGcBudget
|
||||
{
|
||||
private const string BudgetVariable = "DOTNET_GCgen0size";
|
||||
// 64 MiB: a larger budget turns every collection into a blocking gen1/gen2 pass of
|
||||
// about a second (512 MiB measured 8 such pauses in 150 s of Demon's Souls).
|
||||
private const string Budget = "0x4000000";
|
||||
private const string OptOutVariable = "SHARPEMU_ROSETTA_GC_BUDGET";
|
||||
|
||||
public static void ReexecIfNeeded(string[] args)
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
|
||||
Environment.GetEnvironmentVariable(BudgetVariable) is not null ||
|
||||
Environment.GetEnvironmentVariable(OptOutVariable) == "0" ||
|
||||
!IsTranslated())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var environment = new List<string>();
|
||||
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
||||
{
|
||||
environment.Add($"{entry.Key}={entry.Value}");
|
||||
}
|
||||
environment.Add($"{BudgetVariable}={Budget}");
|
||||
|
||||
var argv = BuildArguments(path, Environment.GetCommandLineArgs()[0], args);
|
||||
var nativeArguments = ToNullTerminated(argv);
|
||||
var nativeEnvironment = ToNullTerminated(environment);
|
||||
// Only returns on failure; the emulator then runs with the default budget.
|
||||
_ = Execve(path, nativeArguments, nativeEnvironment);
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
foreach (var pointer in nativeArguments) Marshal.FreeCoTaskMem(pointer);
|
||||
foreach (var pointer in nativeEnvironment) Marshal.FreeCoTaskMem(pointer);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Could not restart with a larger GC budget under Rosetta: errno={error}");
|
||||
}
|
||||
|
||||
internal static string[] BuildArguments(string executable, string entryAssembly, string[] args) =>
|
||||
Path.GetFileNameWithoutExtension(executable).Equals("dotnet", StringComparison.OrdinalIgnoreCase)
|
||||
? [executable, entryAssembly, .. args]
|
||||
: [executable, .. args];
|
||||
|
||||
private static bool IsTranslated()
|
||||
{
|
||||
var value = 0;
|
||||
var size = (nint)sizeof(int);
|
||||
return SysctlByName("sysctl.proc_translated", ref value, ref size, 0, 0) == 0 && value == 1;
|
||||
}
|
||||
|
||||
private static nint[] ToNullTerminated(IReadOnlyList<string> values)
|
||||
{
|
||||
var pointers = new nint[values.Count + 1];
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
pointers[i] = Marshal.StringToCoTaskMemUTF8(values[i]);
|
||||
}
|
||||
return pointers;
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "execve", SetLastError = true)]
|
||||
private static extern int Execve(
|
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] string path,
|
||||
nint[] argv,
|
||||
nint[] envp);
|
||||
|
||||
[DllImport("libc", EntryPoint = "sysctlbyname")]
|
||||
private static extern int SysctlByName(
|
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
|
||||
ref int value,
|
||||
ref nint size,
|
||||
nint newValue,
|
||||
nint newSize);
|
||||
}
|
||||
|
|
@ -261,40 +261,23 @@ public sealed unsafe partial class DirectExecutionBackend
|
|||
return code;
|
||||
}
|
||||
|
||||
// SHARPEMU_MEMCPY_MANAGED_MIN overrides the size from which guest memcpy takes the managed path.
|
||||
internal static readonly int ManagedCopyMinimumBytes =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_MEMCPY_MANAGED_MIN"), out var minimum) && minimum > 0 ? minimum : 4096;
|
||||
|
||||
/// <summary>
|
||||
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
|
||||
/// export copies with memmove semantics, so a destination that starts inside the source
|
||||
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
|
||||
/// So does a copy of at least <see cref="ManagedCopyMinimumBytes"/>: the native copy takes
|
||||
/// one write-tracking fault per GPU-watched page it writes, a signal round trip of about
|
||||
/// 0.2 ms under Rosetta, while the managed copy marks the whole destination written first.
|
||||
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
|
||||
/// flag is clear on entry per the SysV ABI.
|
||||
/// </summary>
|
||||
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
|
||||
{
|
||||
var code = new List<byte>(48);
|
||||
var slowPathFixups = new List<int>();
|
||||
var code = new List<byte>(40);
|
||||
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
|
||||
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
|
||||
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
|
||||
slowPathFixups.Add(code.Count - 1);
|
||||
// cmp rdx, imm32 / jae slow
|
||||
code.AddRange([0x48, 0x81, 0xFA]);
|
||||
code.AddRange(BitConverter.GetBytes(ManagedCopyMinimumBytes));
|
||||
code.AddRange([0x73, 0x00]);
|
||||
slowPathFixups.Add(code.Count - 1);
|
||||
var slowPathFixup = code.Count - 1;
|
||||
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
|
||||
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
|
||||
foreach (var fixup in slowPathFixups)
|
||||
{
|
||||
code[fixup] = checked((byte)(code.Count - (fixup + 1)));
|
||||
}
|
||||
|
||||
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
|
||||
// jmp qword [rip+0] with the absolute trampoline address after it.
|
||||
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
|
||||
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
|
||||
|
|
|
|||
|
|
@ -3978,11 +3978,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
// The handle and context this host thread registered last; a repeat registration (every
|
||||
// scePthreadSelf on an external guest thread, tens of thousands a second) then skips the
|
||||
// scheduler gate.
|
||||
[ThreadStatic] private static ulong _registeredExternalHandle;
|
||||
[ThreadStatic] private static CpuContext? _registeredExternalContext;
|
||||
public bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
|
|
@ -4017,12 +4012,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return;
|
||||
}
|
||||
|
||||
if (_registeredExternalHandle == threadHandle && ReferenceEquals(_registeredExternalContext, context))
|
||||
{
|
||||
Volatile.Write(ref _currentExternalGuestThreadHandle, threadHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
_currentExternalGuestThreadHandle = threadHandle;
|
||||
|
|
@ -4036,19 +4025,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
existing.Context = context;
|
||||
Volatile.Write(ref existing.HostThreadId, hostThreadId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
_registeredExternalHandle = threadHandle;
|
||||
_registeredExternalContext = context;
|
||||
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
|
||||
{
|
||||
Context = context,
|
||||
Name = $"External-{threadHandle:X}",
|
||||
HostThreadId = hostThreadId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6397,15 +6382,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
const uint stubSize = 512u;
|
||||
// The stub page and host-RSP slot are kept per host thread and nesting depth: allocating
|
||||
// and releasing them on every resume cost an mmap/munmap pair and two acquisitions of the
|
||||
// host-memory gate, which the GPU write tracker holds across its own protection changes.
|
||||
var depth = _continuationDepth;
|
||||
if (!TryAcquireContinuationStub(depth, stubSize, out var ptr, out var hostRspStorage, out reason))
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (ptr == null)
|
||||
{
|
||||
reason = "failed to allocate executable memory for guest thread stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
void* hostRspStorage = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
|
||||
if (hostRspStorage == null)
|
||||
{
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
_continuationDepth = depth + 1;
|
||||
var previousActiveBackend = _activeExecutionBackend;
|
||||
var previousActiveContext = _activeCpuContext;
|
||||
var previousSentinel = _activeEntryReturnSentinelRip;
|
||||
|
|
@ -6479,7 +6468,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
reason = $"failed to patch guest continuation return slot at 0x{returnSlotAddress:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
if (!ProtectContinuationStub(ptr, stubSize, executable: true))
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
reason = "failed to seal guest continuation stub execute-read";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
|
|
@ -6539,85 +6529,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
_continuationDepth = depth;
|
||||
NativeMemory.Free(hostRspStorage);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic] private static nint[]? _continuationStubPages;
|
||||
[ThreadStatic] private static nint[]? _continuationHostRspSlots;
|
||||
[ThreadStatic] private static int _continuationDepth;
|
||||
|
||||
// Returns this thread's stub page for the given nesting depth, writable and with a zeroed
|
||||
// host-RSP slot; the first use at a depth allocates them, and they live as long as the thread.
|
||||
private unsafe bool TryAcquireContinuationStub(int depth, uint stubSize, out void* stub, out void* hostRspStorage, out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
stub = null;
|
||||
hostRspStorage = null;
|
||||
var pages = _continuationStubPages ??= new nint[4];
|
||||
var slots = _continuationHostRspSlots ??= new nint[4];
|
||||
if (depth >= pages.Length)
|
||||
{
|
||||
Array.Resize(ref pages, depth + 2);
|
||||
Array.Resize(ref slots, depth + 2);
|
||||
_continuationStubPages = pages;
|
||||
_continuationHostRspSlots = slots;
|
||||
}
|
||||
|
||||
if (pages[depth] == 0)
|
||||
{
|
||||
var page = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (page == null)
|
||||
{
|
||||
reason = "failed to allocate executable memory for guest thread stub";
|
||||
return false;
|
||||
}
|
||||
|
||||
var slot = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
|
||||
if (slot == null)
|
||||
{
|
||||
VirtualFree(page, 0u, 32768u);
|
||||
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
|
||||
return false;
|
||||
}
|
||||
|
||||
pages[depth] = (nint)page;
|
||||
slots[depth] = (nint)slot;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ProtectContinuationStub((void*)pages[depth], stubSize, executable: false))
|
||||
{
|
||||
reason = "failed to reopen guest continuation stub for writing";
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMemory.Clear((void*)slots[depth], (nuint)HostStackStateBytes);
|
||||
}
|
||||
|
||||
stub = (void*)pages[depth];
|
||||
hostRspStorage = (void*)slots[depth];
|
||||
return true;
|
||||
}
|
||||
|
||||
// The stub page is emulator-private, so on POSIX its protection changes bypass the tracked
|
||||
// host-memory layer and its gate.
|
||||
private static unsafe bool ProtectContinuationStub(void* stub, uint stubSize, bool executable)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
uint oldProtect = 0;
|
||||
return VirtualProtect(stub, stubSize, executable ? 32u : 4u, &oldProtect);
|
||||
}
|
||||
|
||||
var pageSize = (nint)Environment.SystemPageSize;
|
||||
var page = (nint)stub & ~(pageSize - 1);
|
||||
return PosixMprotect(page, (nuint)pageSize, executable ? 1 | 4 : 1 | 2) == 0;
|
||||
}
|
||||
|
||||
[DllImport("libc", EntryPoint = "mprotect")]
|
||||
private static extern int PosixMprotect(nint address, nuint length, int protection);
|
||||
|
||||
// The continuation trampoline is rebuilt on every blocked-thread resume.
|
||||
// Keep its tiny writer on the stack: capturing local emit functions create a
|
||||
// managed display-class allocation on this extremely hot path.
|
||||
|
|
|
|||
|
|
@ -322,43 +322,20 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
|
||||
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
|
||||
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
|
||||
// on an ignored error turned UE's idle HTTP thread into a hot spin.
|
||||
var nanosleep = ResolveLibcExport("nanosleep");
|
||||
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
|
||||
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||
var usleep = ResolveLibcExport("usleep");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x53); // push rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
|
||||
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
|
||||
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
|
||||
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
|
||||
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
|
||||
// loop:
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
|
||||
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
|
||||
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
|
||||
Emit(page, ref offset, 0x74, 0x26); // jz done
|
||||
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
|
||||
Emit(page, ref offset, 0x75, 0x15); // jne done
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
|
||||
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
|
||||
// done:
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
|
||||
Emit(page, ref offset, 0x5B); // pop rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
|
|
|
|||
|
|
@ -70,28 +70,6 @@ public sealed class GuiSettings
|
|||
|
||||
public string Resolution { get; set; } = "1920x1080";
|
||||
|
||||
public string GuestResolution { get; set; } = "1920x1080";
|
||||
|
||||
internal static string NormalizeGuestResolution(string? value) =>
|
||||
string.Equals(value, "3840x2160", StringComparison.OrdinalIgnoreCase) ? "3840x2160" : "1920x1080";
|
||||
|
||||
// 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; }
|
||||
|
|
@ -111,6 +89,9 @@ public sealed class GuiSettings
|
|||
|
||||
public string BinkPlaybackMode { get; set; } = "Guest";
|
||||
|
||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
||||
public double RenderResolutionScale { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
|
|
@ -203,8 +184,6 @@ public sealed class GuiSettings
|
|||
"Ukrainian");
|
||||
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
||||
settings.Resolution = NormalizeResolution(settings.Resolution);
|
||||
settings.GuestResolution = NormalizeGuestResolution(settings.GuestResolution);
|
||||
settings.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,10 +219,6 @@
|
|||
"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,10 +74,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklüziv",
|
||||
"Options.Resolution.Label": "Təsvir ölçüsü",
|
||||
"Options.Resolution.Desc": "Pəncərənin başlanğıc ölçüsü və ya eksklüziv tam ekranın təsvir ölçüsü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran rejimi",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilən ekran təsvir ölçüsü. Standart 1080p-dir. Daxili render ölçüsünü oyun seçir; sabit ölçülü oyunlar bu ayarı nəzərə almaya bilər.",
|
||||
"Options.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,10 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de tela cheia exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de exibição do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução informada ao jogo. O padrão é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.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,10 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exklusiv",
|
||||
"Options.Resolution.Label": "Auflösung",
|
||||
"Options.Resolution.Desc": "Anfängliche Fenstergröße oder exklusive Vollbildauflösung.",
|
||||
"Options.GuestResolution.Label": "Anzeigemodus des Spiels",
|
||||
"Options.GuestResolution.Desc": "Dem Spiel gemeldete Bildschirmauflösung. Standard ist 1080p. Spiele wählen ihre interne Renderauflösung selbst; Spiele mit fester Auflösung können diese Einstellung ignorieren.",
|
||||
"Options.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,10 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiv",
|
||||
"Options.Resolution.Label": "Opløsning",
|
||||
"Options.Resolution.Desc": "Oprindelig vinduesstørrelse eller opløsning i eksklusiv fuldskærm.",
|
||||
"Options.GuestResolution.Label": "Spillets skærmtilstand",
|
||||
"Options.GuestResolution.Desc": "Skærmopløsning oplyst til spillet. Standard er 1080p. Spillet vælger den interne renderopløsning; spil med fast opløsning kan ignorere denne indstilling.",
|
||||
"Options.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,10 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiivne",
|
||||
"Options.Resolution.Label": "Eraldusvõime",
|
||||
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
|
||||
"Options.GuestResolution.Label": "Mängu kuvarežiim",
|
||||
"Options.GuestResolution.Desc": "Mängule teatatud ekraani eraldusvõime. Vaikimisi 1080p. Mäng valib sisemise renderduse eraldusvõime; fikseeritud eraldusvõimega mängud võivad seda sätet eirata.",
|
||||
"Options.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,10 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusive",
|
||||
"Options.Resolution.Label": "Resolution",
|
||||
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
|
||||
"Options.GuestResolution.Label": "Game display mode",
|
||||
"Options.GuestResolution.Desc": "Display resolution reported to the game. Defaults to 1080p. Games choose their internal rendering resolution; fixed-resolution games may ignore this setting.",
|
||||
"Options.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,10 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolución",
|
||||
"Options.Resolution.Desc": "Tamaño inicial de la ventana o resolución de pantalla completa exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de pantalla del juego",
|
||||
"Options.GuestResolution.Desc": "Resolución de pantalla comunicada al juego. El valor predeterminado es 1080p. El juego elige su resolución interna de renderizado; los juegos con resolución fija pueden ignorar este ajuste.",
|
||||
"Options.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,10 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusif",
|
||||
"Options.Resolution.Label": "Résolution",
|
||||
"Options.Resolution.Desc": "Taille initiale de la fenêtre ou résolution du plein écran exclusif.",
|
||||
"Options.GuestResolution.Label": "Mode d’affichage du jeu",
|
||||
"Options.GuestResolution.Desc": "Résolution d’écran indiquée au jeu. La valeur par défaut est 1080p. Le jeu choisit sa résolution de rendu interne ; les jeux à résolution fixe peuvent ignorer ce réglage.",
|
||||
"Options.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,10 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Kizárólagos",
|
||||
"Options.Resolution.Label": "Felbontás",
|
||||
"Options.Resolution.Desc": "Kezdeti ablakméret vagy kizárólagos teljes képernyős felbontás.",
|
||||
"Options.GuestResolution.Label": "A játék megjelenítési módja",
|
||||
"Options.GuestResolution.Desc": "A játéknak jelzett képernyőfelbontás. Alapértelmezés: 1080p. A belső renderelési felbontást a játék választja; a rögzített felbontású játékok figyelmen kívül hagyhatják ezt a beállítást.",
|
||||
"Options.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,10 +224,6 @@
|
|||
"Options.WindowMode.Exclusive": "Esclusiva",
|
||||
"Options.Resolution.Label": "Risoluzione",
|
||||
"Options.Resolution.Desc": "Dimensione iniziale della finestra o risoluzione dello schermo intero esclusivo.",
|
||||
"Options.GuestResolution.Label": "Modalità schermo del gioco",
|
||||
"Options.GuestResolution.Desc": "Risoluzione dello schermo comunicata al gioco. Il valore predefinito è 1080p. Il gioco sceglie la risoluzione interna di rendering; i giochi con risoluzione fissa possono ignorare questa impostazione.",
|
||||
"Options.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,10 +219,6 @@
|
|||
"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,10 +219,6 @@
|
|||
"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,10 +219,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusief",
|
||||
"Options.Resolution.Label": "Resolutie",
|
||||
"Options.Resolution.Desc": "Initiële venstergrootte of resolutie voor exclusief volledig scherm.",
|
||||
"Options.GuestResolution.Label": "Weergavemodus van het spel",
|
||||
"Options.GuestResolution.Desc": "Schermresolutie die aan het spel wordt doorgegeven. Standaard 1080p. Het spel kiest de interne renderresolutie; spellen met een vaste resolutie kunnen deze instelling negeren.",
|
||||
"Options.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,10 +85,6 @@
|
|||
"Options.WindowMode.Exclusive": "Wyłączne",
|
||||
"Options.Resolution.Label": "Rozdzielczość",
|
||||
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
|
||||
"Options.GuestResolution.Label": "Tryb wyświetlania gry",
|
||||
"Options.GuestResolution.Desc": "Rozdzielczość ekranu zgłaszana grze. Domyślnie 1080p. Gra wybiera wewnętrzną rozdzielczość renderowania; gry ze stałą rozdzielczością mogą zignorować to ustawienie.",
|
||||
"Options.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,10 +220,6 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusivo",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de ecrã inteiro exclusivo.",
|
||||
"Options.GuestResolution.Label": "Modo de apresentação do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução do ecrã comunicada ao jogo. A predefinição é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.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,10 +81,6 @@
|
|||
"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,10 +63,6 @@
|
|||
"Options.WindowMode.Desc": "Normal pencere, kenarlıksız masaüstü veya özel tam ekran.",
|
||||
"Options.Resolution.Label": "Çözünürlük",
|
||||
"Options.Resolution.Desc": "Başlangıç pencere boyutu veya özel tam ekran çözünürlüğü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran modu",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilen ekran çözünürlüğü. Varsayılan 1080p’dir. Dahili işleme çözünürlüğünü oyun seçer; sabit çözünürlüklü oyunlar bu ayarı yok sayabilir.",
|
||||
"Options.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,8 +45,6 @@ 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;
|
||||
|
|
@ -117,8 +115,6 @@ 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();
|
||||
|
|
@ -220,8 +216,6 @@ 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");
|
||||
|
|
@ -283,8 +277,6 @@ 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,22 +856,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="GameGuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GameGuestResolutionBox"
|
||||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="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],
|
||||
|
|
@ -1506,18 +1490,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GuestResolutionBox" Classes="optionValue" VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="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,9 +322,6 @@ public partial class MainWindow : Window
|
|||
WindowModeBox.SelectionChanged += (_, _) => _settings.WindowMode = SelectedComboText(WindowModeBox, "Windowed");
|
||||
DisplayBox.SelectionChanged += (_, _) => OnHostDisplayChanged();
|
||||
ResolutionBox.SelectionChanged += (_, _) => OnHostResolutionChanged();
|
||||
GuestResolutionBox.SelectionChanged += (_, _) => _settings.GuestResolution = SelectedComboText(GuestResolutionBox, "1920x1080");
|
||||
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;
|
||||
|
|
@ -1252,8 +1249,6 @@ 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;
|
||||
|
|
@ -1329,8 +1324,6 @@ public partial class MainWindow : Window
|
|||
LoadHostDisplayOptions();
|
||||
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
||||
VSyncToggle.IsChecked = _settings.VSync;
|
||||
GuestResolutionBox.SelectedItem = GuiSettings.NormalizeGuestResolution(_settings.GuestResolution);
|
||||
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");
|
||||
|
|
@ -2567,6 +2560,13 @@ public partial class MainWindow : Window
|
|||
});
|
||||
_appliedEnvironmentVariables.Add(BinkModeEnvironmentName);
|
||||
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_RENDER_SCALE",
|
||||
_settings.RenderResolutionScale.ToString(
|
||||
"0.###",
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
_appliedEnvironmentVariables.Add("SHARPEMU_RENDER_SCALE");
|
||||
|
||||
foreach (var entry in customEnvironment)
|
||||
{
|
||||
if (!CustomEnvironmentVariables.TryParseEntry(entry, out var name, out var value))
|
||||
|
|
@ -2784,9 +2784,6 @@ public partial class MainWindow : Window
|
|||
|
||||
arguments.Add($"--window-mode={launch.Settings.WindowMode.ToLowerInvariant()}");
|
||||
arguments.Add($"--resolution={launch.Settings.Resolution}");
|
||||
arguments.Add($"--guest-resolution={launch.Settings.GuestResolution}");
|
||||
arguments.Add(
|
||||
$"--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,10 +26,6 @@ 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; }
|
||||
|
|
@ -56,8 +52,6 @@ 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 &&
|
||||
|
|
@ -151,17 +145,6 @@ 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;
|
||||
|
|
@ -289,8 +272,6 @@ public sealed record EffectiveLaunchSettings(
|
|||
bool LogToFile,
|
||||
string WindowMode,
|
||||
string Resolution,
|
||||
string GuestResolution,
|
||||
float RenderScale,
|
||||
int DisplayIndex,
|
||||
int RefreshRate,
|
||||
string ScalingMode,
|
||||
|
|
@ -308,8 +289,6 @@ 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,108 +242,50 @@ public sealed class GuestPageTracker
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Region locks stay held across uploadFunc only for a written range, which turns GPU-dirty.
|
||||
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;
|
||||
// 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();
|
||||
var held = new List<TrackedRegion>();
|
||||
var cleared = new List<(TrackedRegion Region, ulong Address, ulong Size)>();
|
||||
try
|
||||
{
|
||||
using (GpuMemoryAccessProfile.Measure(GpuMemoryAccessProfile.Operation.UploadTracking, size))
|
||||
{
|
||||
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)
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
{
|
||||
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);
|
||||
region.ForEachCpuUploadRange(preserveHotPages, region.BaseAddress + offset, bytes, ref visitor);
|
||||
var address = region.BaseAddress + offset;
|
||||
region.ForEachCpuUploadRange(
|
||||
preserveHotPages: !isWritten && preserveCpuWriteHotPages,
|
||||
address,
|
||||
bytes,
|
||||
(runAddress, runSize) => cleared.Add((region, runAddress, runSize)),
|
||||
rangeFunc);
|
||||
if (!isWritten)
|
||||
{
|
||||
region.Lock.Exit();
|
||||
held.RemoveAt(held.Count - 1);
|
||||
held.Remove(region);
|
||||
}
|
||||
}
|
||||
|
||||
sink = visitor.Sink;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
sink.Upload();
|
||||
uploadFunc();
|
||||
if (isWritten)
|
||||
{
|
||||
for (var (index, offset, remaining) = (vaddr / BlockBytes, vaddr % BlockBytes, size); remaining != 0; index++, offset = 0)
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
{
|
||||
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
|
||||
|
|
@ -391,8 +333,6 @@ public sealed class GuestPageTracker
|
|||
region.Lock.Exit();
|
||||
}
|
||||
|
||||
held.Clear();
|
||||
cleared.Clear();
|
||||
_uploadOwner = previousOwner;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,24 +144,20 @@ 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<TVisitor>(bool preserveHotPages, ulong address, ulong size, ref TVisitor visitor)
|
||||
where TVisitor : struct, ICpuUploadVisitor
|
||||
public void ForEachCpuUploadRange(
|
||||
bool preserveHotPages,
|
||||
ulong address,
|
||||
ulong size,
|
||||
Action<ulong, ulong> visitCleared,
|
||||
Action<ulong, ulong> visitUpload)
|
||||
{
|
||||
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)
|
||||
{
|
||||
visitor.Cleared(this, BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitCleared(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
_cpuDirty.UnsetRange(runStart, runEnd);
|
||||
_recentCpuUploads.SetRange(runStart, runEnd);
|
||||
}
|
||||
|
|
@ -169,7 +165,7 @@ public sealed class TrackedRegion
|
|||
UpdateCpuProtection(track: true);
|
||||
foreach (var (runStart, runEnd) in upload)
|
||||
{
|
||||
visitor.Upload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitUpload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,43 +462,20 @@ internal static unsafe class PosixHostStubs
|
|||
|
||||
private static nint EmitSleep(byte* page, ref int offset)
|
||||
{
|
||||
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
|
||||
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
|
||||
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
|
||||
// on an ignored error turned UE's idle HTTP thread into a hot spin.
|
||||
var nanosleep = ResolveLibcExport("nanosleep");
|
||||
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
|
||||
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
|
||||
var usleep = ResolveLibcExport("usleep");
|
||||
var start = (nint)(page + offset);
|
||||
Emit(page, ref offset, 0x56); // push rsi
|
||||
Emit(page, ref offset, 0x57); // push rdi
|
||||
Emit(page, ref offset, 0x53); // push rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
|
||||
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
||||
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
|
||||
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
|
||||
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
|
||||
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
|
||||
// loop:
|
||||
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
|
||||
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
|
||||
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
||||
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
||||
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
|
||||
Emit(page, ref offset, 0x76, 0x05); // jbe +5
|
||||
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
|
||||
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
|
||||
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
|
||||
Emit(page, ref offset, 0x74, 0x26); // jz done
|
||||
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
|
||||
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
||||
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
|
||||
Emit(page, ref offset, 0x75, 0x15); // jne done
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
|
||||
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
|
||||
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
|
||||
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
|
||||
// done:
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
|
||||
Emit(page, ref offset, 0x5B); // pop rbx
|
||||
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
||||
Emit(page, ref offset, 0x5F); // pop rdi
|
||||
Emit(page, ref offset, 0x5E); // pop rsi
|
||||
Emit(page, ref offset, 0xC3); // ret
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.HLE.Host.Posix;
|
||||
|
||||
// View mappings bypass the anonymous-allocation tables. Keep their query state together.
|
||||
|
|
@ -13,12 +11,6 @@ internal static class PosixViewRegions
|
|||
uint[]? PageProtections, ulong PageBase);
|
||||
private static readonly List<Region> Regions = new();
|
||||
|
||||
// The regions in address order, replaced whenever the list changes. Queries read it without
|
||||
// Gate: every HLE import queries the guest stack's protection first, and Gate is held across
|
||||
// mprotect by the GPU write tracker, so a locked query made each import wait on page traffic.
|
||||
// Page protections change in place and are read racily, as a concurrent change always was.
|
||||
private static Region[] _snapshot = [];
|
||||
|
||||
internal static void Replace(ulong address, ulong size, uint state, uint protection)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
|
|
@ -54,8 +46,6 @@ internal static class PosixViewRegions
|
|||
else
|
||||
Regions.Add(region);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _snapshot, [.. Regions]);
|
||||
}
|
||||
|
||||
// Mapping creates the storage. Fault-time protection updates must not allocate.
|
||||
|
|
@ -76,8 +66,9 @@ internal static class PosixViewRegions
|
|||
|
||||
internal static bool TryQuery(ulong address, out HostMemory.BasicInfo info)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
foreach (var region in Volatile.Read(ref _snapshot))
|
||||
foreach (var region in Regions)
|
||||
{
|
||||
if (region.Start > address) break;
|
||||
if (address >= region.End) continue;
|
||||
|
|
|
|||
|
|
@ -149,14 +149,6 @@ public static unsafe class HostMemory
|
|||
private static readonly object Gate = new();
|
||||
private static readonly SortedList<ulong, Region> Regions = new();
|
||||
|
||||
// The regions in address order, replaced under Gate whenever a region is added or removed.
|
||||
// Queries read it without the lock: Protect holds Gate across its mprotect call, and GPU
|
||||
// write tracking protects pages constantly, so a locked query (one per HLE call reading
|
||||
// stack arguments) waited tens of microseconds behind those syscalls.
|
||||
private static Region[] RegionSnapshot = [];
|
||||
|
||||
private static void PublishRegionsLocked() => Volatile.Write(ref RegionSnapshot, [.. Regions.Values]);
|
||||
|
||||
private sealed class Region
|
||||
{
|
||||
public ulong Base;
|
||||
|
|
@ -164,10 +156,6 @@ public static unsafe class HostMemory
|
|||
public uint DefaultProtect;
|
||||
public Dictionary<ulong, uint>? PageProtects;
|
||||
|
||||
// The overridden pages in address order, so a query finds where a protection run ends
|
||||
// without visiting every override: write tracking leaves tens of thousands of them.
|
||||
public SortedSet<ulong>? OverridePages;
|
||||
|
||||
public ulong End => Base + Size;
|
||||
|
||||
public uint ProtectAt(ulong pageAddress)
|
||||
|
|
@ -281,7 +269,6 @@ public static unsafe class HostMemory
|
|||
Size = alignedSize,
|
||||
DefaultProtect = protect
|
||||
};
|
||||
PublishRegionsLocked();
|
||||
|
||||
return (void*)result;
|
||||
}
|
||||
|
|
@ -301,7 +288,6 @@ public static unsafe class HostMemory
|
|||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions.Remove((ulong)address);
|
||||
PublishRegionsLocked();
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -339,10 +325,6 @@ public static unsafe class HostMemory
|
|||
{
|
||||
info = default;
|
||||
var pageAddress = AlignDown((ulong)address, PageSize);
|
||||
if (TryQueryUnlocked(pageAddress, ref info))
|
||||
{
|
||||
return (nuint)sizeof(BasicInfo);
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
|
|
@ -358,37 +340,30 @@ public static unsafe class HostMemory
|
|||
var pageProtects = region.PageProtects;
|
||||
uint protect;
|
||||
ulong runEnd;
|
||||
if (pageProtects is null || pageProtects.Count == 0 || region.OverridePages is not { } overridePages)
|
||||
if (pageProtects is null || pageProtects.Count == 0)
|
||||
{
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
}
|
||||
else if (pageProtects.TryGetValue(pageAddress, out protect))
|
||||
{
|
||||
// Overrides run while the next page is overridden with the same protection.
|
||||
runEnd = pageAddress + PageSize;
|
||||
foreach (var overrideAddress in runEnd < region.End ? overridePages.GetViewBetween(runEnd, region.End - 1) : [])
|
||||
while (runEnd < region.End &&
|
||||
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
|
||||
nextProtect == protect)
|
||||
{
|
||||
if (overrideAddress != runEnd || pageProtects[overrideAddress] != protect)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
runEnd += PageSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default protection runs to the next override.
|
||||
protect = region.DefaultProtect;
|
||||
runEnd = region.End;
|
||||
if (pageAddress + PageSize < region.End)
|
||||
foreach (var overrideAddress in pageProtects.Keys)
|
||||
{
|
||||
// The first element only: a view's Count walks the whole view.
|
||||
foreach (var overrideAddress in overridePages.GetViewBetween(pageAddress + PageSize, region.End - 1))
|
||||
if (overrideAddress > pageAddress && overrideAddress < runEnd)
|
||||
{
|
||||
runEnd = overrideAddress;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -407,18 +382,12 @@ public static unsafe class HostMemory
|
|||
// reported as a free block reaching to the next tracked region
|
||||
// so scanning callers keep advancing.
|
||||
var nextBase = ulong.MaxValue;
|
||||
var keys = Regions.Keys;
|
||||
for (int low = 0, high = keys.Count - 1; low <= high;)
|
||||
foreach (var regionBase in Regions.Keys)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (keys[middle] > pageAddress)
|
||||
if (regionBase > pageAddress)
|
||||
{
|
||||
nextBase = keys[middle];
|
||||
high = middle - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
low = middle + 1;
|
||||
nextBase = regionBase;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,56 +416,6 @@ public static unsafe class HostMemory
|
|||
return false;
|
||||
}
|
||||
|
||||
// Answers without Gate for untracked memory and for regions without per-page overrides;
|
||||
// a region with overrides needs their dictionary, which only Gate protects. A concurrent
|
||||
// Protect may land just before or after either way, as with the locked query.
|
||||
private static bool TryQueryUnlocked(ulong pageAddress, ref BasicInfo info)
|
||||
{
|
||||
var regions = Volatile.Read(ref RegionSnapshot);
|
||||
Region? candidate = null;
|
||||
var nextBase = ulong.MaxValue;
|
||||
for (int low = 0, high = regions.Length - 1; low <= high;)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (regions[middle].Base <= pageAddress)
|
||||
{
|
||||
candidate = regions[middle];
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextBase = regions[middle].Base;
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate is not null && pageAddress < candidate.End)
|
||||
{
|
||||
if (Volatile.Read(ref candidate.PageProtects) is { Count: > 0 })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
info.BaseAddress = pageAddress;
|
||||
info.AllocationBase = candidate.Base;
|
||||
info.AllocationProtect = candidate.DefaultProtect;
|
||||
info.RegionSize = candidate.End - pageAddress;
|
||||
info.State = MEM_COMMIT;
|
||||
info.Protect = candidate.DefaultProtect;
|
||||
info.Type = MEM_PRIVATE;
|
||||
return true;
|
||||
}
|
||||
|
||||
info.BaseAddress = pageAddress;
|
||||
info.AllocationBase = 0;
|
||||
info.AllocationProtect = PAGE_NOACCESS;
|
||||
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
|
||||
info.State = MEM_FREE_STATE;
|
||||
info.Protect = PAGE_NOACCESS;
|
||||
info.Type = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFindRegionLocked(ulong address, out Region region)
|
||||
{
|
||||
region = null!;
|
||||
|
|
@ -535,26 +454,20 @@ public static unsafe class HostMemory
|
|||
{
|
||||
region.DefaultProtect = protect;
|
||||
region.PageProtects = null;
|
||||
region.OverridePages = null;
|
||||
return;
|
||||
}
|
||||
|
||||
region.PageProtects ??= new Dictionary<ulong, uint>();
|
||||
region.OverridePages ??= new SortedSet<ulong>();
|
||||
var end = start + size;
|
||||
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
|
||||
{
|
||||
if (protect == region.DefaultProtect)
|
||||
{
|
||||
if (region.PageProtects.Remove(pageAddress))
|
||||
{
|
||||
region.OverridePages.Remove(pageAddress);
|
||||
}
|
||||
region.PageProtects.Remove(pageAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
region.PageProtects[pageAddress] = protect;
|
||||
region.OverridePages.Add(pageAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
|
|
@ -134,15 +133,13 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var indexAddress = ctx[CpuRegister.Rdx];
|
||||
var modifier = ctx[CpuRegister.Rcx];
|
||||
var modifier = (uint)ctx[CpuRegister.Rcx];
|
||||
|
||||
if (commandBufferAddress == 0)
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndex", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
|
||||
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
||||
|
|
@ -242,13 +239,11 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var modifier = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0)
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndexAuto", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
||||
|
|
@ -462,19 +457,4 @@ public static partial class AgcExports
|
|||
#pragma warning restore SHEM006
|
||||
|
||||
private static long _indirectDrawEmitRejectCount;
|
||||
|
||||
private static readonly ConcurrentDictionary<(string, ulong), byte> _reportedDrawModifiers = new();
|
||||
|
||||
// The modifier names the shader stage that receives the draw and the user registers the
|
||||
// command processor fills for indirect draws. A direct draw emits the same packet for every
|
||||
// modifier; rejecting the unfamiliar ones dropped whole passes, such as Unreal's volume
|
||||
// rasterization through the geometry stage, without a trace.
|
||||
private static void NoteDrawModifier(string export, ulong modifier)
|
||||
{
|
||||
if (modifier != 0x4000_0000 && _reportedDrawModifiers.TryAdd((export, modifier), 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] agc.draw_modifier {export} modifier=0x{modifier:X} stage={(uint)modifier >> 29} flags=0x{(uint)modifier & 7u:X}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,24 +64,14 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||
var eventAddress = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || (eventAddress != 0 && (eventType & ~1u) != 0x38))
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
// Counter selection and counter dumps carry a payload in the address fields.
|
||||
var hasAddress = (eventType & ~1u) == 0x38;
|
||||
var packetDwords = hasAddress ? 4u : 2u;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, hasAddress ? eventType | 0x100u : eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (hasAddress &&
|
||||
(!TryWriteUInt32(ctx, commandAddress + 8, (uint)eventAddress & ~7u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(eventAddress >> 32))))
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,9 +149,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
public bool IsCoherent { get; }
|
||||
|
||||
// The host mapping of the whole buffer, or null for memory the host cannot map.
|
||||
public byte* MappedPointer => _mapped;
|
||||
|
||||
public GpuBufferUsage Usage { get; }
|
||||
|
||||
public ulong CpuAddress { get; }
|
||||
|
|
@ -342,7 +339,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
var (required, preferred, avoided) = usage switch
|
||||
{
|
||||
GpuBufferUsage.DeviceLocal => (MemoryPropertyFlags.None, MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Unified => (MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.None),
|
||||
GpuBufferUsage.Upload => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
GpuBufferUsage.Download => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.DeviceLocalBit),
|
||||
_ => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,4 @@ public enum GpuBufferUsage : byte
|
|||
Upload,
|
||||
Download,
|
||||
Stream,
|
||||
// Device-local memory the host can map: guest buffers on unified-memory devices, read back without a copy.
|
||||
Unified,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ 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,7 +41,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly IGpuQueueRelay _relay;
|
||||
private readonly GuestBufferUploader _uploader;
|
||||
private readonly bool _unifiedBuffers;
|
||||
private readonly IGuestBackedSpace _backing;
|
||||
private readonly BdaFaultProcessor _faults;
|
||||
private readonly GpuBuffer _gds;
|
||||
|
|
@ -73,7 +72,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_relay = relay;
|
||||
_backing = backing;
|
||||
_faults = new BdaFaultProcessor(device, scheduler, this, CachingPageBits, CachingPageCount);
|
||||
_unifiedBuffers = UnifiedBuffersEnabled && HasUnifiedMemoryType(device);
|
||||
_gds = new GpuBuffer(device, scheduler, GpuBufferUsage.Stream, 0, GpuBuffer.AllFlags, GdsBufferSize);
|
||||
_bdaPageTable = new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, BdaPageTableSize);
|
||||
_tracker = new GuestPageTracker(pages);
|
||||
|
|
@ -328,18 +326,6 @@ 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)
|
||||
|
|
@ -354,27 +340,6 @@ 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++)
|
||||
|
|
@ -394,9 +359,6 @@ 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)
|
||||
|
|
@ -479,15 +441,11 @@ 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 = (_fillPattern ??= new uint[4096]).AsSpan(0, (int)Math.Min(size / sizeof(uint), 4096));
|
||||
values.Fill(value);
|
||||
var bytes = MemoryMarshal.AsBytes(values);
|
||||
var values = new uint[(int)Math.Min(size / sizeof(uint), 4096)];
|
||||
Array.Fill(values, value);
|
||||
var bytes = MemoryMarshal.AsBytes<uint>(values);
|
||||
for (ulong offset = 0; offset < size;)
|
||||
{
|
||||
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
|
||||
|
|
@ -703,28 +661,13 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_bdaTouchMapping = mapping;
|
||||
}
|
||||
|
||||
// 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)
|
||||
var epoch = _tracker.CpuDirtyEpoch;
|
||||
if (epoch != _bdaSweepEpoch || mapping != _bdaSweepMapping)
|
||||
{
|
||||
BeginUploadBatch();
|
||||
try
|
||||
{
|
||||
foreach (var span in spans)
|
||||
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndUploadBatch();
|
||||
}
|
||||
|
||||
foreach (var span in spans)
|
||||
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
|
||||
_bdaSweepEpoch = epoch;
|
||||
_bdaSweepMapping = mapping;
|
||||
_bdaVisibilityPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -735,15 +678,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
private Action<ulong, ulong>? _uploadDirtyBuffersInRange;
|
||||
|
||||
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 long _bdaSweepEpoch = -1;
|
||||
private ulong _bdaSweepMapping;
|
||||
|
||||
private void TouchBuffersInRange(ulong guestAddress, ulong size)
|
||||
|
|
@ -764,10 +699,8 @@ 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: PreserveHotPagesInSweeps);
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -793,7 +726,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: PreserveHotPagesInSweeps);
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -963,45 +896,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
|
||||
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
|
||||
{
|
||||
// A GPU write that lands after the copy was recorded makes the read stale. Retrying
|
||||
// synchronously stalls the command worker until the GPU drains, so re-issue the read and
|
||||
// wait here again; only the last attempt falls back to the synchronous read.
|
||||
for (var attempt = 1; ; attempt++)
|
||||
PendingDownload? pending = null;
|
||||
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
PendingDownload? pending = null;
|
||||
if (!_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (pending is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pending.Ticket is { } ticket)
|
||||
{
|
||||
(AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.")).Wait(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
_scheduler.WaitForSubmittedTick(pending.MainQueueTick);
|
||||
}
|
||||
|
||||
var finalAttempt = attempt >= PendingReadAttempts;
|
||||
var applied = false;
|
||||
if (!_relay.TryRunOnGpuQueue(() => applied = CompleteReadMemoryOnGpu(pending, retrySynchronously: finalAttempt)))
|
||||
{
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
if (applied || finalAttempt)
|
||||
readback.Wait(pending.Ticket);
|
||||
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return AwaitShutdown();
|
||||
}
|
||||
|
||||
var onQueue = _relay.IsGpuQueueThread;
|
||||
|
|
@ -1232,14 +1144,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
|
||||
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
|
||||
|
||||
// Ticket is set when the readback queue carries the download; otherwise the main queue
|
||||
// does, into MainQueueBuffer, and MainQueueTick is the submission the guest thread waits for.
|
||||
private sealed record PendingDownload
|
||||
{
|
||||
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
|
||||
public ulong MainQueueTick { get; init; }
|
||||
public GpuBuffer? MainQueueBuffer { get; init; }
|
||||
public ulong[] MainQueueOffsets { get; init; } = [];
|
||||
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
|
||||
public required List<DownloadPiece> Copies { get; init; }
|
||||
public required ulong[] WriteTicks { get; init; }
|
||||
public required ulong WindowBegin { get; init; }
|
||||
|
|
@ -1249,9 +1156,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public required bool IsWrite { get; init; }
|
||||
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
|
||||
public required long Started { get; init; }
|
||||
|
||||
// Read from the buffers' own host mappings once the writer tick retires; no copy was recorded.
|
||||
public bool Mapped { get; init; }
|
||||
}
|
||||
|
||||
private PendingDownload? BeginReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source,
|
||||
|
|
@ -1275,53 +1179,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
{
|
||||
NoteHotReadback(guestAddress, size);
|
||||
}
|
||||
if (copies.Count != 0 && _unifiedBuffers && AreMapped(copies, out var writer))
|
||||
{
|
||||
if (writer == 0 || _scheduler.IsTickComplete(writer))
|
||||
{
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allowPending && writer < _scheduler.CurrentTick)
|
||||
{
|
||||
// A guest thread had to wait for this writer: submit later writes to the window right
|
||||
// after their command, so the next wait ends with them instead of with their whole batch.
|
||||
NoteHotReadback(guestAddress, size);
|
||||
|
||||
// The writer is already submitted: wait for it alone, not for the work queued after it.
|
||||
var mappedTicks = new ulong[copies.Count];
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
mappedTicks[index] = copies[index].Buffer.LastGpuWriteTick;
|
||||
copies[index].Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
Mapped = true,
|
||||
MainQueueTick = writer,
|
||||
Copies = copies,
|
||||
WriteTicks = mappedTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (!allowPending)
|
||||
{
|
||||
// On the command worker: submit the writer if it is still recording, then read the mapping.
|
||||
_scheduler.Wait(writer);
|
||||
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending && TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
|
|
@ -1344,31 +1201,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0 && allowPending &&
|
||||
TryBeginDownloadOnMainQueue(copies, out var mainTick, out var mainBuffer, out var mainOffsets, out var mainWriteTicks))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
copy.Buffer.RetainForeignRead();
|
||||
}
|
||||
|
||||
return new PendingDownload
|
||||
{
|
||||
MainQueueTick = mainTick,
|
||||
MainQueueBuffer = mainBuffer,
|
||||
MainQueueOffsets = mainOffsets,
|
||||
Copies = copies,
|
||||
WriteTicks = mainWriteTicks,
|
||||
WindowBegin = windowBegin,
|
||||
WindowEnd = windowEnd,
|
||||
GuestAddress = guestAddress,
|
||||
Size = size,
|
||||
IsWrite = isWrite,
|
||||
Source = source,
|
||||
Started = readbackStarted,
|
||||
};
|
||||
}
|
||||
|
||||
if (copies.Count != 0)
|
||||
{
|
||||
DownloadBufferMemory(copies);
|
||||
|
|
@ -1384,75 +1216,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
// Without a readback queue the main queue copies the pieces into a buffer of their own and
|
||||
// submits at once; the guest thread then waits for that submission instead of the worker
|
||||
// waiting for the whole GPU (a guest read of GPU-written memory otherwise idled the worker
|
||||
// for tens of milliseconds, several times per frame in Silent Hill: The Short Message).
|
||||
private bool TryBeginDownloadOnMainQueue(List<DownloadPiece> copies, out ulong tick, out GpuBuffer buffer, out ulong[] offsets, out ulong[] writeTicks)
|
||||
{
|
||||
tick = 0;
|
||||
buffer = null!;
|
||||
offsets = [];
|
||||
writeTicks = [];
|
||||
if (!MainQueuePendingReads || copies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ticks = new ulong[copies.Count];
|
||||
var placements = new ulong[copies.Count];
|
||||
var total = 0UL;
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var written = copies[index].Buffer.LastGpuWriteTick;
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ticks[index] = written;
|
||||
placements[index] = total;
|
||||
total += (copies[index].Size + BufferDownloadBatchPlanner.Alignment - 1) & ~(BufferDownloadBatchPlanner.Alignment - 1);
|
||||
}
|
||||
|
||||
if (total > AsyncReadbackLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, total);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
buffer.CopyFrom(
|
||||
_scheduler.Current, copies[index].Buffer, copies[index].SourceOffset, placements[index], copies[index].Size,
|
||||
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
|
||||
}
|
||||
|
||||
tick = _scheduler.Flush();
|
||||
offsets = placements;
|
||||
writeTicks = ticks;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool MainQueuePendingReads =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_MAIN_QUEUE_PENDING_READS") != "0";
|
||||
|
||||
// Pending reads a guest thread re-issues before it lets the worker read synchronously.
|
||||
private const int PendingReadAttempts = 4;
|
||||
|
||||
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
if (pending.Mapped)
|
||||
{
|
||||
return CompleteMappedRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
if (pending.Ticket is null)
|
||||
{
|
||||
return CompleteMainQueueRead(pending, retrySynchronously);
|
||||
}
|
||||
|
||||
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
|
||||
try
|
||||
{
|
||||
|
|
@ -1503,155 +1269,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Guest buffers live in host-visible device memory on unified-memory devices (Apple GPUs), so a
|
||||
// readback can copy straight out of the buffer once the last GPU write to it retired. That skips
|
||||
// the copy command at the tail of the queue and the wait for every draw recorded after the writer.
|
||||
// SHARPEMU_UNIFIED_BUFFERS=0 keeps device-local buffers and copy readbacks.
|
||||
private static readonly bool UnifiedBuffersEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_UNIFIED_BUFFERS") != "0";
|
||||
|
||||
private static bool HasUnifiedMemoryType(GpuDeviceInfo device)
|
||||
{
|
||||
const MemoryPropertyFlags unified = MemoryPropertyFlags.DeviceLocalBit | MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit;
|
||||
for (uint index = 0; index < device.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((device.GetMemoryTypeFlags(index) & unified) == unified)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AreMapped(List<DownloadPiece> copies, out ulong writer)
|
||||
{
|
||||
writer = 0;
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
if (copy.Buffer.MappedPointer == null || !copy.Buffer.IsCoherent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
writer = Math.Max(writer, copy.Buffer.LastGpuWriteTick);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ApplyMappedRead(List<DownloadPiece> copies, ulong windowBegin, ulong windowEnd, ulong guestAddress, ulong size,
|
||||
bool isWrite, long started, GuestMemoryProfile.ReadbackSource source)
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
var bytes = new ReadOnlySpan<byte>(copy.Buffer.MappedPointer + copy.SourceOffset, checked((int)copy.Size));
|
||||
if (!_backing.TryWriteBacking(copy.Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{copy.Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(windowBegin, windowEnd - windowBegin);
|
||||
if (isWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(guestAddress, size);
|
||||
}
|
||||
|
||||
MappedReadbacks++;
|
||||
RecordReadback(windowBegin, windowEnd, isWrite, copies, started, source);
|
||||
}
|
||||
|
||||
// Readbacks served from a buffer's own mapping, without a copy command.
|
||||
internal long MappedReadbacks { get; private set; }
|
||||
|
||||
internal bool UnifiedBuffers => _unifiedBuffers;
|
||||
|
||||
private bool CompleteMappedRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
ApplyMappedRead(pending.Copies, pending.WindowBegin, pending.WindowEnd, pending.GuestAddress, pending.Size,
|
||||
pending.IsWrite, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CompleteMainQueueRead(PendingDownload pending, bool retrySynchronously)
|
||||
{
|
||||
var buffer = pending.MainQueueBuffer ?? throw SubmissionScheduler.Fatal("The pending readback lost its buffer.");
|
||||
try
|
||||
{
|
||||
if (!IsPendingDownloadCurrent(pending))
|
||||
{
|
||||
if (retrySynchronously)
|
||||
{
|
||||
PendingReadbacksRetried++;
|
||||
Interlocked.Increment(ref _reportedRetried);
|
||||
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingReadbacksApplied++;
|
||||
Interlocked.Increment(ref _reportedApplied);
|
||||
var copies = pending.Copies;
|
||||
buffer.Invalidate(0, buffer.Size);
|
||||
for (var index = 0; index < copies.Count; index++)
|
||||
{
|
||||
var bytes = buffer.Mapped.Slice((int)pending.MainQueueOffsets[index], (int)copies[index].Size);
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{copies[index].Size:X16}");
|
||||
}
|
||||
|
||||
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
|
||||
}
|
||||
|
||||
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
|
||||
if (pending.IsWrite)
|
||||
{
|
||||
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
|
||||
}
|
||||
|
||||
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The guest thread waited for the submission, so the GPU is done with the buffer.
|
||||
buffer.Dispose();
|
||||
foreach (var copy in pending.Copies)
|
||||
{
|
||||
copy.Buffer.ReleaseForeignRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPendingDownloadCurrent(PendingDownload pending)
|
||||
{
|
||||
var total = 0UL;
|
||||
|
|
@ -2069,7 +1686,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
|
||||
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
|
||||
_device, _scheduler, _unifiedBuffers ? GpuBufferUsage.Unified : GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin, allowSlab: true), overlap.Begin, overlap.End - overlap.Begin);
|
||||
foreach (var oldId in overlapping)
|
||||
{
|
||||
|
|
@ -2096,11 +1713,20 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
profileScope.SwitchPhase(isWritten
|
||||
? RenderPhaseProfile.Phase.BufferDirtySyncWritten
|
||||
: isTexelBuffer ? RenderPhaseProfile.Phase.BufferDirtySyncTexel : RenderPhaseProfile.Phase.BufferDirtySyncUpload);
|
||||
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;
|
||||
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);
|
||||
if (source != null)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
|
|
@ -2108,17 +1734,6 @@ 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,
|
||||
|
|
@ -2145,7 +1760,6 @@ 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)
|
||||
|
|
@ -2154,7 +1768,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, sink.TotalSize, hotBytes, elapsedTicks);
|
||||
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
|
||||
}
|
||||
|
||||
if (isTexelBuffer)
|
||||
|
|
@ -2171,81 +1785,6 @@ 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,8 +11,6 @@ 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,16 +103,6 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
private void DrawIndirectArguments(ulong packetAddress, uint opcode, ulong argumentsAddress, bool indexed)
|
||||
{
|
||||
if (!indexed && _host.ResolvesNonIndexedIndirectDrawOnGpu)
|
||||
{
|
||||
// The host reads the four arguments on the GPU, so a GPU-written argument buffer
|
||||
// costs no wait for the GPU here; it reads them itself when the draw needs counts.
|
||||
_deferredInstanceCountAddress = argumentsAddress + 4;
|
||||
_host.DrawAuto(SubmitId, new DrawAutoArguments(packetAddress, opcode, 1, 1, 0, 0,
|
||||
DrawOffsetSource.IndirectArguments, argumentsAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indexed)
|
||||
{
|
||||
var vertexCount = ReadDword(argumentsAddress);
|
||||
|
|
|
|||
|
|
@ -435,21 +435,15 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
|
||||
}
|
||||
|
||||
// Until real sample queries are implemented, conservatively report visibility.
|
||||
// A one-sample delta can disappear when the guest scales or reduces the counts.
|
||||
const ulong visibleCounterStep = 0x2FFFFFF;
|
||||
// Publish visible occlusion results for each depth block.
|
||||
const ulong readyBit = 1UL << 63;
|
||||
var result = readyBit | SyntheticOcclusionCounter;
|
||||
for (var depthBlock = 0u; depthBlock < 16u; depthBlock++)
|
||||
{
|
||||
// Query reducers can read a GPU-cached copy of this range. Use the same
|
||||
// ordered transfer path as clears instead of changing only CPU backing.
|
||||
var destination = eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong));
|
||||
_host.FillBuffer(destination, sizeof(uint), (uint)result, false);
|
||||
_host.FillBuffer(destination + sizeof(uint), sizeof(uint), (uint)(result >> 32), false);
|
||||
WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result);
|
||||
}
|
||||
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1);
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
if (!satisfied)
|
||||
{
|
||||
Suspend();
|
||||
return;
|
||||
}
|
||||
|
||||
_host.NoteMemoryVisibilityPoint();
|
||||
}
|
||||
|
||||
// The wait bit is PredicationZPassWaitOp: 0 (kWaitForQueryResults) stalls the command
|
||||
|
|
|
|||
|
|
@ -443,8 +443,6 @@ 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,10 +38,7 @@ public readonly record struct DrawAutoArguments(
|
|||
uint InstanceCount,
|
||||
uint FirstVertex,
|
||||
uint FirstInstance,
|
||||
DrawOffsetSource OffsetSource,
|
||||
// Nonzero when the counts are still in guest memory at this address, laid out as
|
||||
// (vertexCount, instanceCount, firstVertex, firstInstance); the other counts are then 1/0.
|
||||
ulong IndirectArgumentsAddress = 0);
|
||||
DrawOffsetSource OffsetSource);
|
||||
|
||||
public enum EndOfPipeWriteKind
|
||||
{
|
||||
|
|
@ -107,13 +104,6 @@ public interface ICommandStreamHost
|
|||
|
||||
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);
|
||||
|
|
@ -146,9 +136,6 @@ public interface ICommandStreamHost
|
|||
// guest memory (DrawIndexedArguments.IndirectArgumentsAddress).
|
||||
bool ResolvesIndirectDrawOnGpu => false;
|
||||
|
||||
// The host also draws non-indexed indirect draws from their arguments in guest memory.
|
||||
bool ResolvesNonIndexedIndirectDrawOnGpu => false;
|
||||
|
||||
// Called when a queue reset packet clears the processor.
|
||||
void OnQueueReset(int queueId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
// 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,14 +14,6 @@ public sealed unsafe partial class CachedImage
|
|||
{
|
||||
private const AccessFlags WriteAccess = AccessFlags.TransferWriteBit | AccessFlags.ShaderWriteBit | AccessFlags.MemoryWriteBit;
|
||||
private const AccessFlags TransferAccess = AccessFlags.TransferReadBit | AccessFlags.TransferWriteBit;
|
||||
private const AccessFlags AnyWriteAccess = WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit;
|
||||
|
||||
// Reads after reads in one layout need no barrier: the state keeps every reader, so the next
|
||||
// writer still waits for all of them. (A sampled depth attachment followed by a draw that only
|
||||
// tests depth would otherwise end the rendering scope.)
|
||||
private static bool IsReadAfterRead(ImageAccessState state, ImageLayout layout, AccessFlags access) =>
|
||||
state.Layout == layout && state.Layout != ImageLayout.Undefined && access != 0 && state.Access != 0 &&
|
||||
(state.Access & AnyWriteAccess) == 0 && (access & AnyWriteAccess) == 0;
|
||||
private const AccessFlags MemoryAccess = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit;
|
||||
private const ImageLayout ReadyLayout = ImageLayout.General;
|
||||
private const AccessFlags ReadyAccess = AccessFlags.ShaderReadBit | AccessFlags.TransferReadBit;
|
||||
|
|
@ -40,12 +32,9 @@ 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)
|
||||
{
|
||||
// Most requests need no barrier; the list exists only once one does. Callers never add to it.
|
||||
List<ImageMemoryBarrier2>? barriers = null;
|
||||
var barriers = new List<ImageMemoryBarrier2>();
|
||||
PipelineStageFlags sourceStages = 0;
|
||||
if ((access & (WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit)) != 0)
|
||||
{
|
||||
|
|
@ -92,13 +81,9 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
var state = states[index];
|
||||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
{
|
||||
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));
|
||||
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
sourceStages |= state.Stage;
|
||||
states[index] = new ImageAccessState(stage, access, layout);
|
||||
}
|
||||
|
|
@ -116,54 +101,15 @@ public sealed unsafe partial class CachedImage
|
|||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout == layout && state.Access == access && !repeatedWrite)
|
||||
{
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
return (barriers, sourceStages);
|
||||
}
|
||||
|
||||
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));
|
||||
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
sourceStages |= state.Stage;
|
||||
}
|
||||
|
||||
Backing.State = new ImageAccessState(stage, access, layout);
|
||||
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;
|
||||
return (barriers, sourceStages);
|
||||
}
|
||||
|
||||
public void Transition(ImageLayout layout, AccessFlags access, SubresourceRange? range, CommandBuffer command)
|
||||
|
|
@ -220,7 +166,7 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
private void RecordBarriers(CommandBuffer command, PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, BufferMemoryBarrier2* bufferBarrier, List<ImageMemoryBarrier2> imageBarriers)
|
||||
{
|
||||
var images = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(imageBarriers);
|
||||
var images = imageBarriers.ToArray();
|
||||
fixed (ImageMemoryBarrier2* imagePointer = images)
|
||||
{
|
||||
VulkanSynchronization.PipelineBarrier(_device.Vk,
|
||||
|
|
@ -296,16 +242,6 @@ 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();
|
||||
|
|
@ -324,12 +260,6 @@ 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);
|
||||
|
|
@ -368,11 +298,6 @@ 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}.");
|
||||
|
|
@ -442,13 +367,6 @@ 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 ||
|
||||
|
|
@ -465,18 +383,16 @@ 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;
|
||||
// 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)
|
||||
if (layers == 0 || Description.Extent.Width > sourceWidth || Description.Extent.Height > sourceHeight ||
|
||||
Description.Extent.Width > destinationWidth || Description.Extent.Height > destinationHeight || !formatsAgree)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal(
|
||||
$"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}.");
|
||||
$"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}.");
|
||||
}
|
||||
|
||||
var resolvedSource = sourceRange with { LayerCount = layers };
|
||||
var resolvedDestination = destinationRange with { LayerCount = layers };
|
||||
var extent = new Extent3D(resolveExtent.Width, resolveExtent.Height, 1);
|
||||
var extent = new Extent3D(Description.Extent.Width, Description.Extent.Height, 1);
|
||||
_scheduler.EndRendering();
|
||||
var command = new CommandBuffer(_scheduler.Current.Handle);
|
||||
source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, resolvedSource, command);
|
||||
|
|
@ -497,13 +413,6 @@ 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 ||
|
||||
|
|
@ -601,11 +510,6 @@ 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}.");
|
||||
|
|
@ -675,11 +579,6 @@ 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}.");
|
||||
|
|
@ -728,11 +627,6 @@ 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,11 +73,6 @@ 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;
|
||||
|
|
@ -91,7 +86,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, bool allowScaling = true)
|
||||
ImageBackingPool? pool = null)
|
||||
{
|
||||
_device = device;
|
||||
_scheduler = scheduler;
|
||||
|
|
@ -99,7 +94,6 @@ 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)
|
||||
{
|
||||
|
|
@ -108,7 +102,7 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
|
||||
Backing.Format = Description.PixelFormat;
|
||||
Backing.ImageType = HostImageType(Description.Type);
|
||||
Backing.Extent = RenderScalePolicy.ScaleExtent(Description.Extent, RenderScale);
|
||||
Backing.Extent = Description.Extent;
|
||||
Backing.GuestPitch = Description.Pitch;
|
||||
Backing.Layers = Description.IsVolume ? 1 : Description.Resources.Layers;
|
||||
Backing.MipLevels = Description.Resources.Levels;
|
||||
|
|
@ -326,13 +320,6 @@ 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; }
|
||||
|
|
@ -617,8 +604,6 @@ 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);
|
||||
|
|
|
|||
|
|
@ -421,9 +421,7 @@ public sealed partial class GuestImageCache
|
|||
}
|
||||
|
||||
PrepareCopyTarget(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);
|
||||
_blit.Reinterpret(cached, replacement);
|
||||
TakeGpuOwnership(replacement);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -703,39 +703,11 @@ public sealed unsafe partial class GuestImageCache
|
|||
}
|
||||
}
|
||||
|
||||
// The mip layout can end before the tile plan does (Silent Hill: a 128x128 slice planned at
|
||||
// the exact end of the mip chain aborted the process). Cover the whole plan when the buffer
|
||||
// holds it; otherwise keep the guest bytes instead of copying a partial image.
|
||||
if (!plan.Depth && plan.Color.Tiled)
|
||||
{
|
||||
var tiledEnd = TiledEndOf(plan.Color.Tiles);
|
||||
if (tiledEnd > copySize)
|
||||
{
|
||||
if (tiledEnd > available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
copySize = tiledEnd;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
|
||||
image.MarkBufferHoldsGpuContents();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong TiledEndOf(List<TileTransfer> tiles)
|
||||
{
|
||||
var end = 0UL;
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
end = Math.Max(end, tile.TiledOffset + tile.TiledSize);
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
// Publishes a GPU-owned image to guest memory after its tick completes; false when it cannot.
|
||||
private bool TryDownloadToGuest(ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -220,14 +220,6 @@ 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);
|
||||
|
|
@ -606,61 +598,10 @@ 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 allowScaling = !IsUnscaledRange(description.Data);
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description, _backingPool, allowScaling));
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description, _backingPool));
|
||||
if (!ImageDescription.IsEmptyRange(description.Data))
|
||||
{
|
||||
AddToIndex(imageIdentifier);
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -166,8 +166,6 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
|
|||
|
||||
// Metal has no 64-bit workgroup atomics here; keep the non-atomic 32-bit pair.
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => false;
|
||||
// The Metal backend runs no f16 conversion probe, so it keeps the exact emulation.
|
||||
bool IShaderPipelineHost.NativeHalfConversionExact => false;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => new(MaxDimension, MaxDimension, MaxDimension, MaxDimension);
|
||||
|
||||
|
|
@ -280,18 +278,6 @@ 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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,14 +46,6 @@ internal interface IShaderPipelineHost
|
|||
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,
|
||||
Images.RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportWidth, MaxViewportDimension)) * 0.5f,
|
||||
Images.RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportHeight, MaxViewportDimension)) * 0.5f);
|
||||
Math.Min(limits.MaxViewportWidth, MaxViewportDimension) * 0.5f,
|
||||
Math.Min(limits.MaxViewportHeight, MaxViewportDimension) * 0.5f);
|
||||
}
|
||||
|
||||
return VertexInputResolver.ResolveVertexInputs(_context, source.Registered, source.UserData,
|
||||
|
|
@ -210,22 +210,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
// One past the highest parameter location the pixel program reads, resolved as its translator does.
|
||||
// The attributes a pixel program interpolates, ascending; scanned once per decoded program
|
||||
// instead of on every draw.
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Gen5ShaderProgram, uint[]> _interpolatedAttributes = new();
|
||||
|
||||
private static uint[] InterpolatedAttributes(Gen5ShaderProgram program) =>
|
||||
_interpolatedAttributes.GetValue(program, static program => program.Instructions
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = pixelProgram.Instructions
|
||||
.Select(static instruction => instruction.Control)
|
||||
.OfType<Gen5InterpolationControl>()
|
||||
.Select(static control => control.Attribute)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray());
|
||||
|
||||
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
|
||||
{
|
||||
var attributes = InterpolatedAttributes(pixelProgram);
|
||||
.ToArray();
|
||||
if (attributes.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
|
|
@ -244,8 +237,16 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
|
||||
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
|
||||
{
|
||||
var attributes = InterpolatedAttributes(program);
|
||||
return attributes.Length == 0 ? 0u : attributes[^1] + 1;
|
||||
var maxAttribute = -1;
|
||||
foreach (var instruction in program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5InterpolationControl interpolation)
|
||||
{
|
||||
maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return (uint)(maxAttribute + 1);
|
||||
}
|
||||
|
||||
// The bound colour slots in order; each output mode names the kind the pixel program exports.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
source.Stage == ShaderStage.Compute && options.ComputeInfo!.DispatchThreadDimensions);
|
||||
}
|
||||
catch (ResourcePlanException exception)
|
||||
{
|
||||
|
|
@ -593,8 +593,6 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
|
||||
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
|
||||
var nativeHalfConversion = _host.NativeHalfConversionExact;
|
||||
var zeroOutOfBoundsReads = _host.ZeroOutOfBoundsBufferReads;
|
||||
switch (source.Stage)
|
||||
{
|
||||
case ShaderStage.Vertex:
|
||||
|
|
@ -607,8 +605,6 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
|
||||
VertexInputs = entry.VertexInputs,
|
||||
PositionExportControl = info.PositionExportControl,
|
||||
|
|
@ -636,8 +632,6 @@ internal sealed class ShaderProgramCache
|
|||
ScratchDwords = info.ScratchDwords,
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
PixelOutputs = options.PixelOutputs,
|
||||
PixelInputEnable = options.PixelInputEnable,
|
||||
PixelCustomInterpolationMask = info.CustomInterpolationMask,
|
||||
|
|
@ -649,12 +643,12 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
default:
|
||||
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private static BindingLayout AllocateLayout(Gen5ShaderProgram program, ShaderResourcePlan plan, SpecializedResourceInfo resources,
|
||||
uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits, ShaderStage stage) =>
|
||||
uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits) =>
|
||||
BindingLayout.Allocate(
|
||||
resources.Info,
|
||||
BindingLayout.CollectUserDataRegisters(program, userDataBase, userDataCount),
|
||||
|
|
@ -662,19 +656,12 @@ internal sealed class ShaderProgramCache
|
|||
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
|
||||
BindingLayout.ReadsShaderBase(program),
|
||||
pushDataCursor,
|
||||
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));
|
||||
usesDispatchThreadLimits: usesDispatchThreadLimits);
|
||||
|
||||
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
|
||||
bool nativeHalfConversion, bool zeroOutOfBoundsReads) =>
|
||||
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision) =>
|
||||
new(plan, resources, layout)
|
||||
{
|
||||
NativeHalfConversionExact = nativeHalfConversion,
|
||||
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
|
||||
WaveSize = info.WaveSize,
|
||||
EnableExecGuardElision = info.WaveSize != 64 || execGuardElision,
|
||||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
|
|
@ -688,8 +675,7 @@ internal sealed class ShaderProgramCache
|
|||
};
|
||||
|
||||
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
|
||||
bool sharedInt64Atomics, bool execGuardElision, bool nativeHalfConversion, bool zeroOutOfBoundsReads,
|
||||
out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
bool sharedInt64Atomics, bool execGuardElision, out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
|
||||
{
|
||||
compiled = null;
|
||||
layout = null;
|
||||
|
|
@ -704,9 +690,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, ShaderStage.Compute);
|
||||
record.Info.DispatchThreadDimensions);
|
||||
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
|
||||
sharedInt64Atomics, execGuardElision, nativeHalfConversion, zeroOutOfBoundsReads);
|
||||
sharedInt64Atomics, execGuardElision);
|
||||
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
|
|
|||
|
|
@ -360,24 +360,7 @@ public static class VertexInputResolver
|
|||
for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++)
|
||||
{
|
||||
var buffer = buffers[bufferIndex];
|
||||
uint minimumFetchBytes = 0;
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
var descriptor = resource.Descriptor;
|
||||
// OOB_SELECT=2 only tests for an unbound (zero-record) descriptor.
|
||||
// A constant stream still reads the whole formatted element, even
|
||||
// when NUM_RECORDS is smaller than that element's byte width.
|
||||
if (descriptor.Stride == 0 && descriptor.OutOfBounds == 2 && descriptor.RecordCount != 0 &&
|
||||
VertexAttributeFormats.TryResolve(in descriptor, 4, out var format, out _))
|
||||
{
|
||||
minimumFetchBytes = Math.Max(minimumFetchBytes, Images.ViewFormatRules.BlockBytes(format));
|
||||
}
|
||||
}
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0)
|
||||
{
|
||||
MinimumFetchBytes = minimumFetchBytes,
|
||||
};
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0);
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
|
|
|
|||
|
|
@ -29,13 +29,6 @@ public static class ColorTargetResolver
|
|||
public static ColorTargetResolution? Resolve(ContextRegisters context, uint slot, uint drawLayerOffset, bool ignoreTargetMask, out uint resolvedSlot)
|
||||
{
|
||||
resolvedSlot = slot == FirstBoundSlot ? FirstBound(context) : slot;
|
||||
// Disabling CB takes precedence over stale target addresses and masks.
|
||||
// Depth-only passes may leave the preceding color targets bound.
|
||||
if (context.ColorControl.Mode == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ImageRequestBuilders.ColorTarget(in context.ColorTargets[resolvedSlot], context.RenderTargetMaskForSlot(resolvedSlot), drawLayerOffset, ignoreTargetMask);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,20 +76,6 @@ public readonly record struct DepthStencilState(
|
|||
: ImageLayout.DepthAttachmentOptimal;
|
||||
}
|
||||
|
||||
// A sampled attachment needs the read-only layouts; a draw that writes needs the writable
|
||||
// one. A draw that neither samples nor writes keeps the read-only layout it is already in,
|
||||
// so sampling and depth-testing draws can alternate inside one rendering scope.
|
||||
public static ImageLayout AttachmentLayoutFor(DepthStencilState state, Format format, bool sampled, ImageLayout? current)
|
||||
{
|
||||
var readOnly = state.AttachmentLayout(format);
|
||||
if (sampled)
|
||||
{
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
return state.IsReadOnly(format) && current == readOnly ? readOnly : WritableAttachmentLayout(format);
|
||||
}
|
||||
|
||||
public ImageLayout AttachmentLayout(Format format)
|
||||
{
|
||||
var available = ViewFormatRules.DepthAspects(format);
|
||||
|
|
|
|||
|
|
@ -15,23 +15,13 @@ public struct ColorTargetState
|
|||
public ResourceSlotIdentifier Image;
|
||||
public ImageView View;
|
||||
|
||||
// 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)
|
||||
public ColorTargetState(in ColorTargetResolution resolution, uint slot, ResourceSlotIdentifier image)
|
||||
{
|
||||
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.
|
||||
|
|
@ -42,22 +32,14 @@ public struct DepthAttachmentState
|
|||
public ImageView View;
|
||||
public bool MetadataClear;
|
||||
|
||||
// See ColorTargetState.RenderScale.
|
||||
public float RenderScale;
|
||||
|
||||
public DepthAttachmentState(in DepthTargetState target, ResourceSlotIdentifier image, float renderScale)
|
||||
public DepthAttachmentState(in DepthTargetState target, ResourceSlotIdentifier image)
|
||||
{
|
||||
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,13 +99,6 @@ 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.
|
||||
|
|
@ -147,9 +140,6 @@ public interface IRenderHost
|
|||
// Whether a texture bound for the draw being prepared reads the depth attachment's subresources.
|
||||
bool SamplesDepthAttachment(in DepthAttachmentState depth) => true;
|
||||
|
||||
// The layout the depth attachment's subresources are in now, or null when unknown or mixed.
|
||||
ImageLayout? DepthAttachmentLayout(in DepthAttachmentState depth) => null;
|
||||
|
||||
// The next draw stores to buffers or storage images; called before its BeginRendering.
|
||||
void PrepareMemoryWritingDraw() { }
|
||||
|
||||
|
|
@ -163,10 +153,6 @@ public interface IRenderHost
|
|||
void DrawIndexedIndirect(BufferBinding arguments) =>
|
||||
throw new NotSupportedException("The render host does not draw from indirect arguments.");
|
||||
|
||||
// One draw whose counts the GPU reads from the buffer (VkDrawIndirectCommand layout).
|
||||
void DrawIndirect(BufferBinding arguments) =>
|
||||
throw new NotSupportedException("The render host does not draw from indirect arguments.");
|
||||
|
||||
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
|
||||
|
||||
// Executes a dispatch from three uint32 group counts in guest memory when the backend can
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// 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;
|
||||
|
||||
|
|
@ -29,18 +28,6 @@ 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)
|
||||
{
|
||||
|
|
@ -49,9 +36,6 @@ 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;
|
||||
|
|
@ -69,13 +53,6 @@ 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;
|
||||
|
|
@ -83,19 +60,17 @@ 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 = RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportWidth, MaxViewportDimension)) * renderScale;
|
||||
viewportHeight = RenderScalePolicy.ClipSpaceReferenceExtent(Math.Min(limits.MaxViewportHeight, MaxViewportDimension)) * renderScale;
|
||||
viewportWidth = Math.Min(limits.MaxViewportWidth, MaxViewportDimension);
|
||||
viewportHeight = Math.Min(limits.MaxViewportHeight, MaxViewportDimension);
|
||||
}
|
||||
else
|
||||
{
|
||||
viewportX = (viewport.XOffset - viewport.XScale) * renderScale;
|
||||
viewportY = (viewport.YOffset - viewport.YScale) * renderScale;
|
||||
viewportWidth = viewport.XScale * 2f * renderScale;
|
||||
viewportHeight = viewport.YScale * 2f * renderScale;
|
||||
viewportX = viewport.XOffset - viewport.XScale;
|
||||
viewportY = viewport.YOffset - viewport.YScale;
|
||||
viewportWidth = viewport.XScale * 2f;
|
||||
viewportHeight = viewport.YScale * 2f;
|
||||
}
|
||||
|
||||
var lineWidth = context.LineWidth;
|
||||
|
|
@ -164,21 +139,6 @@ 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,13 +217,8 @@ public sealed partial class RenderExecutor
|
|||
IPreparedBindings? pixelBindings;
|
||||
try
|
||||
{
|
||||
// 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;
|
||||
vertexBindings = PrepareBindings(vertexInput.Stage);
|
||||
pixelBindings = state.PixelActive ? PrepareBindings(pixelInput.Stage) : null;
|
||||
}
|
||||
catch (DrawImageTypeMismatchException rejection)
|
||||
{
|
||||
|
|
@ -256,7 +251,7 @@ public sealed partial class RenderExecutor
|
|||
var vertexBuffers = AcquireVertexBuffers(vertexInput);
|
||||
var indexBuffer = AcquireIndexBuffer(in indexSource);
|
||||
var indirectArguments = emission.IndirectArgumentsAddress != 0
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, emission.Indexed ? IndexedIndirectArgumentsSize : AutoIndirectArgumentsSize, isWritten: false)
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
|
||||
: default;
|
||||
DropUnwrittenColorTargets(context, ref state, pixelProgram);
|
||||
state.Rendering = AcquireAttachments(ref state);
|
||||
|
|
@ -319,14 +314,7 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
// Uploads and shader writes end with barriers to all commands, so the
|
||||
// indirect read sees them.
|
||||
if (emission.Indexed)
|
||||
{
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
_host.DrawIndirect(indirectArguments);
|
||||
}
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,18 +24,30 @@ public sealed partial class RenderExecutor
|
|||
return false;
|
||||
}
|
||||
|
||||
for (var attempt = 0; attempt < 2; attempt++)
|
||||
for (var slot = 0u; slot < ContextRegisters.ColorTargetCount; slot++)
|
||||
{
|
||||
ResolveTargetImages(context, ref state);
|
||||
if (!TryDemoteMismatchedRenderScales(ref state))
|
||||
if (slot != 0 && (context.RenderTargetMaskForSlot(slot) == 0 || context.ColorTargets[slot].BaseAddress == 0))
|
||||
{
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
|
||||
state.PixelActive = HasActivePixelShader(banks);
|
||||
|
|
@ -66,73 +78,6 @@ 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.
|
||||
|
|
@ -230,12 +175,7 @@ public sealed partial class RenderExecutor
|
|||
readRequest.View.LevelCount,
|
||||
readRequest.View.BaseLayer,
|
||||
readRequest.View.LayerCount);
|
||||
// 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 extent = new Extent3D(write.Width, write.Height, 1);
|
||||
var aspects = (depthCopy ? ImageAspectFlags.DepthBit : 0) | (stencilCopy ? ImageAspectFlags.StencilBit : 0);
|
||||
_host.MarkGpuWritten(destination);
|
||||
_host.CopyDepthStencilImage(source, destination, in range, in extent, aspects);
|
||||
|
|
@ -252,7 +192,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, _host.GetRenderScale(image)) : null;
|
||||
return image.IsValid ? new ColorTargetState(in resolution, resolvedSlot, image) : null;
|
||||
}
|
||||
|
||||
private static bool IsSupportedSampleCount(uint samples) => samples is 1 or 2 or 4 or 8;
|
||||
|
|
@ -329,10 +269,8 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var view = target.Resolution.Request.View;
|
||||
// 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.Width = Math.Min(rendering.Width, target.Resolution.Extent.Width);
|
||||
rendering.Height = Math.Min(rendering.Height, target.Resolution.Extent.Height);
|
||||
rendering.Layers = Math.Min(rendering.Layers, view.LayerCount);
|
||||
var clear = acquired.MetadataClear ? acquired.MetadataClearValue : target.Resolution.ColorClearValue;
|
||||
rendering.ColorAttachments[i] = new RenderingAttachment(
|
||||
|
|
@ -372,15 +310,13 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
var loadState = depth.LoadState;
|
||||
var layout = DepthStencilState.AttachmentLayoutFor(
|
||||
loadState,
|
||||
target.Format,
|
||||
_host.SamplesDepthAttachment(in depth),
|
||||
_host.DepthAttachmentLayout(in depth));
|
||||
var layout = _host.SamplesDepthAttachment(in depth)
|
||||
? loadState.AttachmentLayout(target.Format)
|
||||
: DepthStencilState.WritableAttachmentLayout(target.Format);
|
||||
_host.TransitionDepthAttachment(in depth, layout, loadState.AttachmentWriteAspects(target.Format));
|
||||
var view = target.Request.View;
|
||||
rendering.Width = Math.Min(rendering.Width, depth.HostWidth);
|
||||
rendering.Height = Math.Min(rendering.Height, depth.HostHeight);
|
||||
rendering.Width = Math.Min(rendering.Width, target.Width);
|
||||
rendering.Height = Math.Min(rendering.Height, target.Height);
|
||||
rendering.Layers = Math.Min(rendering.Layers, view.LayerCount);
|
||||
var aspects = ViewFormatRules.DepthAspects(target.Format);
|
||||
rendering.DepthStencilAttachment = new RenderingAttachment(
|
||||
|
|
|
|||
|
|
@ -309,48 +309,6 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
|
||||
public void DrawAuto(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawAutoIndirectOnGpu(banks))
|
||||
{
|
||||
DrawAutoWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawAutoCore(submitId, banks, in arguments);
|
||||
}
|
||||
|
||||
// Fans, polygons and legacy rectangle/quad lists are drawn from their vertex counts, so
|
||||
// those read the indirect arguments on the CPU. Rectangle lists draw through the host's
|
||||
// general rectangle-list variant, as indexed indirect draws do.
|
||||
private static bool CanDrawAutoIndirectOnGpu(RegisterBanks banks) =>
|
||||
(GuestPrimitiveType)banks.UserConfig.PrimitiveType is
|
||||
GuestPrimitiveType.PointList or GuestPrimitiveType.LineList or GuestPrimitiveType.LineStrip or
|
||||
GuestPrimitiveType.TriangleList or GuestPrimitiveType.TriangleStrip or GuestPrimitiveType.RectangleList;
|
||||
|
||||
private void DrawAutoWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
|
||||
Span<byte> bytes = stackalloc byte[(int)AutoIndirectArgumentsSize];
|
||||
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
|
||||
{
|
||||
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
|
||||
}
|
||||
|
||||
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
|
||||
var resolved = arguments with
|
||||
{
|
||||
VertexCount = words[0],
|
||||
InstanceCount = words[1],
|
||||
FirstVertex = words[2],
|
||||
FirstInstance = words[3],
|
||||
IndirectArgumentsAddress = 0,
|
||||
};
|
||||
DrawAutoCore(submitId, banks, in resolved);
|
||||
}
|
||||
|
||||
private const ulong AutoIndirectArgumentsSize = 16;
|
||||
|
||||
private void DrawAutoCore(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
|
||||
if (!_host.IsRecording)
|
||||
|
|
@ -406,13 +364,6 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
|
||||
{
|
||||
// A targetless draw may be retained and replayed later; it needs its counts.
|
||||
DrawAutoWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveShaderPrograms(banks, ref state);
|
||||
var vertexInput = state.Programs.VertexInput;
|
||||
var pixelInput = state.Programs.PixelInput;
|
||||
|
|
@ -445,8 +396,7 @@ public sealed partial class RenderExecutor
|
|||
false,
|
||||
0,
|
||||
(uint)vertexOffset,
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput),
|
||||
arguments.IndirectArgumentsAddress);
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
|
||||
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
|
||||
_host.ResetBindings();
|
||||
}
|
||||
|
|
@ -606,8 +556,7 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
var context = banks.Context;
|
||||
var shaderInterface = context.ShaderInterface;
|
||||
var hasColorOutput = context.ColorControl.Mode != 0 &&
|
||||
(context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,9 +80,6 @@ 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;
|
||||
|
|
@ -96,25 +93,6 @@ 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);
|
||||
|
|
@ -122,9 +100,7 @@ public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z);
|
|||
// The vertex buffer words of one fetch slot as the vertex program declares them.
|
||||
public readonly record struct VertexInputBuffer(ulong Address, uint Stride, uint RecordCount, bool PerInstance = false)
|
||||
{
|
||||
public uint MinimumFetchBytes { get; init; }
|
||||
|
||||
public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes);
|
||||
public ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount;
|
||||
}
|
||||
|
||||
// One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot.
|
||||
|
|
|
|||
|
|
@ -272,18 +272,6 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
|
|||
_lastMemoryDrainTick = nextTick;
|
||||
}
|
||||
|
||||
// Waits for a tick that has already been submitted; safe from any thread, since it only
|
||||
// waits on the timeline and records nothing.
|
||||
public void WaitForSubmittedTick(ulong tick)
|
||||
{
|
||||
if (tick >= CurrentTick)
|
||||
{
|
||||
throw Fatal($"Cannot wait off the queue for a tick that is still recording: {tick} >= {CurrentTick}.");
|
||||
}
|
||||
|
||||
_timeline.Wait(tick);
|
||||
}
|
||||
|
||||
public void Wait(ulong tick)
|
||||
{
|
||||
if (tick > CurrentTick)
|
||||
|
|
|
|||
|
|
@ -1,392 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
// The CPU mirror of the two integer f16 conversion sequences the Gen5 translator emits by
|
||||
// default, plus the vectors the device probe measures the native GLSL ext instructions with.
|
||||
// Kept next to the probe because it is the reference the probe compares against: native is
|
||||
// only used when it reproduces these results bit for bit.
|
||||
internal static class HalfConversionReference
|
||||
{
|
||||
// Mirrors Gen5SpirvTranslator.EmitHalfToFloat: the f32 bits of the f16 in the low 16 bits.
|
||||
public static uint HalfToFloatBits(uint halfBits)
|
||||
{
|
||||
var sign = (halfBits & 0x8000u) << 16;
|
||||
var exponent = (halfBits >> 10) & 0x1Fu;
|
||||
var mantissa = halfBits & 0x3FFu;
|
||||
|
||||
var normal = ((exponent + 112u) << 23) | (mantissa << 13);
|
||||
var infinityNan = 0x7F80_0000u | (mantissa << 13);
|
||||
|
||||
var highBit = (uint)BitOperations.Log2(mantissa | 1u);
|
||||
var shift = (int)(23u - highBit);
|
||||
var subFraction = (mantissa << shift) & 0x7F_FFFFu;
|
||||
var subnormal = mantissa != 0 ? (((highBit + 103u) << 23) | subFraction) : 0u;
|
||||
|
||||
var magnitude = exponent == 0 ? subnormal : exponent == 31 ? infinityNan : normal;
|
||||
return sign | magnitude;
|
||||
}
|
||||
|
||||
// Mirrors Gen5SpirvTranslator.EmitFloatToHalf: the f16 bits, round to nearest even.
|
||||
// NaN collapses to one quiet payload (sign | 0x7C00 | 0x200) by construction.
|
||||
public static uint FloatToHalfBits(uint bits)
|
||||
{
|
||||
var sign = (bits >> 16) & 0x8000u;
|
||||
var absolute = bits & 0x7FFF_FFFFu;
|
||||
|
||||
var isInfinityNan = absolute >= 0x7F80_0000u;
|
||||
var isNan = absolute > 0x7F80_0000u;
|
||||
var infinityNan = sign | 0x7C00u | (isNan ? 0x200u : 0u);
|
||||
|
||||
var exponent = absolute >> 23;
|
||||
var mantissa = absolute & 0x7F_FFFFu;
|
||||
var significand = mantissa | 0x80_0000u;
|
||||
|
||||
var roundBit = (significand >> 13) & 1u;
|
||||
var rounded = (significand + 0xFFFu + roundBit) >> 13;
|
||||
var halfExponent = exponent - 112u;
|
||||
var normalBits = (halfExponent << 10) + (rounded - 0x400u);
|
||||
var normal = exponent >= 113u ? normalBits >= 0x7C00u ? 0x7C00u : normalBits : 0u;
|
||||
|
||||
var distance = 126u - exponent;
|
||||
var shift = (int)(distance > 25u ? 25u : distance);
|
||||
var shiftMask = (1u << shift) - 1u;
|
||||
var halfWay = 1u << (shift - 1);
|
||||
var lowBits = significand & shiftMask;
|
||||
var quotient = significand >> shift;
|
||||
var roundUp = lowBits > halfWay || (lowBits == halfWay && (quotient & 1u) != 0u);
|
||||
var subnormal = quotient + (roundUp ? 1u : 0u);
|
||||
|
||||
var finite = exponent <= 112u ? subnormal : normal;
|
||||
return isInfinityNan ? infinityNan : sign | finite;
|
||||
}
|
||||
|
||||
// Every f16 bit pattern, every f16 value widened to f32, the midpoint between adjacent
|
||||
// f16 values and that midpoint one f32 ulp either way (the rounding decisions), the
|
||||
// overflow boundary, signed zeroes, infinities, NaNs and f32 subnormals. Padded to a
|
||||
// multiple of 64 so the probe dispatch needs no bounds check. Each entry is measured
|
||||
// twice: as the f16 in its low 16 bits, and as an f32 bit pattern.
|
||||
public static uint[] BuildTestVectors()
|
||||
{
|
||||
var values = new List<uint>(1 << 19);
|
||||
for (uint bits = 0; bits <= 0xFFFFu; bits++)
|
||||
{
|
||||
values.Add(bits);
|
||||
values.Add(HalfToFloatBits(bits));
|
||||
}
|
||||
|
||||
// 0x7BFF is the largest finite f16; pair it with its successor to cover the overflow edge.
|
||||
for (uint bits = 0; bits <= 0x7BFFu; bits++)
|
||||
{
|
||||
var low = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits));
|
||||
var high = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits + 1));
|
||||
var midpoint = BitConverter.SingleToUInt32Bits((float)(low + (high - low) / 2));
|
||||
for (var offset = -1; offset <= 1; offset++)
|
||||
{
|
||||
var candidate = (uint)(midpoint + offset);
|
||||
values.Add(candidate);
|
||||
values.Add(candidate | 0x8000_0000u);
|
||||
}
|
||||
}
|
||||
|
||||
float[] specials =
|
||||
[
|
||||
0f, -0f, 1f, -1f, 65504f, -65504f, 65520f, -65520f, 65536f, -65536f,
|
||||
1e30f, -1e30f, float.MaxValue, float.MinValue, float.Epsilon, -float.Epsilon,
|
||||
float.PositiveInfinity, float.NegativeInfinity,
|
||||
5.96046448e-8f, 6.09755516e-5f, -6.09755516e-5f, 3.05175781e-5f, 2.98023224e-8f,
|
||||
];
|
||||
foreach (var value in specials)
|
||||
{
|
||||
values.Add(BitConverter.SingleToUInt32Bits(value));
|
||||
}
|
||||
|
||||
uint[] rawSpecials =
|
||||
[
|
||||
0x7F80_0001u, 0xFF80_0001u, 0x7FC0_0000u, 0xFFC0_0000u, 0x7FFF_FFFFu, 0xFFFF_FFFFu,
|
||||
0x0000_0001u, 0x8000_0001u, 0x007F_FFFFu, 0x807F_FFFFu, 0x0080_0000u,
|
||||
];
|
||||
values.AddRange(rawSpecials);
|
||||
|
||||
while (values.Count % 64 != 0)
|
||||
{
|
||||
values.Add(0);
|
||||
}
|
||||
|
||||
return [.. values];
|
||||
}
|
||||
|
||||
// The probe's match rule. Every finite value, every zero and every infinity must be
|
||||
// bit-exact, sign included. A NaN only has to stay a NaN: the payload and the sign of a
|
||||
// NaN are not defined by either side. The emulation picks its own payload for f32->f16
|
||||
// (sign | 0x7C00 | 0x200) and carries the f16 payload through for f16->f32, while the
|
||||
// measured hardware convert canonicalizes to a positive quiet NaN and drops the sign
|
||||
// (Apple M4: every one of the 2314 disagreements out of 643200 is a negative NaN whose
|
||||
// sign the hardware cleared). Nothing a shader computes from a NaN can see that
|
||||
// difference short of inspecting the bits, and the alternative is paying ~20 integer
|
||||
// ops on every f16 conversion in the program to define a NaN payload the ISA leaves open.
|
||||
public static bool Matches(uint expected, uint actual, bool asHalf)
|
||||
{
|
||||
if (expected == actual)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return asHalf
|
||||
? IsHalfNan(expected) && IsHalfNan(actual)
|
||||
: IsFloatNan(expected) && IsFloatNan(actual);
|
||||
}
|
||||
|
||||
private static bool IsHalfNan(uint bits) => (bits & 0x7C00u) == 0x7C00u && (bits & 0x3FFu) != 0;
|
||||
|
||||
private static bool IsFloatNan(uint bits) => (bits & 0x7F80_0000u) == 0x7F80_0000u && (bits & 0x7F_FFFFu) != 0;
|
||||
}
|
||||
|
||||
// Runs the fixed f16 conversion probe once per device: the native GLSL UnpackHalf2x16 /
|
||||
// PackHalf2x16 are used by the translator only when this says the device reproduces the
|
||||
// emulator's own conversion. Never fatal: a failure leaves the exact emulation in place.
|
||||
internal static unsafe class NativeHalfConversionProbe
|
||||
{
|
||||
private const int ReportedExamples = 6;
|
||||
|
||||
// Note carries the reason the probe could not run at all; Detail the first few disagreements.
|
||||
public readonly record struct Result(bool Exact, int Mismatches, int Tested, string? Note, string? Detail = null);
|
||||
|
||||
// SHARPEMU_NATIVE_HALF=0 forces the emulation, =1 forces native without probing.
|
||||
public static bool? ReadOverride() =>
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_HALF") switch
|
||||
{
|
||||
"0" => false,
|
||||
"1" => true,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
var vectors = HalfConversionReference.BuildTestVectors();
|
||||
try
|
||||
{
|
||||
var results = Measure(device, scheduler, beginCommand, vectors);
|
||||
var mismatches = 0;
|
||||
var examples = new List<string>(ReportedExamples);
|
||||
for (var index = 0; index < vectors.Length; index++)
|
||||
{
|
||||
var wide = HalfConversionReference.HalfToFloatBits(vectors[index] & 0xFFFFu);
|
||||
if (!HalfConversionReference.Matches(wide, results[2 * index], asHalf: false))
|
||||
{
|
||||
mismatches++;
|
||||
if (examples.Count < ReportedExamples)
|
||||
{
|
||||
examples.Add($"h2f(0x{vectors[index] & 0xFFFF:X4})=0x{results[2 * index]:X8} want 0x{wide:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
var narrow = HalfConversionReference.FloatToHalfBits(vectors[index]);
|
||||
if (!HalfConversionReference.Matches(narrow, results[2 * index + 1], asHalf: true))
|
||||
{
|
||||
mismatches++;
|
||||
if (examples.Count < ReportedExamples)
|
||||
{
|
||||
examples.Add($"f2h(0x{vectors[index]:X8})=0x{results[2 * index + 1]:X4} want 0x{narrow:X4}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(
|
||||
mismatches == 0,
|
||||
mismatches,
|
||||
2 * vectors.Length,
|
||||
null,
|
||||
examples.Count == 0 ? null : string.Join("; ", examples));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return new Result(false, -1, 2 * vectors.Length, exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Measure(
|
||||
GpuDeviceInfo device,
|
||||
SubmissionScheduler scheduler,
|
||||
Func<CommandBuffer> beginCommand,
|
||||
uint[] vectors)
|
||||
{
|
||||
var vk = device.Vk;
|
||||
var inputBytes = (ulong)vectors.Length * sizeof(uint);
|
||||
var outputBytes = 2 * inputBytes;
|
||||
|
||||
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, inputBytes);
|
||||
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
|
||||
MemoryMarshal.AsBytes<uint>(vectors).CopyTo(input.Mapped);
|
||||
input.Flush(0, inputBytes);
|
||||
output.Mapped.Clear();
|
||||
output.Flush(0, outputBytes);
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[2];
|
||||
for (uint index = 0; index < 2; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 2,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
|
||||
|
||||
DescriptorPool pool = default;
|
||||
PipelineLayout pipelineLayout = default;
|
||||
Pipeline pipeline = default;
|
||||
try
|
||||
{
|
||||
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = 1,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
|
||||
|
||||
var boundLayout = setLayout;
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
|
||||
|
||||
var infos = stackalloc DescriptorBufferInfo[2];
|
||||
infos[0] = new DescriptorBufferInfo(input.Handle, 0, inputBytes);
|
||||
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
|
||||
var writes = stackalloc WriteDescriptorSet[2];
|
||||
for (uint binding = 0; binding < 2; binding++)
|
||||
{
|
||||
writes[binding] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = set,
|
||||
DstBinding = binding,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = infos + binding,
|
||||
};
|
||||
}
|
||||
|
||||
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
|
||||
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateHalfConversionProbe();
|
||||
ShaderModule module;
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
Silk.NET.Vulkan.Result created;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = module,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
|
||||
}
|
||||
|
||||
vk.DestroyShaderModule(device.Device, module, null);
|
||||
Require(created, "compute pipeline");
|
||||
|
||||
var command = beginCommand();
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
|
||||
var boundSet = set;
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
|
||||
vk.CmdDispatch(command, (uint)vectors.Length / 64, 1, 1);
|
||||
var barrier = new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = output.Handle,
|
||||
Offset = 0,
|
||||
Size = outputBytes,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(vk, command,
|
||||
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
|
||||
0, null, 1, &barrier, 0, null);
|
||||
scheduler.FlushAndWait();
|
||||
|
||||
output.Invalidate(0, outputBytes);
|
||||
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pipeline.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipeline(device.Device, pipeline, null);
|
||||
}
|
||||
|
||||
if (pipelineLayout.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (pool.Handle != 0)
|
||||
{
|
||||
vk.DestroyDescriptorPool(device.Device, pool, null);
|
||||
}
|
||||
|
||||
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(Silk.NET.Vulkan.Result result, string operation)
|
||||
{
|
||||
if (result != Silk.NET.Vulkan.Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"The f16 conversion probe could not create its {operation}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
// Measures whether a storage-buffer read past the end of its descriptor range reads zero on this
|
||||
// device. Every guest buffer word the translator emits carries a range test, an address clamp and
|
||||
// a zero select, which exist only to produce that zero; with robustBufferAccess2 the device owes
|
||||
// it already and the three operations are pure overhead. The capability bit alone does not settle
|
||||
// it — MoltenVK runs on Metal, which has no hardware bounds checking — so the probe reads a bound
|
||||
// range it deliberately undershoots and checks the words past it. Never run on a device that did
|
||||
// not enable robustBufferAccess2: an out-of-range read there is undefined and can fault.
|
||||
internal static unsafe class ZeroOutOfBoundsReadProbe
|
||||
{
|
||||
// The descriptor binds this many words of a larger allocation; the words past it must read
|
||||
// zero, and the words inside it must still read the sentinel the probe wrote.
|
||||
private const uint BoundWords = 256;
|
||||
private const uint ProbeWords = 1024;
|
||||
private const uint Sentinel = 0xA5C3_F00Du;
|
||||
|
||||
public readonly record struct Result(bool Zeroed, int Mismatches, int Tested, string? Note);
|
||||
|
||||
// SHARPEMU_ZERO_OOB_READS=0 keeps the per-access checks, =1 drops them without measuring.
|
||||
public static bool? ReadOverride() =>
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_ZERO_OOB_READS") switch
|
||||
{
|
||||
"0" => false,
|
||||
"1" => true,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
try
|
||||
{
|
||||
var results = Measure(device, scheduler, beginCommand);
|
||||
var mismatches = 0;
|
||||
for (var index = 0u; index < ProbeWords; index++)
|
||||
{
|
||||
var expected = index < BoundWords ? Sentinel + index : 0u;
|
||||
if (results[index] != expected)
|
||||
{
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(mismatches == 0, mismatches, (int)ProbeWords, null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return new Result(false, -1, (int)ProbeWords, exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Measure(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
|
||||
{
|
||||
var vk = device.Vk;
|
||||
var allocationBytes = (ulong)ProbeWords * sizeof(uint);
|
||||
var boundBytes = (ulong)BoundWords * sizeof(uint);
|
||||
var outputBytes = allocationBytes;
|
||||
|
||||
// The allocation is larger than the bound range and carries a sentinel everywhere, so a
|
||||
// device that simply ignores the range reads the sentinel back instead of zero.
|
||||
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, allocationBytes);
|
||||
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
|
||||
var words = new uint[ProbeWords];
|
||||
for (var index = 0u; index < ProbeWords; index++)
|
||||
{
|
||||
words[index] = Sentinel + index;
|
||||
}
|
||||
|
||||
MemoryMarshal.AsBytes<uint>(words).CopyTo(input.Mapped);
|
||||
input.Flush(0, allocationBytes);
|
||||
output.Mapped.Clear();
|
||||
output.Flush(0, outputBytes);
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[2];
|
||||
for (uint index = 0; index < 2; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 2,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
|
||||
|
||||
DescriptorPool pool = default;
|
||||
PipelineLayout pipelineLayout = default;
|
||||
Pipeline pipeline = default;
|
||||
try
|
||||
{
|
||||
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = 1,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
|
||||
|
||||
var boundLayout = setLayout;
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
|
||||
|
||||
var infos = stackalloc DescriptorBufferInfo[2];
|
||||
infos[0] = new DescriptorBufferInfo(input.Handle, 0, boundBytes);
|
||||
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
|
||||
var writes = stackalloc WriteDescriptorSet[2];
|
||||
for (uint binding = 0; binding < 2; binding++)
|
||||
{
|
||||
writes[binding] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = set,
|
||||
DstBinding = binding,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = infos + binding,
|
||||
};
|
||||
}
|
||||
|
||||
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
|
||||
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &boundLayout,
|
||||
};
|
||||
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateUncheckedBufferCopyProbe();
|
||||
ShaderModule module;
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
Silk.NET.Vulkan.Result created;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = module,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
|
||||
}
|
||||
|
||||
vk.DestroyShaderModule(device.Device, module, null);
|
||||
Require(created, "compute pipeline");
|
||||
|
||||
var command = beginCommand();
|
||||
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
|
||||
var boundSet = set;
|
||||
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
|
||||
vk.CmdDispatch(command, ProbeWords / 64, 1, 1);
|
||||
var barrier = new BufferMemoryBarrier2
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = output.Handle,
|
||||
Offset = 0,
|
||||
Size = outputBytes,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(vk, command,
|
||||
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
|
||||
0, null, 1, &barrier, 0, null);
|
||||
scheduler.FlushAndWait();
|
||||
|
||||
output.Invalidate(0, outputBytes);
|
||||
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pipeline.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipeline(device.Device, pipeline, null);
|
||||
}
|
||||
|
||||
if (pipelineLayout.Handle != 0)
|
||||
{
|
||||
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (pool.Handle != 0)
|
||||
{
|
||||
vk.DestroyDescriptorPool(device.Device, pool, null);
|
||||
}
|
||||
|
||||
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(Silk.NET.Vulkan.Result result, string operation)
|
||||
{
|
||||
if (result != Silk.NET.Vulkan.Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"The out-of-range read probe could not create its {operation}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2358,53 +2358,41 @@ public static partial class KernelMemoryCompatExports
|
|||
positionBefore = -1;
|
||||
}
|
||||
|
||||
// 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);
|
||||
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;
|
||||
try
|
||||
{
|
||||
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;
|
||||
positionAfter = stream.Position;
|
||||
}
|
||||
finally
|
||||
catch (IOException)
|
||||
{
|
||||
if (pooled)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -7218,41 +7206,9 @@ 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,7 +1054,12 @@ public static class KernelPthreadCompatExports
|
|||
}
|
||||
|
||||
if (canCooperativelyBlock && waiter is not null &&
|
||||
RequestBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
|
@ -1065,21 +1070,6 @@ 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)
|
||||
|
|
@ -1998,7 +1988,7 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
var waitMilliseconds = 10;
|
||||
var waitDuration = TimeSpan.FromMilliseconds(10);
|
||||
if (timed)
|
||||
{
|
||||
var remaining = GetRemainingTimeout(deadline);
|
||||
|
|
@ -2008,14 +1998,13 @@ public static class KernelPthreadCompatExports
|
|||
break;
|
||||
}
|
||||
|
||||
// Monitor.Wait truncates to whole milliseconds, so a sub-millisecond remainder
|
||||
// became a zero timeout and the wait spun until the deadline. UE's game thread
|
||||
// waits for the render fence in 1 ms slices, which kept a core busy. A timed wait
|
||||
// may wake late, so round up instead.
|
||||
waitMilliseconds = (int)Math.Min(waitMilliseconds, Math.Ceiling(remaining.TotalMilliseconds));
|
||||
if (remaining < waitDuration)
|
||||
{
|
||||
waitDuration = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
_ = Monitor.Wait(state.SyncRoot, waitMilliseconds);
|
||||
_ = Monitor.Wait(state.SyncRoot, waitDuration);
|
||||
if (waiter.CompletionState == 0 &&
|
||||
timed &&
|
||||
GetRemainingTimeout(deadline) <= TimeSpan.Zero)
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@ public static class KernelSyncOnAddressCompatExports
|
|||
{
|
||||
if (Volatile.Read(ref _wakeRequested) == 0)
|
||||
{
|
||||
// Round up so a sub-millisecond remainder does not spin.
|
||||
Monitor.Wait(_hostGate, (int)Math.Ceiling(waitSlice.TotalMilliseconds));
|
||||
Monitor.Wait(_hostGate, waitSlice);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ internal static class HostMovieBridge
|
|||
private static bool _frameBufferPresented;
|
||||
private static MediaFramePlayback? _playback;
|
||||
private static Timer? _playbackWatchdog;
|
||||
private static long _watchdogFrameIndex = -1;
|
||||
private static readonly TimeSpan StalledPlaybackTimeout = TimeSpan.FromSeconds(60);
|
||||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
|
|
@ -328,7 +326,6 @@ internal static class HostMovieBridge
|
|||
private static void ArmPlaybackWatchdogLocked(string hostPath)
|
||||
{
|
||||
_playbackWatchdog?.Dispose();
|
||||
_watchdogFrameIndex = -1;
|
||||
var timeout = TryReadBinkInfo(hostPath, out var info)
|
||||
? GetPlaybackWatchdogTimeout(info)
|
||||
: TimeSpan.FromSeconds(150);
|
||||
|
|
@ -343,17 +340,6 @@ internal static class HostMovieBridge
|
|||
return;
|
||||
}
|
||||
|
||||
// Playback follows the guest's own decode and audio clock, so a slow host plays a
|
||||
// movie slower than real time. Expire only a movie that stopped advancing: forcing EOF
|
||||
// under a guest decoder that is still mid-stream wedges it on the last frame.
|
||||
var frameIndex = _playback?.PlaybackProgress.FrameIndex ?? -1;
|
||||
if (_playback is not null && frameIndex > _watchdogFrameIndex)
|
||||
{
|
||||
_watchdogFrameIndex = frameIndex;
|
||||
_playbackWatchdog?.Change(StalledPlaybackTimeout, Timeout.InfiniteTimeSpan);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 host watchdog expired for " +
|
||||
Path.GetFileName(path) + "; advancing to the next movie.");
|
||||
|
|
|
|||
|
|
@ -21,13 +21,6 @@ public enum HostScalingMode
|
|||
Integer,
|
||||
}
|
||||
|
||||
// The display mode exposed to the guest, independent of host window and guest buffer sizes.
|
||||
public enum GuestDisplayResolution
|
||||
{
|
||||
Hd = 1,
|
||||
UltraHd = 2,
|
||||
}
|
||||
|
||||
public enum HostHdrMode
|
||||
{
|
||||
Auto,
|
||||
|
|
@ -62,12 +55,6 @@ public sealed record HostVideoOptions
|
|||
|
||||
public int Height { get; init; } = 1080;
|
||||
|
||||
public GuestDisplayResolution GuestResolution { get; init; } = GuestDisplayResolution.Hd;
|
||||
|
||||
// Host resolution multiplier of the guest render targets the scaling rules accept.
|
||||
// One renders every target at the size the game asked for.
|
||||
public float RenderScale { get; init; } = 1.0f;
|
||||
|
||||
public int DisplayIndex { get; init; }
|
||||
|
||||
public int RefreshRate { get; init; }
|
||||
|
|
@ -91,20 +78,10 @@ public sealed record HostVideoOptions
|
|||
internal bool CanUseHdr(bool displayHdrEnabled, bool surfaceSupportsHdr, bool autoAllowed) =>
|
||||
surfaceSupportsHdr && (HdrMode == HostHdrMode.On || HdrMode == HostHdrMode.Auto && autoAllowed && displayHdrEnabled);
|
||||
|
||||
public const float MinimumRenderScale = 0.25f;
|
||||
public const float MaximumRenderScale = 2.0f;
|
||||
|
||||
// Rounds to a hundredth so the GUI, the command line and the environment agree bit for bit;
|
||||
// the renderer compares scales for equality when it decides whether two images alias.
|
||||
public static float NormalizeRenderScale(float scale) =>
|
||||
!float.IsFinite(scale) ? 1.0f : MathF.Round(Math.Clamp(scale, MinimumRenderScale, MaximumRenderScale), 2);
|
||||
|
||||
public HostVideoOptions Normalize() => this with
|
||||
{
|
||||
Width = Math.Clamp(Width, 640, 16384),
|
||||
Height = Math.Clamp(Height, 360, 16384),
|
||||
GuestResolution = Enum.IsDefined(GuestResolution) ? GuestResolution : GuestDisplayResolution.Hd,
|
||||
RenderScale = NormalizeRenderScale(RenderScale),
|
||||
DisplayIndex = Math.Max(0, DisplayIndex),
|
||||
RefreshRate = Math.Clamp(RefreshRate, 0, 1000),
|
||||
HdrMode = Enum.IsDefined(HdrMode) ? HdrMode : HostHdrMode.Auto,
|
||||
|
|
@ -128,7 +105,6 @@ public static class HostVideoHost
|
|||
{
|
||||
var normalized = options.Normalize();
|
||||
Volatile.Write(ref _currentOptions, normalized);
|
||||
Gpu.Images.RenderScalePolicy.Configure(normalized.RenderScale);
|
||||
var configured = VulkanVideoPresenter.TryConfigureVideo(normalized) &
|
||||
MetalVideoPresenter.TryConfigureVideo(normalized);
|
||||
if (configured)
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ public static partial class VideoOutExports
|
|||
public int FlipMode;
|
||||
public long FlipArg;
|
||||
public bool GpuQueued;
|
||||
// In command-stream order: a GPU flip from its packet, a CPU flip once its preparation runs on the graphics queue.
|
||||
public bool Ordered;
|
||||
public long? ReadyTimestamp;
|
||||
public ulong EventHint;
|
||||
public FlipEventRegistration[]? FlipEvents;
|
||||
|
|
@ -130,7 +128,6 @@ public static partial class VideoOutExports
|
|||
FlipMode = flipMode,
|
||||
FlipArg = flipArg,
|
||||
GpuQueued = gpuQueued,
|
||||
Ordered = gpuQueued,
|
||||
EventHint = SceVideoOutInternalEventFlip |
|
||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16),
|
||||
FlipEventCount = port.FlipEvents.Count,
|
||||
|
|
@ -299,30 +296,14 @@ public static partial class VideoOutExports
|
|||
}
|
||||
}
|
||||
|
||||
// The graphics queue reached the preparation of a CPU flip, so later waits on its buffer are ordered after it.
|
||||
internal static void MarkFlipOrdered(ulong requestId)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_flipRequests.TryGetValue(requestId, out var request))
|
||||
{
|
||||
request.Ordered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// True when no flip still waits for the presenter on the buffer; a discarded or cancelled flip no longer does.
|
||||
// A CPU flip counts only once the graphics queue has reached it: sceVideoOutSubmitFlip reserves the request
|
||||
// (and may sleep for pacing) before queueing its preparation, so a command buffer submitted earlier must not
|
||||
// wait for it; the preparation sits behind that command buffer and the wait would never end.
|
||||
internal static bool IsFlipDone(int handle, int bufferIndex)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
foreach (var request in _flipRequests.Values)
|
||||
{
|
||||
if (request.Handle == handle && request.BufferIndex == bufferIndex && request.Ordered &&
|
||||
request.Outcome == FlipOutcome.Pending)
|
||||
if (request.Handle == handle && request.BufferIndex == bufferIndex && request.Outcome == FlipOutcome.Pending)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -519,19 +519,18 @@ public static partial class VideoOutExports
|
|||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
|
||||
WriteOutputStatus(status, HostVideoHost.CurrentOptions.GuestResolution, port.RefreshRate,
|
||||
WriteOutputStatus(status, port.OutputWidth, port.OutputHeight, port.RefreshRate,
|
||||
HostVideoHost.IsHdrOutputSupported);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
internal static void WriteOutputStatus(Span<byte> status, GuestDisplayResolution resolution, uint refreshRate,
|
||||
internal static void WriteOutputStatus(Span<byte> status, uint width, uint height, uint refreshRate,
|
||||
bool hdrOutputSupported)
|
||||
{
|
||||
status.Clear();
|
||||
// Buffer registration describes a source image, not the connected display.
|
||||
var resolutionClass = (int)resolution;
|
||||
var resolutionClass = width >= 3840 || height >= 2160 ? 2 : 1;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], hdrOutputSupported ? 2 : 1);
|
||||
// The status uses a refresh-rate code, not the frequency used for pacing.
|
||||
|
|
|
|||
|
|
@ -186,10 +186,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
TrackImageBinding(imageIdentifier);
|
||||
}
|
||||
|
||||
public void DemoteRenderScale(ResourceSlotIdentifier imageIdentifier) => _imageCache.DemoteRenderScale(imageIdentifier);
|
||||
|
||||
public float GetRenderScale(ResourceSlotIdentifier imageIdentifier) => _imageCache.GetImage(imageIdentifier).RenderScale;
|
||||
|
||||
private void ResetImageBindings()
|
||||
{
|
||||
foreach (var imageIdentifier in _trackedImageBindings)
|
||||
|
|
|
|||
|
|
@ -290,7 +290,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
public bool TryReadGuest(ulong address, Span<byte> destination)
|
||||
{
|
||||
if (_bufferCache.TryReadCommandBacking(address, destination)) return true;
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandMemorySync))
|
||||
{
|
||||
if (!_bufferCache.TrySynchronizeCpuRead(address, (ulong)destination.Length,
|
||||
|
|
@ -316,12 +315,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_activeGuestQueue = new VulkanGuestQueueIdentity(_commandQueueNames[queueId], submissionId);
|
||||
BindSubmissionContext(_activeGuestQueue);
|
||||
_ = CurrentRecordingBuffer();
|
||||
_bufferCache.NoteMemoryVisibilityPoint();
|
||||
_translation.BeginSubmission(queueId, submissionId, geometrySnapshots, _commandStream.GetInterpreter(queueId));
|
||||
}
|
||||
|
||||
public void NoteMemoryVisibilityPoint() => _bufferCache.NoteMemoryVisibilityPoint();
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
using var flushScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.Flush);
|
||||
|
|
@ -478,11 +474,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
public bool IsFlipDone(int handle, int index) => VideoOutExports.IsFlipDone(handle, index);
|
||||
|
||||
public void PrepareCpuFlip(int handle, int index, ulong requestId)
|
||||
{
|
||||
VideoOutExports.MarkFlipOrdered(requestId);
|
||||
public void PrepareCpuFlip(int handle, int index, ulong requestId) =>
|
||||
CaptureFlip(handle, index, requestId, flipMode: 0, flipArg: 0);
|
||||
}
|
||||
|
||||
public void DrawIndexed(ulong submitId, in DrawIndexedArguments arguments)
|
||||
{
|
||||
|
|
@ -520,9 +513,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
// SHARPEMU_CPU_INDIRECT_DRAW=1 reads the arguments back on the CPU as before.
|
||||
public bool ResolvesIndirectDrawOnGpu => !_cpuIndirectDraw;
|
||||
|
||||
// RenderExecutor records non-indexed indirect draws with vkCmdDrawIndirect.
|
||||
public bool ResolvesNonIndexedIndirectDrawOnGpu => !_cpuIndirectDraw;
|
||||
|
||||
private static readonly bool _cpuIndirectDraw = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DRAW"), "1", StringComparison.Ordinal);
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var resolution = ImageRequestBuilders.Texture(words, ShapeOf(image));
|
||||
_ = BeginBatchedGuestCommands();
|
||||
var request = resolution.Request;
|
||||
request.ShaderWrite = storage && (image.Written || image.Atomic);
|
||||
var imageIdentifier = _imageCache.FindImage(ref request, resolution.ExactFormat);
|
||||
resolution = resolution with { Request = request };
|
||||
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
|
||||
|
|
@ -294,22 +293,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
stage.WriteDispatchThreadLimits(shaderData);
|
||||
if (layout.UsesRenderScale)
|
||||
{
|
||||
ulong scaledImages = 0;
|
||||
var scaledCount = Math.Min(info.Images.Count, 64);
|
||||
for (var index = 0; index < scaledCount; index++)
|
||||
{
|
||||
var identifier = descriptors.Images[index].ImageIdentifier;
|
||||
if (identifier.IsValid && _imageCache.GetImage(identifier).IsScaled)
|
||||
{
|
||||
scaledImages |= 1UL << index;
|
||||
}
|
||||
}
|
||||
|
||||
stage.WriteRenderScale(shaderData, scaledImages, RenderScalePolicy.Scale);
|
||||
}
|
||||
|
||||
prepared.ShaderData = shaderData;
|
||||
if (layout.Find(DescriptorBindingKind.GlobalDataShare) is not null)
|
||||
{
|
||||
|
|
@ -492,12 +475,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return false;
|
||||
}
|
||||
|
||||
ImageLayout? IRenderHost.DepthAttachmentLayout(in DepthAttachmentState depth)
|
||||
{
|
||||
var view = depth.Target.Target.Request.View;
|
||||
return _imageCache.GetImage(depth.Image).UniformLayout(new SubresourceRange(view.BaseLevel, view.LevelCount, view.BaseLayer, view.LayerCount));
|
||||
}
|
||||
|
||||
private BufferView NullStorageBuffer() => new(_bufferCache.GetBuffer(GuestBufferCache.NullBufferId).Handle, 0, NullStorageBufferBytes);
|
||||
|
||||
// A storage buffer view on the cache buffer, aligned down with the adjustment carried in the memory offsets.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using SharpEmu.Libs.Gpu.Buffers;
|
|||
using SharpEmu.Libs.Gpu.Pipelines;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.Vulkan;
|
||||
|
|
@ -89,8 +88,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
CreateSwapchain();
|
||||
CreateCommandResources();
|
||||
CreateGuestDrawResources();
|
||||
ProbeNativeHalfConversion();
|
||||
ProbeZeroOutOfBoundsReads();
|
||||
_vulkanReady = true;
|
||||
AttachGuestGpuMemory();
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -687,7 +684,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
uint? compute = null;
|
||||
uint? general = null;
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
var flags = families[index].QueueFlags;
|
||||
|
|
@ -705,83 +701,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
compute ??= index;
|
||||
}
|
||||
|
||||
// MoltenVK exposes only general families, one queue each, every one its own
|
||||
// Metal command queue: a second one still runs readbacks beside the main queue.
|
||||
if ((flags & (QueueFlags.TransferBit | QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) != 0)
|
||||
{
|
||||
general ??= index;
|
||||
}
|
||||
}
|
||||
|
||||
return compute ?? general;
|
||||
}
|
||||
|
||||
// Measures GLSL UnpackHalf2x16 / PackHalf2x16 against the translator's own f16 conversion
|
||||
// once per device, before any guest shader is compiled, so the answer is constant for the
|
||||
// process. The spec does not promise it: MoltenVK reports RTE rounding and signed-zero /
|
||||
// Inf / NaN preservation for f16 yet not denorm preservation, so only the device can say.
|
||||
// Never fatal - anything that goes wrong leaves the per-access checks in place. The probe
|
||||
// reads out of range on purpose, so it only runs where the device promised a zero: without
|
||||
// robustBufferAccess2 an out-of-range read is undefined and may fault the device.
|
||||
private void ProbeZeroOutOfBoundsReads()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _zeroOutOfBoundsReadsProbed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var forced = ZeroOutOfBoundsReadProbe.ReadOverride();
|
||||
if (forced is not null)
|
||||
{
|
||||
Volatile.Write(ref _zeroOutOfBoundsReads, forced.Value ? 1 : 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[VK][OOB] out-of-range buffer reads zero={forced.Value} mismatches=unprobed tested=0 " +
|
||||
"(SHARPEMU_ZERO_OOB_READS)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_robustBufferAccess2Enabled)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[VK][OOB] out-of-range buffer reads zero=False mismatches=unprobed tested=0 " +
|
||||
"note=robustBufferAccess2 is not enabled on this device");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = ZeroOutOfBoundsReadProbe.Run(_deviceInfo, _scheduler, CurrentRecordingBuffer);
|
||||
Volatile.Write(ref _zeroOutOfBoundsReads, result.Zeroed ? 1 : 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[VK][OOB] out-of-range buffer reads zero={result.Zeroed} " +
|
||||
$"mismatches={result.Mismatches} tested={result.Tested}" +
|
||||
(result.Note is null ? string.Empty : $" note={result.Note}"));
|
||||
}
|
||||
|
||||
// Never fatal - anything that goes wrong leaves the exact emulation in place.
|
||||
private void ProbeNativeHalfConversion()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _nativeHalfConversionProbed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var forced = NativeHalfConversionProbe.ReadOverride();
|
||||
if (forced is not null)
|
||||
{
|
||||
Volatile.Write(ref _nativeHalfConversionExact, forced.Value ? 1 : 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[VK][F16] native half conversion exact={forced.Value} mismatches=unprobed tested=0 " +
|
||||
"(SHARPEMU_NATIVE_HALF)");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = NativeHalfConversionProbe.Run(_deviceInfo, _scheduler, CurrentRecordingBuffer);
|
||||
Volatile.Write(ref _nativeHalfConversionExact, result.Exact ? 1 : 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[VK][F16] native half conversion exact={result.Exact} " +
|
||||
$"mismatches={result.Mismatches} tested={result.Tested}" +
|
||||
(result.Note is null ? string.Empty : $" note={result.Note}") +
|
||||
(result.Detail is null ? string.Empty : $" first={result.Detail}"));
|
||||
return compute;
|
||||
}
|
||||
|
||||
private bool _supportsFragmentShaderBarycentric;
|
||||
|
|
@ -1124,8 +1046,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
maintenance8Features.PNext = null;
|
||||
robustness2Features.RobustBufferAccess2 =
|
||||
supportsRobustBufferAccess2 && supportedFeatures.RobustBufferAccess;
|
||||
// The feature only reaches the device when the robustness2 struct is chained.
|
||||
_robustBufferAccess2Enabled = robustness2Features.RobustBufferAccess2 && supportsRobustness2;
|
||||
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
|
||||
robustness2Features.NullDescriptor = supportsNullDescriptor;
|
||||
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
|
||||
|
|
|
|||
|
|
@ -92,10 +92,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
|
||||
bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance;
|
||||
|
||||
bool IShaderPipelineHost.NativeHalfConversionExact => NativeHalfConversionExact;
|
||||
|
||||
bool IShaderPipelineHost.ZeroOutOfBoundsBufferReads => ZeroOutOfBoundsBufferReads;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
|
||||
|
||||
SampleCountFlags IShaderPipelineHost.NoAttachmentSampleCounts => _noAttachmentSampleCounts;
|
||||
|
|
|
|||
|
|
@ -585,13 +585,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var view = binding.Request.View;
|
||||
for (var layer = view.BaseLayer; layer < view.BaseLayer + view.LayerCount; layer++)
|
||||
{
|
||||
// Every fixed clear code carries its own colour; only the register code (0x20) needs
|
||||
// the CB clear word, which is known when the surface binds as a target. Silent Hill
|
||||
// fast-clears UE's separate-translucency target to (0,0,0,1) and only samples it:
|
||||
// skipping that clear left stale zeros that blacked out the whole title-menu scene.
|
||||
var tracked = _imageCache.IsMetadataCleared(metadataAddress, layer, out var metadataValue) && (byte)metadataValue == DccClearToZero;
|
||||
var clearValue = default(ClearColorValue);
|
||||
var tracked = _imageCache.IsMetadataCleared(metadataAddress, layer, out var metadataValue) &&
|
||||
TryDecodeDccClear((byte)metadataValue, false, (byte)metadataValue == DccClearToZero || fixedClearSupported, default, out clearValue);
|
||||
ulong guestSlice = 0;
|
||||
if (!tracked &&
|
||||
(sliceSize == 0 || !_imageCache.TryReadGuestDccClear(metadataAddress, sliceSize, layer, out guestSlice, out var code) ||
|
||||
|
|
@ -1046,31 +1041,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
CountDraw();
|
||||
}
|
||||
|
||||
void IRenderHost.DrawIndirect(BufferBinding arguments)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
|
||||
var command = BeginBatchedGuestCommands();
|
||||
if (_boundGraphicsPipeline is { RectangleList: true } entry)
|
||||
{
|
||||
// The count is unknown here, so neither the single-rectangle strip nor a
|
||||
// count check for native rectangles applies.
|
||||
if (_supportsFillRectangle)
|
||||
{
|
||||
BindNativeRectangleList(entry, command);
|
||||
}
|
||||
else
|
||||
{
|
||||
BindRectangleListVariant(entry, strip: false, command);
|
||||
}
|
||||
}
|
||||
|
||||
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Preparation);
|
||||
_vk.CmdDrawIndirect(command, new VkBuffer(arguments.Handle), arguments.Offset, 1, 16);
|
||||
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Draw,
|
||||
_boundGraphicsPipeline?.Id ?? 0, 0, 0);
|
||||
CountDraw();
|
||||
}
|
||||
|
||||
public void Dispatch(uint groupsX, uint groupsY, uint groupsZ)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
|
||||
|
|
|
|||
|
|
@ -101,11 +101,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private void CreateImageCache()
|
||||
{
|
||||
var (memory, _, backing) = RequireGuestMemory("image store");
|
||||
// The guest reads linear GPU-written images back on the CPU (UE's exposure readback
|
||||
// feeds View.PreExposure), so they are published to guest memory after their tick.
|
||||
// SHARPEMU_READBACK_LINEAR_IMAGES=0 keeps them GPU-only.
|
||||
_imageCache = new GuestImageCache(_deviceInfo, _scheduler, memory.Pages, _bufferCache, backing,
|
||||
readbackLinearImages: Environment.GetEnvironmentVariable("SHARPEMU_READBACK_LINEAR_IMAGES") != "0");
|
||||
_imageCache = new GuestImageCache(_deviceInfo, _scheduler, memory.Pages, _bufferCache, backing, readbackLinearImages: false);
|
||||
_bufferCache.ImageCache = _imageCache;
|
||||
_samplerStore = new SamplerStore(_deviceInfo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,8 +162,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
bool execGuardElision)
|
||||
{
|
||||
if (!ShaderProgramCache.TryCompilePrewarm(
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision, NativeHalfConversionExact,
|
||||
ZeroOutOfBoundsBufferReads, out var compiled, out var layout, out var error))
|
||||
record, code, compiler, sharedInt64Atomics, execGuardElision, out var compiled, out var layout, out var error))
|
||||
{
|
||||
NoteShaderPrewarmFailure(record, error);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -97,22 +97,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
(ShaderStageFlags)Volatile.Read(ref _nativeSubgroupShaderStages),
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GRAPHICS_SUBGROUPS"));
|
||||
|
||||
private static int _nativeHalfConversionExact;
|
||||
private static int _nativeHalfConversionProbed;
|
||||
private static int _zeroOutOfBoundsReads;
|
||||
private static int _zeroOutOfBoundsReadsProbed;
|
||||
private static bool _robustBufferAccess2Enabled;
|
||||
|
||||
// Set once per process by the device-setup probe: GLSL UnpackHalf2x16 / PackHalf2x16 produced
|
||||
// exactly what the translator's integer f16 conversion produces, for every test vector. False
|
||||
// until then, so a device that is never probed keeps the exact emulation.
|
||||
internal static bool NativeHalfConversionExact => Volatile.Read(ref _nativeHalfConversionExact) != 0;
|
||||
|
||||
// Set once per process by the device-setup probe: a storage-buffer read past the end of its
|
||||
// descriptor range returned zero on this device, so the translator's own range test, address
|
||||
// clamp and zero select on every guest buffer word are redundant. False until then.
|
||||
internal static bool ZeroOutOfBoundsBufferReads => Volatile.Read(ref _zeroOutOfBoundsReads) != 0;
|
||||
|
||||
private static void SetNativeSubgroupCapabilities(uint subgroupSize, ShaderStageFlags supportedStages)
|
||||
{
|
||||
Volatile.Write(ref _nativeSubgroupShaderStages, (int)supportedStages);
|
||||
|
|
|
|||
|
|
@ -1660,17 +1660,10 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
||||
// one per result lane. Every f16<->f32 conversion goes through
|
||||
// EmitHalfToFloat / EmitFloatToHalf, which default to the explicit integer
|
||||
// sequences below rather than GLSL UnpackHalf2x16 / PackHalf2x16, whose
|
||||
// subnormal and rounding behaviour is implementation-defined without
|
||||
// float-controls execution modes. The host switches those two helpers to the
|
||||
// native ext instructions (ShaderCompileRequest.NativeHalfConversionExact) only
|
||||
// after measuring them bit-exact against these sequences on the running device,
|
||||
// because a device capability bit does not promise that: MoltenVK on Apple
|
||||
// silicon reports shaderRoundingModeRTEFloat16 and
|
||||
// shaderSignedZeroInfNanPreserveFloat16 yet not shaderDenormPreserveFloat16.
|
||||
// The two
|
||||
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
||||
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
||||
// GLSL UnpackHalf2x16 / PackHalf2x16, whose subnormal and rounding behaviour
|
||||
// is implementation-defined without float-controls execution modes. The two
|
||||
// lanes are computed independently: each operand half is widened exactly to
|
||||
// f32, op_sel/op_sel_hi pick the source half and neg_lo/neg_hi negate it, the
|
||||
// op runs in f32, and the result is rounded back to f16 with round-to-nearest-
|
||||
|
|
@ -1785,11 +1778,8 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
// Reads one V_FMA_MIX source as an f32. op_sel_hi selects whether a
|
||||
// register or literal operand is taken as an f16 (the half picked by op_sel,
|
||||
// widened exactly to f32) or as a full f32; inline constants keep their value
|
||||
// either way. A literal is an f16 bit pattern then: reading 0x34CD as an f32
|
||||
// made Silent Hill's luminance weights denormal zeros, and the lighting that
|
||||
// divides by that luminance came out thousands of times too bright. The
|
||||
// register operand is taken as an f16 (the half picked by op_sel, widened
|
||||
// exactly to f32) or as a full f32; inline constants are always f32. The
|
||||
// per-operand neg_hi bit takes the absolute value and neg negates, in that
|
||||
// order (abs-then-neg), reusing the VOP3P modifier fields the way the mix
|
||||
// ops define them rather than the packed low/high-lane meaning.
|
||||
|
|
@ -1801,7 +1791,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var source = instruction.Sources[index];
|
||||
var readAsHalf =
|
||||
((control.OpSelHiMask >> index) & 1) != 0 &&
|
||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister or Gen5OperandKind.LiteralConstant;
|
||||
source.Kind is Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister;
|
||||
|
||||
uint value;
|
||||
if (readAsHalf)
|
||||
|
|
@ -2048,18 +2038,8 @@ public static partial class Gen5SpirvTranslator
|
|||
// Widens an f16 value held in the low 16 bits of `halfBits` to an f32 bit
|
||||
// pattern, exactly (subnormals normalised, Inf/NaN and signed zero preserved).
|
||||
// Mirrors the branchless HalfToFloat reference validated against System.Half.
|
||||
// With NativeHalfConversionExact the same value comes from GLSL UnpackHalf2x16,
|
||||
// which the driver lowers to one hardware convert instead of ~20 integer ops.
|
||||
private uint EmitHalfToFloat(uint halfBits)
|
||||
{
|
||||
if (_nativeHalfConversionExact)
|
||||
{
|
||||
var unpacked = Ext(62, _vec2Type, BitwiseAnd(halfBits, UInt(0xFFFF)));
|
||||
return Bitcast(
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, unpacked, 0));
|
||||
}
|
||||
|
||||
var sign = ShiftLeftLogical(BitwiseAnd(halfBits, UInt(0x8000)), UInt(16));
|
||||
var exponent = BitwiseAnd(ShiftRightLogical(halfBits, UInt(10)), UInt(0x1F));
|
||||
var mantissa = BitwiseAnd(halfBits, UInt(0x3FF));
|
||||
|
|
@ -2089,19 +2069,8 @@ public static partial class Gen5SpirvTranslator
|
|||
// Narrows an f32 bit pattern to an f16 value in the low 16 bits, rounding to
|
||||
// nearest even (subnormals, overflow-to-Inf and NaN/Inf handled). Mirrors the
|
||||
// branchless FloatToHalf reference validated exhaustively against System.Half.
|
||||
// With NativeHalfConversionExact the same value comes from GLSL PackHalf2x16.
|
||||
private uint EmitFloatToHalf(uint bits)
|
||||
{
|
||||
if (_nativeHalfConversionExact)
|
||||
{
|
||||
var pair = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec2Type,
|
||||
Bitcast(_floatType, bits),
|
||||
Float(0));
|
||||
return BitwiseAnd(Ext(58, _uintType, pair), UInt(0xFFFF));
|
||||
}
|
||||
|
||||
var sign = BitwiseAnd(ShiftRightLogical(bits, UInt(16)), UInt(0x8000));
|
||||
var absolute = BitwiseAnd(bits, UInt(0x7FFF_FFFF));
|
||||
|
||||
|
|
@ -3955,7 +3924,7 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
}
|
||||
var shuffled = ShuffleHalfWaveLane(value, targetLane);
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
if (control.FetchInactive)
|
||||
{
|
||||
return shuffled;
|
||||
|
|
@ -3968,7 +3937,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
ShuffleHalfWaveLane(activeWord, targetLane));
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -3991,7 +3960,7 @@ public static partial class Gen5SpirvTranslator
|
|||
// operations are limited to a single half-wave for some encodings, so we
|
||||
// must not clamp wave64 lanes to 31; use the full lane mask instead.
|
||||
safeTarget = BitwiseAnd(safeTarget, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleHalfWaveLane(value, safeTarget);
|
||||
var shuffled = ShuffleLane(value, safeTarget);
|
||||
|
||||
var sourceAvailable = inRange;
|
||||
if (!control.FetchInactive)
|
||||
|
|
@ -4002,7 +3971,7 @@ public static partial class Gen5SpirvTranslator
|
|||
Load(_boolType, _exec),
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var shuffledActive = ShuffleHalfWaveLane(activeWord, safeTarget);
|
||||
var shuffledActive = ShuffleLane(activeWord, safeTarget);
|
||||
sourceAvailable = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
|
|
@ -5040,14 +5009,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var sourceValue = GetRawSource(instruction, 0);
|
||||
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask));
|
||||
|
||||
if (_emulateWave64 && instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister &&
|
||||
TryGetConstantLane(instruction, out var spillLane) &&
|
||||
_laneSpillSlots.TryGetValue((instruction.Sources[0].Value, spillLane), out var spillSlot))
|
||||
{
|
||||
// A pure scalar spill slot: every invocation already holds the wave-uniform value.
|
||||
StoreS(destination, Load(_uintType, spillSlot));
|
||||
}
|
||||
else if (_emulateWave64)
|
||||
if (_emulateWave64)
|
||||
{
|
||||
// The selected guest lane can belong to another host subgroup.
|
||||
// Read it even when the guest execution mask disables that lane.
|
||||
|
|
@ -5124,9 +5086,11 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var targetLane = IAdd(rowBase, selector);
|
||||
// Select a guest lane first; ShuffleHalfWaveLane maps it to the host subgroup.
|
||||
// Mask to guest wave size — on Radeon hardware DPP is limited to a
|
||||
// single half-wave for some encodings, but we must not clamp wave64
|
||||
// lanes to 31; use the full lane mask instead.
|
||||
targetLane = BitwiseAnd(targetLane, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleHalfWaveLane(value, targetLane);
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
var fetchInactive = (control.OperandSelect & 1) != 0;
|
||||
if (fetchInactive)
|
||||
{
|
||||
|
|
@ -5140,7 +5104,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
ShuffleHalfWaveLane(activeWord, targetLane));
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -5235,33 +5199,12 @@ public static partial class Gen5SpirvTranslator
|
|||
return Bitcast(_uintType, value);
|
||||
}
|
||||
|
||||
// Rounds an f32 toward zero onto an f16 value, so packHalf2x16 then encodes it exactly
|
||||
// (V_CVT_PKRTZ_F16_F32). Toward zero, a finite value beyond the f16 range becomes
|
||||
// +-65504, never infinity: Silent Hill's deferred lighting packs values past 65504 and
|
||||
// its fog pass then scales them down, while an infinity stays infinite, turns into NaN
|
||||
// and blacks out the whole title-menu scene. Subnormal results keep whole 2^-24 steps.
|
||||
private uint TruncateFloat32ForPack(uint value)
|
||||
{
|
||||
var normal = Bitcast(
|
||||
_floatType,
|
||||
BitwiseAnd(Bitcast(_uintType, Ext(43, _floatType, value, Float(-65504f), Float(65504f))), UInt(0xFFFF_E000)));
|
||||
var subnormal = _module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
Ext(3, _floatType, _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(16777216f))),
|
||||
Float(1f / 16777216f));
|
||||
var isSubnormal = _module.AddInstruction(
|
||||
SpirvOp.FOrdLessThan,
|
||||
_boolType,
|
||||
Ext(4, _floatType, value),
|
||||
Float(6.10351562e-05f));
|
||||
var finite = _module.AddInstruction(SpirvOp.Select, _floatType, isSubnormal, subnormal, normal);
|
||||
var special = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, value),
|
||||
_module.AddInstruction(SpirvOp.IsInf, _boolType, value));
|
||||
return _module.AddInstruction(SpirvOp.Select, _floatType, special, value, finite);
|
||||
var raw = BitwiseAnd(
|
||||
Bitcast(_uintType, value),
|
||||
UInt(0xFFFF_E000));
|
||||
return Bitcast(_floatType, raw);
|
||||
}
|
||||
|
||||
private uint Ext(uint operation, uint resultType, params uint[] operands)
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
// Internal resolution scaling. The host may hold a guest render target at another size, so
|
||||
// everything the program expresses in pixels has to be translated: its own pixel position,
|
||||
// the integer coordinates it addresses an image with and the sizes a resource query reports.
|
||||
// Which images are scaled is a per-draw fact, so the factors arrive in shader data rather
|
||||
// than through a permutation.
|
||||
public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
private sealed partial class CompilationContext
|
||||
{
|
||||
// Shader-data dwords, relative to BindingLayout.RenderScaleDword.
|
||||
private const uint ImageCoordinateFactorDword = 0;
|
||||
private const uint ImageSizeFactorDword = 1;
|
||||
private const uint ScaledImageMaskLowDword = 2;
|
||||
private const uint ScaledImageMaskHighDword = 3;
|
||||
private const uint PixelPositionFactorDword = 4;
|
||||
|
||||
private bool UsesRenderScale => _request.Bindings.UsesRenderScale;
|
||||
|
||||
private uint LoadRenderScaleFloat(uint dword) =>
|
||||
Bitcast(_floatType, LoadShaderDataDword(UInt(_request.Bindings.RenderScaleDword + dword)));
|
||||
|
||||
// One when the host holds this image at guest resolution, the scale otherwise. The
|
||||
// mask is loaded per use so the value never has to dominate a later block.
|
||||
private uint LoadImageScaleFactor(in SpirvImageResource resource, uint factorDword)
|
||||
{
|
||||
var index = resource.ResourceIndex;
|
||||
var maskDword = index < 32 ? ScaledImageMaskLowDword : ScaledImageMaskHighDword;
|
||||
var bit = UInt(1u << (int)(index & 31));
|
||||
var mask = LoadShaderDataDword(UInt(_request.Bindings.RenderScaleDword + maskDword));
|
||||
var scaled = _module.AddInstruction(SpirvOp.INotEqual, _boolType,
|
||||
_module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, mask, bit), UInt(0));
|
||||
return _module.AddInstruction(SpirvOp.Select, _floatType, scaled, LoadRenderScaleFloat(factorDword), Float(1f));
|
||||
}
|
||||
|
||||
// The pixel position addresses host texels; dividing x and y by the attachments'
|
||||
// factor hands the program the guest pixel it would have shaded natively.
|
||||
private uint ScalePixelPosition(uint fragCoord)
|
||||
{
|
||||
if (!UsesRenderScale)
|
||||
{
|
||||
return fragCoord;
|
||||
}
|
||||
|
||||
var factor = LoadRenderScaleFloat(PixelPositionFactorDword);
|
||||
var components = new uint[4];
|
||||
for (uint component = 0; component < 4; component++)
|
||||
{
|
||||
var value = _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, fragCoord, component);
|
||||
components[component] = component < 2
|
||||
? _module.AddInstruction(SpirvOp.FMul, _floatType, value, factor)
|
||||
: value;
|
||||
}
|
||||
|
||||
return _module.AddInstruction(SpirvOp.CompositeConstruct, _vec4Type, components);
|
||||
}
|
||||
|
||||
// A guest pixel coordinate in host texels. The program computed it against the guest
|
||||
// size, so it is multiplied by the factor and truncated back to a texel index.
|
||||
private uint ScaleIntegerImageCoordinate(uint coordinate, in SpirvImageResource resource)
|
||||
{
|
||||
if (!UsesRenderScale)
|
||||
{
|
||||
return coordinate;
|
||||
}
|
||||
|
||||
var factor = LoadImageScaleFactor(resource, ImageCoordinateFactorDword);
|
||||
var scaled = _module.AddInstruction(SpirvOp.FMul, _floatType,
|
||||
_module.AddInstruction(SpirvOp.ConvertSToF, _floatType, coordinate), factor);
|
||||
return _module.AddInstruction(SpirvOp.ConvertFToS, _intType, scaled);
|
||||
}
|
||||
|
||||
// A resource query reports the size the game declared, not the host's: the program
|
||||
// derives texture coordinates and loop bounds from it in guest pixels.
|
||||
private uint ScaleImageQuerySize(uint size, in SpirvImageResource resource)
|
||||
{
|
||||
if (!UsesRenderScale)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
var factor = LoadImageScaleFactor(resource, ImageSizeFactorDword);
|
||||
var scaled = _module.AddInstruction(SpirvOp.FAdd, _floatType,
|
||||
_module.AddInstruction(SpirvOp.FMul, _floatType,
|
||||
_module.AddInstruction(SpirvOp.ConvertSToF, _floatType, size), factor),
|
||||
Float(0.5f));
|
||||
return _module.AddInstruction(SpirvOp.ConvertFToS, _intType, scaled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,8 +111,6 @@ public static partial class Gen5SpirvTranslator
|
|||
_stage == Gen5SpirvStage.Pixel &&
|
||||
request.Program.Instructions.Any(static instruction => instruction.Control is Gen5ExportControl { ValidMask: true });
|
||||
_enableGraphicsSubgroupOperations = _stage == Gen5SpirvStage.Compute || request.EnableGraphicsSubgroupOperations;
|
||||
_nativeHalfConversionExact = request.NativeHalfConversionExact;
|
||||
_zeroOutOfBoundsBufferReads = request.ZeroOutOfBoundsBufferReads;
|
||||
_waveLaneCount = request.WaveSize == 64 ? 64u : 32u;
|
||||
_localSizeX = Math.Max(request.LocalSizeX, 1);
|
||||
_localSizeY = Math.Max(request.LocalSizeY, 1);
|
||||
|
|
@ -418,11 +416,6 @@ public static partial class Gen5SpirvTranslator
|
|||
// A bounds-checked dword of a storage block; outside the block reads zero.
|
||||
private uint LoadBlockWord(uint block, uint dwordIndex)
|
||||
{
|
||||
if (_zeroOutOfBoundsBufferReads)
|
||||
{
|
||||
return Load(_uintType, BlockWordPointer(block, dwordIndex));
|
||||
}
|
||||
|
||||
var length = _module.AddInstruction(SpirvOp.ArrayLength, _uintType, block, 0);
|
||||
var inRange = _module.AddInstruction(SpirvOp.ULessThan, _boolType, dwordIndex, length);
|
||||
var safeIndex = _module.AddInstruction(SpirvOp.Select, _uintType, inRange, dwordIndex, UInt(0));
|
||||
|
|
@ -1597,8 +1590,7 @@ public static partial class Gen5SpirvTranslator
|
|||
imageClass.Dimension,
|
||||
imageInfo.ConversionFormat,
|
||||
imageInfo.ShaderSwizzle,
|
||||
imageInfo.EmulatedCompareFunction,
|
||||
(uint)resourceIndex);
|
||||
imageInfo.EmulatedCompareFunction);
|
||||
dstSelect = imageInfo.ShaderSwizzle;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,6 @@ public static partial class Gen5SpirvTranslator
|
|||
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
|
||||
private readonly bool _usesPixelValidMask;
|
||||
private readonly bool _enableGraphicsSubgroupOperations;
|
||||
private readonly bool _nativeHalfConversionExact;
|
||||
private readonly bool _zeroOutOfBoundsBufferReads;
|
||||
private readonly uint _waveLaneCount;
|
||||
private readonly bool _emulateWave64;
|
||||
|
||||
|
|
@ -156,7 +154,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private readonly Dictionary<uint, uint> _vectorRegisterVariables = new();
|
||||
private uint _vectorRegisters;
|
||||
private (uint First, uint Last) _dynamicVectorRange;
|
||||
private readonly Dictionary<uint, uint> _packedHalfRegisterVariables = [];
|
||||
private uint _packedHalfRegisters;
|
||||
private uint _scc;
|
||||
private uint _vcc;
|
||||
private uint _exec;
|
||||
|
|
@ -174,7 +172,6 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _storageBlockPointer;
|
||||
private uint _storageUintPointer;
|
||||
private uint _lds;
|
||||
private uint _ldsWritten;
|
||||
private uint _ldsElementPointer;
|
||||
private uint _lds64ElementPointer;
|
||||
private uint _ldsDwordMask;
|
||||
|
|
@ -234,9 +231,7 @@ public static partial class Gen5SpirvTranslator
|
|||
SpirvImageDim Dimension = SpirvImageDim.Dim2D,
|
||||
uint ConversionFormat = 0,
|
||||
uint ShaderSwizzle = 0,
|
||||
int EmulatedCompareFunction = -1,
|
||||
// The dense image resource index, which names this image's bit in the scaled-image mask.
|
||||
uint ResourceIndex = 0);
|
||||
int EmulatedCompareFunction = -1);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
uint Variable,
|
||||
|
|
@ -777,24 +772,14 @@ public static partial class Gen5SpirvTranslator
|
|||
var ldsPointer = _module.TypePointer(storageClass, ldsArrayType);
|
||||
_ldsElementPointer = _module.TypePointer(storageClass, _uintType);
|
||||
_lds64ElementPointer = _module.TypePointer(storageClass, _ulongType);
|
||||
// A private array must read zero where this invocation has not written, but
|
||||
// zero-initializing it costs every invocation the whole array: Metal keeps a
|
||||
// dynamically indexed private array in stack memory, so an NGG vertex shader
|
||||
// cleared 8 KiB per vertex. A written-dword bitmap clears 1/32 of that, and
|
||||
// LdsPointer zeroes a slot the first time it is addressed.
|
||||
_lds = _module.AddGlobalVariable(ldsPointer, storageClass);
|
||||
_lds = storageClass == SpirvStorageClass.Workgroup
|
||||
? _module.AddGlobalVariable(ldsPointer, storageClass)
|
||||
: _module.AddGlobalVariable(
|
||||
ldsPointer,
|
||||
storageClass,
|
||||
_module.ConstantNull(ldsArrayType));
|
||||
_module.AddName(_lds, "lds");
|
||||
_interfaces.Add(_lds);
|
||||
if (storageClass == SpirvStorageClass.Private)
|
||||
{
|
||||
var bitmapType = _module.TypeArray(_uintType, (arrayDwordCount + 31) / 32);
|
||||
_ldsWritten = _module.AddGlobalVariable(
|
||||
_module.TypePointer(SpirvStorageClass.Private, bitmapType),
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantNull(bitmapType));
|
||||
_module.AddName(_ldsWritten, "ldsWritten");
|
||||
_interfaces.Add(_ldsWritten);
|
||||
}
|
||||
}
|
||||
|
||||
internal static SpirvImageFormat DecodeStorageImageFormat(
|
||||
|
|
@ -1262,7 +1247,7 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else if (_stage == Gen5SpirvStage.Pixel)
|
||||
{
|
||||
var fragCoord = ScalePixelPosition(Load(_vec4Type, _fragCoordInput));
|
||||
var fragCoord = Load(_vec4Type, _fragCoordInput);
|
||||
EmitPixelInputState(fragCoord);
|
||||
foreach (var output in _pixelOutputs.Values)
|
||||
{
|
||||
|
|
@ -1468,14 +1453,10 @@ public static partial class Gen5SpirvTranslator
|
|||
var block = blocks[blockIndex];
|
||||
var halfMaskPlan = HalfMaskPlan();
|
||||
// One guest wave can span two host subgroups. Keep its shared-memory phases ordered.
|
||||
// The half-mask plan's barriers only order its own mask exchanges, not the program's
|
||||
// LDS traffic: with phase ordering off under the plan, UE's FFT bloom (wave64, 512
|
||||
// threads, LDS butterflies) read half-written rows and blurred Silent Hill's whole
|
||||
// post chain into a flat haze.
|
||||
var synchronizeSharedMemory = _emulateWave64;
|
||||
// The half-mask plan inserts its own barriers; other wave64 programs also
|
||||
// need LDS phase ordering when reads and writes span basic blocks.
|
||||
var synchronizeSharedMemory = _emulateWave64 && halfMaskPlan is null;
|
||||
var sharedMemoryPhase = SharedMemoryPhase.None;
|
||||
// A wait in another block may or may not have run before this one.
|
||||
_vectorMemoryWaited = null;
|
||||
for (var index = block.StartIndex; index < block.EndIndex; index++)
|
||||
{
|
||||
var instruction = _request.Program.Instructions[index];
|
||||
|
|
@ -1511,50 +1492,6 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
// A run of plain vector ALU writes shares one EXEC test: inside the branch no lane
|
||||
// is masked off, so each write stores straight into its register instead of
|
||||
// selecting against the old value, and the old values stop being live across the
|
||||
// whole run. Only the join needs them.
|
||||
var runLength = ExecRunLength(block, halfMaskPlan, index);
|
||||
if (runLength >= ExecRunMinimum)
|
||||
{
|
||||
var last = index + runLength - 1;
|
||||
var failed = false;
|
||||
var runFailure = string.Empty;
|
||||
EmitConditional(Load(_boolType, _exec), () =>
|
||||
{
|
||||
for (var inner = index; inner <= last && !failed; inner++)
|
||||
{
|
||||
var run = _request.Program.Instructions[inner];
|
||||
_execKnownFull = true;
|
||||
_emittingPc = run.Pc;
|
||||
var ok = TryEmitInstruction(run, out var runError);
|
||||
_emittingPc = null;
|
||||
_execKnownFull = false;
|
||||
if (!ok)
|
||||
{
|
||||
runFailure = $"pc=0x{run.Pc:X} {run.Opcode}: {runError}";
|
||||
failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
CapturePixelVgprs(run);
|
||||
CapturePixelVgprPoints(run);
|
||||
MarkPixelPath(run);
|
||||
CapturePixelExec(run);
|
||||
}
|
||||
});
|
||||
|
||||
if (failed)
|
||||
{
|
||||
error = runFailure;
|
||||
return false;
|
||||
}
|
||||
|
||||
index = last;
|
||||
continue;
|
||||
}
|
||||
|
||||
_execKnownFull = IsExecKnownFull(instruction.Pc);
|
||||
_emittingPc = instruction.Pc;
|
||||
var emitted = TryEmitInstruction(instruction, out error);
|
||||
|
|
@ -1576,59 +1513,6 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
// SHARPEMU_EXEC_RUN=0 turns the shared EXEC test off; SHARPEMU_EXEC_RUN_MIN sets how many
|
||||
// instructions a run needs before the branch pays for itself.
|
||||
private static readonly bool ExecRunGrouping =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_EXEC_RUN") != "0";
|
||||
|
||||
private static readonly int ExecRunMinimum =
|
||||
ExecRunGrouping && int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_EXEC_RUN_MIN"), out var minimum) && minimum >= 2
|
||||
? minimum
|
||||
: ExecRunGrouping ? DefaultExecRunMinimum : int.MaxValue;
|
||||
|
||||
// Six measured best on Silent Hill's volumetric fog: shorter runs pay for the branch more
|
||||
// often than they save, longer ones leave the short runs guarded.
|
||||
private const int DefaultExecRunMinimum = 6;
|
||||
|
||||
// How many instructions from startIndex may share one EXEC test. The run stops at anything
|
||||
// the wave as a whole has to run even where this lane is masked off: every scalar result
|
||||
// (SALU is not EXEC masked on the hardware, and each invocation keeps its own copy of the
|
||||
// scalar registers), every cross-lane read, every memory or LDS access, and every point the
|
||||
// half-mask plan wants a mask exchange or a barrier emitted before.
|
||||
private int ExecRunLength(ShaderBlock block, Ir.Gen5Wave64HalfMaskPlan? halfMaskPlan, int startIndex)
|
||||
{
|
||||
// Compute only. The same grouping measured 28% slower on Silent Hill's deferred
|
||||
// lighting pixel program (11.0 -> 14.1 ms per scope): a pixel wave already runs with
|
||||
// helper lanes and an extra branch per run costs it more than the selects it removes.
|
||||
if (!ExecRunGrouping || _stage != Gen5SpirvStage.Compute)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var instructions = _request.Program.Instructions;
|
||||
var length = 0;
|
||||
for (var index = startIndex; index < block.EndIndex; index++)
|
||||
{
|
||||
var instruction = instructions[index];
|
||||
if (length != 0 && halfMaskPlan is not null &&
|
||||
(halfMaskPlan.ExactPairsBefore.ContainsKey(instruction.Pc) ||
|
||||
halfMaskPlan.SharedMemoryBarriersBefore.Contains(instruction.Pc)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Ir.Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(instruction))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
length++;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
// Stores the next dispatcher block (or ends the program) for a block's terminator.
|
||||
private bool TryEmitBlockTerminator(
|
||||
IReadOnlyList<ShaderBlock> blocks,
|
||||
|
|
@ -2250,15 +2134,10 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
return true;
|
||||
}
|
||||
if (instruction.Opcode == "SWaitcnt")
|
||||
{
|
||||
NoteWaitCount(instruction);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode is
|
||||
"SNop" or
|
||||
"SSetregB32" or
|
||||
"SWaitcnt" or
|
||||
"SInstPrefetch" or
|
||||
"STtraceData" or
|
||||
// Wave scheduling priority hint; no effect on results.
|
||||
|
|
@ -2275,17 +2154,11 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
// s_barrier orders execution; what it publishes is whatever the guest
|
||||
// waited for first. s_waitcnt vmcnt(0) before it means buffer and image
|
||||
// stores are being published to the workgroup, so the barrier carries
|
||||
// AcquireRelease over uniform, workgroup and image memory. A barrier that
|
||||
// only follows lgkmcnt(0) publishes LDS, and ordering device memory there
|
||||
// costs a cache flush the guest never asked for: UE's FFT bloom
|
||||
// convolution barriers 48 times per group, every one of them lgkmcnt-only.
|
||||
// s_waitcnt vmcnt(0) + s_barrier also publishes buffer and image
|
||||
// stores to the workgroup: AcquireRelease over uniform, workgroup
|
||||
// and image memory.
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(_vectorMemoryWaited == false
|
||||
? 0x8u | 0x100u
|
||||
: 0x8u | 0x40u | 0x100u | 0x800u);
|
||||
var semantics = UInt(0x8 | 0x40 | 0x100 | 0x800);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
|
|
@ -2810,30 +2683,11 @@ public static partial class Gen5SpirvTranslator
|
|||
var index = BitwiseAnd(
|
||||
ShiftRightLogical(addressWithOffset, UInt(2)),
|
||||
UInt(_ldsDwordMask));
|
||||
var pointer = _module.AddInstruction(
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_ldsElementPointer,
|
||||
_lds,
|
||||
index);
|
||||
if (_ldsWritten != 0)
|
||||
{
|
||||
var wordPointer = _module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateUintPointer,
|
||||
_ldsWritten,
|
||||
ShiftRightLogical(index, UInt(5)));
|
||||
var bit = ShiftLeftLogical(UInt(1), BitwiseAnd(index, UInt(31)));
|
||||
var word = Load(_uintType, wordPointer);
|
||||
var unwritten = _module.AddInstruction(
|
||||
SpirvOp.IEqual, _boolType, BitwiseAnd(word, bit), UInt(0));
|
||||
EmitConditional(unwritten, () =>
|
||||
{
|
||||
Store(pointer, UInt(0));
|
||||
Store(wordPointer, BitwiseOr(word, bit));
|
||||
});
|
||||
}
|
||||
|
||||
return pointer;
|
||||
}
|
||||
|
||||
private void StoreLds(uint pointer, uint value)
|
||||
|
|
@ -4023,85 +3877,21 @@ public static partial class Gen5SpirvTranslator
|
|||
ShiftRightLogical(descriptorWord3, UInt(12)),
|
||||
UInt(0x7F));
|
||||
var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat);
|
||||
var one = Gfx10FormatOne(numberFormat);
|
||||
|
||||
// Dword-aligned 32-bit components whose number format keeps the raw bits (most
|
||||
// vertex streams) load one word per component; every other format takes the
|
||||
// generic byte-wise decode. The descriptor is uniform, so the branch never diverges.
|
||||
var wordCount = UInt(0);
|
||||
wordCount = SelectUInt(dataFormat, 4, UInt(1), wordCount);
|
||||
wordCount = SelectUInt(dataFormat, 11, UInt(2), wordCount);
|
||||
wordCount = SelectUInt(dataFormat, 13, UInt(3), wordCount);
|
||||
wordCount = SelectUInt(dataFormat, 14, UInt(4), wordCount);
|
||||
var rawBits = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, numberFormat, UInt(7)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, numberFormat, UInt(4)),
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, numberFormat, UInt(5))));
|
||||
var wordPath = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd, _boolType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalAnd, _boolType,
|
||||
_module.AddInstruction(SpirvOp.INotEqual, _boolType, wordCount, UInt(0)),
|
||||
rawBits),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual, _boolType, BitwiseAnd(byteAddress, UInt(3)), UInt(0)));
|
||||
var canonical = new uint[4];
|
||||
var componentBounds = new uint[4];
|
||||
for (var component = 0; component < canonical.Length; component++)
|
||||
{
|
||||
canonical[component] = LoadGfx10BufferFormatComponent(
|
||||
bindingIndex,
|
||||
byteAddress,
|
||||
dataFormat,
|
||||
numberFormat,
|
||||
component,
|
||||
out componentBounds[component]);
|
||||
}
|
||||
|
||||
EmitConditional(
|
||||
wordPath,
|
||||
() =>
|
||||
{
|
||||
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
|
||||
var canonical = new uint[4];
|
||||
var componentBounds = new uint[4];
|
||||
for (uint component = 0; component < 4; component++)
|
||||
{
|
||||
var address = component == 0 ? dwordAddress : IAdd(dwordAddress, UInt(component));
|
||||
var present = _module.AddInstruction(
|
||||
SpirvOp.ULessThan, _boolType, UInt(component), wordCount);
|
||||
canonical[component] = _module.AddInstruction(
|
||||
SpirvOp.Select, _uintType, present,
|
||||
LoadBufferWord(bindingIndex, address),
|
||||
component == 3 ? one : UInt(0));
|
||||
componentBounds[component] = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr, _boolType,
|
||||
_module.AddInstruction(SpirvOp.LogicalNot, _boolType, present),
|
||||
IsBufferWordInRange(bindingIndex, address));
|
||||
}
|
||||
|
||||
StoreBufferFormatComponents(descriptorWord3, vectorData, componentCount, one, canonical, componentBounds);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var canonical = new uint[4];
|
||||
var componentBounds = new uint[4];
|
||||
for (var component = 0; component < canonical.Length; component++)
|
||||
{
|
||||
canonical[component] = LoadGfx10BufferFormatComponent(
|
||||
bindingIndex,
|
||||
byteAddress,
|
||||
dataFormat,
|
||||
numberFormat,
|
||||
component,
|
||||
out componentBounds[component]);
|
||||
}
|
||||
|
||||
StoreBufferFormatComponents(descriptorWord3, vectorData, componentCount, one, canonical, componentBounds);
|
||||
});
|
||||
}
|
||||
|
||||
// Applies the descriptor swizzle to the memory components; only selected memory
|
||||
// components contribute to the shared bounds check.
|
||||
private void StoreBufferFormatComponents(
|
||||
uint descriptorWord3,
|
||||
uint vectorData,
|
||||
uint componentCount,
|
||||
uint one,
|
||||
uint[] canonical,
|
||||
uint[] componentBounds)
|
||||
{
|
||||
// Only selected memory components contribute to the shared bounds check.
|
||||
var selectors = new uint[componentCount];
|
||||
var inBounds = _module.ConstantBool(true);
|
||||
for (uint destination = 0; destination < componentCount; destination++)
|
||||
|
|
@ -4121,6 +3911,7 @@ public static partial class Gen5SpirvTranslator
|
|||
inBounds = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, inBounds, selectedInBounds);
|
||||
}
|
||||
|
||||
var one = Gfx10FormatOne(numberFormat);
|
||||
for (uint destination = 0; destination < componentCount; destination++)
|
||||
{
|
||||
var selector = selectors[destination];
|
||||
|
|
@ -4794,27 +4585,23 @@ public static partial class Gen5SpirvTranslator
|
|||
whenTrue,
|
||||
whenFalse);
|
||||
|
||||
// The four bytes at a byte address span at most two words, so one funnel shift of those
|
||||
// two words is the whole value. Reading each byte from its own bounds-checked word load
|
||||
// (four loads, four range tests and a byte assembly) costs twice as much for the same
|
||||
// result: a word outside the buffer reads zero either way, so the bytes it would have
|
||||
// contributed stay zero, and the formatted-load fallback does this per component.
|
||||
private uint LoadUnalignedBufferWord(int bindingIndex, uint byteAddress)
|
||||
{
|
||||
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
|
||||
var bitOffset = ShiftLeftLogical(BitwiseAnd(byteAddress, UInt(3)), UInt(3));
|
||||
var low = LoadBufferWord(bindingIndex, dwordAddress);
|
||||
var high = LoadBufferWord(bindingIndex, IAdd(dwordAddress, UInt(1)));
|
||||
// A shift by the word width is undefined, so the aligned case keeps the low word.
|
||||
var spanning = BitwiseOr(
|
||||
ShiftRightLogical(low, bitOffset),
|
||||
ShiftLeftLogical(high, _module.AddInstruction(SpirvOp.ISub, _uintType, UInt(32), bitOffset)));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
IsNotZero(bitOffset),
|
||||
spanning,
|
||||
low);
|
||||
var result = UInt(0);
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
var address = index == 0
|
||||
? byteAddress
|
||||
: IAdd(byteAddress, UInt(index));
|
||||
var dwordAddress = ShiftRightLogical(address, UInt(2));
|
||||
var bitOffset = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3));
|
||||
var value = BitwiseAnd(
|
||||
ShiftRightLogical(LoadBufferWord(bindingIndex, dwordAddress), bitOffset),
|
||||
UInt(0xFF));
|
||||
result = BitwiseOr(result, ShiftLeftLogical(value, UInt(index * 8)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private uint LoadSubdwordBufferValue(
|
||||
|
|
@ -5206,12 +4993,6 @@ public static partial class Gen5SpirvTranslator
|
|||
_intType,
|
||||
size,
|
||||
component);
|
||||
// The game reasons about its own resolution; report the guest size.
|
||||
if (component < Math.Min(2u, ImageSpatialComponentCount(resource)))
|
||||
{
|
||||
signedValue = ScaleImageQuerySize(signedValue, resource);
|
||||
}
|
||||
|
||||
value = Bitcast(_uintType, signedValue);
|
||||
}
|
||||
else if (component == 3)
|
||||
|
|
@ -5273,7 +5054,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var coordinates = BuildIntegerCoordinates(
|
||||
resource,
|
||||
image,
|
||||
0,
|
||||
coordinateComponentCount);
|
||||
|
|
@ -5343,7 +5123,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var isMax = instruction.Opcode == "ImageAtomicFmax";
|
||||
var floatCoordinateCount = ImageCoordinateComponentCount(resource);
|
||||
var floatCoordinates = BuildIntegerCoordinates(
|
||||
resource,
|
||||
image,
|
||||
0,
|
||||
floatCoordinateCount);
|
||||
|
|
@ -5388,7 +5167,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var coordinates = BuildIntegerCoordinates(
|
||||
resource,
|
||||
image,
|
||||
0,
|
||||
coordinateComponentCount);
|
||||
|
|
@ -5438,7 +5216,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var coordinates = BuildIntegerCoordinates(
|
||||
resource,
|
||||
image,
|
||||
0,
|
||||
coordinateComponentCount);
|
||||
|
|
@ -5455,7 +5232,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var coordinates = BuildIntegerCoordinates(
|
||||
resource,
|
||||
image,
|
||||
0,
|
||||
coordinateComponentCount);
|
||||
|
|
@ -6352,22 +6128,16 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
private uint BuildIntegerCoordinates(
|
||||
in SpirvImageResource resource,
|
||||
Gen5ImageControl image,
|
||||
int start,
|
||||
uint componentCount)
|
||||
{
|
||||
// Only the two scaled axes move; an array layer or a volume slice is not a pixel.
|
||||
var scaledComponents = start == 0 ? Math.Min(2, (int)ImageSpatialComponentCount(resource)) : 0;
|
||||
var components = new uint[checked((int)componentCount)];
|
||||
for (var component = 0; component < components.Length; component++)
|
||||
{
|
||||
var value = Bitcast(
|
||||
components[component] = Bitcast(
|
||||
_intType,
|
||||
LoadImageIntegerAddress(image, start + component));
|
||||
components[component] = component < scaledComponents
|
||||
? ScaleIntegerImageCoordinate(value, resource)
|
||||
: value;
|
||||
}
|
||||
|
||||
if (componentCount == 1)
|
||||
|
|
@ -6497,30 +6267,6 @@ public static partial class Gen5SpirvTranslator
|
|||
SpirvOp.ConvertSToF,
|
||||
spatialFloatType,
|
||||
texelOffset);
|
||||
// The instruction's offset is in guest texels. Convert it to host texels
|
||||
// before normalizing by the host mip extent. Array layers and depth are
|
||||
// never scaled; the per-image mask also preserves unscaled bindings.
|
||||
if (UsesRenderScale)
|
||||
{
|
||||
var factor = LoadImageScaleFactor(resource, ImageCoordinateFactorDword);
|
||||
if (spatialComponentCount == 1)
|
||||
{
|
||||
offsetFloat = _module.AddInstruction(SpirvOp.FMul, _floatType, offsetFloat, factor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var components = new uint[checked((int)spatialComponentCount)];
|
||||
for (uint component = 0; component < spatialComponentCount; component++)
|
||||
{
|
||||
var value = _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, offsetFloat, component);
|
||||
components[component] = component < 2
|
||||
? _module.AddInstruction(SpirvOp.FMul, _floatType, value, factor)
|
||||
: value;
|
||||
}
|
||||
offsetFloat = _module.AddInstruction(SpirvOp.CompositeConstruct, spatialFloatType, components);
|
||||
}
|
||||
}
|
||||
|
||||
var normalizedOffset = _module.AddInstruction(
|
||||
SpirvOp.FDiv,
|
||||
spatialFloatType,
|
||||
|
|
@ -7652,14 +7398,6 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
private uint LoadBufferWord(int binding, uint dwordAddress)
|
||||
{
|
||||
// With the device measured to return zero past the end of a descriptor range, the
|
||||
// range test, the address clamp and the zero select only reproduce what the read
|
||||
// already does. The guest expects zero there, and so the load stands alone.
|
||||
if (_zeroOutOfBoundsBufferReads)
|
||||
{
|
||||
return Load(_uintType, BufferWordPointer(binding, dwordAddress));
|
||||
}
|
||||
|
||||
var inRange = IsBufferWordInRange(binding, dwordAddress);
|
||||
var safeAddress = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
|
|
@ -7728,7 +7466,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var slots = new List<(uint Register, uint Lane)>();
|
||||
if (_emulateWave64)
|
||||
{
|
||||
return FindPureLaneSpillSlots();
|
||||
return slots;
|
||||
}
|
||||
|
||||
var ownsLaneZero = !UsesSubgroupOperations();
|
||||
|
|
@ -7754,70 +7492,6 @@ public static partial class Gen5SpirvTranslator
|
|||
return slots;
|
||||
}
|
||||
|
||||
// Under wave64 emulation a lane read normally rendezvous with the other host subgroup through
|
||||
// LDS and a workgroup barrier. A VGPR written only by constant-lane V_WRITELANE holds spilled
|
||||
// scalars, which are wave-uniform, so every invocation can keep each written lane itself and a
|
||||
// constant-lane V_READLANE needs no exchange. V_WRITELANE still updates the VGPR, so other
|
||||
// reads of it are unaffected. Relative VGPR addressing could alias any register, so programs
|
||||
// that use it keep the exchange.
|
||||
private List<(uint Register, uint Lane)> FindPureLaneSpillSlots()
|
||||
{
|
||||
var slots = new List<(uint Register, uint Lane)>();
|
||||
var instructions = _request.Program.Instructions;
|
||||
if (instructions.Any(static instruction => instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal)))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
var candidates = new HashSet<uint>();
|
||||
var rejected = new HashSet<uint>();
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
var isWritelane = instruction.Opcode == "VWritelaneB32";
|
||||
var isReadlane = instruction.Opcode == "VReadlaneB32";
|
||||
for (var index = 0; index < instruction.Destinations.Count; index++)
|
||||
{
|
||||
var operand = instruction.Destinations[index];
|
||||
if (operand.Kind != Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isWritelane && index == 0 && TryGetConstantLane(instruction, out _))
|
||||
{
|
||||
candidates.Add(operand.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
rejected.Add(operand.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// A variable lane read cannot use a slot; it keeps reading the VGPR, which V_WRITELANE
|
||||
// still writes, so ordinary vector reads of the register stay exact as well.
|
||||
if (isReadlane && instruction.Sources.Count > 0 &&
|
||||
instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister &&
|
||||
!TryGetConstantLane(instruction, out _))
|
||||
{
|
||||
rejected.Add(instruction.Sources[0].Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (instruction.Opcode == "VWritelaneB32" &&
|
||||
TryGetVectorDestination(instruction, out var register) &&
|
||||
candidates.Contains(register) && !rejected.Contains(register) &&
|
||||
TryGetConstantLane(instruction, out var lane) &&
|
||||
!slots.Contains((register, lane)))
|
||||
{
|
||||
slots.Add((register, lane));
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
// The lane select operand (source 1) of V_READLANE/V_WRITELANE when it is a constant.
|
||||
private bool TryGetConstantLane(Gen5ShaderInstruction instruction, out uint lane)
|
||||
{
|
||||
|
|
@ -7990,27 +7664,31 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
// One variable per register, declared on first use. The packed-half shadow of the vector
|
||||
// register file is only ever addressed by a constant register number, and a 512-entry
|
||||
// private array is the shape that makes Metal keep the file in thread memory instead of
|
||||
// registers - the same trap V_MOVREL hit. It also kept an unused 4 KiB private array as a
|
||||
// named .bss global on AMD, where two stages then failed to link because the driver copied
|
||||
// the NOBITS section as file data and read past the end of the ELF.
|
||||
private uint PackedHalfPointer(uint register)
|
||||
private uint PackedHalfPointer(uint register) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateVec2Pointer,
|
||||
PackedHalfRegisters(),
|
||||
UInt(register));
|
||||
|
||||
// Declared on first use when Private. AMD's compiler keeps an unused 4 KiB private
|
||||
// array as a named .bss global: two stages then fail to link, and the driver copies
|
||||
// the NOBITS section as file data and reads past the end of the ELF.
|
||||
private uint PackedHalfRegisters()
|
||||
{
|
||||
if (_packedHalfRegisterVariables.TryGetValue(register, out var variable))
|
||||
if (_packedHalfRegisters != 0)
|
||||
{
|
||||
return variable;
|
||||
return _packedHalfRegisters;
|
||||
}
|
||||
|
||||
variable = _module.AddGlobalVariable(
|
||||
_privateVec2Pointer,
|
||||
var arrayType = _module.TypeArray(_vec2Type, VectorRegisterCount);
|
||||
_packedHalfRegisters = _module.AddGlobalVariable(
|
||||
_module.TypePointer(SpirvStorageClass.Private, arrayType),
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantNull(_vec2Type));
|
||||
_packedHalfRegisterVariables.Add(register, variable);
|
||||
_interfaces.Add(variable);
|
||||
_module.AddName(variable, $"vgprPackedHalf{register}");
|
||||
return variable;
|
||||
_module.ConstantNull(arrayType));
|
||||
_interfaces.Add(_packedHalfRegisters);
|
||||
_module.AddName(_packedHalfRegisters, "vgprPackedHalf");
|
||||
return _packedHalfRegisters;
|
||||
}
|
||||
|
||||
private uint LoadS(uint register) => Load(_uintType, ScalarPointer(register));
|
||||
|
|
@ -8042,25 +7720,6 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if (_waveLaneCount != 32)
|
||||
{
|
||||
// The lane's bit lives in one of the two mask registers, so select that register
|
||||
// and shift inside it. Composing the 64-bit mask and masking it with a 64-bit
|
||||
// lane bit costs several times as much on a host that emulates 64-bit integers,
|
||||
// and VCC and EXEC refresh their lane flag on every mask write.
|
||||
if (_subgroupInvocationIdInput != 0 && _emulateWave64)
|
||||
{
|
||||
var lane = GuestWaveLane();
|
||||
var word = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, lane, UInt(32)),
|
||||
LoadS(lowRegister),
|
||||
LoadS(lowRegister + 1));
|
||||
return IsNotZero(
|
||||
BitwiseAnd(
|
||||
ShiftRightLogical(word, BitwiseAnd(lane, UInt(31))),
|
||||
UInt(1)));
|
||||
}
|
||||
|
||||
return IsWaveMaskActive(LoadS64(lowRegister));
|
||||
}
|
||||
|
||||
|
|
@ -8346,15 +8005,6 @@ public static partial class Gen5SpirvTranslator
|
|||
? value
|
||||
: _module.AddInstruction(SpirvOp.GroupNonUniformShuffle, _uintType, UInt(3), value, lane);
|
||||
|
||||
// DPP row/quad operations and PERMLANE16 never cross a 32-lane half-wave.
|
||||
// Guest lanes 32..63 belong to the second host subgroup on 32-lane devices;
|
||||
// using those guest indices directly in OpGroupNonUniformShuffle is undefined.
|
||||
// Preserve the current physical half on devices whose subgroup is wider than 32.
|
||||
private uint ShuffleHalfWaveLane(uint value, uint guestLane) =>
|
||||
_subgroupInvocationIdInput == 0 ? value : ShuffleLane(value,
|
||||
BitwiseOr(BitwiseAnd(Load(_uintType, _subgroupInvocationIdInput), UInt(0xFFFF_FFE0)),
|
||||
BitwiseAnd(guestLane, UInt(31))));
|
||||
|
||||
private uint CurrentLaneBit()
|
||||
{
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
|
|
@ -8572,35 +8222,6 @@ public static partial class Gen5SpirvTranslator
|
|||
return _halfMaskPlan;
|
||||
}
|
||||
|
||||
// Null until a wait is seen in the current block; then true once any wait in the block
|
||||
// drained vector memory (vmcnt(0), or vscnt(0), which is what covers stores on GFX10),
|
||||
// which is how the guest publishes buffer and image stores before a barrier. It stays
|
||||
// true for the rest of the block: a later LDS-only wait does not unpublish them.
|
||||
private bool? _vectorMemoryWaited;
|
||||
|
||||
private void NoteWaitCount(Gen5ShaderInstruction instruction)
|
||||
{
|
||||
if (instruction.Words.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var word = instruction.Words[0];
|
||||
if (instruction.Encoding == Gen5ShaderEncoding.Sopk)
|
||||
{
|
||||
// SOPK waits name one counter: 0x17 vscnt, 0x18 vmcnt, 0x19 expcnt, 0x1A lgkmcnt.
|
||||
var counter = (word >> 23) & 0x1Fu;
|
||||
var drainsVectorMemory = counter is 0x17 or 0x18 && (word & 0xFFFFu) == 0;
|
||||
_vectorMemoryWaited = _vectorMemoryWaited == true || drainsVectorMemory;
|
||||
return;
|
||||
}
|
||||
|
||||
// SOPP S_WAITCNT packs vmcnt across bits [3:0] and [15:14].
|
||||
var immediate = word & 0xFFFFu;
|
||||
var vectorCount = (immediate & 0xFu) | (((immediate >> 14) & 0x3u) << 4);
|
||||
_vectorMemoryWaited = _vectorMemoryWaited == true || vectorCount == 0;
|
||||
}
|
||||
|
||||
private void EmitWave64Barrier()
|
||||
{
|
||||
var workgroup = UInt(2);
|
||||
|
|
|
|||
|
|
@ -420,137 +420,6 @@ public static class SpirvFixedShaders
|
|||
return module.Build();
|
||||
}
|
||||
|
||||
// The f16 conversion probe: for input[i] at binding 0, write two dwords at binding 1,
|
||||
// output[2i] = the f32 bits of GLSL UnpackHalf2x16(input[i] & 0xFFFF).x and
|
||||
// output[2i + 1] = GLSL PackHalf2x16(vec2(asfloat(input[i]), 0)) & 0xFFFF. The host
|
||||
// compares both against the integer sequences the translator emits by default, and
|
||||
// only then lets the translator use these two ext instructions. 64 threads per group,
|
||||
// one per value; the input is padded to a multiple of 64 so no thread is out of range.
|
||||
// Copies binding 0 to binding 1 with no bounds check at all, so a read past the descriptor
|
||||
// range reaches the device and the caller can see what it returns.
|
||||
public static byte[] CreateUncheckedBufferCopyProbe()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var uintType = module.TypeInt(32, signed: false);
|
||||
var uvec3Type = module.TypeVector(uintType, 3);
|
||||
var runtimeArray = module.TypeRuntimeArray(uintType);
|
||||
module.AddDecoration(runtimeArray, SpirvDecoration.ArrayStride, 4);
|
||||
var bufferStruct = module.TypeStruct(runtimeArray);
|
||||
module.AddDecoration(bufferStruct, SpirvDecoration.Block);
|
||||
module.AddMemberDecoration(bufferStruct, 0, SpirvDecoration.Offset, 0);
|
||||
var bufferPtrType = module.TypePointer(SpirvStorageClass.StorageBuffer, bufferStruct);
|
||||
var uintStoragePtr = module.TypePointer(SpirvStorageClass.StorageBuffer, uintType);
|
||||
|
||||
uint MakeBuffer(uint binding, string name)
|
||||
{
|
||||
var variable = module.AddGlobalVariable(bufferPtrType, SpirvStorageClass.StorageBuffer);
|
||||
module.AddName(variable, name);
|
||||
module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0);
|
||||
module.AddDecoration(variable, SpirvDecoration.Binding, binding);
|
||||
return variable;
|
||||
}
|
||||
|
||||
var inputVar = MakeBuffer(0, "probe_bounded_input");
|
||||
var outputVar = MakeBuffer(1, "probe_output");
|
||||
var inputUvec3Ptr = module.TypePointer(SpirvStorageClass.Input, uvec3Type);
|
||||
var gidVar = module.AddGlobalVariable(inputUvec3Ptr, SpirvStorageClass.Input);
|
||||
module.AddName(gidVar, "gid");
|
||||
module.AddDecoration(gidVar, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.GlobalInvocationId);
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
var gid = module.AddInstruction(SpirvOp.Load, uvec3Type, gidVar);
|
||||
var id = module.AddInstruction(SpirvOp.CompositeExtract, uintType, gid, 0);
|
||||
var sourcePtr = module.AddInstruction(
|
||||
SpirvOp.AccessChain, uintStoragePtr, inputVar, module.Constant(uintType, 0), id);
|
||||
var value = module.AddInstruction(SpirvOp.Load, uintType, sourcePtr);
|
||||
var destinationPtr = module.AddInstruction(
|
||||
SpirvOp.AccessChain, uintStoragePtr, outputVar, module.Constant(uintType, 0), id);
|
||||
module.AddStatement(SpirvOp.Store, destinationPtr, value);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.LocalSize, 64, 1, 1);
|
||||
module.AddEntryPoint(SpirvExecutionModel.GLCompute, main, "main", [gidVar, inputVar, outputVar]);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateHalfConversionProbe()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
var glsl = module.ImportExtInst("GLSL.std.450");
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var uintType = module.TypeInt(32, signed: false);
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec2Type = module.TypeVector(floatType, 2);
|
||||
var uvec3Type = module.TypeVector(uintType, 3);
|
||||
var runtimeArray = module.TypeRuntimeArray(uintType);
|
||||
module.AddDecoration(runtimeArray, SpirvDecoration.ArrayStride, 4);
|
||||
var bufferStruct = module.TypeStruct(runtimeArray);
|
||||
module.AddDecoration(bufferStruct, SpirvDecoration.Block);
|
||||
module.AddMemberDecoration(bufferStruct, 0, SpirvDecoration.Offset, 0);
|
||||
var bufferPtrType = module.TypePointer(SpirvStorageClass.StorageBuffer, bufferStruct);
|
||||
var uintStoragePtr = module.TypePointer(SpirvStorageClass.StorageBuffer, uintType);
|
||||
|
||||
uint MakeBuffer(uint binding, string name)
|
||||
{
|
||||
var variable = module.AddGlobalVariable(bufferPtrType, SpirvStorageClass.StorageBuffer);
|
||||
module.AddName(variable, name);
|
||||
module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0);
|
||||
module.AddDecoration(variable, SpirvDecoration.Binding, binding);
|
||||
return variable;
|
||||
}
|
||||
|
||||
var inputVar = MakeBuffer(0, "probe_input");
|
||||
var outputVar = MakeBuffer(1, "probe_output");
|
||||
var inputUvec3Ptr = module.TypePointer(SpirvStorageClass.Input, uvec3Type);
|
||||
var gidVar = module.AddGlobalVariable(inputUvec3Ptr, SpirvStorageClass.Input);
|
||||
module.AddName(gidVar, "gid");
|
||||
module.AddDecoration(gidVar, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.GlobalInvocationId);
|
||||
|
||||
uint UInt(uint value) => module.Constant(uintType, value);
|
||||
var zeroFloat = module.ConstantFloat(floatType, 0f);
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
var gid = module.AddInstruction(SpirvOp.Load, uvec3Type, gidVar);
|
||||
var id = module.AddInstruction(SpirvOp.CompositeExtract, uintType, gid, 0);
|
||||
var valuePtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, inputVar, UInt(0), id);
|
||||
var value = module.AddInstruction(SpirvOp.Load, uintType, valuePtr);
|
||||
|
||||
var halfBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, value, UInt(0xFFFF));
|
||||
var unpacked = module.AddInstruction(SpirvOp.ExtInst, vec2Type, glsl, 62, halfBits);
|
||||
var widened = module.AddInstruction(SpirvOp.CompositeExtract, floatType, unpacked, 0);
|
||||
var widenedBits = module.AddInstruction(SpirvOp.Bitcast, uintType, widened);
|
||||
|
||||
var asFloat = module.AddInstruction(SpirvOp.Bitcast, floatType, value);
|
||||
var pair = module.AddInstruction(SpirvOp.CompositeConstruct, vec2Type, asFloat, zeroFloat);
|
||||
var packed = module.AddInstruction(SpirvOp.ExtInst, uintType, glsl, 58, pair);
|
||||
var narrowed = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, packed, UInt(0xFFFF));
|
||||
|
||||
var lowIndex = module.AddInstruction(SpirvOp.IMul, uintType, id, UInt(2));
|
||||
var highIndex = module.AddInstruction(SpirvOp.IAdd, uintType, lowIndex, UInt(1));
|
||||
var lowPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, outputVar, UInt(0), lowIndex);
|
||||
module.AddStatement(SpirvOp.Store, lowPtr, widenedBits);
|
||||
var highPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, outputVar, UInt(0), highIndex);
|
||||
module.AddStatement(SpirvOp.Store, highPtr, narrowed);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.LocalSize, 64, 1, 1);
|
||||
module.AddEntryPoint(SpirvExecutionModel.GLCompute, main, "main", [gidVar, inputVar, outputVar]);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
// Read and clear binding 0. Write the count and 64-bit page addresses to binding 1.
|
||||
// Use 64 threads per group, with one thread per 32-page word.
|
||||
public static byte[] CreateFaultBufferProcess()
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Ir;
|
||||
|
||||
// Which instructions may share one EXEC test. A vector register write is modelled as
|
||||
// select(EXEC, result, old value), so the old value of every register stays live across every
|
||||
// write. A run of instructions whose only effect is their vector destination can instead run
|
||||
// inside one if (EXEC) - the masked-off lanes skip the whole run, which is what the hardware does
|
||||
// to them anyway, and only the join needs the old values.
|
||||
//
|
||||
// A run must stop at anything the wave runs even where this lane is masked off. SALU is not EXEC
|
||||
// masked on the hardware and every invocation keeps its own copy of the scalar registers, so a
|
||||
// lane that skipped a scalar write would carry a different value out of the region than its
|
||||
// neighbours. The same goes for a compare that writes VCC or EXEC (GFX10 keeps that destination in
|
||||
// the instruction's control, not in its destination list), a cross-lane read, LDS, memory, and the
|
||||
// DPP encodings, which read a neighbour's register.
|
||||
public static class Gen5ExecRunAnalysis
|
||||
{
|
||||
public static bool IsExecMaskedVectorAlu(Gen5ShaderInstruction instruction)
|
||||
{
|
||||
var opcode = instruction.Opcode;
|
||||
if (opcode.Length == 0 || opcode[0] != 'V' ||
|
||||
// V_CMP writes a mask register, V_CMPX writes EXEC.
|
||||
opcode.StartsWith("VCmp", StringComparison.Ordinal) ||
|
||||
// Relative addressing can land on any register.
|
||||
opcode.StartsWith("VMovrel", StringComparison.Ordinal) ||
|
||||
// Cross-lane reads need the other lanes to have run.
|
||||
opcode.Contains("Readlane", StringComparison.Ordinal) ||
|
||||
opcode.Contains("Writelane", StringComparison.Ordinal) ||
|
||||
opcode.Contains("Readfirstlane", StringComparison.Ordinal) ||
|
||||
opcode.Contains("Permlane", StringComparison.Ordinal) ||
|
||||
opcode.Contains("Swap", StringComparison.Ordinal) ||
|
||||
// The carry-in/carry-out forms write VCC with no destination operand for it.
|
||||
opcode.Contains("Co", StringComparison.Ordinal) ||
|
||||
opcode.Contains("DivScale", StringComparison.Ordinal) ||
|
||||
opcode.Contains("DivFmas", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Destinations.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var destination in instruction.Destinations)
|
||||
{
|
||||
if (destination.Kind != Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return instruction.Control switch
|
||||
{
|
||||
null => true,
|
||||
Gen5Vop3Control control => control.ScalarDestination is null,
|
||||
Gen5SdwaControl control => control.ScalarDestination is null,
|
||||
Gen5Vop3pControl => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -337,21 +337,12 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
public uint MemoryOffsetDword { get; init; }
|
||||
public uint MemoryOffsetCount { get; init; }
|
||||
public bool UsesDispatchThreadLimits { get; init; }
|
||||
|
||||
// The module reads the internal-resolution dwords: the factor that maps a guest pixel
|
||||
// onto host texels, its reciprocal, the mask of scaled image resources and the factor
|
||||
// the pixel position is divided by.
|
||||
public bool UsesRenderScale { get; init; }
|
||||
public IReadOnlyList<uint> UserDataRegisters { get; init; } = [];
|
||||
public IReadOnlyList<DescriptorBinding> Descriptors { get; init; } = [];
|
||||
|
||||
public const uint RenderScaleDwordCount = 5;
|
||||
|
||||
public uint DispatchThreadLimitsDword => MemoryOffsetDword + (MemoryOffsetCount + 3) / 4;
|
||||
|
||||
public uint RenderScaleDword => DispatchThreadLimitsDword + (UsesDispatchThreadLimits ? 3u : 0u);
|
||||
|
||||
public uint ShaderDataDwordCount => RenderScaleDword + (UsesRenderScale ? RenderScaleDwordCount : 0u);
|
||||
public uint ShaderDataDwordCount => DispatchThreadLimitsDword + (UsesDispatchThreadLimits ? 3u : 0u);
|
||||
|
||||
public bool UsesPushData => PushDataStartDword != PushData.NoStart;
|
||||
|
||||
|
|
@ -570,14 +561,12 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
bool usesFlattenedTable,
|
||||
bool usesShaderBase,
|
||||
uint pushDataStartDword = 0,
|
||||
bool usesDispatchThreadLimits = false,
|
||||
bool usesRenderScale = false)
|
||||
bool usesDispatchThreadLimits = false)
|
||||
{
|
||||
var shaderBaseDword = usesShaderBase ? (uint)userDataRegisters.Count : NoShaderBase;
|
||||
var memoryOffsetDword = (uint)userDataRegisters.Count + (usesShaderBase ? ShaderBaseDwordCount : 0);
|
||||
var memoryOffsetCount = (uint)info.Buffers.Count;
|
||||
var shaderDataDwords = memoryOffsetDword + (memoryOffsetCount + 3) / 4 + (usesDispatchThreadLimits ? 3u : 0u) +
|
||||
(usesRenderScale ? RenderScaleDwordCount : 0u);
|
||||
var shaderDataDwords = memoryOffsetDword + (memoryOffsetCount + 3) / 4 + (usesDispatchThreadLimits ? 3u : 0u);
|
||||
var pushStart = PushData.StartFor(pushDataStartDword, shaderDataDwords);
|
||||
var descriptors = new List<DescriptorBinding>();
|
||||
if (info.Buffers.Count != 0)
|
||||
|
|
@ -655,7 +644,6 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
MemoryOffsetDword = memoryOffsetDword,
|
||||
MemoryOffsetCount = memoryOffsetCount,
|
||||
UsesDispatchThreadLimits = usesDispatchThreadLimits,
|
||||
UsesRenderScale = usesRenderScale,
|
||||
UserDataRegisters = userDataRegisters,
|
||||
Descriptors = descriptors,
|
||||
};
|
||||
|
|
@ -669,14 +657,13 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
MemoryOffsetDword == other.MemoryOffsetDword &&
|
||||
MemoryOffsetCount == other.MemoryOffsetCount &&
|
||||
UsesDispatchThreadLimits == other.UsesDispatchThreadLimits &&
|
||||
UsesRenderScale == other.UsesRenderScale &&
|
||||
UserDataRegisters.SequenceEqual(other.UserDataRegisters) &&
|
||||
Descriptors.Count == other.Descriptors.Count &&
|
||||
Descriptors.Zip(other.Descriptors).All(pair => pair.First.Kind == pair.Second.Kind && pair.First.Resources.SequenceEqual(pair.Second.Resources));
|
||||
|
||||
public override bool Equals(object? obj) => Equals(obj as BindingLayout);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(PushDataStartDword, MemoryOffsetDword, MemoryOffsetCount, Descriptors.Count, UsesDispatchThreadLimits, UsesRenderScale);
|
||||
public override int GetHashCode() => HashCode.Combine(PushDataStartDword, MemoryOffsetDword, MemoryOffsetCount, Descriptors.Count, UsesDispatchThreadLimits);
|
||||
}
|
||||
|
||||
// Recomputes the layout an emitter was given from the same inputs and the same push
|
||||
|
|
@ -698,7 +685,7 @@ public static class BindingLayoutValidator
|
|||
throw new ResourcePlanException("Only a compute shader can use dispatch thread limits.");
|
||||
}
|
||||
|
||||
var expected = BindingLayout.Allocate(info, userDataRegisters, usesGlobalDataShare, usesFlattenedTable, usesShaderBase, layout.AllocationCursor, layout.UsesDispatchThreadLimits, layout.UsesRenderScale);
|
||||
var expected = BindingLayout.Allocate(info, userDataRegisters, usesGlobalDataShare, usesFlattenedTable, usesShaderBase, layout.AllocationCursor, layout.UsesDispatchThreadLimits);
|
||||
if (!expected.Equals(layout))
|
||||
{
|
||||
throw new ResourcePlanException(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ public sealed class ResourceMaterializationCache
|
|||
private static long _totalStale;
|
||||
private static long _totalStaleUnreadable;
|
||||
private static long _totalRefreshes;
|
||||
private static long _totalBypassed;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _readingTable;
|
||||
|
|
@ -48,11 +47,6 @@ public sealed class ResourceMaterializationCache
|
|||
private set => _readingTable = value;
|
||||
}
|
||||
|
||||
private const int ProbationLookups = 64;
|
||||
// A plan keeps using the cache while at least one lookup in this many hits.
|
||||
private const int MinimumHitShare = 8;
|
||||
|
||||
public long Bypassed { get; private set; }
|
||||
public long Hits { get; private set; }
|
||||
public long Misses { get; private set; }
|
||||
public long Uncacheable { get; private set; }
|
||||
|
|
@ -67,10 +61,9 @@ public sealed class ResourceMaterializationCache
|
|||
var stale = Interlocked.Exchange(ref _totalStale, 0);
|
||||
var staleUnreadable = Interlocked.Exchange(ref _totalStaleUnreadable, 0);
|
||||
var refreshes = Interlocked.Exchange(ref _totalRefreshes, 0);
|
||||
var bypassed = Interlocked.Exchange(ref _totalBypassed, 0);
|
||||
var total = hits + misses;
|
||||
return FormattableString.Invariant(
|
||||
$"[PERF][RESOURCE_CACHE] hits={hits} misses={misses} stale={stale} stale_unreadable={staleUnreadable} refreshes={refreshes} bypassed={bypassed} uncacheable={uncacheable} hit_rate={(total == 0 ? 0 : hits * 100.0 / total):F1}% {RawReadPrefetch.TakeReport()}");
|
||||
$"[PERF][RESOURCE_CACHE] hits={hits} misses={misses} stale={stale} stale_unreadable={staleUnreadable} refreshes={refreshes} uncacheable={uncacheable} hit_rate={(total == 0 ? 0 : hits * 100.0 / total):F1}% {RawReadPrefetch.TakeReport()}");
|
||||
}
|
||||
|
||||
public bool Materialize(
|
||||
|
|
@ -81,17 +74,6 @@ public sealed class ResourceMaterializationCache
|
|||
ref ResourceSpecialization specialization,
|
||||
out ResourceMaterializationFailure failure)
|
||||
{
|
||||
// Most draws of a plan whose user data points at per-draw constants never repeat their
|
||||
// inputs; recording their reads and storing entries then costs more than a plain walk.
|
||||
// Such a plan materializes directly and probes the cache again on every 64th draw.
|
||||
var lookups = ++plan.CacheLookups;
|
||||
if (lookups > ProbationLookups && plan.CacheHits * MinimumHitShare < lookups && (lookups & 63) != 0)
|
||||
{
|
||||
Bypassed++;
|
||||
Interlocked.Increment(ref _totalBypassed);
|
||||
return ResourceMaterializer.Materialize(plan, inputs, ref snapshot, ref specialization, out failure);
|
||||
}
|
||||
|
||||
var key = KeyOf(plan, inputs);
|
||||
var found = TryFind(key, plan, inputs, out var cached);
|
||||
if (found)
|
||||
|
|
@ -112,7 +94,6 @@ public sealed class ResourceMaterializationCache
|
|||
}
|
||||
|
||||
Hits++;
|
||||
plan.CacheHits++;
|
||||
Interlocked.Increment(ref _totalHits);
|
||||
snapshot = variant.Snapshot;
|
||||
specialization = variant.Specialization;
|
||||
|
|
@ -126,7 +107,6 @@ public sealed class ResourceMaterializationCache
|
|||
if (TryRefreshTable(key, cached, plan, inputs, residentReader, out var refreshed))
|
||||
{
|
||||
TableRefreshes++;
|
||||
plan.CacheHits++;
|
||||
Interlocked.Increment(ref _totalRefreshes);
|
||||
snapshot = refreshed.Snapshot;
|
||||
specialization = refreshed.Specialization;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,14 @@ internal static class ScalarGraphDiskCache
|
|||
Directory.CreateDirectory(directory);
|
||||
// Keep a bounded cache across compiler versions too. No live
|
||||
// analysis objects or guest memory are retained by the cache.
|
||||
EvictOldest(directory, stream.Length + 32);
|
||||
var entries = new DirectoryInfo(directory).GetFiles("*.graph").OrderBy(f => f.LastWriteTimeUtc).ToArray();
|
||||
var total = entries.Sum(f => f.Length) + stream.Length + 32;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (total <= MaxCacheBytes) break;
|
||||
total -= entry.Length;
|
||||
entry.Delete();
|
||||
}
|
||||
var temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
try
|
||||
{
|
||||
|
|
@ -72,8 +79,6 @@ internal static class ScalarGraphDiskCache
|
|||
file.Write(bytes);
|
||||
}
|
||||
File.Move(temporary, path, true);
|
||||
_entries!.Enqueue((path, stream.Length + 32));
|
||||
_totalBytes += stream.Length + 32;
|
||||
}
|
||||
finally { File.Delete(temporary); }
|
||||
}
|
||||
|
|
@ -84,30 +89,6 @@ internal static class ScalarGraphDiskCache
|
|||
return graph;
|
||||
}
|
||||
|
||||
// The entries of the cache directory, oldest first, and their total size. The directory is
|
||||
// listed once per process: a new build misses on every shader, and listing it per miss cost
|
||||
// more than the analysis it saved. Caller holds WriteLock.
|
||||
private static string? _entriesDirectory;
|
||||
private static Queue<(string Path, long Bytes)>? _entries;
|
||||
private static long _totalBytes;
|
||||
|
||||
private static void EvictOldest(string directory, long incoming)
|
||||
{
|
||||
if (_entries is null || _entriesDirectory != directory)
|
||||
{
|
||||
var files = new DirectoryInfo(directory).GetFiles("*.graph").OrderBy(f => f.LastWriteTimeUtc).ToArray();
|
||||
_entries = new Queue<(string Path, long Bytes)>(files.Select(f => (f.FullName, f.Length)));
|
||||
_totalBytes = files.Sum(f => f.Length);
|
||||
_entriesDirectory = directory;
|
||||
}
|
||||
|
||||
while (_totalBytes + incoming > MaxCacheBytes && _entries.TryDequeue(out var entry))
|
||||
{
|
||||
_totalBytes -= entry.Bytes;
|
||||
File.Delete(entry.Path);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Key(Gen5ShaderProgram program, uint userDataBase, uint userDataCount,
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads, uint waveSize)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ public sealed class ShaderResourcePlan
|
|||
public ScalarValueGraph Graph { get; }
|
||||
public ShaderStage Stage { get; }
|
||||
public ulong Hash { get; }
|
||||
|
||||
// How often ResourceMaterializationCache looked this plan up and found a reusable entry.
|
||||
internal int CacheLookups;
|
||||
internal int CacheHits;
|
||||
public uint UserDataBase => Graph.UserDataBase;
|
||||
public uint UserDataCount => Graph.UserDataCount;
|
||||
public MemoryAccessTable Memory => Graph.Memory;
|
||||
|
|
|
|||
|
|
@ -171,22 +171,6 @@ public sealed class ShaderCompileRequest
|
|||
// 64-bit atomics are emitted as real 64-bit atomics instead of a pair of
|
||||
// 32-bit ones, which is not atomic as a pair.
|
||||
public bool SupportsSharedInt64Atomics { get; init; }
|
||||
|
||||
// True only when the host measured GLSL UnpackHalf2x16 / PackHalf2x16 to be bit-exact
|
||||
// against the emulator's own f16 conversion on this device, for every half bit pattern
|
||||
// and every rounding boundary. The packed-f16 paths then convert with those two ext
|
||||
// instructions instead of the branchless integer sequences, which is what a driver
|
||||
// lowers to one hardware convert. Default false: the spec leaves f16 rounding and
|
||||
// subnormal behaviour to the implementation, so the exact emulation is the only safe
|
||||
// default and native is opted into by a device probe, never by a capability bit.
|
||||
public bool NativeHalfConversionExact { get; init; }
|
||||
|
||||
// True only when the host measured that a storage-buffer read past the end of its descriptor
|
||||
// range reads zero on this device (robustBufferAccess2 that the device actually honours).
|
||||
// Every guest buffer word is then loaded directly: the range test, the address clamp and the
|
||||
// zero select around it exist only to produce the zero the device already returns. Default
|
||||
// false, because an out-of-range read is undefined without that promise.
|
||||
public bool ZeroOutOfBoundsBufferReads { get; init; }
|
||||
public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; }
|
||||
|
||||
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Direct draws carry a modifier naming the shader stage and the user registers of the pipeline.
|
||||
// Every modifier must still produce the draw: Unreal's volume passes use a geometry-stage modifier.
|
||||
public sealed class AgcDirectDrawModifierTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_1000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x80;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
|
||||
public static TheoryData<ulong> Modifiers => new() { 0x4000_0000UL, 0x6000_0000UL, 0xA000_0000UL, 0x4008_0205UL, 0x1_6000_0000UL };
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Modifiers))]
|
||||
public void DrawIndexAutoEmitsTheDrawForEveryModifier(ulong modifier)
|
||||
{
|
||||
var memory = CreateCommandBuffer();
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
context[CpuRegister.Rsi] = 6;
|
||||
context[CpuRegister.Rdx] = modifier;
|
||||
|
||||
AgcExports.DcbDrawIndexAuto(context);
|
||||
|
||||
Assert.Equal(CommandAddress, context[CpuRegister.Rax]);
|
||||
var runner = new StreamRunner();
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 7)));
|
||||
var draw = Assert.Single(runner.Host.AutoDraws);
|
||||
Assert.Equal(6u, draw.VertexCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Modifiers))]
|
||||
public void DrawIndexEmitsTheDrawForEveryModifier(ulong modifier)
|
||||
{
|
||||
var memory = CreateCommandBuffer();
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
context[CpuRegister.Rsi] = 36;
|
||||
context[CpuRegister.Rdx] = StreamRunner.DataAddress;
|
||||
context[CpuRegister.Rcx] = modifier;
|
||||
|
||||
AgcExports.DcbDrawIndex(context);
|
||||
|
||||
Assert.NotEqual(0UL, context[CpuRegister.Rax]);
|
||||
var runner = new StreamRunner();
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 11)));
|
||||
var draw = Assert.Single(runner.Host.IndexedDraws);
|
||||
Assert.Equal(36u, draw.IndexCount);
|
||||
Assert.Equal(StreamRunner.DataAddress, draw.IndexAddress);
|
||||
}
|
||||
|
||||
private static FakeCpuMemory CreateCommandBuffer()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, CommandAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, CommandAddress + 0x100);
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static uint[] ReadDwords(FakeCpuMemory memory, ulong address, int count)
|
||||
{
|
||||
var bytes = new byte[count * sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
var dwords = new uint[count];
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
dwords[index] = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)));
|
||||
}
|
||||
|
||||
return dwords;
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcEventWriteTests
|
||||
{
|
||||
[Fact]
|
||||
public void DrawCounterDumpsPublishBothHalvesOfQueryResults()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
var begin = Build(0x39, StreamRunner.LabelAddress);
|
||||
var end = Build(0x39, StreamRunner.LabelAddress + 8);
|
||||
|
||||
Assert.Equal(0x139u, begin[1]);
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(begin, end));
|
||||
for (var block = 0u; block < 16; block++)
|
||||
{
|
||||
var address = StreamRunner.LabelAddress + block * 16;
|
||||
var first = runner.Host.ReadQword(address);
|
||||
var last = runner.Host.ReadQword(address + 8);
|
||||
Assert.NotEqual(0UL, first & last & (1UL << 63));
|
||||
Assert.True(last > first);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x38u, 0x1234567800UL, 4)]
|
||||
[InlineData(0x39u, 0x1234567800UL, 4)]
|
||||
[InlineData(0x3Au, 0UL, 2)]
|
||||
[InlineData(0x10u, 0UL, 2)]
|
||||
public void DrawEventsPreservePayloadAndAdvanceCursor(uint eventType, ulong payload, int words)
|
||||
{
|
||||
var packet = Build(eventType, payload);
|
||||
Assert.Equal(words, packet.Length);
|
||||
Assert.Equal(eventType | (words == 4 ? 0x100u : 0), packet[1]);
|
||||
if (words == 4)
|
||||
{
|
||||
Assert.Equal((uint)payload, packet[2]);
|
||||
Assert.Equal((uint)(payload >> 32), packet[3]);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Build(uint eventType, ulong payload)
|
||||
{
|
||||
const ulong memoryAddress = 0x2_1000_0000;
|
||||
const ulong commandBuffer = memoryAddress + 0x80;
|
||||
const ulong packetAddress = memoryAddress + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryAddress, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, packetAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], packetAddress + 0x100);
|
||||
Assert.True(memory.TryWrite(commandBuffer + 0x10, bytes));
|
||||
context[CpuRegister.Rdi] = commandBuffer;
|
||||
context[CpuRegister.Rsi] = eventType;
|
||||
context[CpuRegister.Rdx] = payload;
|
||||
|
||||
AgcExports.DcbEventWrite(context);
|
||||
Assert.Equal(packetAddress, context[CpuRegister.Rax]);
|
||||
Assert.True(memory.TryRead(commandBuffer + 0x10, bytes[..8]));
|
||||
var length = checked((int)(BinaryPrimitives.ReadUInt64LittleEndian(bytes) - packetAddress));
|
||||
Assert.InRange(length, 8, 16);
|
||||
Assert.True(memory.TryRead(packetAddress, bytes[..length]));
|
||||
var packet = new uint[length / 4];
|
||||
for (var i = 0; i < packet.Length; i++)
|
||||
packet[i] = BinaryPrimitives.ReadUInt32LittleEndian(bytes[(i * 4)..]);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue