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:
kuba 2026-10-07 12:15:45 +02:00 committed by GitHub
parent 775141d58c
commit e1edda7772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
141 changed files with 691 additions and 6756 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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);
}

View File

@ -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));

View File

@ -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.

View File

@ -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

View File

@ -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");

View File

@ -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": "معدل التحديث",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "リフレッシュレート",

View File

@ -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": "새로 고침 빈도",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Частота обновления",

View File

@ -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ı",

View File

@ -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,

View File

@ -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}}"

View File

@ -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()}");

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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}");
}
}
}

View File

@ -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);
}

View File

@ -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),

View File

@ -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,
}

View File

@ -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.

View File

@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -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))
{

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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(

View File

@ -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);

View File

@ -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

View File

@ -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)
{

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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];

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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.

View File

@ -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

View File

@ -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)
{

View File

@ -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
{

View File

@ -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(

View File

@ -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));
}

View File

@ -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.

View File

@ -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)

View File

@ -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}");
}
}
}

View File

@ -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}");
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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.");

View File

@ -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)

View File

@ -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;
}

View File

@ -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.

View File

@ -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)

View File

@ -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);

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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()

View File

@ -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,
};
}
}

View File

@ -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(

View File

@ -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;

View File

@ -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)
{

View File

@ -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;

View File

@ -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; } = [];

View File

@ -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));
}
}

View File

@ -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