diff --git a/docs/rendering-resolution.md b/docs/rendering-resolution.md deleted file mode 100644 index 6a41d18..0000000 --- a/docs/rendering-resolution.md +++ /dev/null @@ -1,112 +0,0 @@ - - -# 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. diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index a2daec2..197e9f3 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -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=] [--console=] [--console-language=] [--log-level=] [--log-file[=]] [--window-mode=] [--resolution=] [--guest-resolution=<1920x1080|3840x2160>] [--render-scale=<0.5|0.75|1|1.5|2>] [--display=] [--refresh-rate=] [--scaling=] [--vsync=] [--hdr=] [--overlay=] [--overlay-mode=] [--overlay-corner=] [--debug-server[=host:port]] "); + Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=] [--console=] [--console-language=] [--log-level=] [--log-file[=]] [--window-mode=] [--resolution=] [--display=] [--refresh-rate=] [--scaling=] [--vsync=] [--hdr=] [--overlay=] [--overlay-mode=] [--overlay-corner=] [--debug-server[=host:port]] "); 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, diff --git a/src/SharpEmu.CLI/RosettaGcBudget.cs b/src/SharpEmu.CLI/RosettaGcBudget.cs deleted file mode 100644 index 6ad3af2..0000000 --- a/src/SharpEmu.CLI/RosettaGcBudget.cs +++ /dev/null @@ -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; - -/// -/// 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. -/// -/// -/// 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. -/// -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(); - 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 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); -} diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.FastPath.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.FastPath.cs index feefaf8..433464e 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.FastPath.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.FastPath.cs @@ -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; - /// /// memcpy/memmove(dst, src, n) as a forward rep movsb, 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 : 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. /// internal static List EmitGuestMemoryCopyStub(nint fallbackTrampoline) { - var code = new List(48); - var slowPathFixups = new List(); + var code = new List(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)); diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 65edb06..3c36b57 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -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. diff --git a/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs b/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs index 8005aeb..99726f2 100644 --- a/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs +++ b/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs @@ -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 diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs index af2068f..1e48bba 100644 --- a/src/SharpEmu.GUI/GuiSettings.cs +++ b/src/SharpEmu.GUI/GuiSettings.cs @@ -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"; + /// Internal render resolution scale (1.0 = native, 0.5 = half). + public double RenderResolutionScale { get; set; } = 1.0; + /// /// 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"); diff --git a/src/SharpEmu.GUI/Languages/ar.json b/src/SharpEmu.GUI/Languages/ar.json index 7484ea7..7c7c318 100644 --- a/src/SharpEmu.GUI/Languages/ar.json +++ b/src/SharpEmu.GUI/Languages/ar.json @@ -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": "معدل التحديث", diff --git a/src/SharpEmu.GUI/Languages/az.json b/src/SharpEmu.GUI/Languages/az.json index 00900ff..8a418d1 100644 --- a/src/SharpEmu.GUI/Languages/az.json +++ b/src/SharpEmu.GUI/Languages/az.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/br.json b/src/SharpEmu.GUI/Languages/br.json index 6c20521..964ebbf 100644 --- a/src/SharpEmu.GUI/Languages/br.json +++ b/src/SharpEmu.GUI/Languages/br.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/de.json b/src/SharpEmu.GUI/Languages/de.json index e9e16aa..e4b6b76 100644 --- a/src/SharpEmu.GUI/Languages/de.json +++ b/src/SharpEmu.GUI/Languages/de.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/dk.json b/src/SharpEmu.GUI/Languages/dk.json index b36393f..b10d989 100644 --- a/src/SharpEmu.GUI/Languages/dk.json +++ b/src/SharpEmu.GUI/Languages/dk.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/ee.json b/src/SharpEmu.GUI/Languages/ee.json index fda62d0..c6cef57 100644 --- a/src/SharpEmu.GUI/Languages/ee.json +++ b/src/SharpEmu.GUI/Languages/ee.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json index 4740c3a..cedc742 100644 --- a/src/SharpEmu.GUI/Languages/en.json +++ b/src/SharpEmu.GUI/Languages/en.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/es.json b/src/SharpEmu.GUI/Languages/es.json index 20752ce..ed8abdb 100644 --- a/src/SharpEmu.GUI/Languages/es.json +++ b/src/SharpEmu.GUI/Languages/es.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/fr.json b/src/SharpEmu.GUI/Languages/fr.json index 7beaef1..0c425e2 100644 --- a/src/SharpEmu.GUI/Languages/fr.json +++ b/src/SharpEmu.GUI/Languages/fr.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/hu.json b/src/SharpEmu.GUI/Languages/hu.json index 68d45a5..af24a81 100644 --- a/src/SharpEmu.GUI/Languages/hu.json +++ b/src/SharpEmu.GUI/Languages/hu.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/it.json b/src/SharpEmu.GUI/Languages/it.json index 0822364..97a6885 100644 --- a/src/SharpEmu.GUI/Languages/it.json +++ b/src/SharpEmu.GUI/Languages/it.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/ja.json b/src/SharpEmu.GUI/Languages/ja.json index 56429b8..697da9d 100644 --- a/src/SharpEmu.GUI/Languages/ja.json +++ b/src/SharpEmu.GUI/Languages/ja.json @@ -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": "リフレッシュレート", diff --git a/src/SharpEmu.GUI/Languages/ko.json b/src/SharpEmu.GUI/Languages/ko.json index a49aee2..443677a 100644 --- a/src/SharpEmu.GUI/Languages/ko.json +++ b/src/SharpEmu.GUI/Languages/ko.json @@ -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": "새로 고침 빈도", diff --git a/src/SharpEmu.GUI/Languages/nl.json b/src/SharpEmu.GUI/Languages/nl.json index a133e84..0c83373 100644 --- a/src/SharpEmu.GUI/Languages/nl.json +++ b/src/SharpEmu.GUI/Languages/nl.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/pl.json b/src/SharpEmu.GUI/Languages/pl.json index 75f882f..f15bb64 100644 --- a/src/SharpEmu.GUI/Languages/pl.json +++ b/src/SharpEmu.GUI/Languages/pl.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/pt.json b/src/SharpEmu.GUI/Languages/pt.json index bca186e..27f1d15 100644 --- a/src/SharpEmu.GUI/Languages/pt.json +++ b/src/SharpEmu.GUI/Languages/pt.json @@ -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", diff --git a/src/SharpEmu.GUI/Languages/ru.json b/src/SharpEmu.GUI/Languages/ru.json index af1dfa1..7bbd4e8 100644 --- a/src/SharpEmu.GUI/Languages/ru.json +++ b/src/SharpEmu.GUI/Languages/ru.json @@ -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": "Частота обновления", diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json index e3cab4e..6135c15 100644 --- a/src/SharpEmu.GUI/Languages/tr.json +++ b/src/SharpEmu.GUI/Languages/tr.json @@ -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ı", diff --git a/src/SharpEmu.GUI/MainWindow.GameOptions.cs b/src/SharpEmu.GUI/MainWindow.GameOptions.cs index d6fdae4..2f42710 100644 --- a/src/SharpEmu.GUI/MainWindow.GameOptions.cs +++ b/src/SharpEmu.GUI/MainWindow.GameOptions.cs @@ -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, diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index d3abe0f..518bb78 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -856,22 +856,6 @@ SPDX-License-Identifier: GPL-2.0-or-later VerticalAlignment="Center" /> - - - - - - - - - - - - _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()}"); diff --git a/src/SharpEmu.GUI/PerGameSettings.cs b/src/SharpEmu.GUI/PerGameSettings.cs index 3a2aa40..58b2cff 100644 --- a/src/SharpEmu.GUI/PerGameSettings.cs +++ b/src/SharpEmu.GUI/PerGameSettings.cs @@ -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, diff --git a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs index d9eef56..0ac893a 100644 --- a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs +++ b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs @@ -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 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 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? _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 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(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() - : new List(); - 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(); + var cleared = new List<(TrackedRegion Region, ulong Address, ulong Size)>(); try { using (GpuMemoryAccessProfile.Measure(GpuMemoryAccessProfile.Operation.UploadTracking, size)) { - var visitor = new RegionUploadVisitor(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; } } diff --git a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs index e435736..7dfe95c 100644 --- a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs +++ b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs @@ -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(bool preserveHotPages, ulong address, ulong size, ref TVisitor visitor) - where TVisitor : struct, ICpuUploadVisitor + public void ForEachCpuUploadRange( + bool preserveHotPages, + ulong address, + ulong size, + Action visitCleared, + Action 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); } } diff --git a/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs b/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs index c19ef0c..22ae8ec 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs @@ -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 diff --git a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs index ff5f998..24869ab 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs @@ -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 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; diff --git a/src/SharpEmu.HLE/HostMemory.cs b/src/SharpEmu.HLE/HostMemory.cs index 3e6dffa..6586bf5 100644 --- a/src/SharpEmu.HLE/HostMemory.cs +++ b/src/SharpEmu.HLE/HostMemory.cs @@ -149,14 +149,6 @@ public static unsafe class HostMemory private static readonly object Gate = new(); private static readonly SortedList 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? 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? 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(); - region.OverridePages ??= new SortedSet(); 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); } } } diff --git a/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs b/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs index 0317c42..dfb8c60 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.DrawPackets.cs @@ -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}"); - } - } } diff --git a/src/SharpEmu.Libs/Agc/AgcExports.Events.cs b/src/SharpEmu.Libs/Agc/AgcExports.Events.cs index 2637559..d21a8b6 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.Events.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.Events.cs @@ -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); } diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GpuBuffer.cs b/src/SharpEmu.Libs/Gpu/Buffers/GpuBuffer.cs index bf14ce8..fec9cf7 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GpuBuffer.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GpuBuffer.cs @@ -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), diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GpuBufferUsage.cs b/src/SharpEmu.Libs/Gpu/Buffers/GpuBufferUsage.cs index 63dde7c..09fc7c3 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GpuBufferUsage.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GpuBufferUsage.cs @@ -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, } diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GpuDeviceInfo.cs b/src/SharpEmu.Libs/Gpu/Buffers/GpuDeviceInfo.cs index 5396bab..d4c636c 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GpuDeviceInfo.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GpuDeviceInfo.cs @@ -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. diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs index 76c9d1e..183f607 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs @@ -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 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 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(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? _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 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 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 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 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(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(); - 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(); + 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? _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 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)); diff --git a/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs index b1a178e..e6b247b 100644 --- a/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Buffers/IGuestImageCache.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs index becffda..af8fbb6 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs index c15fb34..4cb3dac 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Labels.cs @@ -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; } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs index 0883dd6..7e6f8fd 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs @@ -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 diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs index d5c49e7..ef91e56 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs @@ -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)) { diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs index cbcae94..6196265 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs @@ -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 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); diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Scaling.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Scaling.cs deleted file mode 100644 index c5009c7..0000000 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Scaling.cs +++ /dev/null @@ -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 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; - } -} diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs index 36362f2..f244c77 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs @@ -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 NoBarriers = new(0); - public (List 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? barriers = null; + var barriers = new List(); 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(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(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 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 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 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( diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs index 460badc..365bced 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs @@ -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 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); diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs index a63631f..f61d4b8 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs @@ -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 diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs index d4b8cc9..000513d 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs @@ -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 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) { diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index 175f736..8d50e0a 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -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 _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); diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequest.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequest.cs index beaa5e4..79be977 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequest.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequest.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/Images/RenderScalePolicy.cs b/src/SharpEmu.Libs/Gpu/Images/RenderScalePolicy.cs deleted file mode 100644 index ab85857..0000000 --- a/src/SharpEmu.Libs/Gpu/Images/RenderScalePolicy.cs +++ /dev/null @@ -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? _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 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); - } -} diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs index 9c0af7c..1e5b709 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs @@ -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() { diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs index 998efcc..a120cba 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs @@ -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. diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs index c7aa44f..96a8f8d 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs @@ -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 _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() .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. diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index fd06d43..34f61c3 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -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) diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs index df493cc..4265201 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs @@ -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]; diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ColorTargetResolver.cs b/src/SharpEmu.Libs/Gpu/Rendering/ColorTargetResolver.cs index 32d916e..3e5e630 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ColorTargetResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ColorTargetResolver.cs @@ -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); } } diff --git a/src/SharpEmu.Libs/Gpu/Rendering/DepthTargetResolver.cs b/src/SharpEmu.Libs/Gpu/Rendering/DepthTargetResolver.cs index 69f9a62..c60456a 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/DepthTargetResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/DepthTargetResolver.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/Rendering/DrawTargets.cs b/src/SharpEmu.Libs/Gpu/Rendering/DrawTargets.cs index 3912c01..0b901da 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/DrawTargets.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/DrawTargets.cs @@ -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. diff --git a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs index 56c4b1e..a4ca4e3 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs @@ -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 diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs index 4f4ca77..cc31c82 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs @@ -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) { diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs index 32c576a..5831fae 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Resources.cs @@ -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 { diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs index ba546c7..38fa148 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs @@ -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( diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs index 857eaa8..8558cf4 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs @@ -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 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(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)); } diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs index 1b2f5ee..fa55ad3 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs @@ -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 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 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. diff --git a/src/SharpEmu.Libs/Gpu/Scheduling/SubmissionScheduler.cs b/src/SharpEmu.Libs/Gpu/Scheduling/SubmissionScheduler.cs index d7e6a8e..7999a4d 100644 --- a/src/SharpEmu.Libs/Gpu/Scheduling/SubmissionScheduler.cs +++ b/src/SharpEmu.Libs/Gpu/Scheduling/SubmissionScheduler.cs @@ -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) diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/NativeHalfConversionProbe.cs b/src/SharpEmu.Libs/Gpu/Vulkan/NativeHalfConversionProbe.cs deleted file mode 100644 index c8dbb1e..0000000 --- a/src/SharpEmu.Libs/Gpu/Vulkan/NativeHalfConversionProbe.cs +++ /dev/null @@ -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(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 beginCommand) - { - var vectors = HalfConversionReference.BuildTestVectors(); - try - { - var results = Measure(device, scheduler, beginCommand, vectors); - var mismatches = 0; - var examples = new List(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 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(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 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(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}"); - } - } -} diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/ZeroOutOfBoundsReadProbe.cs b/src/SharpEmu.Libs/Gpu/Vulkan/ZeroOutOfBoundsReadProbe.cs deleted file mode 100644 index 9caec72..0000000 --- a/src/SharpEmu.Libs/Gpu/Vulkan/ZeroOutOfBoundsReadProbe.cs +++ /dev/null @@ -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 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 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(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 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(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}"); - } - } -} diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 725c244..5093b89 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -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.Shared.Rent(requested) : GC.AllocateUninitializedArray(requested); + var buffer = GC.AllocateUninitializedArray(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.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 value) => _inner.AppendFormatted(value); - - public void AppendFormatted(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) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 6bb804b..3cea481 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -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) diff --git a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs index 7dff0eb..17d81da 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs @@ -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); } } diff --git a/src/SharpEmu.Libs/Media/HostMovieBridge.cs b/src/SharpEmu.Libs/Media/HostMovieBridge.cs index 9873ee7..27d7678 100644 --- a/src/SharpEmu.Libs/Media/HostMovieBridge.cs +++ b/src/SharpEmu.Libs/Media/HostMovieBridge.cs @@ -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."); diff --git a/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs b/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs index b0bca72..b0a8f9c 100644 --- a/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs +++ b/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs @@ -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) diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.FlipRequests.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.FlipRequests.cs index 375026b..1181350 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.FlipRequests.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.FlipRequests.cs @@ -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; } diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 25eec50..2688f09 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -519,19 +519,18 @@ public static partial class VideoOutExports } Span 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 status, GuestDisplayResolution resolution, uint refreshRate, + internal static void WriteOutputStatus(Span 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. diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs index 5068036..45f9aab 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs @@ -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) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index ac7532c..36e741d 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -290,7 +290,6 @@ internal static unsafe partial class VulkanVideoPresenter public bool TryReadGuest(ulong address, Span 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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs index d64b782..885b7f5 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs @@ -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. diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index 74d3dca..7f5333f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -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; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index 2e305b9..caa42db 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -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; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index d899ad6..cafaa4d 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs index c56f9df..dbdff71 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs @@ -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); } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs index fd749b8..43a040b 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs @@ -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; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index a862abe..c9a8d52 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -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); diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 6732552..fca2dca 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -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) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RenderScale.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RenderScale.cs deleted file mode 100644 index 0648de1..0000000 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RenderScale.cs +++ /dev/null @@ -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); - } - } -} diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs index f4ed0ff..9d682a8 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs @@ -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; } diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index adcc5b8..9eec5ba 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -70,8 +70,6 @@ public static partial class Gen5SpirvTranslator private readonly IReadOnlyList _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 _vectorRegisterVariables = new(); private uint _vectorRegisters; private (uint First, uint Last) _dynamicVectorRange; - private readonly Dictionary _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 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(); - var rejected = new HashSet(); - 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); diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs index 062d0ad..560daa9 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs @@ -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() diff --git a/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecRunAnalysis.cs b/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecRunAnalysis.cs deleted file mode 100644 index 2d2fb58..0000000 --- a/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecRunAnalysis.cs +++ /dev/null @@ -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, - }; - } -} diff --git a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs index f039f87..9829b63 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs @@ -337,21 +337,12 @@ public sealed class BindingLayout : IEquatable 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 UserDataRegisters { get; init; } = []; public IReadOnlyList 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 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(); if (info.Buffers.Count != 0) @@ -655,7 +644,6 @@ public sealed class BindingLayout : IEquatable MemoryOffsetDword = memoryOffsetDword, MemoryOffsetCount = memoryOffsetCount, UsesDispatchThreadLimits = usesDispatchThreadLimits, - UsesRenderScale = usesRenderScale, UserDataRegisters = userDataRegisters, Descriptors = descriptors, }; @@ -669,14 +657,13 @@ public sealed class BindingLayout : IEquatable 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( diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs index 1308be6..759e5bc 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs @@ -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; diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarGraphDiskCache.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarGraphDiskCache.cs index 826bf14..4f8e10f 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarGraphDiskCache.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarGraphDiskCache.cs @@ -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? fixedFunctionVertexLoads, uint waveSize) { diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs index 3214514..985b04f 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs @@ -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; diff --git a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs index 8414596..32d8b11 100644 --- a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs +++ b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs @@ -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 PixelOutputs { get; init; } = []; diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs deleted file mode 100644 index 6e015b7..0000000 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcDirectDrawModifierTests.cs +++ /dev/null @@ -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 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 buffer = stackalloc byte[sizeof(ulong)]; - BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); - Assert.True(memory.TryWrite(address, buffer)); - } -} diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcEventWriteTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcEventWriteTests.cs deleted file mode 100644 index 5e7a3cb..0000000 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcEventWriteTests.cs +++ /dev/null @@ -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 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; - } -} diff --git a/tests/SharpEmu.Libs.Tests/Cli/RosettaGcBudgetTests.cs b/tests/SharpEmu.Libs.Tests/Cli/RosettaGcBudgetTests.cs deleted file mode 100644 index 52537c1..0000000 --- a/tests/SharpEmu.Libs.Tests/Cli/RosettaGcBudgetTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.CLI; -using Xunit; - -namespace SharpEmu.Libs.Tests.Cli; - -public sealed class RosettaGcBudgetTests -{ - [Fact] - public void DotnetRestartKeepsTheManagedEntryAssemblyAndGuestArguments() - { - Assert.Equal(new[] { "/runtime/dotnet", "/emulator/SharpEmu.CLI.dll", "--hdr=off", "/games/My Game/eboot.bin" }, - RosettaGcBudget.BuildArguments("/runtime/dotnet", "/emulator/SharpEmu.CLI.dll", - ["--hdr=off", "/games/My Game/eboot.bin"])); - } - - [Fact] - public void AppHostRestartDoesNotPassTheManagedAssemblyAsAGuestArgument() - { - Assert.Equal(new[] { "/emulator/SharpEmu.CLI", "--hdr=off", "/games/My Game/eboot.bin" }, - RosettaGcBudget.BuildArguments("/emulator/SharpEmu.CLI", "/emulator/SharpEmu.CLI.dll", - ["--hdr=off", "/games/My Game/eboot.bin"])); - } -} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/GuestFastPathStubTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/GuestFastPathStubTests.cs index 9c3e138..4b84faf 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/GuestFastPathStubTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/GuestFastPathStubTests.cs @@ -297,30 +297,6 @@ public sealed unsafe class GuestFastPathStubTests Assert.Equal(original, buffer); } - [Fact] - public void MemoryCopyStub_LeavesAPageSizedCopyToTheManagedExport() - { - if (!CanRunStubs()) - { - return; - } - - using var stub = new MemoryCopyStub(); - var minimum = DirectExecutionBackend.ManagedCopyMinimumBytes; - var source = Enumerable.Range(0, minimum).Select(value => (byte)(value * 3)).ToArray(); - var destination = new byte[minimum]; - fixed (byte* from = source) - fixed (byte* to = destination) - { - // The managed export marks the whole destination written before it copies, instead of - // taking a write-tracking fault per watched page. - Assert.Equal(MemoryCopySlowPathMarker, stub.Call(to, from, (nuint)minimum)); - Assert.Equal((ulong)to, stub.Call(to, from, (nuint)minimum - 1)); - } - - Assert.Equal(source[..^1], destination[..^1]); - } - /// The memcpy stub called with the platform (SysV) convention the guest uses. private sealed class MemoryCopyStub : IDisposable { diff --git a/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs deleted file mode 100644 index 63b8d20..0000000 --- a/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using System.Diagnostics; -using SharpEmu.Core.Cpu.Native; -using Xunit; - -namespace SharpEmu.Libs.Tests.Cpu; - -// The POSIX stand-in for kernel32!Sleep takes milliseconds in ecx. It must sleep for a second or -// more as well: macOS usleep rejects such lengths with EINVAL, which made the stub return at once. -public sealed unsafe class PosixSleepStubTests -{ - [NativeX64Theory] - [InlineData(1200u, 1.0, 4.0)] - [InlineData(20u, 0.015, 1.0)] - public void SleepStubSleepsForTheRequestedMilliseconds(uint milliseconds, double minSeconds, double maxSeconds) - { - if (OperatingSystem.IsWindows()) return; - // SysV passes the fourth integer argument in rcx, where the stub reads its milliseconds. - var sleep = (delegate* unmanaged)PosixHostStubs.SleepStubAddress; - var started = Stopwatch.GetTimestamp(); - sleep(0, 0, 0, milliseconds); - var elapsed = Stopwatch.GetElapsedTime(started).TotalSeconds; - Assert.InRange(elapsed, minSeconds, maxSeconds); - } -} diff --git a/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs b/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs index b18bbb2..3237d57 100644 --- a/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs +++ b/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs @@ -8,59 +8,6 @@ namespace SharpEmu.Libs.Tests.GUI; public sealed class PerGameSettingsTests { - [Fact] - public void GuestResolutionDefaultsToHdAndCanBeOverriddenPerGame() - { - var global = new GuiSettings { Resolution = "3840x2160" }; - Assert.Equal("1920x1080", EffectiveLaunchSettings.Resolve(global, null).GuestResolution); - var game = new PerGameSettings { GuestResolution = "3840x2160" }; - game.RemoveInheritedValues(global); - Assert.False(game.IsEmpty); - var restored = PerGameSettings.NormalizeFromJson(System.Text.Json.JsonSerializer.Serialize(game)); - Assert.Equal("3840x2160", EffectiveLaunchSettings.Resolve(global, restored).GuestResolution); - global.GuestResolution = "3840x2160"; - game.RemoveInheritedValues(global); - Assert.True(game.IsEmpty); - Assert.Equal("3840x2160", EffectiveLaunchSettings.Resolve(global, game).GuestResolution); - } - - [Fact] - public void RenderScaleDefaultsToNativeAndCanBeOverriddenPerGame() - { - var global = new GuiSettings(); - Assert.Equal(1.0f, EffectiveLaunchSettings.Resolve(global, null).RenderScale); - var game = new PerGameSettings { RenderScale = 0.5f }; - game.RemoveInheritedValues(global); - Assert.False(game.IsEmpty); - var restored = PerGameSettings.NormalizeFromJson(System.Text.Json.JsonSerializer.Serialize(game)); - Assert.Equal(0.5f, EffectiveLaunchSettings.Resolve(global, restored).RenderScale); - global.RenderScale = 0.5f; - game.RemoveInheritedValues(global); - Assert.True(game.IsEmpty); - Assert.Equal(0.5f, EffectiveLaunchSettings.Resolve(global, game).RenderScale); - } - - [Theory] - [InlineData(0f, 0.25f)] - [InlineData(100f, 2.0f)] - [InlineData(float.NaN, 1.0f)] - public void OutOfRangeRenderScaleIsClamped(float value, float expected) - { - Assert.Equal(expected, EffectiveLaunchSettings.Resolve( - new GuiSettings(), new PerGameSettings { RenderScale = value }).RenderScale); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("invalid")] - [InlineData("7680x4320")] - public void InvalidGuestResolutionFallsBackToHd(string? value) - { - Assert.Equal("1920x1080", EffectiveLaunchSettings.Resolve( - new GuiSettings(), new PerGameSettings { GuestResolution = value }).GuestResolution); - } - [Fact] public void NewGameInheritsWritableApp0AndGlobalCrashCapture() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferAsyncReadbackTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferAsyncReadbackTests.cs index 95f94fa..015ec32 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferAsyncReadbackTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferAsyncReadbackTests.cs @@ -48,91 +48,6 @@ public sealed class GuestBufferAsyncReadbackTests(HeadlessVulkanFixture fixture) harness.Shutdown(); } - // Without a readback queue the main queue carries the download and the guest thread waits - // for that submission; the worker records the copies and moves on. - [Fact] - public void AGuestReadWithoutAReadbackQueueStillWaitsOffTheQueueThread() - { - if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan); - using var fatal = new FatalScope(); - Assert.Null(harness.Cache.AsyncReadback); - var address = harness.MapBacked(0x10000, ReadWrite); - GpuWrite(harness, address, 0x55667788); - Assert.True(harness.Cache.HasGpuDirtyBytes(address, 0x100)); - - Assert.True(harness.Cache.DownloadToCpu(address + 8, 4)); - - Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100)); - Assert.Equal((1L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried)); - Assert.Equal(0x55667788u, BitConverter.ToUInt32(harness.Read(address + 0xF0, 4))); - - // A second write after the first read is picked up by the next read, not served stale. - GpuWrite(harness, address, 0x99AABBCCu); - Assert.True(harness.Cache.DownloadToCpu(address, 4)); - Assert.Equal(0x99AABBCCu, BitConverter.ToUInt32(harness.Read(address + 0x40, 4))); - Assert.Equal((2L, 0L), (harness.Cache.PendingReadbacksApplied, harness.Cache.PendingReadbacksRetried)); - harness.Shutdown(); - } - - // On a unified-memory device a read of a retired GPU write copies straight out of the buffer's - // own mapping: no copy command, no pending readback. - [Fact] - public void AReadOfARetiredWriteComesFromTheUnifiedMapping() - { - if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan); - using var fatal = new FatalScope(); - if (!harness.Cache.UnifiedBuffers) - { - harness.Shutdown(); - return; - } - - var address = harness.MapBacked(0x10000, ReadWrite); - GpuWrite(harness, address, 0x0BADF00D); - harness.Worker.Run(() => harness.Scheduler.Wait(harness.Scheduler.Flush())); - - Assert.True(harness.Cache.DownloadToCpu(address + 8, 4)); - - Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100)); - Assert.Equal(0x0BADF00Du, BitConverter.ToUInt32(harness.Read(address + 0x80, 4))); - Assert.Equal(1L, harness.Cache.MappedReadbacks); - Assert.Equal(0L, harness.Cache.PendingReadbacksApplied); - harness.Shutdown(); - } - - // A read while the writer is submitted but still running waits for that submission only, then - // reads the mapping; a write recorded after the read was issued is picked up by the next read. - [Fact] - public void AReadOfASubmittedWriteWaitsForItAndReadsTheUnifiedMapping() - { - if (_vulkan is null || !GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan); - using var fatal = new FatalScope(); - if (!harness.Cache.UnifiedBuffers) - { - harness.Shutdown(); - return; - } - - var address = harness.MapBacked(0x10000, ReadWrite); - GpuWrite(harness, address, 0x12345678); - SubmitBatch(harness); - - Assert.True(harness.Cache.DownloadToCpu(address, 4)); - - Assert.Equal(0x12345678u, BitConverter.ToUInt32(harness.Read(address + 0xFC, 4))); - Assert.Equal(1L, harness.Cache.MappedReadbacks); - Assert.False(harness.Cache.HasGpuDirtyBytes(address, 0x100)); - - GpuWrite(harness, address, 0x9ABCDEF0); - SubmitBatch(harness); - Assert.True(harness.Cache.DownloadToCpu(address, 4)); - Assert.Equal(0x9ABCDEF0u, BitConverter.ToUInt32(harness.Read(address + 0x10, 4))); - harness.Shutdown(); - } - private static void GpuWrite(CacheHarness harness, ulong address, uint value) => harness.Worker.Run(() => { var (buffer, offset) = harness.Cache.ObtainBuffer(address, 0x100, isWritten: true); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs index 632eeaa..5546b62 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Buffers/GuestBufferCacheTests.cs @@ -23,53 +23,6 @@ public sealed class GuestBufferCacheTests : IClassFixture { private const ulong Page = GuestBufferCache.CachingPageSize; - [Fact] - public void CommandBackingReadRejectsGpuImageBytesButAllowsCleanPageNeighbours() - { - if (!GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan!); - var address = harness.MapBacked(Page, ReadWrite); - harness.Write(address + 64, new byte[] { 1, 2, 3, 4 }); - var request = Color32(address, 4); - var id = harness.Acquire(ref request); - harness.Worker.Run(() => - { - Assert.True(harness.Images.TryClearImageFromBuffer(address, 16, 0x22222222)); - Assert.True(harness.Image(id).IsGpuModified); - var tick = harness.Scheduler.CurrentTick; - Span bytes = stackalloc byte[4]; - Assert.False(harness.Cache.TryReadCommandBacking(address, bytes)); - Assert.True(harness.Cache.TryReadCommandBacking(address + 64, bytes)); - Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray()); - Assert.Equal(tick, harness.Scheduler.CurrentTick); - }); - } - - [Fact] - public void CommandBytesNextToPendingGpuCounterDoNotWaitOrUnprotectThePage() - { - if (!GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan!); - var address = harness.MapBacked(Page, ReadWrite); - harness.Write(address + 16, new byte[] { 1, 2, 3, 4 }); - harness.Worker.Run(() => - { - var (buffer, offset) = harness.Cache.ObtainBuffer(address, 4, isWritten: true); - buffer.Fill(offset, 4, 123); - var tick = harness.Scheduler.CurrentTick; - var protection = harness.Protection(address); - Span bytes = stackalloc byte[4]; - Assert.True(harness.Cache.TryReadCommandBacking(address + 16, bytes)); - Assert.Equal(new byte[] { 1, 2, 3, 4 }, bytes.ToArray()); - Assert.False(harness.Cache.TryReadCommandBacking(address, bytes)); - Assert.False(harness.Cache.TryReadCommandBacking(address - 2, bytes)); - Assert.Equal(tick, harness.Scheduler.CurrentTick); - Assert.Equal(protection, harness.Protection(address)); - Assert.True(harness.Cache.HasGpuDirtyPages(address, 4)); - }); - } - - [Fact] public void UnalignedImageObtainUploadsTheWholeDirtyPageToItsBufferOwner() { @@ -674,12 +627,7 @@ public sealed class GuestBufferCacheTests : IClassFixture var address = harness.MapBacked(0x10000, ReadWrite); var (buffer, _) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, 0x8000, isWritten: false)); - harness.Worker.Run(() => - { - harness.Cache.WriteHostMemory(address + 0x1000, Pattern(0x100, 7)); - // Deferred host writes become visible when a command acquires its buffer. - harness.Cache.PrepareBda([new GuestSpan(address, 0x10000)]); - }); + harness.Worker.Run(() => harness.Cache.WriteHostMemory(address + 0x1000, Pattern(0x100, 7))); Assert.Equal(Pattern(0x100, 7), harness.Read(address + 0x1000, 0x100)); Assert.Equal(Pattern(0x100, 7), harness.ReadBack(buffer, 0x1000, 0x100)); @@ -806,8 +754,8 @@ public sealed class GuestBufferCacheTests : IClassFixture harness.Write(address, expected); harness.Worker.Run(() => { - harness.Cache.NoteMemoryVisibilityPoint(); harness.Cache.PrepareBda([new GuestSpan(address, size)]); + Assert.False(harness.Cache.HasCpuDirtyPages(address, size)); Assert.False(harness.Cache.HasGpuDirtyPages(address, size)); harness.Cache.PrepareBda([new GuestSpan(address, size)]); }); @@ -840,11 +788,7 @@ public sealed class GuestBufferCacheTests : IClassFixture }); Assert.Equal(Bytes(0x12345678u), harness.ReadBack(buffer, offset, 4)); Assert.True(harness.Cache.TrySynchronizeCpuRead(address, size)); - harness.Worker.Run(() => - { - harness.Cache.NoteMemoryVisibilityPoint(); - harness.Cache.PrepareBda([new GuestSpan(address, size)]); - }); + harness.Worker.Run(() => harness.Cache.PrepareBda([new GuestSpan(address, size)])); var expected = Pattern((int)size, 7); await Task.Run(() => @@ -852,48 +796,12 @@ public sealed class GuestBufferCacheTests : IClassFixture Assert.True(harness.Store.MarkCpuWrite(address, size)); harness.Write(address, expected); }); - harness.Worker.Run(() => - { - harness.Cache.NoteMemoryVisibilityPoint(); - harness.Cache.PrepareBda([new GuestSpan(address, size)]); - }); + harness.Worker.Run(() => harness.Cache.PrepareBda([new GuestSpan(address, size)])); Assert.False(harness.Cache.HasCpuDirtyPages(address, size)); Assert.Equal(expected, harness.ReadBack(buffer, offset, size)); harness.Shutdown(); } - [Fact] - public void DeviceAddressVisibilityPointRefreshesHotPagesWithoutAnotherWriteFault() - { - if (!GatePrerequisites.Ready(_vulkan)) return; - using var harness = new CacheHarness(_vulkan); - var address = harness.MapBacked(0x20000, ReadWrite); - const ulong size = 0x8000; - var (buffer, offset) = harness.Worker.Run(() => harness.Cache.ObtainBuffer(address, size, false)); - for (var write = 1; write <= 3; write++) - { - Assert.True(harness.Store.MarkCpuWrite(address, size)); - harness.Write(address, Pattern((int)size, (byte)write)); - harness.Worker.Run(() => - { - harness.Cache.NoteMemoryVisibilityPoint(); - harness.Cache.PrepareBda([new GuestSpan(address, size)]); - }); - } - - Assert.True(harness.Cache.HasCpuDirtyPages(address, size)); - var expected = Pattern((int)size, 9); - // A permanently writable hot page changes without another protection fault. - harness.Write(address, expected); - harness.Worker.Run(() => - { - harness.Cache.NoteMemoryVisibilityPoint(); - harness.Cache.PrepareBda([new GuestSpan(address, size)]); - }); - Assert.Equal(expected, harness.ReadBack(buffer, offset, size)); - harness.Shutdown(); - } - [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs index 9761146..c61697d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs @@ -31,11 +31,6 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost public List EndOfPipeWrites { get; } = new(); - // Satisfied waits and command-processor memory writes reported to the host. - public int VisibilityPoints { get; private set; } - - public void NoteMemoryVisibilityPoint() => VisibilityPoints++; - public List GuestReads { get; } = new(); public Action? BeforeGuestRead { get; set; } @@ -185,8 +180,6 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost public bool ResolvesIndirectDrawOnGpu { get; set; } - public bool ResolvesNonIndexedIndirectDrawOnGpu { get; set; } - public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}"); public Exception Fatal(string message) => new CommandStreamFatalException(message); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs index ae04a3c..49b8a3f 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs @@ -48,25 +48,17 @@ public sealed class CommandStreamWorkerTests [Fact] public void Worker_RetriesABlockedHeadAfterTheTimedWait() { - var (host, queue, worker) = NewWorker(cancelBlockedAtStop: true); + var (host, queue, worker) = NewWorker(cancelBlockedAtStop: false); host.WriteDword(Label, 0); - try - { - Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6)); - // A loaded runner can start the worker after a fixed sleep has elapsed. - // Publish the signal only once the worker has actually blocked on it. - Assert.True(SpinWait.SpinUntil(() => queue.BlockedQueueCount == 1, TimeSpan.FromSeconds(5))); - host.WriteDword(Label, 1); + Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6)); + Thread.Sleep(50); + host.WriteDword(Label, 1); - Assert.Equal(IdleOutcome.Completed, queue.WaitForIdle()); - Assert.Equal(6u, queue.GetInterpreter(0).InstanceCount); - Assert.True(queue.BlockedRetries >= 1); - } - finally - { - worker.Stop(); - } + Assert.Equal(IdleOutcome.Completed, queue.WaitForIdle()); + Assert.Equal(6u, queue.GetInterpreter(0).InstanceCount); + Assert.True(queue.BlockedRetries >= 1); + worker.Stop(); } [Fact] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs index 298269d..707e5b0 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterDrawTests.cs @@ -126,24 +126,6 @@ public sealed class GpuCommandInterpreterDrawTests Assert.Contains("arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2)).Message); } - [Fact] - public void NonIndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan() - { - var runner = new StreamRunner(); - runner.Host.ResolvesNonIndexedIndirectDrawOnGpu = true; - runner.Host.WriteWords(Arguments, new uint[] { 9, 3, 1, 2 }); - - runner.Run(SetBase(Arguments, dispatch: false), StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2)); - - var auto = Assert.Single(runner.Host.AutoDraws); - Assert.Equal((Arguments, DrawOffsetSource.IndirectArguments), (auto.IndirectArgumentsAddress, auto.OffsetSource)); - Assert.DoesNotContain(runner.Host.GuestReads, address => address >= Arguments && address < Arguments + 16); - - // A later draw that inherits the instance count reads it then. - runner.Run(StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 5, 0)); - Assert.Equal(3u, runner.Host.AutoDraws[1].InstanceCount); - } - [Fact] public void IndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs index a79f769..1425f35 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLabelTests.cs @@ -325,16 +325,12 @@ public sealed class GpuCommandInterpreterLabelTests Assert.Contains("event type is unknown", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.EventWrite, 0x3F)).Message); runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label))); - // CPU-only stores leave GPU query-reduction inputs stale. Require the - // host transfer path for both words of every counter, including ready. - Assert.Equal(32, runner.Host.Calls.Count); - Assert.All(runner.Host.Calls, call => Assert.StartsWith("fill ", call)); Assert.Equal(1UL << 63, runner.Host.ReadQword(Label)); Assert.Equal(1UL << 63, runner.Host.ReadQword(Label + 15 * 16)); Assert.Equal(0UL, runner.Host.ReadQword(Label + 8)); runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label))); - Assert.Equal((1UL << 63) | 0x2FFFFFFUL, runner.Host.ReadQword(Label)); + Assert.Equal((1UL << 63) | 1, runner.Host.ReadQword(Label)); } [Fact] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLoopTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLoopTests.cs index c74ce4f..4c93b35 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLoopTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterLoopTests.cs @@ -261,30 +261,6 @@ public sealed class GpuCommandInterpreterLoopTests Assert.Contains(Label, runner.Host.GuestReads); } - [Fact] - public void Wait_IsAMemoryVisibilityPointOnlyOnceSatisfied() - { - var runner = new StreamRunner(); - - Assert.Equal(SubmissionProgress.Blocked, runner.Run(WaitEqual(Label, 1))); - Assert.Equal(0, runner.Host.VisibilityPoints); - - runner.Host.WriteDword(Label, 1); - Assert.Equal(SubmissionProgress.Complete, runner.Run()); - Assert.Equal(1, runner.Host.VisibilityPoints); - } - - [Fact] - public void WriteData_IsAMemoryVisibilityPoint() - { - var runner = new StreamRunner(); - - runner.Run(WriteData(Label, 7)); - - Assert.Equal(7u, runner.Host.ReadDword(Label)); - Assert.Equal(1, runner.Host.VisibilityPoints); - } - [Fact] public void PendingCommands_RunBeforeEveryPacket() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/CachedImageTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/CachedImageTests.cs index 28c5bee..1b84c97 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/CachedImageTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/CachedImageTests.cs @@ -203,27 +203,6 @@ public sealed unsafe class CachedImageTests : IClassFixture RenderScalePolicy.ConfigureFormatSupport(_ => true); - - private static ImageDescription ColorTarget(uint width = 1920, uint height = 1080) - { - var description = ImageDescription.Create(); - description.Data = new GuestSpan(0x1_0000, (ulong)width * height * 4); - description.PixelFormat = Format.R8G8B8A8Unorm; - description.GuestFormat = GuestPixelFormat.Bits8_8_8_8UNorm; - description.Extent = new Extent3D(width, height, 1); - description.Pitch = width; - description.BytesPerBlock = 4; - description.TileMode = GuestTileMode.RenderTarget; - description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = description.Data.Size, Pitch = width, Height = height }; - return description; - } - - [Fact] - public void ScreenClassRenderTargetsScale() - { - Assert.Equal(Half, RenderScalePolicy.ScaleFor(ColorTarget(), Half)); - Assert.Equal(Half, RenderScalePolicy.ScaleFor(ColorTarget(3840, 2160), Half)); - Assert.Equal(2.0f, RenderScalePolicy.ScaleFor(ColorTarget(2304, 1296), 2.0f)); - } - - [Fact] - public void NativeScaleNeverChangesAnything() - { - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(ColorTarget(), 1.0f)); - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(ColorTarget(640, 360), 1.0f)); - } - - [Fact] - public void DepthTargetsScale() - { - var description = ColorTarget(); - description.PixelFormat = Format.D32Sfloat; - description.GuestFormat = GuestPixelFormat.Bits32Float; - description.TileMode = GuestTileMode.Depth; - Assert.Equal(Half, RenderScalePolicy.ScaleFor(description, Half)); - } - - [Theory] - [InlineData(1279u, 1080u)] - [InlineData(1920u, 719u)] - [InlineData(64u, 64u)] - public void SmallTargetsStayNative(uint width, uint height) - { - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(ColorTarget(width, height), Half)); - } - - [Fact] - public void SquareTargetsStayNative() - { - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(ColorTarget(2048, 2048), Half)); - } - - [Fact] - public void AssetTilingsStayNative() - { - foreach (var tileMode in new[] { GuestTileMode.Linear, GuestTileMode.Standard64KB, GuestTileMode.Standard4KB, GuestTileMode.Prt }) - { - var description = ColorTarget(); - description.TileMode = tileMode; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(description, Half)); - } - } - - [Fact] - public void FootprintsTheHostCannotReproduceStayNative() - { - var multisample = ColorTarget(); - multisample.Samples = 4; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(multisample, Half)); - - var mipped = ColorTarget(); - mipped.Resources = new SubresourceCount(4, 1); - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(mipped, Half)); - - var layered = ColorTarget(); - layered.Resources = new SubresourceCount(1, 6); - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(layered, Half)); - - var volume = ColorTarget(); - volume.Type = GuestImageType.Color3D; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(volume, Half)); - - var compressed = ColorTarget(); - compressed.GuestFormat = GuestPixelFormat.Bc7UNorm; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(compressed, Half)); - - var htile = ColorTarget(); - htile.PixelFormat = Format.D32Sfloat; - htile.GuestFormat = GuestPixelFormat.Bits32Float; - htile.TileMode = GuestTileMode.Depth; - htile.Metadata = new MetadataDescription { Kind = MetadataKind.Htile, Range = new GuestSpan(0x40_0000, 0x1000) }; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(htile, Half)); - - var stencil = ColorTarget(); - stencil.Stencil = new GuestSpan(0x80_0000, 0x1000); - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(stencil, Half)); - } - - // DCC describes guest bytes the host image never stores, so the backing can be any size. - [Fact] - public void ColourCompressionMetadataStillScales() - { - var description = ColorTarget(); - description.Metadata = new MetadataDescription { Kind = MetadataKind.Dcc, Range = new GuestSpan(0x40_0000, 0x1000) }; - Assert.Equal(Half, RenderScalePolicy.ScaleFor(description, Half)); - } - - [Fact] - public void HelperImagesWithoutGuestMemoryStayNative() - { - var description = ColorTarget(); - description.Data = default; - Assert.Equal(1.0f, RenderScalePolicy.ScaleFor(description, Half)); - } - - [Theory] - [InlineData(1920u, 0.5f, 960u)] - [InlineData(1080u, 0.5f, 540u)] - [InlineData(1281u, 0.5f, 641u)] - [InlineData(1920u, 0.75f, 1440u)] - [InlineData(1920u, 2.0f, 3840u)] - [InlineData(1u, 0.5f, 1u)] - [InlineData(0u, 0.5f, 0u)] - public void LengthsRoundUpSoNoGuestPixelIsLost(uint length, float scale, uint expected) - { - Assert.Equal(expected, RenderScalePolicy.ScaleLength(length, scale)); - } - - [Fact] - public void ExtentScalingLeavesDepthAlone() - { - var scaled = RenderScalePolicy.ScaleExtent(new Extent3D(1920, 1080, 4), Half); - Assert.Equal(new Extent3D(960, 540, 4), scaled); - } -} diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs index b23893b..4d2a98b 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs @@ -315,7 +315,7 @@ public sealed class ShaderPipelineCacheTests : IDisposable var programs = Programs(pixelStage: Stage(new ShaderProgramInfo { Stage = ShaderStageKind.Pixel, PixelColorExportMasks = exportMasks })); var resolution = new ColorTargetResolution( default, 0x1000, 0x10000, new Extent2D(64, 64), 0, 0, 1, ColorComponentMap.Identity, false, false, default); - ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1), 1f)]; + ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1))]; var rendering = new RenderingState { Samples = 1, ColorAttachmentCount = 1 }; rendering.ColorAttachments[0] = new RenderingAttachment( default, ImageLayout.ColorAttachmentOptimal, Format.R8G8B8A8Unorm, 0, 0, 0, 0, false, false, false, false, false); @@ -335,7 +335,7 @@ public sealed class ShaderPipelineCacheTests : IDisposable var programs = Programs(); var resolution = new ColorTargetResolution( default, 0x1000, 0x10000, new Extent2D(64, 64), 0, 0, 1, ColorComponentMap.Identity, false, false, default); - ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1), 1f)]; + ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1))]; var rendering = new RenderingState { Samples = 1, ColorAttachmentCount = 1 }; rendering.ColorAttachments[0] = new RenderingAttachment( default, ImageLayout.ColorAttachmentOptimal, Format.R8G8B8A8Unorm, 0, 0, 0, 0, false, false, false, false, false); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPrewarmListTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPrewarmListTests.cs index 2996fa8..cacde02 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPrewarmListTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPrewarmListTests.cs @@ -60,8 +60,8 @@ public sealed class ShaderPrewarmListTests : IDisposable var (record, code) = Assert.Single(reloaded.LoadedComputes()); Assert.True( ShaderProgramCache.TryCompilePrewarm( - record, code, new FakeShaderCompiler(Compile), host.SharedInt64AtomicsEnabled, host.ExecGuardElisionEnabled, host.NativeHalfConversionExact, - host.ZeroOutOfBoundsBufferReads, out var compiled, out var layout, out var error), + record, code, new FakeShaderCompiler(Compile), host.SharedInt64AtomicsEnabled, host.ExecGuardElisionEnabled, + out var compiled, out var layout, out var error), error); Assert.NotNull(layout); Assert.Equal(runtime, Assert.IsType(compiled).Spirv); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs index de20b9d..63b750e 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs @@ -354,36 +354,4 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Contains("records=4/5", fatal.Message); } - [Theory] - [InlineData(74u, 3, 12ul)] - [InlineData(77u, 4, 16ul)] - [InlineData(77u, 3, 16ul)] - [InlineData(64u, 2, 8ul)] - public void MergeBuffers_ConstantUncheckedStreamIncludesFullFormattedFetch(uint format, int components, ulong expectedSize) - { - var resource = Attribute(StreamBase, 0, 1, 0); - resource = resource with - { - Descriptor = resource.Descriptor with { Word3 = IdentitySelect | (format << 12) | (2u << 28) }, - RegisterCount = components, - }; - var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress)); - Assert.Equal(1u, buffer.RecordCount); - Assert.Equal(0u, buffer.Stride); - Assert.Equal(expectedSize, buffer.Size); - } - - [Theory] - [InlineData(0u, 1u)] - [InlineData(1u, 1u)] - [InlineData(3u, 1u)] - [InlineData(2u, 0u)] - public void MergeBuffers_BoundedOrUnboundConstantStreamKeepsDeclaredFootprint(uint outOfBounds, uint records) - { - var resource = Attribute(StreamBase, 0, records, 0); - resource = resource with { Descriptor = resource.Descriptor with { Word3 = resource.Descriptor.Word3 | (outOfBounds << 28) } }; - var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress)); - Assert.Equal((ulong)records, buffer.Size); - } - } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs index 7801afb..aa772f4 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorDrawTests.cs @@ -112,27 +112,6 @@ public sealed class RenderExecutorDrawTests : IDisposable Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw_indexed_indirect", StringComparison.Ordinal)); } - [Fact] - public void IndirectAutoDraw_LetsTheGpuReadTheArguments() - { - var arguments = Auto(1, source: DrawOffsetSource.IndirectArguments) with { IndirectArgumentsAddress = IndirectArguments }; - _executor.DrawAuto(7, Banks(), arguments); - - AssertOrder("obtain 100600000 10 written=False", "begin_rendering", "bind_pipeline Graphics A1", "draw_indirect", "reset_bindings"); - Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw ", StringComparison.Ordinal)); - } - - [Fact] - public void IndirectAutoTriangleFan_ReadsTheArgumentsOnTheCpu() - { - WriteIndirectArguments(3, 2, 4, 1); - var arguments = Auto(1, source: DrawOffsetSource.IndirectArguments) with { IndirectArgumentsAddress = IndirectArguments }; - _executor.DrawAuto(7, Banks(PrimitiveTriangleFan), arguments); - - Assert.DoesNotContain(_host.Calls, c => c.StartsWith("draw_indirect", StringComparison.Ordinal)); - Assert.Contains(_host.Calls, c => c.StartsWith("draw ", StringComparison.Ordinal)); - } - [Fact] public void AutoDraw_RecordsThePhasesAndTheVertexOffsets() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs index 3aebdab..a046fdc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs @@ -205,21 +205,6 @@ public sealed class RenderExecutorStateTests : IDisposable Assert.Equal(new ScissorRectangle(0, 0, 0, 0), RenderExecutor.ResolveScissor(viewport, in scanMode, 64, 32)); } - [Fact] - public void ScaledScissorRoundsOutwardAndStaysInsideTheAttachment() - { - // A guest rectangle keeps its host coverage: the edges round outward, so no guest - // pixel the game asked for is clipped away by the mapping itself. - Assert.Equal(new ScissorRectangle(5, 2, 51, 29), RenderExecutor.ScaleScissor(new ScissorRectangle(11, 5, 101, 57), 0.5f, 64, 36)); - Assert.Equal(new ScissorRectangle(22, 10, 202, 114), RenderExecutor.ScaleScissor(new ScissorRectangle(11, 5, 101, 57), 2f, 256, 144)); - - // Rounding outward must not push the rectangle past the attachment. - Assert.Equal(new ScissorRectangle(0, 0, 32, 18), RenderExecutor.ScaleScissor(new ScissorRectangle(0, 0, 65, 37), 0.5f, 32, 18)); - - // An empty scissor stays empty rather than growing a row. - Assert.Equal(new ScissorRectangle(5, 5, 5, 5), RenderExecutor.ScaleScissor(new ScissorRectangle(10, 10, 10, 10), 0.5f, 64, 36)); - } - [Fact] public void Scissor_ClipRectangleRulesIntersectTheSelectedRectangles() { @@ -337,25 +322,6 @@ public sealed class RenderExecutorStateTests : IDisposable Assert.Contains("create_graphics_pipeline colors=0 depth=True topology=TriangleList restart=False", _pipelines.Calls); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void Attachments_DisabledColorBufferIgnoresStaleTargetsAndPreservesDepthEffects(bool exportsDepth) - { - var banks = Banks(withDepth: true); - banks.Context.ColorControl = ColorControlRegisters.Decode(0); - banks.Context.ColorTargets[0] = RegisterWords.Color(ColorBase, 32, 32); - banks.Context.ShaderInterface.DepthShaderControl = new DepthShaderControlRegisters { DepthExportEnable = exportsDepth }; - - _executor.DrawIndexed(1, banks, Indexed(3)); - - var rendering = Assert.Single(_host.BegunRenderings); - Assert.Equal((0u, 64u, 64u), (rendering.ColorAttachmentCount, rendering.Width, rendering.Height)); - Assert.Equal(Format.D32Sfloat, rendering.DepthFormat); - Assert.Equal(exportsDepth, Assert.Single(_pipelines.PipelineRequests).PixelActive); - Assert.True(Assert.Single(_host.DynamicStates).DepthWriteEnabled); - } - [Theory] [InlineData(0x0u, 0u, 64u)] [InlineData(0xFu, 1u, 32u)] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs index 0fe26c9..c50241a 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs @@ -177,10 +177,6 @@ internal sealed class RecordingRenderHost : IRenderHost public void BindRenderTarget(ResourceSlotIdentifier image) => Calls.Add($"bind_target {image.Index}"); - public void DemoteRenderScale(ResourceSlotIdentifier image) => Calls.Add($"demote_scale {image.Index}"); - - public float GetRenderScale(ResourceSlotIdentifier image) => 1f; - public void ResetBindings() => Calls.Add("reset_bindings"); public ColorAttachmentAcquisition AcquireColorAttachment(in ColorTargetState target) @@ -329,9 +325,6 @@ internal sealed class RecordingRenderHost : IRenderHost public void DrawIndexedIndirect(BufferBinding arguments) => Calls.Add($"draw_indexed_indirect {arguments.Handle:X}:{arguments.Offset:X}"); - public void DrawIndirect(BufferBinding arguments) => - Calls.Add($"draw_indirect {arguments.Handle:X}:{arguments.Offset:X}"); - public void Dispatch(uint groupsX, uint groupsY, uint groupsZ) => Calls.Add($"dispatch {groupsX} {groupsY} {groupsZ}"); public bool TryDispatchIndirect(ulong argumentsAddress) => false; @@ -504,7 +497,6 @@ internal static class RenderExecutorFixtures public const ulong ComputeShader = 0x2_0002_0000; public const uint PrimitiveTriangleList = 4; public const uint PrimitiveTriangleStrip = 6; - public const uint PrimitiveTriangleFan = 5; private static Exception Fatal(string message) => new RenderExecutorFatalException(message); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/TargetResolverTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/TargetResolverTests.cs index 17d6acd..90fff02 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/TargetResolverTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/TargetResolverTests.cs @@ -234,25 +234,6 @@ public sealed class TargetResolverTests : IClassFixture Assert.Equal(ImageLayout.DepthAttachmentStencilReadOnlyOptimal, depthOnlyWrite.AttachmentLayout(Format.D32SfloatS8Uint)); } - [Fact] - public void AttachmentLayoutFor_KeepsTheReadOnlyLayoutForDrawsThatOnlyTest() - { - var context = StencilContext(pass: 4, writeMask: 0xFF, operationValue: 0x10, DepthControl(CompareOp.Less, CompareOp.Always)); - var writer = DepthTargetResolver.ResolveState(context, true, Fatal) with { StencilTestEnabled = false }; - var tester = writer with { DepthWriteEnabled = false }; - const Format format = Format.D32SfloatS8Uint; - var readOnly = ImageLayout.DepthStencilReadOnlyOptimal; - - // A sampled attachment is read-only whatever it was in before. - Assert.Equal(readOnly, DepthStencilState.AttachmentLayoutFor(tester, format, sampled: true, ImageLayout.DepthStencilAttachmentOptimal)); - // A test-only draw after a sampling draw stays read-only, so the scope continues. - Assert.Equal(readOnly, DepthStencilState.AttachmentLayoutFor(tester, format, sampled: false, readOnly)); - // Otherwise the writable layout keeps toggling depth writes inside one scope. - Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, DepthStencilState.AttachmentLayoutFor(tester, format, sampled: false, ImageLayout.DepthStencilAttachmentOptimal)); - Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, DepthStencilState.AttachmentLayoutFor(tester, format, sampled: false, null)); - Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, DepthStencilState.AttachmentLayoutFor(writer, format, sampled: false, readOnly)); - } - [Fact] public void DepthResolve_UsesTheBuilderThenAddsTheState() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs index c2c66b9..9f7e37b 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs @@ -122,106 +122,5 @@ public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : I } } - // op_sel_hi marks a V_FMA_MIX operand as f16, literals included: 0x34CD is 0.3, not an - // f32 denormal. Silent Hill weights a luminance with such literals and divides by it. - [Fact] - public void FusedMixReadsHalfSelectedLiteralsAsHalfFloats() - { - var vulkan = fixture.Vulkan; - if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; - var halfLiteral = new Gen5Vop3pControl(0, 0b001, 0, 0, false); - var floatLiteral = new Gen5Vop3pControl(0, 0b000, 0, 0, false); - var program = Program( - Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), - Vop3(4, "VFmaMixF32", 12, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x34CD), Gen5Operand.Vector(1), Operand(0)) with { Control = halfLiteral }, - Vop3(16, "VFmaMixF32", 13, new Gen5Operand(Gen5OperandKind.LiteralConstant, Bits(0.25f)), Gen5Operand.Vector(1), Operand(0)) with { Control = floatLiteral }, - Vop2(28, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), - BufferAccess(32, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7), - BufferAccess(40, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7), - EndProgram(48)); - var (plan, resources, layout) = Prepare(program); - var request = new ShaderCompileRequest(plan, resources, layout) - { - LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32, - }; - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - using var harness = new ImageTestHarness(vulkan); - using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); - var result = runner.CreateBuffer(ThreadCount * RowBytes); - var registers = new uint[256]; - registers[6] = ThreadCount * RowBytes; - harness.Run(() => runner.Dispatch(registers, - new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); - var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes); - var weight = (float)BitConverter.UInt16BitsToHalf(0x34CD); - for (var lane = 0u; lane < ThreadCount; lane++) - { - var row = actual.AsSpan((int)(lane * RowBytes)); - Assert.Equal(Bits(MathF.FusedMultiplyAdd(weight, lane, 0f)), BinaryPrimitives.ReadUInt32LittleEndian(row)); - Assert.Equal(Bits(lane * 0.25f), BinaryPrimitives.ReadUInt32LittleEndian(row[4..])); - } - } - - // V_CVT_PKRTZ_F16_F32 rounds toward zero: finite values past the f16 range saturate at - // +-65504 instead of becoming infinity, and subnormal results keep whole 2^-24 steps. - [Fact] - public void PackToHalfRoundsTowardZeroWithoutOverflowingToInfinity() - { - var vulkan = fixture.Vulkan; - if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; - var program = Program( - Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)), - MoveScalar(4, 8, Bits(5003.7f)), - MoveScalar(12, 9, Bits(-80000.3f)), - MoveScalar(20, 10, Bits(3.3e-6f)), - MoveScalar(28, 11, Bits(-5e-5f)), - MoveScalar(36, 12, Bits(float.NaN)), - MoveScalar(44, 13, Bits(float.PositiveInfinity)), - Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)), - Vop3(60, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)), - Vop2(68, "VCvtPkrtzF16F32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(3)), - Vop3(72, "VCvtPkrtzF16F32", 13, Gen5Operand.Scalar(12), Gen5Operand.Scalar(13)), - Vop2(80, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)), - BufferAccess(84, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7), - BufferAccess(92, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7), - EndProgram(100)); - var (plan, resources, layout) = Prepare(program); - var request = new ShaderCompileRequest(plan, resources, layout) - { - LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32, - }; - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - using var harness = new ImageTestHarness(vulkan); - using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); - var result = runner.CreateBuffer(ThreadCount * RowBytes); - var registers = new uint[256]; - registers[6] = ThreadCount * RowBytes; - harness.Run(() => runner.Dispatch(registers, - new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); - var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes); - for (var lane = 0u; lane < ThreadCount; lane++) - { - var row = actual.AsSpan((int)(lane * RowBytes)); - var large = MathF.FusedMultiplyAdd(lane, 5003.7f, -80000.3f); - var small = MathF.FusedMultiplyAdd(lane, 3.3e-6f, -5e-5f); - var packed = BinaryPrimitives.ReadUInt32LittleEndian(row); - Assert.Equal(HalfTowardZero(large), packed & 0xFFFF); - Assert.Equal(HalfTowardZero(small), packed >> 16); - var special = BinaryPrimitives.ReadUInt32LittleEndian(row[4..]); - Assert.True((special & 0x7C00) == 0x7C00 && (special & 0x3FF) != 0, $"lane {lane}: 0x{special:X8}"); - Assert.Equal(0x7C00u, special >> 16); - } - } - - private static uint HalfTowardZero(float value) - { - var sign = value < 0 || (value == 0 && float.IsNegative(value)) ? 0x8000u : 0u; - var magnitude = MathF.Abs(value); - if (magnitude >= 65504f) return sign | 0x7BFF; - if (magnitude < 6.10351562e-05f) return sign | (uint)Math.Floor(magnitude * 16777216.0); - var bits = BitConverter.SingleToUInt32Bits(magnitude); - return sign | ((((bits >> 23) - 127 + 15) << 10) | ((bits >> 13) & 0x3FF)); - } - private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value); } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index 9c4d0d4..5c3f36f 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -128,9 +128,8 @@ internal sealed unsafe class HeadlessVulkan : IDisposable { return Create(); } - catch (Exception exception) + catch (Exception) { - Console.Error.WriteLine($"[TEST][VULKAN] Device setup failed: {exception}"); return null; } } @@ -162,7 +161,6 @@ internal sealed unsafe class HeadlessVulkan : IDisposable public void Dispose() { Vk.DeviceWaitIdle(Device); - _deviceInfo?.Slabs.Destroy(); Vk.DestroyDevice(Device, null); if (_debugUtils is { } debugUtils) { @@ -217,33 +215,22 @@ internal sealed unsafe class HeadlessVulkan : IDisposable var appInfo = new ApplicationInfo { SType = StructureType.ApplicationInfo, ApiVersion = apiVersion }; var validation = Environment.GetEnvironmentVariable(ValidationVariable) == "1"; var layers = validation ? SilkMarshal.StringArrayToPtr(new[] { "VK_LAYER_KHRONOS_validation" }) : 0; - uint instanceExtensionCount = 0; - vk.EnumerateInstanceExtensionProperties((byte*)null, &instanceExtensionCount, null); - var instanceExtensions = new ExtensionProperties[instanceExtensionCount]; - fixed (ExtensionProperties* pointer = instanceExtensions) - vk.EnumerateInstanceExtensionProperties((byte*)null, &instanceExtensionCount, pointer); - var portable = instanceExtensions.Any(extension => - SilkMarshal.PtrToString((nint)extension.ExtensionName) == "VK_KHR_portability_enumeration"); - var instanceNames = new List(); - if (validation) instanceNames.Add(ExtDebugUtils.ExtensionName); - if (portable) instanceNames.Add("VK_KHR_portability_enumeration"); - var extensions = instanceNames.Count != 0 ? SilkMarshal.StringArrayToPtr(instanceNames.ToArray()) : 0; + var extensions = validation ? SilkMarshal.StringArrayToPtr(new[] { ExtDebugUtils.ExtensionName }) : 0; var instanceInfo = new InstanceCreateInfo { SType = StructureType.InstanceCreateInfo, - Flags = portable ? InstanceCreateFlags.EnumeratePortabilityBitKhr : 0, PApplicationInfo = &appInfo, EnabledLayerCount = validation ? 1u : 0u, PpEnabledLayerNames = (byte**)layers, - EnabledExtensionCount = (uint)instanceNames.Count, + EnabledExtensionCount = validation ? 1u : 0u, PpEnabledExtensionNames = (byte**)extensions, }; var created = vk.CreateInstance(&instanceInfo, null, out var instance); if (validation) { SilkMarshal.Free(layers); + SilkMarshal.Free(extensions); } - if (extensions != 0) SilkMarshal.Free(extensions); if (created != Result.Success) { @@ -381,7 +368,6 @@ internal sealed unsafe class HeadlessVulkan : IDisposable vulkan13Features.PNext = &barycentricFeatures; } var extensionNames = new List(); - if (HasDeviceExtensions(vk, physical, ["VK_KHR_portability_subset"])) extensionNames.Add("VK_KHR_portability_subset"); if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames); if (barycentric) extensionNames.Add(barycentricExtension); const string fillRectangleExtension = "VK_NV_fill_rectangle"; diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/NativeHalfConversionProbeDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/NativeHalfConversionProbeDeviceTests.cs deleted file mode 100644 index 61f9648..0000000 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/NativeHalfConversionProbeDeviceTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.Libs.Gpu.Vulkan; -using SharpEmu.Libs.Tests.Gpu.Images; -using Silk.NET.Vulkan; -using Xunit; -using Xunit.Abstractions; - -namespace SharpEmu.Libs.Tests.Gpu.Vulkan; - -public sealed class NativeHalfConversionProbeDeviceTests(HeadlessVulkanFixture fixture, ITestOutputHelper output) - : IClassFixture -{ - // The probe is what decides whether the translator may use GLSL UnpackHalf2x16 / - // PackHalf2x16, so the gate is that it completes and reports a count - not that this - // device passes it. The verdict goes to the test output, where a port to another GPU - // can read it. - [Fact] - public void ProbeRunsAndReportsWhetherNativeHalfConversionIsExact() - { - var vulkan = fixture.Vulkan; - if (!GatePrerequisites.Ready(vulkan)) return; - using var harness = new ImageTestHarness(vulkan); - - var result = default(NativeHalfConversionProbe.Result); - harness.Run(() => result = NativeHalfConversionProbe.Run( - harness.Device, - harness.Scheduler, - () => new CommandBuffer(harness.Scheduler.Current.Handle))); - - output.WriteLine( - $"[VK][F16] native half conversion exact={result.Exact} mismatches={result.Mismatches} " + - $"tested={result.Tested} device={vulkan.DeviceName} first={result.Detail}"); - Assert.Null(result.Note); - Assert.True(result.Tested >= 2 * 0x1_0000, $"tested={result.Tested}"); - Assert.InRange(result.Mismatches, 0, result.Tested); - Assert.Equal(result.Mismatches == 0, result.Exact); - harness.AssertNoValidationMessages(); - } - - // Every test vector must agree with System.Half, which is what proves the integer - // sequences the probe compares against are the right reference in the first place. - [Fact] - public void ReferenceConversionsMatchSystemHalfForEveryTestVector() - { - foreach (var value in HalfConversionReference.BuildTestVectors()) - { - var half = BitConverter.UInt16BitsToHalf((ushort)value); - var widened = BitConverter.SingleToUInt32Bits((float)half); - var wide = HalfConversionReference.HalfToFloatBits(value & 0xFFFFu); - Assert.True( - HalfConversionReference.Matches(widened, wide, asHalf: false), - $"half 0x{value & 0xFFFF:X4} widened to 0x{wide:X8}, System.Half says 0x{widened:X8}"); - - var single = BitConverter.UInt32BitsToSingle(value); - var narrowed = BitConverter.HalfToUInt16Bits((Half)single); - var narrow = HalfConversionReference.FloatToHalfBits(value); - Assert.True( - HalfConversionReference.Matches(narrowed, narrow, asHalf: true), - $"float 0x{value:X8} narrowed to 0x{narrow:X4}, System.Half says 0x{narrowed:X4}"); - } - } -} diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveShuffleDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveShuffleDeviceTests.cs index 2ebadd9..bd09267 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveShuffleDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveShuffleDeviceTests.cs @@ -17,16 +17,12 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO : IClassFixture { [Theory] - [InlineData("Dpp8", 32)] - [InlineData("Dpp8", 64)] - [InlineData("Dpp16", 32)] - [InlineData("Dpp16", 64)] - [InlineData("Permlane16", 32)] - [InlineData("Permlane16", 64)] - [InlineData("Permlanex16", 32)] - [InlineData("Permlanex16", 64)] - [InlineData("Reduction", 64)] - public void WaveShuffle_PreservesEachHalf(string operation, uint waveSize) + [InlineData("Dpp8")] + [InlineData("Dpp16")] + [InlineData("Permlane16")] + [InlineData("Permlanex16")] + [InlineData("Reduction")] + public void Wave64Shuffle_PreservesUpperHalf(string operation) { var vulkan = fixture.Vulkan; if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; @@ -83,7 +79,7 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO var (plan, resources, layout) = Prepare(Program([.. instructions])); var request = new ShaderCompileRequest(plan, resources, layout) { - LocalSizeX = 8, LocalSizeY = waveSize / 8, ThreadCountX = 8, ThreadCountY = waveSize / 8, WaveSize = waveSize, + LocalSizeX = 8, LocalSizeY = 8, ThreadCountX = 8, ThreadCountY = 8, WaveSize = 64, }; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); using var harness = new ImageTestHarness(vulkan); @@ -94,7 +90,7 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO harness.Run(() => runner.Dispatch(registers, new Dictionary { [DescriptorBindingKind.Buffers] = [result] }, 1)); var actual = runner.ReadBack(result, 0, 256); - for (uint lane = 0; lane < waveSize; lane++) + for (uint lane = 0; lane < 64; lane++) { var expected = operation switch { @@ -108,6 +104,6 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(actual.AsSpan((int)lane * 4))); } harness.AssertNoValidationMessages(); - output.WriteLine($"Verified wave{waveSize} {operation} on {vulkan.DeviceName}."); + output.WriteLine($"Verified {operation} on {vulkan.DeviceName}."); } } diff --git a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs index d9e0f0c..58a8fec 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestPageTrackerTests.cs @@ -739,31 +739,6 @@ public sealed class GuestPageTrackerTests : IDisposable Release(address, Page); } - [NativePageProtectionFact] - public void NestedUploadOnAnotherTrackerKeepsTheOuterRollbackState() - { - using var other = new GuestPageTrackerTests(); - var address = Allocate(1); - var otherAddress = other.Allocate(1); - Assert.Throws(() => - _tracker.ForEachUploadRange(address, Page, false, NoRange, () => - { - other._tracker.ForEachUploadRange(otherAddress, Page, false, NoRange, NoUpload); - throw new InvalidOperationException("outer upload failed"); - })); - - Assert.True(_tracker.HasCpuDirtyPages(address, Page)); - Assert.True(IsWritable(address)); - Assert.False(other._tracker.HasCpuDirtyPages(otherAddress, Page)); - var uploaded = 0; - _tracker.ForEachUploadRange(address, Page, false, (_, _) => uploaded++, NoUpload); - Assert.Equal(1, uploaded); - _tracker.UntrackMemory(address, Page); - Release(address, Page); - other._tracker.UntrackMemory(otherAddress, Page); - other.Release(otherAddress, Page); - } - [NativePageProtectionTheory] [InlineData(false, true)] [InlineData(true, true)] diff --git a/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs b/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs deleted file mode 100644 index 12c0700..0000000 --- a/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.HLE; -using Xunit; - -namespace SharpEmu.Libs.Tests.Memory; - -// A POSIX query reports the run of pages sharing the queried page's protection, ending at the -// next protection change, as Win32 VirtualQuery does. -public sealed unsafe class HostMemoryQueryRunTests -{ - [Fact] - public void QueryRunsEndAtTheNextProtectionChange() - { - if (OperatingSystem.IsWindows()) return; - var page = (ulong)Environment.SystemPageSize; - var memory = (byte*)HostMemory.Alloc(null, (nuint)(page * 16), HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT, HostMemory.PAGE_READWRITE); - Assert.True(memory != null); - var start = (ulong)memory; - try - { - Assert.True(HostMemory.Protect((void*)(start + 3 * page), (nuint)(3 * page), HostMemory.PAGE_READONLY, out _)); - Assert.True(HostMemory.Protect((void*)(start + 9 * page), (nuint)page, HostMemory.PAGE_NOACCESS, out _)); - Assert.True(HostMemory.Protect((void*)(start + 15 * page), (nuint)page, HostMemory.PAGE_READONLY, out _)); - - AssertRun(start, 0, 3, HostMemory.PAGE_READWRITE); - AssertRun(start, 3, 3, HostMemory.PAGE_READONLY); - AssertRun(start, 4, 2, HostMemory.PAGE_READONLY); - AssertRun(start, 6, 3, HostMemory.PAGE_READWRITE); - AssertRun(start, 9, 1, HostMemory.PAGE_NOACCESS); - AssertRun(start, 10, 5, HostMemory.PAGE_READWRITE); - // A run at the last page of the mapping. - AssertRun(start, 15, 1, HostMemory.PAGE_READONLY); - - // Restoring the default protection removes the overrides from the run. - Assert.True(HostMemory.Protect((void*)(start + 3 * page), (nuint)(7 * page), HostMemory.PAGE_READWRITE, out _)); - AssertRun(start, 0, 15, HostMemory.PAGE_READWRITE); - } - finally - { - Assert.True(HostMemory.Free(memory, 0, HostMemory.MEM_RELEASE)); - } - - void AssertRun(ulong baseAddress, ulong firstPage, ulong pages, uint protect) - { - Assert.NotEqual(0u, (uint)HostMemory.Query((void*)(baseAddress + firstPage * page + 8), out var info)); - Assert.Equal(baseAddress + firstPage * page, info.BaseAddress); - Assert.Equal(pages * page, info.RegionSize); - Assert.Equal(protect, info.Protect); - } - } -} diff --git a/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj index 95d063b..6694cda 100644 --- a/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj +++ b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj @@ -24,8 +24,4 @@ SPDX-License-Identifier: GPL-2.0-or-later - - - - diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/HostVideoOptionsTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/HostVideoOptionsTests.cs index fd583fd..4d81175 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/HostVideoOptionsTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/HostVideoOptionsTests.cs @@ -18,7 +18,6 @@ public sealed class HostVideoOptionsTests DisplayIndex = -2, RefreshRate = 5000, HdrMode = (HostHdrMode)99, - GuestResolution = (GuestDisplayResolution)99, }.Normalize(); Assert.Equal(640, options.Width); @@ -26,42 +25,5 @@ public sealed class HostVideoOptionsTests Assert.Equal(0, options.DisplayIndex); Assert.Equal(1000, options.RefreshRate); Assert.Equal(HostHdrMode.Auto, options.HdrMode); - Assert.Equal(GuestDisplayResolution.Hd, options.GuestResolution); - } - - [Theory] - [InlineData(0.5f, 0.5f)] - [InlineData(0.75f, 0.75f)] - [InlineData(1.0f, 1.0f)] - [InlineData(2.0f, 2.0f)] - [InlineData(0.0f, HostVideoOptions.MinimumRenderScale)] - [InlineData(-3.0f, HostVideoOptions.MinimumRenderScale)] - [InlineData(9.0f, HostVideoOptions.MaximumRenderScale)] - [InlineData(float.NaN, 1.0f)] - [InlineData(float.PositiveInfinity, 1.0f)] - [InlineData(0.333f, 0.33f)] - public void NormalizeClampsRenderScale(float requested, float expected) - { - Assert.Equal(expected, new HostVideoOptions { RenderScale = requested }.Normalize().RenderScale); - Assert.Equal(expected, HostVideoOptions.NormalizeRenderScale(requested)); - } - - [Fact] - public void RenderScaleDefaultsToNative() - { - Assert.Equal(1.0f, HostVideoOptions.Default.RenderScale); - Assert.Equal(1.0f, HostVideoOptions.Default.Normalize().RenderScale); - } - [Theory] - [InlineData(1920, 1080, GuestDisplayResolution.UltraHd)] - [InlineData(3840, 2160, GuestDisplayResolution.Hd)] - public void GuestModeIsIndependentOfWindowSize(int width, int height, GuestDisplayResolution mode) - { - var options = new HostVideoOptions { Width = width, Height = height, GuestResolution = mode }.Normalize(); - Assert.Equal(mode, options.GuestResolution); - Assert.Equal(GuestDisplayResolution.Hd, HostVideoOptions.Default.GuestResolution); - Span status = stackalloc byte[0x30]; - VideoOutExports.WriteOutputStatus(status, options.GuestResolution, 60, false); - Assert.Equal((int)mode, System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(status)); } } diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs index c2c9fc5..3cb5b8a 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs @@ -414,21 +414,6 @@ public sealed class VideoOutFlipRequestTests : IDisposable Assert.Null(VideoOutExports.GetFlipOutcomeForTests(request)); } - // A CPU flip reserved before its preparation reaches the graphics queue must not hold back an earlier wait. - [Fact] - public void CpuFlip_BlocksBufferWaitsOnlyOnceTheGraphicsQueueReachesIt() - { - Assert.Equal(0, VideoOutExports.TryReserveFlipRequest(_handle, -1, 0, 13, gpuQueued: false, out var request)); - Assert.True(VideoOutExports.IsFlipDone(_handle, -1)); - - VideoOutExports.MarkFlipOrdered(request); - Assert.False(VideoOutExports.IsFlipDone(_handle, -1)); - - VideoOutExports.CompleteFlip(request); - VideoOutExports.MarkFlipPresented(request); - Assert.True(VideoOutExports.IsFlipDone(_handle, -1)); - } - // Update completion counters even if the presenter has already finished the frame. [Fact] public void PresentThenComplete_StillCountsTheFlip() diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs index 34e8d30..cbf2e08 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs @@ -48,7 +48,7 @@ public sealed class VideoOutOutputSupportTests Assert.Equal(expectedHdr, supported); var bytes = Enumerable.Repeat((byte)0xA5, 0x32).ToArray(); - VideoOutExports.WriteOutputStatus(bytes.AsSpan(1, 0x30), GuestDisplayResolution.UltraHd, 120, supported); + VideoOutExports.WriteOutputStatus(bytes.AsSpan(1, 0x30), 3840, 2160, 120, supported); Assert.Equal(0xA5, bytes[0]); Assert.Equal(0xA5, bytes[^1]); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5BarrierMemoryScopeTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5BarrierMemoryScopeTests.cs deleted file mode 100644 index eb1f718..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5BarrierMemoryScopeTests.cs +++ /dev/null @@ -1,149 +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.ShaderCompiler.Tests.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; - -namespace SharpEmu.ShaderCompiler.Tests; - -// What s_barrier publishes is whatever the guest waited for before it. -public sealed class Gen5BarrierMemoryScopeTests -{ - private const ulong ShaderAddress = 0x1_0000_0000; - private const uint Barrier = 0xBF8A0000; - private const uint EndProgram = 0xBF810000; - private const uint WaitLgkmOnly = 0xBF8CC07F; // s_waitcnt lgkmcnt(0) - private const uint WaitVectorMemory = 0xBF8C0F70; // s_waitcnt vmcnt(0) - - private const uint AcquireRelease = 0x8; - private const uint UniformMemory = 0x40; - private const uint WorkgroupMemory = 0x100; - private const uint ImageMemory = 0x800; - - [Theory] - [InlineData(WaitLgkmOnly, AcquireRelease | WorkgroupMemory)] - [InlineData(WaitVectorMemory, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)] - public void BarrierPublishesWhatTheWaitCountWaitedFor(uint wait, uint expected) - { - var program = Decode([wait, Barrier, EndProgram]); - Assert.Equal("SBarrier", program.Instructions[1].Opcode); - - var request = ResourceTestProgram.Request(program, userDataCount: 0); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - Assert.Equal(expected, ControlBarrierSemantics(shader.Spirv)); - } - - private const uint WaitStoresZeroSopk = 0xBBFD0000; // s_waitcnt_vscnt null, 0 - private const uint WaitLgkmZeroSopk = 0xBD7D0000; // s_waitcnt_lgkmcnt null, 0 - - // GFX10 tracks stores with vscnt, and a drained counter stays drained for the block. - [Theory] - [InlineData(new[] { WaitStoresZeroSopk, WaitLgkmOnly }, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)] - [InlineData(new[] { WaitVectorMemory, WaitLgkmOnly }, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)] - [InlineData(new[] { WaitLgkmZeroSopk }, AcquireRelease | WorkgroupMemory)] - public void StoreWaitsCountAndStayCountedAcrossLaterWaits(uint[] waits, uint expected) - { - var program = Decode([.. waits, Barrier, EndProgram]); - var request = ResourceTestProgram.Request(program, userDataCount: 0); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - Assert.Equal(expected, ControlBarrierSemantics(shader.Spirv)); - } - - // A barrier with no wait before it in its block keeps the conservative semantics: the - // publish may have happened in a block the translator cannot order against this one. - [Fact] - public void BarrierWithoutAWaitCountKeepsDeviceMemoryOrdering() - { - var program = Decode([Barrier, EndProgram]); - var request = ResourceTestProgram.Request(program, userDataCount: 0); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - Assert.Equal( - AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory, - ControlBarrierSemantics(shader.Spirv)); - } - - // The value of the memory-semantics operand of the module's single OpControlBarrier. - private static uint ControlBarrierSemantics(byte[] spirv) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - var constants = new Dictionary(); - uint? semanticsId = null; - for (var offset = 5; offset < words.Length;) - { - var wordCount = (int)(words[offset] >> 16); - var opcode = (ushort)(words[offset] & 0xFFFF); - if (opcode == (ushort)SpirvOp.Constant && wordCount == 4) - { - constants[words[offset + 2]] = words[offset + 3]; - } - else if (opcode == (ushort)SpirvOp.ControlBarrier) - { - Assert.Null(semanticsId); - semanticsId = words[offset + 3]; - } - - offset += wordCount; - } - - Assert.NotNull(semanticsId); - return constants[semanticsId.Value]; - } - - private static Gen5ShaderProgram Decode(IReadOnlyList words) - { - var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint)); - var bytes = new byte[words.Count * sizeof(uint)]; - for (var index = 0; index < words.Count; index++) - { - BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]); - } - - Assert.True(memory.TryWrite(ShaderAddress, bytes)); - var context = new CpuContext(memory, Generation.Gen5); - Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ShaderAddress, out var program, out var error), error); - return program; - } - - private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory - { - private readonly byte[] _storage = new byte[size]; - - public bool TryRead(ulong virtualAddress, Span destination) - { - if (!TryResolve(virtualAddress, destination.Length, out var offset)) - { - return false; - } - - _storage.AsSpan(offset, destination.Length).CopyTo(destination); - return true; - } - - public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) - { - if (!TryResolve(virtualAddress, source.Length, out var offset)) - { - return false; - } - - source.CopyTo(_storage.AsSpan(offset, source.Length)); - return true; - } - - private bool TryResolve(ulong address, int length, out int offset) - { - offset = 0; - if (address < baseAddress || address - baseAddress > int.MaxValue) - { - return false; - } - - offset = (int)(address - baseAddress); - return offset <= _storage.Length - length; - } - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecRunTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecRunTests.cs deleted file mode 100644 index e6e58c8..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecRunTests.cs +++ /dev/null @@ -1,201 +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.ShaderCompiler.Ir; -using SharpEmu.ShaderCompiler.Tests.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; - -namespace SharpEmu.ShaderCompiler.Tests; - -// A run of plain vector ALU writes may share one EXEC test. Anything the wave runs even where this -// lane is masked off has to end the run. -public sealed class Gen5ExecRunTests -{ - private const ulong ShaderAddress = 0x1_0000_0000; - private const uint EndProgram = 0xBF810000; - private const uint MultiplyF32 = 0x10040510; // v_mul_f32 v2, v16, v2 - private const uint CompareExecNeU32 = 0x7DAA0480; // v_cmpx_ne_u32 0, v2 (writes EXEC) - private const uint ConvertF32U32 = 0x7E1C0C10; // v_cvt_f32_u32 v14, s16 - private const uint ScalarMove = 0xBE8E047E; // s_mov_b64 s14, exec - - private static Gen5ShaderInstruction Only(uint word) - { - var program = Decode([word, EndProgram]); - return program.Instructions[0]; - } - - [Fact] - public void PlainVectorArithmeticCanShareAnExecTest() - { - Assert.True(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(Only(MultiplyF32))); - Assert.True(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(Only(ConvertF32U32))); - } - - // v_cmpx writes EXEC and v_cmp writes a mask register; GFX10 keeps that destination in the - // instruction's control, so the destination list alone does not show it. - [Fact] - public void ACompareThatWritesAMaskRegisterEndsTheRun() - { - var compare = Only(CompareExecNeU32); - Assert.Empty(compare.Destinations); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(compare)); - } - - // SALU is not EXEC masked on the hardware and every invocation keeps its own scalar registers. - [Fact] - public void AScalarWriteEndsTheRun() => - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(Only(ScalarMove))); - - // The carry forms write VCC without naming it as a destination. - [Theory] - [InlineData("VAddCoU32")] - [InlineData("VAddCoCiU32")] - [InlineData("VSubCoU32")] - [InlineData("VSubrevCoCiU32")] - public void ACarryWritingAddEndsTheRun(string opcode) - { - var instruction = new Gen5ShaderInstruction( - 0, Gen5ShaderEncoding.Vop2, opcode, [0], [Gen5Operand.Vector(2), Gen5Operand.Vector(3)], - [Gen5Operand.Vector(5)], null); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(instruction)); - } - - // A VOP3 compare names its mask register in the control, not in the destination list. - [Fact] - public void AVop3CompareDestinationInTheControlEndsTheRun() - { - var instruction = new Gen5ShaderInstruction( - 0, Gen5ShaderEncoding.Vop3, "VMadU64U32", [0], [Gen5Operand.Vector(2)], [Gen5Operand.Vector(4)], - new Gen5Vop3Control(0, 0, 0, false, 0, 20)); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(instruction)); - - var plain = instruction with { Control = new Gen5Vop3Control(0, 0, 0, false, 0, null) }; - Assert.True(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(plain)); - } - - [Fact] - public void SharedMemoryEndsTheRun() - { - var instruction = new Gen5ShaderInstruction( - 0, Gen5ShaderEncoding.Ds, "VMovB32", [0], [Gen5Operand.Vector(2)], [Gen5Operand.Vector(1)], - new Gen5DataShareControl(0, 0, false)); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(instruction)); - } - - // Cross-lane reads need the lanes they read from to have run. - [Theory] - [InlineData("VReadlaneB32")] - [InlineData("VWritelaneB32")] - [InlineData("VReadfirstlaneB32")] - [InlineData("VPermlane16B32")] - public void CrossLaneReadsEndTheRun(string opcode) - { - var instruction = new Gen5ShaderInstruction( - 0, Gen5ShaderEncoding.Vop1, opcode, [0], [Gen5Operand.Vector(1)], [Gen5Operand.Vector(2)], null); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(instruction)); - } - - // A neighbour's register is only there if the neighbour ran. - [Fact] - public void TheDppEncodingsEndTheRun() - { - var dpp = new Gen5ShaderInstruction( - 0, Gen5ShaderEncoding.Vop1, "VMovB32", [0], [Gen5Operand.Vector(1)], [Gen5Operand.Vector(2)], - new Gen5Dpp8Control(0, false)); - Assert.False(Gen5ExecRunAnalysis.IsExecMaskedVectorAlu(dpp)); - } - - // A run under an EXEC the translator cannot prove full keeps one test for the whole run: the - // guarded translation selects against the old value of every register it writes instead. - [Fact] - public void ARunUnderAnUnknownExecKeepsOneTestInsteadOfOneSelectPerWrite() - { - var words = new List { CompareExecNeU32 }; - for (var index = 0; index < 16; index++) - { - words.Add(MultiplyF32); - } - - words.Add(EndProgram); - var program = Decode(words); - var request = ResourceTestProgram.Request(program, userDataCount: 0); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - - // One shared test instead of one select per write: the sixteen multiplies contribute no - // selects of their own, so the module holds far fewer than sixteen. - Assert.True( - CountOpcode(shader.Spirv, SelectOpcode) < 16, - $"the run still selects per write: selects={CountOpcode(shader.Spirv, SelectOpcode)}"); - } - - private const uint SelectOpcode = 169; - - private static int CountOpcode(byte[] spirv, uint opcode) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - var count = 0; - for (var index = 5; index < words.Length;) - { - var length = words[index] >> 16; - if (length == 0) - { - break; - } - - if ((words[index] & 0xFFFF) == opcode) - { - count++; - } - - index += (int)length; - } - - return count; - } - - private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory - { - private readonly byte[] _storage = new byte[size]; - - public bool TryRead(ulong address, Span destination) - { - if (address < baseAddress || address + (ulong)destination.Length > baseAddress + (ulong)size) - { - return false; - } - - _storage.AsSpan((int)(address - baseAddress), destination.Length).CopyTo(destination); - return true; - } - - public bool TryWrite(ulong address, ReadOnlySpan source) - { - if (address < baseAddress || address + (ulong)source.Length > baseAddress + (ulong)size) - { - return false; - } - - source.CopyTo(_storage.AsSpan((int)(address - baseAddress), source.Length)); - return true; - } - } - - private static Gen5ShaderProgram Decode(IReadOnlyList words) - { - var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint)); - var bytes = new byte[words.Count * sizeof(uint)]; - for (var index = 0; index < words.Count; index++) - { - BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]); - } - - Assert.True(memory.TryWrite(ShaderAddress, bytes)); - var context = new CpuContext(memory, Generation.Gen5); - Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ShaderAddress, out var program, out var error), error); - return program; - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs index 8147011..7d4fb4a 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs @@ -138,79 +138,6 @@ public sealed class Gen5Float16ArithmeticTests error); } - // The native GLSL conversions are used only when the host measured them bit-exact on the - // device; otherwise the branchless integer sequences (FindUMsb = GLSL 75) stay in place. - [Theory] - [InlineData(false)] - [InlineData(true)] - public void PackedFloat16UsesNativeHalfConversionOnlyWhenTheHostMeasuredItExact(bool nativeExact) - { - var program = Decode( - [ - (0x33u << 26) | (0x10u << 16) | 1u, - 0xFFu | (258u << 9), - 0x00002C00u, - SEndpgm, - ]); - - Assert.Equal("VPkMulF16", program.Instructions[0].Opcode); - var (plan, resources, layout) = ResourceTestProgram.Prepare(program, SharpEmu.ShaderCompiler.Resources.ShaderStage.Compute, 0, 0); - var request = new ShaderCompileRequest(plan, resources, layout) - { - NativeHalfConversionExact = nativeExact, - }; - - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - var glsl = ReadGlslExtOpcodes(shader.Spirv); - - // 62 = UnpackHalf2x16, 58 = PackHalf2x16, 75 = FindUMsb. - if (nativeExact) - { - Assert.Contains(62u, glsl); - Assert.Contains(58u, glsl); - } - else - { - Assert.DoesNotContain(62u, glsl); - Assert.DoesNotContain(58u, glsl); - Assert.Contains(75u, glsl); - } - } - - // The GLSL.std.450 instruction number of every ExtInst in the module. - private static IReadOnlyList ReadGlslExtOpcodes(byte[] spirv) - { - var opcodes = new List(); - foreach (var words in ReadInstructionWords(spirv)) - { - if ((ushort)words[0] == (ushort)SpirvOp.ExtInst && words.Length >= 5) - { - opcodes.Add(words[4]); - } - } - - return opcodes; - } - - private static IEnumerable ReadInstructionWords(byte[] spirv) - { - Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv)); - for (var offset = 5 * sizeof(uint); offset < spirv.Length;) - { - var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset)); - var wordCount = checked((int)(header >> 16)); - Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint)); - var words = new uint[wordCount]; - for (var index = 0; index < wordCount; index++) - { - words[index] = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + index * sizeof(uint))); - } - - yield return words; - offset += wordCount * sizeof(uint); - } - } - private static Gen5ShaderProgram Decode(IReadOnlyList words) { var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint)); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5PrivateLdsTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5PrivateLdsTests.cs deleted file mode 100644 index 94906c6..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5PrivateLdsTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.ShaderCompiler.Resources; -using SharpEmu.ShaderCompiler.Tests.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; -using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; - -namespace SharpEmu.ShaderCompiler.Tests; - -// Graphics stages model LDS as a per-invocation private array that reads zero where the -// invocation has not written. A written-dword bitmap provides that instead of a zero -// initializer over the whole array. -public sealed class Gen5PrivateLdsTests -{ - private const uint PrivateStorageClass = 6; - private const uint WorkgroupStorageClass = 4; - - [Fact] - public void VertexStageLdsIsLazilyZeroedThroughAWrittenBitmap() - { - var spirv = Compile(ShaderStage.Vertex); - - var module = new SpirvModuleInspector(spirv); - var lds = module.Names.Single(pair => pair.Value == "lds").Key; - var written = module.Names.Single(pair => pair.Value == "ldsWritten").Key; - Assert.Equal(PrivateStorageClass, module.VariableStorageClasses[lds]); - Assert.Equal(PrivateStorageClass, module.VariableStorageClasses[written]); - Assert.False(HasInitializer(spirv, lds)); - Assert.True(HasInitializer(spirv, written)); - ValidateWhenAvailable(spirv); - } - - [Fact] - public void ComputeLdsStaysWorkgroupMemoryWithoutABitmap() - { - var spirv = Compile(ShaderStage.Compute); - - var module = new SpirvModuleInspector(spirv); - var lds = module.Names.Single(pair => pair.Value == "lds").Key; - Assert.Equal(WorkgroupStorageClass, module.VariableStorageClasses[lds]); - Assert.DoesNotContain("ldsWritten", module.Names.Values); - } - - private static byte[] Compile(ShaderStage stage) - { - var program = Program( - Vop2(0, "VLshlrevB32", 3, Operand(2), Gen5Operand.Vector(0)), - DataShare(4, "DsWriteB32", false, [Gen5Operand.Vector(3), Gen5Operand.Vector(0)], []), - DataShare(12, "DsReadB32", false, [Gen5Operand.Vector(3)], [5], 4), - EndProgram(20)); - var request = Request(program, stage); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - return shader.Spirv; - } - - private static void ValidateWhenAvailable(byte[] code) - { - var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); - var executable = string.IsNullOrWhiteSpace(sdk) ? null - : Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val"); - if (executable is null || !File.Exists(executable)) - { - return; - } - - var path = Path.GetTempFileName(); - try - { - File.WriteAllBytes(path, code); - var start = new System.Diagnostics.ProcessStartInfo(executable) { UseShellExecute = false, RedirectStandardError = true, CreateNoWindow = true }; - start.ArgumentList.Add("--target-env"); - start.ArgumentList.Add("vulkan1.2"); - start.ArgumentList.Add(path); - using var process = System.Diagnostics.Process.Start(start)!; - var error = process.StandardError.ReadToEnd(); - process.WaitForExit(); - Assert.True(process.ExitCode == 0, error); - } - finally - { - File.Delete(path); - } - } - - // OpVariable carries an initializer operand after its storage class. - private static bool HasInitializer(byte[] spirv, uint variable) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - for (var offset = 5; offset < words.Length;) - { - var wordCount = (int)(words[offset] >> 16); - if ((SpirvOp)(words[offset] & 0xFFFF) == SpirvOp.Variable && words[offset + 2] == variable) - { - return wordCount > 4; - } - - offset += Math.Max(wordCount, 1); - } - - throw new InvalidOperationException($"Variable {variable} is not declared."); - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5RenderScaleTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5RenderScaleTests.cs deleted file mode 100644 index 75c9156..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5RenderScaleTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using System.Buffers.Binary; -using System.Diagnostics; -using SharpEmu.ShaderCompiler.Resources; -using SharpEmu.ShaderCompiler.Tests.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; - -namespace SharpEmu.ShaderCompiler.Tests; - -// Internal resolution scaling: the module has to translate the pixel position, integer -// image coordinates and size queries between guest pixels and host texels. -public sealed class Gen5RenderScaleTests -{ - private const uint PositionXBit = 1u << 8; - private const uint PositionYBit = 1u << 9; - private const uint PositionZBit = 1u << 10; - - [Fact] - public void RenderScaleDwordsFollowTheDispatchLimits() - { - var info = new ShaderResourceInfo(); - var plain = BindingLayout.Allocate(info, [0u], false, false, false); - var scaled = BindingLayout.Allocate(info, [0u], false, false, false, 0, false, usesRenderScale: true); - var both = BindingLayout.Allocate(info, [0u], false, false, false, 0, true, usesRenderScale: true); - - Assert.False(plain.UsesRenderScale); - Assert.True(scaled.UsesRenderScale); - Assert.Equal(plain.ShaderDataDwordCount + BindingLayout.RenderScaleDwordCount, scaled.ShaderDataDwordCount); - Assert.Equal(plain.ShaderDataDwordCount, scaled.RenderScaleDword); - Assert.Equal(plain.ShaderDataDwordCount + 3, both.RenderScaleDword); - Assert.NotEqual(plain, scaled); - - // The validator recomputes the layout from the flag, so declarations never drift. - BindingLayoutValidator.Validate(plain, info, [0u], false, false, false, 0, ShaderStage.Pixel); - BindingLayoutValidator.Validate(scaled, info, [0u], false, false, false, 0, ShaderStage.Pixel); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void PixelPositionIsDividedByTheAttachmentFactor(bool usesRenderScale) - { - var spirv = CompilePixelPosition(usesRenderScale); - var instructions = Parse(spirv); - var fragCoord = Load(instructions, FragCoordVariable(instructions)); - - // Only the two scaled axes move; depth and 1/w keep their values. - Assert.Equal(usesRenderScale, FeedsOperation(instructions, fragCoord, 0, SpirvOp.FMul)); - Assert.Equal(usesRenderScale, FeedsOperation(instructions, fragCoord, 1, SpirvOp.FMul)); - Assert.False(FeedsOperation(instructions, fragCoord, 2, SpirvOp.FMul)); - ValidateWhenAvailable(spirv); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IntegerImageCoordinatesAreMappedOntoHostTexels(bool usesRenderScale) - { - var spirv = CompileImageAccess("ImageLoad", usesRenderScale); - var instructions = Parse(spirv); - var fetch = Assert.Single(instructions, item => item.Opcode == SpirvOp.ImageFetch); - var coordinates = Definition(instructions, fetch.Operands[3]); - - // Two coordinates, each either the raw register or that register mapped through - // the image's factor; a selected factor of one leaves an unscaled image alone. - Assert.Equal(SpirvOp.CompositeConstruct, coordinates.Opcode); - foreach (var component in coordinates.Operands.Skip(2)) - { - Assert.Equal(usesRenderScale ? SpirvOp.ConvertFToS : SpirvOp.Bitcast, Definition(instructions, component).Opcode); - } - - if (usesRenderScale) - { - // The factor is selected per image, so an unscaled binding costs only the select. - Assert.Contains(instructions, item => item.Opcode == SpirvOp.Select); - Assert.Contains(instructions, item => item.Opcode == SpirvOp.ConvertSToF); - } - else - { - Assert.DoesNotContain(instructions, item => item.Opcode == SpirvOp.ConvertSToF); - } - ValidateWhenAvailable(spirv); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SizeQueriesReportGuestSizes(bool usesRenderScale) - { - var spirv = CompileImageAccess("ImageGetResinfo", usesRenderScale); - var instructions = Parse(spirv); - var query = Assert.Single(instructions, item => item.Opcode == SpirvOp.ImageQuerySizeLod); - var extracts = instructions - .Where(item => item.Opcode == SpirvOp.CompositeExtract && item.Operands[2] == query.Operands[1]) - .ToArray(); - - Assert.NotEmpty(extracts); - foreach (var extract in extracts) - { - Assert.Equal(usesRenderScale, FeedsOpcode(instructions, extract.Operands[1], SpirvOp.ConvertSToF)); - } - - ValidateWhenAvailable(spirv); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void DynamicSampleOffsetsRemainInGuestTexels(bool usesRenderScale) - { - var spirv = CompileImageAccess("ImageSampleLzO", usesRenderScale); - var instructions = Parse(spirv); - var divide = Assert.Single(instructions, item => item.Opcode == SpirvOp.FDiv); - var offset = Definition(instructions, divide.Operands[2]); - Assert.Equal(usesRenderScale ? SpirvOp.CompositeConstruct : SpirvOp.ConvertSToF, offset.Opcode); - if (usesRenderScale) - { - foreach (var component in offset.Operands.Skip(2)) - { - var multiply = Definition(instructions, component); - Assert.Equal(SpirvOp.FMul, multiply.Opcode); - // Each image selects its factor; unscaled bindings select one. - Assert.Equal(SpirvOp.Select, Definition(instructions, multiply.Operands[3]).Opcode); - } - } - ValidateWhenAvailable(spirv); - } - - private static byte[] CompilePixelPosition(bool usesRenderScale) - { - var program = ResourceTestProgram.Program( - ResourceTestProgram.MoveVector(0, 0, 0), - ResourceTestProgram.EndProgram(4)); - var request = BuildRequest(program, ShaderStage.Pixel, usesRenderScale, - PositionXBit | PositionYBit | PositionZBit); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - return shader.Spirv; - } - - private static byte[] CompileImageAccess(string opcode, bool usesRenderScale) - { - var program = ResourceTestProgram.Program( - ResourceTestProgram.Image(0, opcode, resourceRegister: 8, dimension: 1, dmask: 0x3), - ResourceTestProgram.EndProgram(8)); - var request = BuildRequest(program, ShaderStage.Compute, usesRenderScale); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - return shader.Spirv; - } - - private static ShaderCompileRequest BuildRequest(Gen5ShaderProgram program, ShaderStage stage, bool usesRenderScale, - uint pixelInputs = 0) - { - var plan = ShaderResourcePlan.Extract(program, stage, ResourceTestProgram.Hash, 0, 64); - var resources = ResourceMaterializer.ApplyTo(plan, ResourceSpecialization.Default(plan.Info)); - var layout = BindingLayout.Allocate( - resources.Info, - BindingLayout.CollectUserDataRegisters(program, 0, 64), - BindingLayout.UsesGlobalDataShare(program), - ShaderCompileRequest.RequiresFlattenedTable(plan, resources), - BindingLayout.ReadsShaderBase(program), - 0, - false, - usesRenderScale); - return new ShaderCompileRequest(plan, resources, layout) - { - PixelOutputs = stage == ShaderStage.Pixel ? [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)] : [], - PixelInputAddress = pixelInputs, - PixelInputEnable = pixelInputs, - }; - } - - private static uint FragCoordVariable(IReadOnlyList instructions) => Assert.Single(instructions, - item => item.Opcode == SpirvOp.Decorate && item.Operands.Length >= 3 && - item.Operands[1] == (uint)SpirvDecoration.BuiltIn && item.Operands[2] == (uint)SpirvBuiltIn.FragCoord).Operands[0]; - - private static uint Load(IReadOnlyList instructions, uint variable) => - Assert.Single(instructions, item => item.Opcode == SpirvOp.Load && item.Operands[2] == variable).Operands[1]; - - // True when the given component of the composite is extracted and consumed by the opcode. - private static bool FeedsOperation(IReadOnlyList instructions, uint composite, uint component, SpirvOp opcode) => - instructions.Any(item => item.Opcode == SpirvOp.CompositeExtract && item.Operands[2] == composite && - item.Operands.Length >= 4 && item.Operands[3] == component && - FeedsOpcode(instructions, item.Operands[1], opcode)); - - private static bool FeedsOpcode(IReadOnlyList instructions, uint value, SpirvOp opcode) => - instructions.Any(item => item.Opcode == opcode && item.Operands.Skip(2).Contains(value)); - - // The producer of a value, among the opcodes these tests expect to see one of. - private static Parsed Definition(IReadOnlyList instructions, uint id) => - Assert.Single(instructions, item => ValueOpcodes.Contains(item.Opcode) && item.Operands.Length >= 2 && item.Operands[1] == id); - - private static readonly SpirvOp[] ValueOpcodes = - [ - SpirvOp.CompositeConstruct, SpirvOp.CompositeExtract, SpirvOp.Bitcast, SpirvOp.Load, - SpirvOp.ConvertFToS, SpirvOp.ConvertSToF, SpirvOp.FMul, SpirvOp.FAdd, SpirvOp.Select, - ]; - - private readonly record struct Parsed(SpirvOp Opcode, uint[] Operands); - - private static IReadOnlyList Parse(byte[] spirv) - { - var instructions = new List(); - for (var offset = 5 * sizeof(uint); offset < spirv.Length;) - { - var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset)); - var wordCount = checked((int)(header >> 16)); - Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint)); - var operands = new uint[wordCount - 1]; - for (var operand = 0; operand < operands.Length; operand++) - { - operands[operand] = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + (operand + 1) * sizeof(uint))); - } - - instructions.Add(new Parsed((SpirvOp)(ushort)header, operands)); - offset += wordCount * sizeof(uint); - } - - return instructions; - } - - private static void ValidateWhenAvailable(byte[] spirv) - { - var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); - if (string.IsNullOrEmpty(sdk)) - { - return; - } - - var validator = Path.Combine(sdk, "bin", "spirv-val"); - if (!File.Exists(validator)) - { - return; - } - - var path = Path.GetTempFileName(); - try - { - File.WriteAllBytes(path, spirv); - using var process = Process.Start(new ProcessStartInfo(validator, ["--target-env", "vulkan1.2", path]) - { - RedirectStandardError = true, - })!; - var errors = process.StandardError.ReadToEnd(); - process.WaitForExit(); - Assert.True(process.ExitCode == 0, errors); - } - finally - { - File.Delete(path); - } - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs index 6908d81..1e4bd1e 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs @@ -322,56 +322,6 @@ public sealed class Gen5ScalarLaneTransferTests Assert.Equal(expectSlot, text.Contains("v86_lane3", StringComparison.Ordinal)); } - [Theory] - [InlineData(false, true)] - [InlineData(true, false)] - public void EmulatedWave64KeepsPureScalarSpillsWithoutAnExchange(bool vectorWrite, bool expectSlot) - { - // Wave64 on a 32-lane host reads another lane through LDS and a workgroup barrier. A VGPR - // that only V_WRITELANE writes holds wave-uniform spilled scalars, so its constant-lane - // reads need no exchange; once an ordinary instruction writes it, the exchange returns. - const ulong shaderAddress = 0x1000; - var memory = new TestCpuMemory(shaderAddress, 0x100); - var words = new List - { - 0xD7610056, 0x0001047E, // v_writelane_b32 v86, s126, 2 - 0xD7610056, 0x0001067F, // v_writelane_b32 v86, s127, 3 - }; - if (vectorWrite) - { - words.Add(0x7EAC0280); // v_mov_b32 v86, 0 - } - - words.AddRange( - [ - 0xD7600000, 0x00010556, // v_readlane_b32 s0, v86, 2 - 0xD7600001, 0x00010756, // v_readlane_b32 s1, v86, 3 - 0xBF810000, // s_endpgm - ]); - var shader = new byte[words.Count * sizeof(uint)]; - for (var index = 0; index < words.Count; index++) - { - BinaryPrimitives.WriteUInt32LittleEndian(shader.AsSpan(index * sizeof(uint)), words[index]); - } - - Assert.True(memory.TryWrite(shaderAddress, shader)); - var ctx = new CpuContext(memory, Generation.Gen5); - Assert.True(Gen5ShaderTranslator.TryDecodeProgram(ctx, shaderAddress, out var program, out var decodeError), decodeError); - var (plan, resources, layout) = ResourceTestProgram.Prepare(program); - var request = new ShaderCompileRequest(plan, resources, layout) - { - LocalSizeX = 64, - ThreadCountX = 64, - WaveSize = 64, - }; - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var compileError), compileError); - - var text = System.Text.Encoding.ASCII.GetString(compiled.Spirv); - Assert.Equal(expectSlot, text.Contains("v86_lane2", StringComparison.Ordinal)); - Assert.Equal(expectSlot, text.Contains("v86_lane3", StringComparison.Ordinal)); - Assert.Equal(!expectSlot, ReadSpirvOpcodes(compiled.Spirv).Contains((ushort)SpirvOp.ControlBarrier)); - } - private static Gen5ShaderInstruction ScalarInstruction( uint pc, Gen5ShaderEncoding encoding, diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5WaveMaskLaneFlagTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5WaveMaskLaneFlagTests.cs deleted file mode 100644 index 88295cc..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5WaveMaskLaneFlagTests.cs +++ /dev/null @@ -1,126 +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.ShaderCompiler; -using SharpEmu.ShaderCompiler.Resources; -using SharpEmu.ShaderCompiler.Tests.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; - -namespace SharpEmu.ShaderCompiler.Tests; - -// VCC and EXEC carry a per-lane boolean that every write to their registers refreshes. -public sealed class Gen5WaveMaskLaneFlagTests -{ - private const ulong ShaderAddress = 0x1_0000_0000; - - [Fact] - public void EmulatedWave64TestsTheLaneBitWithoutComposingTheMask() - { - var program = Decode( - [ - 0x7C020300, // v_cmp_lt_f32 vcc, v0, v1 (declares the lane input and writes a mask) - 0xBEFE0300, // s_mov_b32 exec_lo, s0 - 0xBF810000, // s_endpgm - ]); - Assert.Equal("VCmpLtF32", program.Instructions[0].Opcode); - - var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Compute, 0, 0, 0); - // A 64-thread group is the wave64-on-32-lanes case the translator emulates. - var request = new ShaderCompileRequest(plan, resources, layout) - { - WaveSize = 64, - LocalSizeX = 8, - LocalSizeY = 8, - LocalSizeZ = 1, - }; - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - - // The lane's bit lives in one of the two mask registers, so the refresh selects that - // register and shifts inside it. Composing the 64-bit mask and masking it with a 64-bit - // lane bit left 27 of these for this two-instruction program; the remaining six build - // the mask value itself, which stays 64-bit because the guest register pair is. - Assert.Equal(6, Count64BitBitwiseAnd(shader.Spirv)); - } - - private static int Count64BitBitwiseAnd(byte[] spirv) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - var wideTypes = new HashSet(); - var count = 0; - for (var offset = 5; offset < words.Length;) - { - var wordCount = (int)(words[offset] >> 16); - var opcode = (ushort)(words[offset] & 0xFFFF); - if (opcode == (ushort)SpirvOp.TypeInt && words[offset + 2] == 64) - { - wideTypes.Add(words[offset + 1]); - } - else if (opcode == (ushort)SpirvOp.BitwiseAnd && wideTypes.Contains(words[offset + 1])) - { - count++; - } - - offset += wordCount; - } - - return count; - } - - private static Gen5ShaderProgram Decode(IReadOnlyList words) - { - var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint)); - var bytes = new byte[words.Count * sizeof(uint)]; - for (var index = 0; index < words.Count; index++) - { - BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]); - } - - Assert.True(memory.TryWrite(ShaderAddress, bytes)); - var context = new CpuContext(memory, Generation.Gen5); - Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ShaderAddress, out var program, out var error), error); - return program; - } - - private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory - { - private readonly byte[] _storage = new byte[size]; - - public bool TryRead(ulong virtualAddress, Span destination) - { - if (!TryResolve(virtualAddress, destination.Length, out var offset)) - { - return false; - } - - _storage.AsSpan(offset, destination.Length).CopyTo(destination); - return true; - } - - public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) - { - if (!TryResolve(virtualAddress, source.Length, out var offset)) - { - return false; - } - - source.CopyTo(_storage.AsSpan(offset, source.Length)); - return true; - } - - private bool TryResolve(ulong address, int length, out int offset) - { - offset = 0; - if (address < baseAddress || address - baseAddress > int.MaxValue) - { - return false; - } - - offset = (int)(address - baseAddress); - return offset <= _storage.Length - length; - } - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferBoundsCheckTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferBoundsCheckTests.cs deleted file mode 100644 index 03e7c03..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferBoundsCheckTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; -using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; - -namespace SharpEmu.ShaderCompiler.Tests.Resources; - -// A guest buffer word is bounds-checked in the shader unless the host measured that the device -// returns zero past the end of a descriptor range itself. -public sealed class BufferBoundsCheckTests -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public void BufferWordsAreCheckedUnlessTheDeviceZeroesOutOfRangeReads(bool deviceZeroes) - { - var program = Program(ScalarBufferLoad(0, 4, 20, count: 4), EndProgram(8)); - var (plan, resources, layout) = Prepare(program, SharpEmu.ShaderCompiler.Resources.ShaderStage.Compute, 0, 8); - var request = new ShaderCompileRequest(plan, resources, layout) - { - ZeroOutOfBoundsBufferReads = deviceZeroes, - }; - - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - - // One OpArrayLength is one range test. Without the device promise every word carries one. - var checks = CountOpcode(shader.Spirv, SpirvOp.ArrayLength); - if (deviceZeroes) - { - Assert.Equal(0, checks); - } - else - { - Assert.True(checks >= 4, $"expected a range test per word, found {checks}"); - } - } - - private static int CountOpcode(byte[] spirv, SpirvOp opcode) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - var count = 0; - for (var offset = 5; offset < words.Length;) - { - var wordCount = (int)(words[offset] >> 16); - if ((ushort)(words[offset] & 0xFFFF) == (ushort)opcode) - { - count++; - } - - offset += wordCount; - } - - return count; - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferFormatLoadTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferFormatLoadTests.cs deleted file mode 100644 index 436f830..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferFormatLoadTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using System.Diagnostics; -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; -using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; - -namespace SharpEmu.ShaderCompiler.Tests.Resources; - -// A formatted untyped load decodes the descriptor format at run time. Dword-aligned 32-bit -// raw-bit formats take a word-per-component branch; every other format the byte-wise decode. -public sealed class BufferFormatLoadTests -{ - [Theory] - [InlineData(1u)] - [InlineData(3u)] - [InlineData(4u)] - public void FormattedLoadBranchesBetweenTheWordAndByteDecodes(uint dwords) - { - var request = Request(Program(BufferLoad(0, 4, dwords: dwords, formatted: true), EndProgram(8))); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - - var module = new SpirvModuleInspector(shader.Spirv); - // The word branch and the byte-wise branch join at one selection merge. - Assert.Contains((ushort)SpirvOp.SelectionMerge, module.Opcodes); - Assert.Contains((ushort)SpirvOp.BitFieldUExtract, module.Opcodes); - ValidateWhenAvailable(shader.Spirv); - } - - private static void ValidateWhenAvailable(byte[] code) - { - var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); - var executable = string.IsNullOrWhiteSpace(sdk) ? null - : Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val"); - if (executable is null || !File.Exists(executable)) - { - return; - } - - var path = Path.GetTempFileName(); - try - { - File.WriteAllBytes(path, code); - var start = new ProcessStartInfo(executable) { UseShellExecute = false, RedirectStandardError = true, CreateNoWindow = true }; - start.ArgumentList.Add("--target-env"); - start.ArgumentList.Add("vulkan1.2"); - start.ArgumentList.Add(path); - using var process = Process.Start(start)!; - var error = process.StandardError.ReadToEnd(); - process.WaitForExit(); - Assert.True(process.ExitCode == 0, error); - } - finally - { - File.Delete(path); - } - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferUnalignedWordTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferUnalignedWordTests.cs deleted file mode 100644 index aed5eb6..0000000 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BufferUnalignedWordTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.ShaderCompiler.Vulkan; -using Xunit; -using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; - -namespace SharpEmu.ShaderCompiler.Tests.Resources; - -// The byte-wise decode of a formatted buffer load reads the four bytes of a component out of -// the two words they span, not out of four separately bounds-checked words. -public sealed class BufferUnalignedWordTests -{ - [Fact] - public void ByteWiseComponentDecodeReadsTwoBoundsCheckedWords() - { - var request = Request(Program(BufferLoad(0, 4, dwords: 1, formatted: true), EndProgram(8))); - Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); - - // One bounds-checked buffer word load is one OpArrayLength. A formatted load decodes - // four components whatever the opcode's width; the byte-wise branch now reads two words - // per component instead of four, which is eight of these instead of sixteen. - Assert.Equal(24, CountOpcode(shader.Spirv, SpirvOp.ArrayLength)); - } - - private static int CountOpcode(byte[] spirv, SpirvOp opcode) - { - var words = new uint[spirv.Length / 4]; - Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length); - var count = 0; - for (var offset = 5; offset < words.Length;) - { - var wordCount = (int)(words[offset] >> 16); - if ((ushort)(words[offset] & 0xFFFF) == (ushort)opcode) - { - count++; - } - - offset += wordCount; - } - - return count; - } -} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs index a348123..e1f7354 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs @@ -1,312 +1,279 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using SharpEmu.ShaderCompiler.Resources; -using Xunit; -using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; - -namespace SharpEmu.ShaderCompiler.Tests.Resources; - -public sealed class ResourceMaterializationCacheTests -{ - private const ulong HeapBase = 0x1000; - - // A mask word, a two-entry index table and six 32-byte image records. - private sealed class Heap - { - public readonly Dictionary Words = new(); - public int Reads; - public bool GpuOwned; - - public Heap() - { - Words[HeapBase + 0x80] = (1u << 1) | (1u << 4); - Words[HeapBase + 0x40 + 0x90] = 2; - Words[HeapBase + 0x40 + 4 * 0x90] = 5; - foreach (var record in new uint[] { 2, 5 }) - { - var entry = HeapBase + 0x100 + record * 32; - for (uint dword = 0; dword < 8; dword++) - Words[entry + dword * 4] = dword switch - { - 0 => (record & 1) == 0 ? 0x2000u : 0x1000u, - 1 => 20u << 20, - 3 => 0xFACu | (9u << 28), - _ => 0, - }; - } - } - - public bool Read(ulong address, out uint word) - { - Reads++; - return Words.TryGetValue(address, out word); - } - - public bool ReadResident(ulong address, Span destination, bool clean) - { - if (GpuOwned) - return false; - for (var offset = 0; offset < destination.Length; offset += 4) - { - if (!Words.TryGetValue(address + (ulong)offset, out var word)) - return false; - BitConverter.TryWriteBytes(destination[offset..], word); - } - - return true; - } - } - - private static ShaderResourcePlan Plan() => - ShaderResourcePlan.Extract(DirectImageTableTests.CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2); - - private static bool Run(ResourceMaterializationCache cache, ShaderResourcePlan plan, Heap heap, uint[] userData, - out ResourceSnapshot snapshot, out ResourceSpecialization specialization, ulong shaderBase = 0) - { - snapshot = new ResourceSnapshot(); - specialization = new ResourceSpecialization(); - return cache.Materialize(plan, Inputs(userData, readCleanMemory: heap.Read, shaderBase: shaderBase), heap.ReadResident, - ref snapshot, ref specialization, out _); - } - - [Fact] - public void UnchangedMemoryReusesTheMaterialization() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out var firstSpecialization)); - var readsAfterFirst = heap.Reads; - Assert.True(readsAfterFirst > 0); - - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out var secondSpecialization)); - Assert.Equal(readsAfterFirst, heap.Reads); - Assert.Same(first, second); - Assert.Same(firstSpecialization, secondSpecialization); - Assert.Equal((1, 1), (cache.Hits, cache.Misses)); - } - - [Fact] - public void APlanWhoseInputsNeverRepeatStopsRecordingButKeepsItsResults() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - for (var draw = 0u; draw < 200; draw++) - { - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var snapshot, out _, shaderBase: 0x10000 + draw * 0x100)); - var reference = new ResourceSnapshot(); - var referenceSpecialization = new ResourceSpecialization(); - Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read, shaderBase: 0x10000 + draw * 0x100), - ref reference, ref referenceSpecialization)); - Assert.Equal(reference.Images.Select(image => image.ToArray()), snapshot.Images.Select(image => image.ToArray())); - } - - Assert.True(cache.Bypassed > 100); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void APlanThatKeepsHittingKeepsTheCache() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - for (var draw = 0; draw < 200; draw++) - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - - Assert.Equal(0, cache.Bypassed); - Assert.Equal(199, cache.Hits); - } - - [Fact] - public void AChangedDescriptorWordMaterializesAgain() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - - heap.Words[HeapBase + 0x100 + 5 * 32] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); - Assert.NotSame(first, second); - Assert.Equal((0, 2), (cache.Hits, cache.Misses)); - - // An uncached full walk gives the same result as the re-materialization. - var reference = new ResourceSnapshot(); - var referenceSpecialization = new ResourceSpecialization(); - Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read), - ref reference, ref referenceSpecialization)); - Assert.Equal(reference.Images.Select(image => image.ToArray()), second.Images.Select(image => image.ToArray())); - } - - [Fact] - public void AlternatingDescriptorsReuseEveryRecentVariant() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - var word = HeapBase + 0x100 + 5 * 32; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - heap.Words[word] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); - Assert.Equal((0, 2), (cache.Hits, cache.Misses)); - - heap.Words[word] = 0x1000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var firstAgain, out _)); - heap.Words[word] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var secondAgain, out _)); - Assert.Same(first, firstAgain); - Assert.Same(second, secondAgain); - Assert.Equal((2, 2), (cache.Hits, cache.Misses)); - } - - [Fact] - public void AChangedMaskMaterializesAgain() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - heap.Words[HeapBase + 0x80] = 1u << 1; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void GpuOwnedMemoryIsNeverTrusted() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - heap.GpuOwned = true; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void ADifferentDrawKeyIsADifferentEntry() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _, shaderBase: 0x100)); - Assert.NotSame(first, second); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var third, out _)); - Assert.Same(first, third); - Assert.Equal((1, 2), (cache.Hits, cache.Misses)); - } - - [Fact] - public void AFailedReadIsNotCached() - { - var plan = Plan(); - var heap = new Heap(); - heap.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); - var cache = new ResourceMaterializationCache(); - var ok = Run(cache, plan, heap, [0x1000, 0], out _, out _); - var reads = heap.Reads; - Assert.Equal(ok, Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.True(heap.Reads > reads); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void AFailedRecordingDoesNotPoisonTheNextReader() - { - var plan = Plan(); - var incomplete = new Heap(); - incomplete.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); - var cache = new ResourceMaterializationCache(); - Run(cache, plan, incomplete, [0x1000, 0], out _, out _); - - var complete = new Heap(); - Assert.True(Run(cache, plan, complete, [0x1000, 0], out var first, out _)); - Assert.True(Run(cache, plan, complete, [0x1000, 0], out var second, out _)); - Assert.Same(first, second); - Assert.Equal(1, cache.Hits); - - // Retained entries must not refer to scratch reads reused for another key. - complete.Words[HeapBase + 0x80] = 1u << 1; - Assert.True(Run(cache, plan, complete, [0x1000, 0], out _, out _, shaderBase: 0x100)); - var original = new Heap(); - Assert.True(Run(cache, plan, original, [0x1000, 0], out var restored, out _)); - Assert.Same(first, restored); - } - - // The table pointer in s[0:1]; the rest of the nine user-data registers the program declares. - private static readonly uint[] TableUserData = [0x1000, 0, 0, 0, 0, 0, 0, 0, 0]; - - // Reads the four constants of FlattenedReadReuseTests.RepeatedReadProgram from a word memory. - private static bool RunTable(ResourceMaterializationCache cache, ShaderResourcePlan plan, TestWordMemory memory, - out ResourceSnapshot snapshot) - { - snapshot = new ResourceSnapshot(); - var specialization = new ResourceSpecialization(); - return cache.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), (address, destination, _) => - { - for (var offset = 0; offset < destination.Length; offset += 4) - { - if (!memory.Read(address + (ulong)offset, out var word)) - return false; - BitConverter.TryWriteBytes(destination[offset..], word); - } - - return true; - }, ref snapshot, ref specialization, out _); - } - - [Fact] - public void AChangedTableOnlyWordRefreshesOnlyTheTable() - { - var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); - var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; - var cache = new ResourceMaterializationCache(); - Assert.True(RunTable(cache, plan, memory, out var first)); - Assert.Equal([11u, 22, 33, 44], first.FlattenedResourceTable); - - memory.Words[2] = 99; - Assert.True(RunTable(cache, plan, memory, out var second)); - Assert.Equal((0, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); - Assert.Equal([11u, 22, 99, 44], second.FlattenedResourceTable); - Assert.Same(first.Buffers, second.Buffers); - - // The refreshed table matches an uncached full walk, and the refreshed entry is then a hit. - var reference = new ResourceSnapshot(); - var referenceSpecialization = new ResourceSpecialization(); - Assert.True(ResourceMaterializer.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), ref reference, ref referenceSpecialization)); - Assert.Equal(reference.FlattenedResourceTable, second.FlattenedResourceTable); - Assert.True(RunTable(cache, plan, memory, out var third)); - Assert.Same(second, third); - Assert.Equal((1, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); - } - - [Fact] - public void AnUnreadableTableWordFallsBackToAFullWalk() - { - var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); - var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; - var cache = new ResourceMaterializationCache(); - Assert.True(RunTable(cache, plan, memory, out _)); - memory.Words[1] = 7; - memory.FailAddress = 0x1000 + 3 * 4; - RunTable(cache, plan, memory, out _); - Assert.Equal(0, cache.TableRefreshes); - } - - [Fact] - public void AFullGenerationKeepsRecentEntries() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(generationCapacity: 1); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _, shaderBase: 0x100)); - // The first entry moved to the old generation and is still found. - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(1, cache.Hits); - } -} +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.ShaderCompiler.Resources; +using Xunit; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.ShaderCompiler.Tests.Resources; + +public sealed class ResourceMaterializationCacheTests +{ + private const ulong HeapBase = 0x1000; + + // A mask word, a two-entry index table and six 32-byte image records. + private sealed class Heap + { + public readonly Dictionary Words = new(); + public int Reads; + public bool GpuOwned; + + public Heap() + { + Words[HeapBase + 0x80] = (1u << 1) | (1u << 4); + Words[HeapBase + 0x40 + 0x90] = 2; + Words[HeapBase + 0x40 + 4 * 0x90] = 5; + foreach (var record in new uint[] { 2, 5 }) + { + var entry = HeapBase + 0x100 + record * 32; + for (uint dword = 0; dword < 8; dword++) + Words[entry + dword * 4] = dword switch + { + 0 => (record & 1) == 0 ? 0x2000u : 0x1000u, + 1 => 20u << 20, + 3 => 0xFACu | (9u << 28), + _ => 0, + }; + } + } + + public bool Read(ulong address, out uint word) + { + Reads++; + return Words.TryGetValue(address, out word); + } + + public bool ReadResident(ulong address, Span destination, bool clean) + { + if (GpuOwned) + return false; + for (var offset = 0; offset < destination.Length; offset += 4) + { + if (!Words.TryGetValue(address + (ulong)offset, out var word)) + return false; + BitConverter.TryWriteBytes(destination[offset..], word); + } + + return true; + } + } + + private static ShaderResourcePlan Plan() => + ShaderResourcePlan.Extract(DirectImageTableTests.CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2); + + private static bool Run(ResourceMaterializationCache cache, ShaderResourcePlan plan, Heap heap, uint[] userData, + out ResourceSnapshot snapshot, out ResourceSpecialization specialization, ulong shaderBase = 0) + { + snapshot = new ResourceSnapshot(); + specialization = new ResourceSpecialization(); + return cache.Materialize(plan, Inputs(userData, readCleanMemory: heap.Read, shaderBase: shaderBase), heap.ReadResident, + ref snapshot, ref specialization, out _); + } + + [Fact] + public void UnchangedMemoryReusesTheMaterialization() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out var firstSpecialization)); + var readsAfterFirst = heap.Reads; + Assert.True(readsAfterFirst > 0); + + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out var secondSpecialization)); + Assert.Equal(readsAfterFirst, heap.Reads); + Assert.Same(first, second); + Assert.Same(firstSpecialization, secondSpecialization); + Assert.Equal((1, 1), (cache.Hits, cache.Misses)); + } + + [Fact] + public void AChangedDescriptorWordMaterializesAgain() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + + heap.Words[HeapBase + 0x100 + 5 * 32] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); + Assert.NotSame(first, second); + Assert.Equal((0, 2), (cache.Hits, cache.Misses)); + + // An uncached full walk gives the same result as the re-materialization. + var reference = new ResourceSnapshot(); + var referenceSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read), + ref reference, ref referenceSpecialization)); + Assert.Equal(reference.Images.Select(image => image.ToArray()), second.Images.Select(image => image.ToArray())); + } + + [Fact] + public void AlternatingDescriptorsReuseEveryRecentVariant() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + var word = HeapBase + 0x100 + 5 * 32; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + heap.Words[word] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); + Assert.Equal((0, 2), (cache.Hits, cache.Misses)); + + heap.Words[word] = 0x1000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var firstAgain, out _)); + heap.Words[word] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var secondAgain, out _)); + Assert.Same(first, firstAgain); + Assert.Same(second, secondAgain); + Assert.Equal((2, 2), (cache.Hits, cache.Misses)); + } + + [Fact] + public void AChangedMaskMaterializesAgain() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + heap.Words[HeapBase + 0x80] = 1u << 1; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void GpuOwnedMemoryIsNeverTrusted() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + heap.GpuOwned = true; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void ADifferentDrawKeyIsADifferentEntry() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _, shaderBase: 0x100)); + Assert.NotSame(first, second); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var third, out _)); + Assert.Same(first, third); + Assert.Equal((1, 2), (cache.Hits, cache.Misses)); + } + + [Fact] + public void AFailedReadIsNotCached() + { + var plan = Plan(); + var heap = new Heap(); + heap.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); + var cache = new ResourceMaterializationCache(); + var ok = Run(cache, plan, heap, [0x1000, 0], out _, out _); + var reads = heap.Reads; + Assert.Equal(ok, Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.True(heap.Reads > reads); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void AFailedRecordingDoesNotPoisonTheNextReader() + { + var plan = Plan(); + var incomplete = new Heap(); + incomplete.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); + var cache = new ResourceMaterializationCache(); + Run(cache, plan, incomplete, [0x1000, 0], out _, out _); + + var complete = new Heap(); + Assert.True(Run(cache, plan, complete, [0x1000, 0], out var first, out _)); + Assert.True(Run(cache, plan, complete, [0x1000, 0], out var second, out _)); + Assert.Same(first, second); + Assert.Equal(1, cache.Hits); + + // Retained entries must not refer to scratch reads reused for another key. + complete.Words[HeapBase + 0x80] = 1u << 1; + Assert.True(Run(cache, plan, complete, [0x1000, 0], out _, out _, shaderBase: 0x100)); + var original = new Heap(); + Assert.True(Run(cache, plan, original, [0x1000, 0], out var restored, out _)); + Assert.Same(first, restored); + } + + // The table pointer in s[0:1]; the rest of the nine user-data registers the program declares. + private static readonly uint[] TableUserData = [0x1000, 0, 0, 0, 0, 0, 0, 0, 0]; + + // Reads the four constants of FlattenedReadReuseTests.RepeatedReadProgram from a word memory. + private static bool RunTable(ResourceMaterializationCache cache, ShaderResourcePlan plan, TestWordMemory memory, + out ResourceSnapshot snapshot) + { + snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + return cache.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), (address, destination, _) => + { + for (var offset = 0; offset < destination.Length; offset += 4) + { + if (!memory.Read(address + (ulong)offset, out var word)) + return false; + BitConverter.TryWriteBytes(destination[offset..], word); + } + + return true; + }, ref snapshot, ref specialization, out _); + } + + [Fact] + public void AChangedTableOnlyWordRefreshesOnlyTheTable() + { + var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); + var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; + var cache = new ResourceMaterializationCache(); + Assert.True(RunTable(cache, plan, memory, out var first)); + Assert.Equal([11u, 22, 33, 44], first.FlattenedResourceTable); + + memory.Words[2] = 99; + Assert.True(RunTable(cache, plan, memory, out var second)); + Assert.Equal((0, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); + Assert.Equal([11u, 22, 99, 44], second.FlattenedResourceTable); + Assert.Same(first.Buffers, second.Buffers); + + // The refreshed table matches an uncached full walk, and the refreshed entry is then a hit. + var reference = new ResourceSnapshot(); + var referenceSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), ref reference, ref referenceSpecialization)); + Assert.Equal(reference.FlattenedResourceTable, second.FlattenedResourceTable); + Assert.True(RunTable(cache, plan, memory, out var third)); + Assert.Same(second, third); + Assert.Equal((1, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); + } + + [Fact] + public void AnUnreadableTableWordFallsBackToAFullWalk() + { + var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); + var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; + var cache = new ResourceMaterializationCache(); + Assert.True(RunTable(cache, plan, memory, out _)); + memory.Words[1] = 7; + memory.FailAddress = 0x1000 + 3 * 4; + RunTable(cache, plan, memory, out _); + Assert.Equal(0, cache.TableRefreshes); + } + + [Fact] + public void AFullGenerationKeepsRecentEntries() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(generationCapacity: 1); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _, shaderBase: 0x100)); + // The first entry moved to the old generation and is still found. + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(1, cache.Hits); + } +}